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 index 33f03a7..a555dd5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,13 +8,13 @@ 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 + rev: v4.1.0 hooks: - id: conventional-pre-commit stages: [commit-msg] args: [--verbose] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + rev: v5.0.0 hooks: - id: check-merge-conflict - id: end-of-file-fixer @@ -27,7 +27,7 @@ repos: - id: check-json - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.0 + rev: v0.11.7 hooks: - id: ruff args: [ "--fix", "--unsafe-fixes"] # Allow unsafe fixes (ruff pretty strict about what it can fix) @@ -48,9 +48,10 @@ repos: - id: mdformat exclude: "release_notes/.*" - repo: https://github.com/returntocorp/semgrep - rev: v1.89.0 + 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: @@ -60,7 +61,7 @@ repos: exclude: "README.md" # Central hooks - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v2.0.5 + rev: v2.1.0 hooks: - id: build-docs language: python @@ -70,9 +71,6 @@ repos: 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"] 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 index aac0063..fbee5e9 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) 2016-2025 Splunk Inc. + 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. diff --git a/NOTICE b/NOTICE index 6409847..0b0d66d 100644 --- a/NOTICE +++ b/NOTICE @@ -1,12 +1,3 @@ -Splunk SOAR App: Splunk -Copyright (c) 2016-2025 Splunk Inc. +Splunk SOAR App: splunk +Copyright (c) 2016-2026 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 index 252a587..ffaeb76 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Splunk Publisher: Splunk
-Connector Version: 2.20.3
+Connector Version: 3.0.5
Product Vendor: Splunk Inc.
Product Name: Splunk Enterprise
-Minimum Product Version: 6.3.0 +Minimum Product Version: 7.0.0 This app integrates with Splunk to update data on the device, in addition to investigate and ingestion actions @@ -178,6 +178,21 @@ For sending events to Splunk Platform, the User configured in the asset would re - 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. @@ -185,7 +200,7 @@ For sending events to Splunk Platform, the User configured in the asset would re - 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. + parameters. It doesn't store the last run context of the fetched data. - Scheduled/Interval Polling @@ -291,10 +306,10 @@ There can exist more such characters apart from the ones listed above. 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 | +| SERVICE NAME | TRANSPORT PROTOCOL | PORT | |----------------------|--------------------|------| -|         http | tcp | 80 | -|         https | tcp | 443 | +| http | tcp | 80 | +| https | tcp | 443 | 8089 is the default port used by Splunk Server. @@ -311,7 +326,7 @@ VARIABLE | REQUIRED | TYPE | DESCRIPTION **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 | +**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 | @@ -330,27 +345,35 @@ VARIABLE | REQUIRED | TYPE | DESCRIPTION ### 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
+[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) - Ingest logs from the Splunk instance
+[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
-[update event](#action-update-event) - Update a notable event
-[post data](#action-post-data) - Post data to Splunk +[update event](#action-update-event) - Update a notable event ## action: 'test connectivity' -Validate the asset configuration for connectivity. This action logs into the device to check the connection and credentials +test connectivity Type: **test**
Read only: **True** +Basic test for app. + #### Action Parameters No parameters are required for this action #### Action Output -No 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' @@ -359,65 +382,124 @@ 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 | numeric | | +**last_n_days** | optional | Number of days ago | string | | #### 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.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.\*.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.data.\*.\_raw | string | | | +action_result.summary.sid | string | | | 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: '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' -Ingest logs from the Splunk instance +on poll 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.

+Callback action for the on_poll ingest functionality #### 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 | | +**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 @@ -425,8 +507,6 @@ Run a search query on the Splunk device. Please escape any quotes that are part 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 @@ -446,70 +526,22 @@ PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS 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.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' @@ -518,8 +550,6 @@ 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 @@ -527,69 +557,35 @@ 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 | | +**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 | numeric | | +**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 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 | +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 | @@ -597,7 +593,7 @@ ______________________________________________________________________ Auto-generated Splunk SOAR Connector documentation. -Copyright 2025 Splunk Inc. +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. diff --git a/manual_readme_content.md b/manual_readme_content.md index 53c18ed..5f12b49 100644 --- a/manual_readme_content.md +++ b/manual_readme_content.md @@ -168,6 +168,21 @@ For sending events to Splunk Platform, the User configured in the asset would re - 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. @@ -175,7 +190,7 @@ For sending events to Splunk Platform, the User configured in the asset would re - 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. + parameters. It doesn't store the last run context of the fetched data. - Scheduled/Interval Polling @@ -281,9 +296,9 @@ There can exist more such characters apart from the ones listed above. 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 | +| SERVICE NAME | TRANSPORT PROTOCOL | PORT | |----------------------|--------------------|------| -|         http | tcp | 80 | -|         https | tcp | 443 | +| 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..bcf0a25 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.20.1", + "beautifulsoup4>=4.12.0", + "python-dateutil>=2.9.0", + "requests>=2.33.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/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/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_consts.py b/splunk_consts.py deleted file mode 100644 index 9e2e1c3..0000000 --- a/splunk_consts.py +++ /dev/null @@ -1,175 +0,0 @@ -# File: splunk_consts.py -# -# Copyright (c) 2016-2025 Splunk Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under -# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -# either express or implied. See the License for the specific language governing permissions -# and limitations under the License. -# -# -# Success/Error status and 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_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_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_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" - -# Numeric constants -SPLUNK_MILLISECONDS_IN_A_DAY = 86400000 -SPLUNK_NUMBER_OF_DAYS_BEFORE_ENDTIME = 10 - -# Dictionaries -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", - "app": "app", - "bytes_in": "butesIn", - "bytes_out": "bytesOut", - "category": "cat", - "dest": "destinationAddress", - "dest_ip": "destinationAddress", - "dest_mac": "destinationMacAddress", - "dest_nt_domain": "destinationNtDomain", - "dest_port": "destinationPort", - "dest_translated_ip": "destinationTranlsatedAddress", - "dest_translated_port": "destinationTranslatedPort", - "direction": "deviceDirection", - "dns": "destinationDnsDomain", - "dvc": "dvc", - "dvc_ip": "deviceAddress", - "dvc_mac": "deviceMacAddress", - "file_create_time": "fileCreateTime", - "file_hash": "fileHash", - "file_modify_time": "fileModificationTime", - "file_name": "fileName", - "file_path": "filePath", - "file_size": "fileSize", - "message": "message", - "protocol": "transportProtocol", - "request_payload": "request", - "request_payload_type": "requestMethod", - "src": "sourceAddress", - "src_dns": "sourceDnsDomain", - "src_ip": "sourceAddress", - "src_mac": "sourceMacAddress", - "src_nt_domain": "sourceNtDomain", - "src_port": "sourcePort", - "src_translated_ip": "sourceTranslatedAddress", - "src_translated_port": "sourceTranslatedPort", - "src_user": "sourceUserId", - "transport": "transportProtocol", - "url": "requestURL", - "user": "destinationUserName", - "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_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..7fda0d7 --- /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 actions, app + +__ALL__ = [app, actions] diff --git a/__init__.py b/src/actions/__init__.py similarity index 79% rename from __init__.py rename to src/actions/__init__.py index 41abd5e..67f88b8 100644 --- a/__init__.py +++ b/src/actions/__init__.py @@ -1,6 +1,4 @@ -# File: __init__.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,3 +10,5 @@ # 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, make_request, 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..63929e6 --- /dev/null +++ b/src/actions/get_host_events.py @@ -0,0 +1,61 @@ +# 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/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) diff --git a/src/actions/on_poll.py b/src/actions/on_poll.py new file mode 100644 index 0000000..87b687e --- /dev/null +++ b/src/actions/on_poll.py @@ -0,0 +1,226 @@ +# Copyright (c) 2016-2026 Splunk Inc. + +import hashlib +import json +from collections.abc import Iterator +from datetime import UTC + +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 + + 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..928f8f8 --- /dev/null +++ b/src/actions/post_data.py @@ -0,0 +1,70 @@ +# 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..ec98246 --- /dev/null +++ b/src/actions/run_query.py @@ -0,0 +1,175 @@ +# 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="", + ) + 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..8b5cb90 --- /dev/null +++ b/src/actions/update_event.py @@ -0,0 +1,234 @@ +# 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 new file mode 100644 index 0000000..b231cfc --- /dev/null +++ b/src/app.py @@ -0,0 +1,739 @@ +# 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 os +import ssl +import sys +import time +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 +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 soar_sdk.abstract import SOARClient +from soar_sdk.app import App +from soar_sdk.asset import AssetField, BaseAsset, FieldCategory +from soar_sdk.logging import getLogger + +from .splunk_consts import ( + SPLUNK_DEFAULT_REQUEST_TIMEOUT, + SPLUNK_ERR_CONNECTIVITY_FAILED, + SPLUNK_ERR_CONNECTIVITY_TEST, + SPLUNK_ERR_EMPTY_RESPONSE, + SPLUNK_ERR_INVALID_INTEGER, + SPLUNK_ERR_INVALID_SLEEP_TIME, + 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, + 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_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", # 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"), + "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) + + +# --------------------------------------------------------------------------- +# 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) + + +if __name__ == "__main__": + app.cli() diff --git a/src/splunk_consts.py b/src/splunk_consts.py new file mode 100644 index 0000000..4364b6f --- /dev/null +++ b/src/splunk_consts.py @@ -0,0 +1,139 @@ +# 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. + +# 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_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_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}" +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_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." +) + +# Progress messages +SPLUNK_PROG_CREATED_QUERY = "Created query '{query}'" +SPLUNK_PROG_CREATING_SEARCH_JOB = "Creating search job" + +# Default values +SPLUNK_DEFAULT_SOURCE = "Phantom" +SPLUNK_DEFAULT_SOURCE_TYPE = "Automation/Orchestration Platform" + +# Numeric constants +SPLUNK_MILLISECONDS_IN_A_DAY = 86400000 +SPLUNK_NUMBER_OF_DAYS_BEFORE_ENDTIME = 10 + +# Dictionaries +SPLUNK_SEVERITY_MAP = { + "informational": "low", + "low": "low", + "medium": "medium", + "high": "high", + "critical": "high", +} + +CIM_CEF_MAP = { + "action": "act", + "action_name": "act", + "app": "app", + "bytes_in": "butesIn", + "bytes_out": "bytesOut", + "category": "cat", + "dest": "destinationAddress", + "dest_ip": "destinationAddress", + "dest_mac": "destinationMacAddress", + "dest_nt_domain": "destinationNtDomain", + "dest_port": "destinationPort", + "dest_translated_ip": "destinationTranlsatedAddress", + "dest_translated_port": "destinationTranslatedPort", + "direction": "deviceDirection", + "dns": "destinationDnsDomain", + "dvc": "dvc", + "dvc_ip": "deviceAddress", + "dvc_mac": "deviceMacAddress", + "file_create_time": "fileCreateTime", + "file_hash": "fileHash", + "file_modify_time": "fileModificationTime", + "file_name": "fileName", + "file_path": "filePath", + "file_size": "fileSize", + "message": "message", + "protocol": "transportProtocol", + "request_payload": "request", + "request_payload_type": "requestMethod", + "src": "sourceAddress", + "src_dns": "sourceDnsDomain", + "src_ip": "sourceAddress", + "src_mac": "sourceMacAddress", + "src_nt_domain": "sourceNtDomain", + "src_port": "sourcePort", + "src_translated_ip": "sourceTranslatedAddress", + "src_translated_port": "sourceTranslatedPort", + "src_user": "sourceUserId", + "transport": "transportProtocol", + "url": "requestURL", + "user": "destinationUserName", + "user_id": "destinationUserId", +} + +# 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_DEFAULT_REQUEST_TIMEOUT = 60 # in seconds + +# Search Modes +SPLUNK_SEARCH_MODE_SMART = "smart" + +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..6465bfc --- /dev/null +++ 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 new file mode 100644 index 0000000..b355195 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1352 @@ +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 = "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" +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 = "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" +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.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')" }, + { 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.33.0" }, + { name = "splunk-sdk", specifier = ">=2.1.1" }, + { name = "splunk-soar-sdk", specifier = ">=3.20.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.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')" }, + { 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 = "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')" }, + { 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/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/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]] +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 = "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" +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 1809cdb..0000000 Binary files a/wheels/py3/packaging-25.0-py3-none-any.whl and /dev/null differ 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 bc11ad1..0000000 Binary files a/wheels/py3/splunk_sdk-2.1.1-py3-none-any.whl and /dev/null differ 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 98b4c6b..0000000 Binary files a/wheels/shared/deprecation-2.1.0-py2.py3-none-any.whl and /dev/null differ