From b86265ec1bc3852152a6d62b1c7015f81f08f7e9 Mon Sep 17 00:00:00 2001
From: splunk-soar-connectors-admin
Date: Mon, 18 Jul 2022 09:42:04 -0700
Subject: [PATCH 1/4] Adding workflow file for release review
---
.github/workflows/review-release.yml | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 .github/workflows/review-release.yml
diff --git a/.github/workflows/review-release.yml b/.github/workflows/review-release.yml
new file mode 100644
index 0000000..6f3bf31
--- /dev/null
+++ b/.github/workflows/review-release.yml
@@ -0,0 +1,22 @@
+name: Review Release
+concurrency:
+ group: app-release
+ cancel-in-progress: true
+permissions:
+ contents: read
+ id-token: write
+ statuses: write
+on:
+ workflow_dispatch:
+ inputs:
+ task_token:
+ description: 'StepFunction task token'
+ required: true
+
+jobs:
+ review:
+ uses: 'phantomcyber/dev-cicd-tools/.github/workflows/review-release.yml@main'
+ with:
+ task_token: ${{ inputs.task_token }}
+ secrets:
+ resume_release_role_arn: ${{ secrets.RESUME_RELEASE_ROLE_ARN }}
From 2b5336671b196ad9b99ce689c00f4980191fa35d Mon Sep 17 00:00:00 2001
From: splunk-soar-connectors-admin
Date: Thu, 21 Jul 2022 21:06:58 -0700
Subject: [PATCH 2/4] 'stop maintaining and delete release_notes.html'
---
release_notes/release_notes.html | 119 -------------------------------
1 file changed, 119 deletions(-)
delete mode 100644 release_notes/release_notes.html
diff --git a/release_notes/release_notes.html b/release_notes/release_notes.html
deleted file mode 100644
index 193b114..0000000
--- a/release_notes/release_notes.html
+++ /dev/null
@@ -1,119 +0,0 @@
-Splunk Release Notes - Published by Splunk April 26, 2022
-
-Version 2.10.0 - Released April 26, 2022
-
-- Fixed an issue in On Poll action where the index time was not honored during scheduled ingestion [PAPP-25411]
-
-Version 2.9.0 - Released April 01, 2022
-
-- Added 2 new fields ("start_time" and "end_time") to "run query" action [PAPP-24566]
-
-Version 2.8.0 - Released March 07, 2022
-
-- Added a sleep time between REST calls to improve the performance [PAPP-23575]
-
-Version 2.7.0 - Released February 15, 2022
-
-- Added a new 'attach_result' parameter in 'run query' action [PAPP-8315]
-
-Version 2.6.7 - Released February 04, 2022
-
-- Added support for Python 3.9
-
-Version 2.6.6 - Released January 20, 2022
-
-- Changed the hashing algorithm to SHA256 when running in FIPS mode [PAPP-21816]
-
-Version 2.6.3 - Released December 01, 2021
-
-- Fixed a bug in the 'on poll' action [PAPP-20789]
-- Updated the app documentation
-
-Version 2.5.3 - Released November 16, 2021
-
-- Changed the hashing algorithm from md5 to sha-256 [PAPP-19934]
-
-Version 2.4.8 - Released October 19, 2021
-
-- Added a new 'Remove CEF fields having empty values from the artifact' configuration parameter [PAPP-9257]
-
-Version 2.3.3 - Released August 06, 2021
-
-- Updated the 'update event' action's status based on the "success" key in response [PAPP-9587]
-- Modified the code to re-connect based on retry limit in case of "Session not logged in" issue [PAPP-17690]
-- Modified the on-poll action to ingest updated/deleted artifacts in the existing container [PAPP-18788]
-- Updated the document for Update event action with the required role and permission
-
-Version 2.2.3 - Released July 13, 2021
-
-- Added support for custom status ID in the integer status parameter of the 'update event' action [PAPP-9598]
-- Bug fix in the 'run query' action [PAPP-13769]
-- Allow 0 for the 'Max events to ingest for Scheduled Polling' configuration parameter [PAPP-11483]
-- Fix for the 'Values to append to the container name' configuration parameter [PAPP-11072] [PAPP-17977]
-- Handled extra commas in the display parameter of the 'run query' action [PAPP-17228]
-
-Version 2.1.6 - Released June 24, 2021
-
-- Fixed the start_time field in the artifact [PAPP-17613]
-
-Version 2.1.3 - Released April 14, 2021
-
-- Fixed a bug which caused the app to ignore the Global Proxy Settings [PAPP-11360]
-- Fixed a bug during ingestion if an event had multiple associated severities [PAPP-12153]
-
-Version 2.0.34 - Released February 08, 2021
-
-- Updated the app documentation
-- Added the "sid" key in summary of 'update_event', 'run_query', and 'get_host_events' actions
-
-Version 2.0.22 - Released September 18, 2020
-
-- Compatibility changes for Python 3 support
-- Added the "wait_for_confirmation" action parameter in the "update event" action
-- Changed Source Data Identifier of container/artifact to a hash of the combination of "_raw", "source", "sourcetype", and "index"
-- Made the custom view compatible with the Phantom V4.9
-- Added validations on action input parameters
-- Handled the unicode character exceptions
-- Updated the app documentation
-
-Version 1.3.41 - Released January 22, 2020
-
-- Added functionality to run on poll action with only 'query' parameter and empty 'command' parameter
-- Added support for new commands like table, stats, eval
-- Handled exception for Unicode characters issues
-- Bug fixed in 'run query', 'post data', and 'update event' actions
-- Fixed issues in the output views
-- Improved the documentation of the app
-
-Version 1.3.23 - Released July 19, 2018
-
-- Added support for user configurable retries
-- Display parameters are now ordered as specified in the input on 'run query' action
-- Bug fix on 'on poll' ingestion when the license is expired
-- Bug fix on missing data when the fields are case sensitive
-- Moved Splunk SDK wheel into the app for easier app customization
-
-Version 1.3.19 - Released February 07, 2018
-
-- App action views and Logo updates
-
-Version 1.3.16 - Released December 05, 2017
-
-- Added support for on-poll ingestion (Beta Release only)
-
-Version 1.3.7 - Released July 6, 2017
-
-- Added "post data" action
-- Added "update event" action
-- Fixed an issue that caused "get host events" to return no data
-
-Version 1.2.18 - Released October 20, 2016
-
-- Significant improvements to the datapath settings
-- Minor app documentation corrections
-
-Version 1.2.15 - Released June 8, 2016
-
-- Improved table display of "run query" action results.
-- Fixed documentation typos.
-
From 0ae17b0bba2b3012feb16418cc40a0e8b90225f4 Mon Sep 17 00:00:00 2001
From: Mayur Pipaliya
Date: Sun, 4 Sep 2022 22:15:43 -0700
Subject: [PATCH 3/4] Splunk: Feature - added additional authentication type
(#21)
* added: token-auth support
* docs: token-based authentication.
* fix: replaced token with splunkToken
* fix: flake8 suggestions
* Splunk: Added new token based auth method
* Splunk: Forgot linting
* Splunk: Precommit changes, best practices
* Splunk: Handled HTTP connection error
* PAPP-26050: Added splunk best practices methods, replaced TC endpoint
* Splunk: fixed PAPP-26961
* Splunk: changed priority for http and HTTP proxy variables
* Splunk: Added condional based engaging proxy message
* Splunk: updated release notes
* Splunk: Updated latest tested version
* Splunk: Code review changes
* Splunk: added default value for proxy param
* Splunk: Removed future library
* Splunk: Addresed code review changes
* Splunk: Resolved review comments
Co-authored-by: btavethiya
---
LICENSE | 2 +-
NOTICE | 7 -
exclude_files.txt | 5 -
readme.html | 9 +
release_notes/unreleased.md | 3 +
requirements.txt | 2 +-
splunk.json | 216 +++++-----
splunk_connector.py | 377 ++++++++++++------
splunk_consts.py | 20 +-
wheels/py3/certifi-2022.6.15-py3-none-any.whl | Bin 0 -> 160247 bytes
.../certifi-2021.10.8-py2.py3-none-any.whl | Bin 149195 -> 0 bytes
...l => urllib3-1.26.12-py2.py3-none-any.whl} | Bin 138990 -> 140381 bytes
.../xmltodict-0.13.0-py2.py3-none-any.whl | Bin 0 -> 9971 bytes
13 files changed, 413 insertions(+), 228 deletions(-)
delete mode 100644 exclude_files.txt
create mode 100644 wheels/py3/certifi-2022.6.15-py3-none-any.whl
delete mode 100644 wheels/shared/certifi-2021.10.8-py2.py3-none-any.whl
rename wheels/shared/{urllib3-1.26.9-py2.py3-none-any.whl => urllib3-1.26.12-py2.py3-none-any.whl} (57%)
create mode 100644 wheels/shared/xmltodict-0.13.0-py2.py3-none-any.whl
diff --git a/LICENSE b/LICENSE
index e920929..7e2b641 100644
--- a/LICENSE
+++ b/LICENSE
@@ -198,4 +198,4 @@
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.
\ No newline at end of file
+ limitations under the License.
diff --git a/NOTICE b/NOTICE
index 89f6452..2e76229 100644
--- a/NOTICE
+++ b/NOTICE
@@ -10,13 +10,6 @@ Copyright 2004-2017 Leonard Richardson
Copyright 2004-2019 Leonard Richardson
Copyright 2018 Isaac Muse
-Library: future
-Version: 0.18.2
-License: MIT
-Copyright 2013-2019 Python Charmers Pty Ltd, Australia
-Copyright 2013-2019 Python Charmers Pty Ltd, Australia
-Copyright 2013-2019 Python Charmers Pty Ltd, Australia
-
Library: python-dateutil
Version: 2.8.1
License: Apache 2.0
diff --git a/exclude_files.txt b/exclude_files.txt
deleted file mode 100644
index 65c28ec..0000000
--- a/exclude_files.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-docker-compose.yml
-.gitlab-ci.yml
-Makefile
-.git*
-whitesource-results
diff --git a/readme.html b/readme.html
index a0e30af..b763d45 100644
--- a/readme.html
+++ b/readme.html
@@ -15,6 +15,15 @@
+
+ App's Token-Based Authentication Workflow
+
+ - This app also supports API token based authentication.
+ - Please follow the steps mentioned in this documentation to generate an API token.
+ NOTE -
+ If the username/password and API token are both provided then the API token will be given preference and a token-based authentication workflow will be used.
+
+
Splunk-SDK
This app uses the Splunk-SDK module, which is licensed under the Apache Software License, Copyright (c) 2011-2019 Splunk, Inc.
diff --git a/release_notes/unreleased.md b/release_notes/unreleased.md
index fbcb2fd..b061362 100644
--- a/release_notes/unreleased.md
+++ b/release_notes/unreleased.md
@@ -1 +1,4 @@
**Unreleased**
+* Added token-based authentication workflow
+* Replaced an endpoint for test connectivity action
+* Fixed miscellaneous proxy-related issues
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 9243b6e..d458aa6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
beautifulsoup4==4.9.1
-future==0.18.2
python-dateutil==2.8.1
pytz==2021.1
requests==2.25.0
simplejson==3.17.2
splunk-sdk==1.6.18
+xmltodict==0.13.0
diff --git a/splunk.json b/splunk.json
index 2a00e56..7856871 100644
--- a/splunk.json
+++ b/splunk.json
@@ -3,20 +3,40 @@
"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": "Mayur Pipaliya"
+ },
+ {
+ "name": "Chetan Pangam"
+ },
+ {
+ "name": "Govind Salinas"
+ },
+ {
+ "name": "Atif Mahadik"
+ },
+ {
+ "name": "Alexandra Lomotan"
+ },
+ {
+ "name": "Philip Royer"
+ }
+ ],
"type": "siem",
"main_module": "splunk_connector.py",
- "app_version": "2.10.0",
+ "app_version": "2.11.0",
"utctime_updated": "2022-02-04T02:22:09.000000Z",
"package_name": "phantom_splunk",
"product_name": "Splunk Enterprise",
"product_vendor": "Splunk Inc.",
"product_version_regex": ".*",
- "min_phantom_version": "5.2.0",
+ "min_phantom_version": "5.3.0",
"fips_compliant": true,
"python_version": "3",
"latest_tested_versions": [
- "On-premise, Splunk Enterprise Security v8.2.0",
- "Cloud, Splunk Cloud Platform v8.2.2104"
+ "On-premise, Splunk Enterprise Security v9.0.0",
+ "Cloud, Splunk Cloud Platform v8.2.2112"
],
"logo": "logo_splunk.svg",
"logo_dark": "logo_splunk_dark.svg",
@@ -29,16 +49,12 @@
},
{
"module": "certifi",
- "input_file": "wheels/shared/certifi-2021.10.8-py2.py3-none-any.whl"
+ "input_file": "wheels/py3/certifi-2022.6.15-py3-none-any.whl"
},
{
"module": "chardet",
"input_file": "wheels/shared/chardet-3.0.4-py2.py3-none-any.whl"
},
- {
- "module": "future",
- "input_file": "wheels/py3/future-0.18.2-py3-none-any.whl"
- },
{
"module": "idna",
"input_file": "wheels/shared/idna-2.10-py2.py3-none-any.whl"
@@ -73,7 +89,11 @@
},
{
"module": "urllib3",
- "input_file": "wheels/shared/urllib3-1.26.9-py2.py3-none-any.whl"
+ "input_file": "wheels/shared/urllib3-1.26.12-py2.py3-none-any.whl"
+ },
+ {
+ "module": "xmltodict",
+ "input_file": "wheels/shared/xmltodict-0.13.0-py2.py3-none-any.whl"
}
]
},
@@ -100,31 +120,36 @@
"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": 4,
+ "order": 5,
"data_type": "string"
},
"splunk_app": {
"description": "The app context of the namespace",
- "order": 5,
+ "order": 6,
"data_type": "string"
},
"timezone": {
"data_type": "timezone",
- "order": 6,
+ "order": 7,
"description": "Splunk Server Timezone",
"required": true
},
"verify_server_cert": {
"data_type": "boolean",
- "order": 7,
+ "order": 8,
"description": "Verify Server Certificate",
"default": false
},
"on_poll_command": {
"data_type": "string",
- "order": 8,
+ "order": 9,
"description": "Command for query to use with On Poll",
"value_list": [
"",
@@ -138,58 +163,58 @@
},
"on_poll_query": {
"data_type": "string",
- "order": 9,
+ "order": 10,
"description": "Query to use with On Poll"
},
"on_poll_display": {
"data_type": "string",
- "order": 10,
+ "order": 11,
"description": "Fields to save with On Poll"
},
"on_poll_parse_only": {
"data_type": "boolean",
- "order": 11,
+ "order": 12,
"description": "Parse Only",
"default": true
},
"max_container": {
"data_type": "numeric",
- "order": 12,
- "description": "Max events to ingest for Scheduled Polling(Default: 100)",
+ "order": 13,
+ "description": "Max events to ingest for Scheduled Polling (Default: 100)",
"default": 100
},
"container_update_state": {
"data_type": "numeric",
- "order": 13,
+ "order": 14,
"description": "Container count to update the state file",
"default": 100
},
"container_name_prefix": {
"data_type": "string",
- "order": 14,
+ "order": 15,
"description": "Name to give containers created via ingestion"
},
"container_name_values": {
"data_type": "string",
- "order": 15,
+ "order": 16,
"description": "Values to append to container name"
},
"retry_count": {
"description": "Number of retries",
"data_type": "numeric",
- "order": 16,
+ "order": 17,
"default": 3
},
"remove_empty_cef": {
"description": "Remove CEF fields having empty values from the artifact",
"data_type": "boolean",
- "order": 17,
+ "order": 18,
"default": false
},
"sleeptime_in_requests": {
"description": "The time to wait for next REST call (max 120 seconds)",
"data_type": "numeric",
- "order": 18,
+ "order": 19,
"default": 1
}
},
@@ -236,6 +261,14 @@
"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",
@@ -323,31 +356,23 @@
]
},
{
- "data_path": "action_result.status",
+ "data_path": "action_result.summary.sid",
"data_type": "string",
"example_values": [
- "success",
- "failed"
+ "1612177958.977510"
]
},
{
- "data_path": "action_result.message",
- "data_type": "string",
- "example_values": [
- "Sid: 1621953772.25264, Total events: 1"
- ]
+ "data_path": "action_result.summary.total_events",
+ "data_type": "numeric"
},
{
- "data_path": "action_result.summary.sid",
+ "data_path": "action_result.message",
"data_type": "string",
"example_values": [
- "1612177958.977510"
+ "Sid: 1621953772.25264, Total events: 1"
]
},
- {
- "data_path": "action_result.summary.total_events",
- "data_type": "numeric"
- },
{
"data_path": "summary.total_objects",
"data_type": "numeric",
@@ -470,6 +495,14 @@
"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",
@@ -573,6 +606,13 @@
"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",
@@ -763,21 +803,6 @@
"data_type": "string",
"example_value": "/opt/splunk/var/log/splunk/scheduler.log"
},
- {
- "data_path": "action_result.status",
- "data_type": "string",
- "example_values": [
- "success",
- "failed"
- ]
- },
- {
- "data_path": "action_result.message",
- "data_type": "string",
- "example_values": [
- "Sid: 1612177958.977510, Total events: 2"
- ]
- },
{
"data_path": "action_result.summary.sid",
"data_type": "string",
@@ -792,6 +817,13 @@
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",
@@ -877,6 +909,16 @@
}
},
"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",
@@ -959,25 +1001,6 @@
1
]
},
- {
- "data_path": "action_result.status",
- "data_type": "string",
- "example_values": [
- "success",
- "failed"
- ],
- "column_name": "Status",
- "column_order": 0
- },
- {
- "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": "action_result.summary.sid",
"data_type": "string",
@@ -992,6 +1015,15 @@
"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",
@@ -1058,6 +1090,16 @@
}
},
"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",
@@ -1102,14 +1144,8 @@
"data_type": "string"
},
{
- "data_path": "action_result.status",
- "data_type": "string",
- "column_name": "Status",
- "column_order": 0,
- "example_values": [
- "success",
- "failed"
- ]
+ "data_path": "action_result.summary",
+ "data_type": "string"
},
{
"data_path": "action_result.message",
@@ -1120,10 +1156,6 @@
"column_name": "Message",
"column_order": 1
},
- {
- "data_path": "action_result.summary",
- "data_type": "string"
- },
{
"data_path": "summary.total_objects",
"data_type": "numeric",
@@ -1156,16 +1188,12 @@
},
{
"module": "certifi",
- "input_file": "wheels/shared/certifi-2021.10.8-py2.py3-none-any.whl"
+ "input_file": "wheels/py3/certifi-2022.6.15-py3-none-any.whl"
},
{
"module": "chardet",
"input_file": "wheels/shared/chardet-3.0.4-py2.py3-none-any.whl"
},
- {
- "module": "future",
- "input_file": "wheels/py3/future-0.18.2-py3-none-any.whl"
- },
{
"module": "idna",
"input_file": "wheels/shared/idna-2.10-py2.py3-none-any.whl"
@@ -1200,7 +1228,11 @@
},
{
"module": "urllib3",
- "input_file": "wheels/shared/urllib3-1.26.9-py2.py3-none-any.whl"
+ "input_file": "wheels/shared/urllib3-1.26.12-py2.py3-none-any.whl"
+ },
+ {
+ "module": "xmltodict",
+ "input_file": "wheels/shared/xmltodict-0.13.0-py2.py3-none-any.whl"
}
]
}
diff --git a/splunk_connector.py b/splunk_connector.py
index 87b24f2..d33e10c 100644
--- a/splunk_connector.py
+++ b/splunk_connector.py
@@ -39,12 +39,13 @@
import pytz
import requests
import simplejson as json
+import splunklib.binding as splunk_binding
import splunklib.client as splunk_client
import splunklib.results as splunk_results
+import xmltodict
from bs4 import BeautifulSoup, UnicodeDammit
from dateutil.parser import ParserError
from dateutil.parser import parse as dateutil_parse
-from future.standard_library import install_aliases
from past.utils import old_div # noqa
from phantom.base_connector import BaseConnector
from phantom.vault import Vault
@@ -53,8 +54,6 @@
import splunk_consts as consts
-install_aliases()
-
class RetVal(tuple):
def __new__(cls, val1, val2=None):
@@ -88,30 +87,25 @@ def _get_error_message_from_exception(self, e):
:param e: Exception object
:return: error message
"""
+ error_code = None
+ error_msg = consts.SPLUNK_ERR_MSG_UNAVAILABLE
try:
- if e.args:
+ if hasattr(e, "args"):
if len(e.args) > 1:
error_code = e.args[0]
error_msg = e.args[1]
elif len(e.args) == 1:
- error_code = consts.SPLUNK_ERR_CODE_UNAVAILABLE
error_msg = e.args[0]
- else:
- error_code = consts.SPLUNK_ERR_CODE_UNAVAILABLE
- error_msg = consts.SPLUNK_ERR_MSG_UNAVAILABLE
- except:
- error_code = consts.SPLUNK_ERR_CODE_UNAVAILABLE
- error_msg = consts.SPLUNK_ERR_MSG_UNAVAILABLE
+ except Exception as e:
+ self.debug_print("Error occurred while fetching exception information. Details: {}".format(str(e)))
- try:
- error_msg = self._handle_py_ver_compat_for_input_str(error_msg)
- except TypeError:
- error_msg = consts.SPLUNK_UNICODE_DAMMIT_TYPE_ERROR_MESSAGE
- except:
- error_msg = consts.SPLUNK_ERR_MSG_UNAVAILABLE
+ if not error_code:
+ error_text = "Error Message: {}".format(error_msg)
+ else:
+ error_text = "Error Code: {}. Error Message: {}".format(error_code, error_msg)
- return error_code, error_msg
+ return error_text
def initialize(self):
@@ -120,18 +114,22 @@ def initialize(self):
# Fetching the Python major version
try:
self._python_version = int(sys.version_info[0])
- except:
+ except Exception:
return self.set_status(phantom.APP_ERROR, "Error occurred while getting the Phantom server's Python major version")
try:
self.splunk_server = self._handle_py_ver_compat_for_input_str(config[phantom.APP_JSON_DEVICE])
- except:
+ except Exception:
return phantom.APP_ERROR
+ 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 = 'https://{0}:{1}/'.format(self.splunk_server, config.get(phantom.APP_JSON_PORT, 8089))
self._state = self.load_state()
- if not self._state:
- self.debug_print("None obtained while fetching the state file")
+ 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")
@@ -141,16 +139,20 @@ def initialize(self):
self.debug_print("The phantom 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 = {}
- env_vars = config.get('_reserved_environment_variables', {})
- if 'HTTP_PROXY' in env_vars:
- self._proxy['http'] = env_vars['HTTP_PROXY']['value']
+ # 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 env_vars:
- self._proxy['https'] = env_vars['HTTPS_PROXY']['value']
+ 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')
@@ -230,8 +232,8 @@ def request(self, url, message, **kwargs):
}
def handler(self, proxy):
- ''' Splunk SDK Proxy Request Handler
- '''
+ """ Splunk SDK Proxy Request Handler
+ """
proxy_handler = ProxyHandler({'http': proxy, 'https': proxy})
opener = build_opener(proxy_handler)
install_opener(opener)
@@ -244,16 +246,21 @@ def _connect(self, action_result):
config = self.get_config()
- username = config.get('username', None)
-
kwargs_config_flags = {
'host': self.splunk_server,
'port': self.port,
- 'username': username,
- 'password': config.get('password', None),
+ '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
@@ -263,16 +270,32 @@ def _connect(self, action_result):
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.save_progress("[-] Engaging Proxy")
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.debug_print("Error occurred while connecting to the Splunk server. Details: {}".format(error_text))
+ 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,
+ "Error occurred while connecting to the Splunk server. Details: {}".format(error_text))
except Exception as e:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_FAILED,
- error_code=error_code, error_msg=error_msg)
+ error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_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
@@ -285,7 +308,7 @@ def _validate_integer(self, action_result, parameter, key, allow_zero=False):
return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_INVALID_INTEGER.format(param=key)), None
parameter = int(parameter)
- except:
+ except Exception:
return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_INVALID_INTEGER.format(param=key)), None
if parameter < 0:
@@ -305,7 +328,7 @@ def _handle_py_ver_compat_for_input_str(self, input_str, always_encode=False):
try:
if input_str and (self._python_version == 2 or always_encode):
input_str = UnicodeDammit(input_str).unicode_markup.encode('utf-8')
- except:
+ except Exception:
self.debug_print("Error occurred while handling python 2to3 compatibility for the input string")
return input_str
@@ -331,70 +354,212 @@ def _make_rest_call(self, action_result, endpoint, data, params=None, method=req
url = '{0}services/{1}'.format(self._base_url, endpoint)
self.debug_print('Making REST call to {0}'.format(url))
+ auth, auth_headers = None, None
+
+ if self._api_token:
+ # Splunk token-based authentication
+ self.debug_print('Using token-based authentication')
+ auth_headers = {'Authorization': 'Bearer {token}'.format(token=self._api_token)}
+ else:
+ # Splunk username/password based authentication
+ auth = (self._username, self._password)
try:
- response = method(url, data=data, params=params, # nosemgrep
- auth=(config.get(phantom.APP_JSON_USERNAME), config.get(phantom.APP_JSON_PASSWORD)),
- verify=config[phantom.APP_JSON_VERIFY])
+ 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_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_FAILED,
- error_code=error_code, error_msg=error_msg)
+ error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_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 (response is not None):
- action_result.add_debug_data({'r_status_code': response.status_code})
- action_result.add_debug_data({'r_text': response.text})
- action_result.add_debug_data({'r_headers': response.headers})
+ 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 phantom 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 = "Can't process response from server. Status Code: {} Data from server: {}".format(r.status_code, 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, "Unable to parse XML response. Error: {0}".format(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 = 'ErrorType: {} ErrorMessage: {}'.format(error_type, error_message)
+ else:
+ error = 'Unable to parse xml response'
+
+ message = "Error from server. Status Code: {0} Data from server: {1}".format(
+ r.status_code, 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:
- error_text = response.text
+ error_text = consts.SPLUNK_ERR_UNABLE_TO_PARSE_JSON_RESPONSE
- error_text = self._handle_py_ver_compat_for_input_str(error_text)
+ if not error_text:
+ error_text = "Empty response and no information received"
+ message = "Status Code: {}. Data from server:\n{}\n".format(status_code, error_text)
- if response.status_code != 200:
- try:
- return action_result.set_status(phantom.APP_ERROR, "{}. {}".format(consts.SPLUNK_ERR_NOT_200, error_text)), None
- except:
- return action_result.set_status(phantom.APP_ERROR, consts.SPLUNK_ERR_NOT_200), None
+ 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)
- if endpoint != 'notable_update':
- return phantom.APP_SUCCESS, response.text
+ 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 = response.json()
+ resp_json = r.json()
except Exception as e:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_NOT_JSON,
- error_code=error_code, error_msg=error_msg)
- return action_result.set_status(phantom.APP_ERROR, error_text), None
+ error_msg = 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_msg)
+ ), 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 = "Error from server. Details: {}".format(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 = "Error from server. Status Code: {}. Error: {}. Error Details: {}".format(status_code, error, 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 = 'ErrorType: {} ErrorMessage: {}'.format(error_type, error_message)
+ else:
+ error = 'Unable to parse json response'
+ else:
+ error = 'Unable to parse json response'
- return phantom.APP_SUCCESS, resp_json
+ message = "Error from server. Status Code: {0} Data from server: {1}".format(
+ r.status_code, error)
+ else:
+ # You should process the error returned in the json
+ error_text = r.text.replace("{", "{{").replace("}", "}}")
+ message = "Error from server. Status Code: {}. Data from server: {}".format(status_code, error_text)
+
+ return RetVal(action_result.set_status(phantom.APP_ERROR, message), None)
def _get_server_version(self, action_result):
- endpoint = 'server/info'
+ 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'
- if consts.SPLUNK_SERVER_VERSION not in resp_data:
- return 'UNKNOWN'
+ splunk_version = resp_data.get('generator', {}).get('version')
- begin_version = re.search(consts.SPLUNK_SERVER_VERSION, resp_data).end()
- end_version = re.search('', resp_data[begin_version:]).start()
+ if not splunk_version:
+ splunk_version = 'UNKNOWN'
- return resp_data[begin_version:begin_version + end_version]
+ return splunk_version
def _check_for_es(self, action_result):
@@ -434,15 +599,13 @@ def _return_first_row_from_query(self, search_query, action_result, kwargs_creat
self._service.parse(search_query, parse_only=True)
break
except HTTPError as e:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_INVALID_QUERY,
- error_code=error_code, error_msg=error_msg)
+ 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_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_FAILED,
- error_code=error_code, error_msg=error_msg)
+ error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_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))
@@ -464,9 +627,8 @@ def _return_first_row_from_query(self, search_query, action_result, kwargs_creat
break
except Exception as e:
if attempt_count == RETRY_LIMIT - 1:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_UNABLE_TO_CREATE_JOB,
- error_code=error_code, error_msg=error_msg)
+ 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)
while True:
@@ -479,9 +641,8 @@ def _return_first_row_from_query(self, search_query, action_result, kwargs_creat
break
except Exception as e:
if attempt_count == RETRY_LIMIT - 1:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_FAILED,
- error_code=error_code, error_msg=error_msg)
+ error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_FAILED,
+ error_text=self._get_error_message_from_exception(e))
return action_result.set_status(phantom.APP_ERROR, error_text)
stats = self._get_stats(job)
@@ -497,9 +658,8 @@ def _return_first_row_from_query(self, search_query, action_result, kwargs_creat
try:
results = splunk_results.ResultsReader(job.results(count=0))
except Exception as e:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg="Error retrieving results", error_code=error_code,
- error_msg=error_msg)
+ 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:
@@ -556,8 +716,7 @@ def _set_splunk_status_dict(self, action_result):
return False
self._splunk_status_dict = {}
- resp_data_json = json.loads(resp_data)
- entry = resp_data_json.get("entry")
+ entry = resp_data.get("entry")
if not entry:
return False
@@ -739,7 +898,7 @@ def _on_poll(self, param):
search_query = search_string
else:
search_query = '{0} {1}'.format(search_command.strip(), search_string.strip())
- except:
+ except Exception:
return action_result.set_status(phantom.APP_ERROR, "Error occurred while parsing the search query")
search_params = {}
@@ -869,15 +1028,13 @@ def _get_event_start(self, start_time):
datetime_obj = dateutil_parse(start_time)
return datetime_obj.astimezone(pytz.utc).strftime('%Y-%m-%dT%H:%M:%S.%fZ')
except ParserError as parse_err:
- error_code, error_msg = self._get_error_message_from_exception(parse_err)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg="ParserError while parsing _time",
- error_code=error_code, error_msg=error_msg)
+ 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:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg="Exception while parsing _time",
- error_code=error_code, error_msg=error_msg)
+ 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
@@ -962,7 +1119,7 @@ def _handle_run_query(self, param):
search_query = search_string
else:
search_query = '{0} {1}'.format(search_command.strip(), search_string.strip())
- except:
+ except Exception:
return action_result.set_status(phantom.APP_ERROR, "Error occurred while parsing the search query")
self.debug_print("search_query: {0}".format(search_query))
@@ -1082,18 +1239,16 @@ def _run_query(self, search_query, action_result, attach_result=False, kwargs_cr
if (phantom.is_fail(self._connect(action_result))):
return action_result.get_status()
if attempt_count == RETRY_LIMIT - 1:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_INVALID_QUERY,
- error_code=error_code, error_msg=error_msg)
+ 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.debug_print('Failed to validate search query: Reason: %s' % e)
if (phantom.is_fail(self._connect(action_result))):
return action_result.get_status()
if attempt_count == RETRY_LIMIT - 1:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_FAILED,
- error_code=error_code, error_msg=error_msg)
+ error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_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))
@@ -1114,9 +1269,8 @@ def _run_query(self, search_query, action_result, attach_result=False, kwargs_cr
except Exception as e:
self.debug_print('Failed to create job: Reason: %s' % e)
if attempt_count == RETRY_LIMIT - 1:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_UNABLE_TO_CREATE_JOB,
- error_code=error_code, error_msg=error_msg)
+ 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)
summary["sid"] = job.__dict__.get("sid")
@@ -1131,9 +1285,8 @@ def _run_query(self, search_query, action_result, attach_result=False, kwargs_cr
break
except Exception as e:
if attempt_count == RETRY_LIMIT - 1:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_FAILED,
- error_code=error_code, error_msg=error_msg)
+ error_text = consts.SPLUNK_EXCEPTION_ERR_MESSAGE.format(msg=consts.SPLUNK_ERR_CONNECTION_FAILED,
+ error_text=self._get_error_message_from_exception(e))
return action_result.set_status(phantom.APP_ERROR, error_text)
stats = self._get_stats(job)
@@ -1157,9 +1310,8 @@ def _run_query(self, search_query, action_result, attach_result=False, kwargs_cr
try:
results = splunk_results.ResultsReader(job.results(count=kwargs_create.get('max_count', 0)))
except Exception as e:
- error_code, error_msg = self._get_error_message_from_exception(e)
- error_text = consts.SPLUNK_EXCEPTION_ERROR_MESSAGE.format(msg="Error retrieving results",
- error_code=error_code, error_msg=error_msg)
+ 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)
data = []
@@ -1198,7 +1350,7 @@ def add_json_result(self, action_result, data):
json.dump(data, f)
except Exception as e:
- error_code, error_msg = self._get_error_message_from_exception(e)
+ error_msg = self._get_error_message_from_exception(e)
msg = "Error occurred while adding file to Vault. Error Details: {}".format(error_msg)
self.debug_print(msg)
return phantom.APP_ERROR
@@ -1211,8 +1363,7 @@ def add_json_result(self, action_result, data):
vault_ret = Vault.add_attachment(file_path, container_id, 'splunk_run_query_result.json', vault_attach_dict)
except Exception as e:
- error_code, error_msg = self._get_error_message_from_exception(e)
- err = "Error Code: {0}. Error Message: {1}".format(error_code, error_msg)
+ 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))
diff --git a/splunk_consts.py b/splunk_consts.py
index 174fcb5..567e10f 100644
--- a/splunk_consts.py
+++ b/splunk_consts.py
@@ -34,12 +34,18 @@
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_MSG_UNAVAILABLE = "Error message unavailable. Please check the asset configuration and|or action parameters."
-SPLUNK_ERR_CODE_UNAVAILABLE = "Error code unavailable"
-SPLUNK_UNICODE_DAMMIT_TYPE_ERROR_MESSAGE = "Error occurred while connecting to the Splunk server. Please check the asset \
- configuration and|or the action parameters."
-SPLUNK_EXCEPTION_ERROR_MESSAGE = "{msg}. Error Code: {error_code}. Error Message: {error_msg}"
+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_EMPTY_RESPONSE = "Status Code {code}. Empty response and no information in the header."
+
# Progress messages
SPLUNK_PROG_GOT_JOB_ID = "Got job id '{job_id}'"
SPLUNK_PROG_TIME_RANGE = "Using range '{range}'"
@@ -72,6 +78,7 @@
SPLUNK_JSON_TOTAL_EVENTS = "total_events"
SPLUNK_JSON_UPDATED_EVENT_ID = "updated_event_id"
SPLUNK_JSON_ATTACH_RESULT = "attach_result"
+SPLUNK_JSON_API_KEY = "api_token" # pragma: allowlist secret
# Default values
SPLUNK_DEFAULT_EVENT_COUNT = 10
@@ -79,11 +86,6 @@
SPLUNK_DEFAULT_SOURCE = "Phantom"
SPLUNK_DEFAULT_SOURCE_TYPE = "Automation/Orchestration Platform"
-# HTML search strings:
-SPLUNK_POST_DATA_WARN = ''
-SPLUNK_SERVER_VERSION = ''
-SPLUNK_ES_NAME = 'SA-EndpointProtection'
-
# Numeric constants
SPLUNK_MILLISECONDS_IN_A_DAY = 86400000
SPLUNK_NUMBER_OF_DAYS_BEFORE_ENDTIME = 10
diff --git a/wheels/py3/certifi-2022.6.15-py3-none-any.whl b/wheels/py3/certifi-2022.6.15-py3-none-any.whl
new file mode 100644
index 0000000000000000000000000000000000000000..6e70631c740535909f4a1fb7f8b1999f6ac1103d
GIT binary patch
literal 160247
zcmZUULzFHG&|SZ_ZQHhOTeofVwr$(yZQHhO+qV6`WRPU749=iNwQJYlD9V6_WW4rI}_9nAiS^5ky-qlzv#4pK$a$V;%io
zP3^=FJXn6vQwok~3w*`@Lm=!MqDXl6u$J=Q=Ewh!z}C>x?tc@=#VSDuF~WxKdPUAr
zz+3=<^Iw5?1|(za8vKzW?))**82v!byq!&(>o8Xj@489=%gN%ZjgL<;1O8Y@<`|YM
z4tYgc7bcrz{5tvIVHmZ_kRXyPOW>DZ#H6wIPVx*AM)y%JvOJ_`g?>{pS%trknRi4;VoH2NZz(
z|9CVu{9i`t9ZYSfy0sdMBs1wgW@;gMPK!646(T(1wfGoeNaF=z1JkUh2n8km1@A6v
zi};4(d6*k3IAp&COmpb{mg=w>D&|C9eN5YA{@~2|
z@_enih{?zxw{pd{{PileeGt3@=gdS0h
z9DxcAq)Id~NEbB3eVPY7$Wqm5{MX$)j7AG8!_HYxGzTYaV_+sihy%V&I!6AFU9_ve
z3@*t|*hEAHCwYCY-zk$W>WdeG6JH!BbZc&(e1wCl$M66y!fwt0We$gYZElR*hXM99bhvtP#~1=$d&$dTrtnU>CeaDr~^?Hc%&jR<70#GXi6
zc-P*YRT}(YV_!41=63S+pDwr04Wt+pkx2=Z+Cxv@nftG+l?;|v(D|^LgAZw
z9VMhWG7(`#5*SWsH`1~lL=?QLHGjJ4&(9srBA58_)X-`_7$Z#=n2)PHfb#NhtOy
zPB6L`!#aXwI9dsdY}`0ulP1nbA-S}O?UI<;$Uk*-`tsYWGreNc%&t~x)SI2MG;^V+x%JQ*n}Ym{_!di+%6GR%W|(k!pBV`Y
zO>>sHu^Y8-B*kMG_HWeO?ReCc@nWbX{l(jUYLUO{ZQC%k1SV0tp8m0*1zT$WfXG#{
z^?L5l>Q=sVmzIZ!k~^xHQz(m$sjvS%7xPO3=_Xo21fuw*W%1^WN#>Rs%i$UuE|Dqj
zHRE_1TeZrfsJlb>P8b*75l!Q)+OWoEmGpaN<^{=6XyGD`vA0abrXKxdFv;oWVIfd1qM
z%=uBzB3S5%`4{+Xr-=HZ@U}-y4DshsVD%U2
z`;;TtFx_5Yrd7X#^q!&Y?RW6lC;ui+BFjtV|UV;gX
zkc+orG|M1?#$W_3lOLWWNL0|sU6_@k_YTG&icRn}#eHy%D
zqb6iuUG-frXaT35LQQP)TI>`Fv(0=a9iX_nIb0_ZVSennp?x(w{mU!$m{Amk(6uM1
zc`okyC~8wIfpq6o{*rHfe>o_to{2gd03WKt>lG>bd+YMPJHoAjMG=?48q_ah@2Ye!
z4*?t6{JmH5SC0lMp#Q>WNOmeDgR$c3TmkYua4))t<AoFg=wck(F9hn?swUtqB
z?Dj!hgsS*-niZUpi65lOC0xgR!h9c0}^J;O$Gl}d<6)5;)n35L_G
z;!Iv)Z}n-u9lke#j2vsGd|TAfsZkg2+6z1}hz&+KgvV2GmZ7hG>%kT9`Pvt}qp7j!$R>+*`?x?^^eK|@q&`UKebc+nQFTr=jK7>v#A
z4ZT@B?Ot09&^TPmwp#^wyy=5$x3qB=jkb#Ks?j*u95}i=(Vq6#n4}&yAPTPtUhkoE
z2V1x;V<4NZm(s`4pclOFu3SZLw69*SbPs|2%^X>D>vSL*JX|O5p7d)#>LVC$GkJ~>W+g<)RF>O43~V7dBjk3R}1vH^I
zAFE+8yI8p8cre`Bjb7WgaXg2TS5k{-o+=qEUQS)9=T<5x_!M!2GlmmYka|uHTSoSk
ze`~jn{*0J=!P(*n$k)WMkg|G>Uk~gPw;X|38$hus9zRRVR#)EM6$zbTO0Tk1dqh^Q
zfRndL4%rcHDFXZq+QHSu4|7iJ;6B9Y`*;91Jsi+5@2rN_`&hWvAk2`E<5MFt`WDuE
zg~?bgN!grh$?F9SOJPbnO-(auEzShyFEq^b0RHj%0g_;PNkpr#wTf8BVqzx
zGt{(NH|&>WKJVj?5HW0>v`S}fiPb}s>U;AXqdh|(_INo6n^b}>HJm34FcxP~-t;YY
z8nOAZ$qCCmCmP0i|KlbHp+dXOW!0SBA?kD9s3&C$eiHKp{SaJju=gF0d|cTB$N0XR
zBu@QF$%PpV&Fm@=t3_2=mO;KM>P9rrM`~DjB;hzVpMtrmyj&g`F>%A&g*`)ZrFVXc
zoA}KCKG+Qhnjl85r2X-pa695Pa9`lL#l_bgC_KCNDBasZdfk4~%G@1!b~Hq)p}Rne
zFiEA?Z}v=nCyH%KUF&9n9O_UaDesQEVgyr+j5QQD!N3+aW48H(cYlilPha*?Xa^8{
zP)ToE2`sR0c)lBC0_^T0{0>0Da07&qejMWU_#^TdcF-27B(Y9l*wS-2Y+FiC
zlrWrRJ@VKBKJAvYukWCzhc90;UfT{abLDoQB>6F%^J
zlntU8N)isP-#&YN!~q<>9i|NzQ~YE;1(ZDQPqSz)Mnu<#s3p7cde}3XG|0B4Gs!PG
zim9Wj+E~*NN<1b(T{U*|QowZ=#HM?3#s(!e!JkKR(waYF%Q!}s6sD@03jRBw=EETM
zP!ROYqRly2nrd(6%f5p!H)P6e@B%O^g}%1=fnqusT}nxrETo#y4=ZI;9cuJ3MP42%
zcLcycCYuWP1K0$>xvc>|JHQ668Bh;&^9z079$~H93g#I88^^o!2>I#1hsQwa6kQt`
zgr{%@yUASuJMy*`{TAjM9~z#9Nynmn{ml(|6oSQ?5Gx?Aq%MKwY7xS(=DwtX@t22i
zlDI*;3J`t$<5N&Dx1IBODzB;Mnmcf0`ll1onKX1|?`Sr8Jac^l=_!l(5c#n%4MHK;
zZt=<~R@ZVa>(&FOfr`nuGFzY3wak_Hpv?-shM&qJYL5)Zk+U%Z
z3A^6I4hjEJjk@_AMw2$@I8`fIB{fmA+1S@b5bo81ie9e;Py1n9<%e)p&t`8?yBj8&
zb&9|6Xk1wE$nTgosicWIgbUA)TM+xO?!f{=jzb2n5z|Stt_U!dB}sO8dCppo%0o!U
zw&A>q+(U6+PS!M2T^6VdV*{?TsGxW&D!{tbZ@Ih1-AkjD-c%m`{Iod!F-l7#)%U_B
zAfq0I&JM<_2OcQx7T5JkxJ2a$~i#t=~IbYtttQ-Z%v+WvNdi(SVj|nt$GiYK4|9gr?KzCsj_I`L&ni?$G$cuvb{!C
z01#Y89@Jrx6akvlgvx{*8K9K?)i5%;U=s{KBw4g);6y0B21ROIm3v*9U&OSP;m$mN
z6(gkguq}+Nn23Bx8UbIN=g_#>7S2|@wx?cv1v|LLHqxWylS3|{`d@-P3c|dOvR|U|1B`@ccv)UYIfuno0cb%)K1JOP}S(9s2wFu`)z1~+a17u43?WyVOyK|d;_i@F?Cez;p6J^oJ)Q-XK1IKS!NLyz&
zk^!fRKXqw)s-2NK_OtED%;+!qH59zEsq$V>1A?YQ?NMQ?vv%U5xM11Ev4Vhvt>bPe
zbep|o&Z%j_1uuEirIIZJs+SJFqpvAwB|lPolM}Kic>8Ze7}Qr(oNn_Oc)uE2#o+qt
zX70!NnxayGPbt(*BOGh2ROv8ooR!kBfjX
zH?LEeD2g4pe8&1>wnQ=Sc|wje%4)^GUr55ffoisq>KO$ofs8mUCsiw_Dg#2rQZWLc
z3S|y%rVyFJ^b!aCR#a+Cjc?U>w+nXNIK)G2Bz>ckx=y#%a<&!BM)BIUZkau$7!#y!
zO$rPe&{HNc&(dZ09x@l!4ViCRLv4hDY3!`FrXXxQ}3jDDP
z(8=HJ^TFk-!@Lw@4T`bV^Yv$xfR|?>
zZ)^+mHQ^?&`r5;rWv!0}B1ka!tujfa0Xmzmgak=*M~ZQiD1~%9Y-?63f3!9Oo`hqi
z_v)lrx4@=c3@hWJmA@u{dx?B*sSfnf4muicasR*y`$jPj@GA;{p|}0{i-zzW-4E&|
zHw}@yHTU(tB=_lzIiWZ{LWr+$Eq$U65xAYix<1;n(3T0r&QgQ%FMRj16vfEvDLB-H
z{&DP{zu%baCNJsPB2QVcKXxPgO`f7NDtG@|*G0M7A#N0Opy@e&U&v8u`Vg&Kws~Tl
zE6?uG)h7e2lLpcIa*|#ka@bv?HTaaJjQCy-YuF|4iSx*`Lxx6PKq-S
z9F@V8GS!dZ;_H%z&HPMBSmS{g-(4qCkZg6mJB>>sxwP
zfcu1P3{yv)I|*n;jk`)6+rFelC|`vF5d!AZrH>)~j^#Tn!rX(?*VVW~kANtOW5k8y
z(k<;4q`$%>7p}dQlK_*U*n?S{88yh&)|sVruRYK#Fd==zb8SnUvpC0#^2HWis`Y1E
z2^5wkOY#=T=++=B2`OyvV=rNwtlOgUm+?W>Wv?~<)T(_l5&j9=^mSA=9i3@a0Y(>uX@*p{}2~pEn5P&%PLM)$*UQ2f%Ne6bo
z3v3JIW%36c>f%t~;QeGC+#W_W!~^d?cB(#YBP|Ma2AMxg99=_&VznZi^^h-5!=RKG
zXv?8=u~)2Gutz%9y64^uuv@3d0-t%0}5xQ@r+W!!b7v)
zVhTS19sx05HG!vup0F@e{|^S=J*8e+L;aCQLIBqt`a9U%W25bg8Q+}frwW|NZGrv=
zLxH~86?4sLW~;?PQ=gb!)WI*=z~wT8=j{^~cnjqZt%@(k{&26DJC#f@#+u6;Ah8(-
zF`RL(psDV0L#eM-T07Pk-v3+Ll|zMV)=TH6Ih%FL)ny!pVGVxBYSQ#b%f~F9mCdBe
zqOnEHDQxqm++Zx+SL0vMV(2XJVPlOx#c?FIcDolehpdXJWzOhjAM^@nf$)uOO>>Hd
zx=53+s&3~t``Fs&>T`FkZSk!e^QN+Fc8s$T9CUPQd5_+C4%I>_<%lr6)n+&Xc0|`^
zjlcQn$3AU=#`t|x3jKsJ@2B_y>T$fDg}@%cO*n}xGHuU*Fs
zUw&91)0&gD=(WOC_y}ILo^2dCPw{~uLq$i5npUXP&;x0z)w}m-K~pzIq}!UliaA5(
zRMdn&V!m}4(IkJjSqhx<tdh9NYEiG};
zaAs-W{PNxEP&wCvO3-h{igvCInK5xwnM~A$4{N>5sB2YZ5hG9W5*OwxR^gJ%?g7!?
z%s~2g-JgmhCS#2ik>IPwoit-m}H
zhMr$(Kbc#0bMj(t_~z|qeB@Y3r7!7S9y%XE{Z0t_jYHYt%LG9nWClx!{`M@1bEd*@
zU6mmpQ3j`HebSZjZyW;H?W|6U&-{BM164T(lcz1?TrJO3Z%4mi?+s%fj_l)RHQb$6
z-80miJoP;4L50MkUa`BtMo{kq?IKSkxBHs-&SLP~`Ui68>t?srj;+v%aVh7i>{nZ7
z$3QNcPml0=$K0w-U=do;?WM^?T)I7eUV637rL%o`;Jm^Mw?sSBiR=JKN9O5r){RcA
zdp+1JU9$9L
zhtxHp8m}!~;AOEu3rtdmoKu=GgzcM|bzLixe-fX%U^uKgwIM3_*Xk7xPLji|Af2^)
zbonQD7iO1khX+~Ac1p%B!Tq)!gkfqk2LtDN?jsf@mo<)-ukDMN5-1He
z!=tfW2i*OpXh&9w=s|O-W{o*@dq9JCJC)rs=tqaqNfK{BC|j8%L!ay~8s@`j`P?Rs
zdYZd4A`uQWGS5~A*@^W^#ZX-3B0lXjTP!b^K$T9O_$0hl?~TD`hZh$)FLelP?G}^t
zT*n@}xRbC+S-?D>zD~;vLSkrOy%9fJy8mK-(@mErHR*JrvLeoQj97Gt3oVd7p)Uj
z+q*xID5wi*P1rxM)+&)^kHUk{f;Sw}pJ#rzy5}uz+qKj>`rgeqePn355*vQXv&@fI=TV%f0&o>PO=c+s4zR+3ym~TL~5)+fMnp&Pgh>KY=VdN(E^=OGT&59U-KT&Ah
zf`_&O_e_QI7!-2%Evn*;y+4V?i(pPq;_qFwa+%c7R47<8rBU28!v
zf!0+?%vy7Wv4skBh95RH=bDa5i3PUgeL};7uN+j`I$p#Zp9OVFq`2nplwB-9^Mqtr
zJYH*6ec>EM)CEE|I2Un*LSQT0afXA`NPjybw(O@8q`O*NGeT5j=O^M-o|%}
z6SW`YfScbx4OR}lv`scMiCVpx2>h^AMNPDmrp4*dk*|{?GiHoXQ!Hw>nz6k9Ctp>d
zUbm|?e}h1(;wH$S`;3p!Rl^n{z5wT+x;$_}l1QRK&83X6<>9qBD#r2(xD{RTg@}3;
zt3rOAf{)aQ4_tLE#Mi&nCQGgpqSjfi&JF#g6CuSo>0=h)R;od+cD3W3@Z_F9Pj(pk
z#AC!mL6Xx_XquKnOO@)^CkJ)H4gTO;n$0D~ZN+v8+m`S-l3pPz;S$jg2iHnFhr(w>
zX=t;Vo?YY9@nUE52zET5CP@y~yTzySG);$P%696aCi#$wIh`9^0$XQ1=*=D;h~AXM
zpY)sh*!X-}G2E6;9hv(!Hxnv#bQ>Z8dGCk0ww3E**gRN)hi6q$Sak|V>oqi?A$LZ#
zPhO2p>Hf0UHZEy0a0U2IsA`WQ`Q?}6>qQ`giFr-3`xV1rCs7Q$&`%Pl(6jsY_qbT4
zHa>Fe9ebk(4GqW4N+u!$##(nhqA%(1nXva!rA|ys(a;Exd9)l225q1PHaY78jzRhk
z4tE;_JvdzY?DANQd6n=Y{p+HAmE1H9&i9Ju(Z|E|mgYnkruEqxkxw`)Y~5Oz1q8Cu
z;^F-E`08e#=P(@kLvp#5gHg|t3C`|XVrSe4lqdI&_U;pQPw4hj=yOUMjsnwC47X;^
zuAyzI6MI$qyA5tTva(*P1O*mmueUpu3bV@!LQH!ys6c2eW|4)^Cl%YgM^yrMP}%B9
z_Zpb?RnaGENxCny+#IgDnJg6FMx7@_lgK_XQT)KeusF1<9&{R?^G~?GYpv_0t{?RX
zPZ<<0M&@;^QnLa>puH4jCdAeW7A+X&PP`Q$K_4?d3kKnyq8v?&gFVM!tmv?UoK)Dy
zwGg1=S5#CN&83uLUU-sr#9#HW^bd_&>>8*XQwPaQiqWi`!qpw#=>+Dtf`4@_Ml9T5
z6-FEe?6z%Xx6U0AYe_3m6`E48NuU1Kfx(vgl-wFyG;7Q|@*NPbTWFT=xKgqaARwrl
zf*+}4f1tH<)i7}e7EvqQb<7P%`jAhXz`T@aQr6LwUX}))o(SnG_#PJxjisZVXM-Xs
z+3t$R?N@Nd6xBZi?`|~zO`b$|w3yFq=Qt%?Dah@6f2pWX+R<5C4k3Jmeqr9pB`3qN
zE3L%o@SaQbb?VtYYoNL&3SHg57Tjt~uO6o$bKEugo36%x8-U#b%<_TpjgT94IlmN
zic!mR5jf%QFrh=jk`!)FOhzB;4yjM*CTt+G{khY)L;~Ip*U*6nkK0xBVvCKOEt{j&
z`1fz|rAj4AR>Nqwg#E7D=L~HwR3BLL1hr+?l;q!PL~n$noMHh->bilK8ucIUE;*SSuAHu)hH!M%&FVnlMEcibj$nQZf(;rO6
zCn@a>LkRbBx&k!>^Sm$^$67}cR-d84MyJ3mT!Y`E4e3cOrr$B1ZQ;
z#kF_K!GDBp(N}f5h?3xbToS3p>CpvPbtLU4XrcbeHee#$TfEni?#K6KhrxhXA
zt&-hge0QTEXQxT(Nip9ilEH-bj~n5GSwr3rXG0Cb6u{k)`BM#`lSg+|W%=J5O@WJH
zNW+mtDIhZ1_w76O1!shKfB?(>n3l!w(ZlyUy>1h|nWiI}e>Iff*a$rl#ys+~FBie_
z1cD7_Jc>+&xkidVlR&>pY4%i-)N&@Fw7&g%Qo2SEAX)@4s4&txZk;6503CSLH7TJ)
zlbHuQq_s@W+Qxgmds_OrnaF$sg_QF)v0c
zJv+2aC}J97>QJ@2I%up`boRl;-3>G6=yk-@#KhOapGZNEOS`bca<^T?P$^2DVC#>M
z(3Rd2?_+p(89TqZf1vq9UG_f0SatEV#MFylUVwWnp`q9*s488QJ7Q<2{?`dkK678T
zk_O1}@WLeO8NTu8F{5Y4xuHO3l}}?p?;6t6rA+PCa@8cP>
zjQwBcKyJ$hl-%as5$KXFVQr`jx&zrP#cji|*{q^nyF$yBU5L9~aAHo1xR`P6$94NT
z(POfDCOMh&uIA#=UCXWi=-E2eJPJF0cylho=j0e5`Y|*EmS8c1?jPR>>~ov>s9*o8
z3wzMV%XM{Ne_n87kIUy!%q#%;7~2Q5m0x?V*z5K_uB_aTY4~e4_Z(!ymmlUcYzi5)
z1To#rATyLHQ4lhn2rIe35Oi1xw#Wfk+#&Rh`yO&)$yNW*l5-Z&)5g5DWl|!eD^b
zOBf}T_LhkG&Mlm(M;J=ZVGqP4+vE)&4i`%c#IDpq8V4*XR}TgW<$PaAHk5dTmCzf|
zO{pcw0bU2i5J3Z|?_*B2&;~dS1=cfl92Eve)U_U|I7dVl7Y&1i_cRETyd9T&ae?l;
zk9!yEzcT`cL7r+44N?gs&F_J{LaQ_JhIMbkf0vL1@QmzJn)OFU?r@X86A$V|r|cMP
zp0J8Me8q+il<1!4)oXdDJVJ7Btn)8u>w{T;Sk_sc$=DEPZOn>h$oPB72sfg%2_%IG
z!sE!!)Oh+2?YudbWJ@4Gq0%!>cVx(-{Zi)NVoOI<8vR@93n~7=(0&0gt>f#*1oB-r
zN&>k1zH=sney%=#W{!pYTz&jbAMy!mZ*Mh1_xMssu&8(w5XC7ru9Nd2T6*V`{n{Q6
zMVJ!Cmd?MtM;PuCBo|h;o=xWLrKk@d>dF%OUzr;DyuOe&-pgoNTu9$M2u^Jd@uw;_
znL9nTOWR8L7DH{l?9Jc`SRVD(IAX#)c_!ykn?!Gb=qLrYq;E9q)SZ85H;ht~x<
z4{Jmri0y}AH0~V86Jwv3-JGu_rN`E`l_ec2f};gFxfD}>M$MiBCNdP_G6Z4|&dM1Y
zfoL8-7WcSTWH?h%G?l)d9E9xY>E986m{cPy#uJ%Jcm%{@M6!4rK$%A3L
z6;bq(8Fpev^6&Qoh}X3TDH<+*a6c1m(kK$M1TYDbyCjy{h!sytTl=sQz8%lmUq>*8
z7`ip*Xpm*la)JsRnKPA3y~*D)VY1#zG|qAr$*v>!tSqTs-Ekelw~iBIJ(HuZj0Y~n?aj*?TzNV&GF=B@YAUG
zZJ&AHc(Onc`qgY_8c&9fN}d+KNT?&o?9-1YOee%**wz#@Yf*Pp6~B}QfH$ER=>fJM
z6O$lj;Dtz+4sbk5%tP)2Mf-)=?HnBHIGLgF)73
zWy=Q=90A6X6_c@(L!~3en2IWe5e1Qg-Q)8$-2{+DA<3A39G7e1a>z_v=mWpeKtCvb
zrPy_U5yRtoq5nN289@akpVF$UGU21nPx-8q_MK7XaY{|3
zDJK_mWv-oLd|uxs${W;4$U(>quN
zac1wiOt0X#vi}`e*-T$iAFqMYoR|rWXumD1R<)tJzbBQji2f@jrs0OyZt#wptCZh#
zV-{%31lD}B^B&%zx9;{@CmiMScicTx69xZjZ@f<6=@c0GHnKS`N+fEo)VPm2TPgoE
zM+Hw)r9$rEU>x_W>6p4AW$yBdRrIXS!g0q{_s+~39~D+1n~a8U7|hV+Ht1R2$o|iv
z3<#GT1bKw`6_ScYpN_TtT9kI4DlU;Yi^k)yiH&-^#?g5ZYAd@FSEM;^Tr*2gNw#)*
z)zF)Rz~oUPz_ZN+iI9otiv|0_3+du1CnCHEE5wt3+M?%Xp8Tvjs$}d#f>82g-T35s
z&9txfyC4oICM&t;`5ETX{1)cPO)p)*OFzF>5A#m39-F6!CGgf}@AZwQ%qw%_Hnx?H
z&Q)k;f02lMJ!5jZ?8qSwE9%37^=CpxbDqpD_#&s71TL+HUnmTu*cQlf{X_gON{hup
z273cM&GdM1Nfv_MZ{74G5Y}E}#m%gI>a{0WrCnN`V0HL^ZMHHsK^+yj%|~x=PUK=L
z?(V{@ErN)5sOIy)EX7>GBR%DT92|*oD=fyf6^Eh6$qwFBO_4T0@;%P%A!HP1c4Pm26vf?c(MfAP7JhMu&<2L;L`>hvr;5TQ{7dz>f^Q5)_GoK!x)&ZkufdO5c
z`LSX6TBBkpcW5Fbv~i8cE@ig^Sr!yiJOKN7g|G
zf+Yn^#m=pwDap8lM|Lf!EEF4o3H_3x!pIO_#($xqa_C>tqCIJCQLGZUAr;uZc7Bl0KD1p+OyPh7+F
zv+DHjeu_&G`Mp^X7!mxvSm^gn``!V!?K-M?pm$@c?yMSX)cG5^36rgA#9E-@Og<*v
zWEU8~eG`aU*enwJl8S-et%l$EKpszAtvv0)ra^5()S1&Nk4e1=7q~#Y;c$%|@!WhA
z=s9obcvIcCf*vD&EZ|A2JN(nw^;u!LafE7T8U2D3g~M=U#Tw#K(*kFV
zBxW!;mvJ7lprQy^cg8>$WzV1A55!4ggw?rT8n1pd1=5y?m9*wffX4uhqGUU{&{B*i
z^mlF3O?!7X-8A;Voe$Vu`4YLqd|=o1bk7ee6Ej1zUb&G-sUy=alzt6XnD
zJUB@8T|kLGTQqKP#iBhLhmj(q#l>>ja{2(1_}EmeQmZ#SPMsDl=xaVt-DAA)nv|C}~~y@;jRZ`nR-wWCj~7kT`I=Jcw;g
zbIvHJjK<3!l;x^qN#ftFvisjn}n>fR<`PSUMpXr6s}eU)w=s`S(J)K5O4D?UKfx0;?{D
z8T3iljBJwW30ViTxSN1KuS__uQRc+5Ud@5ID8f5|Jhq3vaa>Yda9U=VJ|YBk)H^6Q
z@7^ToP0^2{mzD9t#D3$&A&cC`ZhP;YLNij&nm_KNc@=ekKj>_eP#8
zax@Hw`&P=)GV%$Y+ByGd_|!p?ZbaZ8VN0l4DvVa%kaqHyf9$M~dtmh^f=gy~Tjp+9La@1QSev`{950Ti^w?KsNapV*N5GyZ{aRG0!Wo
zJ3tdTKo?-bN+_o1Q)P-GB^F|43pu0L@Gv%Yr*ZvP41g3Zdh$
zJxc?FF?J?njj*GBkl73l(QrS2ecV+tI(|iRdm97GjQ=ws0(a~Xd}c;zZ1ZvK1FhTb
zV^5_u=z8*^vCO8W1(c>8pxn_2u|c83d@I{fd89-NqKiUc%&F;TBe@ES3I+j)B#63sdywP;@A`X{Bl
z8UH)tiDmG(I?(>0c=HHyygzM$(fDq&I@{HHiK`6#$ua%aoWT6ZLci1-n}rbOtvl`R
zDlX^L&KX{$%O{rX&Po0>z)MGwx%$V85(^f!BU9S2^ws8;$8+2S(!_DxC>3A#S-&O6
z16*0vNEp@G&m-KNpYO;)kq;
znatFrS;d!+(pzDg{i{6rR;fIKIGTYhbptNGecKqdwz+fYnRS#d&Lo!ptEjY(;8Q@r
z{kw9zWkMQL{le!nR|?sgvg{ip=5l6CwkqW;8DKa=#))&uT^xbrXq4+
z5w@uKl4{8~QF&>*sIvTTy)<1%ad}y&Glmq5h`7QGi76aM*Q^7ZI7;Tir+IkTLj^ky
zGREh{+~fC0jjh+ee35~>@;01>LzIoeoD%bdtQjUBw-0`-p(L$GU}mucMfkADLQuaB
zHlr@12*>R2)E3Z1Z-7&VrviI`Ivj2V{erF}#v|-I)NXSJQ~N1fU!uG-7bt(%J)^98_ZzX}25Jaz)}6hBBl{NJSn_my*A1;4FLfpMNt0%l%6
zb0y;k1Xct>nU)N8-LD4{vjnp(n7_V~IG42d)r_j`PxpF0;V`I$cPdD0M~38p@MWT@*3?vc1XR^Vg}$Wb
zG=CkM>3Pj+jrXb2szeNikDKBi=gqamR8Qd%Sgdkp)PGqCyjE|C%LltEml#oN+#)@%o-pO>ap03HQO7{nT$jE9DzsGFo{*z&E?-Bh
z)~-<}_YE?RxvsT{S`fs&WG#Xe+gQyR8b;;r69#!yK~t}lUwS)DOMj+fAQy+xGICCa
zx?YThmezz0Zo=|$)c=;P^QYNum&-S~@+pZ}-{0-@s4$g2$Asdpa3&AayErzu*uzhs
ztD+|C@N(e|cJOA?zPRp<$@p~;O8M#ON?$XU5gtGGf
z*Mo+6qTSscwXA@c0-ubVw4AcU4JTWZ&ONM!kr&V!WB$gFOcr3nM-dh
zifzH2Puf*8$M05*R)qm~pAz0XH4sso#A)9m;zdVRy4LsrWA`3W9{#**sq*h2_1usH
z{)Kr5o~@O$d7?GWAcKdx9_}3J(HkI%*t=r>L-CBhiltsOwz@Uf{7F_O^>kMe>Irt500_a=IELp+`4z}
zJ%$dluLVlNyl*?TYuy!x#GWXW;E->^^qEMcM2cPfrl%r%g0S?=1@rcYM-22|3cf#&T?Ipg+w
z^O)Acgz7uq>Z_*Hq1uID`&R?k@K4M?U2G_>nH+6
zfg0W@YkAj15(k4uUGRwQB(MCpofhaXQ9sn5MFxK#A0>&iI78*RG)HAGN4CP^x(ya>
zc76}59m=bq_O~y>u1_apQ&!#>7|k;U9zUhn+iGkAoN+COktfo?n#X@{8Ks?5?fw$JdxGgfv>z`x~^d#xck#RgE)r4DWcgpyaSM+$dbH1EK+&R
z4b0ZRc=Kp#S~N)c5$HzVhb+cpA0gdV9ek7K=l5{
zO_n0XX>Q)rcbD7FhkQ%yKj=3rbruRGl8_raPfA+l$1v2F)BC18EzFk{5?GlMjp6;G
z8*aSuMyLDbZUo7azAyd0$v~j;>>27F6evGRN4@EiecOH!a8U+@S_OpSA>oo>gYuuk
zmQ_$8<`S8;JyC(WhiD3q=1czM*(rN+r9aCn0~}#r>WK;9_K=G-^0zXB4ClISNqOBAK`861|u}r7@EeSb+J%u$}#-HX1pF0L#r0MGE
z#_dGx&%kzz-?T58-ow)<@IFh&trzQMtSROY${+>gDJB-zWaAH>qXo~R?pL%lm
zJw1OKY;3Cf6`JZrvdaW|GkY|CAMt>#r#P~aqvuQyOJw1o>T9Hi+Z{{DAMX!nE!ktK
z0hMiHL=j<*l}2AEY*NdAU`2vyydg#U&|Yh-y_NVHvpwbCrd{MusfRV_
zz7#}`dwqMq>3`Is;=CRf#C)L<^IUbA@5D4KTFrX(@&DT>xzLl@5y?o+9ErN+!2`##
zhZ)jhLHzu9xkYeR5J7F{$Kdo7OP;aj?m%`+yBT^&5utF3+guLtWS)MmM@VethmfLg
z`a{CVh~oSmxD$bgI_f5`1le<-1^O)4$h#wkswCk#Vt@XHS(3&{)@ofN5Q(RRzIMLR
z>t^X!-7IoP?*=700`8Mv8shRl06IX$zpVXf+;b1RU-1w{+LPlw5BzCF_gC$fIhDm-
zMfQ-J?5;*ab@5YjSH*V`%379=9?$%+TQaN=_i>Fiz8dR~!%Fc)L3%fL1AVANeyEPb$uZdY=QCy%$%M(5e!v01M~
z<^0TUX{AYDaz9q#?9LHERP#-fY2yQIszspCsh@o^MJsum~8
zHLxt=UB3*dP#G*k`AB)}P`WNMVd$+uMlZM}U*)@o`Nm$n5c=r6+VvL~4$CNh6&W1B
zV8Q+03H|=StWWQNf||S2_-7Kg@*|tcVMYqWjkfT=F>A2%NBcrxXQ%i);V#hg(PJsD
zXo~V|b0=HedZ6`+9l4?a6YneQPI%V26kpT|Asy42L}r%g?5y{E{Y*Z6{}=H6-@;V)
z<0h_63W(202qzqxjwlu@^Oi`ffC=2W*t^sc;8F(f^7Ir
z&T5J!SuDuE42myEPu%Z!GSBp<_`-y$IN-P?64!%L-LQfx4&fy(GDa_kE4SW9hWe_P
z>&b{B0iI)WU-|C2
zdry#j^zVMo5~Y>}v5d#_dlFNbBea-3Xv{u8KU89Y1IDhWe0f3G~
zK@@eEkJ?U&IdurnY~WX7q#4X)xTdY(uVeK-3aKbnigfBE6W-yHWG{O$Hl&MH59=kA
z=~Z~Y*KaS6;#wN`-B8Hn9d&!&E#^yvxTSete@{;yiB|S9S?%L-e;lGW3X5uaUFivf
z-V3{(b<3d6X~4egQO^$xdv(qA(eD+_Ir92*xR)E{Y&$tTH?Xw^W#i$6hs#Cmdq_{o
zCNmQmZSR@85SVpkJ+)Bm3B6_)lp9PR*4GDbc}#c@SLkX)PLE{$E;`fAm7cp93JsCF
zJ#U9jzF&lKf4Lmh$&IVpeu}v1#PC}tjtnlHNc~<-S8Ez3WMX$LZ!H|UD<2QYPH1k-
zTKs*A*9TO*Sa%1e>$*PC?F>sf2WHsuFxUQzaLv6opIrX_RlxSUqy8SU{dmOpK^sBA
zFh*lAfguP*FbqLrADRnpu7D8&9CN7@%v!BK&`?I%J(0e%hJ;s|6!
zViL@tF($Fru!uE$q&Jh{^!G8_1n4sP
zRU=6CUX!mQE~Ufc$67((b#z0USP)$N$oQpiF&XIga=vT2UCI3A`zQ)hC9{u}5J)e7
zis$0|pV8(nwUW5kD>7J=;Pq?|xDb#yZoS6YF2
zUWrI=0=jU-ez_5Q*433szItv>T+UDOu{&KwDjuMVN7=Edw7Y$uQ4R{>6YYqsY02o@liGRP(mw$1sC_Cpv#u3jJt3aO71C)3%TpHpl7|i
z#wvw~CO5cs+99|R@fE)iQYhCVKWmWDam7d+*~4(T@C34}{M%a;`~5BrWc}5;-5Sbi
z_cv7uODha7g=1cepG$Md)9NmE6iD&7(XqPT#7}M{M|81NyKJeSFe1##{*hhusPCul
zX6JG&EBc$cIIuf5x0mETUD?Z_%9NfXhm{;ot4iM{N!A$X;fy&iHBU{+xpna}-_^=y
z9}5aUTLLrW%8&~(k-|ZYNr<@a@dc9;r$vbucQP%d7978tUJ`cZEmE=EgFF`S@gyFx
zBx+QB!tO{(e=}$Rf!qHiZi6IJFz3wS9#tP(^h3JEa5DE5SvJMr;p?j&hN)H~0!4am}CczUOj;9nT1Jy+o}Xwe>|%91Uquk*p(Yt4Hy4e%C>SHvr(2j04?Gz~S$Gdtg$HZ&@h%qoiCb=S
zDh16dhmnC^{1759=1on6)43Wqo<;uhy7Qxa7|u=ND$HKT&ZqTLM2K3QV_XtitWtKZ
z@|e%$$%fDahO-}Pcr)riW
zLB7&ZP_z#at>*`cpgfKdCEQxT&(UfKUWDn>GL1Lwrng^ab
z=y@f9NO*Ve$Bdzb60hynZ|GdCzG^Lq+cAoP~+HvIEUo>$K
zSw?>rS(GvOz0zkP*O~Vxcgdpe;50GF4yY~pG%aG*tb=j9n?%?Tk}X*wIYgJ{vV%&t
zY~j&H^C;Pg$G3``E=$-vbDvC$yPIS*HP_^6Xx{D_3x!zG;f~79C5butti}2pddP+k
z7ngg8zXc|t?i_QCqLLa3_D&t@W#CUNv5fr{<%vI+aQtM={K@liD>D7C;j;kEM~0y&oS+#5B~Xf@R-#Ix)EdVjI7*Tq}k?E4WuA}0_1P&Pft+Mxbxq-ACJ6sW{XoFA$-0U2JKab@W+D;>PPjb
zVvWGQB)!g`huxbm?u_;~hbPcV{aV8pHu#c{6PiyG^kMMHN9w0?(hSD(K{KQE$=8Aa
zo)*yh0$~a)Vg}vpe$X2Jt;M4AB;;>zFhR+Uo09g^*q`Afv-Bk=H-FK>3BGt>epnrX
ze(fyztc&avEcqFgREqcL!N+gc;H14c+(tv2x6f86E>QSvclgDcF3nqm^SM5u@`7`s
zV>@#iPdxtIS6PqCWM5bE?S(Q^NA|Wy;*ACO^24Y({VzwGwq9@|Q%Up0(TPeqm@RvSGV
zGq?$3$YSorQzzSw3%j&3#kXC1%)gMrWTd^MWReA8DViK#azkgh0MT51ypR{g%(VqeR9FEx40hOjs
z+~-8!!ZqhuF{%$t%A)pd`*^l~ss?M;GE(tRo{*&v)X?2hQI_u5IBmV
z2*Thrf)gY|(FF5hGscQp`8J^r&;|$8e}zYq00keM0q_W*Ci+jDZxyAFyBe_wKpeGY
zFIMORB5*J;nFLrx0yM%u_2O?rISCF1>GG`}h5Ra?fuo@1AC#VOuwOz0d;?Q|F;LT4
zAr&}KY%<0K3~F8|vpHeJ;1ZAoq-Ow-l0Y{O2-+cFW$A{+z+Z#;Ef`K47`k7`;|IQ-AS@LSK|ifYnlta86ofBB)ypT!KXhK)+aKMBIsFUY7s
zlSPdf6$_^I4*zyV(9cWylNEh0IRpKsgMTtz>N3c$AhN?3;h5d}P&lH-Tsr&ebrG%_
zbhVHDl=Yd@7#b!m$mv;Oj@$9%B9+LyUOak+;wxK@`p48Yj(1Zbr9Bh%
z{&c4UJ!!*&=`WW^5d3AgzA?vM0!5HnitH{Ct*x=_VC_7kK=Kn4RmP*RJqjA7#lIbn
z*E5GzcV9!!x5-a;G!f{7#+OAb_Kl|FrM?fiD||lQDy$d2NzTACRD|6hY0mt+UWx-F
za)!p`^;JM&oM4~JSSmxijO|L|>@Sitz?5``4iS6n_>-)nKWfe-pYOQOW}KhIaS>Bl
zZ=~l1(?(!p&by<$MpZUfR7tS@9~eKdzizX+O)T^Z5
zmNiAq)stl289Ohzisa6^bR@lt_-g=jCS?uU!48C8MNV}uo@?T~yPYjafh#MVY})1{
zx0UCZx;nDJWs845$9*U}%aVKXF1hhSZ^;f#_DGpXG(#NmsqPu0g;WA=1*GivV!9h2
zym=+8{lRC1=~E(e9|Iz98J1xEgDl*a9e!ns
z_+Opit2N?39{l4J0FDtWOkyNXqd2odC4;YYk|tn!{e#AEoWu$0!vX-2fQI`N_$Yy7
zIuI&NKo+0*!+_ZI6JN(6^^iV+#G)?ANP}U
zz)wDe-tT+}q3ytz@v#qKX*f2_m>%pLfA%5N7Pk574Xo7W(eoM6`$!D@2OXHNBLnZv
zW^oNkq0ch|K(^++a?|xcpD?{9GE|xS$U5??b0lYM&XLgXtRuzwN9)Kh&XM9v5&TnI`(J7l*>|78hm*|ES81M~1`Pgbz5o_^{fg;T?MxWtCt@71?gb{>{hURI
zv-ebX)KgswtkM@+ZYKVE2>5V#j8qq7b1F%%B}4eyN4Wic1x!@F%PH(qr&DzHB@#yX
z*X$Vk$Rq+j^5*%a$csO*r57oeXVuV(Mpl}Jy^B|LxtAdg*~A!}sO)KK$_|m$Ru-e^
zxSP0LF5AKGIc5c_qnJ8JZ5-fYR3+xCLvvbm)ds_ca}Vv%^kHW$ZN=~#J*hk$%iZyW
zV>e5k@P~EPx=DUYfpE3N>Ucl9n$X-Ga%7Q%yxiUM4idcGO^F9@{Vep}ypNW}1U_#|
zp&xmsNI$9wlM!AF;f^|RC+Wpurb1l0P#j>_2Y-6LayoR{1%zXd`jjf_AVHD)<_p^ILTzG9j`TC%9#Ig;Z?c7jx%;jV;|xZ;VX
zlQ?_c!oi>mR$tNIv#{GO*{RKuUS=uoH1*Ox%>uVf`W`W`yBNtHa$c#?jjG%fw-dU@
z_eU}0Fr>N$1x;JDnAFQTw4}pGjn$}EoYe>?SmPKylW!4Ss5rD}fm`pnHP&tuRnA3f
zYV`nB;QAIaN8B}oIPZvP3FzDpgL@e52sRmr<3Koz9};Q0K9R^Bk6W#TTZF+KikD{u
zTwI$wI+4=n@#1zEZE{)lx{BRYagyOsN~RN+i+3HZm9;~m{V&G&%{psL1b}Mb3kiest~8d4wsj_;T@=X6
zQ9ys~B9(bn3sQ%FoF2Y@#E5^8(DK>QNmVxVsVHy0rI@%mvfex=MCW3CLu@_b@t%-5e+
z4E@_B|GZ-8-!A!Q71!TTYN4JK(lumEMpb;^sWpIlvbW|zWxKp{;bmT+gfc{(dc7*^
zls~Roacr1$DQ^3snB%sQZ21~#SQ?aP=az^C{owDKaCUOr)YO8!9gmR3&$3oDWHRNX
zFX;H(AH672tEHA-i{%B%5^}sS`k{?H}1NFV63LjR0Wsf_B;c@ZfLy
zAhAZ28XpbNL)@<>=q2?tb#`<^tY?Shq)xPC`zN(l2&nZZC^IIQIey0CuipoM-JWhi
z9kKjmh5W;J;Sp`LCJBp-6Jdpy<{r92HBaqv^ZN@$48oiIFSNVZ0rmFuIp
zazE=;#Re!Zv}`2Ys0Phi!p8Nxsvlp{!d>S!3>@J(r%$!oLnXo{#R{Cba9}5r)g8JL
zjBFQ%%YpS?NtxNN{%jJNdBFLH8{d7C*~_v&zH_a@qgsLXx7SNS$3>I7jLQ>gjrQ;2
z3TNk5(T8j0<>LVkd$Bz)^HY=d`ib2S2kSkFVZ306iewCm5Hv%<6ob()^Wi82wVCea`6e5e0(MMbU~ZkHfxz5KiYXfO%pnb9ft47Ak&N0Riju`-;4^R4ulfHwg*5@e$3Y=iO*p?_JET^TUwokIcEuF#F(
zVD@iC)0HEmkbNkD;vy+>IJhnNfM4LX&x{ej+*LAEAp2lEJE8ho)Q@;aU
z{t8%i@lBCNfwSq}s%#>0zQ@X0i(28KVh>zVte{(2N0202c&_}}!1UiT>Q8-n{{^E4
zRj{uPsOmRHE!nW~hYEDXY%DDLnN@$0u30yFBK{4bKV9SRE)n|OHU91rq2FEO?=BJg
z`5M!TwYehs{OeLVuJDJ#wpq0>uj~rr4mM6{((=;fOr@FYK+?kxOv$hGVS+bs!9m=e7$BMlnogi7rSbtBYs>LF06+5q4CWyz9
z(tA49h3*tEt!t&1KPODv!?3tTV8FV(w(st79e;lIe!R=qC+p{ze_$;%NJzry%2rUA
z!AKO@5{Q4du-MXgD@7sVt-oaRJVG|%tds^4UMLCFG>Oe;H2>JB6mXf9!eBtZ3nw=J
z3LGR2kqB^vm6Lqx7bZTx4aekb$dgFog6KV4dIuvD+lil
zl%mG&qIY@F0Z6HE`+FZF$!s8(F{d!cvxwrA1y;_%-#7o
zI&x8QPJ77Jp?b|7(qqStJh5^9l^n$LT*UPZ{Yi_f{KR&)*V+%>38zAN`;S0knvTFY
zafU+nbBn74&nKMyhn?lpI@svomE==Cyxu*^j
zY||wa2V~>7M@yWl>G0?$asiDXy7FGeo!U;(#U=KLqB58#luG?#<;#3T*4wdIII5Ly
z36v)hA@GgSdDfLg6EeJbUh~u+X&uZJ
zaw@r=?9ZfR-8n{;nTPLl#nWN2l_)3&@wT6sZ5_C;NhfzuotHfJAAAAYqy&X_UeUgl0&BfH9ar8JJi%pcDz?
z)Q5d8$c9NU491mLbc8oy4=e?Nc%E&zM*zeFU6pH${xQKIDNrkeQ_xUCWq?Agf36U`
zg6|4mD}3fSU>Eq-37TzPnj{07M05C8rV;R_NR4kzHyFT9fQ8#w5wYoDqgxdarZ<_+
z2vi%>>~n1q+vvv%kSl8#rglzpKJQTNzFe-$26mQwA^Hl)HZ3W=Qx?%3aRcaE%#K?az-$t`{OD
zc!p?+U0k3Q{Axf;0_Y6YuXEukPOVqs-QYj-gLy8)fYTw@{VI9#&wdxb7yyF4pWOa3
z0ED~#k?OGcDws$}qYnjxNv}my_)YsXYQpbXETbyFmlCT$?mLWm-yGGDxW7$bU`||L
z(aWqp?VQ-BceRo+ojbqrBhE(kabKcm`QZ#)55FGjjV+;TrMw%=)p$p36^%Ne&f(g4
zOGKWsJ%8Y8oC)~{e4d)Uhcan~xTVjZ6!dZuAH`%~YvdDlo`D>lOWu%3n&KnTEzHcz
zONp3`aGmbf3-{0b;mq-Z?<$z+!Eo77jccIi;-Vsu$KA#-4~Lh1Dycg0kijT5!nXNG
zU0c+B4LVHO5LOsAt22TQPiv{F1LK~Bp^k|;)u4Py?z?k}FNpUz!BN`btX2C*4rv~_
zq~G(tgKG!o!UaVUEmVzF|Y%g>D|KN|A4~F1i@EsB3I=r7;7=y*OMv?aPYJ
zNa5{ADL*udR&s4hm*wsnaBX)O(O!Yl_KaHN?szGaFq^w>aR#?|;pOZ7-cJ)#MtXXa
zsR!;|Fe}`h>-j=rDjHvJA%~dSwTH~>^U(G#amyCtIw-hyX_C`$nJSKXWUhPj+e#>f
z^EK>{O1K6l;