splunk-soar-sdk 3.2.3__py3-none-any.whl → 3.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- soar_sdk/abstract.py +4 -4
- soar_sdk/actions_manager.py +13 -37
- soar_sdk/app.py +15 -0
- soar_sdk/app_client.py +2 -1
- soar_sdk/asset.py +27 -0
- soar_sdk/asset_state.py +54 -0
- soar_sdk/cli/cli.py +2 -0
- soar_sdk/cli/manifests/processors.py +5 -0
- soar_sdk/cli/package/cli.py +10 -4
- soar_sdk/cli/package/utils.py +16 -15
- soar_sdk/cli/path_utils.py +0 -5
- soar_sdk/cli/test/__init__.py +0 -0
- soar_sdk/cli/test/cli.py +296 -0
- soar_sdk/decorators/webhook.py +3 -2
- soar_sdk/exceptions.py +9 -0
- soar_sdk/meta/actions.py +9 -0
- soar_sdk/shims/phantom/base_connector.py +7 -2
- soar_sdk/shims/phantom/encryption_helper.py +6 -4
- {splunk_soar_sdk-3.2.3.dist-info → splunk_soar_sdk-3.3.1.dist-info}/METADATA +1 -1
- {splunk_soar_sdk-3.2.3.dist-info → splunk_soar_sdk-3.3.1.dist-info}/RECORD +23 -20
- {splunk_soar_sdk-3.2.3.dist-info → splunk_soar_sdk-3.3.1.dist-info}/WHEEL +1 -1
- {splunk_soar_sdk-3.2.3.dist-info → splunk_soar_sdk-3.3.1.dist-info}/entry_points.txt +0 -0
- {splunk_soar_sdk-3.2.3.dist-info → splunk_soar_sdk-3.3.1.dist-info}/licenses/LICENSE +0 -0
soar_sdk/abstract.py
CHANGED
|
@@ -90,7 +90,7 @@ class SOARClient(Generic[SummaryType]):
|
|
|
90
90
|
headers=headers,
|
|
91
91
|
cookies=cookies,
|
|
92
92
|
timeout=timeout,
|
|
93
|
-
auth=auth,
|
|
93
|
+
auth=auth or httpx.USE_CLIENT_DEFAULT,
|
|
94
94
|
follow_redirects=follow_redirects,
|
|
95
95
|
extensions=extensions,
|
|
96
96
|
)
|
|
@@ -125,7 +125,7 @@ class SOARClient(Generic[SummaryType]):
|
|
|
125
125
|
json=json,
|
|
126
126
|
params=params,
|
|
127
127
|
cookies=cookies,
|
|
128
|
-
auth=auth
|
|
128
|
+
auth=auth or httpx.USE_CLIENT_DEFAULT,
|
|
129
129
|
timeout=timeout,
|
|
130
130
|
follow_redirects=follow_redirects,
|
|
131
131
|
extensions=extensions,
|
|
@@ -161,7 +161,7 @@ class SOARClient(Generic[SummaryType]):
|
|
|
161
161
|
json=json,
|
|
162
162
|
params=params,
|
|
163
163
|
cookies=cookies,
|
|
164
|
-
auth=auth
|
|
164
|
+
auth=auth or httpx.USE_CLIENT_DEFAULT,
|
|
165
165
|
timeout=timeout,
|
|
166
166
|
follow_redirects=follow_redirects,
|
|
167
167
|
extensions=extensions,
|
|
@@ -189,7 +189,7 @@ class SOARClient(Generic[SummaryType]):
|
|
|
189
189
|
params=params,
|
|
190
190
|
headers=headers,
|
|
191
191
|
cookies=cookies,
|
|
192
|
-
auth=auth
|
|
192
|
+
auth=auth or httpx.USE_CLIENT_DEFAULT,
|
|
193
193
|
timeout=timeout,
|
|
194
194
|
follow_redirects=follow_redirects,
|
|
195
195
|
extensions=extensions,
|
soar_sdk/actions_manager.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
import os
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
from soar_sdk.compat import remove_when_soar_newer_than
|
|
5
6
|
from soar_sdk.input_spec import InputSpecification
|
|
@@ -12,11 +13,6 @@ from soar_sdk.shims.phantom.action_result import ActionResult as PhantomActionRe
|
|
|
12
13
|
from soar_sdk.shims.phantom.install_info import is_onprem_broker_install
|
|
13
14
|
from soar_sdk.logging import getLogger
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
_INGEST_STATE_KEY = "ingestion_state"
|
|
17
|
-
_AUTH_STATE_KEY = "auth_state"
|
|
18
|
-
_CACHE_STATE_KEY = "asset_cache"
|
|
19
|
-
|
|
20
16
|
logger = getLogger()
|
|
21
17
|
|
|
22
18
|
|
|
@@ -27,9 +23,7 @@ class ActionsManager(BaseConnector):
|
|
|
27
23
|
super().__init__()
|
|
28
24
|
|
|
29
25
|
self._actions: dict[str, Action] = {}
|
|
30
|
-
self.
|
|
31
|
-
self.auth_state: dict = {}
|
|
32
|
-
self.asset_cache: dict = {}
|
|
26
|
+
self.__app_dir: Path | None = None
|
|
33
27
|
|
|
34
28
|
def get_action(self, identifier: str) -> Action | None:
|
|
35
29
|
"""Convenience method for getting an Action callable from its identifier.
|
|
@@ -95,35 +89,6 @@ class ActionsManager(BaseConnector):
|
|
|
95
89
|
else:
|
|
96
90
|
raise RuntimeError(f"Action {action_id} not found.")
|
|
97
91
|
|
|
98
|
-
def initialize(self) -> bool:
|
|
99
|
-
"""Load asset state into memory at initialization, splitting it into 3 categories.
|
|
100
|
-
|
|
101
|
-
Asset state is used to store data that needs to be accessed across actions.
|
|
102
|
-
Chiefly, it is used to store ingestion state, authentication state, and/or
|
|
103
|
-
used as an asset cache. Returns True only to conform with the BaseConnector interface.
|
|
104
|
-
"""
|
|
105
|
-
state = self.load_state() or {}
|
|
106
|
-
self.ingestion_state = state.get(_INGEST_STATE_KEY, {})
|
|
107
|
-
self.auth_state = state.get(_AUTH_STATE_KEY, {})
|
|
108
|
-
self.asset_cache = state.get(_CACHE_STATE_KEY, {})
|
|
109
|
-
|
|
110
|
-
return True
|
|
111
|
-
|
|
112
|
-
def finalize(self) -> bool:
|
|
113
|
-
"""Save asset state from memory into persistent storage at finalization.
|
|
114
|
-
|
|
115
|
-
Joins the SDK's 3 categories of asset state into a single dictionary, conforming
|
|
116
|
-
to the platform's expectations, and saves it.
|
|
117
|
-
Returns True only to conform with the BaseConnector interface.
|
|
118
|
-
"""
|
|
119
|
-
state = {
|
|
120
|
-
_INGEST_STATE_KEY: self.ingestion_state,
|
|
121
|
-
_AUTH_STATE_KEY: self.auth_state,
|
|
122
|
-
_CACHE_STATE_KEY: self.asset_cache,
|
|
123
|
-
}
|
|
124
|
-
self.save_state(state)
|
|
125
|
-
return True
|
|
126
|
-
|
|
127
92
|
def add_result(self, action_result: PhantomActionResult) -> PhantomActionResult:
|
|
128
93
|
"""Wrapper for BaseConnector's add_action_result method."""
|
|
129
94
|
return self.add_action_result(action_result)
|
|
@@ -145,6 +110,10 @@ class ActionsManager(BaseConnector):
|
|
|
145
110
|
|
|
146
111
|
Returns APP_HOME directly on brokers, which contains the correct SDK app path.
|
|
147
112
|
"""
|
|
113
|
+
# If the app dir has been overridden by calling `override_app_dir`, return that
|
|
114
|
+
if self.__app_dir:
|
|
115
|
+
return self.__app_dir.as_posix()
|
|
116
|
+
|
|
148
117
|
# Remove when 7.1.0 is the min supported broker version
|
|
149
118
|
remove_when_soar_newer_than("7.1.1")
|
|
150
119
|
# On AB, APP_HOME is set by spawn to the full app path at runtime
|
|
@@ -165,3 +134,10 @@ class ActionsManager(BaseConnector):
|
|
|
165
134
|
def get_soar_base_url(cls) -> str:
|
|
166
135
|
"""Get the base URL of the Splunk SOAR instance this app is running on."""
|
|
167
136
|
return cls._get_phantom_base_url()
|
|
137
|
+
|
|
138
|
+
def override_app_dir(self, app_dir: Path) -> None:
|
|
139
|
+
"""Request that the given app_dir be used, instead of whatever super().get_app_dir() returns.
|
|
140
|
+
|
|
141
|
+
This is useful for contexts such as Webhooks, where the app dir isn't necessarily the cwd, but we still need to load the app JSON reliably.
|
|
142
|
+
"""
|
|
143
|
+
self.__app_dir = app_dir
|
soar_sdk/app.py
CHANGED
|
@@ -31,6 +31,7 @@ from soar_sdk.types import Action
|
|
|
31
31
|
from soar_sdk.webhooks.routing import Router
|
|
32
32
|
from soar_sdk.webhooks.models import WebhookRequest, WebhookResponse
|
|
33
33
|
from soar_sdk.exceptions import ActionRegistrationError
|
|
34
|
+
from soar_sdk.asset_state import AssetState
|
|
34
35
|
|
|
35
36
|
import uuid
|
|
36
37
|
from soar_sdk.decorators import (
|
|
@@ -134,6 +135,8 @@ class App:
|
|
|
134
135
|
self.actions_manager: ActionsManager = ActionsManager()
|
|
135
136
|
self.soar_client: SOARClient = AppClient()
|
|
136
137
|
|
|
138
|
+
self.app_root = Path(inspect.stack()[1].filename).parent.parent
|
|
139
|
+
|
|
137
140
|
def get_actions(self) -> dict[str, Action]:
|
|
138
141
|
"""Returns the list of actions registered in the app."""
|
|
139
142
|
return self.actions_manager.get_actions()
|
|
@@ -224,6 +227,15 @@ class App:
|
|
|
224
227
|
"""Returns the asset instance for the app."""
|
|
225
228
|
if not hasattr(self, "_asset"):
|
|
226
229
|
self._asset = self.asset_cls.model_validate(self._raw_asset_config)
|
|
230
|
+
|
|
231
|
+
asset_id = self.soar_client.get_asset_id()
|
|
232
|
+
self._asset._auth_state = AssetState(self.actions_manager, "auth", asset_id)
|
|
233
|
+
self._asset._cache_state = AssetState(
|
|
234
|
+
self.actions_manager, "cache", asset_id
|
|
235
|
+
)
|
|
236
|
+
self._asset._ingest_state = AssetState(
|
|
237
|
+
self.actions_manager, "ingest", asset_id
|
|
238
|
+
)
|
|
227
239
|
return self._asset
|
|
228
240
|
|
|
229
241
|
def register_action(
|
|
@@ -799,6 +811,7 @@ class App:
|
|
|
799
811
|
_, soar_auth_token = soar_rest_client.session.headers["Cookie"].split("=")
|
|
800
812
|
asset_id = soar_rest_client.asset_id
|
|
801
813
|
soar_base_url = soar_rest_client.base_url
|
|
814
|
+
soar_base_url = soar_base_url.removesuffix("/rest")
|
|
802
815
|
soar_auth = SOARClientAuth(
|
|
803
816
|
user_session_token=soar_auth_token,
|
|
804
817
|
base_url=soar_base_url,
|
|
@@ -817,6 +830,8 @@ class App:
|
|
|
817
830
|
else:
|
|
818
831
|
normalized_query[key] = [value]
|
|
819
832
|
|
|
833
|
+
self.actions_manager.override_app_dir(self.app_root)
|
|
834
|
+
self.actions_manager._load_app_json()
|
|
820
835
|
request = WebhookRequest(
|
|
821
836
|
method=method,
|
|
822
837
|
headers=headers,
|
soar_sdk/app_client.py
CHANGED
|
@@ -93,6 +93,7 @@ class AppClient(SOARClient[SummaryType]):
|
|
|
93
93
|
verify=False, # noqa: S501
|
|
94
94
|
)
|
|
95
95
|
if session_id:
|
|
96
|
+
self._client.cookies.set("sessionid", session_id)
|
|
96
97
|
self.__login()
|
|
97
98
|
else:
|
|
98
99
|
if soar_auth.username:
|
|
@@ -106,7 +107,7 @@ class AppClient(SOARClient[SummaryType]):
|
|
|
106
107
|
self._client.headers.update({"Cookie": update_cookies})
|
|
107
108
|
|
|
108
109
|
def __login(self) -> None:
|
|
109
|
-
response = self._client.get("/login")
|
|
110
|
+
response = self._client.get("/login", follow_redirects=True)
|
|
110
111
|
response.raise_for_status()
|
|
111
112
|
self.csrf_token = response.cookies.get("csrftoken") or ""
|
|
112
113
|
self._client.cookies.update(response.cookies)
|
soar_sdk/asset.py
CHANGED
|
@@ -11,6 +11,8 @@ from soar_sdk.compat import remove_when_soar_newer_than
|
|
|
11
11
|
from soar_sdk.meta.datatypes import as_datatype
|
|
12
12
|
from soar_sdk.input_spec import AppConfig
|
|
13
13
|
from soar_sdk.field_utils import parse_json_schema_extra
|
|
14
|
+
from soar_sdk.asset_state import AssetState
|
|
15
|
+
from soar_sdk.exceptions import AppContextRequired
|
|
14
16
|
|
|
15
17
|
remove_when_soar_newer_than(
|
|
16
18
|
"7.0.0", "NotRequired from typing_extensions is in typing in Python 3.11+"
|
|
@@ -283,3 +285,28 @@ class BaseAsset(BaseModel):
|
|
|
283
285
|
for field_name, field in cls.model_fields.items()
|
|
284
286
|
if field.annotation is ZoneInfo
|
|
285
287
|
}
|
|
288
|
+
|
|
289
|
+
_auth_state: AssetState | None = None
|
|
290
|
+
_cache_state: AssetState | None = None
|
|
291
|
+
_ingest_state: AssetState | None = None
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def auth_state(self) -> AssetState:
|
|
295
|
+
"""A place to store authentication data, such as session and refresh tokens, between action runs. This data is stored by the SOAR service, and is encrypted at rest."""
|
|
296
|
+
if self._auth_state is None:
|
|
297
|
+
raise AppContextRequired()
|
|
298
|
+
return self._auth_state
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def cache_state(self) -> AssetState:
|
|
302
|
+
"""A place to cache miscellaneous data between action runs. This data is stored by the SOAR service, and is encrypted at rest."""
|
|
303
|
+
if self._cache_state is None:
|
|
304
|
+
raise AppContextRequired()
|
|
305
|
+
return self._cache_state
|
|
306
|
+
|
|
307
|
+
@property
|
|
308
|
+
def ingest_state(self) -> AssetState:
|
|
309
|
+
"""A place to store ingestion information, such as checkpoints, between action runs. This data is stored by the SOAR service, and is encrypted at rest."""
|
|
310
|
+
if self._ingest_state is None:
|
|
311
|
+
raise AppContextRequired()
|
|
312
|
+
return self._ingest_state
|
soar_sdk/asset_state.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import MutableMapping, Iterator
|
|
3
|
+
|
|
4
|
+
from soar_sdk.shims.phantom.base_connector import BaseConnector
|
|
5
|
+
from soar_sdk.shims.phantom.encryption_helper import encryption_helper
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
AssetStateKeyType = str
|
|
9
|
+
AssetStateValueType = str | bool | int | float | None
|
|
10
|
+
AssetStateType = dict[AssetStateKeyType, AssetStateValueType]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AssetState(MutableMapping[AssetStateKeyType, AssetStateValueType]):
|
|
14
|
+
"""An adapter to the asset state stored within SOAR. The state can be split into multiple partitions; this object represents a single partition. State is automatically encrypted at rest."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, backend: BaseConnector, state_key: str, asset_id: str) -> None:
|
|
17
|
+
self.backend = backend
|
|
18
|
+
self.state_key = state_key
|
|
19
|
+
self.asset_id = asset_id
|
|
20
|
+
|
|
21
|
+
def get_all(self) -> AssetStateType:
|
|
22
|
+
"""Get the entirety of this part of the asset state."""
|
|
23
|
+
state = self.backend.load_state() or {}
|
|
24
|
+
if not (part_encrypted := state.get(self.state_key)):
|
|
25
|
+
return {}
|
|
26
|
+
part_json = encryption_helper.decrypt(part_encrypted, self.asset_id)
|
|
27
|
+
return json.loads(part_json)
|
|
28
|
+
|
|
29
|
+
def put_all(self, new_value: AssetStateType) -> None:
|
|
30
|
+
"""Entirely replace this part of the asset state."""
|
|
31
|
+
part_json = json.dumps(new_value)
|
|
32
|
+
part_encrypted = encryption_helper.encrypt(part_json, salt=self.asset_id)
|
|
33
|
+
state = self.backend.load_state() or {}
|
|
34
|
+
state[self.state_key] = part_encrypted
|
|
35
|
+
self.backend.save_state(state)
|
|
36
|
+
|
|
37
|
+
def __getitem__(self, key: AssetStateKeyType) -> AssetStateValueType:
|
|
38
|
+
return self.get_all()[key]
|
|
39
|
+
|
|
40
|
+
def __setitem__(self, key: AssetStateKeyType, value: AssetStateValueType) -> None:
|
|
41
|
+
s = self.get_all()
|
|
42
|
+
s[key] = value
|
|
43
|
+
self.put_all(s)
|
|
44
|
+
|
|
45
|
+
def __delitem__(self, key: AssetStateKeyType) -> None:
|
|
46
|
+
s = self.get_all()
|
|
47
|
+
del s[key]
|
|
48
|
+
self.put_all(s)
|
|
49
|
+
|
|
50
|
+
def __iter__(self) -> Iterator[AssetStateKeyType]:
|
|
51
|
+
yield from self.get_all().keys()
|
|
52
|
+
|
|
53
|
+
def __len__(self) -> int:
|
|
54
|
+
return len(self.get_all().keys())
|
soar_sdk/cli/cli.py
CHANGED
|
@@ -5,6 +5,7 @@ from soar_sdk.paths import SDK_ROOT
|
|
|
5
5
|
from soar_sdk.cli.manifests.cli import manifests
|
|
6
6
|
from soar_sdk.cli.package.cli import package
|
|
7
7
|
from soar_sdk.cli.init.cli import init, convert
|
|
8
|
+
from soar_sdk.cli.test.cli import test
|
|
8
9
|
|
|
9
10
|
CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
|
|
10
11
|
HELP = """A command-line tool for helping with SOAR Apps development"""
|
|
@@ -18,6 +19,7 @@ app.add_typer(manifests, name="manifests")
|
|
|
18
19
|
app.add_typer(package, name="package")
|
|
19
20
|
app.add_typer(init, name="init")
|
|
20
21
|
app.add_typer(convert, name="convert")
|
|
22
|
+
app.add_typer(test, name="test")
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
@app.command("version")
|
|
@@ -68,6 +68,11 @@ class ManifestProcessor:
|
|
|
68
68
|
|
|
69
69
|
if app.webhook_meta is not None:
|
|
70
70
|
app_meta.webhook = app.webhook_meta
|
|
71
|
+
module_name = self.get_module_dot_path(app_meta.main_module)
|
|
72
|
+
app_instance_name = app_meta.main_module.split(":")[-1]
|
|
73
|
+
app_meta.webhook.handler = (
|
|
74
|
+
f"{module_name}.{app_instance_name}.handle_webhook"
|
|
75
|
+
)
|
|
71
76
|
|
|
72
77
|
return app_meta
|
|
73
78
|
|
soar_sdk/cli/package/cli.py
CHANGED
|
@@ -206,7 +206,11 @@ def build(
|
|
|
206
206
|
|
|
207
207
|
|
|
208
208
|
async def upload_app(
|
|
209
|
-
soar_instance: str,
|
|
209
|
+
soar_instance: str,
|
|
210
|
+
username: str,
|
|
211
|
+
password: str,
|
|
212
|
+
app_tarball: Path,
|
|
213
|
+
force: bool = False,
|
|
210
214
|
) -> httpx.Response:
|
|
211
215
|
"""Asynchronously upload an app tgz to a Splunk SOAR system, via REST API."""
|
|
212
216
|
base_url = (
|
|
@@ -217,12 +221,14 @@ async def upload_app(
|
|
|
217
221
|
|
|
218
222
|
payload = {"app": app_tarball.read_bytes()}
|
|
219
223
|
async with phantom_get_login_session(base_url, username, password) as client:
|
|
220
|
-
response = await phantom_install_app(client, "app_install", payload)
|
|
224
|
+
response = await phantom_install_app(client, "app_install", payload, force)
|
|
221
225
|
return response
|
|
222
226
|
|
|
223
227
|
|
|
224
228
|
@package.command()
|
|
225
|
-
def install(
|
|
229
|
+
def install(
|
|
230
|
+
app_tarball: Path, soar_instance: str, username: str = "", force: bool = False
|
|
231
|
+
) -> None:
|
|
226
232
|
"""Install the app tgz to the specified Splunk SOAR system.
|
|
227
233
|
|
|
228
234
|
..note:
|
|
@@ -243,7 +249,7 @@ def install(app_tarball: Path, soar_instance: str, username: str = "") -> None:
|
|
|
243
249
|
password = typer.prompt("Please enter your SOAR password", hide_input=True)
|
|
244
250
|
|
|
245
251
|
app_install_request = asyncio.run(
|
|
246
|
-
upload_app(soar_instance, username, password, app_tarball)
|
|
252
|
+
upload_app(soar_instance, username, password, app_tarball, force)
|
|
247
253
|
)
|
|
248
254
|
|
|
249
255
|
try:
|
soar_sdk/cli/package/utils.py
CHANGED
|
@@ -14,37 +14,38 @@ async def phantom_get_login_session(
|
|
|
14
14
|
base_url=base_url,
|
|
15
15
|
verify=False, # noqa: S501
|
|
16
16
|
timeout=timeout,
|
|
17
|
+
auth=(username, password), # Use HTTP Basic Auth
|
|
17
18
|
) as client:
|
|
18
|
-
#
|
|
19
|
-
response = await client.get("/
|
|
19
|
+
# Get CSRF token by hitting home page (follow redirects)
|
|
20
|
+
response = await client.get("/", follow_redirects=True)
|
|
20
21
|
response.raise_for_status()
|
|
21
22
|
csrf_token = response.cookies.get("csrftoken")
|
|
23
|
+
if not csrf_token:
|
|
24
|
+
raise RuntimeError("Could not obtain CSRF token from SOAR instance")
|
|
22
25
|
client.cookies.update(response.cookies)
|
|
23
26
|
|
|
24
|
-
await client.post(
|
|
25
|
-
"/login",
|
|
26
|
-
data={
|
|
27
|
-
"username": username,
|
|
28
|
-
"password": password,
|
|
29
|
-
"csrfmiddlewaretoken": csrf_token,
|
|
30
|
-
},
|
|
31
|
-
headers={"Referer": f"{base_url}/login"},
|
|
32
|
-
)
|
|
33
|
-
|
|
34
27
|
yield client
|
|
35
28
|
|
|
36
29
|
|
|
37
30
|
async def phantom_install_app(
|
|
38
|
-
client: httpx.AsyncClient,
|
|
31
|
+
client: httpx.AsyncClient,
|
|
32
|
+
endpoint: str,
|
|
33
|
+
files: dict[str, bytes],
|
|
34
|
+
force: bool = False,
|
|
39
35
|
) -> httpx.Response:
|
|
40
36
|
"""Send a POST request with a CSRF token to the specified endpoint using an authenticated token."""
|
|
41
37
|
csrftoken = client.cookies.get("csrftoken")
|
|
38
|
+
if not csrftoken:
|
|
39
|
+
raise RuntimeError("CSRF token not found in cookies")
|
|
42
40
|
|
|
43
41
|
response = await client.post(
|
|
44
42
|
endpoint,
|
|
45
43
|
files=files,
|
|
46
|
-
data={"csrfmiddlewaretoken": csrftoken},
|
|
47
|
-
headers={
|
|
44
|
+
data={"csrfmiddlewaretoken": csrftoken, "forced_installation": force},
|
|
45
|
+
headers={
|
|
46
|
+
"Referer": f"{client.base_url}/",
|
|
47
|
+
"X-CSRFToken": csrftoken,
|
|
48
|
+
},
|
|
48
49
|
)
|
|
49
50
|
|
|
50
51
|
return response
|
soar_sdk/cli/path_utils.py
CHANGED
|
@@ -36,8 +36,3 @@ def context_directory(path: Path) -> Iterator[None]:
|
|
|
36
36
|
yield
|
|
37
37
|
finally:
|
|
38
38
|
os.chdir(original_dir)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def relative_to_cwd(path: Path) -> str:
|
|
42
|
-
"""Reinterpret the given path as relative to the current working directory."""
|
|
43
|
-
return path.relative_to(Path.cwd()).as_posix()
|
|
File without changes
|
soar_sdk/cli/test/cli.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
test = typer.Typer(
|
|
13
|
+
help="Run unit and integration tests",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@test.command()
|
|
20
|
+
def unit(
|
|
21
|
+
parallel: Annotated[
|
|
22
|
+
bool,
|
|
23
|
+
typer.Option(
|
|
24
|
+
"--parallel/--no-parallel",
|
|
25
|
+
"-p",
|
|
26
|
+
help="Run tests in parallel using pytest-xdist",
|
|
27
|
+
),
|
|
28
|
+
] = True,
|
|
29
|
+
coverage: Annotated[
|
|
30
|
+
bool,
|
|
31
|
+
typer.Option(
|
|
32
|
+
"--coverage",
|
|
33
|
+
"-c",
|
|
34
|
+
help="Run with coverage reporting",
|
|
35
|
+
),
|
|
36
|
+
] = False,
|
|
37
|
+
verbose: Annotated[
|
|
38
|
+
bool,
|
|
39
|
+
typer.Option(
|
|
40
|
+
"--verbose",
|
|
41
|
+
"-v",
|
|
42
|
+
help="Verbose test output",
|
|
43
|
+
),
|
|
44
|
+
] = False,
|
|
45
|
+
test_path: Annotated[
|
|
46
|
+
Path | None,
|
|
47
|
+
typer.Option(
|
|
48
|
+
"--test-path",
|
|
49
|
+
"-t",
|
|
50
|
+
help="Path to specific test file or directory",
|
|
51
|
+
),
|
|
52
|
+
] = None,
|
|
53
|
+
junit_xml: Annotated[
|
|
54
|
+
Path | None,
|
|
55
|
+
typer.Option(
|
|
56
|
+
"--junit-xml",
|
|
57
|
+
help="Path to save JUnit XML test results",
|
|
58
|
+
),
|
|
59
|
+
] = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Run unit tests.
|
|
62
|
+
|
|
63
|
+
This command runs the unit test suite, excluding integration tests.
|
|
64
|
+
By default, tests run in parallel for faster execution.
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
# Run all unit tests in parallel
|
|
68
|
+
soarapps test unit
|
|
69
|
+
|
|
70
|
+
# Run unit tests with coverage
|
|
71
|
+
soarapps test unit --coverage
|
|
72
|
+
|
|
73
|
+
# Run specific test file
|
|
74
|
+
soarapps test unit -t tests/test_decorators.py
|
|
75
|
+
|
|
76
|
+
# Run without parallelism
|
|
77
|
+
soarapps test unit --no-parallel
|
|
78
|
+
"""
|
|
79
|
+
pytest_args = [
|
|
80
|
+
sys.executable,
|
|
81
|
+
"-m",
|
|
82
|
+
"pytest",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
if test_path:
|
|
86
|
+
pytest_args.append(str(test_path))
|
|
87
|
+
|
|
88
|
+
pytest_args.extend(
|
|
89
|
+
[
|
|
90
|
+
"-m",
|
|
91
|
+
"not integration",
|
|
92
|
+
"--tb=short",
|
|
93
|
+
"--color=yes",
|
|
94
|
+
"-o",
|
|
95
|
+
"addopts=",
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if parallel:
|
|
100
|
+
pytest_args.extend(["-n", "auto"])
|
|
101
|
+
|
|
102
|
+
if not coverage:
|
|
103
|
+
pytest_args.append("--no-cov")
|
|
104
|
+
|
|
105
|
+
if verbose:
|
|
106
|
+
pytest_args.append("-v")
|
|
107
|
+
|
|
108
|
+
if junit_xml:
|
|
109
|
+
pytest_args.append(f"--junitxml={junit_xml}")
|
|
110
|
+
|
|
111
|
+
console.print("[bold green]Running unit tests[/bold green]")
|
|
112
|
+
if test_path:
|
|
113
|
+
console.print(f"[dim]Test path: {test_path}[/dim]")
|
|
114
|
+
console.print(f"[dim]Parallel: {parallel}[/dim]")
|
|
115
|
+
console.print(f"[dim]Coverage: {coverage}[/dim]")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
result = subprocess.run( # noqa: S603
|
|
119
|
+
pytest_args,
|
|
120
|
+
check=False,
|
|
121
|
+
)
|
|
122
|
+
if result.returncode != 0:
|
|
123
|
+
console.print(f"[red]Tests failed with exit code {result.returncode}[/red]")
|
|
124
|
+
raise typer.Exit(result.returncode)
|
|
125
|
+
else:
|
|
126
|
+
console.print("[bold green]All tests passed![/bold green]")
|
|
127
|
+
except KeyboardInterrupt:
|
|
128
|
+
console.print("[yellow]Tests interrupted by user[/yellow]")
|
|
129
|
+
raise typer.Exit(130) from None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@test.command()
|
|
133
|
+
def integration(
|
|
134
|
+
instance_ip: Annotated[
|
|
135
|
+
str,
|
|
136
|
+
typer.Argument(
|
|
137
|
+
help="SOAR instance IP address or hostname",
|
|
138
|
+
),
|
|
139
|
+
],
|
|
140
|
+
username: Annotated[
|
|
141
|
+
str | None,
|
|
142
|
+
typer.Option(
|
|
143
|
+
"--username",
|
|
144
|
+
"-u",
|
|
145
|
+
help="SOAR instance username",
|
|
146
|
+
envvar="PHANTOM_USERNAME",
|
|
147
|
+
),
|
|
148
|
+
] = None,
|
|
149
|
+
password: Annotated[
|
|
150
|
+
str | None,
|
|
151
|
+
typer.Option(
|
|
152
|
+
"--password",
|
|
153
|
+
"-p",
|
|
154
|
+
help="SOAR instance password",
|
|
155
|
+
envvar="PHANTOM_PASSWORD",
|
|
156
|
+
),
|
|
157
|
+
] = None,
|
|
158
|
+
retries: Annotated[
|
|
159
|
+
int,
|
|
160
|
+
typer.Option(
|
|
161
|
+
"--retries",
|
|
162
|
+
"-r",
|
|
163
|
+
help="Number of test retries on failure",
|
|
164
|
+
),
|
|
165
|
+
] = 2,
|
|
166
|
+
automation_broker: Annotated[
|
|
167
|
+
str | None,
|
|
168
|
+
typer.Option(
|
|
169
|
+
"--automation-broker",
|
|
170
|
+
"-ab",
|
|
171
|
+
help="Automation broker name for on-prem tests",
|
|
172
|
+
envvar="AUTOMATION_BROKER_NAME",
|
|
173
|
+
),
|
|
174
|
+
] = None,
|
|
175
|
+
force_automation_broker: Annotated[
|
|
176
|
+
bool,
|
|
177
|
+
typer.Option(
|
|
178
|
+
"--force-automation-broker",
|
|
179
|
+
help="Force use of automation broker for all tests",
|
|
180
|
+
envvar="FORCE_AUTOMATION_BROKER",
|
|
181
|
+
),
|
|
182
|
+
] = False,
|
|
183
|
+
verbose: Annotated[
|
|
184
|
+
bool,
|
|
185
|
+
typer.Option(
|
|
186
|
+
"--verbose",
|
|
187
|
+
"-v",
|
|
188
|
+
help="Verbose test output",
|
|
189
|
+
),
|
|
190
|
+
] = False,
|
|
191
|
+
test_path: Annotated[
|
|
192
|
+
Path | None,
|
|
193
|
+
typer.Option(
|
|
194
|
+
"--test-path",
|
|
195
|
+
"-t",
|
|
196
|
+
help="Path to specific test file or directory",
|
|
197
|
+
),
|
|
198
|
+
] = None,
|
|
199
|
+
junit_xml: Annotated[
|
|
200
|
+
Path | None,
|
|
201
|
+
typer.Option(
|
|
202
|
+
"--junit-xml",
|
|
203
|
+
help="Path to save JUnit XML test results",
|
|
204
|
+
),
|
|
205
|
+
] = None,
|
|
206
|
+
) -> None:
|
|
207
|
+
"""Run integration tests against a SOAR instance.
|
|
208
|
+
|
|
209
|
+
This command runs the integration test suite against a specified SOAR instance.
|
|
210
|
+
Tests run similar to the GitHub CI workflow.
|
|
211
|
+
|
|
212
|
+
Examples:
|
|
213
|
+
# Run integration tests against a specific instance
|
|
214
|
+
soarapps test integration 10.1.19.88 -u admin -p password
|
|
215
|
+
|
|
216
|
+
# Run tests with automation broker
|
|
217
|
+
soarapps test integration 10.1.19.88 --automation-broker my-broker
|
|
218
|
+
|
|
219
|
+
# Run specific test file
|
|
220
|
+
soarapps test integration 10.1.19.88 -t tests/integration/test_example_app.py
|
|
221
|
+
|
|
222
|
+
# Save test results to file
|
|
223
|
+
soarapps test integration 10.1.19.88 --junit-xml results.xml
|
|
224
|
+
"""
|
|
225
|
+
if not username:
|
|
226
|
+
console.print(
|
|
227
|
+
"[red]Error: Username is required (use -u or PHANTOM_USERNAME env var)[/red]"
|
|
228
|
+
)
|
|
229
|
+
raise typer.Exit(1)
|
|
230
|
+
|
|
231
|
+
if not password:
|
|
232
|
+
console.print(
|
|
233
|
+
"[red]Error: Password is required (use -p or PHANTOM_PASSWORD env var)[/red]"
|
|
234
|
+
)
|
|
235
|
+
raise typer.Exit(1)
|
|
236
|
+
|
|
237
|
+
phantom_url = f"https://{instance_ip}"
|
|
238
|
+
|
|
239
|
+
env = os.environ.copy()
|
|
240
|
+
env["PHANTOM_URL"] = phantom_url
|
|
241
|
+
env["PHANTOM_USERNAME"] = username
|
|
242
|
+
env["PHANTOM_PASSWORD"] = password
|
|
243
|
+
|
|
244
|
+
if automation_broker:
|
|
245
|
+
env["AUTOMATION_BROKER_NAME"] = automation_broker
|
|
246
|
+
|
|
247
|
+
if force_automation_broker:
|
|
248
|
+
env["FORCE_AUTOMATION_BROKER"] = "true"
|
|
249
|
+
|
|
250
|
+
test_dir = Path("tests/integration/")
|
|
251
|
+
if test_path:
|
|
252
|
+
test_dir = test_path
|
|
253
|
+
|
|
254
|
+
pytest_args = [
|
|
255
|
+
sys.executable,
|
|
256
|
+
"-m",
|
|
257
|
+
"pytest",
|
|
258
|
+
str(test_dir),
|
|
259
|
+
"-m",
|
|
260
|
+
"integration",
|
|
261
|
+
"--no-cov",
|
|
262
|
+
"--tb=short",
|
|
263
|
+
"--color=yes",
|
|
264
|
+
"-o",
|
|
265
|
+
"addopts=",
|
|
266
|
+
f"--reruns={retries}",
|
|
267
|
+
]
|
|
268
|
+
|
|
269
|
+
if verbose:
|
|
270
|
+
pytest_args.append("-v")
|
|
271
|
+
|
|
272
|
+
if junit_xml:
|
|
273
|
+
pytest_args.append(f"--junitxml={junit_xml}")
|
|
274
|
+
|
|
275
|
+
console.print(
|
|
276
|
+
f"[bold green]Running integration tests against {instance_ip}[/bold green]"
|
|
277
|
+
)
|
|
278
|
+
console.print(f"[dim]Test directory: {test_dir}[/dim]")
|
|
279
|
+
console.print(f"[dim]Retries: {retries}[/dim]")
|
|
280
|
+
if automation_broker:
|
|
281
|
+
console.print(f"[dim]Automation broker: {automation_broker}[/dim]")
|
|
282
|
+
|
|
283
|
+
try:
|
|
284
|
+
result = subprocess.run( # noqa: S603
|
|
285
|
+
pytest_args,
|
|
286
|
+
env=env,
|
|
287
|
+
check=False,
|
|
288
|
+
)
|
|
289
|
+
if result.returncode != 0:
|
|
290
|
+
console.print(f"[red]Tests failed with exit code {result.returncode}[/red]")
|
|
291
|
+
raise typer.Exit(result.returncode)
|
|
292
|
+
else:
|
|
293
|
+
console.print("[bold green]All tests passed![/bold green]")
|
|
294
|
+
except KeyboardInterrupt:
|
|
295
|
+
console.print("[yellow]Tests interrupted by user[/yellow]")
|
|
296
|
+
raise typer.Exit(130) from None
|
soar_sdk/decorators/webhook.py
CHANGED
|
@@ -2,7 +2,6 @@ import inspect
|
|
|
2
2
|
from functools import wraps
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from soar_sdk.cli.path_utils import relative_to_cwd
|
|
6
5
|
from soar_sdk.webhooks.models import WebhookRequest, WebhookResponse, WebhookHandler
|
|
7
6
|
from soar_sdk.meta.webhooks import WebhookRouteMeta
|
|
8
7
|
from soar_sdk.async_utils import run_async_if_needed
|
|
@@ -47,7 +46,9 @@ class WebhookDecorator:
|
|
|
47
46
|
|
|
48
47
|
stack = inspect.stack()
|
|
49
48
|
declaration_path_absolute = stack[1].filename
|
|
50
|
-
declaration_path =
|
|
49
|
+
declaration_path = (
|
|
50
|
+
Path(declaration_path_absolute).relative_to(self.app.app_root).as_posix()
|
|
51
|
+
)
|
|
51
52
|
_, declaration_lineno = inspect.getsourcelines(function)
|
|
52
53
|
|
|
53
54
|
self.app.webhook_router.add_route(
|
soar_sdk/exceptions.py
CHANGED
|
@@ -51,6 +51,15 @@ class ActionRegistrationError(Exception):
|
|
|
51
51
|
super().__init__(f"Error registering action: {action}")
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
class AppContextRequired(Exception):
|
|
55
|
+
"""Exception raised when trying to access certain features outside the proper context."""
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
super().__init__(
|
|
59
|
+
"This feature is only available in the context of an action run or webhook handler."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
54
63
|
__all__ = [
|
|
55
64
|
"ActionFailure",
|
|
56
65
|
"ActionRegistrationError",
|
soar_sdk/meta/actions.py
CHANGED
|
@@ -39,6 +39,15 @@ class ActionMeta(BaseModel):
|
|
|
39
39
|
"type": self.render_as,
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
if self.view_handler:
|
|
43
|
+
module = self.view_handler.__module__
|
|
44
|
+
module_parts = module.split(".")
|
|
45
|
+
if len(module_parts) > 1:
|
|
46
|
+
relative_module = ".".join(module_parts[1:])
|
|
47
|
+
else:
|
|
48
|
+
relative_module = module
|
|
49
|
+
data["render"]["view"] = f"{relative_module}.{self.view_handler.__name__}"
|
|
50
|
+
|
|
42
51
|
# Remove view_handler from the output since in render
|
|
43
52
|
data.pop("view_handler", None)
|
|
44
53
|
data.pop("render_as", None)
|
|
@@ -27,6 +27,8 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
27
27
|
self.action_results: list[ActionResult] = []
|
|
28
28
|
self.__conn_result: ConnectorResult
|
|
29
29
|
self.__conn_result = ConnectorResult()
|
|
30
|
+
self.__state: dict = {}
|
|
31
|
+
self.__app_json: dict = {}
|
|
30
32
|
|
|
31
33
|
@staticmethod
|
|
32
34
|
def _get_phantom_base_url() -> str:
|
|
@@ -121,10 +123,10 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
121
123
|
return self.config
|
|
122
124
|
|
|
123
125
|
def save_state(self, state: dict) -> None:
|
|
124
|
-
self.
|
|
126
|
+
self.__state = state
|
|
125
127
|
|
|
126
128
|
def load_state(self) -> dict:
|
|
127
|
-
return self.
|
|
129
|
+
return self.__state
|
|
128
130
|
|
|
129
131
|
def _set_csrf_info(self, token: str, referer: str) -> None:
|
|
130
132
|
pass
|
|
@@ -140,5 +142,8 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
140
142
|
remove_when_soar_newer_than("7.1.1")
|
|
141
143
|
return Path.cwd().as_posix()
|
|
142
144
|
|
|
145
|
+
def _load_app_json(self) -> None:
|
|
146
|
+
pass
|
|
147
|
+
|
|
143
148
|
|
|
144
149
|
__all__ = ["BaseConnector"]
|
|
@@ -11,16 +11,18 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
11
11
|
import base64
|
|
12
12
|
|
|
13
13
|
class encryption_helper: # type: ignore[no-redef]
|
|
14
|
-
"""Simulated encryption helper for environments without BaseConnector.
|
|
14
|
+
"""Simulated encryption helper for environments without BaseConnector.
|
|
15
|
+
|
|
16
|
+
Salt values are optional, as newer versions of SOAR no longer accept them."""
|
|
15
17
|
|
|
16
18
|
@staticmethod
|
|
17
|
-
def encrypt(plain: str, salt: str) -> str:
|
|
19
|
+
def encrypt(plain: str, salt: str = "unused-salt") -> str:
|
|
18
20
|
"""Simulates the behavior of encryption_helper.encrypt."""
|
|
19
21
|
salted = plain + ":" + salt
|
|
20
22
|
return base64.b64encode(salted.encode("utf-8")).decode("utf-8")
|
|
21
23
|
|
|
22
24
|
@staticmethod
|
|
23
|
-
def decrypt(cipher: str, salt: str) -> str:
|
|
25
|
+
def decrypt(cipher: str, salt: str = "unused-salt") -> str:
|
|
24
26
|
"""Simulate the behavior of encryption_helper.decrypt."""
|
|
25
27
|
|
|
26
28
|
if len(cipher) == 0:
|
|
@@ -29,7 +31,7 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
29
31
|
"Parameter validation failed: Invalid length for parameter SecretId, value: 0, valid min length: 1"
|
|
30
32
|
)
|
|
31
33
|
decoded = base64.b64decode(cipher.encode("utf-8")).decode("utf-8")
|
|
32
|
-
plain, decrypted_salt = decoded.
|
|
34
|
+
plain, decrypted_salt = decoded.rsplit(":", 1)
|
|
33
35
|
if salt != decrypted_salt:
|
|
34
36
|
raise ValueError("Salt does not match")
|
|
35
37
|
return plain
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splunk-soar-sdk
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: The official framework for developing and testing Splunk SOAR Apps
|
|
5
5
|
Project-URL: Homepage, https://github.com/phantomcyber/splunk-soar-sdk
|
|
6
6
|
Project-URL: Documentation, https://github.com/phantomcyber/splunk-soar-sdk
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
soar_sdk/__init__.py,sha256=RzAng-ARqpK01SY82lNy4uYJFVG0yW6Q3CccEqbToJ4,726
|
|
2
|
-
soar_sdk/abstract.py,sha256=
|
|
2
|
+
soar_sdk/abstract.py,sha256=0iNvoPNX1DA7ZffYU5Tg7chuQBoiNOXMK1WumHA_1T4,7786
|
|
3
3
|
soar_sdk/action_results.py,sha256=ajlsed6ZkCyOTra4SFy31iYZO2a6mlvBK2-zIyO09q4,11928
|
|
4
|
-
soar_sdk/actions_manager.py,sha256=
|
|
5
|
-
soar_sdk/app.py,sha256=
|
|
4
|
+
soar_sdk/actions_manager.py,sha256=0moWQuKNGMFZqhiINtEoLw45SNHhFbC0jzZ9Puv6wgc,5820
|
|
5
|
+
soar_sdk/app.py,sha256=7pJ-t9uQFaIxCTfHXy9hLRRgnRz9y03bMBObCrsNe_g,35185
|
|
6
6
|
soar_sdk/app_cli_runner.py,sha256=tdglXCIRZS3dc3P18Tha7yUJQX9zIDxJFdST02LL9xY,11644
|
|
7
|
-
soar_sdk/app_client.py,sha256=
|
|
8
|
-
soar_sdk/asset.py,sha256=
|
|
7
|
+
soar_sdk/app_client.py,sha256=maoIR0NqACdjb-0noWIyjAz3LMTtvi4EBMO5dBclab0,6282
|
|
8
|
+
soar_sdk/asset.py,sha256=y9ar5yXA8AKCUuVXnLpRn4BYDklf0mxzB24Hw9q58QY,12255
|
|
9
|
+
soar_sdk/asset_state.py,sha256=u71oUfIH0LOZQ2bNkYsC2A_dZp3KA1JzMTd0ujDd3Qw,2102
|
|
9
10
|
soar_sdk/async_utils.py,sha256=6JtSDd_RKKT85TaUjxofZTKrZr24z74WSljkX47KZqg,1429
|
|
10
11
|
soar_sdk/colors.py,sha256=--i_iXqfyITUz4O95HMjfZQGbwFZ34bLmBhtfpXXqlQ,1095
|
|
11
12
|
soar_sdk/compat.py,sha256=-Z9i9azaW4w0ZGEX2GoukseDglLNRWiBsV7auJAyZbs,3061
|
|
12
13
|
soar_sdk/crypto.py,sha256=qiBMHUQqgn5lPI1DbujSj700s89FuLJrkQgCO9_eBn4,392
|
|
13
|
-
soar_sdk/exceptions.py,sha256=
|
|
14
|
+
soar_sdk/exceptions.py,sha256=413-AcIM7IMixoyVk_0yDaqsUhommb784uH5vSv18lU,2129
|
|
14
15
|
soar_sdk/field_utils.py,sha256=Jb0HteUPd-CtuDM7rNXVLy4uRxl419zeDxY_oOpU8GM,287
|
|
15
16
|
soar_sdk/input_spec.py,sha256=79MFGF2IkAAoWpHmZYP0VqUBu10SkvmF_RPmXkf8bQ4,4677
|
|
16
17
|
soar_sdk/logging.py,sha256=30cUxU7Tjf_OY4rUOrvjDoqPH7pcFydrdDbcG4ppY_s,11424
|
|
@@ -30,18 +31,20 @@ soar_sdk/app_templates/basic_app/logo_dark.svg,sha256=PTxIs_1CKK9ZY3v-K1QoGwaUng
|
|
|
30
31
|
soar_sdk/app_templates/basic_app/uv.lock,sha256=AfgaIBg88KH-0iyXpCXacXAwHYKm0c-on2gWXjV9L-Y,80216
|
|
31
32
|
soar_sdk/app_templates/basic_app/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
33
|
soar_sdk/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
-
soar_sdk/cli/cli.py,sha256=
|
|
34
|
-
soar_sdk/cli/path_utils.py,sha256=
|
|
34
|
+
soar_sdk/cli/cli.py,sha256=dMb9yPyYphvr9wtk2oG8fEw6n_p4WZ7liK5u6-H8ppg,998
|
|
35
|
+
soar_sdk/cli/path_utils.py,sha256=gNqvSlltDGvbydpOm5RKgjwLs7J9YTqqmyiBIXxaX7c,1087
|
|
35
36
|
soar_sdk/cli/utils.py,sha256=GNZLFAMH_BKQo2-D5GvweWxeuR7DfR8eMZS4-sJUggU,1441
|
|
36
37
|
soar_sdk/cli/init/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
38
|
soar_sdk/cli/init/cli.py,sha256=LWwGb_N5KPpKjnk8Kn3IppSrGsHQb65CvDn2NXPokAE,14618
|
|
38
39
|
soar_sdk/cli/manifests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
40
|
soar_sdk/cli/manifests/cli.py,sha256=cly5xVdj4bBIdZVMQPIWTXRgUfd1ON3qKO-76Fwql18,524
|
|
40
41
|
soar_sdk/cli/manifests/deserializers.py,sha256=TjcmPvcfr1auGkf3hBRwjVc0dSuuDNmH5jMOqXHz27s,16515
|
|
41
|
-
soar_sdk/cli/manifests/processors.py,sha256=
|
|
42
|
+
soar_sdk/cli/manifests/processors.py,sha256=uuW_5gCPgpUB3o5L3KMo_eqw9Jm5ps1ga4ZqiYLtEJE,5061
|
|
42
43
|
soar_sdk/cli/manifests/serializers.py,sha256=hSfOcBNhv7s437A7t5BM1NXG5dfjKmh_xbHXQTuBklA,3632
|
|
43
|
-
soar_sdk/cli/package/cli.py,sha256=
|
|
44
|
-
soar_sdk/cli/package/utils.py,sha256=
|
|
44
|
+
soar_sdk/cli/package/cli.py,sha256=l2AygRmUcvmiXbQhRxSnz8lEK6_nLgWoKpwxY44r9SE,9578
|
|
45
|
+
soar_sdk/cli/package/utils.py,sha256=2qLGpR77t6KJABg4q2iMc2ywOChW4W5D10Ct58v89Is,1711
|
|
46
|
+
soar_sdk/cli/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
|
+
soar_sdk/cli/test/cli.py,sha256=iDrthN8L7B1RplLhq0EI69MndaOhvAXn7bqv3XzlfpM,7655
|
|
45
48
|
soar_sdk/code_renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
49
|
soar_sdk/code_renderers/action_renderer.py,sha256=nPbB3SJLm4VDu_zuwY6vO_LgVg1tvW5ZTrzeS7JdyF8,16489
|
|
47
50
|
soar_sdk/code_renderers/app_renderer.py,sha256=OCNNhXc36Amcg1kUUayyyzWx_M2RY8W8wvy0ppujLaE,7791
|
|
@@ -56,9 +59,9 @@ soar_sdk/decorators/on_es_poll.py,sha256=5wiAcboG45ut7dLr0ZRcKOU01yIBfBrq00laGtv
|
|
|
56
59
|
soar_sdk/decorators/on_poll.py,sha256=H_aijFWKeZKwiS72Vsa9uolmj8sziGW2lZF6EhjEDjs,8276
|
|
57
60
|
soar_sdk/decorators/test_connectivity.py,sha256=tZt7w-BnZUpxCyixblXts4tsUp8z-Kmz-JGJ5i5LQs8,3564
|
|
58
61
|
soar_sdk/decorators/view_handler.py,sha256=_qjfk2nQxPwraldjRIl4DWdW-tvANJfdVDU_lLA3UvE,7075
|
|
59
|
-
soar_sdk/decorators/webhook.py,sha256=
|
|
62
|
+
soar_sdk/decorators/webhook.py,sha256=PE37CcGzGYykJpU9AVDVtajENLrOIUA0su28lVDQ76M,2366
|
|
60
63
|
soar_sdk/meta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
61
|
-
soar_sdk/meta/actions.py,sha256=
|
|
64
|
+
soar_sdk/meta/actions.py,sha256=UDf_0cZ8lsAhkyhDIWonneNshUG9pzciG1Go9e7NJ6k,2189
|
|
62
65
|
soar_sdk/meta/adapters.py,sha256=KjSYIUtkCz2eesA_vhsNCjfi5C-Uz71tbSuDIjhuB8U,1112
|
|
63
66
|
soar_sdk/meta/app.py,sha256=eZlM8GIY1B_o-RzJrRNCNVEQSx0sFupxZqCM7sIWGv4,2777
|
|
64
67
|
soar_sdk/meta/datatypes.py,sha256=piR-oBVAATiRciXSdVE7XaqjUZTgSaOvTEqcOcNvCS0,795
|
|
@@ -73,10 +76,10 @@ soar_sdk/models/vault_attachment.py,sha256=IQPX239OFClVfOKr9nHIu9Is55cXWBaOgM2lG
|
|
|
73
76
|
soar_sdk/models/view.py,sha256=frfbNdWfzc0XjiU3CY79zBJxvzUsgLdFmphVeZ6QqTc,777
|
|
74
77
|
soar_sdk/shims/phantom/action_result.py,sha256=yDiV2f3kt5G9UYejpe0JFeo651f3Uv-fTSoIlfg3DGg,1606
|
|
75
78
|
soar_sdk/shims/phantom/app.py,sha256=PpNj9FoXjyj6r5w9S2fpElKFS6EcBIqsnpaTSvnIzyI,303
|
|
76
|
-
soar_sdk/shims/phantom/base_connector.py,sha256
|
|
79
|
+
soar_sdk/shims/phantom/base_connector.py,sha256=85K1vlWXNPHPrHSYDLYiz0aOHD_dKUGHOra_FSSj17o,4873
|
|
77
80
|
soar_sdk/shims/phantom/connector_result.py,sha256=T6eDXdMyblWB0Xa3RW4ojuhy9wJmWZTpY8Oojl5sYYk,641
|
|
78
81
|
soar_sdk/shims/phantom/consts.py,sha256=eq6AIuDhb2Z-CJORwv98D3JbcIOW8CC673zx5dNPFKU,404
|
|
79
|
-
soar_sdk/shims/phantom/encryption_helper.py,sha256=
|
|
82
|
+
soar_sdk/shims/phantom/encryption_helper.py,sha256=20VqqSFuftjB8bMriP6mjgvYWpYqYZyokYzq_aydkqU,1503
|
|
80
83
|
soar_sdk/shims/phantom/install_info.py,sha256=hR3W8pKlNUVlE1jOwGT_o2f6uTGQknPG3GLRDHrGSUQ,994
|
|
81
84
|
soar_sdk/shims/phantom/json_keys.py,sha256=QgDO5qGlcNNqJBsolJQs0UOW1sa8xMYIubqfCXeP4C4,528
|
|
82
85
|
soar_sdk/shims/phantom/ph_ipc.py,sha256=RSL_qB64OSsy0B8AvqDh_biR9lqAHKdRkIj973UQfoU,1381
|
|
@@ -100,8 +103,8 @@ soar_sdk/views/components/pie_chart.py,sha256=LVTeHVJN6nf2vjUs9y7PDBhS0U1fKW750l
|
|
|
100
103
|
soar_sdk/webhooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
101
104
|
soar_sdk/webhooks/models.py,sha256=PG9SDs5xXqtFndm5C8AsJOTYXU5v_UTY7SpYosWT_CA,4542
|
|
102
105
|
soar_sdk/webhooks/routing.py,sha256=MnzbnIDy2uG_5mJzsTeX-NsE6QYvzyqEGbHmEFj-DG8,6900
|
|
103
|
-
splunk_soar_sdk-3.
|
|
104
|
-
splunk_soar_sdk-3.
|
|
105
|
-
splunk_soar_sdk-3.
|
|
106
|
-
splunk_soar_sdk-3.
|
|
107
|
-
splunk_soar_sdk-3.
|
|
106
|
+
splunk_soar_sdk-3.3.1.dist-info/METADATA,sha256=3Y2eWE1VKaxWd8ZDzrvflA5jt2ZCX6LdwR2JRUe7VeY,7334
|
|
107
|
+
splunk_soar_sdk-3.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
108
|
+
splunk_soar_sdk-3.3.1.dist-info/entry_points.txt,sha256=CgBjo2ZWpYNkt9TgvToL26h2Tg1yt8FbvYTb5NVgNuc,51
|
|
109
|
+
splunk_soar_sdk-3.3.1.dist-info/licenses/LICENSE,sha256=gNCGrGhrSQb1PUzBOByVUN1tvaliwLZfna-QU2r2hQ8,11345
|
|
110
|
+
splunk_soar_sdk-3.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|