From 71cd2ce5bf6db3da741cb832c5d37a1419fd47ff Mon Sep 17 00:00:00 2001 From: grokas Date: Fri, 24 Apr 2026 12:10:27 -0700 Subject: [PATCH 01/10] feat!: sdkify splunk BREAKING CHANGE: Converted Splunk app to SDK --- .github/workflows/call-publish.yml | 15 - .gitignore | 3 + .pre-commit-config.yaml | 87 - CONTRIBUTING.md | 3 - LICENSE | 201 --- NOTICE | 12 - README.md | 611 ------- Splunk/manifest.json | 1532 ++++++++++++++++ __init__.py | 14 - manual_readme_content.md | 289 --- pyproject.toml | 136 +- release_notes/1.2.15.md | 7 - release_notes/1.2.18.md | 7 - release_notes/1.3.16.md | 6 - release_notes/1.3.19.md | 6 - release_notes/1.3.23.md | 10 - release_notes/1.3.41.md | 11 - release_notes/1.3.7.md | 8 - release_notes/2.0.22.md | 12 - release_notes/2.0.34.md | 7 - release_notes/2.1.3.md | 7 - release_notes/2.1.6.md | 6 - release_notes/2.10.0.md | 1 - release_notes/2.11.0.md | 3 - release_notes/2.11.1.md | 2 - release_notes/2.12.0.md | 2 - release_notes/2.13.0.md | 1 - release_notes/2.14.0.md | 1 - release_notes/2.15.0.md | 2 - release_notes/2.15.1.md | 1 - release_notes/2.16.0.md | 3 - release_notes/2.16.1.md | 1 - release_notes/2.16.2.md | 1 - release_notes/2.17.0.md | 1 - release_notes/2.18.0.md | 1 - release_notes/2.2.3.md | 10 - release_notes/2.20.0.md | 2 - release_notes/2.20.1.md | 5 - release_notes/2.20.2.md | 2 - release_notes/2.20.3.md | 1 - release_notes/2.3.3.md | 9 - release_notes/2.4.8.md | 6 - release_notes/2.5.3.md | 6 - release_notes/2.6.3.md | 7 - release_notes/2.6.6.md | 6 - release_notes/2.6.7.md | 1 - release_notes/2.7.0.md | 1 - release_notes/2.8.0.md | 1 - release_notes/2.9.0.md | 1 - release_notes/unreleased.md | 1 - requirements.txt | 1 - splunk.json | 1334 -------------- splunk_connector.py | 1542 ----------------- splunk_views.py | 64 - src/__init__.py | 16 + src/app.py | 1314 ++++++++++++++ splunk_consts.py => src/splunk_consts.py | 92 +- .../splunk_run_query.html | 100 +- tests/__init__.py | 0 uv.lock | 1318 ++++++++++++++ wheels/py3/packaging-25.0-py3-none-any.whl | Bin 66469 -> 0 bytes wheels/py3/splunk_sdk-2.1.1-py3-none-any.whl | Bin 125895 -> 0 bytes .../deprecation-2.1.0-py2.py3-none-any.whl | Bin 11178 -> 0 bytes 63 files changed, 4337 insertions(+), 4513 deletions(-) delete mode 100644 .github/workflows/call-publish.yml create mode 100644 .gitignore delete mode 100644 .pre-commit-config.yaml delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE delete mode 100644 NOTICE delete mode 100644 README.md create mode 100644 Splunk/manifest.json delete mode 100644 __init__.py delete mode 100644 manual_readme_content.md delete mode 100644 release_notes/1.2.15.md delete mode 100644 release_notes/1.2.18.md delete mode 100644 release_notes/1.3.16.md delete mode 100644 release_notes/1.3.19.md delete mode 100644 release_notes/1.3.23.md delete mode 100644 release_notes/1.3.41.md delete mode 100644 release_notes/1.3.7.md delete mode 100644 release_notes/2.0.22.md delete mode 100644 release_notes/2.0.34.md delete mode 100644 release_notes/2.1.3.md delete mode 100644 release_notes/2.1.6.md delete mode 100644 release_notes/2.10.0.md delete mode 100644 release_notes/2.11.0.md delete mode 100644 release_notes/2.11.1.md delete mode 100644 release_notes/2.12.0.md delete mode 100644 release_notes/2.13.0.md delete mode 100644 release_notes/2.14.0.md delete mode 100644 release_notes/2.15.0.md delete mode 100644 release_notes/2.15.1.md delete mode 100644 release_notes/2.16.0.md delete mode 100644 release_notes/2.16.1.md delete mode 100644 release_notes/2.16.2.md delete mode 100644 release_notes/2.17.0.md delete mode 100644 release_notes/2.18.0.md delete mode 100644 release_notes/2.2.3.md delete mode 100644 release_notes/2.20.0.md delete mode 100644 release_notes/2.20.1.md delete mode 100644 release_notes/2.20.2.md delete mode 100644 release_notes/2.20.3.md delete mode 100644 release_notes/2.3.3.md delete mode 100644 release_notes/2.4.8.md delete mode 100644 release_notes/2.5.3.md delete mode 100644 release_notes/2.6.3.md delete mode 100644 release_notes/2.6.6.md delete mode 100644 release_notes/2.6.7.md delete mode 100644 release_notes/2.7.0.md delete mode 100644 release_notes/2.8.0.md delete mode 100644 release_notes/2.9.0.md delete mode 100644 release_notes/unreleased.md delete mode 100644 requirements.txt delete mode 100644 splunk.json delete mode 100644 splunk_connector.py delete mode 100644 splunk_views.py create mode 100644 src/__init__.py create mode 100644 src/app.py rename splunk_consts.py => src/splunk_consts.py (56%) rename splunk_run_query.html => templates/splunk_run_query.html (63%) create mode 100644 tests/__init__.py create mode 100644 uv.lock delete mode 100644 wheels/py3/packaging-25.0-py3-none-any.whl delete mode 100644 wheels/py3/splunk_sdk-2.1.1-py3-none-any.whl delete mode 100644 wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl diff --git a/.github/workflows/call-publish.yml b/.github/workflows/call-publish.yml deleted file mode 100644 index 307d855..0000000 --- a/.github/workflows/call-publish.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Call Publish Workflow - -on: - push: - branches: - - main # Runs only on push (merge) to main - -jobs: - call-publish: - uses: splunk-soar-connectors/.github/.github/workflows/publish.yml@main - secrets: - release_queue_url: ${{ secrets.RELEASE_QUEUE_URL }} - splunkbase_user: ${{ secrets.SPLUNKBASE_USER }} - splunkbase_password: ${{ secrets.SPLUNKBASE_PASSWORD }} - semantic_release_pk: ${{ secrets.SEMANTIC_RELEASE_PK }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec08529 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +splunk.tgz +__pycache__/ +*.egg-info/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 33f03a7..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,87 +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.2.0 - hooks: - - id: conventional-pre-commit - stages: [commit-msg] - args: [--verbose] - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.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.13.0 - 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: 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 - args: ['--no-verify'] - exclude_types: [json] - exclude: "README.md" - # Central hooks - - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v2.0.5 - 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/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 6409847..0000000 --- a/NOTICE +++ /dev/null @@ -1,12 +0,0 @@ -Splunk SOAR App: Splunk -Copyright (c) 2016-2025 Splunk Inc. -Third Party Software Attributions: - -@@@@============================================================================ - -Library: splunk-sdk - 2.1.0 -Homepage: http://github.com/splunk/splunk-sdk-python -License: Apache Software License -License Text: - -Please navigate to http://github.com/splunk/splunk-sdk-python to obtain a copy of the license. diff --git a/README.md b/README.md deleted file mode 100644 index 252a587..0000000 --- a/README.md +++ /dev/null @@ -1,611 +0,0 @@ -# Splunk - -Publisher: Splunk
-Connector Version: 2.20.3
-Product Vendor: Splunk Inc.
-Product Name: Splunk Enterprise
-Minimum Product Version: 6.3.0 - -This app integrates with Splunk to update data on the device, in addition to investigate and ingestion actions - -## App's Token-Based Authentication Workflow - -- This app also supports API token based authentication. - -- Please follow the steps mentioned in this - [documentation](https://docs.splunk.com/Documentation/Splunk/9.0.0/Security/CreateAuthTokens) to - generate an API token. - - **NOTE -** If the username/password and API token are both provided then the API token will be - given preference and a token-based authentication workflow will be used. - -## Splunk-SDK - -This app uses the Splunk-SDK module, which is licensed under the Apache Software License, Copyright -(c) 2011-2024 Splunk, Inc. - -## State File Permissions - -Please check the permissions for the state file as mentioned below. - -#### State Filepath - -- For Non-NRI Instance: - /opt/phantom/local_data/app_states/91883aa8-9c81-470b-97a1-5d8f7995f560/{asset_id}\_state.json -- For NRI Instance: - /phantomcyber/local_data/app_states/91883aa8-9c81-470b-97a1-5d8f7995f560/{asset_id}\_state.json - -#### State File Permissions - -- File Rights: rw-rw-r-- (664) (The Splunk SOAR user should have read and write access for the state - file) -- File Owner: appropriate Splunk SOAR user - -## Required Permissions for Post Data Action - -The endpoint used by the post data action is not supported on Splunk Cloud Platform. Hence, the following steps are not applicable for Splunk Cloud Platform. - -For sending events to Splunk Platform, the User configured in the asset would require **edit_tcp** capability. Follow the below steps to configure - -- Login to the Splunk Platform -- Go to **Setting > Roles** -- Click on role of the user configured in the asset(example: user) and go to **Capabilities** -- Search for '**edit_tcp**' in the capabilities enable it for the particular role -- To check if the capability is given to your user, go to **Settings > Users** and in the **Edit dropdown** and select **View Capabilities** -- Search for '**edit_tcp**' and if a tick besides it appears then the permission has been enabled for the user - -## Asset Configuration Parameters - -- container_name_prefix: - - Name to give containers created via ingestion - - - User can select a field name from the events data - - - If the provided field exists, then container_name_prefix will be the value against the - provided field from the events data - - If the provided field does not exist, then container_name_prefix will be the provided - field name itself - - - If the container_name_prefix parameter is not provided: - - - If the event data contains '\_time' field, then container_name_prefix will be 'Splunk - Log Entry on \' - - If the event data does not contain '\_time' field, then container_name_prefix will be - 'Splunk Log Entry' - - - Users can provide a string. Example: Test title -- container_name_values: - - Values to append to the container name created via ingestion - - - User can provide CIM fields - - - If the container_name_values parameter is provided: - - - If the provided field exists, then container_name_values will be the value against the - provided CIM field or its CIM field mapping from the events data - - If neither a CIM field mapping nor CIM field itself is present in the event data, then - container_name_values will be the CIM field mapping or CIM field - - - If the container_name_values parameter is not provided: - - - If 'container_name_prefix' parameter is not provided, then container_name_values will be - 'source' - - If 'container_name_prefix' parameter is provided, then container_name_values will be - empty - - - Users can provide a comma-separated string. Example: test1, test2 -- Container count to update the state file: - - This parameter will allow the user to specify the number of containers and will only be used - in scheduled or interval polling - - Everytime the count of the containers reaches the count provided by the user, the - "start_time" stored in the state file will be updated by the index time of that event - - The default value is 100 -- splunk_app: - - The app context of the namespace - - As per Splunk SDK's documentation, if the splunk_app parameter is not provided, then - "system" will be considered as splunk_app -- splunk_owner: - - The owner context of the namespace - - As per Splunk SDK's documentation, if the splunk_owner parameter is not provided, then - "nobody" will be considered as splunk_owner -- retry_count: - - Number of retries - - To ask a query to the Splunk server using the splunklib library, first, the query asked by - the user is to be parsed. Then, this parsed query is used to create a job and once this job - is ready the results are ready to be fetched. So while performing any of the above steps, if - any exception occurs then, the code will retry that step for the number of retries provided - in the "retry count" configuration parameter. - - It will also be used if an error or an exception occurs while posting the data in the "post - data" action or modifying the event in the "update event" action. -- remove_empty_cef: - - Remove CEF fields having empty values from the artifact - - It allows the user to remove CEF fields having empty values from the artifact during - ingestion. If the value of the parameter is 'true', CEF fields having empty values will be - removed. -- sleeptime_in_requests: - - The time to wait for next REST call(max 120 seconds) - - It allows the user to add sleep time between the REST calls while performing the - "run_query", "update_event", "get host events" and "on poll" action. -- splunk_job_timeout: - - The duration in seconds to wait before a scheduled Splunk job times out - - It allows the user to configure the duration after which the connector should consider the Splunk job as timed out. -- on_poll_display: - - Fields to save with On Poll - - Users can select the fields from the events which the user wants to ingest in the artifact - - If the on_poll_display parameter is not provided, then all the fields that are extracted - from the events will be ingested in the respective artifacts - - Users can provide comma-separated field names. Example: field1, field2, field3 -- If the on_poll_query(query to use with On Poll) parameter is not provided, then an error message - will be returned -- If the on_poll_command(command for the query to use with On Poll) parameter is not provided and - the on_poll_query does not start with "|" or "search", then the "search" keyword is added at - the beginning of the on_poll_query - Example: - - on_poll_command: None - on_poll_query: index = "main" - Final query generated internally: search index = "main" -- If the on_poll_command parameter is not provided and the on_poll_query starts with "|" or - "search", then the final query would be the same as the query provided in the on_poll_query - parameter - Example: - - on_poll_command: None - on_poll_query: search index = "main" - Final query generated internally: search index = "main" -- If on_poll_command parameter is provided, then query is formed as: {on_poll_command} - {on_poll_query} - Example: - - on_poll_command: search - on_poll_query: index = "main" - Final query generated internally: search index = "main" - -## Update Event - -- To execute this action successfully, the minimum role required is "ess_analyst", but the user - can have other roles too. - -- If the **wait_for_confirmation** parameter is False (which is the default), it will be faster - but there will be no confirmation that the notable ID corresponded with an actual notable event. - Setting it to True will cause the action to take longer because it will require an SPL search, - but it will provide more assurance that the update took place. - -- The action updates the event for the provided "event_id". If the **wait_for_confirmation** - parameter is True, the action validates the "event_id" provided by the user using the search - command: 'search \`notable\` | search event_id="\"'. - - - If this search command returns more than 0 results, the action updates the event. - - If this search command does not return any results then, the action fails with the message - "Please provide a valid event ID". - -- Use the integer status field to set custom status values (e.g., 1 for 'New', 2 for 'In Progress', etc.). Similarly, use the integer disposition field for custom disposition values (e.g., 0 for 'Undetermined'). - -## On Poll - -- There are two approaches to polling as mentioned below. - - - POLL NOW (Manual polling) - - - It will fetch the data every time as per the corresponding asset configuration - parameters. It doesn’t store the last run context of the fetched data. - - - Scheduled/Interval Polling - - - The ingestion action will be triggered after each specified time interval. It stores the - last run context of the fetched data and starts fetching new data based on the - combination of the values of stored context for the previous ingestion run and the - corresponding asset configuration parameters. - -- Notes - - - In case "on poll" returns any 4XX except 403, validate your search Query on Splunk - - Sample "Query" to use with On Poll: index="\_internal" | stats count by host, source, - sourcetype | head 5 | rename host as h0st | rename source as devicehostname - - Sample "Fields to save with On Poll" (if not provided, "on poll" will store all the fields): - source,sourcetype,hostname - - For the **on_poll_parse_only** parameter, if **True** , disables the expansion of search due - to evaluation of sub-searches, time term expansion, lookups, tags, eventtypes, and - sourcetype aliases. This parameter is used for the validation of the Splunk query before - fetching the results - - If multiple severities are returned for the incident in the "on poll" action, then the - highest "severity" will be given priority. If the "severity" is not present in the incident, - then the "urgency" of the incident will be considered. If the "urgency" is also not present, - then the ingested container "severity" will be taken as "medium" by default. - -- Helpful examples to run on poll - - 1. The query will fetch top 10 events from the result of index = "main" search. - - on_poll_command: "search" - - on_poll_query: index = "main" | head 10 - - Final query generated internally: search index = "main" | head 10 - 1. The query will execute the query saved in the savedsearch named "Dashboard Views - Action - History". - - on_poll_command: "savedsearch" - - on_poll_query: "Dashboard Views - Action History" - - Final query generated internally: savedsearch "Dashboard Views - Action History" - 1. The query will perform statistics for datamodel and will give total count of events fetched - for datamodel = authentication. - - on_poll_command: "tstats" - - on_poll_query: "count from datamodel=Authentication" - - Final query generated internally: "tstats count from datamodel=Authentication" - 1. The query will display field "a" in table format for the results fetched from 'search index - = "\_internal"' search. - - on_poll_command: None - - on_poll_query: index = "\_internal" | table a - - Final query generated internally: search index = "\_internal" | table a - 1. This query will fetch all the events with sourcetype = "modular_alerts:notable", - app="phantom", and user="admin". - - on_poll_command: None - - on_poll_query: index=\* sourcetype="modular_alerts:notable" app="phantom" user="admin" - - Final query generated internally: search index=\* sourcetype="modular_alerts:notable" - app="phantom" user="admin" - 1. This query will get the count of the events that are indexed in index named "main". - - on_poll_command: None - - on_poll_query: index="main" | stats count - - Final query generated internally: search index="main" | stats count - 1. This query will add a field with name = "a" and value = "abc" in all the events that are - indexed in index named "main". - - on_poll_command: None - - on_poll_query: index="main" | eval a = "abc" - - Final query generated internally: search index="main" | eval a = "abc" - 1. This query will fetch only the sourcetype of all the events that are indexed in index named - "main". - - on_poll_command: None - - on_poll_query: index="main" | fields sourcetype - - Final query generated internally: search index="main" | fields sourcetype - 1. This query will fetch all the events having tag = error and index = main. - - on_poll_command: None - - on_poll_query: index="\_internal" tag=error - - Final query generated internally: search index="\_internal" tag="error" - 1. This query will show the data of "ppf_action_history_searches" lookup. - - on_poll_command: None - - on_poll_query: |inputlookup ppf_action_history_searches - - Final query generated internally: |inputlookup ppf_action_history_searches - -## Naming Ingested Containers - -By default, the "source" field is used to name the ingested containers. To customize the container -names, use the two settings in the asset configuration. For example, if a hostname is expected in -the container name, the "Name to give containers created via ingestion" parameter can be set to -"Notable Splunk Event" and "Values to append to container name" parameter can be set to "host". This -will set the container name to "Notable Splunk Event, host=my.sample.host". The appended values can -be a comma-separated list. - -## Special characters present in the Splunk query can affect the output - -The user must use appropriate special characters in the query according to individual use-case -otherwise the query will end up providing unexpected results. Following is a list of several such -special characters: - -- Non-breaking space -- Soft hyphen -- Micro symbol -- Division symbol -- Non-breaking hyphen -- En dash -- Em dash -- Ellipsis - -There can exist more such characters apart from the ones listed above. - -## Port Information - -The app uses HTTP/ HTTPS protocol for communicating with the Splunk server. Below are the default -ports used by Splunk SOAR. - -|         SERVICE NAME | TRANSPORT PROTOCOL | PORT | -|----------------------|--------------------|------| -|         http | tcp | 80 | -|         https | tcp | 443 | - -8089 is the default port used by Splunk Server. - -### Configuration variables - -This table lists the configuration variables required to operate Splunk. These variables are specified when configuring a Splunk Enterprise asset in Splunk SOAR. - -VARIABLE | REQUIRED | TYPE | DESCRIPTION --------- | -------- | ---- | ----------- -**device** | required | string | Device IP/Hostname | -**port** | optional | numeric | Port | -**username** | optional | string | Username | -**password** | optional | password | Password | -**api_token** | optional | password | API token | -**splunk_owner** | optional | string | The owner context of the namespace | -**splunk_app** | optional | string | The app context of the namespace | -**timezone** | required | timezone | Splunk Server Timezone | -**verify_server_cert** | optional | boolean | Verify Server Certificate | -**on_poll_command** | optional | string | Command for query to use with On Poll | -**on_poll_query** | optional | string | Query to use with On Poll | -**on_poll_display** | optional | string | Fields to save with On Poll | -**on_poll_parse_only** | optional | boolean | Parse Only | -**max_container** | optional | numeric | Max events to ingest for Scheduled Polling (Default: 100) | -**container_update_state** | optional | numeric | Container count to update the state file | -**container_name_prefix** | optional | string | Name to give containers created via ingestion | -**container_name_values** | optional | string | Values to append to container name | -**retry_count** | optional | numeric | Number of retries | -**remove_empty_cef** | optional | boolean | Remove CEF fields having empty values from the artifact | -**sleeptime_in_requests** | optional | numeric | The time to wait for next REST call (max 120 seconds) | -**include_cim_fields** | optional | boolean | Option to keep original Splunk CIM together with SOAR CEF fields | -**splunk_job_timeout** | optional | numeric | The duration in seconds to wait before a scheduled Splunk job times out | -**use_event_id_sdi** | optional | boolean | Option to use the event_id field value as the source data identifier instead of the full event hash | - -### Supported Actions - -[test connectivity](#action-test-connectivity) - Validate the asset configuration for connectivity. This action logs into the device to check the connection and credentials
-[get host events](#action-get-host-events) - Get events pertaining to a host that have occurred in the last 'N' days
-[on poll](#action-on-poll) - Ingest logs from the Splunk instance
-[run query](#action-run-query) - Run a search query on the Splunk device. Please escape any quotes that are part of the query string
-[update event](#action-update-event) - Update a notable event
-[post data](#action-post-data) - Post data to Splunk - -## action: 'test connectivity' - -Validate the asset configuration for connectivity. This action logs into the device to check the connection and credentials - -Type: **test**
-Read only: **True** - -#### Action Parameters - -No parameters are required for this action - -#### Action Output - -No Output - -## action: 'get host events' - -Get events pertaining to a host that have occurred in the last 'N' days - -Type: **investigate**
-Read only: **True** - -
  • The last_n_days parameter must be greater than 0.
  • The action will search for the events of the hostname (provided in the 'ip_hostname' parameter) in the default index configured on the Splunk instance.
- -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**ip_hostname** | required | Hostname/IP to search the events of | string | `ip` `host name` | -**last_n_days** | optional | Number of days ago | numeric | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.ip_hostname | string | `ip` `host name` | test_host | -action_result.parameter.last_n_days | numeric | | 2 | -action_result.data.\*.\_bkt | string | | | -action_result.data.\*.\_cd | string | | | -action_result.data.\*.\_indextime | string | | | -action_result.data.\*.\_raw | string | | | -action_result.data.\*.\_serial | string | | | -action_result.data.\*.\_si | string | | | -action_result.data.\*.\_sourcetype | string | | | -action_result.data.\*.\_time | string | | | -action_result.data.\*.host | string | `host name` | | -action_result.data.\*.index | string | | | -action_result.data.\*.linecount | string | | | -action_result.data.\*.source | string | | | -action_result.data.\*.sourcetype | string | | | -action_result.data.\*.splunk_server | string | `host name` | | -action_result.summary.sid | string | | 1612177958.977510 | -action_result.summary.total_events | numeric | | | -action_result.message | string | | Sid: 1621953772.25264, Total events: 1 | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 1 | - -## action: 'on poll' - -Ingest logs from the Splunk instance - -Type: **ingest**
-Read only: **True** - -The configured query is what will be used during ingestion. If you only wish to show certain fields, you can specify these as a comma-separated list in the configuration. If left unspecified, all available fields will be added to each artifact. When limiting the number of events to ingest, it will ingest the most recent events. To avoid duplication in polling, append '| fields \*' to the query.

- -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**container_id** | optional | Parameter ignored in this app | numeric | | -**start_time** | optional | Parameter ignored in this app | numeric | | -**end_time** | optional | Parameter ignored in this app | numeric | | -**container_count** | optional | Maximum number of events to query for | numeric | | -**artifact_count** | optional | Parameter ignored in this app | numeric | | - -#### Action Output - -No Output - -## action: 'run query' - -Run a search query on the Splunk device. Please escape any quotes that are part of the query string - -Type: **investigate**
-Read only: **True** - -By default, the widget for the "run query" action will show the host, time, and raw fields. If you would like to see specific fields parsed out, they can be listed in a comma-separated format in the "display" parameter.

Please keep in mind that Splunk does not always return all possible fields. Splunk may not return fields that are calculated or not present in the event.

To work around this you can force Splunk to return specific fields by using the "fields". By appending "| fields + \*" to your query, Splunk will return every field. You can replace the asterisk with a comma-separated list of fields to only return specific fields.

Finally, some searches (such as those based on data models) can contain name-spaced fields. If a data model called "my_model" with a search "my_search" has a field "hash" then the field will be named "my_search.hash" and that is what must be used in the Splunk fields command and the display parameter. If using a non-global lookup file that is only accessible by a specific Splunk App, make sure to note the specific Splunk App in your asset configuration. The parse_only parameter, if True, it disables the expansion of search due to evaluation of sub-searches, time term expansion, lookups, tags, eventtypes, and sourcetype alias. This parameter is used for the validation of the Splunk query before fetching the results.

Learn more below: - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**command** | optional | Beginning command (in Splunk Processing Language) | string | | -**query** | required | Query to run (in Splunk Processing Language) | string | `splunk query` | -**display** | optional | Display fields (comma-separated) | string | | -**parse_only** | optional | Parse only | boolean | | -**add_raw_field** | optional | Ingest \_raw field data | boolean | | -**attach_result** | optional | Attach result to the vault | boolean | | -**start_time** | optional | Earliest time modifier | string | | -**end_time** | optional | Latest time modifier | string | | -**search_mode** | optional | Search mode | string | | -**time_format** | optional | Custom timestamp format | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.attach_result | boolean | | True False | -action_result.parameter.command | string | | savedsearch | -action_result.parameter.display | string | | \_time index | -action_result.parameter.end_time | string | | -2d 2022-03-18T16:12:09.130+00:00 | -action_result.parameter.parse_only | boolean | | True False | -action_result.parameter.query | string | `splunk query` | "Send to test" | -action_result.parameter.search_mode | string | | smart | -action_result.parameter.start_time | string | | -2d 2022-03-18T16:12:07.130+00:00 | -action_result.data.\*.\_bkt | string | | | -action_result.data.\*.\_cd | string | | | -action_result.data.\*.\_indextime | string | | | -action_result.data.\*.\_key | string | | user | -action_result.data.\*.\_kv | string | | 1 | -action_result.data.\*.\_origtime | string | | 1659398400 | -action_result.data.\*.\_raw | string | | | -action_result.data.\*.\_serial | string | | | -action_result.data.\*.\_si | string | | | -action_result.data.\*.\_sourcetype | string | | | -action_result.data.\*.\_subsecond | string | | .427 | -action_result.data.\*.\_time | string | | | -action_result.data.\*.\_value | string | | 184 | -action_result.data.\*.a | string | | abc | -action_result.data.\*.content.app | string | | search | -action_result.data.\*.content.host | string | | test | -action_result.data.\*.content.info | string | | granted | -action_result.data.\*.content.search | string | | index = main | -action_result.data.\*.content.search_type | string | | adhoc | -action_result.data.\*.content.sid | string | | 1621953839.25275 | -action_result.data.\*.content.source | string | | source | -action_result.data.\*.content.sourcetype | string | | source | -action_result.data.\*.content.uri | string | | /en-US/app/search/search?q=search%20index%3Dmain%20%7C%20head%2010&sid=1651356328.532450&display.page.search.mode=smart&dispatch.sample_ratio=1&workload_pool=&earliest=-24h%40h&latest=now | -action_result.data.\*.content.view | string | | search | -action_result.data.\*.count | string | | 3058733 | -action_result.data.\*.count(host) | string | | 28 | -action_result.data.\*.event | string | | {"data": {"count": 3, "size": 112, "transform": "access_app_tracker"}, "version": "1.0"} | -action_result.data.\*.host | string | `host name` | 10.1.67.187:8088 | -action_result.data.\*.index | string | | | -action_result.data.\*.is_Acceleration_Jobs | string | | 0 | -action_result.data.\*.is_Adhoc_Jobs | string | | 1 | -action_result.data.\*.is_Failed_Jobs | string | | 0 | -action_result.data.\*.is_Realtime_Jobs | string | | 0 | -action_result.data.\*.is_Scheduled_Jobs | string | | 0 | -action_result.data.\*.is_Subsearch_Jobs | string | | 0 | -action_result.data.\*.is_not_Acceleration_Jobs | string | | 1 | -action_result.data.\*.is_not_Adhoc_Jobs | string | | 0 | -action_result.data.\*.is_not_Failed_Jobs | string | | 1 | -action_result.data.\*.is_not_Realtime_Jobs | string | | 1 | -action_result.data.\*.is_not_Scheduled_Jobs | string | | 1 | -action_result.data.\*.is_not_Subsearch_Jobs | string | | 1 | -action_result.data.\*.linecount | string | | | -action_result.data.\*.source | string | | | -action_result.data.\*.sourcetype | string | | | -action_result.data.\*.spent | string | | 223 | -action_result.data.\*.splunk_server | string | `host name` | | -action_result.data.\*.user | string | | admin | -action_result.data.\*.values(source) | string | | /opt/splunk/var/log/splunk/scheduler.log | -action_result.summary.sid | string | | 1612177958.977510 | -action_result.summary.total_events | numeric | | 2 | -action_result.message | string | | Sid: 1612177958.977510, Total events: 2 | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 1 | -action_result.parameter.add_raw_field | boolean | | | -action_result.parameter.time_format | string | | | - -## action: 'update event' - -Update a notable event - -Type: **generic**
-Read only: **False** - -The event_ids parameter takes a single event_id (which has the format: 68E08B8B-A853-3A20-9768-231C97B7EE76@@notable@@a4bd78810ae8e03e285e552fac0ddb23) or an adaptive response SID + RID combo (which has the format: scheduler\_\_admin\_\_SplunkEnterpriseSecuritySuite\_\_RMD515d4671130158e57_at_1532441220_4982+0).

NOTE: This action only works with a notable event from Splunk ES.

Second Note: The status parameter takes a string value, but custom status values are unique to installation and not available at app creation. The integer_status parameter takes a positive integer denoting the custom value desired. This integer must be determined by the customer on-site. If set it will override status. - -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**event_ids** | required | Event ID to update | string | `splunk notable event id` | -**owner** | optional | New owner for the event | string | | -**status** | optional | New status for the event | string | | -**integer_status** | optional | Integer representing custom status value | numeric | | -**urgency** | optional | New urgency for the event | string | | -**comment** | optional | New comment for the event | string | | -**disposition** | optional | New disposition field | string | | -**integer_disposition** | optional | Integer representing custom disposition value | numeric | | -**wait_for_confirmation** | optional | Validate event_ids | boolean | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.comment | string | | test comment | -action_result.parameter.disposition | string | | unassigned | -action_result.parameter.event_ids | string | `splunk notable event id` | 1542751027.136723+0 | -action_result.parameter.integer_disposition | numeric | | 1 | -action_result.parameter.integer_status | numeric | | 1 | -action_result.parameter.owner | string | | test | -action_result.parameter.status | string | | new | -action_result.parameter.urgency | string | | low | -action_result.parameter.wait_for_confirmation | boolean | | False True | -action_result.data.\*.failure_count | numeric | | 0 | -action_result.data.\*.message | string | | 1 event updated successfully | -action_result.data.\*.success | boolean | | False True | -action_result.data.\*.success_count | numeric | | 1 | -action_result.summary.sid | string | | 1612177958.977510 | -action_result.summary.updated_event_id | string | | 2CF264EE-6016-4F6A-BCC3-4B7251E113F7@@notable@@035142b19c09ab645c6bbfb847e866f4 | -action_result.message | string | | Updated event id: 2CF264EE-6016-4F6A-BCC3-4B7251E113F7@@notable@@035142b19c09ab645c6bbfb847e866f4 | -summary.total_objects | numeric | | 1 | -summary.total_objects_successful | numeric | | 1 | - -## action: 'post data' - -Post data to Splunk - -Type: **generic**
-Read only: **False** - -This action creates an event on Splunk with the data included in the data parameter. If not specified the parameters will default to the following:
  • host - The IP of the Splunk SOAR instance running the action.
  • index - The default index configured on the Splunk instance.
  • source - "Phantom".
  • source_type - "Automation/Orchestration Platform".
- -#### Action Parameters - -PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS ---------- | -------- | ----------- | ---- | -------- -**data** | required | Data to post | string | | -**host** | optional | Host for event | string | `ip` `host name` | -**index** | optional | Index to send event to | string | | -**source** | optional | Source for event | string | | -**source_type** | optional | Type of source for event | string | | - -#### Action Output - -DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES ---------- | ---- | -------- | -------------- -action_result.status | string | | success failed | -action_result.parameter.data | string | | test_data | -action_result.parameter.host | string | `ip` `host name` | test_host | -action_result.parameter.index | string | | main | -action_result.parameter.source | string | | test | -action_result.parameter.source_type | string | | pb | -action_result.data | string | | | -action_result.summary | string | | | -action_result.message | string | | Successfully posted the data | -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/Splunk/manifest.json b/Splunk/manifest.json new file mode 100644 index 0000000..e5c36b1 --- /dev/null +++ b/Splunk/manifest.json @@ -0,0 +1,1532 @@ +{ + "name": "Splunk", + "description": "This app integrates with Splunk to update data on the device, in addition to investigate and ingestion actions", + "appid": "91883aa8-9c81-470b-97a1-5d8f7995f560", + "type": "siem", + "product_vendor": "Splunk Inc.", + "app_version": "3.0.1", + "license": "Copyright (c) 2016-2026 Splunk Inc.", + "min_phantom_version": "7.0.0", + "package_name": "phantom_splunk", + "main_module": "src.app:app", + "logo": "logo_splunk.svg", + "logo_dark": "logo_splunk_dark.svg", + "product_name": "Splunk Enterprise", + "python_version": "3.13,3.14", + "product_version_regex": ".*", + "publisher": "Splunk", + "utctime_updated": "2026-04-23T00:53:52.579554Z", + "fips_compliant": true, + "contributors": [], + "configuration": { + "device": { + "data_type": "string", + "required": true, + "description": "Device IP/Hostname", + "order": 0, + "category": "connectivity" + }, + "port": { + "data_type": "numeric", + "required": false, + "description": "Port", + "order": 1, + "category": "connectivity", + "default": 8089 + }, + "username": { + "data_type": "string", + "required": false, + "description": "Username", + "order": 2, + "category": "connectivity", + "default": "" + }, + "password": { + "data_type": "password", + "required": false, + "description": "Password", + "order": 3, + "category": "connectivity", + "default": "" + }, + "api_token": { + "data_type": "password", + "required": false, + "description": "API token", + "order": 4, + "category": "connectivity", + "default": "" + }, + "splunk_owner": { + "data_type": "string", + "required": false, + "description": "The owner context of the namespace", + "order": 5, + "category": "connectivity", + "default": "" + }, + "splunk_app": { + "data_type": "string", + "required": false, + "description": "The app context of the namespace", + "order": 6, + "category": "connectivity", + "default": "" + }, + "timezone": { + "data_type": "string", + "required": true, + "description": "Splunk Server Timezone", + "order": 7, + "category": "connectivity" + }, + "verify_server_cert": { + "data_type": "boolean", + "required": false, + "description": "Verify Server Certificate", + "order": 8, + "category": "connectivity", + "default": false + }, + "on_poll_command": { + "data_type": "string", + "required": false, + "description": "Command for query to use with On Poll", + "order": 9, + "category": "ingest", + "default": "", + "value_list": [ + "", + "search", + "eval", + "savedsearch", + "stats", + "table", + "tstats" + ] + }, + "on_poll_query": { + "data_type": "string", + "required": false, + "description": "Query to use with On Poll", + "order": 10, + "category": "ingest", + "default": "" + }, + "on_poll_display": { + "data_type": "string", + "required": false, + "description": "Fields to save with On Poll", + "order": 11, + "category": "ingest", + "default": "" + }, + "on_poll_parse_only": { + "data_type": "boolean", + "required": false, + "description": "Parse Only", + "order": 12, + "category": "ingest", + "default": true + }, + "max_container": { + "data_type": "numeric", + "required": false, + "description": "Max events to ingest for Scheduled Polling (Default: 100)", + "order": 13, + "category": "ingest", + "default": 100 + }, + "container_update_state": { + "data_type": "numeric", + "required": false, + "description": "Container count to update the state file", + "order": 14, + "category": "ingest", + "default": 100 + }, + "container_name_prefix": { + "data_type": "string", + "required": false, + "description": "Name to give containers created via ingestion", + "order": 15, + "category": "ingest", + "default": "" + }, + "container_name_values": { + "data_type": "string", + "required": false, + "description": "Values to append to container name", + "order": 16, + "category": "ingest", + "default": "" + }, + "retry_count": { + "data_type": "numeric", + "required": false, + "description": "Number of retries", + "order": 17, + "category": "connectivity", + "default": 3 + }, + "remove_empty_cef": { + "data_type": "boolean", + "required": false, + "description": "Remove CEF fields having empty values from the artifact", + "order": 18, + "category": "ingest", + "default": false + }, + "sleeptime_in_requests": { + "data_type": "numeric", + "required": false, + "description": "The time to wait for next REST call (max 120 seconds)", + "order": 19, + "category": "connectivity", + "default": 1 + }, + "include_cim_fields": { + "data_type": "boolean", + "required": false, + "description": "Option to keep original Splunk CIM together with SOAR CEF fields", + "order": 20, + "category": "ingest", + "default": false + }, + "splunk_job_timeout": { + "data_type": "numeric", + "required": false, + "description": "The duration in seconds to wait before a scheduled Splunk job times out", + "order": 21, + "category": "connectivity", + "default": 1200 + }, + "use_event_id_sdi": { + "data_type": "boolean", + "required": false, + "description": "Option to use the event_id field value as the source data identifier instead of the full event hash", + "order": 22, + "category": "ingest", + "default": false + } + }, + "actions": [ + { + "action": "test connectivity", + "identifier": "test_connectivity", + "description": "test connectivity", + "type": "test", + "read_only": true, + "versions": "EQ(*)", + "verbose": "Basic test for app.", + "parameters": {}, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failure" + ] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [ + 1 + ] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [ + 1 + ] + } + ] + }, + { + "action": "run query", + "identifier": "run_query", + "description": "Run a search query on the Splunk device. Please escape any quotes that are part of the query string", + "type": "investigate", + "read_only": true, + "versions": "EQ(*)", + "verbose": "", + "parameters": { + "command": { + "order": 0, + "name": "command", + "description": "Beginning command (in Splunk Processing Language)", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "search", + "value_list": [ + "search", + "eval", + "savedsearch", + "stats", + "table", + "tstats" + ] + }, + "query": { + "order": 1, + "name": "query", + "description": "Query to run (in Splunk Processing Language)", + "data_type": "string", + "required": true, + "primary": true, + "allow_list": false, + "contains": [ + "splunk query" + ] + }, + "display": { + "order": 2, + "name": "display", + "description": "Display fields (comma-separated)", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + }, + "parse_only": { + "order": 3, + "name": "parse_only", + "description": "Parse only", + "data_type": "boolean", + "required": false, + "primary": false, + "allow_list": false, + "default": false + }, + "add_raw_field": { + "order": 4, + "name": "add_raw_field", + "description": "Ingest _raw field data", + "data_type": "boolean", + "required": false, + "primary": false, + "allow_list": false, + "default": true + }, + "attach_result": { + "order": 5, + "name": "attach_result", + "description": "Attach result to the vault", + "data_type": "boolean", + "required": false, + "primary": false, + "allow_list": false, + "default": false + }, + "start_time": { + "order": 6, + "name": "start_time", + "description": "Earliest time modifier", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + }, + "end_time": { + "order": 7, + "name": "end_time", + "description": "Latest time modifier", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + }, + "search_mode": { + "order": 8, + "name": "search_mode", + "description": "Search mode", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "smart", + "value_list": [ + "fast", + "verbose", + "smart" + ] + }, + "time_format": { + "order": 9, + "name": "time_format", + "description": "Custom timestamp format", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failure" + ] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.command", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.query", + "data_type": "string", + "contains": [ + "splunk query" + ] + }, + { + "data_path": "action_result.parameter.display", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.parse_only", + "data_type": "boolean" + }, + { + "data_path": "action_result.parameter.add_raw_field", + "data_type": "boolean" + }, + { + "data_path": "action_result.parameter.attach_result", + "data_type": "boolean" + }, + { + "data_path": "action_result.parameter.start_time", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.end_time", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.search_mode", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.time_format", + "data_type": "string" + }, + { + "data_path": "action_result.summary.sid", + "data_type": "string" + }, + { + "data_path": "action_result.summary.total_events", + "data_type": "numeric" + }, + { + "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": "custom", + "view": "src.app.display_view" + } + }, + { + "action": "get host events", + "identifier": "get_host_events", + "description": "Get events pertaining to a host that have occurred in the last 'N' days", + "type": "investigate", + "read_only": true, + "versions": "EQ(*)", + "verbose": "", + "parameters": { + "ip_hostname": { + "order": 0, + "name": "ip_hostname", + "description": "Hostname/IP to search the events of", + "data_type": "string", + "required": true, + "primary": true, + "allow_list": false, + "contains": [ + "ip", + "host name" + ] + }, + "last_n_days": { + "order": 1, + "name": "last_n_days", + "description": "Number of days ago", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failure" + ] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.ip_hostname", + "data_type": "string", + "contains": [ + "ip", + "host name" + ] + }, + { + "data_path": "action_result.parameter.last_n_days", + "data_type": "string" + }, + { + "data_path": "action_result.summary.sid", + "data_type": "string" + }, + { + "data_path": "action_result.summary.total_events", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [ + 1 + ] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [ + 1 + ] + } + ] + }, + { + "action": "update event", + "identifier": "update_event", + "description": "Update a notable event", + "type": "generic", + "read_only": false, + "versions": "EQ(*)", + "verbose": "", + "parameters": { + "event_ids": { + "order": 0, + "name": "event_ids", + "description": "Event ID to update", + "data_type": "string", + "required": true, + "primary": true, + "allow_list": false, + "contains": [ + "splunk notable event id" + ] + }, + "owner": { + "order": 1, + "name": "owner", + "description": "New owner for the event", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + }, + "status": { + "order": 2, + "name": "status", + "description": "New status for the event", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "", + "value_list": [ + "", + "unassigned", + "new", + "in progress", + "pending", + "resolved", + "closed" + ] + }, + "integer_status": { + "order": 3, + "name": "integer_status", + "description": "Integer representing custom status value", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + }, + "urgency": { + "order": 4, + "name": "urgency", + "description": "New urgency for the event", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "", + "value_list": [ + "", + "informational", + "low", + "medium", + "high", + "critical" + ] + }, + "comment": { + "order": 5, + "name": "comment", + "description": "New comment for the event", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + }, + "disposition": { + "order": 6, + "name": "disposition", + "description": "New disposition field", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "", + "value_list": [ + "", + "Unassigned", + "True Positive - Suspicious Activity", + "Benign Positive - Suspicious But Expected", + "False Positive - Incorrect Analytic Logic", + "False Positive - Inaccurate Data", + "Undetermined", + "Other" + ] + }, + "integer_disposition": { + "order": 7, + "name": "integer_disposition", + "description": "Integer representing custom disposition value", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + }, + "wait_for_confirmation": { + "order": 8, + "name": "wait_for_confirmation", + "description": "Validate event_ids", + "data_type": "boolean", + "required": false, + "primary": false, + "allow_list": false, + "default": false + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failure" + ] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.event_ids", + "data_type": "string", + "contains": [ + "splunk notable event id" + ] + }, + { + "data_path": "action_result.parameter.owner", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.status", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.integer_status", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.urgency", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.comment", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.disposition", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.integer_disposition", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.wait_for_confirmation", + "data_type": "boolean" + }, + { + "data_path": "action_result.data.*.failure_count", + "data_type": "numeric" + }, + { + "data_path": "action_result.data.*.message", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.success", + "data_type": "boolean", + "example_values": [ + true, + false + ] + }, + { + "data_path": "action_result.data.*.success_count", + "data_type": "numeric" + }, + { + "data_path": "action_result.summary.sid", + "data_type": "string" + }, + { + "data_path": "action_result.summary.updated_event_id", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [ + 1 + ] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [ + 1 + ] + } + ] + }, + { + "action": "post data", + "identifier": "post_data", + "description": "Post data to Splunk", + "type": "generic", + "read_only": false, + "versions": "EQ(*)", + "verbose": "", + "parameters": { + "data": { + "order": 0, + "name": "data", + "description": "Data to post", + "data_type": "string", + "required": true, + "primary": false, + "allow_list": false + }, + "host": { + "order": 1, + "name": "host", + "description": "Host for event", + "data_type": "string", + "required": false, + "primary": true, + "allow_list": false, + "contains": [ + "ip", + "host name" + ], + "default": "" + }, + "index": { + "order": 2, + "name": "index", + "description": "Index to send event to", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "" + }, + "source": { + "order": 3, + "name": "source", + "description": "Source for event", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "Phantom" + }, + "source_type": { + "order": 4, + "name": "source_type", + "description": "Type of source for event", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": false, + "default": "Automation/Orchestration Platform" + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failure" + ] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.data", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.host", + "data_type": "string", + "contains": [ + "ip", + "host name" + ] + }, + { + "data_path": "action_result.parameter.index", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.source", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.source_type", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [ + 1 + ] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [ + 1 + ] + } + ] + }, + { + "action": "on poll", + "identifier": "on_poll", + "description": "on poll", + "type": "ingest", + "read_only": true, + "versions": "EQ(*)", + "verbose": "Callback action for the on_poll ingest functionality", + "parameters": { + "start_time": { + "order": 0, + "name": "start_time", + "description": "Start of time range, in epoch time (milliseconds).", + "data_type": "numeric", + "required": false, + "primary": false, + "allow_list": false + }, + "end_time": { + "order": 1, + "name": "end_time", + "description": "End of time range, in epoch time (milliseconds).", + "data_type": "numeric", + "required": false, + "primary": false, + "allow_list": false + }, + "container_count": { + "order": 2, + "name": "container_count", + "description": "Maximum number of container records to query for.", + "data_type": "numeric", + "required": false, + "primary": false, + "allow_list": false + }, + "artifact_count": { + "order": 3, + "name": "artifact_count", + "description": "Maximum number of artifact records to query for.", + "data_type": "numeric", + "required": false, + "primary": false, + "allow_list": false + }, + "container_id": { + "order": 4, + "name": "container_id", + "description": "Comma-separated list of container IDs to limit the ingestion to.", + "data_type": "string", + "required": false, + "primary": false, + "allow_list": true + } + }, + "output": [] + } + ], + "pip313_dependencies": { + "wheel": [ + { + "module": "annotated-doc", + "input_file": "wheels/shared/annotated_doc-0.0.4-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/annotated_doc-0.0.4-py3-none-any.whl" + }, + { + "module": "annotated-types", + "input_file": "wheels/shared/annotated_types-0.7.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/annotated_types-0.7.0-py3-none-any.whl" + }, + { + "module": "anyio", + "input_file": "wheels/shared/anyio-4.13.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/anyio-4.13.0-py3-none-any.whl" + }, + { + "module": "authlib", + "input_file": "wheels/shared/authlib-1.7.0-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/authlib-1.7.0-py2.py3-none-any.whl" + }, + { + "module": "bleach", + "input_file": "wheels/shared/bleach-6.3.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/bleach-6.3.0-py3-none-any.whl" + }, + { + "module": "build", + "input_file": "wheels/shared/build-1.4.3-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/build-1.4.3-py3-none-any.whl" + }, + { + "module": "cffi", + "input_file": "wheels/python313/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "input_file_aarch64": "wheels/python313/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" + }, + { + "module": "click", + "input_file": "wheels/shared/click-8.1.8-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/click-8.1.8-py3-none-any.whl" + }, + { + "module": "colorclass", + "input_file": "wheels/shared/colorclass-2.2.2-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/colorclass-2.2.2-py2.py3-none-any.whl" + }, + { + "module": "compressed-rtf", + "input_file": "wheels/shared/compressed_rtf-1.0.7-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/compressed_rtf-1.0.7-py3-none-any.whl" + }, + { + "module": "cryptography", + "input_file": "wheels/shared/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", + "input_file_aarch64": "wheels/shared/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl" + }, + { + "module": "deprecation", + "input_file": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl" + }, + { + "module": "distro", + "input_file": "wheels/shared/distro-1.9.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/distro-1.9.0-py3-none-any.whl" + }, + { + "module": "easygui", + "input_file": "wheels/shared/easygui-0.98.3-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/easygui-0.98.3-py2.py3-none-any.whl" + }, + { + "module": "ebcdic", + "input_file": "wheels/shared/ebcdic-1.1.1-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/ebcdic-1.1.1-py2.py3-none-any.whl" + }, + { + "module": "extract-msg", + "input_file": "wheels/shared/extract_msg-0.55.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/extract_msg-0.55.0-py3-none-any.whl" + }, + { + "module": "h11", + "input_file": "wheels/shared/h11-0.16.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/h11-0.16.0-py3-none-any.whl" + }, + { + "module": "hatchling", + "input_file": "wheels/shared/hatchling-1.29.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/hatchling-1.29.0-py3-none-any.whl" + }, + { + "module": "httpcore", + "input_file": "wheels/shared/httpcore-1.0.9-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/httpcore-1.0.9-py3-none-any.whl" + }, + { + "module": "httpx", + "input_file": "wheels/shared/httpx-0.28.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/httpx-0.28.1-py3-none-any.whl" + }, + { + "module": "httpx-retries", + "input_file": "wheels/shared/httpx_retries-0.5.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/httpx_retries-0.5.0-py3-none-any.whl" + }, + { + "module": "humanize", + "input_file": "wheels/shared/humanize-4.15.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/humanize-4.15.0-py3-none-any.whl" + }, + { + "module": "jinja2", + "input_file": "wheels/shared/jinja2-3.1.6-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/jinja2-3.1.6-py3-none-any.whl" + }, + { + "module": "joserfc", + "input_file": "wheels/shared/joserfc-1.6.4-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/joserfc-1.6.4-py3-none-any.whl" + }, + { + "module": "lark", + "input_file": "wheels/shared/lark-1.3.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/lark-1.3.1-py3-none-any.whl" + }, + { + "module": "markdown-it-py", + "input_file": "wheels/shared/markdown_it_py-4.0.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/markdown_it_py-4.0.0-py3-none-any.whl" + }, + { + "module": "markupsafe", + "input_file": "wheels/python313/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "input_file_aarch64": "wheels/python313/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "module": "mdurl", + "input_file": "wheels/shared/mdurl-0.1.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/mdurl-0.1.2-py3-none-any.whl" + }, + { + "module": "msoffcrypto-tool", + "input_file": "wheels/shared/msoffcrypto_tool-6.0.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/msoffcrypto_tool-6.0.0-py3-none-any.whl" + }, + { + "module": "olefile", + "input_file": "wheels/shared/olefile-0.47-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/olefile-0.47-py2.py3-none-any.whl" + }, + { + "module": "oletools", + "input_file": "wheels/shared/oletools-0.60.2-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/oletools-0.60.2-py2.py3-none-any.whl" + }, + { + "module": "packaging", + "input_file": "wheels/shared/packaging-26.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/packaging-26.1-py3-none-any.whl" + }, + { + "module": "pathspec", + "input_file": "wheels/shared/pathspec-1.0.4-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pathspec-1.0.4-py3-none-any.whl" + }, + { + "module": "pcodedmp", + "input_file": "wheels/shared/pcodedmp-1.2.6-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pcodedmp-1.2.6-py2.py3-none-any.whl" + }, + { + "module": "pluggy", + "input_file": "wheels/shared/pluggy-1.6.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pluggy-1.6.0-py3-none-any.whl" + }, + { + "module": "pycparser", + "input_file": "wheels/shared/pycparser-3.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pycparser-3.0-py3-none-any.whl" + }, + { + "module": "pydantic", + "input_file": "wheels/shared/pydantic-2.13.3-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pydantic-2.13.3-py3-none-any.whl" + }, + { + "module": "pydantic-core", + "input_file": "wheels/python313/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "input_file_aarch64": "wheels/python313/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "module": "pygments", + "input_file": "wheels/shared/pygments-2.20.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pygments-2.20.0-py3-none-any.whl" + }, + { + "module": "pyjwt", + "input_file": "wheels/shared/pyjwt-2.12.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pyjwt-2.12.1-py3-none-any.whl" + }, + { + "module": "pyparsing", + "input_file": "wheels/shared/pyparsing-3.3.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pyparsing-3.3.2-py3-none-any.whl" + }, + { + "module": "pyproject-hooks", + "input_file": "wheels/shared/pyproject_hooks-1.2.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pyproject_hooks-1.2.0-py3-none-any.whl" + }, + { + "module": "python-dateutil", + "input_file": "wheels/shared/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/python_dateutil-2.9.0.post0-py2.py3-none-any.whl" + }, + { + "module": "red-black-tree-mod", + "input_file": "wheels/shared/red_black_tree_mod-1.22-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/red_black_tree_mod-1.22-py3-none-any.whl" + }, + { + "module": "rich", + "input_file": "wheels/shared/rich-15.0.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/rich-15.0.0-py3-none-any.whl" + }, + { + "module": "rtfde", + "input_file": "wheels/shared/rtfde-0.1.2.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/rtfde-0.1.2.2-py3-none-any.whl" + }, + { + "module": "setuptools", + "input_file": "wheels/shared/setuptools-82.0.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/setuptools-82.0.1-py3-none-any.whl" + }, + { + "module": "shellingham", + "input_file": "wheels/shared/shellingham-1.5.4-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/shellingham-1.5.4-py2.py3-none-any.whl" + }, + { + "module": "splunk-sdk", + "input_file": "wheels/shared/splunk_sdk-2.1.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/splunk_sdk-2.1.1-py3-none-any.whl" + }, + { + "module": "toml", + "input_file": "wheels/shared/toml-0.10.2-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/toml-0.10.2-py2.py3-none-any.whl" + }, + { + "module": "tqdm", + "input_file": "wheels/shared/tqdm-4.67.3-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/tqdm-4.67.3-py3-none-any.whl" + }, + { + "module": "trove-classifiers", + "input_file": "wheels/shared/trove_classifiers-2026.1.14.14-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/trove_classifiers-2026.1.14.14-py3-none-any.whl" + }, + { + "module": "typer", + "input_file": "wheels/shared/typer-0.23.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/typer-0.23.1-py3-none-any.whl" + }, + { + "module": "typing-extensions", + "input_file": "wheels/shared/typing_extensions-4.15.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/typing_extensions-4.15.0-py3-none-any.whl" + }, + { + "module": "typing-inspection", + "input_file": "wheels/shared/typing_inspection-0.4.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/typing_inspection-0.4.2-py3-none-any.whl" + }, + { + "module": "tzlocal", + "input_file": "wheels/shared/tzlocal-5.3.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/tzlocal-5.3.1-py3-none-any.whl" + }, + { + "module": "webencodings", + "input_file": "wheels/shared/webencodings-0.5.1-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/webencodings-0.5.1-py2.py3-none-any.whl" + }, + { + "module": "soar_sdk", + "input_file": "wheels/shared/splunk_soar_sdk-3.19.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/splunk_soar_sdk-3.19.2-py3-none-any.whl" + } + ] + }, + "pip314_dependencies": { + "wheel": [ + { + "module": "annotated-doc", + "input_file": "wheels/shared/annotated_doc-0.0.4-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/annotated_doc-0.0.4-py3-none-any.whl" + }, + { + "module": "annotated-types", + "input_file": "wheels/shared/annotated_types-0.7.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/annotated_types-0.7.0-py3-none-any.whl" + }, + { + "module": "anyio", + "input_file": "wheels/shared/anyio-4.13.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/anyio-4.13.0-py3-none-any.whl" + }, + { + "module": "authlib", + "input_file": "wheels/shared/authlib-1.7.0-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/authlib-1.7.0-py2.py3-none-any.whl" + }, + { + "module": "bleach", + "input_file": "wheels/shared/bleach-6.3.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/bleach-6.3.0-py3-none-any.whl" + }, + { + "module": "build", + "input_file": "wheels/shared/build-1.4.3-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/build-1.4.3-py3-none-any.whl" + }, + { + "module": "cffi", + "input_file": "wheels/python314/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", + "input_file_aarch64": "wheels/python314/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" + }, + { + "module": "click", + "input_file": "wheels/shared/click-8.1.8-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/click-8.1.8-py3-none-any.whl" + }, + { + "module": "colorclass", + "input_file": "wheels/shared/colorclass-2.2.2-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/colorclass-2.2.2-py2.py3-none-any.whl" + }, + { + "module": "compressed-rtf", + "input_file": "wheels/shared/compressed_rtf-1.0.7-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/compressed_rtf-1.0.7-py3-none-any.whl" + }, + { + "module": "cryptography", + "input_file": "wheels/shared/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", + "input_file_aarch64": "wheels/shared/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl" + }, + { + "module": "deprecation", + "input_file": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl" + }, + { + "module": "distro", + "input_file": "wheels/shared/distro-1.9.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/distro-1.9.0-py3-none-any.whl" + }, + { + "module": "easygui", + "input_file": "wheels/shared/easygui-0.98.3-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/easygui-0.98.3-py2.py3-none-any.whl" + }, + { + "module": "ebcdic", + "input_file": "wheels/shared/ebcdic-1.1.1-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/ebcdic-1.1.1-py2.py3-none-any.whl" + }, + { + "module": "extract-msg", + "input_file": "wheels/shared/extract_msg-0.55.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/extract_msg-0.55.0-py3-none-any.whl" + }, + { + "module": "h11", + "input_file": "wheels/shared/h11-0.16.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/h11-0.16.0-py3-none-any.whl" + }, + { + "module": "hatchling", + "input_file": "wheels/shared/hatchling-1.29.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/hatchling-1.29.0-py3-none-any.whl" + }, + { + "module": "httpcore", + "input_file": "wheels/shared/httpcore-1.0.9-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/httpcore-1.0.9-py3-none-any.whl" + }, + { + "module": "httpx", + "input_file": "wheels/shared/httpx-0.28.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/httpx-0.28.1-py3-none-any.whl" + }, + { + "module": "httpx-retries", + "input_file": "wheels/shared/httpx_retries-0.5.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/httpx_retries-0.5.0-py3-none-any.whl" + }, + { + "module": "humanize", + "input_file": "wheels/shared/humanize-4.15.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/humanize-4.15.0-py3-none-any.whl" + }, + { + "module": "jinja2", + "input_file": "wheels/shared/jinja2-3.1.6-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/jinja2-3.1.6-py3-none-any.whl" + }, + { + "module": "joserfc", + "input_file": "wheels/shared/joserfc-1.6.4-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/joserfc-1.6.4-py3-none-any.whl" + }, + { + "module": "lark", + "input_file": "wheels/shared/lark-1.3.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/lark-1.3.1-py3-none-any.whl" + }, + { + "module": "markdown-it-py", + "input_file": "wheels/shared/markdown_it_py-4.0.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/markdown_it_py-4.0.0-py3-none-any.whl" + }, + { + "module": "markupsafe", + "input_file": "wheels/python314/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", + "input_file_aarch64": "wheels/python314/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "module": "mdurl", + "input_file": "wheels/shared/mdurl-0.1.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/mdurl-0.1.2-py3-none-any.whl" + }, + { + "module": "msoffcrypto-tool", + "input_file": "wheels/shared/msoffcrypto_tool-6.0.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/msoffcrypto_tool-6.0.0-py3-none-any.whl" + }, + { + "module": "olefile", + "input_file": "wheels/shared/olefile-0.47-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/olefile-0.47-py2.py3-none-any.whl" + }, + { + "module": "oletools", + "input_file": "wheels/shared/oletools-0.60.2-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/oletools-0.60.2-py2.py3-none-any.whl" + }, + { + "module": "packaging", + "input_file": "wheels/shared/packaging-26.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/packaging-26.1-py3-none-any.whl" + }, + { + "module": "pathspec", + "input_file": "wheels/shared/pathspec-1.0.4-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pathspec-1.0.4-py3-none-any.whl" + }, + { + "module": "pcodedmp", + "input_file": "wheels/shared/pcodedmp-1.2.6-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pcodedmp-1.2.6-py2.py3-none-any.whl" + }, + { + "module": "pluggy", + "input_file": "wheels/shared/pluggy-1.6.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pluggy-1.6.0-py3-none-any.whl" + }, + { + "module": "pycparser", + "input_file": "wheels/shared/pycparser-3.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pycparser-3.0-py3-none-any.whl" + }, + { + "module": "pydantic", + "input_file": "wheels/shared/pydantic-2.13.3-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pydantic-2.13.3-py3-none-any.whl" + }, + { + "module": "pydantic-core", + "input_file": "wheels/python314/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", + "input_file_aarch64": "wheels/python314/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" + }, + { + "module": "pygments", + "input_file": "wheels/shared/pygments-2.20.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pygments-2.20.0-py3-none-any.whl" + }, + { + "module": "pyjwt", + "input_file": "wheels/shared/pyjwt-2.12.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pyjwt-2.12.1-py3-none-any.whl" + }, + { + "module": "pyparsing", + "input_file": "wheels/shared/pyparsing-3.3.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pyparsing-3.3.2-py3-none-any.whl" + }, + { + "module": "pyproject-hooks", + "input_file": "wheels/shared/pyproject_hooks-1.2.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/pyproject_hooks-1.2.0-py3-none-any.whl" + }, + { + "module": "python-dateutil", + "input_file": "wheels/shared/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/python_dateutil-2.9.0.post0-py2.py3-none-any.whl" + }, + { + "module": "red-black-tree-mod", + "input_file": "wheels/shared/red_black_tree_mod-1.22-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/red_black_tree_mod-1.22-py3-none-any.whl" + }, + { + "module": "rich", + "input_file": "wheels/shared/rich-15.0.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/rich-15.0.0-py3-none-any.whl" + }, + { + "module": "rtfde", + "input_file": "wheels/shared/rtfde-0.1.2.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/rtfde-0.1.2.2-py3-none-any.whl" + }, + { + "module": "setuptools", + "input_file": "wheels/shared/setuptools-82.0.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/setuptools-82.0.1-py3-none-any.whl" + }, + { + "module": "shellingham", + "input_file": "wheels/shared/shellingham-1.5.4-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/shellingham-1.5.4-py2.py3-none-any.whl" + }, + { + "module": "splunk-sdk", + "input_file": "wheels/shared/splunk_sdk-2.1.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/splunk_sdk-2.1.1-py3-none-any.whl" + }, + { + "module": "toml", + "input_file": "wheels/shared/toml-0.10.2-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/toml-0.10.2-py2.py3-none-any.whl" + }, + { + "module": "tqdm", + "input_file": "wheels/shared/tqdm-4.67.3-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/tqdm-4.67.3-py3-none-any.whl" + }, + { + "module": "trove-classifiers", + "input_file": "wheels/shared/trove_classifiers-2026.1.14.14-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/trove_classifiers-2026.1.14.14-py3-none-any.whl" + }, + { + "module": "typer", + "input_file": "wheels/shared/typer-0.23.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/typer-0.23.1-py3-none-any.whl" + }, + { + "module": "typing-extensions", + "input_file": "wheels/shared/typing_extensions-4.15.0-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/typing_extensions-4.15.0-py3-none-any.whl" + }, + { + "module": "typing-inspection", + "input_file": "wheels/shared/typing_inspection-0.4.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/typing_inspection-0.4.2-py3-none-any.whl" + }, + { + "module": "tzlocal", + "input_file": "wheels/shared/tzlocal-5.3.1-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/tzlocal-5.3.1-py3-none-any.whl" + }, + { + "module": "webencodings", + "input_file": "wheels/shared/webencodings-0.5.1-py2.py3-none-any.whl", + "input_file_aarch64": "wheels/shared/webencodings-0.5.1-py2.py3-none-any.whl" + }, + { + "module": "soar_sdk", + "input_file": "wheels/shared/splunk_soar_sdk-3.19.2-py3-none-any.whl", + "input_file_aarch64": "wheels/shared/splunk_soar_sdk-3.19.2-py3-none-any.whl" + } + ] + }, + "supports_es_polling": false +} \ No newline at end of file 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/manual_readme_content.md b/manual_readme_content.md deleted file mode 100644 index 53c18ed..0000000 --- a/manual_readme_content.md +++ /dev/null @@ -1,289 +0,0 @@ -## App's Token-Based Authentication Workflow - -- This app also supports API token based authentication. - -- Please follow the steps mentioned in this - [documentation](https://docs.splunk.com/Documentation/Splunk/9.0.0/Security/CreateAuthTokens) to - generate an API token. - - **NOTE -** If the username/password and API token are both provided then the API token will be - given preference and a token-based authentication workflow will be used. - -## Splunk-SDK - -This app uses the Splunk-SDK module, which is licensed under the Apache Software License, Copyright -(c) 2011-2024 Splunk, Inc. - -## State File Permissions - -Please check the permissions for the state file as mentioned below. - -#### State Filepath - -- For Non-NRI Instance: - /opt/phantom/local_data/app_states/91883aa8-9c81-470b-97a1-5d8f7995f560/{asset_id}\_state.json -- For NRI Instance: - /phantomcyber/local_data/app_states/91883aa8-9c81-470b-97a1-5d8f7995f560/{asset_id}\_state.json - -#### State File Permissions - -- File Rights: rw-rw-r-- (664) (The Splunk SOAR user should have read and write access for the state - file) -- File Owner: appropriate Splunk SOAR user - -## Required Permissions for Post Data Action - -The endpoint used by the post data action is not supported on Splunk Cloud Platform. Hence, the following steps are not applicable for Splunk Cloud Platform. - -For sending events to Splunk Platform, the User configured in the asset would require **edit_tcp** capability. Follow the below steps to configure - -- Login to the Splunk Platform -- Go to **Setting > Roles** -- Click on role of the user configured in the asset(example: user) and go to **Capabilities** -- Search for '**edit_tcp**' in the capabilities enable it for the particular role -- To check if the capability is given to your user, go to **Settings > Users** and in the **Edit dropdown** and select **View Capabilities** -- Search for '**edit_tcp**' and if a tick besides it appears then the permission has been enabled for the user - -## Asset Configuration Parameters - -- container_name_prefix: - - Name to give containers created via ingestion - - - User can select a field name from the events data - - - If the provided field exists, then container_name_prefix will be the value against the - provided field from the events data - - If the provided field does not exist, then container_name_prefix will be the provided - field name itself - - - If the container_name_prefix parameter is not provided: - - - If the event data contains '\_time' field, then container_name_prefix will be 'Splunk - Log Entry on \' - - If the event data does not contain '\_time' field, then container_name_prefix will be - 'Splunk Log Entry' - - - Users can provide a string. Example: Test title -- container_name_values: - - Values to append to the container name created via ingestion - - - User can provide CIM fields - - - If the container_name_values parameter is provided: - - - If the provided field exists, then container_name_values will be the value against the - provided CIM field or its CIM field mapping from the events data - - If neither a CIM field mapping nor CIM field itself is present in the event data, then - container_name_values will be the CIM field mapping or CIM field - - - If the container_name_values parameter is not provided: - - - If 'container_name_prefix' parameter is not provided, then container_name_values will be - 'source' - - If 'container_name_prefix' parameter is provided, then container_name_values will be - empty - - - Users can provide a comma-separated string. Example: test1, test2 -- Container count to update the state file: - - This parameter will allow the user to specify the number of containers and will only be used - in scheduled or interval polling - - Everytime the count of the containers reaches the count provided by the user, the - "start_time" stored in the state file will be updated by the index time of that event - - The default value is 100 -- splunk_app: - - The app context of the namespace - - As per Splunk SDK's documentation, if the splunk_app parameter is not provided, then - "system" will be considered as splunk_app -- splunk_owner: - - The owner context of the namespace - - As per Splunk SDK's documentation, if the splunk_owner parameter is not provided, then - "nobody" will be considered as splunk_owner -- retry_count: - - Number of retries - - To ask a query to the Splunk server using the splunklib library, first, the query asked by - the user is to be parsed. Then, this parsed query is used to create a job and once this job - is ready the results are ready to be fetched. So while performing any of the above steps, if - any exception occurs then, the code will retry that step for the number of retries provided - in the "retry count" configuration parameter. - - It will also be used if an error or an exception occurs while posting the data in the "post - data" action or modifying the event in the "update event" action. -- remove_empty_cef: - - Remove CEF fields having empty values from the artifact - - It allows the user to remove CEF fields having empty values from the artifact during - ingestion. If the value of the parameter is 'true', CEF fields having empty values will be - removed. -- sleeptime_in_requests: - - The time to wait for next REST call(max 120 seconds) - - It allows the user to add sleep time between the REST calls while performing the - "run_query", "update_event", "get host events" and "on poll" action. -- splunk_job_timeout: - - The duration in seconds to wait before a scheduled Splunk job times out - - It allows the user to configure the duration after which the connector should consider the Splunk job as timed out. -- on_poll_display: - - Fields to save with On Poll - - Users can select the fields from the events which the user wants to ingest in the artifact - - If the on_poll_display parameter is not provided, then all the fields that are extracted - from the events will be ingested in the respective artifacts - - Users can provide comma-separated field names. Example: field1, field2, field3 -- If the on_poll_query(query to use with On Poll) parameter is not provided, then an error message - will be returned -- If the on_poll_command(command for the query to use with On Poll) parameter is not provided and - the on_poll_query does not start with "|" or "search", then the "search" keyword is added at - the beginning of the on_poll_query - Example: - - on_poll_command: None - on_poll_query: index = "main" - Final query generated internally: search index = "main" -- If the on_poll_command parameter is not provided and the on_poll_query starts with "|" or - "search", then the final query would be the same as the query provided in the on_poll_query - parameter - Example: - - on_poll_command: None - on_poll_query: search index = "main" - Final query generated internally: search index = "main" -- If on_poll_command parameter is provided, then query is formed as: {on_poll_command} - {on_poll_query} - Example: - - on_poll_command: search - on_poll_query: index = "main" - Final query generated internally: search index = "main" - -## Update Event - -- To execute this action successfully, the minimum role required is "ess_analyst", but the user - can have other roles too. - -- If the **wait_for_confirmation** parameter is False (which is the default), it will be faster - but there will be no confirmation that the notable ID corresponded with an actual notable event. - Setting it to True will cause the action to take longer because it will require an SPL search, - but it will provide more assurance that the update took place. - -- The action updates the event for the provided "event_id". If the **wait_for_confirmation** - parameter is True, the action validates the "event_id" provided by the user using the search - command: 'search \`notable\` | search event_id="\"'. - - - If this search command returns more than 0 results, the action updates the event. - - If this search command does not return any results then, the action fails with the message - "Please provide a valid event ID". - -- Use the integer status field to set custom status values (e.g., 1 for 'New', 2 for 'In Progress', etc.). Similarly, use the integer disposition field for custom disposition values (e.g., 0 for 'Undetermined'). - -## On Poll - -- There are two approaches to polling as mentioned below. - - - POLL NOW (Manual polling) - - - It will fetch the data every time as per the corresponding asset configuration - parameters. It doesn’t store the last run context of the fetched data. - - - Scheduled/Interval Polling - - - The ingestion action will be triggered after each specified time interval. It stores the - last run context of the fetched data and starts fetching new data based on the - combination of the values of stored context for the previous ingestion run and the - corresponding asset configuration parameters. - -- Notes - - - In case "on poll" returns any 4XX except 403, validate your search Query on Splunk - - Sample "Query" to use with On Poll: index="\_internal" | stats count by host, source, - sourcetype | head 5 | rename host as h0st | rename source as devicehostname - - Sample "Fields to save with On Poll" (if not provided, "on poll" will store all the fields): - source,sourcetype,hostname - - For the **on_poll_parse_only** parameter, if **True** , disables the expansion of search due - to evaluation of sub-searches, time term expansion, lookups, tags, eventtypes, and - sourcetype aliases. This parameter is used for the validation of the Splunk query before - fetching the results - - If multiple severities are returned for the incident in the "on poll" action, then the - highest "severity" will be given priority. If the "severity" is not present in the incident, - then the "urgency" of the incident will be considered. If the "urgency" is also not present, - then the ingested container "severity" will be taken as "medium" by default. - -- Helpful examples to run on poll - - 1. The query will fetch top 10 events from the result of index = "main" search. - - on_poll_command: "search" - - on_poll_query: index = "main" | head 10 - - Final query generated internally: search index = "main" | head 10 - 1. The query will execute the query saved in the savedsearch named "Dashboard Views - Action - History". - - on_poll_command: "savedsearch" - - on_poll_query: "Dashboard Views - Action History" - - Final query generated internally: savedsearch "Dashboard Views - Action History" - 1. The query will perform statistics for datamodel and will give total count of events fetched - for datamodel = authentication. - - on_poll_command: "tstats" - - on_poll_query: "count from datamodel=Authentication" - - Final query generated internally: "tstats count from datamodel=Authentication" - 1. The query will display field "a" in table format for the results fetched from 'search index - = "\_internal"' search. - - on_poll_command: None - - on_poll_query: index = "\_internal" | table a - - Final query generated internally: search index = "\_internal" | table a - 1. This query will fetch all the events with sourcetype = "modular_alerts:notable", - app="phantom", and user="admin". - - on_poll_command: None - - on_poll_query: index=\* sourcetype="modular_alerts:notable" app="phantom" user="admin" - - Final query generated internally: search index=\* sourcetype="modular_alerts:notable" - app="phantom" user="admin" - 1. This query will get the count of the events that are indexed in index named "main". - - on_poll_command: None - - on_poll_query: index="main" | stats count - - Final query generated internally: search index="main" | stats count - 1. This query will add a field with name = "a" and value = "abc" in all the events that are - indexed in index named "main". - - on_poll_command: None - - on_poll_query: index="main" | eval a = "abc" - - Final query generated internally: search index="main" | eval a = "abc" - 1. This query will fetch only the sourcetype of all the events that are indexed in index named - "main". - - on_poll_command: None - - on_poll_query: index="main" | fields sourcetype - - Final query generated internally: search index="main" | fields sourcetype - 1. This query will fetch all the events having tag = error and index = main. - - on_poll_command: None - - on_poll_query: index="\_internal" tag=error - - Final query generated internally: search index="\_internal" tag="error" - 1. This query will show the data of "ppf_action_history_searches" lookup. - - on_poll_command: None - - on_poll_query: |inputlookup ppf_action_history_searches - - Final query generated internally: |inputlookup ppf_action_history_searches - -## Naming Ingested Containers - -By default, the "source" field is used to name the ingested containers. To customize the container -names, use the two settings in the asset configuration. For example, if a hostname is expected in -the container name, the "Name to give containers created via ingestion" parameter can be set to -"Notable Splunk Event" and "Values to append to container name" parameter can be set to "host". This -will set the container name to "Notable Splunk Event, host=my.sample.host". The appended values can -be a comma-separated list. - -## Special characters present in the Splunk query can affect the output - -The user must use appropriate special characters in the query according to individual use-case -otherwise the query will end up providing unexpected results. Following is a list of several such -special characters: - -- Non-breaking space -- Soft hyphen -- Micro symbol -- Division symbol -- Non-breaking hyphen -- En dash -- Em dash -- Ellipsis - -There can exist more such characters apart from the ones listed above. - -## Port Information - -The app uses HTTP/ HTTPS protocol for communicating with the Splunk server. Below are the default -ports used by Splunk SOAR. - -|         SERVICE NAME | TRANSPORT PROTOCOL | PORT | -|----------------------|--------------------|------| -|         http | tcp | 80 | -|         https | tcp | 443 | - -8089 is the default port used by Splunk Server. diff --git a/pyproject.toml b/pyproject.toml index a816e31..92fa84e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,79 +1,135 @@ -# Ruff linting +[project] +name = "splunk" +version = "3.0.5" +description = "This app integrates with Splunk to update data on the device, in addition to investigate and ingestion actions" +license = "Copyright (c) 2016-2026 Splunk Inc." +requires-python = ">=3.13, <3.15" +authors = [] +dependencies = [ + "splunk-sdk>=2.1.1", + "splunk-soar-sdk>=3.18.1", + "beautifulsoup4>=4.12.0", + "python-dateutil>=2.9.0", + "requests>=2.31.0", + "xmltodict>=0.13.0", +] + + +[tool.soar.app] +main_module = "src.app:app" + +### YOU SHOULD NOT NEED TO TOUCH ANYTHING BELOW THIS LINE ### + +[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.uv.index]] +url = "https://pypi.python.org/simple" + +[tool.uv] +environments = [ + "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'", + "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.14'", + "sys_platform == 'linux' and platform_machine == 'aarch64' and python_version == '3.14'", + "sys_platform == 'darwin' and platform_machine == 'x86_64' and python_version == '3.14'", + "sys_platform == 'darwin' and platform_machine == 'arm64' and python_version == '3.14'", +] +required-environments = [ + "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.13'", + "sys_platform == 'linux' and platform_machine == 'x86_64' and python_version == '3.14'", +] + [tool.ruff] -line-length = 145 -target-version = "py39" +output-format = "full" +fix = true +target-version = "py313" [tool.ruff.lint] -select = [ # Auto-fixable rules only - "I", # isort - "UP", # pyupgrade - "F401", # unused imports - "RUF" # ruff rules +select = [ + "ERA", + "YTT", + "S", + "B", + "A", + "DTZ", + "T10", + "ISC", + "PT", + "SIM", + "PTH", + "E", + "F", + "W", + "PL", + "UP", + "RUF", ] - ignore = [ - "RUF012", # Not auto-fixable (remove eventually) - "RUF001" # Not auto-fixable (remove eventually) + "E402", + "E501", + "PT006", + "PT007", + "PTH123", + "PLR", ] -[tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401", "UP035"] # __init__.py file exceptions +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", + "S", +] +"src/**/*" = [ + "PT", +] -[tool.ruff.lint.isort] -combine-as-imports = true -lines-after-imports = 2 +[tool.ruff.lint.pyupgrade] +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] profile = "django" extension = "html" indent = 2 - -# Auto-fixable rules only include = "H008,H009,H010,H014,H024,H026,H033,T028,T034" - -# Ignore troublesome rules that aren't auto-fixable or causing issues ignore = "D004,D018,H005,H006,H007,H011,H012,H013,H015,H016,H017,H019,H020,H021,H022,H023,H025,H029,H030,H031,H035,H036,H037,J004,J018,T001,T002,T003,T027,T032" -# Markdown linting [tool.mdformat] wrap = true number = true -# Semgrep configuration [tool.semgrep] config = [ - "p/python", # Built-in Python rules - "semgrep", # Look for our other rules - "r/typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml" # TypeScript React security rule + "p/python", + "semgrep", + "r/typescript.react.security.audit.react-dangerouslysetinnerhtml.react-dangerouslysetinnerhtml", ] 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/1.2.15.md b/release_notes/1.2.15.md deleted file mode 100644 index e738578..0000000 --- a/release_notes/1.2.15.md +++ /dev/null @@ -1,7 +0,0 @@ -**Splunk Release Notes - Published by Splunk June 8, 2016** - - -**Version 1.2.15 - Released June 8, 2016** - -* Improved table display of "run query" action results. -* Fixed documentation typos. diff --git a/release_notes/1.2.18.md b/release_notes/1.2.18.md deleted file mode 100644 index 76a9ddd..0000000 --- a/release_notes/1.2.18.md +++ /dev/null @@ -1,7 +0,0 @@ -**Splunk Release Notes - Published by Splunk October 20, 2016** - - -**Version 1.2.18 - Released October 20, 2016** - -* Significant improvements to the datapath settings -* Minor app documentation corrections diff --git a/release_notes/1.3.16.md b/release_notes/1.3.16.md deleted file mode 100644 index 8a81764..0000000 --- a/release_notes/1.3.16.md +++ /dev/null @@ -1,6 +0,0 @@ -**Splunk Release Notes - Published by Splunk December 05, 2017** - - -**Version 1.3.16 - Released December 05, 2017** - -* Added support for on-poll ingestion (Beta Release only) diff --git a/release_notes/1.3.19.md b/release_notes/1.3.19.md deleted file mode 100644 index 8873751..0000000 --- a/release_notes/1.3.19.md +++ /dev/null @@ -1,6 +0,0 @@ -**Splunk Release Notes - Published by Splunk February 07, 2018** - - -**Version 1.3.19 - Released February 07, 2018** - -* App action views and Logo updates diff --git a/release_notes/1.3.23.md b/release_notes/1.3.23.md deleted file mode 100644 index c275fe1..0000000 --- a/release_notes/1.3.23.md +++ /dev/null @@ -1,10 +0,0 @@ -**Splunk Release Notes - Published by Splunk July 19, 2018** - - -**Version 1.3.23 - Released July 19, 2018** - -* Added support for user configurable retries -* Display parameters are now ordered as specified in the input on 'run query' action -* Bug fix on 'on poll' ingestion when the license is expired -* Bug fix on missing data when the fields are case sensitive -* Moved Splunk SDK wheel into the app for easier app customization diff --git a/release_notes/1.3.41.md b/release_notes/1.3.41.md deleted file mode 100644 index f060b90..0000000 --- a/release_notes/1.3.41.md +++ /dev/null @@ -1,11 +0,0 @@ -**Splunk Release Notes - Published by Splunk January 22, 2020** - - -**Version 1.3.41 - Released January 22, 2020** - -* Added functionality to run on poll action with only 'query' parameter and empty 'command' parameter -* Added support for new commands like table, stats, eval -* Handled exception for Unicode characters issues -* Bug fixed in 'run query', 'post data', and 'update event' actions -* Fixed issues in the output views -* Improved the documentation of the app diff --git a/release_notes/1.3.7.md b/release_notes/1.3.7.md deleted file mode 100644 index 84cd757..0000000 --- a/release_notes/1.3.7.md +++ /dev/null @@ -1,8 +0,0 @@ -**Splunk Release Notes - Published by Splunk July 6, 2017** - - -**Version 1.3.7 - Released July 6, 2017** - -* Added "post data" action -* Added "update event" action -* Fixed an issue that caused "get host events" to return no data diff --git a/release_notes/2.0.22.md b/release_notes/2.0.22.md deleted file mode 100644 index 8266689..0000000 --- a/release_notes/2.0.22.md +++ /dev/null @@ -1,12 +0,0 @@ -**Splunk Release Notes - Published by Splunk September 18, 2020** - - -**Version 2.0.22 - Released September 18, 2020** - -* Compatibility changes for Python 3 support -* Added the "wait\_for\_confirmation" action parameter in the "update event" action -* Changed Source Data Identifier of container/artifact to a hash of the combination of "\_raw", "source", "sourcetype", and "index" -* Made the custom view compatible with the Phantom V4.9 -* Added validations on action input parameters -* Handled the unicode character exceptions -* Updated the app documentation diff --git a/release_notes/2.0.34.md b/release_notes/2.0.34.md deleted file mode 100644 index 0ccbdf2..0000000 --- a/release_notes/2.0.34.md +++ /dev/null @@ -1,7 +0,0 @@ -**Splunk Release Notes - Published by Splunk February 08, 2021** - - -**Version 2.0.34 - Released February 08, 2021** - -* Updated the app documentation -* Added the "sid" key in summary of 'update\_event', 'run\_query', and 'get\_host\_events' actions diff --git a/release_notes/2.1.3.md b/release_notes/2.1.3.md deleted file mode 100644 index 053068c..0000000 --- a/release_notes/2.1.3.md +++ /dev/null @@ -1,7 +0,0 @@ -**Splunk Release Notes - Published by Splunk April 14, 2021** - - -**Version 2.1.3 - Released April 14, 2021** - -* Fixed a bug which caused the app to ignore the Global Proxy Settings [PAPP-11360] -* Fixed a bug during ingestion if an event had multiple associated severities [PAPP-12153] diff --git a/release_notes/2.1.6.md b/release_notes/2.1.6.md deleted file mode 100644 index 4729bea..0000000 --- a/release_notes/2.1.6.md +++ /dev/null @@ -1,6 +0,0 @@ -**Splunk Release Notes - Published by Splunk June 24, 2021** - - -**Version 2.1.6 - Released June 24, 2021** - -* Fixed the start\_time field in the artifact [PAPP-17613] diff --git a/release_notes/2.10.0.md b/release_notes/2.10.0.md deleted file mode 100644 index dcb85f1..0000000 --- a/release_notes/2.10.0.md +++ /dev/null @@ -1 +0,0 @@ -* Fixed an issue in On Poll action where the index time was not honored during scheduled ingestion [PAPP-25411] \ No newline at end of file diff --git a/release_notes/2.11.0.md b/release_notes/2.11.0.md deleted file mode 100644 index c6c0185..0000000 --- a/release_notes/2.11.0.md +++ /dev/null @@ -1,3 +0,0 @@ -* Added token-based authentication workflow -* Replaced an endpoint for test connectivity action -* Fixed miscellaneous proxy-related issues \ No newline at end of file diff --git a/release_notes/2.11.1.md b/release_notes/2.11.1.md deleted file mode 100644 index 5b03297..0000000 --- a/release_notes/2.11.1.md +++ /dev/null @@ -1,2 +0,0 @@ -* Improved error logging -* Removed python 2 related code \ No newline at end of file diff --git a/release_notes/2.12.0.md b/release_notes/2.12.0.md deleted file mode 100644 index 65e5280..0000000 --- a/release_notes/2.12.0.md +++ /dev/null @@ -1,2 +0,0 @@ -* Added search_mode parameter to "run query" action with fast, verbose, and smart as possible values [PAPP-10085] -* Update Splunk SDK to 1.7.2 and changed ResultsReader to JSONResultsReader [PAPP-27658] \ No newline at end of file diff --git a/release_notes/2.13.0.md b/release_notes/2.13.0.md deleted file mode 100644 index 7e7b49f..0000000 --- a/release_notes/2.13.0.md +++ /dev/null @@ -1 +0,0 @@ -* Fixed load balancer sticky sessions related cookie persistence bug [PAPP-27448, PAPP-26097] \ No newline at end of file diff --git a/release_notes/2.14.0.md b/release_notes/2.14.0.md deleted file mode 100644 index 41c5631..0000000 --- a/release_notes/2.14.0.md +++ /dev/null @@ -1 +0,0 @@ -* Fixed the issue related to source_data_identifier [PAPP-29653] \ No newline at end of file diff --git a/release_notes/2.15.0.md b/release_notes/2.15.0.md deleted file mode 100644 index d45f138..0000000 --- a/release_notes/2.15.0.md +++ /dev/null @@ -1,2 +0,0 @@ -* Bug fix for removing temp files [PAPP-30430] -* Added a new feature to include both CEF and original CIM field [PAPP-30037] \ No newline at end of file diff --git a/release_notes/2.15.1.md b/release_notes/2.15.1.md deleted file mode 100644 index 2c23017..0000000 --- a/release_notes/2.15.1.md +++ /dev/null @@ -1 +0,0 @@ -* Updated dependency packages, removed future [PAPP-31089] \ No newline at end of file diff --git a/release_notes/2.16.0.md b/release_notes/2.16.0.md deleted file mode 100644 index 5da072a..0000000 --- a/release_notes/2.16.0.md +++ /dev/null @@ -1,3 +0,0 @@ -* Documentation update for steps to allow edit_tcp capability for a user [PAPP-31540] -* Bug fix for 'on poll' cef field names [PAPP-30430] -* Bug fix for accessing vault temp directory path [PAPP-32416] \ No newline at end of file diff --git a/release_notes/2.16.1.md b/release_notes/2.16.1.md deleted file mode 100644 index 8847d1f..0000000 --- a/release_notes/2.16.1.md +++ /dev/null @@ -1 +0,0 @@ -* Changed logic in 'run query' action in order to decrease memory usage [PAPP-32609] \ No newline at end of file diff --git a/release_notes/2.16.2.md b/release_notes/2.16.2.md deleted file mode 100644 index 7e02622..0000000 --- a/release_notes/2.16.2.md +++ /dev/null @@ -1 +0,0 @@ -* Fixed Django template, `ifnotequal` tag was deprecated. \ No newline at end of file diff --git a/release_notes/2.17.0.md b/release_notes/2.17.0.md deleted file mode 100644 index f0dffbb..0000000 --- a/release_notes/2.17.0.md +++ /dev/null @@ -1 +0,0 @@ -* Added 'splunk_job_timeout' parameter to asset config [PAPP-34684] \ No newline at end of file diff --git a/release_notes/2.18.0.md b/release_notes/2.18.0.md deleted file mode 100644 index adac350..0000000 --- a/release_notes/2.18.0.md +++ /dev/null @@ -1 +0,0 @@ -* Added 2 new fields ("disposition" and "integer_disposition") to "update event" action \ No newline at end of file diff --git a/release_notes/2.2.3.md b/release_notes/2.2.3.md deleted file mode 100644 index 74a7496..0000000 --- a/release_notes/2.2.3.md +++ /dev/null @@ -1,10 +0,0 @@ -**Splunk Release Notes - Published by Splunk July 13, 2021** - - -**Version 2.2.3 - Released July 13, 2021** - -* Added support for custom status ID in the integer status parameter of the 'update event' action [PAPP-9598] -* Bug fix in the 'run query' action [PAPP-13769] -* Allow 0 for the 'Max events to ingest for Scheduled Polling' configuration parameter [PAPP-11483] -* Fix for the 'Values to append to the container name' configuration parameter [PAPP-11072] [PAPP-17977] -* Handled extra commas in the display parameter of the 'run query' action [PAPP-17228] diff --git a/release_notes/2.20.0.md b/release_notes/2.20.0.md deleted file mode 100644 index 43be1a2..0000000 --- a/release_notes/2.20.0.md +++ /dev/null @@ -1,2 +0,0 @@ -* Added 'use_event_id_sdi' parameter to asset config to allow updated event ingestion into the original container -* Added parameter in run query action to optionally remove the "_raw" field [PAPP-26864] \ No newline at end of file diff --git a/release_notes/2.20.1.md b/release_notes/2.20.1.md deleted file mode 100644 index 46df760..0000000 --- a/release_notes/2.20.1.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/2.20.2.md b/release_notes/2.20.2.md deleted file mode 100644 index 0aa400b..0000000 --- a/release_notes/2.20.2.md +++ /dev/null @@ -1,2 +0,0 @@ -* Resolved app issues related to Python 3.13 upgrade -* updated dependencies for python 3.13 \ No newline at end of file diff --git a/release_notes/2.20.3.md b/release_notes/2.20.3.md deleted file mode 100644 index 52795ae..0000000 --- a/release_notes/2.20.3.md +++ /dev/null @@ -1 +0,0 @@ -* Support specifying custom timestamp format for the 'run query' action. \ No newline at end of file diff --git a/release_notes/2.3.3.md b/release_notes/2.3.3.md deleted file mode 100644 index b148d03..0000000 --- a/release_notes/2.3.3.md +++ /dev/null @@ -1,9 +0,0 @@ -**Splunk Release Notes - Published by Splunk August 06, 2021** - - -**Version 2.3.3 - Released August 06, 2021** - -* Updated the 'update event' action's status based on the "success" key in response [PAPP-9587] -* Modified the code to re-connect based on retry limit in case of "Session not logged in" issue [PAPP-17690] -* Modified the on-poll action to ingest updated/deleted artifacts in the existing container [PAPP-18788] -* Updated the document for Update event action with the required role and permission diff --git a/release_notes/2.4.8.md b/release_notes/2.4.8.md deleted file mode 100644 index d22074b..0000000 --- a/release_notes/2.4.8.md +++ /dev/null @@ -1,6 +0,0 @@ -**Splunk Release Notes - Published by Splunk October 19, 2021** - - -**Version 2.4.8 - Released October 19, 2021** - -* Added a new 'Remove CEF fields having empty values from the artifact' configuration parameter [PAPP-9257] diff --git a/release_notes/2.5.3.md b/release_notes/2.5.3.md deleted file mode 100644 index a904e1c..0000000 --- a/release_notes/2.5.3.md +++ /dev/null @@ -1,6 +0,0 @@ -**Splunk Release Notes - Published by Splunk November 16, 2021** - - -**Version 2.5.3 - Released November 16, 2021** - -* Changed the hashing algorithm from md5 to sha-256 [PAPP-19934] diff --git a/release_notes/2.6.3.md b/release_notes/2.6.3.md deleted file mode 100644 index 74f955b..0000000 --- a/release_notes/2.6.3.md +++ /dev/null @@ -1,7 +0,0 @@ -**Splunk Release Notes - Published by Splunk December 01, 2021** - - -**Version 2.6.3 - Released December 01, 2021** - -* Fixed a bug in the 'on poll' action [PAPP-20789] -* Updated the app documentation diff --git a/release_notes/2.6.6.md b/release_notes/2.6.6.md deleted file mode 100644 index e623787..0000000 --- a/release_notes/2.6.6.md +++ /dev/null @@ -1,6 +0,0 @@ -**Splunk Release Notes - Published by Splunk January 20, 2022** - - -**Version 2.6.6 - Released January 20, 2022** - -* Changed the hashing algorithm to SHA256 when running in FIPS mode [PAPP-21816] \ No newline at end of file diff --git a/release_notes/2.6.7.md b/release_notes/2.6.7.md deleted file mode 100644 index ee41f86..0000000 --- a/release_notes/2.6.7.md +++ /dev/null @@ -1 +0,0 @@ -* Added support for Python 3.9 \ No newline at end of file diff --git a/release_notes/2.7.0.md b/release_notes/2.7.0.md deleted file mode 100644 index 8f73a32..0000000 --- a/release_notes/2.7.0.md +++ /dev/null @@ -1 +0,0 @@ -* Added a new 'attach_result' parameter in 'run query' action [PAPP-8315] \ No newline at end of file diff --git a/release_notes/2.8.0.md b/release_notes/2.8.0.md deleted file mode 100644 index 33461da..0000000 --- a/release_notes/2.8.0.md +++ /dev/null @@ -1 +0,0 @@ -* Added a sleep time between REST calls to improve the performance [PAPP-23575] \ No newline at end of file diff --git a/release_notes/2.9.0.md b/release_notes/2.9.0.md deleted file mode 100644 index 1c8186f..0000000 --- a/release_notes/2.9.0.md +++ /dev/null @@ -1 +0,0 @@ -* Added 2 new fields ("start_time" and "end_time") to "run query" action [PAPP-24566] \ 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 5f3a2d9..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -splunk-sdk==2.1.1 diff --git a/splunk.json b/splunk.json deleted file mode 100644 index b022cdc..0000000 --- a/splunk.json +++ /dev/null @@ -1,1334 +0,0 @@ -{ - "appid": "91883aa8-9c81-470b-97a1-5d8f7995f560", - "name": "Splunk", - "description": "This app integrates with Splunk to update data on the device, in addition to investigate and ingestion actions", - "publisher": "Splunk", - "contributors": [ - { - "name": "Jeff Berry" - }, - { - "name": "Mayur Pipaliya" - }, - { - "name": "Chetan Pangam" - }, - { - "name": "Govind Salinas" - }, - { - "name": "Atif Mahadik" - }, - { - "name": "Alexandra Lomotan" - }, - { - "name": "Philip Royer" - }, - { - "name": "Bartosz Debek" - }, - { - "name": "Tony Cihak" - }, - { - "name": "Mhike" - }, - { - "name": "Brendan Shea" - } - ], - "type": "siem", - "main_module": "splunk_connector.py", - "app_version": "2.20.3", - "utctime_updated": "2025-11-26T05:19:41.105110Z", - "package_name": "phantom_splunk", - "product_name": "Splunk Enterprise", - "product_vendor": "Splunk Inc.", - "product_version_regex": ".*", - "min_phantom_version": "6.3.0", - "fips_compliant": true, - "python_version": "3.9, 3.13", - "latest_tested_versions": [ - "On-premise, Splunk Enterprise Security v9.0.0, Jan 8 2024", - "Cloud, Splunk Cloud Platform v9.0.2303.202, Jan 8 2024" - ], - "logo": "logo_splunk.svg", - "logo_dark": "logo_splunk_dark.svg", - "license": "Copyright (c) 2016-2025 Splunk Inc.", - "configuration": { - "device": { - "description": "Device IP/Hostname", - "data_type": "string", - "order": 0, - "required": true - }, - "port": { - "description": "Port", - "data_type": "numeric", - "order": 1, - "default": 8089 - }, - "username": { - "description": "Username", - "order": 2, - "data_type": "string" - }, - "password": { - "description": "Password", - "order": 3, - "data_type": "password" - }, - "api_token": { - "description": "API token", - "order": 4, - "data_type": "password" - }, - "splunk_owner": { - "description": "The owner context of the namespace", - "order": 5, - "data_type": "string" - }, - "splunk_app": { - "description": "The app context of the namespace", - "order": 6, - "data_type": "string" - }, - "timezone": { - "data_type": "timezone", - "order": 7, - "description": "Splunk Server Timezone", - "required": true - }, - "verify_server_cert": { - "data_type": "boolean", - "order": 8, - "description": "Verify Server Certificate", - "default": false - }, - "on_poll_command": { - "data_type": "string", - "order": 9, - "description": "Command for query to use with On Poll", - "value_list": [ - "", - "search", - "eval", - "savedsearch", - "stats", - "table", - "tstats" - ] - }, - "on_poll_query": { - "data_type": "string", - "order": 10, - "description": "Query to use with On Poll" - }, - "on_poll_display": { - "data_type": "string", - "order": 11, - "description": "Fields to save with On Poll" - }, - "on_poll_parse_only": { - "data_type": "boolean", - "order": 12, - "description": "Parse Only", - "default": true - }, - "max_container": { - "data_type": "numeric", - "order": 13, - "description": "Max events to ingest for Scheduled Polling (Default: 100)", - "default": 100 - }, - "container_update_state": { - "data_type": "numeric", - "order": 14, - "description": "Container count to update the state file", - "default": 100 - }, - "container_name_prefix": { - "data_type": "string", - "order": 15, - "description": "Name to give containers created via ingestion" - }, - "container_name_values": { - "data_type": "string", - "order": 16, - "description": "Values to append to container name" - }, - "retry_count": { - "description": "Number of retries", - "data_type": "numeric", - "order": 17, - "default": 3 - }, - "remove_empty_cef": { - "description": "Remove CEF fields having empty values from the artifact", - "data_type": "boolean", - "order": 18, - "default": false - }, - "sleeptime_in_requests": { - "description": "The time to wait for next REST call (max 120 seconds)", - "data_type": "numeric", - "order": 19, - "default": 1 - }, - "include_cim_fields": { - "description": "Option to keep original Splunk CIM together with SOAR CEF fields", - "data_type": "boolean", - "order": 20, - "default": false, - "name": "include_cim_fields" - }, - "splunk_job_timeout": { - "description": "The duration in seconds to wait before a scheduled Splunk job times out", - "data_type": "numeric", - "order": 21, - "default": 1200, - "name": "splunk_job_timeout" - }, - "use_event_id_sdi": { - "description": "Option to use the event_id field value as the source data identifier instead of the full event hash", - "data_type": "boolean", - "order": 22, - "default": "False", - "name": "use_event_id_sdi", - "id": 22 - } - }, - "actions": [ - { - "action": "test connectivity", - "description": "Validate the asset configuration for connectivity. This action logs into the device to check the connection and credentials", - "type": "test", - "identifier": "test_asset_connectivity", - "read_only": true, - "parameters": {}, - "output": [], - "versions": "EQ(*)" - }, - { - "action": "get host events", - "description": "Get events pertaining to a host that have occurred in the last 'N' days", - "verbose": "
  • The last_n_days parameter must be greater than 0.
  • The action will search for the events of the hostname (provided in the 'ip_hostname' parameter) in the default index configured on the Splunk instance.
", - "type": "investigate", - "identifier": "get_host_events", - "read_only": true, - "parameters": { - "ip_hostname": { - "description": "Hostname/IP to search the events of", - "data_type": "string", - "order": 0, - "contains": [ - "ip", - "host name" - ], - "required": true, - "primary": true - }, - "last_n_days": { - "description": "Number of days ago", - "data_type": "numeric", - "order": 1 - } - }, - "render": { - "type": "table", - "width": 12, - "height": 5, - "title": "Search Results" - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.ip_hostname", - "data_type": "string", - "example_values": [ - "test_host" - ], - "contains": [ - "ip", - "host name" - ] - }, - { - "data_path": "action_result.parameter.last_n_days", - "data_type": "numeric", - "example_values": [ - 2 - ] - }, - { - "data_path": "action_result.data.*._bkt", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._cd", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._indextime", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._raw", - "column_name": "Raw", - "column_order": 2, - "data_type": "string" - }, - { - "data_path": "action_result.data.*._serial", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._si", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._sourcetype", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._time", - "column_name": "Time", - "column_order": 1, - "data_type": "string" - }, - { - "data_path": "action_result.data.*.host", - "column_name": "Host", - "column_order": 0, - "data_type": "string", - "contains": [ - "host name" - ] - }, - { - "data_path": "action_result.data.*.index", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.linecount", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.source", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.sourcetype", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.splunk_server", - "data_type": "string", - "contains": [ - "host name" - ] - }, - { - "data_path": "action_result.summary.sid", - "data_type": "string", - "example_values": [ - "1612177958.977510" - ] - }, - { - "data_path": "action_result.summary.total_events", - "data_type": "numeric" - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Sid: 1621953772.25264, Total events: 1" - ] - }, - { - "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": "on poll", - "description": "Ingest logs from the Splunk instance", - "verbose": "The configured query is what will be used during ingestion. If you only wish to show certain fields, you can specify these as a comma-separated list in the configuration. If left unspecified, all available fields will be added to each artifact. When limiting the number of events to ingest, it will ingest the most recent events. To avoid duplication in polling, append '| fields *' to the query.

", - "type": "ingest", - "identifier": "on_poll", - "read_only": true, - "parameters": { - "container_id": { - "description": "Parameter ignored in this app", - "data_type": "numeric", - "order": 0 - }, - "start_time": { - "description": "Parameter ignored in this app", - "data_type": "numeric", - "order": 1 - }, - "end_time": { - "description": "Parameter ignored in this app", - "data_type": "numeric", - "order": 2 - }, - "container_count": { - "description": "Maximum number of events to query for", - "data_type": "numeric", - "default": 100, - "order": 3 - }, - "artifact_count": { - "description": "Parameter ignored in this app", - "data_type": "numeric", - "order": 4 - } - }, - "output": [], - "versions": "EQ(*)" - }, - { - "action": "run query", - "description": "Run a search query on the Splunk device. Please escape any quotes that are part of the query string", - "verbose": "By default, the widget for the "run query" action will show the host, time, and raw fields. If you would like to see specific fields parsed out, they can be listed in a comma-separated format in the "display" parameter.

Please keep in mind that Splunk does not always return all possible fields. Splunk may not return fields that are calculated or not present in the event.

To work around this you can force Splunk to return specific fields by using the "fields". By appending "| fields + *" to your query, Splunk will return every field. You can replace the asterisk with a comma-separated list of fields to only return specific fields.

Finally, some searches (such as those based on data models) can contain name-spaced fields. If a data model called "my_model" with a search "my_search" has a field "hash" then the field will be named "my_search.hash" and that is what must be used in the Splunk fields command and the display parameter. If using a non-global lookup file that is only accessible by a specific Splunk App, make sure to note the specific Splunk App in your asset configuration. The parse_only parameter, if True, it disables the expansion of search due to evaluation of sub-searches, time term expansion, lookups, tags, eventtypes, and sourcetype alias. This parameter is used for the validation of the Splunk query before fetching the results.

Learn more below:", - "type": "investigate", - "identifier": "run_query", - "read_only": true, - "parameters": { - "command": { - "description": "Beginning command (in Splunk Processing Language)", - "data_type": "string", - "order": 0, - "value_list": [ - "search", - "eval", - "savedsearch", - "stats", - "table", - "tstats" - ], - "default": "search" - }, - "query": { - "description": "Query to run (in Splunk Processing Language)", - "data_type": "string", - "order": 1, - "required": true, - "primary": true, - "contains": [ - "splunk query" - ] - }, - "display": { - "description": "Display fields (comma-separated)", - "data_type": "string", - "order": 2 - }, - "parse_only": { - "description": "Parse only", - "data_type": "boolean", - "order": 3, - "default": false - }, - "add_raw_field": { - "description": "Ingest _raw field data", - "data_type": "boolean", - "order": 4, - "default": true - }, - "attach_result": { - "description": "Attach result to the vault", - "data_type": "boolean", - "order": 5, - "default": false - }, - "start_time": { - "description": "Earliest time modifier", - "data_type": "string", - "order": 6 - }, - "end_time": { - "description": "Latest time modifier", - "data_type": "string", - "order": 7 - }, - "search_mode": { - "description": "Search mode", - "data_type": "string", - "value_list": [ - "fast", - "verbose", - "smart" - ], - "default": "smart", - "order": 8 - }, - "time_format": { - "description": "Custom timestamp format", - "data_type": "string", - "order": 9 - } - }, - "render": { - "type": "custom", - "width": 10, - "height": 5, - "view": "splunk_views.display_view", - "title": "Search Results" - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.attach_result", - "data_type": "boolean", - "example_values": [ - true, - false - ] - }, - { - "data_path": "action_result.parameter.command", - "data_type": "string", - "example_values": [ - "savedsearch" - ] - }, - { - "data_path": "action_result.parameter.display", - "data_type": "string", - "example_values": [ - "_time", - "index" - ] - }, - { - "data_path": "action_result.parameter.end_time", - "data_type": "string", - "example_values": [ - "-2d", - "2022-03-18T16:12:09.130+00:00" - ] - }, - { - "data_path": "action_result.parameter.parse_only", - "data_type": "boolean", - "example_values": [ - true, - false - ] - }, - { - "data_path": "action_result.parameter.query", - "data_type": "string", - "contains": [ - "splunk query" - ], - "example_values": [ - "\"Send to test\"" - ] - }, - { - "data_path": "action_result.parameter.search_mode", - "data_type": "string", - "example_values": [ - "smart" - ] - }, - { - "data_path": "action_result.parameter.start_time", - "data_type": "string", - "example_values": [ - "-2d", - "2022-03-18T16:12:07.130+00:00" - ] - }, - { - "data_path": "action_result.data.*._bkt", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._cd", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._indextime", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._key", - "data_type": "string", - "example_values": [ - "user" - ] - }, - { - "data_path": "action_result.data.*._kv", - "data_type": "string", - "example_values": [ - "1" - ] - }, - { - "data_path": "action_result.data.*._origtime", - "data_type": "string", - "example_values": [ - "1659398400" - ] - }, - { - "data_path": "action_result.data.*._raw", - "column_name": "Raw", - "column_order": 2, - "data_type": "string" - }, - { - "data_path": "action_result.data.*._serial", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._si", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._sourcetype", - "data_type": "string" - }, - { - "data_path": "action_result.data.*._subsecond", - "data_type": "string", - "example_values": [ - ".427" - ] - }, - { - "data_path": "action_result.data.*._time", - "column_name": "Time", - "column_order": 1, - "data_type": "string" - }, - { - "data_path": "action_result.data.*._value", - "data_type": "string", - "example_values": [ - "184" - ] - }, - { - "data_path": "action_result.data.*.a", - "data_type": "string", - "example_values": [ - "abc" - ] - }, - { - "data_path": "action_result.data.*.content.app", - "data_type": "string", - "example_values": [ - "search" - ] - }, - { - "data_path": "action_result.data.*.content.host", - "data_type": "string", - "example_values": [ - "test" - ] - }, - { - "data_path": "action_result.data.*.content.info", - "data_type": "string", - "example_values": [ - "granted" - ] - }, - { - "data_path": "action_result.data.*.content.search", - "data_type": "string", - "example_values": [ - "index = main" - ] - }, - { - "data_path": "action_result.data.*.content.search_type", - "data_type": "string", - "example_values": [ - "adhoc" - ] - }, - { - "data_path": "action_result.data.*.content.sid", - "data_type": "string", - "example_values": [ - "1621953839.25275" - ] - }, - { - "data_path": "action_result.data.*.content.source", - "data_type": "string", - "example_values": [ - "source" - ] - }, - { - "data_path": "action_result.data.*.content.sourcetype", - "data_type": "string", - "example_values": [ - "source" - ] - }, - { - "data_path": "action_result.data.*.content.uri", - "data_type": "string", - "example_values": [ - "/en-US/app/search/search?q=search%20index%3Dmain%20%7C%20head%2010&sid=1651356328.532450&display.page.search.mode=smart&dispatch.sample_ratio=1&workload_pool=&earliest=-24h%40h&latest=now" - ] - }, - { - "data_path": "action_result.data.*.content.view", - "data_type": "string", - "example_values": [ - "search" - ] - }, - { - "data_path": "action_result.data.*.count", - "data_type": "string", - "example_values": [ - "3058733" - ] - }, - { - "data_path": "action_result.data.*.count(host)", - "data_type": "string", - "example_values": [ - "28" - ] - }, - { - "data_path": "action_result.data.*.event", - "data_type": "string", - "example_values": [ - "{\"data\": {\"count\": 3, \"size\": 112, \"transform\": \"access_app_tracker\"}, \"version\": \"1.0\"}" - ] - }, - { - "data_path": "action_result.data.*.host", - "column_name": "Host", - "column_order": 0, - "data_type": "string", - "contains": [ - "host name" - ], - "example_values": [ - "10.1.67.187:8088" - ] - }, - { - "data_path": "action_result.data.*.index", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.is_Acceleration_Jobs", - "data_type": "string", - "example_values": [ - "0" - ] - }, - { - "data_path": "action_result.data.*.is_Adhoc_Jobs", - "data_type": "string", - "example_values": [ - "1" - ] - }, - { - "data_path": "action_result.data.*.is_Failed_Jobs", - "data_type": "string", - "example_values": [ - "0" - ] - }, - { - "data_path": "action_result.data.*.is_Realtime_Jobs", - "data_type": "string", - "example_values": [ - "0" - ] - }, - { - "data_path": "action_result.data.*.is_Scheduled_Jobs", - "data_type": "string", - "example_values": [ - "0" - ] - }, - { - "data_path": "action_result.data.*.is_Subsearch_Jobs", - "data_type": "string", - "example_values": [ - "0" - ] - }, - { - "data_path": "action_result.data.*.is_not_Acceleration_Jobs", - "data_type": "string", - "example_values": [ - "1" - ] - }, - { - "data_path": "action_result.data.*.is_not_Adhoc_Jobs", - "data_type": "string", - "example_values": [ - "0" - ] - }, - { - "data_path": "action_result.data.*.is_not_Failed_Jobs", - "data_type": "string", - "example_values": [ - "1" - ] - }, - { - "data_path": "action_result.data.*.is_not_Realtime_Jobs", - "data_type": "string", - "example_values": [ - "1" - ] - }, - { - "data_path": "action_result.data.*.is_not_Scheduled_Jobs", - "data_type": "string", - "example_values": [ - "1" - ] - }, - { - "data_path": "action_result.data.*.is_not_Subsearch_Jobs", - "data_type": "string", - "example_values": [ - "1" - ] - }, - { - "data_path": "action_result.data.*.linecount", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.source", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.sourcetype", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.spent", - "data_type": "string", - "example_values": [ - "223" - ] - }, - { - "data_path": "action_result.data.*.splunk_server", - "data_type": "string", - "contains": [ - "host name" - ] - }, - { - "data_path": "action_result.data.*.user", - "data_type": "string", - "example_values": [ - "admin" - ] - }, - { - "data_path": "action_result.data.*.values(source)", - "data_type": "string", - "example_values": [ - "/opt/splunk/var/log/splunk/scheduler.log" - ] - }, - { - "data_path": "action_result.summary.sid", - "data_type": "string", - "example_values": [ - "1612177958.977510" - ] - }, - { - "data_path": "action_result.summary.total_events", - "data_type": "numeric", - "example_values": [ - 2 - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Sid: 1612177958.977510, Total events: 2" - ] - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "action_result.parameter.add_raw_field", - "data_type": "boolean" - }, - { - "data_path": "action_result.parameter.time_format", - "data_type": "string" - } - ], - "versions": "EQ(*)" - }, - { - "action": "update event", - "description": "Update a notable event", - "verbose": "The event_ids parameter takes a single event_id (which has the format: 68E08B8B-A853-3A20-9768-231C97B7EE76@@notable@@a4bd78810ae8e03e285e552fac0ddb23) or an adaptive response SID + RID combo (which has the format: scheduler__admin__SplunkEnterpriseSecuritySuite__RMD515d4671130158e57_at_1532441220_4982+0).

NOTE: This action only works with a notable event from Splunk ES.

Second Note: The status parameter takes a string value, but custom status values are unique to installation and not available at app creation. The integer_status parameter takes a positive integer denoting the custom value desired. This integer must be determined by the customer on-site. If set it will override status.", - "type": "generic", - "identifier": "update_event", - "read_only": false, - "parameters": { - "event_ids": { - "description": "Event ID to update", - "data_type": "string", - "contains": [ - "splunk notable event id" - ], - "required": true, - "order": 0, - "primary": true - }, - "owner": { - "description": "New owner for the event", - "data_type": "string", - "order": 1 - }, - "status": { - "description": "New status for the event", - "data_type": "string", - "value_list": [ - "", - "unassigned", - "new", - "in progress", - "pending", - "resolved", - "closed" - ], - "order": 2 - }, - "integer_status": { - "description": "Integer representing custom status value", - "data_type": "numeric", - "order": 3 - }, - "urgency": { - "description": "New urgency for the event", - "data_type": "string", - "value_list": [ - "", - "informational", - "low", - "medium", - "high", - "critical" - ], - "order": 4 - }, - "comment": { - "description": "New comment for the event", - "data_type": "string", - "order": 5 - }, - "disposition": { - "description": "New disposition field", - "data_type": "string", - "value_list": [ - "", - "Unassigned", - "True Positive - Suspicious Activity", - "Benign Positive - Suspicious But Expected", - "False Positive - Incorrect Analytic Logic", - "False Positive - Inaccurate Data", - "Undetermined", - "Other" - ], - "order": 6 - }, - "integer_disposition": { - "description": "Integer representing custom disposition value", - "data_type": "numeric", - "order": 7 - }, - "wait_for_confirmation": { - "description": "Validate event_ids", - "data_type": "boolean", - "default": false, - "order": 8 - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failed" - ], - "column_name": "Status", - "column_order": 0 - }, - { - "data_path": "action_result.parameter.comment", - "data_type": "string", - "example_values": [ - "test comment" - ] - }, - { - "data_path": "action_result.parameter.disposition", - "data_type": "string", - "example_values": [ - "unassigned" - ] - }, - { - "data_path": "action_result.parameter.event_ids", - "data_type": "string", - "contains": [ - "splunk notable event id" - ], - "example_values": [ - "1542751027.136723+0" - ] - }, - { - "data_path": "action_result.parameter.integer_disposition", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "action_result.parameter.integer_status", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "action_result.parameter.owner", - "data_type": "string", - "example_values": [ - "test" - ] - }, - { - "data_path": "action_result.parameter.status", - "data_type": "string", - "example_values": [ - "new" - ] - }, - { - "data_path": "action_result.parameter.urgency", - "data_type": "string", - "example_values": [ - "low" - ] - }, - { - "data_path": "action_result.parameter.wait_for_confirmation", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.failure_count", - "data_type": "numeric", - "example_values": [ - 0 - ] - }, - { - "data_path": "action_result.data.*.message", - "data_type": "string", - "example_values": [ - "1 event updated successfully" - ] - }, - { - "data_path": "action_result.data.*.success", - "data_type": "boolean", - "example_values": [ - false, - true - ] - }, - { - "data_path": "action_result.data.*.success_count", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "action_result.summary.sid", - "data_type": "string", - "example_values": [ - "1612177958.977510" - ] - }, - { - "data_path": "action_result.summary.updated_event_id", - "data_type": "string", - "example_values": [ - "2CF264EE-6016-4F6A-BCC3-4B7251E113F7@@notable@@035142b19c09ab645c6bbfb847e866f4" - ] - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Updated event id: 2CF264EE-6016-4F6A-BCC3-4B7251E113F7@@notable@@035142b19c09ab645c6bbfb847e866f4" - ], - "column_name": "Message", - "column_order": 1 - }, - { - "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(*)", - "render": { - "width": 12, - "title": "Update Event", - "type": "table", - "height": 5 - } - }, - { - "action": "post data", - "description": "Post data to Splunk", - "verbose": "This action creates an event on Splunk with the data included in the data parameter. If not specified the parameters will default to the following:
  • host - The IP of the Splunk SOAR instance running the action.
  • index - The default index configured on the Splunk instance.
  • source - "Phantom".
  • source_type - "Automation/Orchestration Platform".
", - "type": "generic", - "identifier": "post_data", - "read_only": false, - "parameters": { - "data": { - "description": "Data to post", - "data_type": "string", - "required": true, - "order": 0 - }, - "host": { - "description": "Host for event", - "data_type": "string", - "contains": [ - "ip", - "host name" - ], - "primary": true, - "order": 1 - }, - "index": { - "description": "Index to send event to", - "data_type": "string", - "order": 4 - }, - "source": { - "description": "Source for event", - "data_type": "string", - "default": "Phantom", - "order": 2 - }, - "source_type": { - "description": "Type of source for event", - "data_type": "string", - "default": "Automation/Orchestration Platform", - "order": 3 - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "column_name": "Status", - "column_order": 0, - "example_values": [ - "success", - "failed" - ] - }, - { - "data_path": "action_result.parameter.data", - "data_type": "string", - "example_values": [ - "test_data" - ] - }, - { - "data_path": "action_result.parameter.host", - "data_type": "string", - "example_values": [ - "test_host" - ], - "contains": [ - "ip", - "host name" - ] - }, - { - "data_path": "action_result.parameter.index", - "data_type": "string", - "example_values": [ - "main" - ] - }, - { - "data_path": "action_result.parameter.source", - "data_type": "string", - "example_values": [ - "test" - ] - }, - { - "data_path": "action_result.parameter.source_type", - "data_type": "string", - "example_values": [ - "pb" - ] - }, - { - "data_path": "action_result.data", - "data_type": "string" - }, - { - "data_path": "action_result.summary", - "data_type": "string" - }, - { - "data_path": "action_result.message", - "data_type": "string", - "example_values": [ - "Successfully posted the data" - ], - "column_name": "Message", - "column_order": 1 - }, - { - "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(*)", - "render": { - "width": 12, - "title": "Post Data", - "type": "table", - "height": 5 - } - } - ], - "pip39_dependencies": { - "wheel": [ - { - "module": "deprecation", - "input_file": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl" - }, - { - "module": "packaging", - "input_file": "wheels/py3/packaging-25.0-py3-none-any.whl" - }, - { - "module": "splunk_sdk", - "input_file": "wheels/py3/splunk_sdk-2.1.1-py3-none-any.whl" - } - ] - }, - "pip313_dependencies": { - "wheel": [ - { - "module": "deprecation", - "input_file": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl" - }, - { - "module": "packaging", - "input_file": "wheels/py3/packaging-25.0-py3-none-any.whl" - }, - { - "module": "splunk_sdk", - "input_file": "wheels/py3/splunk_sdk-2.1.1-py3-none-any.whl" - } - ] - } -} diff --git a/splunk_connector.py b/splunk_connector.py deleted file mode 100644 index 47dab3c..0000000 --- a/splunk_connector.py +++ /dev/null @@ -1,1542 +0,0 @@ -# File: splunk_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 hashlib -import json -import os -import re -import ssl -import sys -import tempfile -import time -import traceback -from datetime import datetime, timezone -from io import BytesIO -from typing import Optional -from urllib.error import HTTPError as UrllibHTTPError, URLError -from urllib.request import ProxyHandler, Request, build_opener, install_opener, urlopen -from zoneinfo import ZoneInfo - -import phantom.app as phantom -import phantom.rules as soar_vault -import requests -import splunklib.binding as splunk_binding -import splunklib.client as splunk_client -import splunklib.results as splunk_results -import xmltodict -from bs4 import BeautifulSoup -from bs4.dammit import UnicodeDammit -from dateutil.parser import ParserError, parse as dateutil_parse -from phantom.base_connector import BaseConnector -from phantom.vault import Vault -from splunklib.binding import HTTPError - -import splunk_consts as consts - - -class RetVal(tuple): - def __new__(cls, val1, val2=None): - return tuple.__new__(RetVal, (val1, val2)) - - -class SplunkConnector(BaseConnector): - ACTION_ID_POST_DATA = "post_data" - ACTION_ID_RUN_QUERY = "run_query" - ACTION_ID_UPDATE_EVENT = "update_event" - ACTION_ID_GET_HOST_EVENTS = "get_host_events" - - def __init__(self): - # Call the BaseConnectors init first - super().__init__() - self._service = None - self._base_url = None - self.splunk_server = None - self.retry_count = None - self.port = None - self.max_container = None - self._splunk_status_dict = None - self._splunk_disposition_dict = None - self.container_update_state = None - self.remove_empty_cef = None - self.sleeptime_in_requests = None - - def _get_error_message_from_exception(self, e): - """This method is used to get appropriate error message from the exception. - :param e: Exception object - :return: error message - """ - error_code = None - error_message = consts.SPLUNK_ERR_MESSAGE_UNAVAILABLE - - self.error_print(f"Traceback: {traceback.format_stack()}") - try: - if hasattr(e, "args"): - if len(e.args) > 1: - error_code = e.args[0] - error_message = e.args[1] - elif len(e.args) == 1: - error_message = e.args[0] - else: - error_message = consts.SPLUNK_ERR_MESSAGE_UNAVAILABLE - - if error_message == consts.SPLUNK_ERR_MESSAGE_UNAVAILABLE: - error_message = str(e).strip().replace("'", "").replace('"', "").replace("\n", "").replace("\r", "") - if len(error_message) > 500: - error_message = f"{error_message[:500]} - truncated" - error_message = f"{error_message} ({sys.exc_info()[-1].tb_lineno})" - except Exception as e: - self._dump_error_log(e, "Error occurred while fetching exception information") - - if not error_code: - error_message = f"Error Message: {error_message}" - else: - error_message = f"Error Code: {error_code}. Error Message: {error_message}" - - return error_message - - def initialize(self): - config = self.get_config() - - self.splunk_server = config[phantom.APP_JSON_DEVICE] - - self._username = config.get(phantom.APP_JSON_USERNAME) - self._password = config.get(phantom.APP_JSON_PASSWORD) - self._api_token = config.get(consts.SPLUNK_JSON_API_KEY) - - self._base_url = f"https://{self.splunk_server}:{config.get(phantom.APP_JSON_PORT, 8089)}/" - self._state = self.load_state() - if not isinstance(self._state, dict): - self.debug_print("State file format is not valid") - self._state = {} - self.save_state(self._state) - self.debug_print("Recreated the state file with current app_version") - self._state = self.load_state() - if self._state is None: - self.debug_print("Please check the owner, owner group, and the permissions of the state file") - self.debug_print( - "The Splunk SOAR user should have correct access rights and ownership for the \ - corresponding state file (refer readme file for more information)" - ) - return phantom.APP_ERROR - - self._proxy = {} - - # Either username and password or API token must be provided - if not self._api_token and (not self._username or not self._password): - return self.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_REQUIRED_CONFIG_PARAMS) - - if "http_proxy" in os.environ: - self._proxy["http"] = os.environ.get("http_proxy") - elif "HTTP_PROXY" in os.environ: - self._proxy["http"] = os.environ.get("HTTP_PROXY") - - if "https_proxy" in os.environ: - self._proxy["https"] = os.environ.get("https_proxy") - elif "HTTPS_PROXY" in os.environ: - self._proxy["https"] = os.environ.get("HTTPS_PROXY") - - self._container_name_prefix = config.get("container_name_prefix", "") - container_name_values = config.get("container_name_values") - if container_name_values: - self._container_name_values = [x.strip() for x in container_name_values.split(",")] - else: - self._container_name_values = [] - - # Validate retry_count - ret_val, self.retry_count = self._validate_integer(self, config.get("retry_count", 3), consts.SPLUNK_RETRY_COUNT_KEY) - if phantom.is_fail(ret_val): - return self.get_status() - - # Validate port - ret_val, self.port = self._validate_integer(self, config.get("port", 8089), consts.SPLUNK_PORT_KEY) - if phantom.is_fail(ret_val): - return self.get_status() - - # Validate max_container - ret_val, self.max_container = self._validate_integer(self, config.get("max_container", 100), consts.SPLUNK_MAX_CONTAINER_KEY, True) - if phantom.is_fail(ret_val): - return self.get_status() - - # Validate container_update_state - ret_val, self.container_update_state = self._validate_integer( - self, config.get("container_update_state", 100), consts.SPLUNK_CONTAINER_UPDATE_STATE_KEY - ) - if phantom.is_fail(ret_val): - return self.get_status() - - # Validate splunk_job_timeout - ret_val, self.splunk_job_timeout = self._validate_integer(self, config.get("splunk_job_timeout"), consts.SPLUNK_JOB_TIMEOUT_KEY) - if phantom.is_fail(ret_val): - return self.get_status() - - # Validate sleeptime_in_requests - ret_val, self.sleeptime_in_requests = self._validate_integer( - self, config.get("sleeptime_in_requests", 1), consts.SPLUNK_SLEEPTIME_IN_REQUESTS_KEY - ) - if phantom.is_fail(ret_val): - return self.get_status() - - # Validate if user has entered more than 120 seconds - if self.sleeptime_in_requests > 120: - return self.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_INVALID_SLEEP_TIME.format(param=consts.SPLUNK_SLEEPTIME_IN_REQUESTS_KEY)) - - self.remove_empty_cef = config.get("remove_empty_cef", False) - - return phantom.APP_SUCCESS - - def finalize(self): - if self._state is not None: - self.save_state(self._state) - return phantom.APP_SUCCESS - - def _dump_error_log(self, error, message="Exception occurred."): - self.error_print(message, dump_object=error) - - def request(self, url, message, **kwargs): - """Splunk SDK Proxy handler""" - method = message["method"].lower() - config = self.get_config() - data = message.get("body", "") if method == "post" else None - headers = dict(message.get("headers", [])) - req = Request(url, data, headers) - try: - response = urlopen(req) - self.debug_print(response) - except UrllibHTTPError: - self.save_progress("Check the proxy settings") - pass # Propagate HTTP errors via the returned response message - except URLError: - # If running Python 2.7.9+, disable SSL certificate validation and try again - if sys.version_info >= (2, 7, 9) and not config[phantom.APP_JSON_VERIFY]: - response = urlopen(req, context=ssl._create_unverified_context()) # nosemgrep - else: - raise - return {"status": response.code, "reason": response.msg, "headers": response.getheaders(), "body": BytesIO(response.read())} - - def handler(self, proxy): - """Splunk SDK Proxy Request Handler""" - proxy_handler = ProxyHandler({"http": proxy, "https": proxy}) - opener = build_opener(proxy_handler) - install_opener(opener) - return self.request - - def _connect(self, action_result): - if self._service is not None: - return phantom.APP_SUCCESS - - config = self.get_config() - - kwargs_config_flags = { - "host": self.splunk_server, - "port": self.port, - "username": self._username, - "password": self._password, - "owner": config.get("splunk_owner", None), - "app": config.get("splunk_app", None), - } - - # token-based authentication - if self._api_token: - self.save_progress("Using token-based authentication") - kwargs_config_flags["splunkToken"] = self._api_token - kwargs_config_flags.pop(phantom.APP_JSON_USERNAME) - kwargs_config_flags.pop(phantom.APP_JSON_PASSWORD) - - self.save_progress(phantom.APP_PROG_CONNECTING_TO_ELLIPSES, self.splunk_server) - - proxy_param = None - - if self._proxy.get("http", None) is not None: - proxy_param = self._proxy.get("http") - if self._proxy.get("https", None) is not None: - proxy_param = self._proxy.get("https") - - no_proxy_host = os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")) - if self.splunk_server in no_proxy_host.split(","): - pass - elif self._api_token: - if any(proxy_var in os.environ for proxy_var in ["HTTPS_PROXY", "https_proxy"]): - self.save_progress("[-] Engaging Proxy") - else: - if any(proxy_var in os.environ for proxy_var in ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"]): - self.save_progress("[-] Engaging Proxy") - - try: - if proxy_param: - self._service = splunk_client.connect(handler=self.handler(proxy_param), **kwargs_config_flags) - else: - self._service = splunk_client.connect(**kwargs_config_flags) - except splunk_binding.HTTPError as e: - error_text = self._get_error_message_from_exception(e) - self._dump_error_log(e, "Error occurred while connecting to the Splunk server.") - if "405 Method Not Allowed" in error_text: - return action_result.set_status(phantom.APP_ERROR, "Error occurred while connecting to the Splunk server") - else: - return action_result.set_status( - phantom.APP_ERROR, f"Error occurred while connecting to the Splunk server. Details: {error_text}" - ) - except Exception as e: - self._dump_error_log(e) - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg=consts.SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text) - - # Must return success if we want handle_action to be called - return phantom.APP_SUCCESS - - def _validate_integer(self, action_result, parameter, key, allow_zero=False): - if parameter is not None: - try: - if not float(parameter).is_integer(): - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_INVALID_INTEGER.format(param=key)), None - - parameter = int(parameter) - except Exception: - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_INVALID_INTEGER.format(param=key)), None - - if parameter < 0: - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_NON_NEGATIVE_INTEGER.format(param=key)), None - if not allow_zero and parameter == 0: - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_INVALID_PARAM.format(param=key)), None - - return phantom.APP_SUCCESS, parameter - - def _make_rest_call_retry(self, action_result, endpoint, data, params=None, method=requests.post): - if params is None: - params = {} - - RETRY_LIMIT = self.retry_count - - for _ in range(0, RETRY_LIMIT): - ret_val, resp_data = self._make_rest_call(action_result, endpoint, data, params, method) - - if not phantom.is_fail(ret_val): - break - return ret_val, resp_data - - def _make_rest_call(self, action_result, endpoint, data, params=None, method=requests.post): - if params is None: - params = {} - - config = self.get_config() - url = f"{self._base_url}services/{endpoint}" - self.debug_print(f"Making REST call to {url}") - - auth, auth_headers = None, None - - if self._api_token: - # Splunk token-based authentication - self.debug_print("Using token-based authentication") - auth_headers = {"Authorization": f"Bearer {self._api_token}"} - else: - # Splunk username/password based authentication - auth = (self._username, self._password) - try: - r = method( - url, - data=data, - params=params, - auth=auth, - headers=auth_headers, - verify=config[phantom.APP_JSON_VERIFY], - timeout=consts.SPLUNK_DEFAULT_REQUEST_TIMEOUT, - ) - except Exception as e: - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg=consts.SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text), None - - return self._process_response(r, action_result) - - def _process_response(self, r, action_result): - """ - Process API response. - - :param r: response object - :param action_result: object of Action Result - :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message) - """ - # store the r_text in debug data, it will get dumped in the logs if an error occurs - if hasattr(action_result, "add_debug_data"): - if r is not None: - 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}) - else: - action_result.add_debug_data({"r_text": "r is None"}) - - # Process each 'Content-Type' of response separately - # Process a json response - if "json" in r.headers.get("Content-Type", ""): - return self._process_json_response(r, action_result) - - # Process an HTML response, Do this no matter what the api talks. - # There is a high chance of a PROXY in between Splunk SOAR and the rest of - # world, in case of errors, PROXY's return HTML, this function parses - # the error and adds it to the action_result. - if "html" in r.headers.get("Content-Type", ""): - return self._process_html_response(r, action_result) - - if "xml" in r.headers.get("Content-Type", ""): - return self._process_xml_response(r, action_result) - - # it's not content-type that is to be parsed, handle an empty response - if not r.text: - return self._process_empty_response(r, action_result) - - # everything else is actually an error at this point - error_text = r.text.replace("{", "{{").replace("}", "}}") - message = f"Can't process response from server. Status Code: {r.status_code} Data from server: {error_text}" - - return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) - - def _process_empty_response(self, response, action_result): - """ - Process empty response. - - :param response: response object - :param action_result: object of Action Result - :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message) - """ - if response.status_code == 200 or response.status_code == 204: - return RetVal(phantom.APP_SUCCESS, {}) - - return RetVal(action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_EMPTY_RESPONSE.format(code=response.status_code)), None) - - 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) - - error_type = resp_json.get("response", {}).get("messages", {}).get("msg", {}).get("@type") - error_message = resp_json.get("response", {}).get("messages", {}).get("msg", {}).get("#text") - - if error_type or error_message: - error = f"ErrorType: {error_type} ErrorMessage: {error_message}" - else: - error = "Unable to parse xml response" - - message = f"Error from server. Status Code: {r.status_code} Data from server: {error}" - - return RetVal(action_result.set_status(phantom.APP_ERROR, message), resp_json) - - def _process_html_response(self, response, action_result): - """ - Process html response. - - :param response: response object - :param action_result: object of Action Result - :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message) - """ - # An html response, treat it like an error - status_code = response.status_code - - try: - soup = BeautifulSoup(response.text, "html.parser") - # Remove the script, style, footer and navigation part from the HTML message - 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 e: - error_message = self._get_error_message_from_exception(e) - error_text = consts.SPLUNK_ERR_UNABLE_TO_PARSE_HTML_RESPONSE.format(error=error_message) - - if not error_text: - error_text = "Empty response and no information received" - message = f"Status Code: {status_code}. Data from server:\n{error_text}\n" - - message = message.replace("{", "{{").replace("}", "}}") - - if len(message) > 500: - message = "Error occurred while connecting to the Splunk server" - - return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) - - def _process_json_response(self, r, action_result): - """ - Process json response. - - :param r: response object - :param action_result: object of Action Result - :return: status phantom.APP_ERROR/phantom.APP_SUCCESS(along with appropriate message) - """ - status_code = r.status_code - # Try a json parse - try: - resp_json = r.json() - except Exception as e: - error_message = self._get_error_message_from_exception(e) - return RetVal( - action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_UNABLE_TO_PARSE_JSON_RESPONSE.format(error=error_message)), None - ) - - # Please specify the status codes here - if 200 <= r.status_code < 399: - return RetVal(phantom.APP_SUCCESS, resp_json) - - if isinstance(resp_json, str): - message = f"Error from server. Details: {resp_json}" - elif resp_json.get("error") or resp_json.get("error_description"): - error = resp_json.get("error", "Unavailable") - error_details = resp_json.get("error_description", "Unavailable") - message = f"Error from server. Status Code: {status_code}. Error: {error}. Error Details: {error_details}" - elif resp_json.get("messages"): - if resp_json["messages"]: - error_type = resp_json["messages"][0].get("type") - error_message = resp_json["messages"][0].get("text") - - if error_type or error_message: - error = f"ErrorType: {error_type} ErrorMessage: {error_message}" - else: - error = "Unable to parse json response" - else: - error = "Unable to parse json response" - - message = f"Error from server. Status Code: {r.status_code} Data from server: {error}" - else: - # You should process the error returned in the json - error_text = r.text.replace("{", "{{").replace("}", "}}") - message = f"Error from server. Status Code: {status_code}. Data from server: {error_text}" - - return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) - - def _get_server_version(self, action_result): - endpoint = "authentication/users?output_mode=json" - ret_val, resp_data = self._make_rest_call_retry(action_result, endpoint, {}, method=requests.get) - - if phantom.is_fail(ret_val): - return "FAILURE" - - splunk_version = resp_data.get("generator", {}).get("version") - - if not splunk_version: - splunk_version = "UNKNOWN" - - return splunk_version - - def _check_for_es(self, action_result): - endpoint = "apps/local/SplunkEnterpriseSecuritySuite" - ret_val, resp_data = self._make_rest_call_retry(action_result, endpoint, {}, method=requests.get) - if phantom.is_fail(ret_val) or not resp_data: - return False - return True - - def _resolve_event_id(self, sidandrid, action_result, kwargs_create=dict()): - """Query the splunk instance using the SID+RID of the notable to find the notable ID""" - - self.send_progress(f"Running search_query: {consts.SPLUNK_RID_SID_NOTABLE_QUERY}") - - result = self._return_first_row_from_query(consts.SPLUNK_RID_SID_NOTABLE_QUERY.format(sidandrid), action_result) - - if phantom.is_fail(result): - return RetVal(action_result.get_status(), None) - - if "event_id" in result: - return RetVal(phantom.APP_SUCCESS, result["event_id"]) - - return RetVal(action_result.set_status(phantom.APP_ERROR, "could not find event_id of splunk event"), None) - - def _return_first_row_from_query(self, search_query, action_result, kwargs_create=dict()): - """Function that executes the query on splunk""" - - self.debug_print("Search Query:", search_query) - RETRY_LIMIT = self.retry_count - - if phantom.is_fail(self._connect(action_result)): - return action_result.get_status() - - # Validate the search query - for attempt_count in range(0, RETRY_LIMIT): - try: - self._service.parse(search_query, parse_only=True) - break - except HTTPError as e: - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg=consts.SPLUNK_ERR_INVALID_QUERY, error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text, query=search_query) - except Exception as e: - if attempt_count == RETRY_LIMIT - 1: - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg=consts.SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text) - - self.debug_print(consts.SPLUNK_PROG_CREATED_QUERY.format(query=search_query)) - - # Creating search job - self.save_progress(consts.SPLUNK_PROG_CREATING_SEARCH_JOB) - - # Set any search creation flags here - kwargs_create.update({"exec_mode": "normal"}) - - self.debug_print("kwargs_create", kwargs_create) - - # Create the job - for search_attempt_count in range(0, RETRY_LIMIT): - # Create the job - is_created_successfully, job = self._create_splunk_job( - action_result=action_result, retry_limit=RETRY_LIMIT, search_query=search_query, kwargs_create=kwargs_create - ) - if phantom.is_fail(is_created_successfully): - return phantom.APP_ERROR - - while True: - is_job_successful: bool = self._wait_until_splunk_job_results_are_ready(action_result, job, RETRY_LIMIT) - if phantom.is_fail(is_job_successful): - return phantom.APP_ERROR - - stats = self._get_stats(job) - - status = ("Progress: %(progress)03.1f%% %(scan_count)d scanned %(event_count)d matched %(result_count)d results") % stats # noqa: UP031 - self.send_progress(status) - if stats["is_done"] == "1": - break - time.sleep(self.sleeptime_in_requests) - self.send_progress("Parsing results...") - - try: - results = splunk_results.JSONResultsReader(job.results(count=0, output_mode="json")) - except Exception as e: - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg="Error retrieving results", error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text) - - for result in results: - if isinstance(result, dict): - return result - time.sleep(20) - - return action_result.set_status(phantom.APP_ERROR) - - def _post_data(self, param): - action_result = self.add_action_result(phantom.ActionResult(dict(param))) - - host = param.get(consts.SPLUNK_JSON_HOST) - index = param.get(consts.SPLUNK_JSON_INDEX) - source = param.get(consts.SPLUNK_JSON_SOURCE, consts.SPLUNK_DEFAULT_SOURCE) - source_type = param.get(consts.SPLUNK_JSON_SOURCE_TYPE, consts.SPLUNK_DEFAULT_SOURCE_TYPE) - try: - post_data = UnicodeDammit(param[consts.SPLUNK_JSON_DATA]).unicode_markup.encode("utf-8") - except Exception as e: - self._dump_error_log(e, "Error while encoding data.") - - get_params = {"source": source, "sourcetype": source_type} - - if host: - get_params["host"] = host - if index: - get_params["index"] = index - - endpoint = "receivers/simple" - ret_val, _resp_data = self._make_rest_call_retry(action_result, endpoint, post_data, params=get_params) - - if phantom.is_fail(ret_val): - return ret_val - - return action_result.set_status(phantom.APP_SUCCESS, "Successfully posted the data") - - def _get_stats(self, job): - stats = { - "is_done": job["isDone"] if ("isDone" in job) else "Unknown status", - "progress": ( - float(job["doneProgress"]) * 100 - if ("doneProgress" in job) - else consts.SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Done progress") - ), - "scan_count": ( - int(job["scanCount"]) if ("scanCount" in job) else consts.SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Scan count") - ), - "event_count": ( - int(job["eventCount"]) if ("eventCount" in job) else consts.SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Event count") - ), - "result_count": ( - int(job["resultCount"]) if ("resultCount" in job) else consts.SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Result count") - ), - } - - return stats - - def _set_splunk_status_dict(self, action_result, type): - splunk_dict = {} - - endpoint = "alerts/reviewstatuses?count=-1&output_mode=json" - ret_val, resp_data = self._make_rest_call_retry(action_result, endpoint, {}, method=requests.get) - - if phantom.is_fail(ret_val) or not resp_data: - return splunk_dict - - entry = resp_data.get("entry") - - if not entry: - return splunk_dict - - for data in entry: - object_id = data.get("name").split(":")[-1] - object_name = data.get("content", {}).get("label") - is_enabled = str(data.get("content", {}).get("disabled")) == "0" - is_allowed_type = data.get("content", {}).get("status_type") == type - if object_id and object_id.isdigit() and object_name and is_enabled and is_allowed_type: - if type == "notable": - object_name = object_name.lower() - splunk_dict[object_name] = int(object_id) - - return splunk_dict - - def _update_event(self, param): - action_result = self.add_action_result(phantom.ActionResult(dict(param))) - - if not self._check_for_es(action_result): - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_NOT_ES) - - owner = param.get(consts.SPLUNK_JSON_OWNER) - ids = param.get(consts.SPLUNK_JSON_EVENT_IDS) - status = param.get(consts.SPLUNK_JSON_STATUS) - - ret_val, integer_status = self._validate_integer( - action_result, param.get("integer_status"), consts.SPLUNK_INT_STATUS_KEY, allow_zero=True - ) - - if phantom.is_fail(ret_val): - return action_result.get_status() - - ret_val, integer_disposition = self._validate_integer( - action_result, param.get("integer_disposition"), consts.SPLUNK_INT_DISPOSITION_KEY, allow_zero=True - ) - - if phantom.is_fail(ret_val): - return action_result.get_status() - - comment = param.get(consts.SPLUNK_JSON_COMMENT) - urgency = param.get(consts.SPLUNK_JSON_URGENCY) - wait_for_confirmation = param.get("wait_for_confirmation", False) - disposition = param.get("disposition", "") - regexp = re.compile(r"\+\d*(\.\d+)?[\"$]") - if regexp.search(json.dumps(ids)): - self.send_progress("Interpreting the event ID as an SID + RID combo; querying for the actual event_id...") - self.debug_print("Interpreting the event ID as an SID + RID combo; querying for the actual event_id...") - ret_val, event_id = self._resolve_event_id(ids, action_result, param) - if phantom.is_fail(ret_val): - return action_result.set_status(phantom.APP_ERROR, "Unable to find underlying event_id from SID + RID combo") - ids = event_id - - if not any([comment, status, urgency, owner, disposition]) and integer_status is None and integer_disposition is None: - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_NEED_PARAM) - - if status or integer_status is not None: - self._splunk_status_dict = self._set_splunk_status_dict(action_result, "notable") - if not self._splunk_status_dict: - return action_result.set_status(phantom.APP_ERROR, "Error occurred while fetching Splunk event status") - - if disposition or integer_disposition is not None: - self._splunk_disposition_dict = self._set_splunk_status_dict(action_result, "disposition") - if not self._splunk_disposition_dict: - return action_result.set_status(phantom.APP_ERROR, "Error occurred while fetching Splunk event disposition") - - self.debug_print("Attempting to create a connection") - - # 1. Connect and validate whether the given Event IDs are valid or not - if phantom.is_fail(self._connect(action_result)): - return action_result.get_status() - - self.debug_print("Connection established.") - - if wait_for_confirmation: - self.debug_print("Searching for the event ID.") - search_query = f"search `notable_by_id({ids})`" - ret_val = self._run_query(search_query, action_result) - - if phantom.is_fail(ret_val): - return action_result.set_status( - phantom.APP_ERROR, f"Error occurred while validating the provided event ID. Error: {action_result.get_message()}" - ) - - if int(action_result.get_data_size()) <= 0: - return action_result.set_status(phantom.APP_ERROR, "Please provide a valid event ID") - - self.debug_print("Event ID found") - - # 2. Re-initialize the action_result object for update event - self.remove_action_result(action_result) - action_result = self.add_action_result(phantom.ActionResult(dict(param))) - - # 3. Update the provided Events ID - request_body = {"ruleUIDs": ids} - - if integer_status is not None: - if int(integer_status) not in list(self._splunk_status_dict.values()): - return action_result.set_status( - phantom.APP_ERROR, - "Please provide a valid value in 'integer_status' action\ - parameter. Valid values: {}".format(", ".join(map(str, list(self._splunk_status_dict.values())))), - ) - request_body["status"] = str(integer_status) - elif status: - if status not in self._splunk_status_dict: - if not status.isdigit(): - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_BAD_STATUS) - request_body["status"] = status - else: - request_body["status"] = self._splunk_status_dict[status] - - if integer_disposition is not None: - if int(integer_disposition) not in self._splunk_disposition_dict.values(): - self.debug_print(f"int disposition: {self._splunk_disposition_dict}") - return action_result.set_status( - phantom.APP_ERROR, - "Please provide a valid value in 'integer_disposition' action\ - parameter. Valid values: {}".format(", ".join(map(str, self._splunk_disposition_dict.values()))), - ) - request_body["disposition"] = consts.SPLUNK_DISPOSITION_QUERY_FORMAT.format(integer_disposition) - elif disposition: - if disposition not in self._splunk_disposition_dict: - if not disposition.isdigit(): - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_BAD_DISPOSITION) - request_body["disposition"] = consts.SPLUNK_DISPOSITION_QUERY_FORMAT.format(disposition) - else: - request_body["disposition"] = consts.SPLUNK_DISPOSITION_QUERY_FORMAT.format(self._splunk_disposition_dict[disposition]) - - param_mapping = {"urgency": urgency, "comment": comment, "newOwner": owner} - - request_body.update({k: v for k, v in param_mapping.items() if v}) - - self.debug_print("Updating the event") - - endpoint = "notable_update" - ret_val, resp_data = self._make_rest_call_retry(action_result, endpoint, request_body) - - if not ret_val: - return ret_val - - if resp_data and "success" in resp_data and not resp_data.get("success"): - msg = resp_data.get("message") - return action_result.set_status(phantom.APP_ERROR, msg if msg else "Unable to update the notable event") - - action_result.add_data(resp_data) - action_result.update_summary({consts.SPLUNK_JSON_UPDATED_EVENT_ID: ids}) - if wait_for_confirmation: - return action_result.set_status(phantom.APP_SUCCESS) - return action_result.set_status( - phantom.APP_SUCCESS, - f"Updated Event ID: {ids}. The event_id has not been verified. \ - Please confirm that the provided event_id corresponds to an actual notable event", - ) - - def _get_host_events(self, param): - """Executes the query to get events pertaining to a host - Gets the events for a host for the last 'N' number of days - """ - self.save_progress(f"In action handler for: {self.get_action_identifier()}") - - action_result = self.add_action_result(phantom.ActionResult(dict(param))) - - # Connect - if phantom.is_fail(self._connect(action_result)): - return action_result.get_status() - - ip_hostname = param[phantom.APP_JSON_IP_HOSTNAME] - - # Validate last_n_days - ret_val, last_n_days = self._validate_integer(action_result, param.get(consts.SPLUNK_JSON_LAST_N_DAYS), consts.SPLUNK_LAST_N_DAYS_KEY) - if phantom.is_fail(ret_val): - return action_result.get_status() - - search_query = 'search host="{}"{}'.format(ip_hostname, f" earliest=-{last_n_days}d" if last_n_days else "") - - self.debug_print(f"search_query: {search_query}") - return self._run_query(search_query, action_result) - - def _get_fips_enabled(self): - try: - from phantom_common.install_info import is_fips_enabled - except ImportError: - return False - - fips_enabled = is_fips_enabled() - if fips_enabled: - self.debug_print("FIPS is enabled") - else: - self.debug_print("FIPS is not enabled") - return fips_enabled - - def _on_poll(self, param): - action_result = self.add_action_result(phantom.ActionResult(dict(param))) - - if phantom.is_fail(self._connect(action_result)): - return action_result.get_status() - - config = self.get_config() - search_command = config.get("on_poll_command") - search_string = config.get("on_poll_query") - po = config.get("on_poll_parse_only", False) - include_cim_fields = config.get("include_cim_fields", False) - use_event_id_sdi = config.get("use_event_id_sdi", False) - - if not search_string: - self.save_progress("Need to specify Query String to use polling") - return action_result.set_status(phantom.APP_ERROR) - - try: - if not search_command: - if (search_string[0] != "|") and (search_string.find("search", 0) != 0): - search_string = f"search {search_string.strip()}" - search_query = search_string - else: - search_query = f"{search_command.strip()} {search_string.strip()}" - except Exception: - return action_result.set_status(phantom.APP_ERROR, "Error occurred while parsing the search query") - - search_params = {} - - if self.is_poll_now(): - search_params["max_count"] = param.get("container_count", 100) - else: - search_params["max_count"] = self.max_container - start_time = self._state.get("start_time") - if start_time: - search_params["index_earliest"] = start_time - - if int(search_params["max_count"]) <= 0: - self.debug_print( - "The value of 'container_count' parameter must be a positive integer. \ - The value provided in the 'container_count' parameter is {}.\ - Therefore, 'container_count' parameter will be ignored".format(int(search_params["max_count"])) - ) - search_params.pop("max_count") - - ret_val = self._run_query(search_query, action_result, kwargs_create=search_params, parse_only=po) - if phantom.is_fail(ret_val): - if "Invalid index_earliest" in action_result.get_message(): - self.debug_print( - "The value of 'start_time' parameter {} is not a valid epoch time. Re-invoking api without start_time".format( - search_params.get("index_earliest") - ) - ) - del self._state["start_time"] - else: - self.save_progress(action_result.get_message()) - return action_result.set_status(phantom.APP_ERROR) - - display = config.get("on_poll_display") - header_set = None - if display: - header_set = [x.strip().lower() for x in display.split(",")] - - # Set the most recent event to data[0] - data = list(reversed(action_result.get_data())) - self.save_progress("Finished search") - - self.debug_print(f"Total {len(data)} event(s) fetched") - - count = 1 - - for item in data: - container = {} - cef = {} - if "_serial" in item: - item.pop("_serial") - if header_set: - name_mappings = {} - for k, v in list(item.items()): - if k.lower() in header_set: - # Use this to keep the orignal capitalization from splunk - name_mappings[k.lower()] = k - for h in header_set: - cef_name = consts.CIM_CEF_MAP.get(h, h) - cef_name = name_mappings.get(cef_name, cef_name) - cef_key_value = name_mappings.get(h, h) - cef[cef_name] = item.get(cef_key_value) - # Add original CIM fields if option is checked - cef.update({cef_key_value: item.get(cef_key_value)} if include_cim_fields else {}) - else: - for k, v in list(item.items()): - cef[consts.CIM_CEF_MAP.get(k, k)] = v - # Add original CIM fields if option is checked - cef.update({k: v} if include_cim_fields else {}) - - # If the boolean in the asset is checked, attempt to use event_id as the source data identifier - # If event_id is missing from event, print warning and use hash SDI - if use_event_id_sdi and "event_id" in item: - sdi = item["event_id"] - else: - if use_event_id_sdi and "event_id" not in item: - self.save_progress("Use event_id as SDI is activated in the asset but event_id is missing from this event.") - self.save_progress("Defaulting to event hash") - input_str = json.dumps(item) - input_str = UnicodeDammit(input_str).unicode_markup.encode("utf-8") - fips_enabled = self._get_fips_enabled() - # if fips is not enabled, we should continue with our existing md5 usage for generating SDIs - # to not impact existing customers - if not fips_enabled: - sdi = hashlib.md5(input_str).hexdigest() # nosemgrep - else: - sdi = hashlib.sha256(input_str).hexdigest() - - severity = self._get_splunk_severity(item) - spl_event_start = self._get_event_start(item.get("_time")) - - container["name"] = self._get_splunk_title(item) - container["severity"] = severity - container["source_data_identifier"] = sdi - - ret_val, msg, cid = self.save_container(container) - if phantom.is_fail(ret_val): - self.save_progress(f"Error saving container: {msg}") - self.debug_print(f"Error saving container: {msg} -- CID: {cid}") - continue - - if self.remove_empty_cef: - cleaned_cef = {} - for key, value in list(cef.items()): - if value is not None: - cleaned_cef[key] = value - cef = cleaned_cef - artifact = [ - { - "cef": cef, - "name": "Field Values", - "source_data_identifier": sdi, - "severity": severity, - "start_time": spl_event_start, - "container_id": cid, - } - ] - create_artifact_status, create_artifact_msg, _ = self.save_artifacts(artifact) - if phantom.is_fail(create_artifact_status): - self.save_progress(f"Error saving artifact: {create_artifact_msg}") - self.debug_print(f"Error saving artifact: {create_artifact_msg}") - continue - - if count == self.container_update_state and not self.is_poll_now(): - self._state["start_time"] = item.get("_indextime") - self.save_state(self._state) - self.debug_print("Index time updated") - count = 0 - - count += 1 - - if data and not self.is_poll_now(): - self._state["start_time"] = data[-1].get("_indextime") - - return action_result.set_status(phantom.APP_SUCCESS) - - def _get_event_start(self, start_time): - # use platform default start_time - if not start_time: - return None - - try: - # convert to Splunk SOAR timestamp format - # '%Y-%m-%dT%H:%M:%S.%fZ - datetime_obj = dateutil_parse(start_time) - return datetime_obj.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") - except ParserError as parse_err: - self._dump_error_log(parse_err, "ParserError while parsing _time.") - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg="ParserError while parsing _time", error_text=self._get_error_message_from_exception(parse_err) - ) - self.save_progress(error_text) - return None - except Exception as e: - self._dump_error_log(e, "Exception while parsing _time.") - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg="Exception while parsing _time", error_text=self._get_error_message_from_exception(e) - ) - self.save_progress(error_text) - return None - - def _get_splunk_title(self, item): - title = self._container_name_prefix - if not title and not self._container_name_values: - self._container_name_values.append("source") - values = "" - for i in range(len(self._container_name_values)): - if consts.CIM_CEF_MAP.get(self._container_name_values[i]) and item.get(consts.CIM_CEF_MAP.get(self._container_name_values[i])): - value = item.get(consts.CIM_CEF_MAP.get(self._container_name_values[i])) - elif item.get(self._container_name_values[i]): - value = item.get(self._container_name_values[i]) - else: - value = consts.CIM_CEF_MAP.get(self._container_name_values[i], self._container_name_values[i]) - values += "{}{}".format(value, "" if i == len(self._container_name_values) - 1 else ", ") - - if not title: - time = item.get("_time") - if time: - title = f"Splunk Log Entry on {time}" - else: - title = "Splunk Log Entry" - else: - title = item.get(title, title) - - return f"{title}: {values}" - - def _get_splunk_severity(self, item): - severity = item.get("severity") - if isinstance(severity, list): - severity_keys = ["critical", "high", "medium", "low", "informational"] - for severity_key in severity_keys: - if severity_key in severity: - severity = consts.SPLUNK_SEVERITY_MAP[severity_key] - break - else: - severity = "" - else: - severity = consts.SPLUNK_SEVERITY_MAP.get(severity) - - if not severity: - # Check to see if urgency is set - urgency = item.get("urgency") - severity = consts.SPLUNK_SEVERITY_MAP.get(urgency, "medium") - return severity - - def _handle_run_query(self, param): - """Perform Splunk run query - - How we run Splunk search: https://dev.splunk.com/enterprise/docs/devtools/python/sdk-python/howtousesplunkpython/howtorunsearchespython/ # noqa - Raw REST endpoint: https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsearch#search.2Fjobs - Time modifiers: https://docs.splunk.com/Documentation/Splunk/8.2.5/SearchReference/SearchTimeModifiers - """ - self.save_progress(f"In action handler for: {self.get_action_identifier()}") - action_result = self.add_action_result(phantom.ActionResult(dict(param))) - - # Connect - if phantom.is_fail(self._connect(action_result)): - return action_result.get_status() - - search_command = param.get(consts.SPLUNK_JSON_COMMAND) - search_string = param.get(consts.SPLUNK_JSON_QUERY) - po = param.get(consts.SPLUNK_JSON_PARSE_ONLY, False) - attach_result = param.get(consts.SPLUNK_JSON_ATTACH_RESULT, False) - search_mode = param.get(consts.SPLUNK_JSON_SEARCH_MODE, consts.SPLUNK_SEARCH_MODE_SMART) - add_raw = param.get(consts.SPLUNK_JSON_ADD_RAW_DATA) - time_format = param.get(consts.SPLUNK_JSON_TIME_FORMAT) - - # More info on valid time modifier at https://docs.splunk.com/Documentation/Splunk/8.2.5/SearchReference/SearchTimeModifiers - start_time = phantom.get_value(param, consts.SPLUNK_JSON_START_TIME) - end_time = phantom.get_value(param, consts.SPLUNK_JSON_END_TIME) - self.debug_print(f"Run query with timeframe ({start_time}, {end_time})") - kwargs = {} - if start_time: - kwargs["earliest_time"] = start_time - if end_time: - kwargs["latest_time"] = end_time - if time_format: - kwargs["time_format"] = time_format - - kwargs["adhoc_search_level"] = search_mode - - try: - if not search_command: - if (search_string[0] != "|") and (search_string.find("search", 0) != 0): - search_string = f"search {search_string.strip()}" - search_query = search_string - else: - search_query = f"{search_command.strip()} {search_string.strip()}" - except Exception as e: - self._dump_error_log(e) - return action_result.set_status(phantom.APP_ERROR, "Error occurred while parsing the search query") - - self.debug_print(f"search_query: {search_query}") - return self._run_query( - search_query, action_result, attach_result=attach_result, kwargs_create=kwargs, parse_only=po, add_raw_field=add_raw - ) - - def _get_tz_str_from_epoch(self, time_format_str, epoch_milli): - # Need to convert from UTC to the device's timezone, get the device's tz from config - config = self.get_config() - device_tz_sting = config[consts.SPLUNK_JSON_TIMEZONE] - - to_tz = ZoneInfo(device_tz_sting) - - utc_dt = datetime.fromtimestamp(epoch_milli // 1000, tz=timezone.utc) - to_dt = utc_dt.astimezone(to_tz) - - # return utc_dt.strftime('%Y-%m-%d %H:%M:%S') - return to_dt.strftime(time_format_str) - - def _list_alerts(self, param, action_result=None): - if not action_result: - # Create a action result to represent this action - action_result = self.add_action_result(phantom.ActionResult(dict(param))) - - # If end_time is not given, then end_time is 'now' - # If start_time is not given, then start_time is SPLUNK_NUMBER_OF_DAYS_BEFORE_ENDTIME - # days behind end_time - curr_epoch_msecs = int(time.time()) * 1000 - start_time_msecs = 0 - end_time_msecs = int(phantom.get_value(param, consts.SPLUNK_JSON_END_TIME, curr_epoch_msecs)) - start_time_msecs = int( - phantom.get_value( - param, - consts.SPLUNK_JSON_START_TIME, - end_time_msecs - (consts.SPLUNK_MILLISECONDS_IN_A_DAY * consts.SPLUNK_NUMBER_OF_DAYS_BEFORE_ENDTIME), - ) - ) - - if end_time_msecs < start_time_msecs: - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_INVALID_TIME_RANGE) - - # From splunk documentation - # To search with an exact date as boundary, such as from November 5 at 8 PM to November 12 at 8 PM, - # use the timeformat: %m/%d/%Y:%H:%M:%S - # TODO, We need not convert the epoch to formatted and then pass the format string also to splunk - # We should be able to work off of just epoch, however not too sure what the input epoch UTC format - # is to splunk and the doc is not that clear. - time_format_str = "%m/%d/%Y:%H:%M:%S" - earliest_time = f"{self._get_tz_str_from_epoch(time_format_str, start_time_msecs)}" - latest_time = f"{self._get_tz_str_from_epoch(time_format_str, end_time_msecs)}" - - kwargs_create = {"earliest_time": earliest_time, "latest_time": latest_time, "time_format": time_format_str} - # kwargs_create = {"time_format": "%m/%d/%Y:%H:%M:%S", - # "latest_time": "03/21/2015:14:29:25", - # "earliest_time": "03/21/2015:14:24:25"} - - self.save_progress(consts.SPLUNK_PROG_TIME_RANGE, range=json.dumps(kwargs_create)) - - count = int(phantom.get_value(param, phantom.APP_JSON_CONTAINER_COUNT, consts.SPLUNK_DEFAULT_ALERT_COUNT)) - - # Work of the saved search name, if given - ss_name = phantom.get_value(self.get_config(), consts.SPLUNK_JSON_ALERT_NAME, None) - - # default to blank - ss_query = "" - - if ss_name: - # create a list of query's is easier then just replacing the ',' with 'OR ss_name= - # that way we can work on each one of them seperately, like strip them or add quotes - # if not present etc. - ss_names = ['"{}"'.format(x.strip(' "')) for x in ss_name.split(",") if len(x.strip()) > 0] - self.debug_print("ss_names", ss_names) - ss_query = "ss_name = {}".format(" OR ss_name = ".join(ss_names)) - - query = consts.SPLUNK_SEARCH_AUDIT_INDEX_QUERY.format(ss_query, count) - - self.debug_print("query", query) - - self._run_query(query, action_result, kwargs_create=kwargs_create) - - return action_result.get_status() - - def _test_asset_connectivity(self, param): - action_result = self.add_action_result(phantom.ActionResult(dict(param))) - - if phantom.is_fail(self._connect(action_result)): - self.debug_print("connect failed") - self.save_progress(consts.SPLUNK_ERR_CONNECTIVITY_TEST) - return action_result.append_to_message(consts.SPLUNK_ERR_CONNECTIVITY_TEST) - - version = self._get_server_version(action_result) - if version == "FAILURE": - return action_result.append_to_message(consts.SPLUNK_ERR_CONNECTIVITY_TEST) - - is_es = self._check_for_es(action_result) - - self.save_progress("Detected Splunk {}server version {}".format("ES " if is_es else "", version)) - - self.debug_print("connect passed") - self.save_progress(consts.SPLUNK_SUCCESS_CONNECTIVITY_TEST) - return action_result.set_status(phantom.APP_SUCCESS, consts.SPLUNK_SUCCESS_CONNECTIVITY_TEST) - - def _run_query(self, search_query, action_result, attach_result=False, kwargs_create=dict(), parse_only=True, add_raw_field=True): - """Function that executes the query on splunk""" - self.debug_print("Start run query") - RETRY_LIMIT = self.retry_count - summary = action_result.update_summary({}) - summary["sid"] = "Search ID not created" - - # Validate the search query - for attempt_count in range(0, RETRY_LIMIT): - try: - self._service.parse(search_query, parse_only=parse_only) - break - except HTTPError as e: - self._dump_error_log(e, "Failed to validate search query.") - if phantom.is_fail(self._connect(action_result)): - return action_result.get_status() - if attempt_count == RETRY_LIMIT - 1: - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg=consts.SPLUNK_ERR_INVALID_QUERY, error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text, query=search_query) - except Exception as e: - self._dump_error_log(e, "Failed to validate search query.") - if phantom.is_fail(self._connect(action_result)): - return action_result.get_status() - if attempt_count == RETRY_LIMIT - 1: - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg=consts.SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text) - - self.debug_print(consts.SPLUNK_PROG_CREATED_QUERY.format(query=search_query)) - - # Creating search job - self.save_progress(consts.SPLUNK_PROG_CREATING_SEARCH_JOB) - - # Set any search creation flags here - kwargs_create.update({"exec_mode": "normal"}) - - self.debug_print("kwargs_create", kwargs_create) - - # Create the job - is_created_successfully, job = self._create_splunk_job( - action_result=action_result, retry_limit=RETRY_LIMIT, search_query=search_query, kwargs_create=kwargs_create - ) - if phantom.is_fail(is_created_successfully): - return phantom.APP_ERROR - - summary["sid"] = job.__dict__.get("sid") - - result_count = 0 - while True: - is_job_successful: bool = self._wait_until_splunk_job_results_are_ready(action_result, job, RETRY_LIMIT) - if phantom.is_fail(is_job_successful): - return phantom.APP_ERROR - - stats = self._get_stats(job) - - if not ("doneProgress" in job and "scanCount" in job and "eventCount" in job and "resultCount" in job): - status = "Progress: {} {} scanned {} matched {} results".format( - stats.get("progress"), stats.get("scan_count"), stats.get("event_count"), stats.get("result_count") - ) - else: - status = ("Progress: %(progress)03.1f%% %(scan_count)d scanned %(event_count)d matched %(result_count)d results") % stats # noqa: UP031 - self.send_progress(status) - if stats["is_done"] == "1": - result_count = stats["result_count"] - break - time.sleep(self.sleeptime_in_requests) - - self.send_progress("Parsing results...") - result_index = 0 - ten_percent = float(result_count) * 0.10 - - try: - results = splunk_results.JSONResultsReader(job.results(count=kwargs_create.get("max_count", 0), output_mode="json")) - except Exception as e: - self._dump_error_log(e) - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg="Error retrieving results", error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text) - - for result in results: - if not isinstance(result, dict): - continue - - if not add_raw_field: - result.pop("_raw", None) - - action_result.add_data(result) - - result_index += 1 - - if (result_index % ten_percent) == 0: - status = f"Finished parsing {float(result_index) / float(result_count):.1%} of results" - self.send_progress(status) - - if attach_result: - self.add_json_result(action_result) - - summary[consts.SPLUNK_JSON_TOTAL_EVENTS] = result_index - self.debug_print("Done run query") - return action_result.set_status(phantom.APP_SUCCESS) - - def _wait_until_splunk_job_results_are_ready(self, action_result: phantom.ActionResult, job: splunk_client.Job, retry_limit: int) -> bool: - for attempt_count in range(1, retry_limit + 1): - max_waiting_time: float = time.time() + self.splunk_job_timeout - try: - # Timing out the splunk job is required, because the job - # could be stuck in permanent "QUEUED" state after the Splunk - # stack has crashed. - while not job.is_ready(): - if time.time() > max_waiting_time: - return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_SPLUNK_JOB_HAS_TIMED_OUT) - time.sleep(self.sleeptime_in_requests) - job.refresh() - break - except Exception as e: - self.debug_print(f"Attempt {attempt_count} out of {retry_limit} to connect to splunk server failed with error: {e}.") - if attempt_count == retry_limit: - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg=consts.SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text) - return True - - def _create_splunk_job( - self, action_result: phantom.ActionResult, retry_limit: int, search_query: str, kwargs_create: dict - ) -> tuple[bool, Optional[splunk_client.Job]]: - for attempt_count in range(1, retry_limit + 1): - try: - job: splunk_client.Job = self._service.jobs.create(search_query, **kwargs_create) - break - except Exception as e: - self.debug_print(f"Attempt {attempt_count} out of {retry_limit} to create splunk job failed with error: {e}.") - self._dump_error_log(e, "Failed to create job.") - if attempt_count == retry_limit: - error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format( - msg=consts.SPLUNK_ERR_UNABLE_TO_CREATE_JOB, error_text=self._get_error_message_from_exception(e) - ) - return action_result.set_status(phantom.APP_ERROR, error_text), None - return True, job - - def add_json_result(self, action_result): - _fd, path = tempfile.mkstemp(dir=Vault.get_vault_tmp_dir(), text=True) - vault_attach_dict = {} - - vault_attach_dict[phantom.APP_JSON_ACTION_NAME] = self.get_action_name() - vault_attach_dict[phantom.APP_JSON_APP_RUN_ID] = self.get_app_run_id() - - try: - with open(path, "w") as f: - json.dump(action_result.get_data(), f) - - except Exception as e: - self._dump_error_log(e, "Error occurred while adding file to Vault.") - error_message = self._get_error_message_from_exception(e) - msg = f"Error occurred while adding file to Vault. Error Details: {error_message}" - self.debug_print(msg) - return phantom.APP_ERROR - - container_id = self.get_container_id() - - try: - success, message, _ = soar_vault.vault_add(container_id, path, "splunk_run_query_result.json", vault_attach_dict) - - except Exception as e: - self._dump_error_log(e) - err = self._get_error_message_from_exception(e) - self.debug_print(phantom.APP_ERR_FILE_ADD_TO_VAULT.format(err)) - return action_result.set_status(phantom.APP_ERROR, phantom.APP_ERR_FILE_ADD_TO_VAULT.format(err)) - - if not success: - err = f"Failed to add file to Vault: {message}" - self.debug_print(err) - return action_result.set_status(phantom.APP_ERROR, err) - - def handle_action(self, param): - """Function that handles all the actions - Args: - The json containing config, action and supporting parameters - Handle to the ph_connector, should be used/passed when making ph_connector function calls - Return: - status code - """ - - # Get the action that we are supposed to carry out, set it in the connection result object - action = self.get_action_identifier() - self.send_progress(f"executing action: {action}") - result = None - if action == self.ACTION_ID_RUN_QUERY: - result = self._handle_run_query(param) - elif action == self.ACTION_ID_POST_DATA: - result = self._post_data(param) - elif action == self.ACTION_ID_UPDATE_EVENT: - result = self._update_event(param) - elif action == self.ACTION_ID_GET_HOST_EVENTS: - result = self._get_host_events(param) - elif action == phantom.ACTION_ID_TEST_ASSET_CONNECTIVITY: - result = self._test_asset_connectivity(param) - elif action == "on_poll": - result = self._on_poll(param) - - return result - - -if __name__ == "__main__": - import argparse - - import pudb - import requests - - 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() - session_id = None - - username = args.username - password = args.password - verify = args.verify - - if username is not None and password is None: - # User specified a username but not a password, so ask - import getpass - - password = getpass.getpass("Password: ") - - if username and password: - login_url = BaseConnector._get_phantom_base_url() + "login" - try: - print("Accessing the Login page") - r = requests.get(login_url, verify=verify, timeout=consts.SPLUNK_DEFAULT_REQUEST_TIMEOUT) - csrftoken = r.cookies["csrftoken"] - - data = dict() - data["username"] = username - data["password"] = password - data["csrfmiddlewaretoken"] = csrftoken - - headers = dict() - headers["Cookie"] = "csrftoken=" + csrftoken - headers["Referer"] = login_url - - print("Logging into Platform to get the session id") - r2 = requests.post(login_url, verify=verify, data=data, headers=headers, timeout=consts.SPLUNK_DEFAULT_REQUEST_TIMEOUT) - session_id = r2.cookies["sessionid"] - except Exception as e: - print("Unable to get session id from the platfrom. Error: " + str(e)) - 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 = SplunkConnector() - 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/splunk_views.py b/splunk_views.py deleted file mode 100644 index d05625a..0000000 --- a/splunk_views.py +++ /dev/null @@ -1,64 +0,0 @@ -# File: splunk_views.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. -def _get_ctx_result(result, provides): - ctx_result = {} - headers = [] - processed_data = [] - - param = result.get_param() - summary = result.get_summary() - data = result.get_data() - - ctx_result["param"] = param - ctx_result["action_name"] = provides - if summary: - ctx_result["summary"] = summary - - if not data: - ctx_result["data"] = {} - return ctx_result - - if param.get("display"): - headers = [x.strip() for x in param["display"].split(",")] - headers = list(filter(None, headers)) - - else: - for key in data[0].keys(): - if key[0] != "_": - headers.append(key) - - for item in data: - header_values = dict() - for header in headers: - header_values[header] = item.get(header) - processed_data.append(header_values) - - ctx_result["data"] = data - ctx_result["processed_data"] = processed_data - ctx_result["headers"] = headers - - return ctx_result - - -def display_view(provides, all_app_runs, context): - context["results"] = results = [] - for summary, action_results in all_app_runs: - for result in action_results: - ctx_result = _get_ctx_result(result, provides) - if not ctx_result: - continue - results.append(ctx_result) - - return "splunk_run_query.html" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..897c86c --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,16 @@ +# Copyright (c) 2016-2026 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/src/app.py b/src/app.py new file mode 100644 index 0000000..5edd7e4 --- /dev/null +++ b/src/app.py @@ -0,0 +1,1314 @@ +# Copyright (c) 2016-2026 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 hashlib +import json +import os +import re +import ssl +import sys +import time +from collections.abc import Iterator +from datetime import datetime, UTC +from dateutil.parser import ParserError, parse as dateutil_parse +from io import BytesIO +from urllib.error import HTTPError as UrllibHTTPError, URLError +from urllib.request import ProxyHandler, Request, build_opener, install_opener, urlopen +from zoneinfo import ZoneInfo + +import requests +import splunklib.binding as splunk_binding +import splunklib.client as splunk_client +import splunklib.results as splunk_results +from splunklib.binding import HTTPError as SplunkHTTPError +import xmltodict +from bs4 import BeautifulSoup +from bs4.dammit import UnicodeDammit +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, OutputField, PermissiveActionOutput +from soar_sdk.app import App +from soar_sdk.asset import AssetField, BaseAsset, FieldCategory +from soar_sdk.logging import getLogger +from soar_sdk.models.artifact import Artifact +from soar_sdk.models.container import Container +from soar_sdk.params import OnPollParams, Param, Params + +from .splunk_consts import ( + CIM_CEF_MAP, + SPLUNK_DEFAULT_REQUEST_TIMEOUT, + SPLUNK_DEFAULT_SOURCE, + SPLUNK_DEFAULT_SOURCE_TYPE, + SPLUNK_DISPOSITION_QUERY_FORMAT, + SPLUNK_ERR_BAD_DISPOSITION, + SPLUNK_ERR_BAD_STATUS, + SPLUNK_ERR_CONNECTIVITY_FAILED, + SPLUNK_ERR_CONNECTIVITY_TEST, + SPLUNK_ERR_EMPTY_RESPONSE, + SPLUNK_ERR_INVALID_INTEGER, + SPLUNK_ERR_INVALID_SLEEP_TIME, + SPLUNK_ERR_NEED_PARAM, + SPLUNK_ERR_NON_NEGATIVE_INTEGER, + SPLUNK_ERR_NOT_ES, + SPLUNK_ERR_INVALID_PARAM, + SPLUNK_ERR_REQUIRED_CONFIG_PARAMS, + SPLUNK_ERR_SPLUNK_JOB_HAS_TIMED_OUT, + SPLUNK_ERR_UNABLE_TO_CREATE_JOB, + SPLUNK_ERR_UNABLE_TO_PARSE_HTML_RESPONSE, + SPLUNK_ERR_UNABLE_TO_PARSE_JSON_RESPONSE, + SPLUNK_EXCEPTION_ERR_MESSAGE, + SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE, + SPLUNK_PROG_CREATED_QUERY, + SPLUNK_PROG_CREATING_SEARCH_JOB, + SPLUNK_RID_SID_NOTABLE_QUERY, + SPLUNK_SEARCH_MODE_SMART, + SPLUNK_SEVERITY_MAP, + SPLUNK_SUCCESS_CONNECTIVITY_TEST, +) + +logger = getLogger() + + +# --------------------------------------------------------------------------- +# Asset +# --------------------------------------------------------------------------- +class Asset(BaseAsset): + device: str = AssetField( + required=True, + description="Device IP/Hostname", + category=FieldCategory.CONNECTIVITY, + ) + port: int = AssetField( + description="Port", + required=False, + default=8089, + category=FieldCategory.CONNECTIVITY, + ) + username: str = AssetField( + description="Username", + required=False, + default="", + category=FieldCategory.CONNECTIVITY, + ) + password: str = AssetField( + description="Password", + required=False, + default="", + sensitive=True, + category=FieldCategory.CONNECTIVITY, + ) + api_token: str = AssetField( + description="API token", + required=False, + default="", + sensitive=True, + category=FieldCategory.CONNECTIVITY, + ) + splunk_owner: str = AssetField( + description="The owner context of the namespace", + required=False, + default="", + category=FieldCategory.CONNECTIVITY, + ) + splunk_app: str = AssetField( + description="The app context of the namespace", + required=False, + default="", + category=FieldCategory.CONNECTIVITY, + ) + timezone: str = AssetField( + required=False, + default="UTC", + description="Splunk Server Timezone", + category=FieldCategory.CONNECTIVITY, + ) + verify_server_cert: bool = AssetField( + description="Verify Server Certificate", + required=False, + default=False, + category=FieldCategory.CONNECTIVITY, + ) + + # Ingestion fields + on_poll_command: str = AssetField( + description="Command for query to use with On Poll", + required=False, + default="", + value_list=["", "search", "eval", "savedsearch", "stats", "table", "tstats"], + category=FieldCategory.INGEST, + ) + on_poll_query: str = AssetField( + description="Query to use with On Poll", + required=False, + default="", + category=FieldCategory.INGEST, + ) + on_poll_display: str = AssetField( + description="Fields to save with On Poll", + required=False, + default="", + category=FieldCategory.INGEST, + ) + on_poll_parse_only: bool = AssetField( + description="Parse Only", + required=False, + default=True, + category=FieldCategory.INGEST, + ) + max_container: int = AssetField( + description="Max events to ingest for Scheduled Polling (Default: 100)", + required=False, + default=100, + category=FieldCategory.INGEST, + ) + container_update_state: int = AssetField( + description="Container count to update the state file", + required=False, + default=100, + category=FieldCategory.INGEST, + ) + container_name_prefix: str = AssetField( + description="Name to give containers created via ingestion", + required=False, + default="", + category=FieldCategory.INGEST, + ) + container_name_values: str = AssetField( + description="Values to append to container name", + required=False, + default="", + category=FieldCategory.INGEST, + ) + retry_count: int = AssetField( + description="Number of retries", + required=False, + default=3, + category=FieldCategory.CONNECTIVITY, + ) + remove_empty_cef: bool = AssetField( + description="Remove CEF fields having empty values from the artifact", + required=False, + default=False, + category=FieldCategory.INGEST, + ) + sleeptime_in_requests: int = AssetField( + description="The time to wait for next REST call (max 120 seconds)", + required=False, + default=1, + category=FieldCategory.CONNECTIVITY, + ) + include_cim_fields: bool = AssetField( + description="Option to keep original Splunk CIM together with SOAR CEF fields", + required=False, + default=False, + category=FieldCategory.INGEST, + ) + splunk_job_timeout: int = AssetField( + description="The duration in seconds to wait before a scheduled Splunk job times out", + required=False, + default=1200, + category=FieldCategory.CONNECTIVITY, + ) + use_event_id_sdi: bool = AssetField( + description="Option to use the event_id field value as the source data identifier instead of the full event hash", + required=False, + default=False, + category=FieldCategory.INGEST, + ) + + +# --------------------------------------------------------------------------- +# App +# --------------------------------------------------------------------------- +app = App( + name="Splunk", + app_type="siem", + logo="logo_splunk.svg", + logo_dark="logo_splunk_dark.svg", + product_vendor="Splunk Inc.", + product_name="Splunk Enterprise", + publisher="Splunk", + appid="91883aa8-9c81-470b-97a1-5d8f7995f560", + fips_compliant=True, + asset_cls=Asset, +) + + +# --------------------------------------------------------------------------- +# Helper +# --------------------------------------------------------------------------- +class SplunkHelper: + """Manages the Splunk SDK connection and REST calls.""" + + def __init__(self, asset: Asset): + self.asset = asset + self._service: splunk_client.Service | None = None + self._base_url = f"https://{asset.device}:{asset.port}/" + self._proxy: dict[str, str] = {} + + if "http_proxy" in os.environ: + self._proxy["http"] = os.environ["http_proxy"] + elif "HTTP_PROXY" in os.environ: + self._proxy["http"] = os.environ["HTTP_PROXY"] + + if "https_proxy" in os.environ: + self._proxy["https"] = os.environ["https_proxy"] + elif "HTTPS_PROXY" in os.environ: + self._proxy["https"] = os.environ["HTTPS_PROXY"] + + # -- validation ---------------------------------------------------------- + @staticmethod + def validate_integer(value, name: str, allow_zero: bool = False) -> int | None: + if value is None or value == "": + return None + try: + if not float(value).is_integer(): + raise ValueError(SPLUNK_ERR_INVALID_INTEGER.format(param=name)) + value = int(value) + except (ValueError, TypeError): + raise ValueError(SPLUNK_ERR_INVALID_INTEGER.format(param=name)) from None + + if value < 0: + raise ValueError(SPLUNK_ERR_NON_NEGATIVE_INTEGER.format(param=name)) + if not allow_zero and value == 0: + raise ValueError(SPLUNK_ERR_INVALID_PARAM.format(param=name)) + return value + + def validate_asset(self): + if not self.asset.api_token and (not self.asset.username or not self.asset.password): + raise ValueError(SPLUNK_ERR_REQUIRED_CONFIG_PARAMS) + + self.validate_integer(self.asset.retry_count, "'retry_count' configuration") + self.validate_integer(self.asset.port, "'port' configuration") + self.validate_integer(self.asset.max_container, "'max_container' configuration", allow_zero=True) + self.validate_integer(self.asset.container_update_state, "'Container count to update the state file' configuration") + self.validate_integer(self.asset.splunk_job_timeout, "'splunk_job_timeout' configuration") + self.validate_integer(self.asset.sleeptime_in_requests, "'sleeptime_in_requests' configuration") + + if self.asset.sleeptime_in_requests > 120: + raise ValueError(SPLUNK_ERR_INVALID_SLEEP_TIME.format(param="'sleeptime_in_requests'")) + + # -- proxy handler for splunklib ---------------------------------------- + def _proxy_request(self, url, message, **kwargs): + method = message["method"].lower() + data = message.get("body", "") if method == "post" else None + headers = dict(message.get("headers", [])) + req = Request(url, data, headers) # noqa: S310 + try: + response = urlopen(req) # noqa: S310 + except UrllibHTTPError: + logger.warning("Check the proxy settings") + raise + except URLError: + if sys.version_info >= (2, 7, 9) and not self.asset.verify_server_cert: + response = urlopen(req, context=ssl._create_unverified_context()) # noqa: S310, S323 + else: + raise + return { + "status": response.code, + "reason": response.msg, + "headers": response.getheaders(), + "body": BytesIO(response.read()), + } + + def _make_proxy_handler(self, proxy: str): + proxy_handler = ProxyHandler({"http": proxy, "https": proxy}) + opener = build_opener(proxy_handler) + install_opener(opener) + return self._proxy_request + + # -- connection ---------------------------------------------------------- + def connect(self): + if self._service is not None: + return + + kwargs_config = { + "host": self.asset.device, + "port": self.asset.port, + "username": self.asset.username, + "password": self.asset.password, + "owner": self.asset.splunk_owner or None, + "app": self.asset.splunk_app or None, + "verify": self.asset.verify_server_cert, + } + + if self.asset.api_token: + logger.info("Using token-based authentication") + kwargs_config["splunkToken"] = self.asset.api_token + kwargs_config.pop("username", None) + kwargs_config.pop("password", None) + + proxy_param = self._proxy.get("https") or self._proxy.get("http") + + no_proxy = os.environ.get("no_proxy", os.environ.get("NO_PROXY", "")) + if self.asset.device in no_proxy.split(","): + proxy_param = None + + try: + if proxy_param: + logger.info("Engaging proxy") + self._service = splunk_client.connect( + handler=self._make_proxy_handler(proxy_param), **kwargs_config + ) + else: + self._service = splunk_client.connect(**kwargs_config) + except splunk_binding.HTTPError as e: + error_text = str(e) + if "405 Method Not Allowed" in error_text: + raise ConnectionError("Error occurred while connecting to the Splunk server") from e + raise ConnectionError(f"Error occurred while connecting to the Splunk server. Details: {error_text}") from e + except Exception as e: + raise ConnectionError( + SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e) + ) from e + + @property + def service(self) -> splunk_client.Service: + if self._service is None: + self.connect() + return self._service + + # -- REST calls ---------------------------------------------------------- + def make_rest_call(self, endpoint: str, data, params: dict | None = None, method=requests.post) -> dict: + url = f"{self._base_url}services/{endpoint}" + logger.debug("Making REST call to %s", url) + + auth, auth_headers = None, None + if self.asset.api_token: + auth_headers = {"Authorization": f"Bearer {self.asset.api_token}"} + else: + auth = (self.asset.username, self.asset.password) + + try: + r = method( + url, + data=data, + params=params or {}, + auth=auth, + headers=auth_headers, + verify=self.asset.verify_server_cert, + timeout=SPLUNK_DEFAULT_REQUEST_TIMEOUT, + ) + except Exception as e: + raise ConnectionError( + SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e) + ) from e + + return self._process_response(r) + + def make_rest_call_retry(self, endpoint: str, data, params: dict | None = None, method=requests.post) -> dict: + last_err = None + for _ in range(self.asset.retry_count): + try: + return self.make_rest_call(endpoint, data, params, method) + except Exception as e: + last_err = e + raise last_err # type: ignore[misc] + + # -- response processing ------------------------------------------------- + def _process_response(self, r: requests.Response) -> dict: + content_type = r.headers.get("Content-Type", "") + if "json" in content_type: + return self._process_json_response(r) + if "html" in content_type: + return self._process_html_response(r) + if "xml" in content_type: + return self._process_xml_response(r) + if not r.text: + return self._process_empty_response(r) + + error_text = r.text.replace("{", "{{").replace("}", "}}") + raise RuntimeError( + f"Can't process response from server. Status Code: {r.status_code} Data from server: {error_text}" + ) + + @staticmethod + def _process_empty_response(r: requests.Response) -> dict: + if r.status_code in (200, 204): + return {} + raise RuntimeError(SPLUNK_ERR_EMPTY_RESPONSE.format(code=r.status_code)) + + @staticmethod + def _process_xml_response(r: requests.Response) -> dict: + try: + resp_json = xmltodict.parse(r.text) if r.text else None + except Exception as e: + raise RuntimeError(f"Unable to parse XML response. Error: {e}") from e + + if 200 <= r.status_code < 400: + return resp_json or {} + + error_type = resp_json.get("response", {}).get("messages", {}).get("msg", {}).get("@type") if resp_json else None + error_message = resp_json.get("response", {}).get("messages", {}).get("msg", {}).get("#text") if resp_json else None + if error_type or error_message: + error = f"ErrorType: {error_type} ErrorMessage: {error_message}" + else: + error = "Unable to parse xml response" + raise RuntimeError(f"Error from server. Status Code: {r.status_code} Data from server: {error}") + + @staticmethod + def _process_html_response(r: requests.Response) -> dict: + try: + soup = BeautifulSoup(r.text, "html.parser") + for element in soup(["script", "style", "footer", "nav"]): + element.extract() + error_text = "\n".join(line.strip() for line in soup.text.split("\n") if line.strip()) + except Exception as e: + error_text = SPLUNK_ERR_UNABLE_TO_PARSE_HTML_RESPONSE.format(error=e) + + if not error_text: + error_text = "Empty response and no information received" + + message = f"Status Code: {r.status_code}. Data from server:\n{error_text}\n" + if len(message) > 500: + message = "Error occurred while connecting to the Splunk server" + raise RuntimeError(message) + + @staticmethod + def _process_json_response(r: requests.Response) -> dict: + try: + resp_json = r.json() + except Exception as e: + raise RuntimeError(SPLUNK_ERR_UNABLE_TO_PARSE_JSON_RESPONSE.format(error=e)) from e + + if 200 <= r.status_code < 399: + return resp_json + + if isinstance(resp_json, str): + raise RuntimeError(f"Error from server. Details: {resp_json}") + if resp_json.get("error") or resp_json.get("error_description"): + raise RuntimeError( + f"Error from server. Status Code: {r.status_code}. " + f"Error: {resp_json.get('error', 'Unavailable')}. " + f"Error Details: {resp_json.get('error_description', 'Unavailable')}" + ) + if resp_json.get("messages") and resp_json["messages"]: + msg = resp_json["messages"][0] + error = f"ErrorType: {msg.get('type')} ErrorMessage: {msg.get('text')}" + raise RuntimeError(f"Error from server. Status Code: {r.status_code} Data from server: {error}") + + error_text = r.text.replace("{", "{{").replace("}", "}}") + raise RuntimeError(f"Error from server. Status Code: {r.status_code}. Data from server: {error_text}") + + # -- server info --------------------------------------------------------- + def get_server_version(self) -> str: + try: + resp = self.make_rest_call_retry( + "authentication/users?output_mode=json", {}, method=requests.get + ) + except Exception: + return "FAILURE" + return resp.get("generator", {}).get("version", "UNKNOWN") + + def check_for_es(self) -> bool: + try: + resp = self.make_rest_call_retry( + "apps/local/SplunkEnterpriseSecuritySuite", {}, method=requests.get + ) + return bool(resp) + except Exception: + return False + + # -- splunk search jobs -------------------------------------------------- + def create_job(self, search_query: str, kwargs_create: dict) -> splunk_client.Job: + last_err = None + for attempt in range(1, self.asset.retry_count + 1): + try: + return self.service.jobs.create(search_query, **kwargs_create) + except Exception as e: + logger.debug("Attempt %d to create splunk job failed: %s", attempt, e) + last_err = e + raise RuntimeError( + SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_UNABLE_TO_CREATE_JOB, error_text=last_err) + ) + + def wait_for_job(self, job: splunk_client.Job): + last_err = None + for attempt in range(1, self.asset.retry_count + 1): + try: + max_wait = time.time() + self.asset.splunk_job_timeout + while not job.is_ready(): + if time.time() > max_wait: + raise TimeoutError(SPLUNK_ERR_SPLUNK_JOB_HAS_TIMED_OUT) + time.sleep(self.asset.sleeptime_in_requests) + job.refresh() + return + except TimeoutError: + raise + except Exception as e: + logger.debug("Attempt %d to wait for job failed: %s", attempt, e) + last_err = e + raise RuntimeError( + SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=last_err) + ) + + def get_job_stats(self, job) -> dict: + return { + "is_done": job["isDone"] if "isDone" in job else "Unknown status", + "progress": float(job["doneProgress"]) * 100 if "doneProgress" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Done progress"), + "scan_count": int(job["scanCount"]) if "scanCount" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Scan count"), + "event_count": int(job["eventCount"]) if "eventCount" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Event count"), + "result_count": int(job["resultCount"]) if "resultCount" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Result count"), + } + + def validate_query(self, search_query: str, parse_only: bool = True): + for attempt in range(self.asset.retry_count): + try: + self.service.parse(search_query, parse_only=parse_only) + return + except SplunkHTTPError as e: + self._service = None + self.connect() + if attempt == self.asset.retry_count - 1: + raise ValueError( + SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=f"Query invalid '{search_query}'", error_text=e) + ) from e + except Exception as e: + self._service = None + self.connect() + if attempt == self.asset.retry_count - 1: + raise RuntimeError( + SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e) + ) from e + + def run_query(self, search_query: str, kwargs_create: dict | None = None, parse_only: bool = True, add_raw_field: bool = True) -> tuple[str, list[dict]]: + if kwargs_create is None: + kwargs_create = {} + + self.validate_query(search_query, parse_only) + logger.debug(SPLUNK_PROG_CREATED_QUERY.format(query=search_query)) + logger.progress(SPLUNK_PROG_CREATING_SEARCH_JOB) + + kwargs_create["exec_mode"] = "normal" + job = self.create_job(search_query, kwargs_create) + sid = job.__dict__.get("sid", "") + + while True: + self.wait_for_job(job) + stats = self.get_job_stats(job) + if stats["is_done"] == "1": + break + time.sleep(self.asset.sleeptime_in_requests) + + results_list: list[dict] = [] + + try: + results = splunk_results.JSONResultsReader( + job.results(count=kwargs_create.get("max_count", 0), output_mode="json") + ) + except Exception as e: + raise RuntimeError( + SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg="Error retrieving results", error_text=e) + ) from e + + for result in results: + if not isinstance(result, dict): + continue + if not add_raw_field: + result.pop("_raw", None) + results_list.append(result) + + return sid, results_list + + def resolve_event_id(self, sidandrid: str) -> str: + logger.progress("Resolving SID+RID to event_id") + search_query = SPLUNK_RID_SID_NOTABLE_QUERY.format(sidandrid) + _sid, results = self.run_query(search_query) + for row in results: + if "event_id" in row: + return row["event_id"] + raise RuntimeError("could not find event_id of splunk event") + + def get_status_dict(self, status_type: str) -> dict[str, int]: + splunk_dict: dict[str, int] = {} + try: + resp = self.make_rest_call_retry( + "alerts/reviewstatuses?count=-1&output_mode=json", {}, method=requests.get + ) + except Exception: + return splunk_dict + + for data in resp.get("entry", []): + obj_id = data.get("name", "").split(":")[-1] + obj_name = data.get("content", {}).get("label") + is_enabled = str(data.get("content", {}).get("disabled")) == "0" + is_type = data.get("content", {}).get("status_type") == status_type + if obj_id and obj_id.isdigit() and obj_name and is_enabled and is_type: + key = obj_name.lower() if status_type == "notable" else obj_name + splunk_dict[key] = int(obj_id) + return splunk_dict + + def get_tz_str_from_epoch(self, fmt: str, epoch_milli: int) -> str: + to_tz = ZoneInfo(self.asset.timezone) + utc_dt = datetime.fromtimestamp(epoch_milli // 1000, tz=UTC) + return utc_dt.astimezone(to_tz).strftime(fmt) + + +# --------------------------------------------------------------------------- +# Ingestion helpers +# --------------------------------------------------------------------------- +def _get_event_start(start_time: str | None) -> str | None: + if not start_time: + return None + try: + datetime_obj = dateutil_parse(start_time) + return datetime_obj.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + except ParserError as e: + logger.error("ParserError while parsing _time: %s", e) + return None + except Exception as e: + logger.error("Exception while parsing _time: %s", e) + return None + + +def _get_fips_enabled() -> bool: + try: + from phantom_common.install_info import is_fips_enabled # noqa: PLC0415 + return is_fips_enabled() + except ImportError: + return False + + +def _get_splunk_severity(item: dict) -> str: + severity = item.get("severity") + if isinstance(severity, list): + for key in ["critical", "high", "medium", "low", "informational"]: + if key in severity: + return SPLUNK_SEVERITY_MAP[key] + return "" + severity = SPLUNK_SEVERITY_MAP.get(severity) if severity else None + if not severity: + urgency = item.get("urgency") + severity = SPLUNK_SEVERITY_MAP.get(urgency, "medium") + return severity + + +def _get_splunk_title(item: dict, prefix: str, name_values: list[str]) -> str: + title = prefix + values_list = list(name_values) + if not title and not values_list: + values_list.append("source") + + values = "" + for i, nv in enumerate(values_list): + if CIM_CEF_MAP.get(nv) and item.get(CIM_CEF_MAP.get(nv)): + value = item.get(CIM_CEF_MAP.get(nv)) + elif item.get(nv): + value = item.get(nv) + else: + value = CIM_CEF_MAP.get(nv, nv) + values += f"{value}" + ("" if i == len(values_list) - 1 else ", ") + + if not title: + t = item.get("_time") + title = f"Splunk Log Entry on {t}" if t else "Splunk Log Entry" + else: + title = item.get(title, title) + + return f"{title}: {values}" + + +# --------------------------------------------------------------------------- +# Params / Outputs +# --------------------------------------------------------------------------- +class RunQueryParams(Params): + command: str = Param( + description="Beginning command (in Splunk Processing Language)", + required=False, + value_list=["search", "eval", "savedsearch", "stats", "table", "tstats"], + default="search", + ) + query: str = Param( + description="Query to run (in Splunk Processing Language)", + required=True, + primary=True, + cef_types=["splunk query"], + ) + display: str = Param(description="Display fields (comma-separated)", required=False, default="") + parse_only: bool = Param(description="Parse only", required=False, default=False) + add_raw_field: bool = Param(description="Ingest _raw field data", required=False, default=True) + attach_result: bool = Param(description="Attach result to the vault", required=False, default=False) + start_time: str = Param(description="Earliest time modifier", required=False, default="") + end_time: str = Param(description="Latest time modifier", required=False, default="") + search_mode: str = Param( + description="Search mode", + required=False, + value_list=["fast", "verbose", "smart"], + default="smart", + ) + time_format: str = Param(description="Custom timestamp format", required=False, default="") + + +class RunQueryOutput(PermissiveActionOutput): + pass + + +class RunQuerySummary(ActionOutput): + sid: str | None = None + total_events: int | None = None + + +class GetHostEventsParams(Params): + ip_hostname: str = Param( + description="Hostname/IP to search the events of", + required=True, + primary=True, + cef_types=["ip", "host name"], + ) + last_n_days: str = Param(description="Number of days ago", required=False, default="") + + +class GetHostEventsOutput(PermissiveActionOutput): + host: str | None = OutputField(column_name="Host") + time: str | None = OutputField(column_name="Time", alias="_time") + raw: str | None = OutputField(column_name="Raw", alias="_raw") + + +class GetHostEventsSummary(ActionOutput): + sid: str | None = None + total_events: int | None = None + + +class UpdateEventParams(Params): + event_ids: str = Param( + description="Event ID to update", + required=True, + primary=True, + cef_types=["splunk notable event id"], + ) + owner: str = Param(description="New owner for the event", required=False, default="") + status: str = Param( + description="New status for the event", + required=False, + default="", + value_list=["", "unassigned", "new", "in progress", "pending", "resolved", "closed"], + ) + integer_status: str = Param(description="Integer representing custom status value", required=False, default="") + urgency: str = Param( + description="New urgency for the event", + required=False, + default="", + value_list=["", "informational", "low", "medium", "high", "critical"], + ) + comment: str = Param(description="New comment for the event", required=False, default="") + disposition: str = Param( + description="New disposition field", + required=False, + default="", + value_list=[ + "", "Unassigned", "True Positive - Suspicious Activity", + "Benign Positive - Suspicious But Expected", + "False Positive - Incorrect Analytic Logic", + "False Positive - Inaccurate Data", "Undetermined", "Other", + ], + ) + integer_disposition: str = Param(description="Integer representing custom disposition value", required=False, default="") + wait_for_confirmation: bool = Param(description="Validate event_ids", required=False, default=False) + + +class UpdateEventOutput(ActionOutput): + status: str | None = OutputField(column_name="Status") + failure_count: int | None = None + message: str | None = OutputField(column_name="Message") + success: bool | None = None + success_count: int | None = None + + +class UpdateEventSummary(ActionOutput): + sid: str | None = None + updated_event_id: str | None = None + + +class PostDataParams(Params): + data: str = Param(description="Data to post", required=True) + host: str = Param( + description="Host for event", + required=False, + default="", + primary=True, + cef_types=["ip", "host name"], + ) + index: str = Param(description="Index to send event to", required=False, default="") + source: str = Param(description="Source for event", required=False, default="Phantom") + source_type: str = Param( + description="Type of source for event", + required=False, + default="Automation/Orchestration Platform", + ) + + +class PostDataOutput(ActionOutput): + status: str | None = OutputField(column_name="Status") + message: str | None = OutputField(column_name="Message") + + +# --------------------------------------------------------------------------- +# Custom view for run query +# --------------------------------------------------------------------------- +@app.view_handler(template="splunk_run_query.html") +def display_view(outputs: list[RunQueryOutput]) -> dict: + if not outputs: + return {"results": [{"data": {}, "param": {}}]} + + first = outputs[0].model_dump(exclude_none=True) + param = { + "query": first.get("_param_query", ""), + "command": first.get("_param_command", ""), + "display": first.get("_param_display", ""), + "parse_only": first.get("_param_parse_only", False), + "search_mode": first.get("_param_search_mode", "smart"), + } + display_fields = param.get("display", "") + + all_data = [] + for output in outputs: + data = {k: v for k, v in output.model_dump(exclude_none=True).items() if not k.startswith("_")} + all_data.append(data) + + if display_fields: + headers = [x.strip() for x in display_fields.split(",") if x.strip()] + elif all_data: + headers = [k for k in all_data[0] if not k.startswith("_")] + else: + headers = [] + + processed_data = [{h: item.get(h) for h in headers} for item in all_data] + + return { + "results": [{ + "param": param, + "data": all_data or {}, + "processed_data": processed_data, + "headers": headers, + }], + } + + +# --------------------------------------------------------------------------- +# Test Connectivity +# --------------------------------------------------------------------------- +@app.test_connectivity() +def test_connectivity(soar: SOARClient, asset: Asset) -> None: + helper = SplunkHelper(asset) + helper.validate_asset() + + try: + helper.connect() + except Exception as e: + soar.set_message(SPLUNK_ERR_CONNECTIVITY_TEST) + raise RuntimeError(f"{SPLUNK_ERR_CONNECTIVITY_TEST}: {e}") from e + + version = helper.get_server_version() + if version == "FAILURE": + soar.set_message(SPLUNK_ERR_CONNECTIVITY_TEST) + raise RuntimeError(SPLUNK_ERR_CONNECTIVITY_TEST) + + is_es = helper.check_for_es() + logger.progress("Detected Splunk %sserver version %s", "ES " if is_es else "", version) + soar.set_message(SPLUNK_SUCCESS_CONNECTIVITY_TEST) + logger.info(SPLUNK_SUCCESS_CONNECTIVITY_TEST) + + +# --------------------------------------------------------------------------- +# Run Query +# --------------------------------------------------------------------------- +@app.action( + description="Run a search query on the Splunk device. Please escape any quotes that are part of the query string", + action_type="investigate", + read_only=True, + view_handler=display_view, + summary_type=RunQuerySummary, +) +def run_query(params: RunQueryParams, soar: SOARClient, asset: Asset) -> list[RunQueryOutput]: + helper = SplunkHelper(asset) + helper.validate_asset() + helper.connect() + + search_mode = params.search_mode or SPLUNK_SEARCH_MODE_SMART + kwargs: dict = {"adhoc_search_level": search_mode} + if params.start_time: + kwargs["earliest_time"] = params.start_time + if params.end_time: + kwargs["latest_time"] = params.end_time + if params.time_format: + kwargs["time_format"] = params.time_format + + search_command = params.command + search_string = params.query + + if not search_command: + if search_string[0] != "|" and not search_string.startswith("search"): + search_string = f"search {search_string.strip()}" + search_query = search_string + else: + search_query = f"{search_command.strip()} {search_string.strip()}" + + sid, results_list = helper.run_query( + search_query, + kwargs_create=kwargs, + parse_only=params.parse_only, + add_raw_field=params.add_raw_field, + ) + + if params.attach_result: + _attach_json_result(soar, results_list) + + soar.set_summary(RunQuerySummary(sid=sid, total_events=len(results_list))) + soar.set_message(f"Sid: {sid}, Total events: {len(results_list)}") + + param_info = { + "_param_query": params.query, + "_param_command": params.command, + "_param_display": params.display, + "_param_parse_only": params.parse_only, + "_param_search_mode": search_mode, + } + return [RunQueryOutput(**{**r, **param_info}) for r in results_list] + + +def _attach_json_result(soar: SOARClient, data: list[dict]): + try: + container_id = soar.get_executing_container_id() + soar.vault.create_attachment( + container_id=container_id, + file_content=json.dumps(data), + file_name="splunk_run_query_result.json", + ) + except Exception as e: + logger.error("Error attaching results to vault: %s", e) + + +# --------------------------------------------------------------------------- +# Get Host Events +# --------------------------------------------------------------------------- +@app.action( + description="Get events pertaining to a host that have occurred in the last 'N' days", + action_type="investigate", + read_only=True, + render_as="table", + summary_type=GetHostEventsSummary, +) +def get_host_events(params: GetHostEventsParams, soar: SOARClient, asset: Asset) -> list[GetHostEventsOutput]: + helper = SplunkHelper(asset) + helper.validate_asset() + helper.connect() + + ip_hostname = params.ip_hostname + last_n_days = SplunkHelper.validate_integer(params.last_n_days, "'last_n_days' action") + + search_query = f'search host="{ip_hostname}"' + if last_n_days: + search_query += f" earliest=-{last_n_days}d" + + sid, results_list = helper.run_query(search_query) + + soar.set_summary(GetHostEventsSummary(sid=sid, total_events=len(results_list))) + soar.set_message(f"Sid: {sid}, Total events: {len(results_list)}") + + return [GetHostEventsOutput(**r) for r in results_list] + + +# --------------------------------------------------------------------------- +# Update Event +# --------------------------------------------------------------------------- +@app.action( + description="Update a notable event", + action_type="generic", + read_only=False, + render_as="table", + summary_type=UpdateEventSummary, +) +def update_event(params: UpdateEventParams, soar: SOARClient, asset: Asset) -> list[UpdateEventOutput]: + helper = SplunkHelper(asset) + helper.validate_asset() + + if not helper.check_for_es(): + raise RuntimeError(SPLUNK_ERR_NOT_ES) + + ids = params.event_ids + owner = params.owner + status = params.status + comment = params.comment + urgency = params.urgency + disposition = params.disposition or "" + wait_for_confirmation = params.wait_for_confirmation + + integer_status = SplunkHelper.validate_integer( + params.integer_status, "'integer_status' action", allow_zero=True + ) + integer_disposition = SplunkHelper.validate_integer( + params.integer_disposition, "'integer_disposition' action", allow_zero=True + ) + + if not any([comment, status, urgency, owner, disposition]) and integer_status is None and integer_disposition is None: + raise ValueError(SPLUNK_ERR_NEED_PARAM) + + splunk_status_dict: dict[str, int] = {} + splunk_disposition_dict: dict[str, int] = {} + + if status or integer_status is not None: + splunk_status_dict = helper.get_status_dict("notable") + if not splunk_status_dict: + raise RuntimeError("Error occurred while fetching Splunk event status") + + if disposition or integer_disposition is not None: + splunk_disposition_dict = helper.get_status_dict("disposition") + if not splunk_disposition_dict: + raise RuntimeError("Error occurred while fetching Splunk event disposition") + + helper.connect() + + # Resolve SID+RID combo to event_id + regexp = re.compile(r"\+\d*(\.\d+)?[\"$]") + if regexp.search(json.dumps(ids)): + logger.progress("Interpreting the event ID as an SID + RID combo") + try: + ids = helper.resolve_event_id(ids) + except Exception: + raise RuntimeError("Unable to find underlying event_id from SID + RID combo") from None + + if wait_for_confirmation: + search_query = f"search `notable_by_id({ids})`" + _sid, validate_results = helper.run_query(search_query) + if not validate_results: + raise ValueError("Please provide a valid event ID") + + request_body: dict = {"ruleUIDs": ids} + + # Status + if integer_status is not None: + if int(integer_status) not in list(splunk_status_dict.values()): + raise ValueError( + "Please provide a valid value in 'integer_status' action parameter. " + f"Valid values: {', '.join(map(str, splunk_status_dict.values()))}" + ) + request_body["status"] = str(integer_status) + elif status: + if status not in splunk_status_dict: + if not status.isdigit(): + raise ValueError(SPLUNK_ERR_BAD_STATUS) + request_body["status"] = status + else: + request_body["status"] = splunk_status_dict[status] + + # Disposition + if integer_disposition is not None: + if int(integer_disposition) not in splunk_disposition_dict.values(): + raise ValueError( + "Please provide a valid value in 'integer_disposition' action parameter. " + f"Valid values: {', '.join(map(str, splunk_disposition_dict.values()))}" + ) + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(integer_disposition) + elif disposition: + if disposition not in splunk_disposition_dict: + if not disposition.isdigit(): + raise ValueError(SPLUNK_ERR_BAD_DISPOSITION) + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(disposition) + else: + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(splunk_disposition_dict[disposition]) + + param_mapping = {"urgency": urgency, "comment": comment, "newOwner": owner} + request_body.update({k: v for k, v in param_mapping.items() if v}) + + resp_data = helper.make_rest_call_retry("notable_update", request_body) + + if resp_data and "success" in resp_data and not resp_data.get("success"): + msg = resp_data.get("message") + raise RuntimeError(msg if msg else "Unable to update the notable event") + + soar.set_summary(UpdateEventSummary(updated_event_id=ids)) + + if wait_for_confirmation: + msg = f"Updated Event ID: {ids}" + else: + msg = ( + f"Updated Event ID: {ids}. The event_id has not been verified. " + "Please confirm that the provided event_id corresponds to an actual notable event" + ) + + soar.set_message(msg) + + if resp_data: + resp_data["status"] = "success" + resp_data["message"] = msg + return [UpdateEventOutput(**resp_data)] + return [UpdateEventOutput(status="success", message=msg)] + + +# --------------------------------------------------------------------------- +# Post Data +# --------------------------------------------------------------------------- +@app.action( + description="Post data to Splunk", + action_type="generic", + read_only=False, + render_as="table", +) +def post_data(params: PostDataParams, soar: SOARClient, asset: Asset) -> list[PostDataOutput]: + helper = SplunkHelper(asset) + helper.validate_asset() + + try: + post_bytes = UnicodeDammit(params.data).unicode_markup.encode("utf-8") + except Exception as e: + logger.error("Error while encoding data: %s", e) + post_bytes = params.data.encode("utf-8") + + get_params: dict[str, str] = { + "source": params.source or SPLUNK_DEFAULT_SOURCE, + "sourcetype": params.source_type or SPLUNK_DEFAULT_SOURCE_TYPE, + } + if params.host: + get_params["host"] = params.host + if params.index: + get_params["index"] = params.index + + helper.make_rest_call_retry("receivers/simple", post_bytes, params=get_params) + + soar.set_message("Successfully posted the data") + return [PostDataOutput(status="success", message="Successfully posted the data")] + + +# --------------------------------------------------------------------------- +# On Poll +# --------------------------------------------------------------------------- +@app.on_poll() +def on_poll(params: OnPollParams, soar: SOARClient, asset: Asset) -> Iterator[Container | Artifact]: + helper = SplunkHelper(asset) + helper.validate_asset() + helper.connect() + + search_command = asset.on_poll_command + search_string = asset.on_poll_query + po = asset.on_poll_parse_only + include_cim_fields = asset.include_cim_fields + use_event_id_sdi = asset.use_event_id_sdi + + if not search_string: + raise ValueError("Need to specify Query String to use polling") + + try: + if not search_command: + if search_string[0] != "|" and not search_string.startswith("search"): + search_string = f"search {search_string.strip()}" + search_query = search_string + else: + search_query = f"{search_command.strip()} {search_string.strip()}" + except Exception: + raise ValueError("Error occurred while parsing the search query") from None + + search_params: dict = {} + state = asset.ingest_state + is_poll_now = params.is_manual_poll() + + if is_poll_now: + search_params["max_count"] = params.container_count or 100 + else: + search_params["max_count"] = asset.max_container + start_time = state.get("start_time") + if start_time: + search_params["index_earliest"] = start_time + + if int(search_params["max_count"]) <= 0: + logger.debug("container_count <= 0, ignoring max_count") + search_params.pop("max_count") + + try: + _sid, results_list = helper.run_query(search_query, kwargs_create=search_params, parse_only=po) + except Exception as e: + msg = str(e) + if "Invalid index_earliest" in msg: + logger.debug("Invalid start_time %s, retrying without it", search_params.get("index_earliest")) + state.pop("start_time", None) + raise + + display = asset.on_poll_display + header_set = None + if display: + header_set = [x.strip().lower() for x in display.split(",")] + + data = list(reversed(results_list)) + logger.info("Total %d event(s) fetched", len(data)) + + container_name_prefix = asset.container_name_prefix or "" + raw_values = asset.container_name_values + container_name_values = [x.strip() for x in raw_values.split(",")] if raw_values else [] + + count = 1 + for item in data: + try: + cef: dict = {} + if "_serial" in item: + item.pop("_serial") + + if header_set: + name_mappings = {k.lower(): k for k in item if k.lower() in header_set} + for h in header_set: + cef_name = CIM_CEF_MAP.get(h, h) + cef_name = name_mappings.get(cef_name, cef_name) + cef_key_value = name_mappings.get(h, h) + cef[cef_name] = item.get(cef_key_value) + if include_cim_fields: + cef[cef_key_value] = item.get(cef_key_value) + else: + for k, v in item.items(): + cef[CIM_CEF_MAP.get(k, k)] = v + if include_cim_fields: + cef[k] = v + + if use_event_id_sdi and "event_id" in item: + sdi = item["event_id"] + else: + if use_event_id_sdi and "event_id" not in item: + logger.warning("use_event_id_sdi enabled but event_id missing, using hash") + input_str = UnicodeDammit(json.dumps(item)).unicode_markup.encode("utf-8") + if _get_fips_enabled(): + sdi = hashlib.sha256(input_str).hexdigest() + else: + sdi = hashlib.md5(input_str).hexdigest() # noqa: S324 + + severity = _get_splunk_severity(item) + spl_event_start = _get_event_start(item.get("_time")) + container_name = _get_splunk_title(item, container_name_prefix, container_name_values) + + yield Container( + name=container_name, + severity=severity, + source_data_identifier=sdi, + ) + + if asset.remove_empty_cef: + cef = {k: v for k, v in cef.items() if v is not None} + + yield Artifact( + cef=cef, + name="Field Values", + source_data_identifier=sdi, + severity=severity, + start_time=spl_event_start, + ) + + if count == asset.container_update_state and not is_poll_now: + state["start_time"] = item.get("_indextime") + count = 0 + count += 1 + + except Exception as e: + logger.error("Error processing event: %s", e) + continue + + if data and not is_poll_now: + state["start_time"] = data[-1].get("_indextime") + + +if __name__ == "__main__": + app.cli() diff --git a/splunk_consts.py b/src/splunk_consts.py similarity index 56% rename from splunk_consts.py rename to src/splunk_consts.py index 9e2e1c3..ff4f790 100644 --- a/splunk_consts.py +++ b/src/splunk_consts.py @@ -1,6 +1,4 @@ -# File: splunk_consts.py -# -# Copyright (c) 2016-2025 Splunk Inc. +# Copyright (c) 2016-2026 Splunk Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,23 +10,21 @@ # 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. -# -# -# Success/Error status and messages + +# Success/Error messages SPLUNK_ERR_INVALID_QUERY = "Query invalid '{query}'" SPLUNK_ERR_BAD_STATUS = "The supplied status is invalid" SPLUNK_ERR_BAD_DISPOSITION = "The supplied disposition is invalid" SPLUNK_ERR_CONNECTIVITY_TEST = "Connectivity test failed" SPLUNK_SUCCESS_CONNECTIVITY_TEST = "Connectivity test passed" -SPLUNK_ERR_NOT_JSON = "Splunk server response was not JSON" -SPLUNK_ERR_NOT_200 = "Splunk server returned error from API call" SPLUNK_ERR_CONNECTIVITY_FAILED = "Failed to connect to splunk server" SPLUNK_ERR_UNABLE_TO_CREATE_JOB = "Failed to get a job id from splunk server" -SPLUNK_ERR_GET_EVENTS = "Error getting events for alert '{ss_name}' having sid '{sid}'" SPLUNK_ERR_NOT_ES = "This instance does not seem to be Splunk ES. This action cannot be run" SPLUNK_ERR_INVALID_TIME_RANGE = "Invalid Time range specified, where the end time is less than start time" -SPLUNK_ERR_NEED_PARAM = "One of comment, status, integer_status, disposition, integer_disposition, urgency, or owner parameters needs \ - to be supplied to run this action" +SPLUNK_ERR_NEED_PARAM = ( + "One of comment, status, integer_status, disposition, integer_disposition, " + "urgency, or owner parameters needs to be supplied to run this action" +) SPLUNK_ERR_INVALID_INTEGER = "Please provide a valid integer value in the {param} parameter" SPLUNK_ERR_NON_NEGATIVE_INTEGER = "Please provide a valid non-negative integer value in the {param} parameter" SPLUNK_ERR_INVALID_PARAM = "Please provide non-zero positive integer in {param}" @@ -36,57 +32,20 @@ SPLUNK_EXCEPTION_ERR_MESSAGE = "{msg}. {error_text}" SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE = "{field} not found" SPLUNK_ERR_INVALID_SLEEP_TIME = "Please provide a value <= 120 seconds in the {param} parameter" -SPLUNK_ERR_REQUIRED_CONFIG_PARAMS = "Please provide either API token or username and password in the asset \ - configuration parameters for authentication" -SPLUNK_STATE_FILE_CORRUPT_ERR = ( - "Error occurred while loading the state file due to its unexpected format. " - "Resetting the state file with the default format. Please try again." +SPLUNK_ERR_REQUIRED_CONFIG_PARAMS = ( + "Please provide either API token or username and password in the asset " + "configuration parameters for authentication" ) +SPLUNK_ERR_SPLUNK_JOB_HAS_TIMED_OUT = "Failed to retrieve splunk job results. The splunk job has timed out." SPLUNK_ERR_UNABLE_TO_PARSE_JSON_RESPONSE = "Unable to parse response as JSON. {error}" SPLUNK_ERR_UNABLE_TO_PARSE_HTML_RESPONSE = "Unable to parse HTML response. {error}" SPLUNK_ERR_EMPTY_RESPONSE = "Status Code {code}. Empty response and no information in the header." -SPLUNK_ERR_SPLUNK_JOB_HAS_TIMED_OUT = "Failed to retrieve splunk job results. The splunk job has timed out." # Progress messages -SPLUNK_PROG_GOT_JOB_ID = "Got job id '{job_id}'" -SPLUNK_PROG_TIME_RANGE = "Using range '{range}'" SPLUNK_PROG_CREATED_QUERY = "Created query '{query}'" SPLUNK_PROG_CREATING_SEARCH_JOB = "Creating search job" -SPLUNK_PROG_WAITING_ON_JOB_ID = "Waiting for job (id:{job_id}) to finish" -SPLUNK_PROG_CHECKING_STATUS_OF_JOB_ID = "Checking status of job id '{job_id}'" -SPLUNK_PROG_JOB_ID_DONE_RETRIEVING_RESULTS = "Retrieving results for job id '{job_id}'" - -# Json keys -SPLUNK_JSON_COMMAND = "command" -SPLUNK_JSON_PARSE_ONLY = "parse_only" -SPLUNK_JSON_ADD_RAW_DATA = "add_raw_field" -SPLUNK_JSON_HOST = "host" -SPLUNK_JSON_DATA = "data" -SPLUNK_JSON_INDEX = "index" -SPLUNK_JSON_QUERY = "query" -SPLUNK_JSON_COUNT = "count" -SPLUNK_JSON_OWNER = "owner" -SPLUNK_JSON_SOURCE = "source" -SPLUNK_JSON_STATUS = "status" -SPLUNK_JSON_URGENCY = "urgency" -SPLUNK_JSON_COMMENT = "comment" -SPLUNK_JSON_ALERT_NAME = "alert" -SPLUNK_JSON_END_TIME = "end_time" -SPLUNK_JSON_TIMEZONE = "timezone" -SPLUNK_JSON_EVENT_IDS = "event_ids" -SPLUNK_JSON_START_TIME = "start_time" -SPLUNK_JSON_SOURCE_TYPE = "source_type" -SPLUNK_JSON_LAST_N_DAYS = "last_n_days" -SPLUNK_JSON_TOTAL_EVENTS = "total_events" -SPLUNK_JSON_UPDATED_EVENT_ID = "updated_event_id" -SPLUNK_JSON_ATTACH_RESULT = "attach_result" -SPLUNK_JSON_SEARCH_MODE = "search_mode" -SPLUNK_JSON_API_KEY = "api_token" # pragma: allowlist secret -SPLUNK_JSON_TIME_FORMAT = "time_format" # Default values -SPLUNK_DEFAULT_EVENT_COUNT = 10 -SPLUNK_DEFAULT_ALERT_COUNT = 100 SPLUNK_DEFAULT_SOURCE = "Phantom" SPLUNK_DEFAULT_SOURCE_TYPE = "Automation/Orchestration Platform" @@ -95,9 +54,14 @@ SPLUNK_NUMBER_OF_DAYS_BEFORE_ENDTIME = 10 # Dictionaries -SPLUNK_SEVERITY_MAP = {"informational": "low", "low": "low", "medium": "medium", "high": "high", "critical": "high"} +SPLUNK_SEVERITY_MAP = { + "informational": "low", + "low": "low", + "medium": "medium", + "high": "high", + "critical": "high", +} -# This will map certain splunk CIM fields to their CEF equivalent CIM_CEF_MAP = { "action": "act", "action_name": "act", @@ -142,34 +106,16 @@ "user_id": "destinationUserId", } -SPLUNK_INVALID_COMMAND = "Streaming/Transforming command operates on the events returned by some search.\ - So for using (eval, stats, table) commands, user should provide 'search' in 'command' parameter \ - and provide whole query in the 'query' parameter" - -# Validation keys -SPLUNK_INT_STATUS_KEY = "'integer_status' action" -SPLUNK_INT_DISPOSITION_KEY = "'integer_disposition' action" -SPLUNK_RETRY_COUNT_KEY = "'retry_count' configuration" -SPLUNK_PORT_KEY = "'port' configuration" -SPLUNK_MAX_CONTAINER_KEY = "'max_container' configuration" -SPLUNK_CONTAINER_UPDATE_STATE_KEY = "'Container count to update the state file' configuration" -SPLUNK_LAST_N_DAYS_KEY = "'last_n_days' action" -SPLUNK_SLEEPTIME_IN_REQUESTS_KEY = "'The time to wait for next REST call (max 120 seconds)' configuration" -SPLUNK_JOB_TIMEOUT_KEY = "'The duration in seconds to wait before a scheduled Splunk job times out' configuration" - # Queries SPLUNK_RID_SID_NOTABLE_QUERY = r'search [| makeresults | eval myfield = "{}"' SPLUNK_RID_SID_NOTABLE_QUERY += r' | rex field=myfield "^(?.*)\+(?\d*(\.\d+)?)"' SPLUNK_RID_SID_NOTABLE_QUERY += r' | eval search = "( (sid::" . sid . " OR orig_sid::" . sid . ")' SPLUNK_RID_SID_NOTABLE_QUERY += r' (rid::" . rid . " OR orig_rid::" . rid . ") )"' SPLUNK_RID_SID_NOTABLE_QUERY += r" | table search] `notable` | table event_id" -SPLUNK_SEARCH_AUDIT_INDEX_QUERY = "search index=_audit action=alert_fired {0} | head {1} | \ - fields ss_name sid trigger_time severity" SPLUNK_DEFAULT_REQUEST_TIMEOUT = 60 # in seconds # Search Modes SPLUNK_SEARCH_MODE_SMART = "smart" -SPLUNK_SEARCH_MODE_FAST = "fast" -SPLUNK_SEARCH_MODE_VERBOSE = "verbose" + SPLUNK_DISPOSITION_QUERY_FORMAT = "disposition:{}" diff --git a/splunk_run_query.html b/templates/splunk_run_query.html similarity index 63% rename from splunk_run_query.html rename to templates/splunk_run_query.html index c678206..dd7c649 100644 --- a/splunk_run_query.html +++ b/templates/splunk_run_query.html @@ -1,104 +1,86 @@ -{% extends 'widgets/widget_template.html' %} -{% load custom_template %} -{% block custom_title_prop %} - {% if title_logo %} - style="background-size: auto 60%; background-position: 50%; background-repeat: no-repeat; background-image: url('/app_resource/{{ title_logo }}');" - {% endif %} -{% endblock %} -{% block title1 %}{{ title1 }}{% endblock %} -{% block title2 %}{{ title2 }}{% endblock %} -{% block custom_tools %}{% endblock %} -{% block widget_content %} - - +{% extends 'base/logo_header.html' %} +{% block widget_content %}
- + padding-left: 10px; + padding-right: 10px"> {% for result in results %} -
- {% if not result.data %}

No data found

{% elif not result.headers %} @@ -137,7 +119,6 @@

Info

Results

- @@ -148,12 +129,12 @@

Results

{% for item in result.processed_data %} {% for header in result.headers %} - {% for k, v in item.items %} + {% for k, v in item.items() %} {% if header == k %} {% if k == 'splunk_server' or k == 'host' %}
+ onclick="context_menu(this, [{'contains': ['host name'], 'value': '{{ v }}' }], 0, {{ container }}, null, false);"> {{ v }}   @@ -172,9 +153,7 @@

Results

{% endif %} {% endfor %} - - {% endblock %} - diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..43ec43f --- /dev/null +++ b/uv.lock @@ -0,0 +1,1318 @@ +version = 1 +revision = 3 +requires-python = ">=3.13, <3.15" +resolution-markers = [ + "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +supported-markers = [ + "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version < '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'arm64' and sys_platform == 'darwin'", +] +required-markers = [ + "python_full_version < '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", + "python_full_version >= '3.14' and platform_machine == 'x86_64' and sys_platform == 'linux'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "idna", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "authlib" +version = "1.7.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "cryptography", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "joserfc", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/82/4d0603f30c1b4629b1f091bb266b0d7986434891d6940a8c87f8098db24e/authlib-1.7.0.tar.gz", hash = "sha256:b3e326c9aa9cc3ea95fe7d89fd880722d3608da4d00e8a27e061e64b48d801d5", size = 175890, upload-time = "2026-04-18T11:00:28.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/48/c954218b2a250e23f178f10167c4173fecb5a75d2c206f0a67ba58006c26/authlib-1.7.0-py2.py3-none-any.whl", hash = "sha256:e36817afb02f6f0b6bf55f150782499ddd6ddf44b402bb055d3263cc65ac9ae0", size = 258779, upload-time = "2026-04-18T11:00:26.64Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.13.5" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "soupsieve", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/2e/3e5079847e653b1f6dc647aa24549d68c6addb4c595cc0d902d1b19308ad/beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695", size = 622954, upload-time = "2025-08-24T14:06:13.168Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/eb/f4151e0c7377a6e08a38108609ba5cede57986802757848688aeedd1b9e8/beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a", size = 105113, upload-time = "2025-08-24T14:06:14.884Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "webencodings", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[[package]] +name = "build" +version = "1.4.3" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "packaging", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyproject-hooks", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/16/4b272700dea44c1d2e8ca963ebb3c684efe22b3eba8cfa31c5fdb60de707/build-1.4.3.tar.gz", hash = "sha256:5aa4231ae0e807efdf1fd0623e07366eca2ab215921345a2e38acdd5d0fa0a74", size = 89314, upload-time = "2026-04-10T21:25:40.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/30/f169e1d8b2071beaf8b97088787e30662b1d8fb82f8c0941d14678c0cbf1/build-1.4.3-py3-none-any.whl", hash = "sha256:1bc22b19b383303de8f2c8554c9a32894a58d3f185fe3756b0b20d255bee9a38", size = 26171, upload-time = "2026-04-10T21:25:39.671Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "pycparser", marker = "(implementation_name != 'PyPy' and platform_machine == 'arm64' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and platform_machine == 'x86_64' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and platform_machine == 'aarch64' and sys_platform == 'linux') or (implementation_name != 'PyPy' and platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[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 = "colorclass" +version = "2.2.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/1a/31ff00a33569a3b59d65bbdc445c73e12f92ad28195b7ace299f68b9af70/colorclass-2.2.2.tar.gz", hash = "sha256:6d4fe287766166a98ca7bc6f6312daf04a0481b1eda43e7173484051c0ab4366", size = 16709, upload-time = "2021-12-09T00:41:35.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/b6/daf3e2976932da4ed3579cff7a30a53d22ea9323ee4f0d8e43be60454897/colorclass-2.2.2-py2.py3-none-any.whl", hash = "sha256:6f10c273a0ef7a1150b1120b6095cbdd68e5cf36dfd5d0fc957a2500bbf99a55", size = 18995, upload-time = "2021-12-09T00:41:34.653Z" }, +] + +[[package]] +name = "compressed-rtf" +version = "1.0.7" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/0c/929a4e8ef9d7143f54d77dadb5f370cc7b98534b1bd6e1124d0abe8efb24/compressed_rtf-1.0.7.tar.gz", hash = "sha256:7c30859334839f3cdc7d10796af5b434bb326b9df7cb5a65e95a8eacb2951b0e", size = 8152, upload-time = "2025-03-24T22:39:32.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/1d/62f5bf92e12335eb63517f42671ed78512d48bbc69e02a942dd7b90f03f0/compressed_rtf-1.0.7-py3-none-any.whl", hash = "sha256:b7904921d78c67a0a4b7fff9fb361a00ae2b447b6edca010ce321cd98fa0fcc0", size = 7968, upload-time = "2025-03-24T23:03:57.433Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "cffi", marker = "(platform_machine == 'arm64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "packaging", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[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.9.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "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 = "easygui" +version = "0.98.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/ad/e35f7a30272d322be09dc98592d2f55d27cc933a7fde8baccbbeb2bd9409/easygui-0.98.3.tar.gz", hash = "sha256:d653ff79ee1f42f63b5a090f2f98ce02335d86ad8963b3ce2661805cafe99a04", size = 85583, upload-time = "2022-04-01T13:15:50.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/a7/b276ff776533b423710a285c8168b52551cb2ab0855443131fdc7fd8c16f/easygui-0.98.3-py2.py3-none-any.whl", hash = "sha256:33498710c68b5376b459cd3fc48d1d1f33822139eb3ed01defbc0528326da3ba", size = 92655, upload-time = "2022-04-01T13:15:49.568Z" }, +] + +[[package]] +name = "ebcdic" +version = "1.1.1" +source = { registry = "https://pypi.python.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/2f/633031205333bee5f9f93761af8268746aa75f38754823aabb8570eb245b/ebcdic-1.1.1-py2.py3-none-any.whl", hash = "sha256:33b4cb729bc2d0bf46cc1847b0e5946897cb8d3f53520c5b9aa5fa98d7e735f1", size = 128537, upload-time = "2019-08-09T00:54:35.544Z" }, +] + +[[package]] +name = "extract-msg" +version = "0.55.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "beautifulsoup4", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "compressed-rtf", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "ebcdic", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "olefile", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "red-black-tree-mod", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "rtfde", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tzlocal", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/65/c70afb3b119a44b3ee36b029485dc15326cf3a7c50da19a1ecbbf949c5d1/extract_msg-0.55.0.tar.gz", hash = "sha256:cf08283498c3dfcc7f894dad1579f52e3ced9fb76b865c2355cbe757af8a54e1", size = 331170, upload-time = "2025-08-12T16:07:56.537Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/81/87d5241036046ea17c5c8db228f4c9e04e07e53b627015d4496a99449aaf/extract_msg-0.55.0-py3-none-any.whl", hash = "sha256:baf0cdee9a8d267b70c366bc57ceb03dbfa1e7ab2dca6824169a7fe623f0917c", size = 336033, upload-time = "2025-08-12T16:07:54.886Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[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 = "hatchling" +version = "1.29.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "packaging", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pathspec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pluggy", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "trove-classifiers", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/9c/b4cfe330cd4f49cff17fd771154730555fa4123beb7f292cf0098b4e6c20/hatchling-1.29.0.tar.gz", hash = "sha256:793c31816d952cee405b83488ce001c719f325d9cda69f1fc4cd750527640ea6", size = 55656, upload-time = "2026-02-23T19:42:06.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/8a/44032265776062a89171285ede55a0bdaadc8ac00f27f0512a71a9e3e1c8/hatchling-1.29.0-py3-none-any.whl", hash = "sha256:50af9343281f34785fab12da82e445ed987a6efb34fd8c2fc0f6e6630dbcc1b0", size = 76356, upload-time = "2026-02-23T19:42:05.197Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "certifi", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "h11", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (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 = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "certifi", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpcore", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "idna", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (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 = "httpx-retries" +version = "0.5.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "httpx", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/f5/046cac13877ce9b55aebdbb3999e0e45b19b989a95c5fd1040fa04bd1f92/httpx_retries-0.5.0.tar.gz", hash = "sha256:d8c8e1e0852d84be3837aba0bcf78aeb89a4b77db95e8cc988c8c058830b3044", size = 15647, upload-time = "2026-04-20T01:21:47.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/a8/aadeaa9a28510727d538636ee8688f0782a98523147852b29404ce696f1b/httpx_retries-0.5.0-py3-none-any.whl", hash = "sha256:d3124592979a9dc6197e666d1f02e9ab996a0c58fce59fad8db6201a6a87304e", size = 8908, upload-time = "2026-04-20T01:21:46.157Z" }, +] + +[[package]] +name = "humanize" +version = "4.15.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "markupsafe", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (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 = "joserfc" +version = "1.6.4" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "cryptography", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/c6/de8fdbdfa75c8ca04fead38a82d573df8a82906e984c349d58665f459558/joserfc-1.6.4.tar.gz", hash = "sha256:34ce5f499bfcc5e9ad4cc75077f9278ab3227b71da9aaf28f9ab705f8a560d3c", size = 231866, upload-time = "2026-04-13T13:15:40.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "librt" +version = "0.9.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "mdurl", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, +] + +[[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 = "msoffcrypto-tool" +version = "6.0.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "cryptography", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "olefile", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/34/6250bdddaeaae24098e45449ea362fb3555a65fba30cad0ad5630ea48d1a/msoffcrypto_tool-6.0.0.tar.gz", hash = "sha256:9a5ebc4c0096b42e5d7ebc2350afdc92dc511061e935ca188468094fdd032bbe", size = 40593, upload-time = "2026-01-12T08:59:56.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/85/9e359fa9279e1d6861faaf9b6f037a3226374deb20a054c3937be6992013/msoffcrypto_tool-6.0.0-py3-none-any.whl", hash = "sha256:46c394ed5d9641e802fc79bf3fb0666a53748b23fa8c4aa634ae9d30d46fe397", size = 48791, upload-time = "2026-01-12T08:59:55.394Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "librt", marker = "(platform_machine == 'arm64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, + { name = "mypy-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pathspec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[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.10.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "olefile" +version = "0.47" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/1b/077b508e3e500e1629d366249c3ccb32f95e50258b231705c09e3c7a4366/olefile-0.47.zip", hash = "sha256:599383381a0bf3dfbd932ca0ca6515acd174ed48870cbf7fee123d698c192c1c", size = 112240, upload-time = "2023-12-01T16:22:53.025Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d3/b64c356a907242d719fc668b71befd73324e47ab46c8ebbbede252c154b2/olefile-0.47-py2.py3-none-any.whl", hash = "sha256:543c7da2a7adadf21214938bb79c83ea12b473a4b6ee4ad4bf854e7715e13d1f", size = 114565, upload-time = "2023-12-01T16:22:51.518Z" }, +] + +[[package]] +name = "oletools" +version = "0.60.2" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "colorclass", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "easygui", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "msoffcrypto-tool", marker = "(platform_machine == 'arm64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "olefile", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pcodedmp", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyparsing", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2f/037f40e44706d542b94a2312ccc33ee2701ebfc9a83b46b55263d49ce55a/oletools-0.60.2.zip", hash = "sha256:ad452099f4695ffd8855113f453348200d195ee9fa341a09e197d66ee7e0b2c3", size = 3433750, upload-time = "2024-07-02T14:50:38.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/ff/05257b7183279b80ecec6333744de23f48f0faeeba46c93e6d13ce835515/oletools-0.60.2-py2.py3-none-any.whl", hash = "sha256:72ad8bd748fd0c4e7b5b4733af770d11543ebb2bf2697455f99f975fcd50cc96", size = 989449, upload-time = "2024-07-02T14:50:29.122Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[package]] +name = "pcodedmp" +version = "1.2.6" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "oletools", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/20/6d461e29135f474408d0d7f95b2456a9ba245560768ee51b788af10f7429/pcodedmp-1.2.6.tar.gz", hash = "sha256:025f8c809a126f45a082ffa820893e6a8d990d9d7ddb68694b5a9f0a6dbcd955", size = 35549, upload-time = "2019-07-30T18:05:42.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/72/b380fb5c89d89c3afafac8cf02a71a45f4f4a4f35531ca949a34683962d1/pcodedmp-1.2.6-py2.py3-none-any.whl", hash = "sha256:4441f7c0ab4cbda27bd4668db3b14f36261d86e5059ce06c0828602cbe1c4278", size = 30939, upload-time = "2019-07-30T18:05:40.483Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "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.6.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "cfgv", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "identify", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nodeenv", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyyaml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "virtualenv", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "annotated-types", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pydantic-core", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-inspection", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "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" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "iniconfig", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "packaging", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pluggy", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (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.15.1" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "pytest", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pytest-watch" +version = "4.2.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "colorama", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "docopt", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "watchdog", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (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 = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "six", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "filelock", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "platformdirs", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, +] + +[[package]] +name = "red-black-tree-mod" +version = "1.22" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/48/75/bfa342a2ebfc9623b701f1c6995b9906fd6dd2cedf6bce777d09e23303ac/red-black-tree-mod-1.22.tar.gz", hash = "sha256:38e3652903a2bf96379c27c2082ca0b7b905158662dd7ef0c97f4fd93a9aa908", size = 34173, upload-time = "2023-12-26T14:00:22.056Z" } + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "certifi", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "charset-normalizer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "idna", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "urllib3", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "markdown-it-py", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pygments", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rtfde" +version = "0.1.2.2" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "lark", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "oletools", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/5c/116a016b38af589e8141160bc9b034b73dde2e50c22a921751f4d982a7ca/rtfde-0.1.2.2.tar.gz", hash = "sha256:2f0cd6ecd644071e39452e6fc4f4a1435453af0ec7c90ea86fb4fc96010c7f1b", size = 33408, upload-time = "2025-12-09T17:10:31.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/24/5a653278259be44c1845ddd56dd30cfa7265281ba149b9342b79f9d4f788/rtfde-0.1.2.2-py3-none-any.whl", hash = "sha256:d43868c74f21ae9ea5acbfd4176d5de1f2cfae0ff7f267698471c606287c04ec", size = 36713, upload-time = "2025-12-09T17:10:30.893Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[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 = "six" +version = "1.17.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "splunk" +version = "3.0.0" +source = { virtual = "." } +dependencies = [ + { name = "beautifulsoup4", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "python-dateutil", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "requests", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "splunk-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "splunk-soar-sdk", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "xmltodict", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "mypy", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pre-commit", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest-mock", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pytest-watch", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "ruff", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[package.metadata] +requires-dist = [ + { name = "beautifulsoup4", specifier = ">=4.12.0" }, + { name = "python-dateutil", specifier = ">=2.9.0" }, + { name = "requests", specifier = ">=2.31.0" }, + { name = "splunk-sdk", specifier = ">=2.1.1" }, + { name = "splunk-soar-sdk", specifier = ">=3.18.1" }, + { name = "xmltodict", specifier = ">=0.13.0" }, +] + +[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 = "splunk-sdk" +version = "2.1.1" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "deprecation", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/c8/c54008fdb14c081fa4c60ed363b3dff9a2104ee77eb1717d717767486493/splunk-sdk-2.1.1.tar.gz", hash = "sha256:46300d52f09e0aed7e5962ce2ba08ef54421ffb3a538c6af6164dcbf9f075faa", size = 109168, upload-time = "2025-08-26T12:00:02.539Z" } + +[[package]] +name = "splunk-soar-sdk" +version = "3.19.2" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "authlib", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "beautifulsoup4", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "bleach", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "build", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "click", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "distro", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "extract-msg", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "hatchling", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpx", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "httpx-retries", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "humanize", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "jinja2", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "packaging", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pydantic", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pyjwt", extra = ["crypto"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "requests", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "setuptools", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "toml", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "tqdm", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/d4/907defe570d3de094fcfc1cec10f2e456656e6fb303cde3b95e54ac91279/splunk_soar_sdk-3.19.2.tar.gz", hash = "sha256:a126b5902de986de6413ca75577561ccf19d758de909348f5a4bd366ada1c5f2", size = 650629, upload-time = "2026-04-13T18:25:47.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/19/7c2ff188923a42bcfe14d9ca2b192704970d6b3d16695759e9cbf8245864/splunk_soar_sdk-3.19.2-py3-none-any.whl", hash = "sha256:bba4dfc45fada0758dbae456d1a3c6611a87a6d01dadbbe3f6e821c6aa53aed3", size = 207232, upload-time = "2026-04-13T18:25:48.308Z" }, +] + +[[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 = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "trove-classifiers" +version = "2026.1.14.14" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/43/7935f8ea93fcb6680bc10a6fdbf534075c198eeead59150dd5ed68449642/trove_classifiers-2026.1.14.14.tar.gz", hash = "sha256:00492545a1402b09d4858605ba190ea33243d361e2b01c9c296ce06b5c3325f3", size = 16997, upload-time = "2026-01-14T14:54:50.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/4a/2e5583e544bc437d5e8e54b47db87430df9031b29b48d17f26d129fa60c0/trove_classifiers-2026.1.14.14-py3-none-any.whl", hash = "sha256:1f9553927f18d0513d8e5ff80ab8980b8202ce37ecae0e3274ed2ef11880e74d", size = 14197, upload-time = "2026-01-14T14:54:49.067Z" }, +] + +[[package]] +name = "typer" +version = "0.23.1" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "annotated-doc", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "click", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "rich", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "shellingham", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.3.1" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.4" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "distlib", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "filelock", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "platformdirs", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "python-discovery", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, +] + +[[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/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/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" }, +] + +[[package]] +name = "xmltodict" +version = "1.0.4" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/70/80f3b7c10d2630aa66414bf23d210386700aa390547278c789afa994fd7e/xmltodict-1.0.4.tar.gz", hash = "sha256:6d94c9f834dd9e44514162799d344d815a3a4faec913717a9ecbfa5be1bb8e61", size = 26124, upload-time = "2026-02-22T02:21:22.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, +] diff --git a/wheels/py3/packaging-25.0-py3-none-any.whl b/wheels/py3/packaging-25.0-py3-none-any.whl deleted file mode 100644 index 1809cdbcfcf8bbf81c0985ba3e60781d5952e875..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66469 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
ZYN#Y{G54 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 diff --git a/wheels/py3/splunk_sdk-2.1.1-py3-none-any.whl b/wheels/py3/splunk_sdk-2.1.1-py3-none-any.whl deleted file mode 100644 index bc11ad17380c93a977f9c28082e5a302ed57760e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 125895 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

&uq{E05 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%0AY!EqJ2=X1O)A`z@=is!R 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 Ik9fd;0YEU`Qvd(} diff --git a/wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl b/wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl deleted file mode 100644 index 98b4c6b70e213933a5692ad689f2ea399fd02b03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11178 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 From 2c97f7a6aff42646ab6362331c187a26a3f46dba Mon Sep 17 00:00:00 2001 From: grokas Date: Fri, 24 Apr 2026 13:53:56 -0700 Subject: [PATCH 02/10] refactor: restore release notes --- release_notes/1.2.15.md | 7 +++++++ release_notes/1.2.18.md | 7 +++++++ release_notes/1.3.16.md | 6 ++++++ release_notes/1.3.19.md | 6 ++++++ release_notes/1.3.23.md | 10 ++++++++++ release_notes/1.3.41.md | 11 +++++++++++ release_notes/1.3.7.md | 8 ++++++++ release_notes/2.0.22.md | 12 ++++++++++++ release_notes/2.0.34.md | 7 +++++++ release_notes/2.1.3.md | 7 +++++++ release_notes/2.1.6.md | 6 ++++++ release_notes/2.10.0.md | 1 + release_notes/2.11.0.md | 3 +++ release_notes/2.11.1.md | 2 ++ release_notes/2.12.0.md | 2 ++ release_notes/2.13.0.md | 1 + release_notes/2.14.0.md | 1 + release_notes/2.15.0.md | 2 ++ release_notes/2.15.1.md | 1 + release_notes/2.16.0.md | 3 +++ release_notes/2.16.1.md | 1 + release_notes/2.16.2.md | 1 + release_notes/2.17.0.md | 1 + release_notes/2.18.0.md | 1 + release_notes/2.2.3.md | 10 ++++++++++ release_notes/2.20.0.md | 2 ++ release_notes/2.20.1.md | 5 +++++ release_notes/2.20.2.md | 2 ++ release_notes/2.20.3.md | 1 + release_notes/2.3.3.md | 9 +++++++++ release_notes/2.4.8.md | 6 ++++++ release_notes/2.5.3.md | 6 ++++++ release_notes/2.6.3.md | 7 +++++++ release_notes/2.6.6.md | 6 ++++++ release_notes/2.6.7.md | 1 + release_notes/2.7.0.md | 1 + release_notes/2.8.0.md | 1 + release_notes/2.9.0.md | 1 + release_notes/unreleased.md | 1 + 39 files changed, 166 insertions(+) create mode 100644 release_notes/1.2.15.md create mode 100644 release_notes/1.2.18.md create mode 100644 release_notes/1.3.16.md create mode 100644 release_notes/1.3.19.md create mode 100644 release_notes/1.3.23.md create mode 100644 release_notes/1.3.41.md create mode 100644 release_notes/1.3.7.md create mode 100644 release_notes/2.0.22.md create mode 100644 release_notes/2.0.34.md create mode 100644 release_notes/2.1.3.md create mode 100644 release_notes/2.1.6.md create mode 100644 release_notes/2.10.0.md create mode 100644 release_notes/2.11.0.md create mode 100644 release_notes/2.11.1.md create mode 100644 release_notes/2.12.0.md create mode 100644 release_notes/2.13.0.md create mode 100644 release_notes/2.14.0.md create mode 100644 release_notes/2.15.0.md create mode 100644 release_notes/2.15.1.md create mode 100644 release_notes/2.16.0.md create mode 100644 release_notes/2.16.1.md create mode 100644 release_notes/2.16.2.md create mode 100644 release_notes/2.17.0.md create mode 100644 release_notes/2.18.0.md create mode 100644 release_notes/2.2.3.md create mode 100644 release_notes/2.20.0.md create mode 100644 release_notes/2.20.1.md create mode 100644 release_notes/2.20.2.md create mode 100644 release_notes/2.20.3.md create mode 100644 release_notes/2.3.3.md create mode 100644 release_notes/2.4.8.md create mode 100644 release_notes/2.5.3.md create mode 100644 release_notes/2.6.3.md create mode 100644 release_notes/2.6.6.md create mode 100644 release_notes/2.6.7.md create mode 100644 release_notes/2.7.0.md create mode 100644 release_notes/2.8.0.md create mode 100644 release_notes/2.9.0.md create mode 100644 release_notes/unreleased.md diff --git a/release_notes/1.2.15.md b/release_notes/1.2.15.md new file mode 100644 index 0000000..e738578 --- /dev/null +++ b/release_notes/1.2.15.md @@ -0,0 +1,7 @@ +**Splunk Release Notes - Published by Splunk June 8, 2016** + + +**Version 1.2.15 - Released June 8, 2016** + +* Improved table display of "run query" action results. +* Fixed documentation typos. diff --git a/release_notes/1.2.18.md b/release_notes/1.2.18.md new file mode 100644 index 0000000..76a9ddd --- /dev/null +++ b/release_notes/1.2.18.md @@ -0,0 +1,7 @@ +**Splunk Release Notes - Published by Splunk October 20, 2016** + + +**Version 1.2.18 - Released October 20, 2016** + +* Significant improvements to the datapath settings +* Minor app documentation corrections diff --git a/release_notes/1.3.16.md b/release_notes/1.3.16.md new file mode 100644 index 0000000..8a81764 --- /dev/null +++ b/release_notes/1.3.16.md @@ -0,0 +1,6 @@ +**Splunk Release Notes - Published by Splunk December 05, 2017** + + +**Version 1.3.16 - Released December 05, 2017** + +* Added support for on-poll ingestion (Beta Release only) diff --git a/release_notes/1.3.19.md b/release_notes/1.3.19.md new file mode 100644 index 0000000..8873751 --- /dev/null +++ b/release_notes/1.3.19.md @@ -0,0 +1,6 @@ +**Splunk Release Notes - Published by Splunk February 07, 2018** + + +**Version 1.3.19 - Released February 07, 2018** + +* App action views and Logo updates diff --git a/release_notes/1.3.23.md b/release_notes/1.3.23.md new file mode 100644 index 0000000..c275fe1 --- /dev/null +++ b/release_notes/1.3.23.md @@ -0,0 +1,10 @@ +**Splunk Release Notes - Published by Splunk July 19, 2018** + + +**Version 1.3.23 - Released July 19, 2018** + +* Added support for user configurable retries +* Display parameters are now ordered as specified in the input on 'run query' action +* Bug fix on 'on poll' ingestion when the license is expired +* Bug fix on missing data when the fields are case sensitive +* Moved Splunk SDK wheel into the app for easier app customization diff --git a/release_notes/1.3.41.md b/release_notes/1.3.41.md new file mode 100644 index 0000000..f060b90 --- /dev/null +++ b/release_notes/1.3.41.md @@ -0,0 +1,11 @@ +**Splunk Release Notes - Published by Splunk January 22, 2020** + + +**Version 1.3.41 - Released January 22, 2020** + +* Added functionality to run on poll action with only 'query' parameter and empty 'command' parameter +* Added support for new commands like table, stats, eval +* Handled exception for Unicode characters issues +* Bug fixed in 'run query', 'post data', and 'update event' actions +* Fixed issues in the output views +* Improved the documentation of the app diff --git a/release_notes/1.3.7.md b/release_notes/1.3.7.md new file mode 100644 index 0000000..84cd757 --- /dev/null +++ b/release_notes/1.3.7.md @@ -0,0 +1,8 @@ +**Splunk Release Notes - Published by Splunk July 6, 2017** + + +**Version 1.3.7 - Released July 6, 2017** + +* Added "post data" action +* Added "update event" action +* Fixed an issue that caused "get host events" to return no data diff --git a/release_notes/2.0.22.md b/release_notes/2.0.22.md new file mode 100644 index 0000000..8266689 --- /dev/null +++ b/release_notes/2.0.22.md @@ -0,0 +1,12 @@ +**Splunk Release Notes - Published by Splunk September 18, 2020** + + +**Version 2.0.22 - Released September 18, 2020** + +* Compatibility changes for Python 3 support +* Added the "wait\_for\_confirmation" action parameter in the "update event" action +* Changed Source Data Identifier of container/artifact to a hash of the combination of "\_raw", "source", "sourcetype", and "index" +* Made the custom view compatible with the Phantom V4.9 +* Added validations on action input parameters +* Handled the unicode character exceptions +* Updated the app documentation diff --git a/release_notes/2.0.34.md b/release_notes/2.0.34.md new file mode 100644 index 0000000..0ccbdf2 --- /dev/null +++ b/release_notes/2.0.34.md @@ -0,0 +1,7 @@ +**Splunk Release Notes - Published by Splunk February 08, 2021** + + +**Version 2.0.34 - Released February 08, 2021** + +* Updated the app documentation +* Added the "sid" key in summary of 'update\_event', 'run\_query', and 'get\_host\_events' actions diff --git a/release_notes/2.1.3.md b/release_notes/2.1.3.md new file mode 100644 index 0000000..053068c --- /dev/null +++ b/release_notes/2.1.3.md @@ -0,0 +1,7 @@ +**Splunk Release Notes - Published by Splunk April 14, 2021** + + +**Version 2.1.3 - Released April 14, 2021** + +* Fixed a bug which caused the app to ignore the Global Proxy Settings [PAPP-11360] +* Fixed a bug during ingestion if an event had multiple associated severities [PAPP-12153] diff --git a/release_notes/2.1.6.md b/release_notes/2.1.6.md new file mode 100644 index 0000000..4729bea --- /dev/null +++ b/release_notes/2.1.6.md @@ -0,0 +1,6 @@ +**Splunk Release Notes - Published by Splunk June 24, 2021** + + +**Version 2.1.6 - Released June 24, 2021** + +* Fixed the start\_time field in the artifact [PAPP-17613] diff --git a/release_notes/2.10.0.md b/release_notes/2.10.0.md new file mode 100644 index 0000000..dcb85f1 --- /dev/null +++ b/release_notes/2.10.0.md @@ -0,0 +1 @@ +* Fixed an issue in On Poll action where the index time was not honored during scheduled ingestion [PAPP-25411] \ No newline at end of file diff --git a/release_notes/2.11.0.md b/release_notes/2.11.0.md new file mode 100644 index 0000000..c6c0185 --- /dev/null +++ b/release_notes/2.11.0.md @@ -0,0 +1,3 @@ +* Added token-based authentication workflow +* Replaced an endpoint for test connectivity action +* Fixed miscellaneous proxy-related issues \ No newline at end of file diff --git a/release_notes/2.11.1.md b/release_notes/2.11.1.md new file mode 100644 index 0000000..5b03297 --- /dev/null +++ b/release_notes/2.11.1.md @@ -0,0 +1,2 @@ +* Improved error logging +* Removed python 2 related code \ No newline at end of file diff --git a/release_notes/2.12.0.md b/release_notes/2.12.0.md new file mode 100644 index 0000000..65e5280 --- /dev/null +++ b/release_notes/2.12.0.md @@ -0,0 +1,2 @@ +* Added search_mode parameter to "run query" action with fast, verbose, and smart as possible values [PAPP-10085] +* Update Splunk SDK to 1.7.2 and changed ResultsReader to JSONResultsReader [PAPP-27658] \ No newline at end of file diff --git a/release_notes/2.13.0.md b/release_notes/2.13.0.md new file mode 100644 index 0000000..7e7b49f --- /dev/null +++ b/release_notes/2.13.0.md @@ -0,0 +1 @@ +* Fixed load balancer sticky sessions related cookie persistence bug [PAPP-27448, PAPP-26097] \ No newline at end of file diff --git a/release_notes/2.14.0.md b/release_notes/2.14.0.md new file mode 100644 index 0000000..41c5631 --- /dev/null +++ b/release_notes/2.14.0.md @@ -0,0 +1 @@ +* Fixed the issue related to source_data_identifier [PAPP-29653] \ No newline at end of file diff --git a/release_notes/2.15.0.md b/release_notes/2.15.0.md new file mode 100644 index 0000000..d45f138 --- /dev/null +++ b/release_notes/2.15.0.md @@ -0,0 +1,2 @@ +* Bug fix for removing temp files [PAPP-30430] +* Added a new feature to include both CEF and original CIM field [PAPP-30037] \ No newline at end of file diff --git a/release_notes/2.15.1.md b/release_notes/2.15.1.md new file mode 100644 index 0000000..2c23017 --- /dev/null +++ b/release_notes/2.15.1.md @@ -0,0 +1 @@ +* Updated dependency packages, removed future [PAPP-31089] \ No newline at end of file diff --git a/release_notes/2.16.0.md b/release_notes/2.16.0.md new file mode 100644 index 0000000..5da072a --- /dev/null +++ b/release_notes/2.16.0.md @@ -0,0 +1,3 @@ +* Documentation update for steps to allow edit_tcp capability for a user [PAPP-31540] +* Bug fix for 'on poll' cef field names [PAPP-30430] +* Bug fix for accessing vault temp directory path [PAPP-32416] \ No newline at end of file diff --git a/release_notes/2.16.1.md b/release_notes/2.16.1.md new file mode 100644 index 0000000..8847d1f --- /dev/null +++ b/release_notes/2.16.1.md @@ -0,0 +1 @@ +* Changed logic in 'run query' action in order to decrease memory usage [PAPP-32609] \ No newline at end of file diff --git a/release_notes/2.16.2.md b/release_notes/2.16.2.md new file mode 100644 index 0000000..7e02622 --- /dev/null +++ b/release_notes/2.16.2.md @@ -0,0 +1 @@ +* Fixed Django template, `ifnotequal` tag was deprecated. \ No newline at end of file diff --git a/release_notes/2.17.0.md b/release_notes/2.17.0.md new file mode 100644 index 0000000..f0dffbb --- /dev/null +++ b/release_notes/2.17.0.md @@ -0,0 +1 @@ +* Added 'splunk_job_timeout' parameter to asset config [PAPP-34684] \ No newline at end of file diff --git a/release_notes/2.18.0.md b/release_notes/2.18.0.md new file mode 100644 index 0000000..adac350 --- /dev/null +++ b/release_notes/2.18.0.md @@ -0,0 +1 @@ +* Added 2 new fields ("disposition" and "integer_disposition") to "update event" action \ No newline at end of file diff --git a/release_notes/2.2.3.md b/release_notes/2.2.3.md new file mode 100644 index 0000000..74a7496 --- /dev/null +++ b/release_notes/2.2.3.md @@ -0,0 +1,10 @@ +**Splunk Release Notes - Published by Splunk July 13, 2021** + + +**Version 2.2.3 - Released July 13, 2021** + +* Added support for custom status ID in the integer status parameter of the 'update event' action [PAPP-9598] +* Bug fix in the 'run query' action [PAPP-13769] +* Allow 0 for the 'Max events to ingest for Scheduled Polling' configuration parameter [PAPP-11483] +* Fix for the 'Values to append to the container name' configuration parameter [PAPP-11072] [PAPP-17977] +* Handled extra commas in the display parameter of the 'run query' action [PAPP-17228] diff --git a/release_notes/2.20.0.md b/release_notes/2.20.0.md new file mode 100644 index 0000000..43be1a2 --- /dev/null +++ b/release_notes/2.20.0.md @@ -0,0 +1,2 @@ +* Added 'use_event_id_sdi' parameter to asset config to allow updated event ingestion into the original container +* Added parameter in run query action to optionally remove the "_raw" field [PAPP-26864] \ No newline at end of file diff --git a/release_notes/2.20.1.md b/release_notes/2.20.1.md new file mode 100644 index 0000000..46df760 --- /dev/null +++ b/release_notes/2.20.1.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/2.20.2.md b/release_notes/2.20.2.md new file mode 100644 index 0000000..0aa400b --- /dev/null +++ b/release_notes/2.20.2.md @@ -0,0 +1,2 @@ +* Resolved app issues related to Python 3.13 upgrade +* updated dependencies for python 3.13 \ No newline at end of file diff --git a/release_notes/2.20.3.md b/release_notes/2.20.3.md new file mode 100644 index 0000000..52795ae --- /dev/null +++ b/release_notes/2.20.3.md @@ -0,0 +1 @@ +* Support specifying custom timestamp format for the 'run query' action. \ No newline at end of file diff --git a/release_notes/2.3.3.md b/release_notes/2.3.3.md new file mode 100644 index 0000000..b148d03 --- /dev/null +++ b/release_notes/2.3.3.md @@ -0,0 +1,9 @@ +**Splunk Release Notes - Published by Splunk August 06, 2021** + + +**Version 2.3.3 - Released August 06, 2021** + +* Updated the 'update event' action's status based on the "success" key in response [PAPP-9587] +* Modified the code to re-connect based on retry limit in case of "Session not logged in" issue [PAPP-17690] +* Modified the on-poll action to ingest updated/deleted artifacts in the existing container [PAPP-18788] +* Updated the document for Update event action with the required role and permission diff --git a/release_notes/2.4.8.md b/release_notes/2.4.8.md new file mode 100644 index 0000000..d22074b --- /dev/null +++ b/release_notes/2.4.8.md @@ -0,0 +1,6 @@ +**Splunk Release Notes - Published by Splunk October 19, 2021** + + +**Version 2.4.8 - Released October 19, 2021** + +* Added a new 'Remove CEF fields having empty values from the artifact' configuration parameter [PAPP-9257] diff --git a/release_notes/2.5.3.md b/release_notes/2.5.3.md new file mode 100644 index 0000000..a904e1c --- /dev/null +++ b/release_notes/2.5.3.md @@ -0,0 +1,6 @@ +**Splunk Release Notes - Published by Splunk November 16, 2021** + + +**Version 2.5.3 - Released November 16, 2021** + +* Changed the hashing algorithm from md5 to sha-256 [PAPP-19934] diff --git a/release_notes/2.6.3.md b/release_notes/2.6.3.md new file mode 100644 index 0000000..74f955b --- /dev/null +++ b/release_notes/2.6.3.md @@ -0,0 +1,7 @@ +**Splunk Release Notes - Published by Splunk December 01, 2021** + + +**Version 2.6.3 - Released December 01, 2021** + +* Fixed a bug in the 'on poll' action [PAPP-20789] +* Updated the app documentation diff --git a/release_notes/2.6.6.md b/release_notes/2.6.6.md new file mode 100644 index 0000000..e623787 --- /dev/null +++ b/release_notes/2.6.6.md @@ -0,0 +1,6 @@ +**Splunk Release Notes - Published by Splunk January 20, 2022** + + +**Version 2.6.6 - Released January 20, 2022** + +* Changed the hashing algorithm to SHA256 when running in FIPS mode [PAPP-21816] \ No newline at end of file diff --git a/release_notes/2.6.7.md b/release_notes/2.6.7.md new file mode 100644 index 0000000..ee41f86 --- /dev/null +++ b/release_notes/2.6.7.md @@ -0,0 +1 @@ +* Added support for Python 3.9 \ No newline at end of file diff --git a/release_notes/2.7.0.md b/release_notes/2.7.0.md new file mode 100644 index 0000000..8f73a32 --- /dev/null +++ b/release_notes/2.7.0.md @@ -0,0 +1 @@ +* Added a new 'attach_result' parameter in 'run query' action [PAPP-8315] \ No newline at end of file diff --git a/release_notes/2.8.0.md b/release_notes/2.8.0.md new file mode 100644 index 0000000..33461da --- /dev/null +++ b/release_notes/2.8.0.md @@ -0,0 +1 @@ +* Added a sleep time between REST calls to improve the performance [PAPP-23575] \ No newline at end of file diff --git a/release_notes/2.9.0.md b/release_notes/2.9.0.md new file mode 100644 index 0000000..1c8186f --- /dev/null +++ b/release_notes/2.9.0.md @@ -0,0 +1 @@ +* Added 2 new fields ("start_time" and "end_time") to "run query" action [PAPP-24566] \ 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 c37714fe461876630fd588a1096a409c5b03eee9 Mon Sep 17 00:00:00 2001 From: grokas Date: Fri, 24 Apr 2026 13:54:03 -0700 Subject: [PATCH 03/10] refactor: split actions into separate modules --- src/__init__.py | 4 +- src/actions/__init__.py | 14 + src/actions/get_host_events.py | 55 +++ src/actions/on_poll.py | 209 +++++++++++ src/actions/post_data.py | 66 ++++ src/actions/run_query.py | 155 ++++++++ src/actions/update_event.py | 194 ++++++++++ src/app.py | 656 +-------------------------------- 8 files changed, 697 insertions(+), 656 deletions(-) create mode 100644 src/actions/__init__.py create mode 100644 src/actions/get_host_events.py create mode 100644 src/actions/on_poll.py create mode 100644 src/actions/post_data.py create mode 100644 src/actions/run_query.py create mode 100644 src/actions/update_event.py diff --git a/src/__init__.py b/src/__init__.py index 897c86c..7fda0d7 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -11,6 +11,6 @@ # 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 +from . import actions, app -__ALL__ = [app] +__ALL__ = [app, actions] diff --git a/src/actions/__init__.py b/src/actions/__init__.py new file mode 100644 index 0000000..49bb430 --- /dev/null +++ b/src/actions/__init__.py @@ -0,0 +1,14 @@ +# Copyright (c) 2016-2026 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 get_host_events, on_poll, post_data, run_query, update_event # noqa: F401 diff --git a/src/actions/get_host_events.py b/src/actions/get_host_events.py new file mode 100644 index 0000000..d747a3a --- /dev/null +++ b/src/actions/get_host_events.py @@ -0,0 +1,55 @@ +# Copyright (c) 2016-2026 Splunk Inc. + +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, OutputField, PermissiveActionOutput +from soar_sdk.params import Param, Params + +from ..app import Asset, SplunkHelper, app + + +class GetHostEventsParams(Params): + ip_hostname: str = Param( + description="Hostname/IP to search the events of", + required=True, + primary=True, + cef_types=["ip", "host name"], + ) + last_n_days: str = Param(description="Number of days ago", required=False, default="") + + +class GetHostEventsOutput(PermissiveActionOutput): + host: str | None = OutputField(column_name="Host") + time: str | None = OutputField(column_name="Time", alias="_time") + raw: str | None = OutputField(column_name="Raw", alias="_raw") + + +class GetHostEventsSummary(ActionOutput): + sid: str | None = None + total_events: int | None = None + + +@app.action( + description="Get events pertaining to a host that have occurred in the last 'N' days", + action_type="investigate", + read_only=True, + render_as="table", + summary_type=GetHostEventsSummary, +) +def get_host_events(params: GetHostEventsParams, soar: SOARClient, asset: Asset) -> list[GetHostEventsOutput]: + helper = SplunkHelper(asset) + helper.validate_asset() + helper.connect() + + ip_hostname = params.ip_hostname + last_n_days = SplunkHelper.validate_integer(params.last_n_days, "'last_n_days' action") + + search_query = f'search host="{ip_hostname}"' + if last_n_days: + search_query += f" earliest=-{last_n_days}d" + + sid, results_list = helper.run_query(search_query) + + soar.set_summary(GetHostEventsSummary(sid=sid, total_events=len(results_list))) + soar.set_message(f"Sid: {sid}, Total events: {len(results_list)}") + + return [GetHostEventsOutput(**r) for r in results_list] diff --git a/src/actions/on_poll.py b/src/actions/on_poll.py new file mode 100644 index 0000000..cca6294 --- /dev/null +++ b/src/actions/on_poll.py @@ -0,0 +1,209 @@ +# Copyright (c) 2016-2026 Splunk Inc. + +import hashlib +import json +from collections.abc import Iterator +from datetime import UTC, datetime + +from bs4.dammit import UnicodeDammit +from soar_sdk.abstract import SOARClient +from soar_sdk.logging import getLogger +from soar_sdk.models.artifact import Artifact +from soar_sdk.models.container import Container +from soar_sdk.params import OnPollParams + +from ..app import Asset, SplunkHelper, app +from ..splunk_consts import CIM_CEF_MAP, SPLUNK_SEVERITY_MAP + +logger = getLogger() + + +def _get_event_start(start_time: str | None) -> str | None: + if not start_time: + return None + try: + from dateutil.parser import ParserError, parse as dateutil_parse + datetime_obj = dateutil_parse(start_time) + return datetime_obj.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ") + except ParserError as e: + logger.error("ParserError while parsing _time: %s", e) + return None + except Exception as e: + logger.error("Exception while parsing _time: %s", e) + return None + + +def _get_fips_enabled() -> bool: + try: + from phantom_common.install_info import is_fips_enabled # noqa: PLC0415 + return is_fips_enabled() + except ImportError: + return False + + +def _get_splunk_severity(item: dict) -> str: + severity = item.get("severity") + if isinstance(severity, list): + for key in ["critical", "high", "medium", "low", "informational"]: + if key in severity: + return SPLUNK_SEVERITY_MAP[key] + return "" + severity = SPLUNK_SEVERITY_MAP.get(severity) if severity else None + if not severity: + urgency = item.get("urgency") + severity = SPLUNK_SEVERITY_MAP.get(urgency, "medium") + return severity + + +def _get_splunk_title(item: dict, prefix: str, name_values: list[str]) -> str: + title = prefix + values_list = list(name_values) + if not title and not values_list: + values_list.append("source") + + values = "" + for i, nv in enumerate(values_list): + if CIM_CEF_MAP.get(nv) and item.get(CIM_CEF_MAP.get(nv)): + value = item.get(CIM_CEF_MAP.get(nv)) + elif item.get(nv): + value = item.get(nv) + else: + value = CIM_CEF_MAP.get(nv, nv) + values += f"{value}" + ("" if i == len(values_list) - 1 else ", ") + + if not title: + t = item.get("_time") + title = f"Splunk Log Entry on {t}" if t else "Splunk Log Entry" + else: + title = item.get(title, title) + + return f"{title}: {values}" + + +@app.on_poll() +def on_poll(params: OnPollParams, soar: SOARClient, asset: Asset) -> Iterator[Container | Artifact]: + helper = SplunkHelper(asset) + helper.validate_asset() + helper.connect() + + search_command = asset.on_poll_command + search_string = asset.on_poll_query + po = asset.on_poll_parse_only + include_cim_fields = asset.include_cim_fields + use_event_id_sdi = asset.use_event_id_sdi + + if not search_string: + raise ValueError("Need to specify Query String to use polling") + + try: + if not search_command: + if search_string[0] != "|" and not search_string.startswith("search"): + search_string = f"search {search_string.strip()}" + search_query = search_string + else: + search_query = f"{search_command.strip()} {search_string.strip()}" + except Exception: + raise ValueError("Error occurred while parsing the search query") from None + + search_params: dict = {} + state = asset.ingest_state + is_poll_now = params.is_manual_poll() + + if is_poll_now: + search_params["max_count"] = params.container_count or 100 + else: + search_params["max_count"] = asset.max_container + start_time = state.get("start_time") + if start_time: + search_params["index_earliest"] = start_time + + if int(search_params["max_count"]) <= 0: + logger.debug("container_count <= 0, ignoring max_count") + search_params.pop("max_count") + + try: + _sid, results_list = helper.run_query(search_query, kwargs_create=search_params, parse_only=po) + except Exception as e: + msg = str(e) + if "Invalid index_earliest" in msg: + logger.debug("Invalid start_time %s, retrying without it", search_params.get("index_earliest")) + state.pop("start_time", None) + raise + + display = asset.on_poll_display + header_set = None + if display: + header_set = [x.strip().lower() for x in display.split(",")] + + data = list(reversed(results_list)) + logger.info("Total %d event(s) fetched", len(data)) + + container_name_prefix = asset.container_name_prefix or "" + raw_values = asset.container_name_values + container_name_values = [x.strip() for x in raw_values.split(",")] if raw_values else [] + + count = 1 + for item in data: + try: + cef: dict = {} + if "_serial" in item: + item.pop("_serial") + + if header_set: + name_mappings = {k.lower(): k for k in item if k.lower() in header_set} + for h in header_set: + cef_name = CIM_CEF_MAP.get(h, h) + cef_name = name_mappings.get(cef_name, cef_name) + cef_key_value = name_mappings.get(h, h) + cef[cef_name] = item.get(cef_key_value) + if include_cim_fields: + cef[cef_key_value] = item.get(cef_key_value) + else: + for k, v in item.items(): + cef[CIM_CEF_MAP.get(k, k)] = v + if include_cim_fields: + cef[k] = v + + if use_event_id_sdi and "event_id" in item: + sdi = item["event_id"] + else: + if use_event_id_sdi and "event_id" not in item: + logger.warning("use_event_id_sdi enabled but event_id missing, using hash") + input_str = UnicodeDammit(json.dumps(item)).unicode_markup.encode("utf-8") + if _get_fips_enabled(): + sdi = hashlib.sha256(input_str).hexdigest() + else: + sdi = hashlib.md5(input_str).hexdigest() # noqa: S324 + + severity = _get_splunk_severity(item) + spl_event_start = _get_event_start(item.get("_time")) + container_name = _get_splunk_title(item, container_name_prefix, container_name_values) + + yield Container( + name=container_name, + severity=severity, + source_data_identifier=sdi, + ) + + if asset.remove_empty_cef: + cef = {k: v for k, v in cef.items() if v is not None} + + yield Artifact( + cef=cef, + name="Field Values", + source_data_identifier=sdi, + severity=severity, + start_time=spl_event_start, + ) + + if count == asset.container_update_state and not is_poll_now: + state["start_time"] = item.get("_indextime") + count = 0 + count += 1 + + except Exception as e: + logger.error("Error processing event: %s", e) + continue + + if data and not is_poll_now: + state["start_time"] = data[-1].get("_indextime") diff --git a/src/actions/post_data.py b/src/actions/post_data.py new file mode 100644 index 0000000..3a93217 --- /dev/null +++ b/src/actions/post_data.py @@ -0,0 +1,66 @@ +# Copyright (c) 2016-2026 Splunk Inc. + +from bs4.dammit import UnicodeDammit +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, OutputField +from soar_sdk.logging import getLogger +from soar_sdk.params import Param, Params + +from ..app import Asset, SplunkHelper, app +from ..splunk_consts import SPLUNK_DEFAULT_SOURCE, SPLUNK_DEFAULT_SOURCE_TYPE + +logger = getLogger() + + +class PostDataParams(Params): + data: str = Param(description="Data to post", required=True) + host: str = Param( + description="Host for event", + required=False, + default="", + primary=True, + cef_types=["ip", "host name"], + ) + index: str = Param(description="Index to send event to", required=False, default="") + source: str = Param(description="Source for event", required=False, default="Phantom") + source_type: str = Param( + description="Type of source for event", + required=False, + default="Automation/Orchestration Platform", + ) + + +class PostDataOutput(ActionOutput): + status: str | None = OutputField(column_name="Status") + message: str | None = OutputField(column_name="Message") + + +@app.action( + description="Post data to Splunk", + action_type="generic", + read_only=False, + render_as="table", +) +def post_data(params: PostDataParams, soar: SOARClient, asset: Asset) -> list[PostDataOutput]: + helper = SplunkHelper(asset) + helper.validate_asset() + + try: + post_bytes = UnicodeDammit(params.data).unicode_markup.encode("utf-8") + except Exception as e: + logger.error("Error while encoding data: %s", e) + post_bytes = params.data.encode("utf-8") + + get_params: dict[str, str] = { + "source": params.source or SPLUNK_DEFAULT_SOURCE, + "sourcetype": params.source_type or SPLUNK_DEFAULT_SOURCE_TYPE, + } + if params.host: + get_params["host"] = params.host + if params.index: + get_params["index"] = params.index + + helper.make_rest_call_retry("receivers/simple", post_bytes, params=get_params) + + soar.set_message("Successfully posted the data") + return [PostDataOutput(status="success", message="Successfully posted the data")] diff --git a/src/actions/run_query.py b/src/actions/run_query.py new file mode 100644 index 0000000..a3067c4 --- /dev/null +++ b/src/actions/run_query.py @@ -0,0 +1,155 @@ +# Copyright (c) 2016-2026 Splunk Inc. + +import json + +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, PermissiveActionOutput +from soar_sdk.logging import getLogger +from soar_sdk.params import Param, Params + +from ..app import Asset, SplunkHelper, app +from ..splunk_consts import SPLUNK_SEARCH_MODE_SMART + +logger = getLogger() + + +class RunQueryParams(Params): + command: str = Param( + description="Beginning command (in Splunk Processing Language)", + required=False, + value_list=["search", "eval", "savedsearch", "stats", "table", "tstats"], + default="search", + ) + query: str = Param( + description="Query to run (in Splunk Processing Language)", + required=True, + primary=True, + cef_types=["splunk query"], + ) + display: str = Param(description="Display fields (comma-separated)", required=False, default="") + parse_only: bool = Param(description="Parse only", required=False, default=False) + add_raw_field: bool = Param(description="Ingest _raw field data", required=False, default=True) + attach_result: bool = Param(description="Attach result to the vault", required=False, default=False) + start_time: str = Param(description="Earliest time modifier", required=False, default="") + end_time: str = Param(description="Latest time modifier", required=False, default="") + search_mode: str = Param( + description="Search mode", + required=False, + value_list=["fast", "verbose", "smart"], + default="smart", + ) + time_format: str = Param(description="Custom timestamp format", required=False, default="") + + +class RunQueryOutput(PermissiveActionOutput): + pass + + +class RunQuerySummary(ActionOutput): + sid: str | None = None + total_events: int | None = None + + +@app.view_handler(template="splunk_run_query.html") +def display_view(outputs: list[RunQueryOutput]) -> dict: + if not outputs: + return {"results": [{"data": {}, "param": {}}]} + + first = outputs[0].model_dump(exclude_none=True) + param = { + "query": first.get("_param_query", ""), + "command": first.get("_param_command", ""), + "display": first.get("_param_display", ""), + "parse_only": first.get("_param_parse_only", False), + "search_mode": first.get("_param_search_mode", "smart"), + } + display_fields = param.get("display", "") + + all_data = [] + for output in outputs: + data = {k: v for k, v in output.model_dump(exclude_none=True).items() if not k.startswith("_")} + all_data.append(data) + + if display_fields: + headers = [x.strip() for x in display_fields.split(",") if x.strip()] + elif all_data: + headers = [k for k in all_data[0] if not k.startswith("_")] + else: + headers = [] + + processed_data = [{h: item.get(h) for h in headers} for item in all_data] + + return { + "results": [{ + "param": param, + "data": all_data or {}, + "processed_data": processed_data, + "headers": headers, + }], + } + + +@app.action( + description="Run a search query on the Splunk device. Please escape any quotes that are part of the query string", + action_type="investigate", + read_only=True, + view_handler=display_view, + summary_type=RunQuerySummary, +) +def run_query(params: RunQueryParams, soar: SOARClient, asset: Asset) -> list[RunQueryOutput]: + helper = SplunkHelper(asset) + helper.validate_asset() + helper.connect() + + search_mode = params.search_mode or SPLUNK_SEARCH_MODE_SMART + kwargs: dict = {"adhoc_search_level": search_mode} + if params.start_time: + kwargs["earliest_time"] = params.start_time + if params.end_time: + kwargs["latest_time"] = params.end_time + if params.time_format: + kwargs["time_format"] = params.time_format + + search_command = params.command + search_string = params.query + + if not search_command: + if search_string[0] != "|" and not search_string.startswith("search"): + search_string = f"search {search_string.strip()}" + search_query = search_string + else: + search_query = f"{search_command.strip()} {search_string.strip()}" + + sid, results_list = helper.run_query( + search_query, + kwargs_create=kwargs, + parse_only=params.parse_only, + add_raw_field=params.add_raw_field, + ) + + if params.attach_result: + _attach_json_result(soar, results_list) + + soar.set_summary(RunQuerySummary(sid=sid, total_events=len(results_list))) + soar.set_message(f"Sid: {sid}, Total events: {len(results_list)}") + + param_info = { + "_param_query": params.query, + "_param_command": params.command, + "_param_display": params.display, + "_param_parse_only": params.parse_only, + "_param_search_mode": search_mode, + } + return [RunQueryOutput(**{**r, **param_info}) for r in results_list] + + +def _attach_json_result(soar: SOARClient, data: list[dict]): + try: + container_id = soar.get_executing_container_id() + soar.vault.create_attachment( + container_id=container_id, + file_content=json.dumps(data), + file_name="splunk_run_query_result.json", + ) + except Exception as e: + logger.error("Error attaching results to vault: %s", e) diff --git a/src/actions/update_event.py b/src/actions/update_event.py new file mode 100644 index 0000000..97ad4cb --- /dev/null +++ b/src/actions/update_event.py @@ -0,0 +1,194 @@ +# Copyright (c) 2016-2026 Splunk Inc. + +import json +import re + +from soar_sdk.abstract import SOARClient +from soar_sdk.action_results import ActionOutput, OutputField +from soar_sdk.logging import getLogger +from soar_sdk.params import Param, Params + +from ..app import Asset, SplunkHelper, app +from ..splunk_consts import ( + SPLUNK_DISPOSITION_QUERY_FORMAT, + SPLUNK_ERR_BAD_DISPOSITION, + SPLUNK_ERR_BAD_STATUS, + SPLUNK_ERR_NEED_PARAM, + SPLUNK_ERR_NOT_ES, +) + +logger = getLogger() + + +class UpdateEventParams(Params): + event_ids: str = Param( + description="Event ID to update", + required=True, + primary=True, + cef_types=["splunk notable event id"], + ) + owner: str = Param(description="New owner for the event", required=False, default="") + status: str = Param( + description="New status for the event", + required=False, + default="", + value_list=["", "unassigned", "new", "in progress", "pending", "resolved", "closed"], + ) + integer_status: str = Param(description="Integer representing custom status value", required=False, default="") + urgency: str = Param( + description="New urgency for the event", + required=False, + default="", + value_list=["", "informational", "low", "medium", "high", "critical"], + ) + comment: str = Param(description="New comment for the event", required=False, default="") + disposition: str = Param( + description="New disposition field", + required=False, + default="", + value_list=[ + "", "Unassigned", "True Positive - Suspicious Activity", + "Benign Positive - Suspicious But Expected", + "False Positive - Incorrect Analytic Logic", + "False Positive - Inaccurate Data", "Undetermined", "Other", + ], + ) + integer_disposition: str = Param(description="Integer representing custom disposition value", required=False, default="") + wait_for_confirmation: bool = Param(description="Validate event_ids", required=False, default=False) + + +class UpdateEventOutput(ActionOutput): + status: str | None = OutputField(column_name="Status") + failure_count: int | None = None + message: str | None = OutputField(column_name="Message") + success: bool | None = None + success_count: int | None = None + + +class UpdateEventSummary(ActionOutput): + sid: str | None = None + updated_event_id: str | None = None + + +@app.action( + description="Update a notable event", + action_type="generic", + read_only=False, + render_as="table", + summary_type=UpdateEventSummary, +) +def update_event(params: UpdateEventParams, soar: SOARClient, asset: Asset) -> list[UpdateEventOutput]: + helper = SplunkHelper(asset) + helper.validate_asset() + + if not helper.check_for_es(): + raise RuntimeError(SPLUNK_ERR_NOT_ES) + + ids = params.event_ids + owner = params.owner + status = params.status + comment = params.comment + urgency = params.urgency + disposition = params.disposition or "" + wait_for_confirmation = params.wait_for_confirmation + + integer_status = SplunkHelper.validate_integer( + params.integer_status, "'integer_status' action", allow_zero=True + ) + integer_disposition = SplunkHelper.validate_integer( + params.integer_disposition, "'integer_disposition' action", allow_zero=True + ) + + if not any([comment, status, urgency, owner, disposition]) and integer_status is None and integer_disposition is None: + raise ValueError(SPLUNK_ERR_NEED_PARAM) + + splunk_status_dict: dict[str, int] = {} + splunk_disposition_dict: dict[str, int] = {} + + if status or integer_status is not None: + splunk_status_dict = helper.get_status_dict("notable") + if not splunk_status_dict: + raise RuntimeError("Error occurred while fetching Splunk event status") + + if disposition or integer_disposition is not None: + splunk_disposition_dict = helper.get_status_dict("disposition") + if not splunk_disposition_dict: + raise RuntimeError("Error occurred while fetching Splunk event disposition") + + helper.connect() + + # Resolve SID+RID combo to event_id + regexp = re.compile(r"\+\d*(\.\d+)?[\"$]") + if regexp.search(json.dumps(ids)): + logger.progress("Interpreting the event ID as an SID + RID combo") + try: + ids = helper.resolve_event_id(ids) + except Exception: + raise RuntimeError("Unable to find underlying event_id from SID + RID combo") from None + + if wait_for_confirmation: + search_query = f"search `notable_by_id({ids})`" + _sid, validate_results = helper.run_query(search_query) + if not validate_results: + raise ValueError("Please provide a valid event ID") + + request_body: dict = {"ruleUIDs": ids} + + # Status + if integer_status is not None: + if int(integer_status) not in list(splunk_status_dict.values()): + raise ValueError( + "Please provide a valid value in 'integer_status' action parameter. " + f"Valid values: {', '.join(map(str, splunk_status_dict.values()))}" + ) + request_body["status"] = str(integer_status) + elif status: + if status not in splunk_status_dict: + if not status.isdigit(): + raise ValueError(SPLUNK_ERR_BAD_STATUS) + request_body["status"] = status + else: + request_body["status"] = splunk_status_dict[status] + + # Disposition + if integer_disposition is not None: + if int(integer_disposition) not in splunk_disposition_dict.values(): + raise ValueError( + "Please provide a valid value in 'integer_disposition' action parameter. " + f"Valid values: {', '.join(map(str, splunk_disposition_dict.values()))}" + ) + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(integer_disposition) + elif disposition: + if disposition not in splunk_disposition_dict: + if not disposition.isdigit(): + raise ValueError(SPLUNK_ERR_BAD_DISPOSITION) + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(disposition) + else: + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(splunk_disposition_dict[disposition]) + + param_mapping = {"urgency": urgency, "comment": comment, "newOwner": owner} + request_body.update({k: v for k, v in param_mapping.items() if v}) + + resp_data = helper.make_rest_call_retry("notable_update", request_body) + + if resp_data and "success" in resp_data and not resp_data.get("success"): + msg = resp_data.get("message") + raise RuntimeError(msg if msg else "Unable to update the notable event") + + soar.set_summary(UpdateEventSummary(updated_event_id=ids)) + + if wait_for_confirmation: + msg = f"Updated Event ID: {ids}" + else: + msg = ( + f"Updated Event ID: {ids}. The event_id has not been verified. " + "Please confirm that the provided event_id corresponds to an actual notable event" + ) + + soar.set_message(msg) + + if resp_data: + resp_data["status"] = "success" + resp_data["message"] = msg + return [UpdateEventOutput(**resp_data)] + return [UpdateEventOutput(status="success", message=msg)] diff --git a/src/app.py b/src/app.py index 5edd7e4..fba68fa 100644 --- a/src/app.py +++ b/src/app.py @@ -11,16 +11,11 @@ # either express or implied. See the License for the specific language governing permissions # and limitations under the License. -import hashlib -import json import os -import re import ssl import sys import time -from collections.abc import Iterator -from datetime import datetime, UTC -from dateutil.parser import ParserError, parse as dateutil_parse +from datetime import UTC, datetime from io import BytesIO from urllib.error import HTTPError as UrllibHTTPError, URLError from urllib.request import ProxyHandler, Request, build_opener, install_opener, urlopen @@ -33,33 +28,20 @@ from splunklib.binding import HTTPError as SplunkHTTPError import xmltodict from bs4 import BeautifulSoup -from bs4.dammit import UnicodeDammit from soar_sdk.abstract import SOARClient -from soar_sdk.action_results import ActionOutput, OutputField, PermissiveActionOutput from soar_sdk.app import App from soar_sdk.asset import AssetField, BaseAsset, FieldCategory from soar_sdk.logging import getLogger -from soar_sdk.models.artifact import Artifact -from soar_sdk.models.container import Container -from soar_sdk.params import OnPollParams, Param, Params from .splunk_consts import ( - CIM_CEF_MAP, SPLUNK_DEFAULT_REQUEST_TIMEOUT, - SPLUNK_DEFAULT_SOURCE, - SPLUNK_DEFAULT_SOURCE_TYPE, - SPLUNK_DISPOSITION_QUERY_FORMAT, - SPLUNK_ERR_BAD_DISPOSITION, - SPLUNK_ERR_BAD_STATUS, SPLUNK_ERR_CONNECTIVITY_FAILED, SPLUNK_ERR_CONNECTIVITY_TEST, SPLUNK_ERR_EMPTY_RESPONSE, SPLUNK_ERR_INVALID_INTEGER, SPLUNK_ERR_INVALID_SLEEP_TIME, - SPLUNK_ERR_NEED_PARAM, - SPLUNK_ERR_NON_NEGATIVE_INTEGER, - SPLUNK_ERR_NOT_ES, SPLUNK_ERR_INVALID_PARAM, + SPLUNK_ERR_NON_NEGATIVE_INTEGER, SPLUNK_ERR_REQUIRED_CONFIG_PARAMS, SPLUNK_ERR_SPLUNK_JOB_HAS_TIMED_OUT, SPLUNK_ERR_UNABLE_TO_CREATE_JOB, @@ -70,8 +52,6 @@ SPLUNK_PROG_CREATED_QUERY, SPLUNK_PROG_CREATING_SEARCH_JOB, SPLUNK_RID_SID_NOTABLE_QUERY, - SPLUNK_SEARCH_MODE_SMART, - SPLUNK_SEVERITY_MAP, SPLUNK_SUCCESS_CONNECTIVITY_TEST, ) @@ -653,246 +633,6 @@ def get_tz_str_from_epoch(self, fmt: str, epoch_milli: int) -> str: return utc_dt.astimezone(to_tz).strftime(fmt) -# --------------------------------------------------------------------------- -# Ingestion helpers -# --------------------------------------------------------------------------- -def _get_event_start(start_time: str | None) -> str | None: - if not start_time: - return None - try: - datetime_obj = dateutil_parse(start_time) - return datetime_obj.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ") - except ParserError as e: - logger.error("ParserError while parsing _time: %s", e) - return None - except Exception as e: - logger.error("Exception while parsing _time: %s", e) - return None - - -def _get_fips_enabled() -> bool: - try: - from phantom_common.install_info import is_fips_enabled # noqa: PLC0415 - return is_fips_enabled() - except ImportError: - return False - - -def _get_splunk_severity(item: dict) -> str: - severity = item.get("severity") - if isinstance(severity, list): - for key in ["critical", "high", "medium", "low", "informational"]: - if key in severity: - return SPLUNK_SEVERITY_MAP[key] - return "" - severity = SPLUNK_SEVERITY_MAP.get(severity) if severity else None - if not severity: - urgency = item.get("urgency") - severity = SPLUNK_SEVERITY_MAP.get(urgency, "medium") - return severity - - -def _get_splunk_title(item: dict, prefix: str, name_values: list[str]) -> str: - title = prefix - values_list = list(name_values) - if not title and not values_list: - values_list.append("source") - - values = "" - for i, nv in enumerate(values_list): - if CIM_CEF_MAP.get(nv) and item.get(CIM_CEF_MAP.get(nv)): - value = item.get(CIM_CEF_MAP.get(nv)) - elif item.get(nv): - value = item.get(nv) - else: - value = CIM_CEF_MAP.get(nv, nv) - values += f"{value}" + ("" if i == len(values_list) - 1 else ", ") - - if not title: - t = item.get("_time") - title = f"Splunk Log Entry on {t}" if t else "Splunk Log Entry" - else: - title = item.get(title, title) - - return f"{title}: {values}" - - -# --------------------------------------------------------------------------- -# Params / Outputs -# --------------------------------------------------------------------------- -class RunQueryParams(Params): - command: str = Param( - description="Beginning command (in Splunk Processing Language)", - required=False, - value_list=["search", "eval", "savedsearch", "stats", "table", "tstats"], - default="search", - ) - query: str = Param( - description="Query to run (in Splunk Processing Language)", - required=True, - primary=True, - cef_types=["splunk query"], - ) - display: str = Param(description="Display fields (comma-separated)", required=False, default="") - parse_only: bool = Param(description="Parse only", required=False, default=False) - add_raw_field: bool = Param(description="Ingest _raw field data", required=False, default=True) - attach_result: bool = Param(description="Attach result to the vault", required=False, default=False) - start_time: str = Param(description="Earliest time modifier", required=False, default="") - end_time: str = Param(description="Latest time modifier", required=False, default="") - search_mode: str = Param( - description="Search mode", - required=False, - value_list=["fast", "verbose", "smart"], - default="smart", - ) - time_format: str = Param(description="Custom timestamp format", required=False, default="") - - -class RunQueryOutput(PermissiveActionOutput): - pass - - -class RunQuerySummary(ActionOutput): - sid: str | None = None - total_events: int | None = None - - -class GetHostEventsParams(Params): - ip_hostname: str = Param( - description="Hostname/IP to search the events of", - required=True, - primary=True, - cef_types=["ip", "host name"], - ) - last_n_days: str = Param(description="Number of days ago", required=False, default="") - - -class GetHostEventsOutput(PermissiveActionOutput): - host: str | None = OutputField(column_name="Host") - time: str | None = OutputField(column_name="Time", alias="_time") - raw: str | None = OutputField(column_name="Raw", alias="_raw") - - -class GetHostEventsSummary(ActionOutput): - sid: str | None = None - total_events: int | None = None - - -class UpdateEventParams(Params): - event_ids: str = Param( - description="Event ID to update", - required=True, - primary=True, - cef_types=["splunk notable event id"], - ) - owner: str = Param(description="New owner for the event", required=False, default="") - status: str = Param( - description="New status for the event", - required=False, - default="", - value_list=["", "unassigned", "new", "in progress", "pending", "resolved", "closed"], - ) - integer_status: str = Param(description="Integer representing custom status value", required=False, default="") - urgency: str = Param( - description="New urgency for the event", - required=False, - default="", - value_list=["", "informational", "low", "medium", "high", "critical"], - ) - comment: str = Param(description="New comment for the event", required=False, default="") - disposition: str = Param( - description="New disposition field", - required=False, - default="", - value_list=[ - "", "Unassigned", "True Positive - Suspicious Activity", - "Benign Positive - Suspicious But Expected", - "False Positive - Incorrect Analytic Logic", - "False Positive - Inaccurate Data", "Undetermined", "Other", - ], - ) - integer_disposition: str = Param(description="Integer representing custom disposition value", required=False, default="") - wait_for_confirmation: bool = Param(description="Validate event_ids", required=False, default=False) - - -class UpdateEventOutput(ActionOutput): - status: str | None = OutputField(column_name="Status") - failure_count: int | None = None - message: str | None = OutputField(column_name="Message") - success: bool | None = None - success_count: int | None = None - - -class UpdateEventSummary(ActionOutput): - sid: str | None = None - updated_event_id: str | None = None - - -class PostDataParams(Params): - data: str = Param(description="Data to post", required=True) - host: str = Param( - description="Host for event", - required=False, - default="", - primary=True, - cef_types=["ip", "host name"], - ) - index: str = Param(description="Index to send event to", required=False, default="") - source: str = Param(description="Source for event", required=False, default="Phantom") - source_type: str = Param( - description="Type of source for event", - required=False, - default="Automation/Orchestration Platform", - ) - - -class PostDataOutput(ActionOutput): - status: str | None = OutputField(column_name="Status") - message: str | None = OutputField(column_name="Message") - - -# --------------------------------------------------------------------------- -# Custom view for run query -# --------------------------------------------------------------------------- -@app.view_handler(template="splunk_run_query.html") -def display_view(outputs: list[RunQueryOutput]) -> dict: - if not outputs: - return {"results": [{"data": {}, "param": {}}]} - - first = outputs[0].model_dump(exclude_none=True) - param = { - "query": first.get("_param_query", ""), - "command": first.get("_param_command", ""), - "display": first.get("_param_display", ""), - "parse_only": first.get("_param_parse_only", False), - "search_mode": first.get("_param_search_mode", "smart"), - } - display_fields = param.get("display", "") - - all_data = [] - for output in outputs: - data = {k: v for k, v in output.model_dump(exclude_none=True).items() if not k.startswith("_")} - all_data.append(data) - - if display_fields: - headers = [x.strip() for x in display_fields.split(",") if x.strip()] - elif all_data: - headers = [k for k in all_data[0] if not k.startswith("_")] - else: - headers = [] - - processed_data = [{h: item.get(h) for h in headers} for item in all_data] - - return { - "results": [{ - "param": param, - "data": all_data or {}, - "processed_data": processed_data, - "headers": headers, - }], - } - - # --------------------------------------------------------------------------- # Test Connectivity # --------------------------------------------------------------------------- @@ -918,397 +658,5 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: logger.info(SPLUNK_SUCCESS_CONNECTIVITY_TEST) -# --------------------------------------------------------------------------- -# Run Query -# --------------------------------------------------------------------------- -@app.action( - description="Run a search query on the Splunk device. Please escape any quotes that are part of the query string", - action_type="investigate", - read_only=True, - view_handler=display_view, - summary_type=RunQuerySummary, -) -def run_query(params: RunQueryParams, soar: SOARClient, asset: Asset) -> list[RunQueryOutput]: - helper = SplunkHelper(asset) - helper.validate_asset() - helper.connect() - - search_mode = params.search_mode or SPLUNK_SEARCH_MODE_SMART - kwargs: dict = {"adhoc_search_level": search_mode} - if params.start_time: - kwargs["earliest_time"] = params.start_time - if params.end_time: - kwargs["latest_time"] = params.end_time - if params.time_format: - kwargs["time_format"] = params.time_format - - search_command = params.command - search_string = params.query - - if not search_command: - if search_string[0] != "|" and not search_string.startswith("search"): - search_string = f"search {search_string.strip()}" - search_query = search_string - else: - search_query = f"{search_command.strip()} {search_string.strip()}" - - sid, results_list = helper.run_query( - search_query, - kwargs_create=kwargs, - parse_only=params.parse_only, - add_raw_field=params.add_raw_field, - ) - - if params.attach_result: - _attach_json_result(soar, results_list) - - soar.set_summary(RunQuerySummary(sid=sid, total_events=len(results_list))) - soar.set_message(f"Sid: {sid}, Total events: {len(results_list)}") - - param_info = { - "_param_query": params.query, - "_param_command": params.command, - "_param_display": params.display, - "_param_parse_only": params.parse_only, - "_param_search_mode": search_mode, - } - return [RunQueryOutput(**{**r, **param_info}) for r in results_list] - - -def _attach_json_result(soar: SOARClient, data: list[dict]): - try: - container_id = soar.get_executing_container_id() - soar.vault.create_attachment( - container_id=container_id, - file_content=json.dumps(data), - file_name="splunk_run_query_result.json", - ) - except Exception as e: - logger.error("Error attaching results to vault: %s", e) - - -# --------------------------------------------------------------------------- -# Get Host Events -# --------------------------------------------------------------------------- -@app.action( - description="Get events pertaining to a host that have occurred in the last 'N' days", - action_type="investigate", - read_only=True, - render_as="table", - summary_type=GetHostEventsSummary, -) -def get_host_events(params: GetHostEventsParams, soar: SOARClient, asset: Asset) -> list[GetHostEventsOutput]: - helper = SplunkHelper(asset) - helper.validate_asset() - helper.connect() - - ip_hostname = params.ip_hostname - last_n_days = SplunkHelper.validate_integer(params.last_n_days, "'last_n_days' action") - - search_query = f'search host="{ip_hostname}"' - if last_n_days: - search_query += f" earliest=-{last_n_days}d" - - sid, results_list = helper.run_query(search_query) - - soar.set_summary(GetHostEventsSummary(sid=sid, total_events=len(results_list))) - soar.set_message(f"Sid: {sid}, Total events: {len(results_list)}") - - return [GetHostEventsOutput(**r) for r in results_list] - - -# --------------------------------------------------------------------------- -# Update Event -# --------------------------------------------------------------------------- -@app.action( - description="Update a notable event", - action_type="generic", - read_only=False, - render_as="table", - summary_type=UpdateEventSummary, -) -def update_event(params: UpdateEventParams, soar: SOARClient, asset: Asset) -> list[UpdateEventOutput]: - helper = SplunkHelper(asset) - helper.validate_asset() - - if not helper.check_for_es(): - raise RuntimeError(SPLUNK_ERR_NOT_ES) - - ids = params.event_ids - owner = params.owner - status = params.status - comment = params.comment - urgency = params.urgency - disposition = params.disposition or "" - wait_for_confirmation = params.wait_for_confirmation - - integer_status = SplunkHelper.validate_integer( - params.integer_status, "'integer_status' action", allow_zero=True - ) - integer_disposition = SplunkHelper.validate_integer( - params.integer_disposition, "'integer_disposition' action", allow_zero=True - ) - - if not any([comment, status, urgency, owner, disposition]) and integer_status is None and integer_disposition is None: - raise ValueError(SPLUNK_ERR_NEED_PARAM) - - splunk_status_dict: dict[str, int] = {} - splunk_disposition_dict: dict[str, int] = {} - - if status or integer_status is not None: - splunk_status_dict = helper.get_status_dict("notable") - if not splunk_status_dict: - raise RuntimeError("Error occurred while fetching Splunk event status") - - if disposition or integer_disposition is not None: - splunk_disposition_dict = helper.get_status_dict("disposition") - if not splunk_disposition_dict: - raise RuntimeError("Error occurred while fetching Splunk event disposition") - - helper.connect() - - # Resolve SID+RID combo to event_id - regexp = re.compile(r"\+\d*(\.\d+)?[\"$]") - if regexp.search(json.dumps(ids)): - logger.progress("Interpreting the event ID as an SID + RID combo") - try: - ids = helper.resolve_event_id(ids) - except Exception: - raise RuntimeError("Unable to find underlying event_id from SID + RID combo") from None - - if wait_for_confirmation: - search_query = f"search `notable_by_id({ids})`" - _sid, validate_results = helper.run_query(search_query) - if not validate_results: - raise ValueError("Please provide a valid event ID") - - request_body: dict = {"ruleUIDs": ids} - - # Status - if integer_status is not None: - if int(integer_status) not in list(splunk_status_dict.values()): - raise ValueError( - "Please provide a valid value in 'integer_status' action parameter. " - f"Valid values: {', '.join(map(str, splunk_status_dict.values()))}" - ) - request_body["status"] = str(integer_status) - elif status: - if status not in splunk_status_dict: - if not status.isdigit(): - raise ValueError(SPLUNK_ERR_BAD_STATUS) - request_body["status"] = status - else: - request_body["status"] = splunk_status_dict[status] - - # Disposition - if integer_disposition is not None: - if int(integer_disposition) not in splunk_disposition_dict.values(): - raise ValueError( - "Please provide a valid value in 'integer_disposition' action parameter. " - f"Valid values: {', '.join(map(str, splunk_disposition_dict.values()))}" - ) - request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(integer_disposition) - elif disposition: - if disposition not in splunk_disposition_dict: - if not disposition.isdigit(): - raise ValueError(SPLUNK_ERR_BAD_DISPOSITION) - request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(disposition) - else: - request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(splunk_disposition_dict[disposition]) - - param_mapping = {"urgency": urgency, "comment": comment, "newOwner": owner} - request_body.update({k: v for k, v in param_mapping.items() if v}) - - resp_data = helper.make_rest_call_retry("notable_update", request_body) - - if resp_data and "success" in resp_data and not resp_data.get("success"): - msg = resp_data.get("message") - raise RuntimeError(msg if msg else "Unable to update the notable event") - - soar.set_summary(UpdateEventSummary(updated_event_id=ids)) - - if wait_for_confirmation: - msg = f"Updated Event ID: {ids}" - else: - msg = ( - f"Updated Event ID: {ids}. The event_id has not been verified. " - "Please confirm that the provided event_id corresponds to an actual notable event" - ) - - soar.set_message(msg) - - if resp_data: - resp_data["status"] = "success" - resp_data["message"] = msg - return [UpdateEventOutput(**resp_data)] - return [UpdateEventOutput(status="success", message=msg)] - - -# --------------------------------------------------------------------------- -# Post Data -# --------------------------------------------------------------------------- -@app.action( - description="Post data to Splunk", - action_type="generic", - read_only=False, - render_as="table", -) -def post_data(params: PostDataParams, soar: SOARClient, asset: Asset) -> list[PostDataOutput]: - helper = SplunkHelper(asset) - helper.validate_asset() - - try: - post_bytes = UnicodeDammit(params.data).unicode_markup.encode("utf-8") - except Exception as e: - logger.error("Error while encoding data: %s", e) - post_bytes = params.data.encode("utf-8") - - get_params: dict[str, str] = { - "source": params.source or SPLUNK_DEFAULT_SOURCE, - "sourcetype": params.source_type or SPLUNK_DEFAULT_SOURCE_TYPE, - } - if params.host: - get_params["host"] = params.host - if params.index: - get_params["index"] = params.index - - helper.make_rest_call_retry("receivers/simple", post_bytes, params=get_params) - - soar.set_message("Successfully posted the data") - return [PostDataOutput(status="success", message="Successfully posted the data")] - - -# --------------------------------------------------------------------------- -# On Poll -# --------------------------------------------------------------------------- -@app.on_poll() -def on_poll(params: OnPollParams, soar: SOARClient, asset: Asset) -> Iterator[Container | Artifact]: - helper = SplunkHelper(asset) - helper.validate_asset() - helper.connect() - - search_command = asset.on_poll_command - search_string = asset.on_poll_query - po = asset.on_poll_parse_only - include_cim_fields = asset.include_cim_fields - use_event_id_sdi = asset.use_event_id_sdi - - if not search_string: - raise ValueError("Need to specify Query String to use polling") - - try: - if not search_command: - if search_string[0] != "|" and not search_string.startswith("search"): - search_string = f"search {search_string.strip()}" - search_query = search_string - else: - search_query = f"{search_command.strip()} {search_string.strip()}" - except Exception: - raise ValueError("Error occurred while parsing the search query") from None - - search_params: dict = {} - state = asset.ingest_state - is_poll_now = params.is_manual_poll() - - if is_poll_now: - search_params["max_count"] = params.container_count or 100 - else: - search_params["max_count"] = asset.max_container - start_time = state.get("start_time") - if start_time: - search_params["index_earliest"] = start_time - - if int(search_params["max_count"]) <= 0: - logger.debug("container_count <= 0, ignoring max_count") - search_params.pop("max_count") - - try: - _sid, results_list = helper.run_query(search_query, kwargs_create=search_params, parse_only=po) - except Exception as e: - msg = str(e) - if "Invalid index_earliest" in msg: - logger.debug("Invalid start_time %s, retrying without it", search_params.get("index_earliest")) - state.pop("start_time", None) - raise - - display = asset.on_poll_display - header_set = None - if display: - header_set = [x.strip().lower() for x in display.split(",")] - - data = list(reversed(results_list)) - logger.info("Total %d event(s) fetched", len(data)) - - container_name_prefix = asset.container_name_prefix or "" - raw_values = asset.container_name_values - container_name_values = [x.strip() for x in raw_values.split(",")] if raw_values else [] - - count = 1 - for item in data: - try: - cef: dict = {} - if "_serial" in item: - item.pop("_serial") - - if header_set: - name_mappings = {k.lower(): k for k in item if k.lower() in header_set} - for h in header_set: - cef_name = CIM_CEF_MAP.get(h, h) - cef_name = name_mappings.get(cef_name, cef_name) - cef_key_value = name_mappings.get(h, h) - cef[cef_name] = item.get(cef_key_value) - if include_cim_fields: - cef[cef_key_value] = item.get(cef_key_value) - else: - for k, v in item.items(): - cef[CIM_CEF_MAP.get(k, k)] = v - if include_cim_fields: - cef[k] = v - - if use_event_id_sdi and "event_id" in item: - sdi = item["event_id"] - else: - if use_event_id_sdi and "event_id" not in item: - logger.warning("use_event_id_sdi enabled but event_id missing, using hash") - input_str = UnicodeDammit(json.dumps(item)).unicode_markup.encode("utf-8") - if _get_fips_enabled(): - sdi = hashlib.sha256(input_str).hexdigest() - else: - sdi = hashlib.md5(input_str).hexdigest() # noqa: S324 - - severity = _get_splunk_severity(item) - spl_event_start = _get_event_start(item.get("_time")) - container_name = _get_splunk_title(item, container_name_prefix, container_name_values) - - yield Container( - name=container_name, - severity=severity, - source_data_identifier=sdi, - ) - - if asset.remove_empty_cef: - cef = {k: v for k, v in cef.items() if v is not None} - - yield Artifact( - cef=cef, - name="Field Values", - source_data_identifier=sdi, - severity=severity, - start_time=spl_event_start, - ) - - if count == asset.container_update_state and not is_poll_now: - state["start_time"] = item.get("_indextime") - count = 0 - count += 1 - - except Exception as e: - logger.error("Error processing event: %s", e) - continue - - if data and not is_poll_now: - state["start_time"] = data[-1].get("_indextime") - - if __name__ == "__main__": app.cli() From 09fe383f07b32b94c297177e285fbe540f1649f8 Mon Sep 17 00:00:00 2001 From: grokas Date: Mon, 27 Apr 2026 13:40:45 -0700 Subject: [PATCH 04/10] fix: change action default due to different sdk param behaviour --- src/actions/run_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/run_query.py b/src/actions/run_query.py index a3067c4..269ddc3 100644 --- a/src/actions/run_query.py +++ b/src/actions/run_query.py @@ -18,7 +18,7 @@ class RunQueryParams(Params): description="Beginning command (in Splunk Processing Language)", required=False, value_list=["search", "eval", "savedsearch", "stats", "table", "tstats"], - default="search", + default="", ) query: str = Param( description="Query to run (in Splunk Processing Language)", From b9255c1524c04b025eb08e05f69ac95043b8b175 Mon Sep 17 00:00:00 2001 From: grokas Date: Mon, 27 Apr 2026 13:58:36 -0700 Subject: [PATCH 05/10] chore: ci fixes --- .pre-commit-config.yaml | 85 ++++++++++++ LICENSE | 201 +++++++++++++++++++++++++++ NOTICE | 3 + README.md | 262 ++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- release_notes/unreleased.md | 4 + tests/__init__.py | 13 ++ uv.lock | 4 +- 8 files changed, 571 insertions(+), 3 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a555dd5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,85 @@ +# 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/phantomcyber/soar-app-linter + rev: 0.1.0 + hooks: + - 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.136.0 + hooks: + - id: semgrep + additional_dependencies: ["setuptools==81.0.0"] + - 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.1.0 + hooks: + - id: build-docs + language: python + additional_dependencies: ["local-hooks"] + args: ['.'] + - id: copyright + language: python + additional_dependencies: ["local-hooks"] + args: ['.'] + - 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/LICENSE b/LICENSE new file mode 100644 index 0000000..fbee5e9 --- /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) 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..0b0d66d --- /dev/null +++ b/NOTICE @@ -0,0 +1,3 @@ +Splunk SOAR App: splunk +Copyright (c) 2016-2026 Splunk Inc. +Third Party Software Attributions: diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a02a7e --- /dev/null +++ b/README.md @@ -0,0 +1,262 @@ +# Splunk + +Publisher: Splunk
+Connector Version: 3.0.5
+Product Vendor: Splunk Inc.
+Product Name: Splunk Enterprise
+Minimum Product Version: 7.0.0 + +This app integrates with Splunk to update data on the device, in addition to investigate and ingestion actions + +### Configuration variables + +This table lists the configuration variables required to operate Splunk. These variables are specified when configuring a Splunk Enterprise asset in Splunk SOAR. + +VARIABLE | REQUIRED | TYPE | DESCRIPTION +-------- | -------- | ---- | ----------- +**device** | required | string | Device IP/Hostname | +**port** | optional | numeric | Port | +**username** | optional | string | Username | +**password** | optional | password | Password | +**api_token** | optional | password | API token | +**splunk_owner** | optional | string | The owner context of the namespace | +**splunk_app** | optional | string | The app context of the namespace | +**timezone** | optional | string | Splunk Server Timezone | +**verify_server_cert** | optional | boolean | Verify Server Certificate | +**on_poll_command** | optional | string | Command for query to use with On Poll | +**on_poll_query** | optional | string | Query to use with On Poll | +**on_poll_display** | optional | string | Fields to save with On Poll | +**on_poll_parse_only** | optional | boolean | Parse Only | +**max_container** | optional | numeric | Max events to ingest for Scheduled Polling (Default: 100) | +**container_update_state** | optional | numeric | Container count to update the state file | +**container_name_prefix** | optional | string | Name to give containers created via ingestion | +**container_name_values** | optional | string | Values to append to container name | +**retry_count** | optional | numeric | Number of retries | +**remove_empty_cef** | optional | boolean | Remove CEF fields having empty values from the artifact | +**sleeptime_in_requests** | optional | numeric | The time to wait for next REST call (max 120 seconds) | +**include_cim_fields** | optional | boolean | Option to keep original Splunk CIM together with SOAR CEF fields | +**splunk_job_timeout** | optional | numeric | The duration in seconds to wait before a scheduled Splunk job times out | +**use_event_id_sdi** | optional | boolean | Option to use the event_id field value as the source data identifier instead of the full event hash | + +### Supported Actions + +[test connectivity](#action-test-connectivity) - test connectivity
+[get host events](#action-get-host-events) - Get events pertaining to a host that have occurred in the last 'N' days
+[on poll](#action-on-poll) - on poll
+[post data](#action-post-data) - Post data to Splunk
+[run query](#action-run-query) - Run a search query on the Splunk device. Please escape any quotes that are part of the query string
+[update event](#action-update-event) - Update a notable event + +## action: 'test connectivity' + +test connectivity + +Type: **test**
+Read only: **True** + +Basic test for app. + +#### Action Parameters + +No parameters are required for this action + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failure | +action_result.message | string | | | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'get host events' + +Get events pertaining to a host that have occurred in the last 'N' days + +Type: **investigate**
+Read only: **True** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**ip_hostname** | required | Hostname/IP to search the events of | string | `ip` `host name` | +**last_n_days** | optional | Number of days ago | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failure | +action_result.message | string | | | +action_result.parameter.ip_hostname | string | `ip` `host name` | | +action_result.parameter.last_n_days | string | | | +action_result.data.\*.host | string | | | +action_result.data.\*.\_time | string | | | +action_result.data.\*.\_raw | string | | | +action_result.summary.sid | string | | | +action_result.summary.total_events | numeric | | | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'on poll' + +on poll + +Type: **ingest**
+Read only: **True** + +Callback action for the on_poll ingest functionality + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**start_time** | optional | Start of time range, in epoch time (milliseconds). | numeric | | +**end_time** | optional | End of time range, in epoch time (milliseconds). | numeric | | +**container_count** | optional | Maximum number of container records to query for. | numeric | | +**artifact_count** | optional | Maximum number of artifact records to query for. | numeric | | +**container_id** | optional | Comma-separated list of container IDs to limit the ingestion to. | string | | + +#### Action Output + +No Output + +## action: 'post data' + +Post data to Splunk + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**data** | required | Data to post | string | | +**host** | optional | Host for event | string | `ip` `host name` | +**index** | optional | Index to send event to | string | | +**source** | optional | Source for event | string | | +**source_type** | optional | Type of source for event | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failure | +action_result.message | string | | | +action_result.parameter.data | string | | | +action_result.parameter.host | string | `ip` `host name` | | +action_result.parameter.index | string | | | +action_result.parameter.source | string | | | +action_result.parameter.source_type | string | | | +action_result.data.\*.status | string | | | +action_result.data.\*.message | string | | | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'run query' + +Run a search query on the Splunk device. Please escape any quotes that are part of the query string + +Type: **investigate**
+Read only: **True** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**command** | optional | Beginning command (in Splunk Processing Language) | string | | +**query** | required | Query to run (in Splunk Processing Language) | string | `splunk query` | +**display** | optional | Display fields (comma-separated) | string | | +**parse_only** | optional | Parse only | boolean | | +**add_raw_field** | optional | Ingest \_raw field data | boolean | | +**attach_result** | optional | Attach result to the vault | boolean | | +**start_time** | optional | Earliest time modifier | string | | +**end_time** | optional | Latest time modifier | string | | +**search_mode** | optional | Search mode | string | | +**time_format** | optional | Custom timestamp format | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failure | +action_result.message | string | | | +action_result.parameter.command | string | | | +action_result.parameter.query | string | `splunk query` | | +action_result.parameter.display | string | | | +action_result.parameter.parse_only | boolean | | | +action_result.parameter.add_raw_field | boolean | | | +action_result.parameter.attach_result | boolean | | | +action_result.parameter.start_time | string | | | +action_result.parameter.end_time | string | | | +action_result.parameter.search_mode | string | | | +action_result.parameter.time_format | string | | | +action_result.summary.sid | string | | | +action_result.summary.total_events | numeric | | | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'update event' + +Update a notable event + +Type: **generic**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**event_ids** | required | Event ID to update | string | `splunk notable event id` | +**owner** | optional | New owner for the event | string | | +**status** | optional | New status for the event | string | | +**integer_status** | optional | Integer representing custom status value | string | | +**urgency** | optional | New urgency for the event | string | | +**comment** | optional | New comment for the event | string | | +**disposition** | optional | New disposition field | string | | +**integer_disposition** | optional | Integer representing custom disposition value | string | | +**wait_for_confirmation** | optional | Validate event_ids | boolean | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failure | +action_result.message | string | | | +action_result.parameter.event_ids | string | `splunk notable event id` | | +action_result.parameter.owner | string | | | +action_result.parameter.status | string | | | +action_result.parameter.integer_status | string | | | +action_result.parameter.urgency | string | | | +action_result.parameter.comment | string | | | +action_result.parameter.disposition | string | | | +action_result.parameter.integer_disposition | string | | | +action_result.parameter.wait_for_confirmation | boolean | | | +action_result.data.\*.status | string | | | +action_result.data.\*.failure_count | numeric | | | +action_result.data.\*.message | string | | | +action_result.data.\*.success | boolean | | True False | +action_result.data.\*.success_count | numeric | | | +action_result.summary.sid | string | | | +action_result.summary.updated_event_id | string | | | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +______________________________________________________________________ + +Auto-generated Splunk SOAR Connector documentation. + +Copyright 2026 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/pyproject.toml b/pyproject.toml index 92fa84e..e4c5a04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "splunk-soar-sdk>=3.18.1", "beautifulsoup4>=4.12.0", "python-dateutil>=2.9.0", - "requests>=2.31.0", + "requests>=2.33.0", "xmltodict>=0.13.0", ] diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md index fbcb2fd..9f6593d 100644 --- a/release_notes/unreleased.md +++ b/release_notes/unreleased.md @@ -1 +1,5 @@ **Unreleased** + +* Migrated app to Splunk SOAR SDK +* Modernized app, structure, and dependencies +* Universal api make request action diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..6465bfc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2026 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/uv.lock b/uv.lock index 43ec43f..9ed82ec 100644 --- a/uv.lock +++ b/uv.lock @@ -1103,7 +1103,7 @@ wheels = [ [[package]] name = "splunk" -version = "3.0.0" +version = "3.0.5" source = { virtual = "." } dependencies = [ { name = "beautifulsoup4", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -1129,7 +1129,7 @@ dev = [ requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.12.0" }, { name = "python-dateutil", specifier = ">=2.9.0" }, - { name = "requests", specifier = ">=2.31.0" }, + { name = "requests", specifier = ">=2.33.0" }, { name = "splunk-sdk", specifier = ">=2.1.1" }, { name = "splunk-soar-sdk", specifier = ">=3.18.1" }, { name = "xmltodict", specifier = ">=0.13.0" }, From c855b8f7ac5e4c31fc4a18f2d7d6d2f64db33513 Mon Sep 17 00:00:00 2001 From: grokas Date: Mon, 27 Apr 2026 15:00:17 -0700 Subject: [PATCH 06/10] chore: pre-commit fixes --- Splunk/manifest.json | 1532 -------------------------------- src/actions/get_host_events.py | 12 +- src/actions/on_poll.py | 35 +- src/actions/post_data.py | 8 +- src/actions/run_query.py | 48 +- src/actions/update_event.py | 68 +- src/app.py | 141 ++- src/splunk_consts.py | 36 +- 8 files changed, 265 insertions(+), 1615 deletions(-) delete mode 100644 Splunk/manifest.json diff --git a/Splunk/manifest.json b/Splunk/manifest.json deleted file mode 100644 index e5c36b1..0000000 --- a/Splunk/manifest.json +++ /dev/null @@ -1,1532 +0,0 @@ -{ - "name": "Splunk", - "description": "This app integrates with Splunk to update data on the device, in addition to investigate and ingestion actions", - "appid": "91883aa8-9c81-470b-97a1-5d8f7995f560", - "type": "siem", - "product_vendor": "Splunk Inc.", - "app_version": "3.0.1", - "license": "Copyright (c) 2016-2026 Splunk Inc.", - "min_phantom_version": "7.0.0", - "package_name": "phantom_splunk", - "main_module": "src.app:app", - "logo": "logo_splunk.svg", - "logo_dark": "logo_splunk_dark.svg", - "product_name": "Splunk Enterprise", - "python_version": "3.13,3.14", - "product_version_regex": ".*", - "publisher": "Splunk", - "utctime_updated": "2026-04-23T00:53:52.579554Z", - "fips_compliant": true, - "contributors": [], - "configuration": { - "device": { - "data_type": "string", - "required": true, - "description": "Device IP/Hostname", - "order": 0, - "category": "connectivity" - }, - "port": { - "data_type": "numeric", - "required": false, - "description": "Port", - "order": 1, - "category": "connectivity", - "default": 8089 - }, - "username": { - "data_type": "string", - "required": false, - "description": "Username", - "order": 2, - "category": "connectivity", - "default": "" - }, - "password": { - "data_type": "password", - "required": false, - "description": "Password", - "order": 3, - "category": "connectivity", - "default": "" - }, - "api_token": { - "data_type": "password", - "required": false, - "description": "API token", - "order": 4, - "category": "connectivity", - "default": "" - }, - "splunk_owner": { - "data_type": "string", - "required": false, - "description": "The owner context of the namespace", - "order": 5, - "category": "connectivity", - "default": "" - }, - "splunk_app": { - "data_type": "string", - "required": false, - "description": "The app context of the namespace", - "order": 6, - "category": "connectivity", - "default": "" - }, - "timezone": { - "data_type": "string", - "required": true, - "description": "Splunk Server Timezone", - "order": 7, - "category": "connectivity" - }, - "verify_server_cert": { - "data_type": "boolean", - "required": false, - "description": "Verify Server Certificate", - "order": 8, - "category": "connectivity", - "default": false - }, - "on_poll_command": { - "data_type": "string", - "required": false, - "description": "Command for query to use with On Poll", - "order": 9, - "category": "ingest", - "default": "", - "value_list": [ - "", - "search", - "eval", - "savedsearch", - "stats", - "table", - "tstats" - ] - }, - "on_poll_query": { - "data_type": "string", - "required": false, - "description": "Query to use with On Poll", - "order": 10, - "category": "ingest", - "default": "" - }, - "on_poll_display": { - "data_type": "string", - "required": false, - "description": "Fields to save with On Poll", - "order": 11, - "category": "ingest", - "default": "" - }, - "on_poll_parse_only": { - "data_type": "boolean", - "required": false, - "description": "Parse Only", - "order": 12, - "category": "ingest", - "default": true - }, - "max_container": { - "data_type": "numeric", - "required": false, - "description": "Max events to ingest for Scheduled Polling (Default: 100)", - "order": 13, - "category": "ingest", - "default": 100 - }, - "container_update_state": { - "data_type": "numeric", - "required": false, - "description": "Container count to update the state file", - "order": 14, - "category": "ingest", - "default": 100 - }, - "container_name_prefix": { - "data_type": "string", - "required": false, - "description": "Name to give containers created via ingestion", - "order": 15, - "category": "ingest", - "default": "" - }, - "container_name_values": { - "data_type": "string", - "required": false, - "description": "Values to append to container name", - "order": 16, - "category": "ingest", - "default": "" - }, - "retry_count": { - "data_type": "numeric", - "required": false, - "description": "Number of retries", - "order": 17, - "category": "connectivity", - "default": 3 - }, - "remove_empty_cef": { - "data_type": "boolean", - "required": false, - "description": "Remove CEF fields having empty values from the artifact", - "order": 18, - "category": "ingest", - "default": false - }, - "sleeptime_in_requests": { - "data_type": "numeric", - "required": false, - "description": "The time to wait for next REST call (max 120 seconds)", - "order": 19, - "category": "connectivity", - "default": 1 - }, - "include_cim_fields": { - "data_type": "boolean", - "required": false, - "description": "Option to keep original Splunk CIM together with SOAR CEF fields", - "order": 20, - "category": "ingest", - "default": false - }, - "splunk_job_timeout": { - "data_type": "numeric", - "required": false, - "description": "The duration in seconds to wait before a scheduled Splunk job times out", - "order": 21, - "category": "connectivity", - "default": 1200 - }, - "use_event_id_sdi": { - "data_type": "boolean", - "required": false, - "description": "Option to use the event_id field value as the source data identifier instead of the full event hash", - "order": 22, - "category": "ingest", - "default": false - } - }, - "actions": [ - { - "action": "test connectivity", - "identifier": "test_connectivity", - "description": "test connectivity", - "type": "test", - "read_only": true, - "versions": "EQ(*)", - "verbose": "Basic test for app.", - "parameters": {}, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failure" - ] - }, - { - "data_path": "action_result.message", - "data_type": "string" - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - } - ] - }, - { - "action": "run query", - "identifier": "run_query", - "description": "Run a search query on the Splunk device. Please escape any quotes that are part of the query string", - "type": "investigate", - "read_only": true, - "versions": "EQ(*)", - "verbose": "", - "parameters": { - "command": { - "order": 0, - "name": "command", - "description": "Beginning command (in Splunk Processing Language)", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "search", - "value_list": [ - "search", - "eval", - "savedsearch", - "stats", - "table", - "tstats" - ] - }, - "query": { - "order": 1, - "name": "query", - "description": "Query to run (in Splunk Processing Language)", - "data_type": "string", - "required": true, - "primary": true, - "allow_list": false, - "contains": [ - "splunk query" - ] - }, - "display": { - "order": 2, - "name": "display", - "description": "Display fields (comma-separated)", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - }, - "parse_only": { - "order": 3, - "name": "parse_only", - "description": "Parse only", - "data_type": "boolean", - "required": false, - "primary": false, - "allow_list": false, - "default": false - }, - "add_raw_field": { - "order": 4, - "name": "add_raw_field", - "description": "Ingest _raw field data", - "data_type": "boolean", - "required": false, - "primary": false, - "allow_list": false, - "default": true - }, - "attach_result": { - "order": 5, - "name": "attach_result", - "description": "Attach result to the vault", - "data_type": "boolean", - "required": false, - "primary": false, - "allow_list": false, - "default": false - }, - "start_time": { - "order": 6, - "name": "start_time", - "description": "Earliest time modifier", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - }, - "end_time": { - "order": 7, - "name": "end_time", - "description": "Latest time modifier", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - }, - "search_mode": { - "order": 8, - "name": "search_mode", - "description": "Search mode", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "smart", - "value_list": [ - "fast", - "verbose", - "smart" - ] - }, - "time_format": { - "order": 9, - "name": "time_format", - "description": "Custom timestamp format", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failure" - ] - }, - { - "data_path": "action_result.message", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.command", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.query", - "data_type": "string", - "contains": [ - "splunk query" - ] - }, - { - "data_path": "action_result.parameter.display", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.parse_only", - "data_type": "boolean" - }, - { - "data_path": "action_result.parameter.add_raw_field", - "data_type": "boolean" - }, - { - "data_path": "action_result.parameter.attach_result", - "data_type": "boolean" - }, - { - "data_path": "action_result.parameter.start_time", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.end_time", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.search_mode", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.time_format", - "data_type": "string" - }, - { - "data_path": "action_result.summary.sid", - "data_type": "string" - }, - { - "data_path": "action_result.summary.total_events", - "data_type": "numeric" - }, - { - "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": "custom", - "view": "src.app.display_view" - } - }, - { - "action": "get host events", - "identifier": "get_host_events", - "description": "Get events pertaining to a host that have occurred in the last 'N' days", - "type": "investigate", - "read_only": true, - "versions": "EQ(*)", - "verbose": "", - "parameters": { - "ip_hostname": { - "order": 0, - "name": "ip_hostname", - "description": "Hostname/IP to search the events of", - "data_type": "string", - "required": true, - "primary": true, - "allow_list": false, - "contains": [ - "ip", - "host name" - ] - }, - "last_n_days": { - "order": 1, - "name": "last_n_days", - "description": "Number of days ago", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failure" - ] - }, - { - "data_path": "action_result.message", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.ip_hostname", - "data_type": "string", - "contains": [ - "ip", - "host name" - ] - }, - { - "data_path": "action_result.parameter.last_n_days", - "data_type": "string" - }, - { - "data_path": "action_result.summary.sid", - "data_type": "string" - }, - { - "data_path": "action_result.summary.total_events", - "data_type": "numeric" - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - } - ] - }, - { - "action": "update event", - "identifier": "update_event", - "description": "Update a notable event", - "type": "generic", - "read_only": false, - "versions": "EQ(*)", - "verbose": "", - "parameters": { - "event_ids": { - "order": 0, - "name": "event_ids", - "description": "Event ID to update", - "data_type": "string", - "required": true, - "primary": true, - "allow_list": false, - "contains": [ - "splunk notable event id" - ] - }, - "owner": { - "order": 1, - "name": "owner", - "description": "New owner for the event", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - }, - "status": { - "order": 2, - "name": "status", - "description": "New status for the event", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "", - "value_list": [ - "", - "unassigned", - "new", - "in progress", - "pending", - "resolved", - "closed" - ] - }, - "integer_status": { - "order": 3, - "name": "integer_status", - "description": "Integer representing custom status value", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - }, - "urgency": { - "order": 4, - "name": "urgency", - "description": "New urgency for the event", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "", - "value_list": [ - "", - "informational", - "low", - "medium", - "high", - "critical" - ] - }, - "comment": { - "order": 5, - "name": "comment", - "description": "New comment for the event", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - }, - "disposition": { - "order": 6, - "name": "disposition", - "description": "New disposition field", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "", - "value_list": [ - "", - "Unassigned", - "True Positive - Suspicious Activity", - "Benign Positive - Suspicious But Expected", - "False Positive - Incorrect Analytic Logic", - "False Positive - Inaccurate Data", - "Undetermined", - "Other" - ] - }, - "integer_disposition": { - "order": 7, - "name": "integer_disposition", - "description": "Integer representing custom disposition value", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - }, - "wait_for_confirmation": { - "order": 8, - "name": "wait_for_confirmation", - "description": "Validate event_ids", - "data_type": "boolean", - "required": false, - "primary": false, - "allow_list": false, - "default": false - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failure" - ] - }, - { - "data_path": "action_result.message", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.event_ids", - "data_type": "string", - "contains": [ - "splunk notable event id" - ] - }, - { - "data_path": "action_result.parameter.owner", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.status", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.integer_status", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.urgency", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.comment", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.disposition", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.integer_disposition", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.wait_for_confirmation", - "data_type": "boolean" - }, - { - "data_path": "action_result.data.*.failure_count", - "data_type": "numeric" - }, - { - "data_path": "action_result.data.*.message", - "data_type": "string" - }, - { - "data_path": "action_result.data.*.success", - "data_type": "boolean", - "example_values": [ - true, - false - ] - }, - { - "data_path": "action_result.data.*.success_count", - "data_type": "numeric" - }, - { - "data_path": "action_result.summary.sid", - "data_type": "string" - }, - { - "data_path": "action_result.summary.updated_event_id", - "data_type": "string" - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - } - ] - }, - { - "action": "post data", - "identifier": "post_data", - "description": "Post data to Splunk", - "type": "generic", - "read_only": false, - "versions": "EQ(*)", - "verbose": "", - "parameters": { - "data": { - "order": 0, - "name": "data", - "description": "Data to post", - "data_type": "string", - "required": true, - "primary": false, - "allow_list": false - }, - "host": { - "order": 1, - "name": "host", - "description": "Host for event", - "data_type": "string", - "required": false, - "primary": true, - "allow_list": false, - "contains": [ - "ip", - "host name" - ], - "default": "" - }, - "index": { - "order": 2, - "name": "index", - "description": "Index to send event to", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "" - }, - "source": { - "order": 3, - "name": "source", - "description": "Source for event", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "Phantom" - }, - "source_type": { - "order": 4, - "name": "source_type", - "description": "Type of source for event", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": false, - "default": "Automation/Orchestration Platform" - } - }, - "output": [ - { - "data_path": "action_result.status", - "data_type": "string", - "example_values": [ - "success", - "failure" - ] - }, - { - "data_path": "action_result.message", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.data", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.host", - "data_type": "string", - "contains": [ - "ip", - "host name" - ] - }, - { - "data_path": "action_result.parameter.index", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.source", - "data_type": "string" - }, - { - "data_path": "action_result.parameter.source_type", - "data_type": "string" - }, - { - "data_path": "summary.total_objects", - "data_type": "numeric", - "example_values": [ - 1 - ] - }, - { - "data_path": "summary.total_objects_successful", - "data_type": "numeric", - "example_values": [ - 1 - ] - } - ] - }, - { - "action": "on poll", - "identifier": "on_poll", - "description": "on poll", - "type": "ingest", - "read_only": true, - "versions": "EQ(*)", - "verbose": "Callback action for the on_poll ingest functionality", - "parameters": { - "start_time": { - "order": 0, - "name": "start_time", - "description": "Start of time range, in epoch time (milliseconds).", - "data_type": "numeric", - "required": false, - "primary": false, - "allow_list": false - }, - "end_time": { - "order": 1, - "name": "end_time", - "description": "End of time range, in epoch time (milliseconds).", - "data_type": "numeric", - "required": false, - "primary": false, - "allow_list": false - }, - "container_count": { - "order": 2, - "name": "container_count", - "description": "Maximum number of container records to query for.", - "data_type": "numeric", - "required": false, - "primary": false, - "allow_list": false - }, - "artifact_count": { - "order": 3, - "name": "artifact_count", - "description": "Maximum number of artifact records to query for.", - "data_type": "numeric", - "required": false, - "primary": false, - "allow_list": false - }, - "container_id": { - "order": 4, - "name": "container_id", - "description": "Comma-separated list of container IDs to limit the ingestion to.", - "data_type": "string", - "required": false, - "primary": false, - "allow_list": true - } - }, - "output": [] - } - ], - "pip313_dependencies": { - "wheel": [ - { - "module": "annotated-doc", - "input_file": "wheels/shared/annotated_doc-0.0.4-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/annotated_doc-0.0.4-py3-none-any.whl" - }, - { - "module": "annotated-types", - "input_file": "wheels/shared/annotated_types-0.7.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/annotated_types-0.7.0-py3-none-any.whl" - }, - { - "module": "anyio", - "input_file": "wheels/shared/anyio-4.13.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/anyio-4.13.0-py3-none-any.whl" - }, - { - "module": "authlib", - "input_file": "wheels/shared/authlib-1.7.0-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/authlib-1.7.0-py2.py3-none-any.whl" - }, - { - "module": "bleach", - "input_file": "wheels/shared/bleach-6.3.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/bleach-6.3.0-py3-none-any.whl" - }, - { - "module": "build", - "input_file": "wheels/shared/build-1.4.3-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/build-1.4.3-py3-none-any.whl" - }, - { - "module": "cffi", - "input_file": "wheels/python313/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", - "input_file_aarch64": "wheels/python313/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" - }, - { - "module": "click", - "input_file": "wheels/shared/click-8.1.8-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/click-8.1.8-py3-none-any.whl" - }, - { - "module": "colorclass", - "input_file": "wheels/shared/colorclass-2.2.2-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/colorclass-2.2.2-py2.py3-none-any.whl" - }, - { - "module": "compressed-rtf", - "input_file": "wheels/shared/compressed_rtf-1.0.7-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/compressed_rtf-1.0.7-py3-none-any.whl" - }, - { - "module": "cryptography", - "input_file": "wheels/shared/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", - "input_file_aarch64": "wheels/shared/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl" - }, - { - "module": "deprecation", - "input_file": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl" - }, - { - "module": "distro", - "input_file": "wheels/shared/distro-1.9.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/distro-1.9.0-py3-none-any.whl" - }, - { - "module": "easygui", - "input_file": "wheels/shared/easygui-0.98.3-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/easygui-0.98.3-py2.py3-none-any.whl" - }, - { - "module": "ebcdic", - "input_file": "wheels/shared/ebcdic-1.1.1-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/ebcdic-1.1.1-py2.py3-none-any.whl" - }, - { - "module": "extract-msg", - "input_file": "wheels/shared/extract_msg-0.55.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/extract_msg-0.55.0-py3-none-any.whl" - }, - { - "module": "h11", - "input_file": "wheels/shared/h11-0.16.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/h11-0.16.0-py3-none-any.whl" - }, - { - "module": "hatchling", - "input_file": "wheels/shared/hatchling-1.29.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/hatchling-1.29.0-py3-none-any.whl" - }, - { - "module": "httpcore", - "input_file": "wheels/shared/httpcore-1.0.9-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/httpcore-1.0.9-py3-none-any.whl" - }, - { - "module": "httpx", - "input_file": "wheels/shared/httpx-0.28.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/httpx-0.28.1-py3-none-any.whl" - }, - { - "module": "httpx-retries", - "input_file": "wheels/shared/httpx_retries-0.5.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/httpx_retries-0.5.0-py3-none-any.whl" - }, - { - "module": "humanize", - "input_file": "wheels/shared/humanize-4.15.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/humanize-4.15.0-py3-none-any.whl" - }, - { - "module": "jinja2", - "input_file": "wheels/shared/jinja2-3.1.6-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/jinja2-3.1.6-py3-none-any.whl" - }, - { - "module": "joserfc", - "input_file": "wheels/shared/joserfc-1.6.4-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/joserfc-1.6.4-py3-none-any.whl" - }, - { - "module": "lark", - "input_file": "wheels/shared/lark-1.3.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/lark-1.3.1-py3-none-any.whl" - }, - { - "module": "markdown-it-py", - "input_file": "wheels/shared/markdown_it_py-4.0.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/markdown_it_py-4.0.0-py3-none-any.whl" - }, - { - "module": "markupsafe", - "input_file": "wheels/python313/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", - "input_file_aarch64": "wheels/python313/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" - }, - { - "module": "mdurl", - "input_file": "wheels/shared/mdurl-0.1.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/mdurl-0.1.2-py3-none-any.whl" - }, - { - "module": "msoffcrypto-tool", - "input_file": "wheels/shared/msoffcrypto_tool-6.0.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/msoffcrypto_tool-6.0.0-py3-none-any.whl" - }, - { - "module": "olefile", - "input_file": "wheels/shared/olefile-0.47-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/olefile-0.47-py2.py3-none-any.whl" - }, - { - "module": "oletools", - "input_file": "wheels/shared/oletools-0.60.2-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/oletools-0.60.2-py2.py3-none-any.whl" - }, - { - "module": "packaging", - "input_file": "wheels/shared/packaging-26.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/packaging-26.1-py3-none-any.whl" - }, - { - "module": "pathspec", - "input_file": "wheels/shared/pathspec-1.0.4-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pathspec-1.0.4-py3-none-any.whl" - }, - { - "module": "pcodedmp", - "input_file": "wheels/shared/pcodedmp-1.2.6-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pcodedmp-1.2.6-py2.py3-none-any.whl" - }, - { - "module": "pluggy", - "input_file": "wheels/shared/pluggy-1.6.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pluggy-1.6.0-py3-none-any.whl" - }, - { - "module": "pycparser", - "input_file": "wheels/shared/pycparser-3.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pycparser-3.0-py3-none-any.whl" - }, - { - "module": "pydantic", - "input_file": "wheels/shared/pydantic-2.13.3-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pydantic-2.13.3-py3-none-any.whl" - }, - { - "module": "pydantic-core", - "input_file": "wheels/python313/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "input_file_aarch64": "wheels/python313/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "module": "pygments", - "input_file": "wheels/shared/pygments-2.20.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pygments-2.20.0-py3-none-any.whl" - }, - { - "module": "pyjwt", - "input_file": "wheels/shared/pyjwt-2.12.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pyjwt-2.12.1-py3-none-any.whl" - }, - { - "module": "pyparsing", - "input_file": "wheels/shared/pyparsing-3.3.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pyparsing-3.3.2-py3-none-any.whl" - }, - { - "module": "pyproject-hooks", - "input_file": "wheels/shared/pyproject_hooks-1.2.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pyproject_hooks-1.2.0-py3-none-any.whl" - }, - { - "module": "python-dateutil", - "input_file": "wheels/shared/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/python_dateutil-2.9.0.post0-py2.py3-none-any.whl" - }, - { - "module": "red-black-tree-mod", - "input_file": "wheels/shared/red_black_tree_mod-1.22-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/red_black_tree_mod-1.22-py3-none-any.whl" - }, - { - "module": "rich", - "input_file": "wheels/shared/rich-15.0.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/rich-15.0.0-py3-none-any.whl" - }, - { - "module": "rtfde", - "input_file": "wheels/shared/rtfde-0.1.2.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/rtfde-0.1.2.2-py3-none-any.whl" - }, - { - "module": "setuptools", - "input_file": "wheels/shared/setuptools-82.0.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/setuptools-82.0.1-py3-none-any.whl" - }, - { - "module": "shellingham", - "input_file": "wheels/shared/shellingham-1.5.4-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/shellingham-1.5.4-py2.py3-none-any.whl" - }, - { - "module": "splunk-sdk", - "input_file": "wheels/shared/splunk_sdk-2.1.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/splunk_sdk-2.1.1-py3-none-any.whl" - }, - { - "module": "toml", - "input_file": "wheels/shared/toml-0.10.2-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/toml-0.10.2-py2.py3-none-any.whl" - }, - { - "module": "tqdm", - "input_file": "wheels/shared/tqdm-4.67.3-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/tqdm-4.67.3-py3-none-any.whl" - }, - { - "module": "trove-classifiers", - "input_file": "wheels/shared/trove_classifiers-2026.1.14.14-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/trove_classifiers-2026.1.14.14-py3-none-any.whl" - }, - { - "module": "typer", - "input_file": "wheels/shared/typer-0.23.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/typer-0.23.1-py3-none-any.whl" - }, - { - "module": "typing-extensions", - "input_file": "wheels/shared/typing_extensions-4.15.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/typing_extensions-4.15.0-py3-none-any.whl" - }, - { - "module": "typing-inspection", - "input_file": "wheels/shared/typing_inspection-0.4.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/typing_inspection-0.4.2-py3-none-any.whl" - }, - { - "module": "tzlocal", - "input_file": "wheels/shared/tzlocal-5.3.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/tzlocal-5.3.1-py3-none-any.whl" - }, - { - "module": "webencodings", - "input_file": "wheels/shared/webencodings-0.5.1-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/webencodings-0.5.1-py2.py3-none-any.whl" - }, - { - "module": "soar_sdk", - "input_file": "wheels/shared/splunk_soar_sdk-3.19.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/splunk_soar_sdk-3.19.2-py3-none-any.whl" - } - ] - }, - "pip314_dependencies": { - "wheel": [ - { - "module": "annotated-doc", - "input_file": "wheels/shared/annotated_doc-0.0.4-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/annotated_doc-0.0.4-py3-none-any.whl" - }, - { - "module": "annotated-types", - "input_file": "wheels/shared/annotated_types-0.7.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/annotated_types-0.7.0-py3-none-any.whl" - }, - { - "module": "anyio", - "input_file": "wheels/shared/anyio-4.13.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/anyio-4.13.0-py3-none-any.whl" - }, - { - "module": "authlib", - "input_file": "wheels/shared/authlib-1.7.0-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/authlib-1.7.0-py2.py3-none-any.whl" - }, - { - "module": "bleach", - "input_file": "wheels/shared/bleach-6.3.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/bleach-6.3.0-py3-none-any.whl" - }, - { - "module": "build", - "input_file": "wheels/shared/build-1.4.3-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/build-1.4.3-py3-none-any.whl" - }, - { - "module": "cffi", - "input_file": "wheels/python314/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", - "input_file_aarch64": "wheels/python314/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl" - }, - { - "module": "click", - "input_file": "wheels/shared/click-8.1.8-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/click-8.1.8-py3-none-any.whl" - }, - { - "module": "colorclass", - "input_file": "wheels/shared/colorclass-2.2.2-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/colorclass-2.2.2-py2.py3-none-any.whl" - }, - { - "module": "compressed-rtf", - "input_file": "wheels/shared/compressed_rtf-1.0.7-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/compressed_rtf-1.0.7-py3-none-any.whl" - }, - { - "module": "cryptography", - "input_file": "wheels/shared/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", - "input_file_aarch64": "wheels/shared/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl" - }, - { - "module": "deprecation", - "input_file": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl" - }, - { - "module": "distro", - "input_file": "wheels/shared/distro-1.9.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/distro-1.9.0-py3-none-any.whl" - }, - { - "module": "easygui", - "input_file": "wheels/shared/easygui-0.98.3-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/easygui-0.98.3-py2.py3-none-any.whl" - }, - { - "module": "ebcdic", - "input_file": "wheels/shared/ebcdic-1.1.1-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/ebcdic-1.1.1-py2.py3-none-any.whl" - }, - { - "module": "extract-msg", - "input_file": "wheels/shared/extract_msg-0.55.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/extract_msg-0.55.0-py3-none-any.whl" - }, - { - "module": "h11", - "input_file": "wheels/shared/h11-0.16.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/h11-0.16.0-py3-none-any.whl" - }, - { - "module": "hatchling", - "input_file": "wheels/shared/hatchling-1.29.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/hatchling-1.29.0-py3-none-any.whl" - }, - { - "module": "httpcore", - "input_file": "wheels/shared/httpcore-1.0.9-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/httpcore-1.0.9-py3-none-any.whl" - }, - { - "module": "httpx", - "input_file": "wheels/shared/httpx-0.28.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/httpx-0.28.1-py3-none-any.whl" - }, - { - "module": "httpx-retries", - "input_file": "wheels/shared/httpx_retries-0.5.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/httpx_retries-0.5.0-py3-none-any.whl" - }, - { - "module": "humanize", - "input_file": "wheels/shared/humanize-4.15.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/humanize-4.15.0-py3-none-any.whl" - }, - { - "module": "jinja2", - "input_file": "wheels/shared/jinja2-3.1.6-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/jinja2-3.1.6-py3-none-any.whl" - }, - { - "module": "joserfc", - "input_file": "wheels/shared/joserfc-1.6.4-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/joserfc-1.6.4-py3-none-any.whl" - }, - { - "module": "lark", - "input_file": "wheels/shared/lark-1.3.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/lark-1.3.1-py3-none-any.whl" - }, - { - "module": "markdown-it-py", - "input_file": "wheels/shared/markdown_it_py-4.0.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/markdown_it_py-4.0.0-py3-none-any.whl" - }, - { - "module": "markupsafe", - "input_file": "wheels/python314/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", - "input_file_aarch64": "wheels/python314/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" - }, - { - "module": "mdurl", - "input_file": "wheels/shared/mdurl-0.1.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/mdurl-0.1.2-py3-none-any.whl" - }, - { - "module": "msoffcrypto-tool", - "input_file": "wheels/shared/msoffcrypto_tool-6.0.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/msoffcrypto_tool-6.0.0-py3-none-any.whl" - }, - { - "module": "olefile", - "input_file": "wheels/shared/olefile-0.47-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/olefile-0.47-py2.py3-none-any.whl" - }, - { - "module": "oletools", - "input_file": "wheels/shared/oletools-0.60.2-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/oletools-0.60.2-py2.py3-none-any.whl" - }, - { - "module": "packaging", - "input_file": "wheels/shared/packaging-26.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/packaging-26.1-py3-none-any.whl" - }, - { - "module": "pathspec", - "input_file": "wheels/shared/pathspec-1.0.4-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pathspec-1.0.4-py3-none-any.whl" - }, - { - "module": "pcodedmp", - "input_file": "wheels/shared/pcodedmp-1.2.6-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pcodedmp-1.2.6-py2.py3-none-any.whl" - }, - { - "module": "pluggy", - "input_file": "wheels/shared/pluggy-1.6.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pluggy-1.6.0-py3-none-any.whl" - }, - { - "module": "pycparser", - "input_file": "wheels/shared/pycparser-3.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pycparser-3.0-py3-none-any.whl" - }, - { - "module": "pydantic", - "input_file": "wheels/shared/pydantic-2.13.3-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pydantic-2.13.3-py3-none-any.whl" - }, - { - "module": "pydantic-core", - "input_file": "wheels/python314/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", - "input_file_aarch64": "wheels/python314/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "module": "pygments", - "input_file": "wheels/shared/pygments-2.20.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pygments-2.20.0-py3-none-any.whl" - }, - { - "module": "pyjwt", - "input_file": "wheels/shared/pyjwt-2.12.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pyjwt-2.12.1-py3-none-any.whl" - }, - { - "module": "pyparsing", - "input_file": "wheels/shared/pyparsing-3.3.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pyparsing-3.3.2-py3-none-any.whl" - }, - { - "module": "pyproject-hooks", - "input_file": "wheels/shared/pyproject_hooks-1.2.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/pyproject_hooks-1.2.0-py3-none-any.whl" - }, - { - "module": "python-dateutil", - "input_file": "wheels/shared/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/python_dateutil-2.9.0.post0-py2.py3-none-any.whl" - }, - { - "module": "red-black-tree-mod", - "input_file": "wheels/shared/red_black_tree_mod-1.22-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/red_black_tree_mod-1.22-py3-none-any.whl" - }, - { - "module": "rich", - "input_file": "wheels/shared/rich-15.0.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/rich-15.0.0-py3-none-any.whl" - }, - { - "module": "rtfde", - "input_file": "wheels/shared/rtfde-0.1.2.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/rtfde-0.1.2.2-py3-none-any.whl" - }, - { - "module": "setuptools", - "input_file": "wheels/shared/setuptools-82.0.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/setuptools-82.0.1-py3-none-any.whl" - }, - { - "module": "shellingham", - "input_file": "wheels/shared/shellingham-1.5.4-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/shellingham-1.5.4-py2.py3-none-any.whl" - }, - { - "module": "splunk-sdk", - "input_file": "wheels/shared/splunk_sdk-2.1.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/splunk_sdk-2.1.1-py3-none-any.whl" - }, - { - "module": "toml", - "input_file": "wheels/shared/toml-0.10.2-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/toml-0.10.2-py2.py3-none-any.whl" - }, - { - "module": "tqdm", - "input_file": "wheels/shared/tqdm-4.67.3-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/tqdm-4.67.3-py3-none-any.whl" - }, - { - "module": "trove-classifiers", - "input_file": "wheels/shared/trove_classifiers-2026.1.14.14-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/trove_classifiers-2026.1.14.14-py3-none-any.whl" - }, - { - "module": "typer", - "input_file": "wheels/shared/typer-0.23.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/typer-0.23.1-py3-none-any.whl" - }, - { - "module": "typing-extensions", - "input_file": "wheels/shared/typing_extensions-4.15.0-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/typing_extensions-4.15.0-py3-none-any.whl" - }, - { - "module": "typing-inspection", - "input_file": "wheels/shared/typing_inspection-0.4.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/typing_inspection-0.4.2-py3-none-any.whl" - }, - { - "module": "tzlocal", - "input_file": "wheels/shared/tzlocal-5.3.1-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/tzlocal-5.3.1-py3-none-any.whl" - }, - { - "module": "webencodings", - "input_file": "wheels/shared/webencodings-0.5.1-py2.py3-none-any.whl", - "input_file_aarch64": "wheels/shared/webencodings-0.5.1-py2.py3-none-any.whl" - }, - { - "module": "soar_sdk", - "input_file": "wheels/shared/splunk_soar_sdk-3.19.2-py3-none-any.whl", - "input_file_aarch64": "wheels/shared/splunk_soar_sdk-3.19.2-py3-none-any.whl" - } - ] - }, - "supports_es_polling": false -} \ No newline at end of file diff --git a/src/actions/get_host_events.py b/src/actions/get_host_events.py index d747a3a..63929e6 100644 --- a/src/actions/get_host_events.py +++ b/src/actions/get_host_events.py @@ -14,7 +14,9 @@ class GetHostEventsParams(Params): primary=True, cef_types=["ip", "host name"], ) - last_n_days: str = Param(description="Number of days ago", required=False, default="") + last_n_days: str = Param( + description="Number of days ago", required=False, default="" + ) class GetHostEventsOutput(PermissiveActionOutput): @@ -35,13 +37,17 @@ class GetHostEventsSummary(ActionOutput): render_as="table", summary_type=GetHostEventsSummary, ) -def get_host_events(params: GetHostEventsParams, soar: SOARClient, asset: Asset) -> list[GetHostEventsOutput]: +def get_host_events( + params: GetHostEventsParams, soar: SOARClient, asset: Asset +) -> list[GetHostEventsOutput]: helper = SplunkHelper(asset) helper.validate_asset() helper.connect() ip_hostname = params.ip_hostname - last_n_days = SplunkHelper.validate_integer(params.last_n_days, "'last_n_days' action") + last_n_days = SplunkHelper.validate_integer( + params.last_n_days, "'last_n_days' action" + ) search_query = f'search host="{ip_hostname}"' if last_n_days: diff --git a/src/actions/on_poll.py b/src/actions/on_poll.py index cca6294..87b687e 100644 --- a/src/actions/on_poll.py +++ b/src/actions/on_poll.py @@ -3,7 +3,7 @@ import hashlib import json from collections.abc import Iterator -from datetime import UTC, datetime +from datetime import UTC from bs4.dammit import UnicodeDammit from soar_sdk.abstract import SOARClient @@ -23,6 +23,7 @@ def _get_event_start(start_time: str | None) -> str | None: return None try: from dateutil.parser import ParserError, parse as dateutil_parse + datetime_obj = dateutil_parse(start_time) return datetime_obj.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%S.%fZ") except ParserError as e: @@ -35,7 +36,8 @@ def _get_event_start(start_time: str | None) -> str | None: def _get_fips_enabled() -> bool: try: - from phantom_common.install_info import is_fips_enabled # noqa: PLC0415 + from phantom_common.install_info import is_fips_enabled + return is_fips_enabled() except ImportError: return False @@ -81,7 +83,9 @@ def _get_splunk_title(item: dict, prefix: str, name_values: list[str]) -> str: @app.on_poll() -def on_poll(params: OnPollParams, soar: SOARClient, asset: Asset) -> Iterator[Container | Artifact]: +def on_poll( + params: OnPollParams, soar: SOARClient, asset: Asset +) -> Iterator[Container | Artifact]: helper = SplunkHelper(asset) helper.validate_asset() helper.connect() @@ -122,11 +126,16 @@ def on_poll(params: OnPollParams, soar: SOARClient, asset: Asset) -> Iterator[Co search_params.pop("max_count") try: - _sid, results_list = helper.run_query(search_query, kwargs_create=search_params, parse_only=po) + _sid, results_list = helper.run_query( + search_query, kwargs_create=search_params, parse_only=po + ) except Exception as e: msg = str(e) if "Invalid index_earliest" in msg: - logger.debug("Invalid start_time %s, retrying without it", search_params.get("index_earliest")) + logger.debug( + "Invalid start_time %s, retrying without it", + search_params.get("index_earliest"), + ) state.pop("start_time", None) raise @@ -140,7 +149,9 @@ def on_poll(params: OnPollParams, soar: SOARClient, asset: Asset) -> Iterator[Co container_name_prefix = asset.container_name_prefix or "" raw_values = asset.container_name_values - container_name_values = [x.strip() for x in raw_values.split(",")] if raw_values else [] + container_name_values = ( + [x.strip() for x in raw_values.split(",")] if raw_values else [] + ) count = 1 for item in data: @@ -168,8 +179,12 @@ def on_poll(params: OnPollParams, soar: SOARClient, asset: Asset) -> Iterator[Co sdi = item["event_id"] else: if use_event_id_sdi and "event_id" not in item: - logger.warning("use_event_id_sdi enabled but event_id missing, using hash") - input_str = UnicodeDammit(json.dumps(item)).unicode_markup.encode("utf-8") + logger.warning( + "use_event_id_sdi enabled but event_id missing, using hash" + ) + input_str = UnicodeDammit(json.dumps(item)).unicode_markup.encode( + "utf-8" + ) if _get_fips_enabled(): sdi = hashlib.sha256(input_str).hexdigest() else: @@ -177,7 +192,9 @@ def on_poll(params: OnPollParams, soar: SOARClient, asset: Asset) -> Iterator[Co severity = _get_splunk_severity(item) spl_event_start = _get_event_start(item.get("_time")) - container_name = _get_splunk_title(item, container_name_prefix, container_name_values) + container_name = _get_splunk_title( + item, container_name_prefix, container_name_values + ) yield Container( name=container_name, diff --git a/src/actions/post_data.py b/src/actions/post_data.py index 3a93217..928f8f8 100644 --- a/src/actions/post_data.py +++ b/src/actions/post_data.py @@ -22,7 +22,9 @@ class PostDataParams(Params): cef_types=["ip", "host name"], ) index: str = Param(description="Index to send event to", required=False, default="") - source: str = Param(description="Source for event", required=False, default="Phantom") + source: str = Param( + description="Source for event", required=False, default="Phantom" + ) source_type: str = Param( description="Type of source for event", required=False, @@ -41,7 +43,9 @@ class PostDataOutput(ActionOutput): read_only=False, render_as="table", ) -def post_data(params: PostDataParams, soar: SOARClient, asset: Asset) -> list[PostDataOutput]: +def post_data( + params: PostDataParams, soar: SOARClient, asset: Asset +) -> list[PostDataOutput]: helper = SplunkHelper(asset) helper.validate_asset() diff --git a/src/actions/run_query.py b/src/actions/run_query.py index 269ddc3..ec98246 100644 --- a/src/actions/run_query.py +++ b/src/actions/run_query.py @@ -26,19 +26,31 @@ class RunQueryParams(Params): primary=True, cef_types=["splunk query"], ) - display: str = Param(description="Display fields (comma-separated)", required=False, default="") + display: str = Param( + description="Display fields (comma-separated)", required=False, default="" + ) parse_only: bool = Param(description="Parse only", required=False, default=False) - add_raw_field: bool = Param(description="Ingest _raw field data", required=False, default=True) - attach_result: bool = Param(description="Attach result to the vault", required=False, default=False) - start_time: str = Param(description="Earliest time modifier", required=False, default="") - end_time: str = Param(description="Latest time modifier", required=False, default="") + add_raw_field: bool = Param( + description="Ingest _raw field data", required=False, default=True + ) + attach_result: bool = Param( + description="Attach result to the vault", required=False, default=False + ) + start_time: str = Param( + description="Earliest time modifier", required=False, default="" + ) + end_time: str = Param( + description="Latest time modifier", required=False, default="" + ) search_mode: str = Param( description="Search mode", required=False, value_list=["fast", "verbose", "smart"], default="smart", ) - time_format: str = Param(description="Custom timestamp format", required=False, default="") + time_format: str = Param( + description="Custom timestamp format", required=False, default="" + ) class RunQueryOutput(PermissiveActionOutput): @@ -67,7 +79,11 @@ def display_view(outputs: list[RunQueryOutput]) -> dict: all_data = [] for output in outputs: - data = {k: v for k, v in output.model_dump(exclude_none=True).items() if not k.startswith("_")} + data = { + k: v + for k, v in output.model_dump(exclude_none=True).items() + if not k.startswith("_") + } all_data.append(data) if display_fields: @@ -80,12 +96,14 @@ def display_view(outputs: list[RunQueryOutput]) -> dict: processed_data = [{h: item.get(h) for h in headers} for item in all_data] return { - "results": [{ - "param": param, - "data": all_data or {}, - "processed_data": processed_data, - "headers": headers, - }], + "results": [ + { + "param": param, + "data": all_data or {}, + "processed_data": processed_data, + "headers": headers, + } + ], } @@ -96,7 +114,9 @@ def display_view(outputs: list[RunQueryOutput]) -> dict: view_handler=display_view, summary_type=RunQuerySummary, ) -def run_query(params: RunQueryParams, soar: SOARClient, asset: Asset) -> list[RunQueryOutput]: +def run_query( + params: RunQueryParams, soar: SOARClient, asset: Asset +) -> list[RunQueryOutput]: helper = SplunkHelper(asset) helper.validate_asset() helper.connect() diff --git a/src/actions/update_event.py b/src/actions/update_event.py index 97ad4cb..8b5cb90 100644 --- a/src/actions/update_event.py +++ b/src/actions/update_event.py @@ -27,34 +27,60 @@ class UpdateEventParams(Params): primary=True, cef_types=["splunk notable event id"], ) - owner: str = Param(description="New owner for the event", required=False, default="") + owner: str = Param( + description="New owner for the event", required=False, default="" + ) status: str = Param( description="New status for the event", required=False, default="", - value_list=["", "unassigned", "new", "in progress", "pending", "resolved", "closed"], + value_list=[ + "", + "unassigned", + "new", + "in progress", + "pending", + "resolved", + "closed", + ], + ) + integer_status: str = Param( + description="Integer representing custom status value", + required=False, + default="", ) - integer_status: str = Param(description="Integer representing custom status value", required=False, default="") urgency: str = Param( description="New urgency for the event", required=False, default="", value_list=["", "informational", "low", "medium", "high", "critical"], ) - comment: str = Param(description="New comment for the event", required=False, default="") + comment: str = Param( + description="New comment for the event", required=False, default="" + ) disposition: str = Param( description="New disposition field", required=False, default="", value_list=[ - "", "Unassigned", "True Positive - Suspicious Activity", + "", + "Unassigned", + "True Positive - Suspicious Activity", "Benign Positive - Suspicious But Expected", "False Positive - Incorrect Analytic Logic", - "False Positive - Inaccurate Data", "Undetermined", "Other", + "False Positive - Inaccurate Data", + "Undetermined", + "Other", ], ) - integer_disposition: str = Param(description="Integer representing custom disposition value", required=False, default="") - wait_for_confirmation: bool = Param(description="Validate event_ids", required=False, default=False) + integer_disposition: str = Param( + description="Integer representing custom disposition value", + required=False, + default="", + ) + wait_for_confirmation: bool = Param( + description="Validate event_ids", required=False, default=False + ) class UpdateEventOutput(ActionOutput): @@ -77,7 +103,9 @@ class UpdateEventSummary(ActionOutput): render_as="table", summary_type=UpdateEventSummary, ) -def update_event(params: UpdateEventParams, soar: SOARClient, asset: Asset) -> list[UpdateEventOutput]: +def update_event( + params: UpdateEventParams, soar: SOARClient, asset: Asset +) -> list[UpdateEventOutput]: helper = SplunkHelper(asset) helper.validate_asset() @@ -99,7 +127,11 @@ def update_event(params: UpdateEventParams, soar: SOARClient, asset: Asset) -> l params.integer_disposition, "'integer_disposition' action", allow_zero=True ) - if not any([comment, status, urgency, owner, disposition]) and integer_status is None and integer_disposition is None: + if ( + not any([comment, status, urgency, owner, disposition]) + and integer_status is None + and integer_disposition is None + ): raise ValueError(SPLUNK_ERR_NEED_PARAM) splunk_status_dict: dict[str, int] = {} @@ -124,7 +156,9 @@ def update_event(params: UpdateEventParams, soar: SOARClient, asset: Asset) -> l try: ids = helper.resolve_event_id(ids) except Exception: - raise RuntimeError("Unable to find underlying event_id from SID + RID combo") from None + raise RuntimeError( + "Unable to find underlying event_id from SID + RID combo" + ) from None if wait_for_confirmation: search_query = f"search `notable_by_id({ids})`" @@ -157,14 +191,20 @@ def update_event(params: UpdateEventParams, soar: SOARClient, asset: Asset) -> l "Please provide a valid value in 'integer_disposition' action parameter. " f"Valid values: {', '.join(map(str, splunk_disposition_dict.values()))}" ) - request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(integer_disposition) + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format( + integer_disposition + ) elif disposition: if disposition not in splunk_disposition_dict: if not disposition.isdigit(): raise ValueError(SPLUNK_ERR_BAD_DISPOSITION) - request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(disposition) + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format( + disposition + ) else: - request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format(splunk_disposition_dict[disposition]) + request_body["disposition"] = SPLUNK_DISPOSITION_QUERY_FORMAT.format( + splunk_disposition_dict[disposition] + ) param_mapping = {"urgency": urgency, "comment": comment, "newOwner": owner} request_body.update({k: v for k, v in param_mapping.items() if v}) diff --git a/src/app.py b/src/app.py index fba68fa..574195f 100644 --- a/src/app.py +++ b/src/app.py @@ -264,18 +264,31 @@ def validate_integer(value, name: str, allow_zero: bool = False) -> int | None: return value def validate_asset(self): - if not self.asset.api_token and (not self.asset.username or not self.asset.password): + if not self.asset.api_token and ( + not self.asset.username or not self.asset.password + ): raise ValueError(SPLUNK_ERR_REQUIRED_CONFIG_PARAMS) self.validate_integer(self.asset.retry_count, "'retry_count' configuration") self.validate_integer(self.asset.port, "'port' configuration") - self.validate_integer(self.asset.max_container, "'max_container' configuration", allow_zero=True) - self.validate_integer(self.asset.container_update_state, "'Container count to update the state file' configuration") - self.validate_integer(self.asset.splunk_job_timeout, "'splunk_job_timeout' configuration") - self.validate_integer(self.asset.sleeptime_in_requests, "'sleeptime_in_requests' configuration") + self.validate_integer( + self.asset.max_container, "'max_container' configuration", allow_zero=True + ) + self.validate_integer( + self.asset.container_update_state, + "'Container count to update the state file' configuration", + ) + self.validate_integer( + self.asset.splunk_job_timeout, "'splunk_job_timeout' configuration" + ) + self.validate_integer( + self.asset.sleeptime_in_requests, "'sleeptime_in_requests' configuration" + ) if self.asset.sleeptime_in_requests > 120: - raise ValueError(SPLUNK_ERR_INVALID_SLEEP_TIME.format(param="'sleeptime_in_requests'")) + raise ValueError( + SPLUNK_ERR_INVALID_SLEEP_TIME.format(param="'sleeptime_in_requests'") + ) # -- proxy handler for splunklib ---------------------------------------- def _proxy_request(self, url, message, **kwargs): @@ -344,11 +357,17 @@ def connect(self): except splunk_binding.HTTPError as e: error_text = str(e) if "405 Method Not Allowed" in error_text: - raise ConnectionError("Error occurred while connecting to the Splunk server") from e - raise ConnectionError(f"Error occurred while connecting to the Splunk server. Details: {error_text}") from e + raise ConnectionError( + "Error occurred while connecting to the Splunk server" + ) from e + raise ConnectionError( + f"Error occurred while connecting to the Splunk server. Details: {error_text}" + ) from e except Exception as e: raise ConnectionError( - SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e) + SPLUNK_EXCEPTION_ERR_MESSAGE.format( + msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e + ) ) from e @property @@ -358,7 +377,9 @@ def service(self) -> splunk_client.Service: return self._service # -- REST calls ---------------------------------------------------------- - def make_rest_call(self, endpoint: str, data, params: dict | None = None, method=requests.post) -> dict: + def make_rest_call( + self, endpoint: str, data, params: dict | None = None, method=requests.post + ) -> dict: url = f"{self._base_url}services/{endpoint}" logger.debug("Making REST call to %s", url) @@ -380,12 +401,16 @@ def make_rest_call(self, endpoint: str, data, params: dict | None = None, method ) except Exception as e: raise ConnectionError( - SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e) + SPLUNK_EXCEPTION_ERR_MESSAGE.format( + msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e + ) ) from e return self._process_response(r) - def make_rest_call_retry(self, endpoint: str, data, params: dict | None = None, method=requests.post) -> dict: + def make_rest_call_retry( + self, endpoint: str, data, params: dict | None = None, method=requests.post + ) -> dict: last_err = None for _ in range(self.asset.retry_count): try: @@ -427,13 +452,29 @@ def _process_xml_response(r: requests.Response) -> dict: if 200 <= r.status_code < 400: return resp_json or {} - error_type = resp_json.get("response", {}).get("messages", {}).get("msg", {}).get("@type") if resp_json else None - error_message = resp_json.get("response", {}).get("messages", {}).get("msg", {}).get("#text") if resp_json else None + error_type = ( + resp_json.get("response", {}) + .get("messages", {}) + .get("msg", {}) + .get("@type") + if resp_json + else None + ) + error_message = ( + resp_json.get("response", {}) + .get("messages", {}) + .get("msg", {}) + .get("#text") + if resp_json + else None + ) if error_type or error_message: error = f"ErrorType: {error_type} ErrorMessage: {error_message}" else: error = "Unable to parse xml response" - raise RuntimeError(f"Error from server. Status Code: {r.status_code} Data from server: {error}") + raise RuntimeError( + f"Error from server. Status Code: {r.status_code} Data from server: {error}" + ) @staticmethod def _process_html_response(r: requests.Response) -> dict: @@ -441,7 +482,9 @@ def _process_html_response(r: requests.Response) -> dict: soup = BeautifulSoup(r.text, "html.parser") for element in soup(["script", "style", "footer", "nav"]): element.extract() - error_text = "\n".join(line.strip() for line in soup.text.split("\n") if line.strip()) + error_text = "\n".join( + line.strip() for line in soup.text.split("\n") if line.strip() + ) except Exception as e: error_text = SPLUNK_ERR_UNABLE_TO_PARSE_HTML_RESPONSE.format(error=e) @@ -458,7 +501,9 @@ def _process_json_response(r: requests.Response) -> dict: try: resp_json = r.json() except Exception as e: - raise RuntimeError(SPLUNK_ERR_UNABLE_TO_PARSE_JSON_RESPONSE.format(error=e)) from e + raise RuntimeError( + SPLUNK_ERR_UNABLE_TO_PARSE_JSON_RESPONSE.format(error=e) + ) from e if 200 <= r.status_code < 399: return resp_json @@ -474,10 +519,14 @@ def _process_json_response(r: requests.Response) -> dict: if resp_json.get("messages") and resp_json["messages"]: msg = resp_json["messages"][0] error = f"ErrorType: {msg.get('type')} ErrorMessage: {msg.get('text')}" - raise RuntimeError(f"Error from server. Status Code: {r.status_code} Data from server: {error}") + raise RuntimeError( + f"Error from server. Status Code: {r.status_code} Data from server: {error}" + ) error_text = r.text.replace("{", "{{").replace("}", "}}") - raise RuntimeError(f"Error from server. Status Code: {r.status_code}. Data from server: {error_text}") + raise RuntimeError( + f"Error from server. Status Code: {r.status_code}. Data from server: {error_text}" + ) # -- server info --------------------------------------------------------- def get_server_version(self) -> str: @@ -508,7 +557,9 @@ def create_job(self, search_query: str, kwargs_create: dict) -> splunk_client.Jo logger.debug("Attempt %d to create splunk job failed: %s", attempt, e) last_err = e raise RuntimeError( - SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_UNABLE_TO_CREATE_JOB, error_text=last_err) + SPLUNK_EXCEPTION_ERR_MESSAGE.format( + msg=SPLUNK_ERR_UNABLE_TO_CREATE_JOB, error_text=last_err + ) ) def wait_for_job(self, job: splunk_client.Job): @@ -528,16 +579,26 @@ def wait_for_job(self, job: splunk_client.Job): logger.debug("Attempt %d to wait for job failed: %s", attempt, e) last_err = e raise RuntimeError( - SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=last_err) + SPLUNK_EXCEPTION_ERR_MESSAGE.format( + msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=last_err + ) ) def get_job_stats(self, job) -> dict: return { - "is_done": job["isDone"] if "isDone" in job else "Unknown status", - "progress": float(job["doneProgress"]) * 100 if "doneProgress" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Done progress"), - "scan_count": int(job["scanCount"]) if "scanCount" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Scan count"), - "event_count": int(job["eventCount"]) if "eventCount" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Event count"), - "result_count": int(job["resultCount"]) if "resultCount" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Result count"), + "is_done": job.get("isDone", "Unknown status"), + "progress": float(job["doneProgress"]) * 100 + if "doneProgress" in job + else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Done progress"), + "scan_count": int(job["scanCount"]) + if "scanCount" in job + else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Scan count"), + "event_count": int(job["eventCount"]) + if "eventCount" in job + else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Event count"), + "result_count": int(job["resultCount"]) + if "resultCount" in job + else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Result count"), } def validate_query(self, search_query: str, parse_only: bool = True): @@ -550,17 +611,27 @@ def validate_query(self, search_query: str, parse_only: bool = True): self.connect() if attempt == self.asset.retry_count - 1: raise ValueError( - SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=f"Query invalid '{search_query}'", error_text=e) + SPLUNK_EXCEPTION_ERR_MESSAGE.format( + msg=f"Query invalid '{search_query}'", error_text=e + ) ) from e except Exception as e: self._service = None self.connect() if attempt == self.asset.retry_count - 1: raise RuntimeError( - SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e) + SPLUNK_EXCEPTION_ERR_MESSAGE.format( + msg=SPLUNK_ERR_CONNECTIVITY_FAILED, error_text=e + ) ) from e - def run_query(self, search_query: str, kwargs_create: dict | None = None, parse_only: bool = True, add_raw_field: bool = True) -> tuple[str, list[dict]]: + def run_query( + self, + search_query: str, + kwargs_create: dict | None = None, + parse_only: bool = True, + add_raw_field: bool = True, + ) -> tuple[str, list[dict]]: if kwargs_create is None: kwargs_create = {} @@ -587,7 +658,9 @@ def run_query(self, search_query: str, kwargs_create: dict | None = None, parse_ ) except Exception as e: raise RuntimeError( - SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg="Error retrieving results", error_text=e) + SPLUNK_EXCEPTION_ERR_MESSAGE.format( + msg="Error retrieving results", error_text=e + ) ) from e for result in results: @@ -612,7 +685,9 @@ def get_status_dict(self, status_type: str) -> dict[str, int]: splunk_dict: dict[str, int] = {} try: resp = self.make_rest_call_retry( - "alerts/reviewstatuses?count=-1&output_mode=json", {}, method=requests.get + "alerts/reviewstatuses?count=-1&output_mode=json", + {}, + method=requests.get, ) except Exception: return splunk_dict @@ -653,7 +728,9 @@ def test_connectivity(soar: SOARClient, asset: Asset) -> None: raise RuntimeError(SPLUNK_ERR_CONNECTIVITY_TEST) is_es = helper.check_for_es() - logger.progress("Detected Splunk %sserver version %s", "ES " if is_es else "", version) + logger.progress( + "Detected Splunk %sserver version %s", "ES " if is_es else "", version + ) soar.set_message(SPLUNK_SUCCESS_CONNECTIVITY_TEST) logger.info(SPLUNK_SUCCESS_CONNECTIVITY_TEST) diff --git a/src/splunk_consts.py b/src/splunk_consts.py index ff4f790..4364b6f 100644 --- a/src/splunk_consts.py +++ b/src/splunk_consts.py @@ -19,27 +19,41 @@ SPLUNK_SUCCESS_CONNECTIVITY_TEST = "Connectivity test passed" SPLUNK_ERR_CONNECTIVITY_FAILED = "Failed to connect to splunk server" SPLUNK_ERR_UNABLE_TO_CREATE_JOB = "Failed to get a job id from splunk server" -SPLUNK_ERR_NOT_ES = "This instance does not seem to be Splunk ES. This action cannot be run" -SPLUNK_ERR_INVALID_TIME_RANGE = "Invalid Time range specified, where the end time is less than start time" +SPLUNK_ERR_NOT_ES = ( + "This instance does not seem to be Splunk ES. This action cannot be run" +) +SPLUNK_ERR_INVALID_TIME_RANGE = ( + "Invalid Time range specified, where the end time is less than start time" +) SPLUNK_ERR_NEED_PARAM = ( "One of comment, status, integer_status, disposition, integer_disposition, " "urgency, or owner parameters needs to be supplied to run this action" ) -SPLUNK_ERR_INVALID_INTEGER = "Please provide a valid integer value in the {param} parameter" -SPLUNK_ERR_NON_NEGATIVE_INTEGER = "Please provide a valid non-negative integer value in the {param} parameter" +SPLUNK_ERR_INVALID_INTEGER = ( + "Please provide a valid integer value in the {param} parameter" +) +SPLUNK_ERR_NON_NEGATIVE_INTEGER = ( + "Please provide a valid non-negative integer value in the {param} parameter" +) SPLUNK_ERR_INVALID_PARAM = "Please provide non-zero positive integer in {param}" SPLUNK_ERR_MESSAGE_UNAVAILABLE = "Error message unavailable. Please check the asset configuration and|or action parameters." SPLUNK_EXCEPTION_ERR_MESSAGE = "{msg}. {error_text}" SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE = "{field} not found" -SPLUNK_ERR_INVALID_SLEEP_TIME = "Please provide a value <= 120 seconds in the {param} parameter" +SPLUNK_ERR_INVALID_SLEEP_TIME = ( + "Please provide a value <= 120 seconds in the {param} parameter" +) SPLUNK_ERR_REQUIRED_CONFIG_PARAMS = ( "Please provide either API token or username and password in the asset " "configuration parameters for authentication" ) -SPLUNK_ERR_SPLUNK_JOB_HAS_TIMED_OUT = "Failed to retrieve splunk job results. The splunk job has timed out." +SPLUNK_ERR_SPLUNK_JOB_HAS_TIMED_OUT = ( + "Failed to retrieve splunk job results. The splunk job has timed out." +) SPLUNK_ERR_UNABLE_TO_PARSE_JSON_RESPONSE = "Unable to parse response as JSON. {error}" SPLUNK_ERR_UNABLE_TO_PARSE_HTML_RESPONSE = "Unable to parse HTML response. {error}" -SPLUNK_ERR_EMPTY_RESPONSE = "Status Code {code}. Empty response and no information in the header." +SPLUNK_ERR_EMPTY_RESPONSE = ( + "Status Code {code}. Empty response and no information in the header." +) # Progress messages SPLUNK_PROG_CREATED_QUERY = "Created query '{query}'" @@ -108,8 +122,12 @@ # Queries SPLUNK_RID_SID_NOTABLE_QUERY = r'search [| makeresults | eval myfield = "{}"' -SPLUNK_RID_SID_NOTABLE_QUERY += r' | rex field=myfield "^(?.*)\+(?\d*(\.\d+)?)"' -SPLUNK_RID_SID_NOTABLE_QUERY += r' | eval search = "( (sid::" . sid . " OR orig_sid::" . sid . ")' +SPLUNK_RID_SID_NOTABLE_QUERY += ( + r' | rex field=myfield "^(?.*)\+(?\d*(\.\d+)?)"' +) +SPLUNK_RID_SID_NOTABLE_QUERY += ( + r' | eval search = "( (sid::" . sid . " OR orig_sid::" . sid . ")' +) SPLUNK_RID_SID_NOTABLE_QUERY += r' (rid::" . rid . " OR orig_rid::" . rid . ") )"' SPLUNK_RID_SID_NOTABLE_QUERY += r" | table search] `notable` | table event_id" From 091b2cbe798d786f45cda78549f0f19337394b2a Mon Sep 17 00:00:00 2001 From: grokas Date: Mon, 27 Apr 2026 15:54:57 -0700 Subject: [PATCH 07/10] fix: test issue --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 574195f..b231cfc 100644 --- a/src/app.py +++ b/src/app.py @@ -586,7 +586,7 @@ def wait_for_job(self, job: splunk_client.Job): def get_job_stats(self, job) -> dict: return { - "is_done": job.get("isDone", "Unknown status"), + "is_done": job["isDone"] if "isDone" in job else "Unknown status", # noqa: SIM401 - job is a splunklib Entity, not a dict "progress": float(job["doneProgress"]) * 100 if "doneProgress" in job else SPLUNK_JOB_FIELD_NOT_FOUND_MESSAGE.format(field="Done progress"), From 23d4bdab289471b5afbcfd95ef7753c53d8862df Mon Sep 17 00:00:00 2001 From: grokas Date: Tue, 28 Apr 2026 10:55:45 -0700 Subject: [PATCH 08/10] feat: add universal api call make request action --- README.md | 40 +++++++++++++ src/actions/__init__.py | 2 +- src/actions/make_request.py | 112 ++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/actions/make_request.py diff --git a/README.md b/README.md index 4a02a7e..b7e221f 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ VARIABLE | REQUIRED | TYPE | DESCRIPTION [test connectivity](#action-test-connectivity) - test connectivity
[get host events](#action-get-host-events) - Get events pertaining to a host that have occurred in the last 'N' days
+[make request](#action-make-request) - make request
[on poll](#action-on-poll) - on poll
[post data](#action-post-data) - Post data to Splunk
[run query](#action-run-query) - Run a search query on the Splunk device. Please escape any quotes that are part of the query string
@@ -99,6 +100,45 @@ action_result.summary.total_events | numeric | | | summary.total_objects | numeric | | 1 | summary.total_objects_successful | numeric | | 1 | +## action: 'make request' + +make request + +Type: **generic**
+Read only: **False** + +'make request' action for the app. Used to handle arbitrary HTTP requests with the app's asset + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**http_method** | required | The HTTP method to use for the request. | string | | +**endpoint** | required | Splunk REST API endpoint to call, appended to https://:/. Example: 'services/search/jobs' | string | | +**headers** | optional | The headers to send with the request (JSON object). An example is {'Content-Type': 'application/json'} | string | | +**query_parameters** | optional | Parameters to append to the URL (JSON object or query string). An example is ?key=value&key2=value2 | string | | +**body** | optional | The body to send with the request (JSON object). An example is {'key': 'value', 'key2': 'value2'} | string | | +**timeout** | optional | The timeout for the request in seconds. | numeric | | +**verify_ssl** | optional | Whether to verify the SSL certificate. Defaults to the asset's 'Verify Server Certificate' setting. | boolean | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failure | +action_result.message | string | | | +action_result.parameter.http_method | string | | | +action_result.parameter.endpoint | string | | | +action_result.parameter.headers | string | | | +action_result.parameter.query_parameters | string | | | +action_result.parameter.body | string | | | +action_result.parameter.timeout | numeric | | | +action_result.parameter.verify_ssl | boolean | | | +action_result.data.\*.status_code | numeric | | 200 | +action_result.data.\*.response_body | string | | {} | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + ## action: 'on poll' on poll diff --git a/src/actions/__init__.py b/src/actions/__init__.py index 49bb430..67f88b8 100644 --- a/src/actions/__init__.py +++ b/src/actions/__init__.py @@ -11,4 +11,4 @@ # either express or implied. See the License for the specific language governing permissions # and limitations under the License. -from . import get_host_events, on_poll, post_data, run_query, update_event # noqa: F401 +from . import get_host_events, make_request, on_poll, post_data, run_query, update_event # noqa: F401 diff --git a/src/actions/make_request.py b/src/actions/make_request.py new file mode 100644 index 0000000..d968ff8 --- /dev/null +++ b/src/actions/make_request.py @@ -0,0 +1,112 @@ +# Copyright (c) 2016-2026 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 requests +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 MakeRequestParams, Param + +from ..app import Asset, app + +logger = getLogger() + + +class SplunkMakeRequestParams(MakeRequestParams): + endpoint: str = Param( + description=( + "Splunk REST API endpoint to call, appended to https://:/. " + "Example: 'services/search/jobs'" + ), + required=True, + ) + verify_ssl: bool | None = Param( + description="Whether to verify the SSL certificate. Defaults to the asset's 'Verify Server Certificate' setting.", + required=False, + default=None, + ) + + +class SplunkMakeRequestOutput(ActionOutput): + status_code: int = OutputField(example_values=[200]) + response_body: str = OutputField(example_values=["{}"]) + + @classmethod + def from_response(cls, response: requests.Response) -> "SplunkMakeRequestOutput": + return cls(status_code=response.status_code, response_body=response.text) + + +@app.make_request() +def http_action( + params: SplunkMakeRequestParams, asset: Asset +) -> SplunkMakeRequestOutput: + if params.endpoint.startswith(("http://", "https://")): + raise ActionFailure( + f"Invalid endpoint: {params.endpoint}. Do not include the base URL — " + "it is derived from the asset configuration." + ) + + base_url = f"https://{asset.device}:{asset.port}/" + endpoint = params.endpoint.lstrip("/") + url = f"{base_url}{endpoint}" + + auth = None + headers: dict = {} + if asset.api_token: + headers["Authorization"] = f"Bearer {asset.api_token}" + else: + auth = (asset.username, asset.password) + + if params.headers: + try: + headers.update(json.loads(params.headers)) + except (json.JSONDecodeError, TypeError) as e: + raise ActionFailure(f"Invalid JSON headers: {params.headers}") from e + + query_params = None + if params.query_parameters: + try: + query_params = json.loads(params.query_parameters) + except (json.JSONDecodeError, TypeError): + query_string = params.query_parameters.lstrip("?") + url = f"{url}?{query_string}" if "?" not in url else f"{url}&{query_string}" + + body = None + if params.body: + try: + body = json.loads(params.body) + except (json.JSONDecodeError, TypeError) as e: + raise ActionFailure(f"Invalid JSON body: {params.body}") from e + + timeout = params.timeout or None + verify = ( + params.verify_ssl if params.verify_ssl is not None else asset.verify_server_cert + ) + + try: + response = requests.request( + method=params.http_method, + url=url, + auth=auth, + headers=headers or None, + params=query_params, + json=body, + timeout=timeout, + verify=verify, + ) + except Exception as e: + raise ActionFailure(f"Request failed: {e}") from e + + return SplunkMakeRequestOutput.from_response(response) From 8f9a73d52c4dd3d3b6651a9a6951de1299afdaea Mon Sep 17 00:00:00 2001 From: grokas Date: Tue, 28 Apr 2026 15:26:44 -0700 Subject: [PATCH 09/10] chore: docs fixing --- .github/workflows/call-publish.yml | 15 ++ README.md | 305 +++++++++++++++++++++++++++++ manual_readme_content.md | 304 ++++++++++++++++++++++++++++ 3 files changed, 624 insertions(+) create mode 100644 .github/workflows/call-publish.yml create mode 100644 manual_readme_content.md diff --git a/.github/workflows/call-publish.yml b/.github/workflows/call-publish.yml new file mode 100644 index 0000000..307d855 --- /dev/null +++ b/.github/workflows/call-publish.yml @@ -0,0 +1,15 @@ +name: Call Publish Workflow + +on: + push: + branches: + - main # Runs only on push (merge) to main + +jobs: + call-publish: + uses: splunk-soar-connectors/.github/.github/workflows/publish.yml@main + secrets: + release_queue_url: ${{ secrets.RELEASE_QUEUE_URL }} + splunkbase_user: ${{ secrets.SPLUNKBASE_USER }} + splunkbase_password: ${{ secrets.SPLUNKBASE_PASSWORD }} + semantic_release_pk: ${{ secrets.SEMANTIC_RELEASE_PK }} diff --git a/README.md b/README.md index b7e221f..ffaeb76 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,311 @@ Minimum Product Version: 7.0.0 This app integrates with Splunk to update data on the device, in addition to investigate and ingestion actions +## App's Token-Based Authentication Workflow + +- This app also supports API token based authentication. + +- Please follow the steps mentioned in this + [documentation](https://docs.splunk.com/Documentation/Splunk/9.0.0/Security/CreateAuthTokens) to + generate an API token. + + **NOTE -** If the username/password and API token are both provided then the API token will be + given preference and a token-based authentication workflow will be used. + +## Splunk-SDK + +This app uses the Splunk-SDK module, which is licensed under the Apache Software License, Copyright +(c) 2011-2024 Splunk, Inc. + +## State File Permissions + +Please check the permissions for the state file as mentioned below. + +#### State Filepath + +- For Non-NRI Instance: + /opt/phantom/local_data/app_states/91883aa8-9c81-470b-97a1-5d8f7995f560/{asset_id}\_state.json +- For NRI Instance: + /phantomcyber/local_data/app_states/91883aa8-9c81-470b-97a1-5d8f7995f560/{asset_id}\_state.json + +#### State File Permissions + +- File Rights: rw-rw-r-- (664) (The Splunk SOAR user should have read and write access for the state + file) +- File Owner: appropriate Splunk SOAR user + +## Required Permissions for Post Data Action + +The endpoint used by the post data action is not supported on Splunk Cloud Platform. Hence, the following steps are not applicable for Splunk Cloud Platform. + +For sending events to Splunk Platform, the User configured in the asset would require **edit_tcp** capability. Follow the below steps to configure + +- Login to the Splunk Platform +- Go to **Setting > Roles** +- Click on role of the user configured in the asset(example: user) and go to **Capabilities** +- Search for '**edit_tcp**' in the capabilities enable it for the particular role +- To check if the capability is given to your user, go to **Settings > Users** and in the **Edit dropdown** and select **View Capabilities** +- Search for '**edit_tcp**' and if a tick besides it appears then the permission has been enabled for the user + +## Asset Configuration Parameters + +- container_name_prefix: + - Name to give containers created via ingestion + + - User can select a field name from the events data + + - If the provided field exists, then container_name_prefix will be the value against the + provided field from the events data + - If the provided field does not exist, then container_name_prefix will be the provided + field name itself + + - If the container_name_prefix parameter is not provided: + + - If the event data contains '\_time' field, then container_name_prefix will be 'Splunk + Log Entry on \' + - If the event data does not contain '\_time' field, then container_name_prefix will be + 'Splunk Log Entry' + + - Users can provide a string. Example: Test title +- container_name_values: + - Values to append to the container name created via ingestion + + - User can provide CIM fields + + - If the container_name_values parameter is provided: + + - If the provided field exists, then container_name_values will be the value against the + provided CIM field or its CIM field mapping from the events data + - If neither a CIM field mapping nor CIM field itself is present in the event data, then + container_name_values will be the CIM field mapping or CIM field + + - If the container_name_values parameter is not provided: + + - If 'container_name_prefix' parameter is not provided, then container_name_values will be + 'source' + - If 'container_name_prefix' parameter is provided, then container_name_values will be + empty + + - Users can provide a comma-separated string. Example: test1, test2 +- Container count to update the state file: + - This parameter will allow the user to specify the number of containers and will only be used + in scheduled or interval polling + - Everytime the count of the containers reaches the count provided by the user, the + "start_time" stored in the state file will be updated by the index time of that event + - The default value is 100 +- splunk_app: + - The app context of the namespace + - As per Splunk SDK's documentation, if the splunk_app parameter is not provided, then + "system" will be considered as splunk_app +- splunk_owner: + - The owner context of the namespace + - As per Splunk SDK's documentation, if the splunk_owner parameter is not provided, then + "nobody" will be considered as splunk_owner +- retry_count: + - Number of retries + - To ask a query to the Splunk server using the splunklib library, first, the query asked by + the user is to be parsed. Then, this parsed query is used to create a job and once this job + is ready the results are ready to be fetched. So while performing any of the above steps, if + any exception occurs then, the code will retry that step for the number of retries provided + in the "retry count" configuration parameter. + - It will also be used if an error or an exception occurs while posting the data in the "post + data" action or modifying the event in the "update event" action. +- remove_empty_cef: + - Remove CEF fields having empty values from the artifact + - It allows the user to remove CEF fields having empty values from the artifact during + ingestion. If the value of the parameter is 'true', CEF fields having empty values will be + removed. +- sleeptime_in_requests: + - The time to wait for next REST call(max 120 seconds) + - It allows the user to add sleep time between the REST calls while performing the + "run_query", "update_event", "get host events" and "on poll" action. +- splunk_job_timeout: + - The duration in seconds to wait before a scheduled Splunk job times out + - It allows the user to configure the duration after which the connector should consider the Splunk job as timed out. +- on_poll_display: + - Fields to save with On Poll + - Users can select the fields from the events which the user wants to ingest in the artifact + - If the on_poll_display parameter is not provided, then all the fields that are extracted + from the events will be ingested in the respective artifacts + - Users can provide comma-separated field names. Example: field1, field2, field3 +- If the on_poll_query(query to use with On Poll) parameter is not provided, then an error message + will be returned +- If the on_poll_command(command for the query to use with On Poll) parameter is not provided and + the on_poll_query does not start with "|" or "search", then the "search" keyword is added at + the beginning of the on_poll_query + Example: + - on_poll_command: None + on_poll_query: index = "main" + Final query generated internally: search index = "main" +- If the on_poll_command parameter is not provided and the on_poll_query starts with "|" or + "search", then the final query would be the same as the query provided in the on_poll_query + parameter + Example: + - on_poll_command: None + on_poll_query: search index = "main" + Final query generated internally: search index = "main" +- If on_poll_command parameter is provided, then query is formed as: {on_poll_command} + {on_poll_query} + Example: + - on_poll_command: search + on_poll_query: index = "main" + Final query generated internally: search index = "main" + +## Update Event + +- To execute this action successfully, the minimum role required is "ess_analyst", but the user + can have other roles too. + +- If the **wait_for_confirmation** parameter is False (which is the default), it will be faster + but there will be no confirmation that the notable ID corresponded with an actual notable event. + Setting it to True will cause the action to take longer because it will require an SPL search, + but it will provide more assurance that the update took place. + +- The action updates the event for the provided "event_id". If the **wait_for_confirmation** + parameter is True, the action validates the "event_id" provided by the user using the search + command: 'search \`notable\` | search event_id="\"'. + + - If this search command returns more than 0 results, the action updates the event. + - If this search command does not return any results then, the action fails with the message + "Please provide a valid event ID". + +- Use the integer status field to set custom status values (e.g., 1 for 'New', 2 for 'In Progress', etc.). Similarly, use the integer disposition field for custom disposition values (e.g., 0 for 'Undetermined'). + +## Make Request + +- This action allows executing arbitrary Splunk REST API calls using the asset's configured + credentials and connection settings. + +- The **endpoint** parameter is appended to the base URL derived from the asset's device and port + (e.g., `https://:/`). Do not include the base URL in the endpoint parameter. + Example: `services/search/jobs`, `services/server/info` + +- The **verify_ssl** parameter defaults to the asset's **Verify Server Certificate** setting if + not explicitly provided. + +- Authentication uses the asset's API token (Bearer) or username/password, consistent with all + other actions in this app. + +## On Poll + +- There are two approaches to polling as mentioned below. + + - POLL NOW (Manual polling) + + - It will fetch the data every time as per the corresponding asset configuration + parameters. It doesn't store the last run context of the fetched data. + + - Scheduled/Interval Polling + + - The ingestion action will be triggered after each specified time interval. It stores the + last run context of the fetched data and starts fetching new data based on the + combination of the values of stored context for the previous ingestion run and the + corresponding asset configuration parameters. + +- Notes + + - In case "on poll" returns any 4XX except 403, validate your search Query on Splunk + - Sample "Query" to use with On Poll: index="\_internal" | stats count by host, source, + sourcetype | head 5 | rename host as h0st | rename source as devicehostname + - Sample "Fields to save with On Poll" (if not provided, "on poll" will store all the fields): + source,sourcetype,hostname + - For the **on_poll_parse_only** parameter, if **True** , disables the expansion of search due + to evaluation of sub-searches, time term expansion, lookups, tags, eventtypes, and + sourcetype aliases. This parameter is used for the validation of the Splunk query before + fetching the results + - If multiple severities are returned for the incident in the "on poll" action, then the + highest "severity" will be given priority. If the "severity" is not present in the incident, + then the "urgency" of the incident will be considered. If the "urgency" is also not present, + then the ingested container "severity" will be taken as "medium" by default. + +- Helpful examples to run on poll + + 1. The query will fetch top 10 events from the result of index = "main" search. + - on_poll_command: "search" + - on_poll_query: index = "main" | head 10 + - Final query generated internally: search index = "main" | head 10 + 1. The query will execute the query saved in the savedsearch named "Dashboard Views - Action + History". + - on_poll_command: "savedsearch" + - on_poll_query: "Dashboard Views - Action History" + - Final query generated internally: savedsearch "Dashboard Views - Action History" + 1. The query will perform statistics for datamodel and will give total count of events fetched + for datamodel = authentication. + - on_poll_command: "tstats" + - on_poll_query: "count from datamodel=Authentication" + - Final query generated internally: "tstats count from datamodel=Authentication" + 1. The query will display field "a" in table format for the results fetched from 'search index + = "\_internal"' search. + - on_poll_command: None + - on_poll_query: index = "\_internal" | table a + - Final query generated internally: search index = "\_internal" | table a + 1. This query will fetch all the events with sourcetype = "modular_alerts:notable", + app="phantom", and user="admin". + - on_poll_command: None + - on_poll_query: index=\* sourcetype="modular_alerts:notable" app="phantom" user="admin" + - Final query generated internally: search index=\* sourcetype="modular_alerts:notable" + app="phantom" user="admin" + 1. This query will get the count of the events that are indexed in index named "main". + - on_poll_command: None + - on_poll_query: index="main" | stats count + - Final query generated internally: search index="main" | stats count + 1. This query will add a field with name = "a" and value = "abc" in all the events that are + indexed in index named "main". + - on_poll_command: None + - on_poll_query: index="main" | eval a = "abc" + - Final query generated internally: search index="main" | eval a = "abc" + 1. This query will fetch only the sourcetype of all the events that are indexed in index named + "main". + - on_poll_command: None + - on_poll_query: index="main" | fields sourcetype + - Final query generated internally: search index="main" | fields sourcetype + 1. This query will fetch all the events having tag = error and index = main. + - on_poll_command: None + - on_poll_query: index="\_internal" tag=error + - Final query generated internally: search index="\_internal" tag="error" + 1. This query will show the data of "ppf_action_history_searches" lookup. + - on_poll_command: None + - on_poll_query: |inputlookup ppf_action_history_searches + - Final query generated internally: |inputlookup ppf_action_history_searches + +## Naming Ingested Containers + +By default, the "source" field is used to name the ingested containers. To customize the container +names, use the two settings in the asset configuration. For example, if a hostname is expected in +the container name, the "Name to give containers created via ingestion" parameter can be set to +"Notable Splunk Event" and "Values to append to container name" parameter can be set to "host". This +will set the container name to "Notable Splunk Event, host=my.sample.host". The appended values can +be a comma-separated list. + +## Special characters present in the Splunk query can affect the output + +The user must use appropriate special characters in the query according to individual use-case +otherwise the query will end up providing unexpected results. Following is a list of several such +special characters: + +- Non-breaking space +- Soft hyphen +- Micro symbol +- Division symbol +- Non-breaking hyphen +- En dash +- Em dash +- Ellipsis + +There can exist more such characters apart from the ones listed above. + +## Port Information + +The app uses HTTP/ HTTPS protocol for communicating with the Splunk server. Below are the default +ports used by Splunk SOAR. + +| SERVICE NAME | TRANSPORT PROTOCOL | PORT | +|----------------------|--------------------|------| +| http | tcp | 80 | +| https | tcp | 443 | + +8089 is the default port used by Splunk Server. + ### Configuration variables This table lists the configuration variables required to operate Splunk. These variables are specified when configuring a Splunk Enterprise asset in Splunk SOAR. diff --git a/manual_readme_content.md b/manual_readme_content.md new file mode 100644 index 0000000..5f12b49 --- /dev/null +++ b/manual_readme_content.md @@ -0,0 +1,304 @@ +## App's Token-Based Authentication Workflow + +- This app also supports API token based authentication. + +- Please follow the steps mentioned in this + [documentation](https://docs.splunk.com/Documentation/Splunk/9.0.0/Security/CreateAuthTokens) to + generate an API token. + + **NOTE -** If the username/password and API token are both provided then the API token will be + given preference and a token-based authentication workflow will be used. + +## Splunk-SDK + +This app uses the Splunk-SDK module, which is licensed under the Apache Software License, Copyright +(c) 2011-2024 Splunk, Inc. + +## State File Permissions + +Please check the permissions for the state file as mentioned below. + +#### State Filepath + +- For Non-NRI Instance: + /opt/phantom/local_data/app_states/91883aa8-9c81-470b-97a1-5d8f7995f560/{asset_id}\_state.json +- For NRI Instance: + /phantomcyber/local_data/app_states/91883aa8-9c81-470b-97a1-5d8f7995f560/{asset_id}\_state.json + +#### State File Permissions + +- File Rights: rw-rw-r-- (664) (The Splunk SOAR user should have read and write access for the state + file) +- File Owner: appropriate Splunk SOAR user + +## Required Permissions for Post Data Action + +The endpoint used by the post data action is not supported on Splunk Cloud Platform. Hence, the following steps are not applicable for Splunk Cloud Platform. + +For sending events to Splunk Platform, the User configured in the asset would require **edit_tcp** capability. Follow the below steps to configure + +- Login to the Splunk Platform +- Go to **Setting > Roles** +- Click on role of the user configured in the asset(example: user) and go to **Capabilities** +- Search for '**edit_tcp**' in the capabilities enable it for the particular role +- To check if the capability is given to your user, go to **Settings > Users** and in the **Edit dropdown** and select **View Capabilities** +- Search for '**edit_tcp**' and if a tick besides it appears then the permission has been enabled for the user + +## Asset Configuration Parameters + +- container_name_prefix: + - Name to give containers created via ingestion + + - User can select a field name from the events data + + - If the provided field exists, then container_name_prefix will be the value against the + provided field from the events data + - If the provided field does not exist, then container_name_prefix will be the provided + field name itself + + - If the container_name_prefix parameter is not provided: + + - If the event data contains '\_time' field, then container_name_prefix will be 'Splunk + Log Entry on \' + - If the event data does not contain '\_time' field, then container_name_prefix will be + 'Splunk Log Entry' + + - Users can provide a string. Example: Test title +- container_name_values: + - Values to append to the container name created via ingestion + + - User can provide CIM fields + + - If the container_name_values parameter is provided: + + - If the provided field exists, then container_name_values will be the value against the + provided CIM field or its CIM field mapping from the events data + - If neither a CIM field mapping nor CIM field itself is present in the event data, then + container_name_values will be the CIM field mapping or CIM field + + - If the container_name_values parameter is not provided: + + - If 'container_name_prefix' parameter is not provided, then container_name_values will be + 'source' + - If 'container_name_prefix' parameter is provided, then container_name_values will be + empty + + - Users can provide a comma-separated string. Example: test1, test2 +- Container count to update the state file: + - This parameter will allow the user to specify the number of containers and will only be used + in scheduled or interval polling + - Everytime the count of the containers reaches the count provided by the user, the + "start_time" stored in the state file will be updated by the index time of that event + - The default value is 100 +- splunk_app: + - The app context of the namespace + - As per Splunk SDK's documentation, if the splunk_app parameter is not provided, then + "system" will be considered as splunk_app +- splunk_owner: + - The owner context of the namespace + - As per Splunk SDK's documentation, if the splunk_owner parameter is not provided, then + "nobody" will be considered as splunk_owner +- retry_count: + - Number of retries + - To ask a query to the Splunk server using the splunklib library, first, the query asked by + the user is to be parsed. Then, this parsed query is used to create a job and once this job + is ready the results are ready to be fetched. So while performing any of the above steps, if + any exception occurs then, the code will retry that step for the number of retries provided + in the "retry count" configuration parameter. + - It will also be used if an error or an exception occurs while posting the data in the "post + data" action or modifying the event in the "update event" action. +- remove_empty_cef: + - Remove CEF fields having empty values from the artifact + - It allows the user to remove CEF fields having empty values from the artifact during + ingestion. If the value of the parameter is 'true', CEF fields having empty values will be + removed. +- sleeptime_in_requests: + - The time to wait for next REST call(max 120 seconds) + - It allows the user to add sleep time between the REST calls while performing the + "run_query", "update_event", "get host events" and "on poll" action. +- splunk_job_timeout: + - The duration in seconds to wait before a scheduled Splunk job times out + - It allows the user to configure the duration after which the connector should consider the Splunk job as timed out. +- on_poll_display: + - Fields to save with On Poll + - Users can select the fields from the events which the user wants to ingest in the artifact + - If the on_poll_display parameter is not provided, then all the fields that are extracted + from the events will be ingested in the respective artifacts + - Users can provide comma-separated field names. Example: field1, field2, field3 +- If the on_poll_query(query to use with On Poll) parameter is not provided, then an error message + will be returned +- If the on_poll_command(command for the query to use with On Poll) parameter is not provided and + the on_poll_query does not start with "|" or "search", then the "search" keyword is added at + the beginning of the on_poll_query + Example: + - on_poll_command: None + on_poll_query: index = "main" + Final query generated internally: search index = "main" +- If the on_poll_command parameter is not provided and the on_poll_query starts with "|" or + "search", then the final query would be the same as the query provided in the on_poll_query + parameter + Example: + - on_poll_command: None + on_poll_query: search index = "main" + Final query generated internally: search index = "main" +- If on_poll_command parameter is provided, then query is formed as: {on_poll_command} + {on_poll_query} + Example: + - on_poll_command: search + on_poll_query: index = "main" + Final query generated internally: search index = "main" + +## Update Event + +- To execute this action successfully, the minimum role required is "ess_analyst", but the user + can have other roles too. + +- If the **wait_for_confirmation** parameter is False (which is the default), it will be faster + but there will be no confirmation that the notable ID corresponded with an actual notable event. + Setting it to True will cause the action to take longer because it will require an SPL search, + but it will provide more assurance that the update took place. + +- The action updates the event for the provided "event_id". If the **wait_for_confirmation** + parameter is True, the action validates the "event_id" provided by the user using the search + command: 'search \`notable\` | search event_id="\"'. + + - If this search command returns more than 0 results, the action updates the event. + - If this search command does not return any results then, the action fails with the message + "Please provide a valid event ID". + +- Use the integer status field to set custom status values (e.g., 1 for 'New', 2 for 'In Progress', etc.). Similarly, use the integer disposition field for custom disposition values (e.g., 0 for 'Undetermined'). + +## Make Request + +- This action allows executing arbitrary Splunk REST API calls using the asset's configured + credentials and connection settings. + +- The **endpoint** parameter is appended to the base URL derived from the asset's device and port + (e.g., `https://:/`). Do not include the base URL in the endpoint parameter. + Example: `services/search/jobs`, `services/server/info` + +- The **verify_ssl** parameter defaults to the asset's **Verify Server Certificate** setting if + not explicitly provided. + +- Authentication uses the asset's API token (Bearer) or username/password, consistent with all + other actions in this app. + +## On Poll + +- There are two approaches to polling as mentioned below. + + - POLL NOW (Manual polling) + + - It will fetch the data every time as per the corresponding asset configuration + parameters. It doesn't store the last run context of the fetched data. + + - Scheduled/Interval Polling + + - The ingestion action will be triggered after each specified time interval. It stores the + last run context of the fetched data and starts fetching new data based on the + combination of the values of stored context for the previous ingestion run and the + corresponding asset configuration parameters. + +- Notes + + - In case "on poll" returns any 4XX except 403, validate your search Query on Splunk + - Sample "Query" to use with On Poll: index="\_internal" | stats count by host, source, + sourcetype | head 5 | rename host as h0st | rename source as devicehostname + - Sample "Fields to save with On Poll" (if not provided, "on poll" will store all the fields): + source,sourcetype,hostname + - For the **on_poll_parse_only** parameter, if **True** , disables the expansion of search due + to evaluation of sub-searches, time term expansion, lookups, tags, eventtypes, and + sourcetype aliases. This parameter is used for the validation of the Splunk query before + fetching the results + - If multiple severities are returned for the incident in the "on poll" action, then the + highest "severity" will be given priority. If the "severity" is not present in the incident, + then the "urgency" of the incident will be considered. If the "urgency" is also not present, + then the ingested container "severity" will be taken as "medium" by default. + +- Helpful examples to run on poll + + 1. The query will fetch top 10 events from the result of index = "main" search. + - on_poll_command: "search" + - on_poll_query: index = "main" | head 10 + - Final query generated internally: search index = "main" | head 10 + 1. The query will execute the query saved in the savedsearch named "Dashboard Views - Action + History". + - on_poll_command: "savedsearch" + - on_poll_query: "Dashboard Views - Action History" + - Final query generated internally: savedsearch "Dashboard Views - Action History" + 1. The query will perform statistics for datamodel and will give total count of events fetched + for datamodel = authentication. + - on_poll_command: "tstats" + - on_poll_query: "count from datamodel=Authentication" + - Final query generated internally: "tstats count from datamodel=Authentication" + 1. The query will display field "a" in table format for the results fetched from 'search index + = "\_internal"' search. + - on_poll_command: None + - on_poll_query: index = "\_internal" | table a + - Final query generated internally: search index = "\_internal" | table a + 1. This query will fetch all the events with sourcetype = "modular_alerts:notable", + app="phantom", and user="admin". + - on_poll_command: None + - on_poll_query: index=\* sourcetype="modular_alerts:notable" app="phantom" user="admin" + - Final query generated internally: search index=\* sourcetype="modular_alerts:notable" + app="phantom" user="admin" + 1. This query will get the count of the events that are indexed in index named "main". + - on_poll_command: None + - on_poll_query: index="main" | stats count + - Final query generated internally: search index="main" | stats count + 1. This query will add a field with name = "a" and value = "abc" in all the events that are + indexed in index named "main". + - on_poll_command: None + - on_poll_query: index="main" | eval a = "abc" + - Final query generated internally: search index="main" | eval a = "abc" + 1. This query will fetch only the sourcetype of all the events that are indexed in index named + "main". + - on_poll_command: None + - on_poll_query: index="main" | fields sourcetype + - Final query generated internally: search index="main" | fields sourcetype + 1. This query will fetch all the events having tag = error and index = main. + - on_poll_command: None + - on_poll_query: index="\_internal" tag=error + - Final query generated internally: search index="\_internal" tag="error" + 1. This query will show the data of "ppf_action_history_searches" lookup. + - on_poll_command: None + - on_poll_query: |inputlookup ppf_action_history_searches + - Final query generated internally: |inputlookup ppf_action_history_searches + +## Naming Ingested Containers + +By default, the "source" field is used to name the ingested containers. To customize the container +names, use the two settings in the asset configuration. For example, if a hostname is expected in +the container name, the "Name to give containers created via ingestion" parameter can be set to +"Notable Splunk Event" and "Values to append to container name" parameter can be set to "host". This +will set the container name to "Notable Splunk Event, host=my.sample.host". The appended values can +be a comma-separated list. + +## Special characters present in the Splunk query can affect the output + +The user must use appropriate special characters in the query according to individual use-case +otherwise the query will end up providing unexpected results. Following is a list of several such +special characters: + +- Non-breaking space +- Soft hyphen +- Micro symbol +- Division symbol +- Non-breaking hyphen +- En dash +- Em dash +- Ellipsis + +There can exist more such characters apart from the ones listed above. + +## Port Information + +The app uses HTTP/ HTTPS protocol for communicating with the Splunk server. Below are the default +ports used by Splunk SOAR. + +| SERVICE NAME | TRANSPORT PROTOCOL | PORT | +|----------------------|--------------------|------| +| http | tcp | 80 | +| https | tcp | 443 | + +8089 is the default port used by Splunk Server. From 3b315c895ed03a1639fe1359972d7329fcc93402 Mon Sep 17 00:00:00 2001 From: grokas Date: Wed, 29 Apr 2026 10:22:29 -0700 Subject: [PATCH 10/10] chore: bump sdk version --- pyproject.toml | 2 +- uv.lock | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e4c5a04..bcf0a25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.13, <3.15" authors = [] dependencies = [ "splunk-sdk>=2.1.1", - "splunk-soar-sdk>=3.18.1", + "splunk-soar-sdk>=3.20.1", "beautifulsoup4>=4.12.0", "python-dateutil>=2.9.0", "requests>=2.33.0", diff --git a/uv.lock b/uv.lock index 9ed82ec..b355195 100644 --- a/uv.lock +++ b/uv.lock @@ -763,6 +763,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/72/b380fb5c89d89c3afafac8cf02a71a45f4f4a4f35531ca949a34683962d1/pcodedmp-1.2.6-py2.py3-none-any.whl", hash = "sha256:4441f7c0ab4cbda27bd4668db3b14f36261d86e5059ce06c0828602cbe1c4278", size = 30939, upload-time = "2019-07-30T18:05:40.483Z" }, ] +[[package]] +name = "pip-licenses" +version = "5.5.5" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "prettytable", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/18/ddd93af610a04f56a51a27095ddfe55238e1ec236f6758730a0d2c0b49f2/pip_licenses-5.5.5.tar.gz", hash = "sha256:60750c006adf7a0910347b726e8ee9fee3bc8d2e7c8307a5c4ec0776c8e2a276", size = 54955, upload-time = "2026-03-28T22:12:56.48Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/9a/6acfdb8d463eac7cdae7534d35d72237eca63f5fbafe797289d8a5fae447/pip_licenses-5.5.5-py3-none-any.whl", hash = "sha256:f4c4c6d9e6a03612cf59f29f19dc8ab54904d82e055b8e191498f2279a224e14", size = 23247, upload-time = "2026-03-28T22:12:54.89Z" }, +] + [[package]] name = "platformdirs" version = "4.9.6" @@ -797,6 +809,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "prettytable" +version = "3.17.0" +source = { registry = "https://pypi.python.org/simple" } +dependencies = [ + { name = "wcwidth", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/45/b0847d88d6cfeb4413566738c8bbf1e1995fad3d42515327ff32cc1eb578/prettytable-3.17.0.tar.gz", hash = "sha256:59f2590776527f3c9e8cf9fe7b66dd215837cca96a9c39567414cbc632e8ddb0", size = 67892, upload-time = "2025-11-14T17:33:20.212Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/8c/83087ebc47ab0396ce092363001fa37c17153119ee282700c0713a195853/prettytable-3.17.0-py3-none-any.whl", hash = "sha256:aad69b294ddbe3e1f95ef8886a060ed1666a0b83018bbf56295f6f226c43d287", size = 34433, upload-time = "2025-11-14T17:33:19.093Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1131,7 +1155,7 @@ requires-dist = [ { name = "python-dateutil", specifier = ">=2.9.0" }, { name = "requests", specifier = ">=2.33.0" }, { name = "splunk-sdk", specifier = ">=2.1.1" }, - { name = "splunk-soar-sdk", specifier = ">=3.18.1" }, + { name = "splunk-soar-sdk", specifier = ">=3.20.1" }, { name = "xmltodict", specifier = ">=0.13.0" }, ] @@ -1157,7 +1181,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/16/c8/c54008fdb14c081fa [[package]] name = "splunk-soar-sdk" -version = "3.19.2" +version = "3.20.1" source = { registry = "https://pypi.python.org/simple" } dependencies = [ { name = "authlib", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -1173,6 +1197,7 @@ dependencies = [ { name = "humanize", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "jinja2", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "packaging", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "pip-licenses", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "pydantic", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "pyjwt", extra = ["crypto"], marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "requests", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, @@ -1181,9 +1206,9 @@ dependencies = [ { name = "tqdm", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, { name = "typer", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/d4/907defe570d3de094fcfc1cec10f2e456656e6fb303cde3b95e54ac91279/splunk_soar_sdk-3.19.2.tar.gz", hash = "sha256:a126b5902de986de6413ca75577561ccf19d758de909348f5a4bd366ada1c5f2", size = 650629, upload-time = "2026-04-13T18:25:47.05Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/46/14f7166c35aeef4481990c4d1b1700222722d37ac238cde7eef9139430d4/splunk_soar_sdk-3.20.1.tar.gz", hash = "sha256:f64a3c4dbaa200a929fe262691e3bfc2e413340d5b09cc617a3a25fb349752ac", size = 667999, upload-time = "2026-04-27T18:48:07.468Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/19/7c2ff188923a42bcfe14d9ca2b192704970d6b3d16695759e9cbf8245864/splunk_soar_sdk-3.19.2-py3-none-any.whl", hash = "sha256:bba4dfc45fada0758dbae456d1a3c6611a87a6d01dadbbe3f6e821c6aa53aed3", size = 207232, upload-time = "2026-04-13T18:25:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ad/a68d01c5ad072ab0c4f6fa2f0e44b5be2adeb37db5067b5699b9761fe059/splunk_soar_sdk-3.20.1-py3-none-any.whl", hash = "sha256:4dcae09c0bd333affa119b25c943c18fa80838f381f41589ac4f3f704600be39", size = 209611, upload-time = "2026-04-27T18:48:06.257Z" }, ] [[package]] @@ -1299,6 +1324,15 @@ wheels = [ { 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 = "wcwidth" +version = "0.6.0" +source = { registry = "https://pypi.python.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, +] + [[package]] name = "webencodings" version = "0.5.1"