splunk-soar-sdk 2.3.7__py3-none-any.whl → 3.1.0__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 +38 -41
- soar_sdk/action_results.py +41 -18
- soar_sdk/actions_manager.py +5 -7
- soar_sdk/apis/utils.py +3 -3
- soar_sdk/apis/vault.py +10 -10
- soar_sdk/app.py +58 -51
- soar_sdk/app_cli_runner.py +8 -8
- soar_sdk/app_client.py +10 -10
- soar_sdk/asset.py +45 -33
- soar_sdk/async_utils.py +2 -2
- soar_sdk/cli/init/cli.py +7 -9
- soar_sdk/cli/manifests/deserializers.py +15 -15
- soar_sdk/cli/manifests/processors.py +4 -10
- soar_sdk/cli/manifests/serializers.py +16 -8
- soar_sdk/cli/package/cli.py +6 -6
- soar_sdk/cli/package/utils.py +1 -1
- soar_sdk/code_renderers/action_renderer.py +35 -18
- soar_sdk/code_renderers/app_renderer.py +1 -2
- soar_sdk/code_renderers/asset_renderer.py +4 -5
- soar_sdk/code_renderers/renderer.py +2 -2
- soar_sdk/code_renderers/templates/pyproject.toml.jinja +1 -1
- soar_sdk/compat.py +6 -6
- soar_sdk/decorators/action.py +14 -15
- soar_sdk/decorators/make_request.py +4 -3
- soar_sdk/decorators/on_poll.py +5 -4
- soar_sdk/decorators/test_connectivity.py +2 -2
- soar_sdk/decorators/view_handler.py +11 -17
- soar_sdk/decorators/webhook.py +1 -2
- soar_sdk/exceptions.py +1 -4
- soar_sdk/field_utils.py +8 -0
- soar_sdk/input_spec.py +13 -17
- soar_sdk/logging.py +3 -3
- soar_sdk/meta/actions.py +6 -22
- soar_sdk/meta/app.py +10 -7
- soar_sdk/meta/dependencies.py +48 -42
- soar_sdk/meta/webhooks.py +12 -12
- soar_sdk/models/artifact.py +20 -23
- soar_sdk/models/container.py +30 -33
- soar_sdk/models/finding.py +54 -0
- soar_sdk/models/vault_attachment.py +6 -6
- soar_sdk/models/view.py +10 -13
- soar_sdk/params.py +57 -39
- soar_sdk/shims/phantom/action_result.py +4 -4
- soar_sdk/shims/phantom/base_connector.py +5 -5
- soar_sdk/shims/phantom/ph_ipc.py +3 -3
- soar_sdk/shims/phantom/vault.py +35 -34
- soar_sdk/types.py +3 -2
- soar_sdk/views/template_filters.py +4 -4
- soar_sdk/views/template_renderer.py +2 -2
- soar_sdk/views/view_parser.py +3 -4
- soar_sdk/webhooks/models.py +7 -6
- soar_sdk/webhooks/routing.py +4 -3
- {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/METADATA +5 -6
- splunk_soar_sdk-3.1.0.dist-info/RECORD +105 -0
- splunk_soar_sdk-2.3.7.dist-info/RECORD +0 -103
- {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/WHEEL +0 -0
- {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/entry_points.txt +0 -0
- {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/licenses/LICENSE +0 -0
soar_sdk/abstract.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from typing import Any,
|
|
2
|
+
from typing import Any, Generic, TypeVar
|
|
3
3
|
from collections.abc import Mapping, Iterable, AsyncIterable
|
|
4
4
|
|
|
5
5
|
from soar_sdk.apis.vault import Vault
|
|
@@ -8,9 +8,9 @@ from soar_sdk.apis.container import Container
|
|
|
8
8
|
from soar_sdk.action_results import ActionOutput
|
|
9
9
|
import httpx
|
|
10
10
|
from pydantic.dataclasses import dataclass
|
|
11
|
-
from pydantic import
|
|
11
|
+
from pydantic import field_validator
|
|
12
12
|
|
|
13
|
-
JSONType =
|
|
13
|
+
JSONType = dict[str, Any] | list[Any] | str | int | float | bool | None
|
|
14
14
|
SummaryType = TypeVar("SummaryType", bound=ActionOutput)
|
|
15
15
|
|
|
16
16
|
|
|
@@ -23,7 +23,8 @@ class SOARClientAuth:
|
|
|
23
23
|
password: str = ""
|
|
24
24
|
user_session_token: str = ""
|
|
25
25
|
|
|
26
|
-
@
|
|
26
|
+
@field_validator("base_url")
|
|
27
|
+
@classmethod
|
|
27
28
|
def validate_phantom_url(cls, value: str) -> str:
|
|
28
29
|
"""Validate and format the base URL for the SOAR API."""
|
|
29
30
|
return (
|
|
@@ -74,13 +75,13 @@ class SOARClient(Generic[SummaryType]):
|
|
|
74
75
|
self,
|
|
75
76
|
endpoint: str,
|
|
76
77
|
*,
|
|
77
|
-
params:
|
|
78
|
-
headers:
|
|
79
|
-
cookies:
|
|
80
|
-
timeout:
|
|
81
|
-
auth:
|
|
78
|
+
params: dict[str, Any] | httpx.QueryParams | None = None,
|
|
79
|
+
headers: dict[str, str] | None = None,
|
|
80
|
+
cookies: dict[str, str] | None = None,
|
|
81
|
+
timeout: httpx.Timeout | None = None,
|
|
82
|
+
auth: httpx.Auth | tuple[str, str] | None = None,
|
|
82
83
|
follow_redirects: bool = False,
|
|
83
|
-
extensions:
|
|
84
|
+
extensions: Mapping[str, Any] | None = None,
|
|
84
85
|
) -> httpx.Response:
|
|
85
86
|
"""Perform a GET request to the specific endpoint using the SOAR client."""
|
|
86
87
|
response = self.client.get(
|
|
@@ -100,19 +101,17 @@ class SOARClient(Generic[SummaryType]):
|
|
|
100
101
|
self,
|
|
101
102
|
endpoint: str,
|
|
102
103
|
*,
|
|
103
|
-
content:
|
|
104
|
-
|
|
105
|
-
] = None,
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
|
|
113
|
-
timeout: Optional[Union[float, httpx.Timeout]] = None,
|
|
104
|
+
content: str | bytes | Iterable[bytes] | AsyncIterable[bytes] | None = None,
|
|
105
|
+
data: Mapping[str, Any] | None = None,
|
|
106
|
+
files: dict[str, Any] | None = None,
|
|
107
|
+
json: JSONType | None = None,
|
|
108
|
+
params: dict[str, Any] | None = None,
|
|
109
|
+
headers: dict[str, str] | None = None,
|
|
110
|
+
cookies: dict[str, str] | None = None,
|
|
111
|
+
auth: httpx.Auth | tuple[str, str] | None = None,
|
|
112
|
+
timeout: float | httpx.Timeout | None = None,
|
|
114
113
|
follow_redirects: bool = True,
|
|
115
|
-
extensions:
|
|
114
|
+
extensions: Mapping[str, Any] | None = None,
|
|
116
115
|
) -> httpx.Response:
|
|
117
116
|
"""Perform a POST request to the specific endpoint using the SOAR client."""
|
|
118
117
|
headers = headers or {}
|
|
@@ -138,19 +137,17 @@ class SOARClient(Generic[SummaryType]):
|
|
|
138
137
|
self,
|
|
139
138
|
endpoint: str,
|
|
140
139
|
*,
|
|
141
|
-
content:
|
|
142
|
-
|
|
143
|
-
] = None,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
|
|
151
|
-
timeout: Optional[Union[float, httpx.Timeout]] = None,
|
|
140
|
+
content: str | bytes | Iterable[bytes] | AsyncIterable[bytes] | None = None,
|
|
141
|
+
data: Mapping[str, Any] | None = None,
|
|
142
|
+
files: dict[str, Any] | None = None,
|
|
143
|
+
json: JSONType | None = None,
|
|
144
|
+
params: dict[str, Any] | None = None,
|
|
145
|
+
headers: dict[str, str] | None = None,
|
|
146
|
+
cookies: dict[str, str] | None = None,
|
|
147
|
+
auth: httpx.Auth | tuple[str, str] | None = None,
|
|
148
|
+
timeout: float | httpx.Timeout | None = None,
|
|
152
149
|
follow_redirects: bool = True,
|
|
153
|
-
extensions:
|
|
150
|
+
extensions: Mapping[str, Any] | None = None,
|
|
154
151
|
) -> httpx.Response:
|
|
155
152
|
"""Perform a PUT request to the specific endpoint using the SOAR client."""
|
|
156
153
|
headers = headers or {}
|
|
@@ -176,13 +173,13 @@ class SOARClient(Generic[SummaryType]):
|
|
|
176
173
|
self,
|
|
177
174
|
endpoint: str,
|
|
178
175
|
*,
|
|
179
|
-
params:
|
|
180
|
-
headers:
|
|
181
|
-
cookies:
|
|
182
|
-
auth:
|
|
183
|
-
timeout:
|
|
176
|
+
params: dict[str, Any] | httpx.QueryParams | None = None,
|
|
177
|
+
headers: dict[str, str] | None = None,
|
|
178
|
+
cookies: dict[str, str] | None = None,
|
|
179
|
+
auth: httpx.Auth | tuple[str, str] | None = None,
|
|
180
|
+
timeout: httpx.Timeout | None = None,
|
|
184
181
|
follow_redirects: bool = False,
|
|
185
|
-
extensions:
|
|
182
|
+
extensions: Mapping[str, Any] | None = None,
|
|
186
183
|
) -> httpx.Response:
|
|
187
184
|
"""Perform a DELETE request to the specific endpoint using the SOAR client."""
|
|
188
185
|
headers = headers or {}
|
|
@@ -229,7 +226,7 @@ class SOARClient(Generic[SummaryType]):
|
|
|
229
226
|
pass
|
|
230
227
|
|
|
231
228
|
@abstractmethod
|
|
232
|
-
def get_summary(self) ->
|
|
229
|
+
def get_summary(self) -> SummaryType | None:
|
|
233
230
|
"""Get the summary for the action run."""
|
|
234
231
|
pass
|
|
235
232
|
|
soar_sdk/action_results.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Union, get_origin, get_args, Any
|
|
2
2
|
from collections.abc import Iterator
|
|
3
|
-
from typing_extensions import
|
|
4
|
-
from
|
|
3
|
+
from typing_extensions import TypedDict
|
|
4
|
+
from typing import NotRequired
|
|
5
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
5
6
|
import itertools
|
|
7
|
+
import types
|
|
6
8
|
|
|
7
9
|
from soar_sdk.compat import remove_when_soar_newer_than
|
|
8
10
|
from soar_sdk.shims.phantom.action_result import ActionResult as PhantomActionResult
|
|
9
11
|
from soar_sdk.meta.datatypes import as_datatype
|
|
12
|
+
from soar_sdk.field_utils import parse_json_schema_extra
|
|
10
13
|
|
|
11
14
|
remove_when_soar_newer_than(
|
|
12
15
|
"7.0.0", "NotRequired from typing_extensions is in typing in Python 3.11+"
|
|
@@ -40,7 +43,7 @@ class ActionResult(PhantomActionResult):
|
|
|
40
43
|
self,
|
|
41
44
|
status: bool,
|
|
42
45
|
message: str,
|
|
43
|
-
param:
|
|
46
|
+
param: dict | None = None,
|
|
44
47
|
) -> None:
|
|
45
48
|
"""Initialize an ActionResult with status, message, and optional parameters.
|
|
46
49
|
|
|
@@ -82,16 +85,16 @@ class OutputFieldSpecification(TypedDict):
|
|
|
82
85
|
data_path: str
|
|
83
86
|
data_type: str
|
|
84
87
|
contains: NotRequired[list[str]]
|
|
85
|
-
example_values: NotRequired[list[
|
|
88
|
+
example_values: NotRequired[list[str | float | bool]]
|
|
86
89
|
column_name: NotRequired[str]
|
|
87
90
|
column_order: NotRequired[int]
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
def OutputField(
|
|
91
|
-
cef_types:
|
|
92
|
-
example_values:
|
|
93
|
-
alias:
|
|
94
|
-
column_name:
|
|
94
|
+
cef_types: list[str] | None = None,
|
|
95
|
+
example_values: list[str | float | bool] | None = None,
|
|
96
|
+
alias: str | None = None,
|
|
97
|
+
column_name: str | None = None,
|
|
95
98
|
) -> Any: # noqa: ANN401
|
|
96
99
|
"""Define metadata for an action output field.
|
|
97
100
|
|
|
@@ -121,11 +124,18 @@ def OutputField(
|
|
|
121
124
|
... )
|
|
122
125
|
... count: int = OutputField(example_values=[1, 5, 10])
|
|
123
126
|
"""
|
|
127
|
+
json_schema_extra: dict[str, Any] = {}
|
|
128
|
+
if cef_types is not None:
|
|
129
|
+
json_schema_extra["cef_types"] = cef_types
|
|
130
|
+
if example_values is not None:
|
|
131
|
+
json_schema_extra["examples"] = example_values
|
|
132
|
+
if column_name is not None:
|
|
133
|
+
json_schema_extra["column_name"] = column_name
|
|
134
|
+
|
|
124
135
|
return Field(
|
|
125
|
-
|
|
136
|
+
default=...,
|
|
126
137
|
alias=alias,
|
|
127
|
-
|
|
128
|
-
column_name=column_name,
|
|
138
|
+
json_schema_extra=json_schema_extra if json_schema_extra else None,
|
|
129
139
|
)
|
|
130
140
|
|
|
131
141
|
|
|
@@ -158,11 +168,14 @@ class ActionOutput(BaseModel):
|
|
|
158
168
|
Nested ActionOutput classes are supported for complex data structures.
|
|
159
169
|
"""
|
|
160
170
|
|
|
171
|
+
# Allow instantiation with both field names and aliases for backward compatibility
|
|
172
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
173
|
+
|
|
161
174
|
@classmethod
|
|
162
175
|
def _to_json_schema(
|
|
163
176
|
cls,
|
|
164
177
|
parent_datapath: str = "action_result.data.*",
|
|
165
|
-
column_order_counter:
|
|
178
|
+
column_order_counter: itertools.count | None = None,
|
|
166
179
|
) -> Iterator[OutputFieldSpecification]:
|
|
167
180
|
"""Convert the ActionOutput class to SOAR-compatible JSON schema.
|
|
168
181
|
|
|
@@ -191,15 +204,18 @@ class ActionOutput(BaseModel):
|
|
|
191
204
|
if column_order_counter is None:
|
|
192
205
|
column_order_counter = itertools.count()
|
|
193
206
|
|
|
194
|
-
for _field_name, field in cls.
|
|
207
|
+
for _field_name, field in cls.model_fields.items():
|
|
195
208
|
field_name = alias if (alias := field.alias) else _field_name
|
|
196
209
|
|
|
197
210
|
field_type = field.annotation
|
|
211
|
+
if field_type is None:
|
|
212
|
+
continue
|
|
213
|
+
|
|
198
214
|
datapath = parent_datapath + f".{field_name}"
|
|
199
215
|
|
|
200
216
|
# Handle lists and optional types, even nested ones
|
|
201
217
|
origin = get_origin(field_type)
|
|
202
|
-
while origin in [list, Union,
|
|
218
|
+
while origin in [list, Union, types.UnionType]:
|
|
203
219
|
type_args = [
|
|
204
220
|
arg
|
|
205
221
|
for arg in get_args(field_type)
|
|
@@ -221,6 +237,11 @@ class ActionOutput(BaseModel):
|
|
|
221
237
|
field_type = type_args[0]
|
|
222
238
|
origin = get_origin(field_type)
|
|
223
239
|
|
|
240
|
+
if not isinstance(field_type, type):
|
|
241
|
+
raise TypeError(
|
|
242
|
+
f"Output field {field_name} has invalid type annotation: {field_type}"
|
|
243
|
+
)
|
|
244
|
+
|
|
224
245
|
if issubclass(field_type, ActionOutput):
|
|
225
246
|
# If the field is another ActionOutput, recursively call _to_json_schema
|
|
226
247
|
yield from field_type._to_json_schema(datapath, column_order_counter)
|
|
@@ -237,15 +258,17 @@ class ActionOutput(BaseModel):
|
|
|
237
258
|
data_path=datapath, data_type=type_name
|
|
238
259
|
)
|
|
239
260
|
|
|
240
|
-
|
|
261
|
+
json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
|
|
262
|
+
|
|
263
|
+
if cef_types := json_schema_extra.get("cef_types"):
|
|
241
264
|
schema_field["contains"] = cef_types
|
|
242
|
-
if examples :=
|
|
265
|
+
if examples := json_schema_extra.get("examples"):
|
|
243
266
|
schema_field["example_values"] = examples
|
|
244
267
|
|
|
245
268
|
if field_type is bool:
|
|
246
269
|
schema_field["example_values"] = [True, False]
|
|
247
270
|
|
|
248
|
-
column_name =
|
|
271
|
+
column_name = json_schema_extra.get("column_name")
|
|
249
272
|
|
|
250
273
|
if column_name is not None:
|
|
251
274
|
schema_field["column_name"] = column_name
|
soar_sdk/actions_manager.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Any
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
from soar_sdk.compat import remove_when_soar_newer_than
|
|
@@ -31,7 +31,7 @@ class ActionsManager(BaseConnector):
|
|
|
31
31
|
self.auth_state: dict = {}
|
|
32
32
|
self.asset_cache: dict = {}
|
|
33
33
|
|
|
34
|
-
def get_action(self, identifier: str) ->
|
|
34
|
+
def get_action(self, identifier: str) -> Action | None:
|
|
35
35
|
"""Convenience method for getting an Action callable from its identifier.
|
|
36
36
|
|
|
37
37
|
Returns None if there are no actions managed by this object matching the given
|
|
@@ -60,14 +60,12 @@ class ActionsManager(BaseConnector):
|
|
|
60
60
|
"""
|
|
61
61
|
self._actions[action_identifier] = wrapped_function
|
|
62
62
|
|
|
63
|
-
def handle(
|
|
64
|
-
self, input_data: InputSpecification, handle: Optional[int] = None
|
|
65
|
-
) -> str:
|
|
63
|
+
def handle(self, input_data: InputSpecification, handle: int | None = None) -> str:
|
|
66
64
|
"""Runs handling of the input data on connector."""
|
|
67
65
|
action_id = input_data.identifier
|
|
68
66
|
if self.get_action(action_id):
|
|
69
67
|
self.print_progress_message = True
|
|
70
|
-
return self._handle_action(input_data.
|
|
68
|
+
return self._handle_action(input_data.model_dump_json(), handle or 0)
|
|
71
69
|
else:
|
|
72
70
|
raise RuntimeError(
|
|
73
71
|
f"Action {action_id} not recognized"
|
|
@@ -86,7 +84,7 @@ class ActionsManager(BaseConnector):
|
|
|
86
84
|
|
|
87
85
|
if handler := self.get_action(action_id):
|
|
88
86
|
try:
|
|
89
|
-
params = handler.meta.parameters.
|
|
87
|
+
params = handler.meta.parameters.model_validate(param)
|
|
90
88
|
except (ValueError, ValidationError) as e:
|
|
91
89
|
self.save_progress(
|
|
92
90
|
f"Validation Error - the params data for action could not be parsed: {e!s}"
|
soar_sdk/apis/utils.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import httpx
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Any
|
|
3
3
|
from collections.abc import Generator
|
|
4
4
|
|
|
5
5
|
|
|
@@ -11,9 +11,9 @@ def is_client_authenticated(client: httpx.Client) -> bool:
|
|
|
11
11
|
def get_request_iter_pages(
|
|
12
12
|
client: httpx.Client,
|
|
13
13
|
endpoint: str,
|
|
14
|
-
params:
|
|
14
|
+
params: dict | None = None,
|
|
15
15
|
page_size: int = 50,
|
|
16
|
-
) -> Generator[Any
|
|
16
|
+
) -> Generator[Any]:
|
|
17
17
|
"""Iterate through REST JSON results using the provided paging."""
|
|
18
18
|
params = params or {}
|
|
19
19
|
|
soar_sdk/apis/vault.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
2
|
from soar_sdk.models.vault_attachment import VaultAttachment
|
|
3
3
|
from soar_sdk.shims.phantom.vault import PhantomVault, VaultBase
|
|
4
4
|
|
|
@@ -47,9 +47,9 @@ class Vault:
|
|
|
47
47
|
def create_attachment(
|
|
48
48
|
self,
|
|
49
49
|
container_id: int,
|
|
50
|
-
file_content:
|
|
50
|
+
file_content: str | bytes,
|
|
51
51
|
file_name: str,
|
|
52
|
-
metadata:
|
|
52
|
+
metadata: dict[str, str] | None = None,
|
|
53
53
|
) -> str:
|
|
54
54
|
"""Create a vault attachment from file content.
|
|
55
55
|
|
|
@@ -87,7 +87,7 @@ class Vault:
|
|
|
87
87
|
container_id: int,
|
|
88
88
|
file_location: str,
|
|
89
89
|
file_name: str,
|
|
90
|
-
metadata:
|
|
90
|
+
metadata: dict[str, str] | None = None,
|
|
91
91
|
) -> str:
|
|
92
92
|
"""Add an existing file to the vault as an attachment.
|
|
93
93
|
|
|
@@ -121,9 +121,9 @@ class Vault:
|
|
|
121
121
|
|
|
122
122
|
def get_attachment(
|
|
123
123
|
self,
|
|
124
|
-
vault_id:
|
|
125
|
-
file_name:
|
|
126
|
-
container_id:
|
|
124
|
+
vault_id: str | None = None,
|
|
125
|
+
file_name: str | None = None,
|
|
126
|
+
container_id: int | None = None,
|
|
127
127
|
) -> list[VaultAttachment]:
|
|
128
128
|
"""Retrieve attachment(s) from the vault.
|
|
129
129
|
|
|
@@ -161,9 +161,9 @@ class Vault:
|
|
|
161
161
|
|
|
162
162
|
def delete_attachment(
|
|
163
163
|
self,
|
|
164
|
-
vault_id:
|
|
165
|
-
file_name:
|
|
166
|
-
container_id:
|
|
164
|
+
vault_id: str | None = None,
|
|
165
|
+
file_name: str | None = None,
|
|
166
|
+
container_id: int | None = None,
|
|
167
167
|
remove_all: bool = False,
|
|
168
168
|
) -> list[str]:
|
|
169
169
|
"""Delete attachment(s) from the vault.
|
soar_sdk/app.py
CHANGED
|
@@ -3,7 +3,8 @@ import inspect
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
import sys
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from collections.abc import Iterator
|
|
8
9
|
from zoneinfo import ZoneInfo
|
|
9
10
|
|
|
@@ -101,7 +102,7 @@ class App:
|
|
|
101
102
|
product_name: str,
|
|
102
103
|
publisher: str,
|
|
103
104
|
appid: str,
|
|
104
|
-
python_version:
|
|
105
|
+
python_version: list[PythonVersion] | str | None = None,
|
|
105
106
|
min_phantom_version: str = MIN_PHANTOM_VERSION,
|
|
106
107
|
fips_compliant: bool = False,
|
|
107
108
|
asset_cls: type[BaseAsset] = BaseAsset,
|
|
@@ -164,13 +165,13 @@ class App:
|
|
|
164
165
|
runner = AppCliRunner(self)
|
|
165
166
|
runner.run()
|
|
166
167
|
|
|
167
|
-
def handle(self, raw_input_data: str, handle:
|
|
168
|
+
def handle(self, raw_input_data: str, handle: int | None = None) -> str:
|
|
168
169
|
"""Runs handling of the input data on connector.
|
|
169
170
|
|
|
170
171
|
NOTE: handle is actually a pointer address to spawn's internal state.
|
|
171
172
|
In versions of SOAR >6.4.1, handle will not be passed to the app.
|
|
172
173
|
"""
|
|
173
|
-
input_data = InputSpecification.
|
|
174
|
+
input_data = InputSpecification.model_validate(json.loads(raw_input_data))
|
|
174
175
|
self._raw_asset_config = input_data.config.get_asset_config()
|
|
175
176
|
|
|
176
177
|
# Decrypt sensitive fields in the asset configuration
|
|
@@ -221,27 +222,27 @@ class App:
|
|
|
221
222
|
def asset(self) -> BaseAsset:
|
|
222
223
|
"""Returns the asset instance for the app."""
|
|
223
224
|
if not hasattr(self, "_asset"):
|
|
224
|
-
self._asset = self.asset_cls.
|
|
225
|
+
self._asset = self.asset_cls.model_validate(self._raw_asset_config)
|
|
225
226
|
return self._asset
|
|
226
227
|
|
|
227
228
|
def register_action(
|
|
228
229
|
self,
|
|
229
230
|
/,
|
|
230
|
-
action:
|
|
231
|
+
action: str | Callable,
|
|
231
232
|
*,
|
|
232
|
-
name:
|
|
233
|
-
identifier:
|
|
234
|
-
description:
|
|
233
|
+
name: str | None = None,
|
|
234
|
+
identifier: str | None = None,
|
|
235
|
+
description: str | None = None,
|
|
235
236
|
verbose: str = "",
|
|
236
237
|
action_type: str = "generic", # TODO: consider introducing enum type for that
|
|
237
238
|
read_only: bool = True,
|
|
238
|
-
params_class:
|
|
239
|
-
output_class:
|
|
240
|
-
render_as:
|
|
241
|
-
view_handler:
|
|
242
|
-
view_template:
|
|
239
|
+
params_class: type[Params] | None = None,
|
|
240
|
+
output_class: type[ActionOutput] | None = None,
|
|
241
|
+
render_as: str | None = None,
|
|
242
|
+
view_handler: str | Callable | None = None,
|
|
243
|
+
view_template: str | None = None,
|
|
243
244
|
versions: str = "EQ(*)",
|
|
244
|
-
summary_type:
|
|
245
|
+
summary_type: type[ActionOutput] | None = None,
|
|
245
246
|
enable_concurrency_lock: bool = False,
|
|
246
247
|
) -> Action:
|
|
247
248
|
"""Dynamically register an action function defined in another module.
|
|
@@ -415,18 +416,18 @@ class App:
|
|
|
415
416
|
def action(
|
|
416
417
|
self,
|
|
417
418
|
*,
|
|
418
|
-
name:
|
|
419
|
-
identifier:
|
|
420
|
-
description:
|
|
419
|
+
name: str | None = None,
|
|
420
|
+
identifier: str | None = None,
|
|
421
|
+
description: str | None = None,
|
|
421
422
|
verbose: str = "",
|
|
422
423
|
action_type: str = "generic", # TODO: consider introducing enum type for that
|
|
423
424
|
read_only: bool = True,
|
|
424
|
-
params_class:
|
|
425
|
-
output_class:
|
|
426
|
-
render_as:
|
|
427
|
-
view_handler:
|
|
425
|
+
params_class: type[Params] | None = None,
|
|
426
|
+
output_class: type[ActionOutput] | None = None,
|
|
427
|
+
render_as: str | None = None,
|
|
428
|
+
view_handler: Callable | None = None,
|
|
428
429
|
versions: str = "EQ(*)",
|
|
429
|
-
summary_type:
|
|
430
|
+
summary_type: type[ActionOutput] | None = None,
|
|
430
431
|
enable_concurrency_lock: bool = False,
|
|
431
432
|
) -> ActionDecorator:
|
|
432
433
|
"""Decorator for registering an action function.
|
|
@@ -497,7 +498,7 @@ class App:
|
|
|
497
498
|
def view_handler(
|
|
498
499
|
self,
|
|
499
500
|
*,
|
|
500
|
-
template:
|
|
501
|
+
template: str | None = None,
|
|
501
502
|
) -> ViewHandlerDecorator:
|
|
502
503
|
"""Decorator for custom view functions with output parsing and template rendering.
|
|
503
504
|
|
|
@@ -525,7 +526,7 @@ class App:
|
|
|
525
526
|
return ViewHandlerDecorator(self, template=template)
|
|
526
527
|
|
|
527
528
|
def make_request(
|
|
528
|
-
self, output_class:
|
|
529
|
+
self, output_class: type[ActionOutput] | None = None
|
|
529
530
|
) -> MakeRequestDecorator:
|
|
530
531
|
"""Decorator for registering a ``make request`` action function.
|
|
531
532
|
|
|
@@ -556,7 +557,7 @@ class App:
|
|
|
556
557
|
def _validate_params_class(
|
|
557
558
|
action_name: str,
|
|
558
559
|
spec: inspect.FullArgSpec,
|
|
559
|
-
params_class:
|
|
560
|
+
params_class: type[Params] | None = None,
|
|
560
561
|
) -> type[Params]:
|
|
561
562
|
"""Validates the class used for params argument of the action.
|
|
562
563
|
|
|
@@ -572,7 +573,7 @@ class App:
|
|
|
572
573
|
"Action function must accept at least the params positional argument"
|
|
573
574
|
)
|
|
574
575
|
params_arg = spec.args[0]
|
|
575
|
-
annotated_params_type:
|
|
576
|
+
annotated_params_type: type | None = spec.annotations.get(params_arg)
|
|
576
577
|
if annotated_params_type is None:
|
|
577
578
|
raise TypeError(
|
|
578
579
|
f"Action {action_name} has no params type set. "
|
|
@@ -594,7 +595,7 @@ class App:
|
|
|
594
595
|
"""
|
|
595
596
|
# The reason we wrap values in callables is to avoid evaluating any lazy attributes
|
|
596
597
|
# (like asset) unless they're actually going to be used in the action function.
|
|
597
|
-
magic_args: dict[str,
|
|
598
|
+
magic_args: dict[str, object | Callable[[], object]] = {
|
|
598
599
|
"soar": self.soar_client,
|
|
599
600
|
"asset": lambda: self.asset,
|
|
600
601
|
}
|
|
@@ -626,18 +627,16 @@ class App:
|
|
|
626
627
|
|
|
627
628
|
@staticmethod
|
|
628
629
|
def _adapt_action_result(
|
|
629
|
-
result:
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
bool,
|
|
636
|
-
],
|
|
630
|
+
result: ActionOutput
|
|
631
|
+
| ActionResult
|
|
632
|
+
| list[ActionOutput]
|
|
633
|
+
| Iterator[ActionOutput]
|
|
634
|
+
| tuple[bool, str]
|
|
635
|
+
| bool,
|
|
637
636
|
actions_manager: ActionsManager,
|
|
638
|
-
action_params:
|
|
637
|
+
action_params: Params | None = None,
|
|
639
638
|
message: str = "",
|
|
640
|
-
summary:
|
|
639
|
+
summary: ActionOutput | None = None,
|
|
641
640
|
) -> bool:
|
|
642
641
|
"""Handles multiple ways of returning response from action.
|
|
643
642
|
|
|
@@ -657,13 +656,21 @@ class App:
|
|
|
657
656
|
)
|
|
658
657
|
# Handle empty list/iterator case
|
|
659
658
|
if not statuses:
|
|
660
|
-
|
|
659
|
+
# Create ActionResult directly for empty list
|
|
660
|
+
param_dict = action_params.model_dump() if action_params else None
|
|
661
|
+
result = ActionResult(
|
|
662
|
+
status=True,
|
|
663
|
+
message=message,
|
|
664
|
+
param=param_dict,
|
|
665
|
+
)
|
|
666
|
+
if summary:
|
|
667
|
+
result.set_summary(summary.model_dump(by_alias=True))
|
|
661
668
|
else:
|
|
662
669
|
return all(statuses)
|
|
663
670
|
|
|
664
671
|
if isinstance(result, ActionOutput):
|
|
665
|
-
output_dict = result.
|
|
666
|
-
param_dict = action_params.
|
|
672
|
+
output_dict = result.model_dump(by_alias=True)
|
|
673
|
+
param_dict = action_params.model_dump() if action_params else None
|
|
667
674
|
|
|
668
675
|
result = ActionResult(
|
|
669
676
|
status=True,
|
|
@@ -672,7 +679,7 @@ class App:
|
|
|
672
679
|
)
|
|
673
680
|
result.add_data(output_dict)
|
|
674
681
|
if summary:
|
|
675
|
-
result.set_summary(summary.
|
|
682
|
+
result.set_summary(summary.model_dump(by_alias=True))
|
|
676
683
|
|
|
677
684
|
if isinstance(result, ActionResult):
|
|
678
685
|
actions_manager.add_result(result)
|
|
@@ -695,14 +702,14 @@ class App:
|
|
|
695
702
|
|
|
696
703
|
pytest.mark.skip(inner)
|
|
697
704
|
|
|
698
|
-
webhook_meta:
|
|
699
|
-
webhook_router:
|
|
705
|
+
webhook_meta: WebhookMeta | None = None
|
|
706
|
+
webhook_router: Router | None = None
|
|
700
707
|
|
|
701
708
|
def enable_webhooks(
|
|
702
709
|
self,
|
|
703
710
|
default_requires_auth: bool = True,
|
|
704
|
-
default_allowed_headers:
|
|
705
|
-
default_ip_allowlist:
|
|
711
|
+
default_allowed_headers: list[str] | None = None,
|
|
712
|
+
default_ip_allowlist: list[str] | None = None,
|
|
706
713
|
) -> "App":
|
|
707
714
|
"""Enable webhook functionality for the app.
|
|
708
715
|
|
|
@@ -731,7 +738,7 @@ class App:
|
|
|
731
738
|
default_ip_allowlist = ["0.0.0.0/0", "::/0"]
|
|
732
739
|
|
|
733
740
|
self.webhook_meta = WebhookMeta(
|
|
734
|
-
handler=None,
|
|
741
|
+
handler=None,
|
|
735
742
|
requires_auth=default_requires_auth,
|
|
736
743
|
allowed_headers=default_allowed_headers,
|
|
737
744
|
ip_allowlist=default_ip_allowlist,
|
|
@@ -742,7 +749,7 @@ class App:
|
|
|
742
749
|
return self
|
|
743
750
|
|
|
744
751
|
def webhook(
|
|
745
|
-
self, url_pattern: str, allowed_methods:
|
|
752
|
+
self, url_pattern: str, allowed_methods: list[str] | None = None
|
|
746
753
|
) -> WebhookDecorator:
|
|
747
754
|
"""Decorator for registering a webhook handler."""
|
|
748
755
|
return WebhookDecorator(self, url_pattern, allowed_methods)
|
|
@@ -752,8 +759,8 @@ class App:
|
|
|
752
759
|
method: str,
|
|
753
760
|
headers: dict[str, str],
|
|
754
761
|
path_parts: list[str],
|
|
755
|
-
query: dict[str,
|
|
756
|
-
body:
|
|
762
|
+
query: dict[str, str | list[str] | None],
|
|
763
|
+
body: str | None,
|
|
757
764
|
asset: dict,
|
|
758
765
|
soar_rest_client: SoarRestClient,
|
|
759
766
|
) -> dict:
|
|
@@ -801,4 +808,4 @@ class App:
|
|
|
801
808
|
raise TypeError(
|
|
802
809
|
f"Webhook handler must return a WebhookResponse, got {type(response)}"
|
|
803
810
|
)
|
|
804
|
-
return response.
|
|
811
|
+
return response.model_dump()
|