splunk-soar-sdk 2.3.7__py3-none-any.whl → 3.0.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/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.0.0.dist-info}/METADATA +5 -6
- splunk_soar_sdk-3.0.0.dist-info/RECORD +104 -0
- splunk_soar_sdk-2.3.7.dist-info/RECORD +0 -103
- {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.0.0.dist-info}/WHEEL +0 -0
- {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.0.0.dist-info}/entry_points.txt +0 -0
- {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.0.0.dist-info}/licenses/LICENSE +0 -0
soar_sdk/app_cli_runner.py
CHANGED
|
@@ -3,7 +3,7 @@ import inspect
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
import typing
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import Any
|
|
7
7
|
import os
|
|
8
8
|
from pydantic import ValidationError
|
|
9
9
|
from urllib.parse import urlparse, parse_qs
|
|
@@ -31,7 +31,7 @@ class AppCliRunner:
|
|
|
31
31
|
def __init__(self, app: "App") -> None:
|
|
32
32
|
self.app = app
|
|
33
33
|
|
|
34
|
-
def parse_args(self, argv:
|
|
34
|
+
def parse_args(self, argv: list[str] | None = None) -> argparse.Namespace:
|
|
35
35
|
"""Parse command line arguments for the app CLI runner."""
|
|
36
36
|
root_parser = argparse.ArgumentParser()
|
|
37
37
|
root_parser.add_argument(
|
|
@@ -162,13 +162,13 @@ class AppCliRunner:
|
|
|
162
162
|
)
|
|
163
163
|
|
|
164
164
|
try:
|
|
165
|
-
param = chosen_action.params_class.
|
|
165
|
+
param = chosen_action.params_class.model_validate(params_json)
|
|
166
166
|
except Exception as e:
|
|
167
167
|
root_parser.error(
|
|
168
168
|
f"Unable to parse parameter JSON file {params_file}:\n{e}"
|
|
169
169
|
)
|
|
170
170
|
|
|
171
|
-
parameter_list.append(ActionParameter(**param.
|
|
171
|
+
parameter_list.append(ActionParameter(**param.model_dump()))
|
|
172
172
|
|
|
173
173
|
input_data = InputSpecification(
|
|
174
174
|
action=args.identifier,
|
|
@@ -191,7 +191,7 @@ class AppCliRunner:
|
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
input_data.config = AppConfig(
|
|
194
|
-
**input_data.config.
|
|
194
|
+
**input_data.config.model_dump(),
|
|
195
195
|
**asset_json, # Merge asset JSON into config
|
|
196
196
|
)
|
|
197
197
|
|
|
@@ -207,7 +207,7 @@ class AppCliRunner:
|
|
|
207
207
|
except ValidationError as e:
|
|
208
208
|
root_parser.error(f"Provided soar auth arguments are invalid: {e}.")
|
|
209
209
|
|
|
210
|
-
args.raw_input_data = input_data.
|
|
210
|
+
args.raw_input_data = input_data.model_dump_json()
|
|
211
211
|
|
|
212
212
|
def _parse_webhook_args(
|
|
213
213
|
self,
|
|
@@ -250,14 +250,14 @@ class AppCliRunner:
|
|
|
250
250
|
path_parts=path_parts,
|
|
251
251
|
query=query,
|
|
252
252
|
body=args.data,
|
|
253
|
-
asset=self.app.asset_cls.
|
|
253
|
+
asset=self.app.asset_cls.model_validate(asset_json),
|
|
254
254
|
soar_base_url=soar_base_url,
|
|
255
255
|
soar_auth_token=soar_auth_token,
|
|
256
256
|
asset_id=args.asset_id,
|
|
257
257
|
)
|
|
258
258
|
print(f"Parsed webhook request: {args.webhook_request}")
|
|
259
259
|
|
|
260
|
-
def run(self, argv:
|
|
260
|
+
def run(self, argv: list[str] | None = None) -> None:
|
|
261
261
|
"""Run the app CLI."""
|
|
262
262
|
args = self.parse_args(argv=argv)
|
|
263
263
|
|
soar_sdk/app_client.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import Any, TYPE_CHECKING
|
|
2
|
+
from typing import Any, TYPE_CHECKING
|
|
3
3
|
from collections.abc import Mapping
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
@@ -41,9 +41,9 @@ class AppClient(SOARClient[SummaryType]):
|
|
|
41
41
|
self._artifacts_api = Artifact(soar_client=self)
|
|
42
42
|
self._containers_api = Container(soar_client=self)
|
|
43
43
|
self._vault_api = Vault(soar_client=self)
|
|
44
|
-
self.basic_auth:
|
|
44
|
+
self.basic_auth: BasicAuth | None = None
|
|
45
45
|
|
|
46
|
-
self._summary:
|
|
46
|
+
self._summary: SummaryType | None = None
|
|
47
47
|
self._message: str = ""
|
|
48
48
|
self.__container_id: int = 0
|
|
49
49
|
self.__asset_id: str = ""
|
|
@@ -132,13 +132,13 @@ class AppClient(SOARClient[SummaryType]):
|
|
|
132
132
|
self,
|
|
133
133
|
endpoint: str,
|
|
134
134
|
*,
|
|
135
|
-
params:
|
|
136
|
-
headers:
|
|
137
|
-
cookies:
|
|
138
|
-
auth:
|
|
139
|
-
timeout:
|
|
135
|
+
params: dict[str, Any] | httpx.QueryParams | None = None,
|
|
136
|
+
headers: dict[str, str] | None = None,
|
|
137
|
+
cookies: dict[str, str] | None = None,
|
|
138
|
+
auth: httpx.Auth | tuple[str, str] | None = None,
|
|
139
|
+
timeout: httpx.Timeout | None = None,
|
|
140
140
|
follow_redirects: bool = False,
|
|
141
|
-
extensions:
|
|
141
|
+
extensions: Mapping[str, Any] | None = None,
|
|
142
142
|
) -> httpx.Response:
|
|
143
143
|
"""Perform a DELETE request to the specific endpoint using the SOAR client."""
|
|
144
144
|
headers = headers or {}
|
|
@@ -167,7 +167,7 @@ class AppClient(SOARClient[SummaryType]):
|
|
|
167
167
|
"""Set the message for the action result."""
|
|
168
168
|
self._message = message
|
|
169
169
|
|
|
170
|
-
def get_summary(self) ->
|
|
170
|
+
def get_summary(self) -> SummaryType | None:
|
|
171
171
|
"""Get the summary for the action result."""
|
|
172
172
|
return self._summary
|
|
173
173
|
|
soar_sdk/asset.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any
|
|
2
2
|
from zoneinfo import ZoneInfo
|
|
3
|
-
from pydantic import BaseModel,
|
|
4
|
-
from
|
|
3
|
+
from pydantic import BaseModel, model_validator, ConfigDict, Field
|
|
4
|
+
from pydantic_core import PydanticUndefined
|
|
5
5
|
|
|
6
|
-
from typing_extensions import
|
|
6
|
+
from typing_extensions import TypedDict
|
|
7
|
+
from typing import NotRequired
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
from soar_sdk.compat import remove_when_soar_newer_than
|
|
10
11
|
from soar_sdk.meta.datatypes import as_datatype
|
|
11
12
|
from soar_sdk.input_spec import AppConfig
|
|
13
|
+
from soar_sdk.field_utils import parse_json_schema_extra
|
|
12
14
|
|
|
13
15
|
remove_when_soar_newer_than(
|
|
14
16
|
"7.0.0", "NotRequired from typing_extensions is in typing in Python 3.11+"
|
|
@@ -16,12 +18,12 @@ remove_when_soar_newer_than(
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
def AssetField(
|
|
19
|
-
description:
|
|
21
|
+
description: str | None = None,
|
|
20
22
|
required: bool = True,
|
|
21
|
-
default:
|
|
22
|
-
value_list:
|
|
23
|
+
default: Any | None = None, # noqa: ANN401
|
|
24
|
+
value_list: list | None = None,
|
|
23
25
|
sensitive: bool = False,
|
|
24
|
-
alias:
|
|
26
|
+
alias: str | None = None,
|
|
25
27
|
) -> Any: # noqa: ANN401
|
|
26
28
|
"""Representation of an asset configuration field.
|
|
27
29
|
|
|
@@ -42,13 +44,22 @@ def AssetField(
|
|
|
42
44
|
Returns:
|
|
43
45
|
The FieldInfo object as pydantic.Field.
|
|
44
46
|
"""
|
|
47
|
+
json_schema_extra: dict[str, Any] = {}
|
|
48
|
+
if required is not None:
|
|
49
|
+
json_schema_extra["required"] = required
|
|
50
|
+
if value_list is not None:
|
|
51
|
+
json_schema_extra["value_list"] = value_list
|
|
52
|
+
if sensitive is not None:
|
|
53
|
+
json_schema_extra["sensitive"] = sensitive
|
|
54
|
+
|
|
55
|
+
# Use ... for required fields
|
|
56
|
+
field_default: Any = ... if default is None and required else default
|
|
57
|
+
|
|
45
58
|
return Field(
|
|
46
|
-
default=
|
|
59
|
+
default=field_default,
|
|
47
60
|
description=description,
|
|
48
|
-
required=required,
|
|
49
|
-
value_list=value_list,
|
|
50
|
-
sensitive=sensitive,
|
|
51
61
|
alias=alias,
|
|
62
|
+
json_schema_extra=json_schema_extra if json_schema_extra else None,
|
|
52
63
|
)
|
|
53
64
|
|
|
54
65
|
|
|
@@ -71,7 +82,7 @@ class AssetFieldSpecification(TypedDict):
|
|
|
71
82
|
data_type: str
|
|
72
83
|
description: NotRequired[str]
|
|
73
84
|
required: NotRequired[bool]
|
|
74
|
-
default: NotRequired[
|
|
85
|
+
default: NotRequired[str | int | float | bool]
|
|
75
86
|
value_list: NotRequired[list[str]]
|
|
76
87
|
order: NotRequired[int]
|
|
77
88
|
|
|
@@ -106,16 +117,12 @@ class BaseAsset(BaseModel):
|
|
|
106
117
|
the SOAR platform to avoid conflicts with internal fields.
|
|
107
118
|
"""
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Note that we are using the `arbitrary_types_allowed` setting, which is generally not recommended.
|
|
113
|
-
However, we are checking all of the field types via `soar_sdk.datatypes.as_datatype`, so we have confidence in their validity.
|
|
114
|
-
"""
|
|
115
|
-
|
|
116
|
-
arbitrary_types_allowed = True
|
|
120
|
+
model_config = ConfigDict(
|
|
121
|
+
arbitrary_types_allowed=True,
|
|
122
|
+
)
|
|
117
123
|
|
|
118
|
-
@
|
|
124
|
+
@model_validator(mode="before")
|
|
125
|
+
@classmethod
|
|
119
126
|
def validate_no_reserved_fields(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
120
127
|
"""Prevents subclasses from defining fields starting with "_reserved_".
|
|
121
128
|
|
|
@@ -148,7 +155,7 @@ class BaseAsset(BaseModel):
|
|
|
148
155
|
# This accounts for some bad behavior by the platform; it injects a few app-related
|
|
149
156
|
# metadata fields directly into asset configuration dictionaries, which can lead to
|
|
150
157
|
# undefined behavior if an asset tries to use the same field names.
|
|
151
|
-
if field_name in AppConfig.
|
|
158
|
+
if field_name in AppConfig.model_fields:
|
|
152
159
|
raise ValueError(
|
|
153
160
|
f"Field name '{field_name}' is reserved by the platform and cannot be used in an asset"
|
|
154
161
|
)
|
|
@@ -206,8 +213,10 @@ class BaseAsset(BaseModel):
|
|
|
206
213
|
"""
|
|
207
214
|
params: dict[str, AssetFieldSpecification] = {}
|
|
208
215
|
|
|
209
|
-
for field_order, (field_name, field) in enumerate(cls.
|
|
216
|
+
for field_order, (field_name, field) in enumerate(cls.model_fields.items()):
|
|
210
217
|
field_type = field.annotation
|
|
218
|
+
if field_type is None:
|
|
219
|
+
continue
|
|
211
220
|
|
|
212
221
|
try:
|
|
213
222
|
type_name = as_datatype(field_type)
|
|
@@ -216,32 +225,34 @@ class BaseAsset(BaseModel):
|
|
|
216
225
|
f"Failed to serialize asset field {field_name}: {e}"
|
|
217
226
|
) from None
|
|
218
227
|
|
|
219
|
-
|
|
228
|
+
json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
|
|
229
|
+
|
|
230
|
+
if json_schema_extra.get("sensitive", False):
|
|
220
231
|
if field_type is not str:
|
|
221
232
|
raise TypeError(
|
|
222
233
|
f"Sensitive parameter {field_name} must be type str, not {field_type.__name__}"
|
|
223
234
|
)
|
|
224
235
|
type_name = "password"
|
|
225
236
|
|
|
226
|
-
if not (description := field.
|
|
237
|
+
if not (description := field.description):
|
|
227
238
|
description = cls._default_field_description(field_name)
|
|
228
239
|
|
|
229
240
|
params_field = AssetFieldSpecification(
|
|
230
241
|
data_type=type_name,
|
|
231
|
-
required=
|
|
242
|
+
required=bool(json_schema_extra.get("required", True)),
|
|
232
243
|
description=description,
|
|
233
244
|
order=field_order,
|
|
234
245
|
)
|
|
235
246
|
|
|
236
|
-
if (default := field.
|
|
247
|
+
if (default := field.default) not in (PydanticUndefined, None):
|
|
237
248
|
if isinstance(default, ZoneInfo):
|
|
238
249
|
params_field["default"] = default.key
|
|
239
250
|
else:
|
|
240
251
|
params_field["default"] = default
|
|
241
|
-
if value_list :=
|
|
252
|
+
if value_list := json_schema_extra.get("value_list"):
|
|
242
253
|
params_field["value_list"] = value_list
|
|
243
254
|
|
|
244
|
-
params[field.alias] = params_field
|
|
255
|
+
params[field.alias or field_name] = params_field
|
|
245
256
|
|
|
246
257
|
return params
|
|
247
258
|
|
|
@@ -255,8 +266,9 @@ class BaseAsset(BaseModel):
|
|
|
255
266
|
"""
|
|
256
267
|
return {
|
|
257
268
|
field_name
|
|
258
|
-
for field_name, field in cls.
|
|
259
|
-
if field.
|
|
269
|
+
for field_name, field in cls.model_fields.items()
|
|
270
|
+
if isinstance(field.json_schema_extra, dict)
|
|
271
|
+
and field.json_schema_extra.get("sensitive", False)
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
@classmethod
|
|
@@ -268,6 +280,6 @@ class BaseAsset(BaseModel):
|
|
|
268
280
|
"""
|
|
269
281
|
return {
|
|
270
282
|
field_name
|
|
271
|
-
for field_name, field in cls.
|
|
283
|
+
for field_name, field in cls.model_fields.items()
|
|
272
284
|
if field.annotation is ZoneInfo
|
|
273
285
|
}
|
soar_sdk/async_utils.py
CHANGED
|
@@ -17,7 +17,7 @@ def is_async_generator(obj: Any) -> bool: # noqa: ANN401
|
|
|
17
17
|
return inspect.isasyncgen(obj)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
async def async_generator_to_list(agen: AsyncGenerator[T
|
|
20
|
+
async def async_generator_to_list(agen: AsyncGenerator[T]) -> list[T]:
|
|
21
21
|
"""Consume an async generator and return its items in a list."""
|
|
22
22
|
result: list[T] = []
|
|
23
23
|
# Python 3.9 coverage limitation with async for loops
|
|
@@ -31,7 +31,7 @@ def run_async_if_needed(result: Coroutine[Any, Any, T]) -> T: ...
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@overload
|
|
34
|
-
def run_async_if_needed(result: AsyncGenerator[T
|
|
34
|
+
def run_async_if_needed(result: AsyncGenerator[T]) -> list[T]: ...
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
@overload
|
soar_sdk/cli/init/cli.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Annotated,
|
|
1
|
+
from typing import Annotated, cast
|
|
2
2
|
import datetime
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
@@ -65,7 +65,7 @@ def init_callback(
|
|
|
65
65
|
type: str = "generic", # noqa: A002
|
|
66
66
|
vendor: str = "Splunk Inc.",
|
|
67
67
|
publisher: str = "Splunk Inc.",
|
|
68
|
-
product:
|
|
68
|
+
product: str | None = None,
|
|
69
69
|
fips_compliant: bool = False,
|
|
70
70
|
overwrite: bool = False,
|
|
71
71
|
) -> None:
|
|
@@ -107,11 +107,11 @@ def init_sdk_app(
|
|
|
107
107
|
publisher: str,
|
|
108
108
|
logo: Path,
|
|
109
109
|
logo_dark: Path,
|
|
110
|
-
product:
|
|
110
|
+
product: str | None = None,
|
|
111
111
|
fips_compliant: bool = False,
|
|
112
112
|
overwrite: bool = False,
|
|
113
|
-
app_content:
|
|
114
|
-
asset_class:
|
|
113
|
+
app_content: list[ast.stmt] | None = None,
|
|
114
|
+
asset_class: ast.ClassDef | None = None,
|
|
115
115
|
) -> None:
|
|
116
116
|
"""Initialize a new SOAR app in the specified directory."""
|
|
117
117
|
app_dir.mkdir(exist_ok=True)
|
|
@@ -149,9 +149,7 @@ def init_sdk_app(
|
|
|
149
149
|
name=name,
|
|
150
150
|
version=version,
|
|
151
151
|
description=description,
|
|
152
|
-
copyright=copyright.format(
|
|
153
|
-
year=datetime.datetime.now(datetime.timezone.utc).year
|
|
154
|
-
),
|
|
152
|
+
copyright=copyright.format(year=datetime.datetime.now(datetime.UTC).year),
|
|
155
153
|
python_versions=python_versions,
|
|
156
154
|
authors=authors,
|
|
157
155
|
dependencies=dependencies,
|
|
@@ -242,7 +240,7 @@ def convert_connector_to_sdk(
|
|
|
242
240
|
),
|
|
243
241
|
],
|
|
244
242
|
output_dir: Annotated[
|
|
245
|
-
|
|
243
|
+
Path | None,
|
|
246
244
|
typer.Argument(
|
|
247
245
|
file_okay=False,
|
|
248
246
|
dir_okay=True,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import json
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import Any, NamedTuple,
|
|
4
|
+
from typing import Any, NamedTuple, TypeVar, cast
|
|
5
5
|
import pydantic
|
|
6
6
|
|
|
7
7
|
from soar_sdk.action_results import ActionOutput, OutputFieldSpecification, OutputField
|
|
@@ -94,10 +94,10 @@ class OutputFieldModel:
|
|
|
94
94
|
|
|
95
95
|
data_path: str
|
|
96
96
|
data_type: str
|
|
97
|
-
contains:
|
|
98
|
-
example_values:
|
|
99
|
-
column_name:
|
|
100
|
-
column_order:
|
|
97
|
+
contains: list[str] | None = None
|
|
98
|
+
example_values: list[str | float | bool] | None = None
|
|
99
|
+
column_name: str | None = None
|
|
100
|
+
column_order: int | None = None
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
class DeserializedActionMeta(NamedTuple):
|
|
@@ -129,7 +129,7 @@ class ActionDeserializer:
|
|
|
129
129
|
)
|
|
130
130
|
action["output"] = cls.parse_output(action["action"], action.get("output", []))
|
|
131
131
|
return DeserializedActionMeta(
|
|
132
|
-
action_meta=ActionMeta.
|
|
132
|
+
action_meta=ActionMeta.model_validate(action),
|
|
133
133
|
has_custom_view=action.get("render", {}).get("type") == "custom",
|
|
134
134
|
)
|
|
135
135
|
|
|
@@ -212,7 +212,7 @@ class ActionDeserializer:
|
|
|
212
212
|
@staticmethod
|
|
213
213
|
def _build_output_structure(
|
|
214
214
|
datapath_specs: dict[str, OutputFieldModel],
|
|
215
|
-
) -> dict[str,
|
|
215
|
+
) -> dict[str, list | dict | OutputFieldModel]:
|
|
216
216
|
"""Parse a datapath string into a dictionary.
|
|
217
217
|
|
|
218
218
|
Args:
|
|
@@ -223,10 +223,10 @@ class ActionDeserializer:
|
|
|
223
223
|
"""
|
|
224
224
|
|
|
225
225
|
def set_nested_value(
|
|
226
|
-
field_struct: dict[str,
|
|
226
|
+
field_struct: dict[str, list | dict | OutputFieldModel],
|
|
227
227
|
path_parts: list[str],
|
|
228
228
|
field_spec: OutputFieldModel,
|
|
229
|
-
) ->
|
|
229
|
+
) -> list | dict | OutputFieldModel:
|
|
230
230
|
"""Recursively set a field spec in the nested output field structure."""
|
|
231
231
|
# Base case: we're at a leaf node and can return the field spec directly
|
|
232
232
|
if not path_parts:
|
|
@@ -240,7 +240,7 @@ class ActionDeserializer:
|
|
|
240
240
|
|
|
241
241
|
# Recursive case: this portion of the datapath is an object key
|
|
242
242
|
next_field_struct = cast(
|
|
243
|
-
dict[str,
|
|
243
|
+
dict[str, list | dict | OutputFieldModel],
|
|
244
244
|
field_struct.get(current_key, {}),
|
|
245
245
|
)
|
|
246
246
|
field_struct[current_key] = set_nested_value(
|
|
@@ -248,7 +248,7 @@ class ActionDeserializer:
|
|
|
248
248
|
)
|
|
249
249
|
return field_struct
|
|
250
250
|
|
|
251
|
-
MergeT = TypeVar("MergeT", bound=
|
|
251
|
+
MergeT = TypeVar("MergeT", bound=list | dict | OutputFieldModel)
|
|
252
252
|
|
|
253
253
|
def merge(base: MergeT, new_structure: MergeT) -> MergeT:
|
|
254
254
|
"""Merge two nested structures, handling arrays and objects."""
|
|
@@ -280,12 +280,12 @@ class ActionDeserializer:
|
|
|
280
280
|
# Should never happen in reality, hence the pragma, but we handle it gracefully
|
|
281
281
|
return new_structure # pragma: no cover
|
|
282
282
|
|
|
283
|
-
result: dict[str,
|
|
283
|
+
result: dict[str, list | dict | OutputFieldModel] = {}
|
|
284
284
|
|
|
285
285
|
for datapath, field_spec in datapath_specs.items():
|
|
286
286
|
path_parts = datapath.split(".")
|
|
287
287
|
nested_structure = cast(
|
|
288
|
-
dict[str,
|
|
288
|
+
dict[str, list | dict | OutputFieldModel],
|
|
289
289
|
set_nested_value({}, path_parts, field_spec),
|
|
290
290
|
)
|
|
291
291
|
merged = merge(result, nested_structure)
|
|
@@ -298,7 +298,7 @@ class ActionDeserializer:
|
|
|
298
298
|
def _build_output_class(
|
|
299
299
|
cls,
|
|
300
300
|
action_name: str,
|
|
301
|
-
output_structure: dict[str,
|
|
301
|
+
output_structure: dict[str, dict | list | OutputFieldModel],
|
|
302
302
|
) -> type[ActionOutput]:
|
|
303
303
|
"""Build dynamic pydantic models for an action output, from the output data paths.
|
|
304
304
|
|
|
@@ -323,7 +323,7 @@ class ActionDeserializer:
|
|
|
323
323
|
|
|
324
324
|
@classmethod
|
|
325
325
|
def _build_output_field(
|
|
326
|
-
cls, field_name: str, output_structure:
|
|
326
|
+
cls, field_name: str, output_structure: list | dict | OutputFieldModel
|
|
327
327
|
) -> tuple[str, FieldSpec]:
|
|
328
328
|
"""Build dynamic specs for an action output field, from an output data path.
|
|
329
329
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import importlib
|
|
2
2
|
import json
|
|
3
3
|
import toml
|
|
4
|
-
from datetime import datetime,
|
|
4
|
+
from datetime import datetime, UTC
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from pprint import pprint
|
|
7
7
|
|
|
8
8
|
from soar_sdk.app import App
|
|
9
9
|
from soar_sdk.cli.path_utils import context_directory
|
|
10
|
-
from soar_sdk.compat import
|
|
10
|
+
from soar_sdk.compat import UPDATE_TIME_FORMAT
|
|
11
11
|
from soar_sdk.meta.adapters import TOMLDataAdapter
|
|
12
12
|
from soar_sdk.meta.app import AppMeta
|
|
13
13
|
from soar_sdk.meta.dependencies import UvLock
|
|
@@ -30,9 +30,7 @@ class ManifestProcessor:
|
|
|
30
30
|
app = self.import_app_instance(app_meta)
|
|
31
31
|
app_meta.configuration = app.asset_cls.to_json_schema()
|
|
32
32
|
app_meta.actions = app.actions_manager.get_actions_meta_list()
|
|
33
|
-
app_meta.utctime_updated = datetime.now(
|
|
34
|
-
UPDATE_TIME_FORMAT
|
|
35
|
-
)
|
|
33
|
+
app_meta.utctime_updated = datetime.now(UTC).strftime(UPDATE_TIME_FORMAT)
|
|
36
34
|
for field, value in app.app_meta_info.items():
|
|
37
35
|
setattr(app_meta, field, value)
|
|
38
36
|
|
|
@@ -43,16 +41,12 @@ class ManifestProcessor:
|
|
|
43
41
|
dep for dep in dependencies if dep.name != "splunk-soar-sdk"
|
|
44
42
|
]
|
|
45
43
|
|
|
46
|
-
app_meta.
|
|
44
|
+
app_meta.pip313_dependencies, app_meta.pip314_dependencies = (
|
|
47
45
|
uv_lock.resolve_dependencies(dependencies)
|
|
48
46
|
)
|
|
49
47
|
|
|
50
48
|
if app.webhook_meta is not None:
|
|
51
|
-
remove_when_soar_newer_than("6.4.0")
|
|
52
49
|
app_meta.webhook = app.webhook_meta
|
|
53
|
-
app_meta.webhook.handler = (
|
|
54
|
-
f"{app_meta.main_module.replace(':', '.')}.handle_webhook"
|
|
55
|
-
)
|
|
56
50
|
|
|
57
51
|
return app_meta
|
|
58
52
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any
|
|
2
2
|
from collections.abc import Iterator
|
|
3
3
|
from logging import getLogger
|
|
4
4
|
import itertools
|
|
@@ -6,6 +6,7 @@ import itertools
|
|
|
6
6
|
from soar_sdk.meta.datatypes import as_datatype
|
|
7
7
|
from soar_sdk.params import Params
|
|
8
8
|
from soar_sdk.action_results import ActionOutput, OutputFieldSpecification
|
|
9
|
+
from soar_sdk.field_utils import parse_json_schema_extra
|
|
9
10
|
|
|
10
11
|
logger = getLogger(__name__)
|
|
11
12
|
|
|
@@ -16,7 +17,7 @@ class ParamsSerializer:
|
|
|
16
17
|
@staticmethod
|
|
17
18
|
def get_sorted_fields_keys(params_class: type[Params]) -> list[str]:
|
|
18
19
|
"""Lists the fields of a Params class in order of declaration."""
|
|
19
|
-
return list(params_class.
|
|
20
|
+
return list(params_class.model_fields.keys())
|
|
20
21
|
|
|
21
22
|
@classmethod
|
|
22
23
|
def serialize_fields_info(cls, params_class: type[Params]) -> dict[str, Any]:
|
|
@@ -30,21 +31,28 @@ class OutputsSerializer:
|
|
|
30
31
|
@staticmethod
|
|
31
32
|
def serialize_parameter_datapaths(
|
|
32
33
|
params_class: type[Params],
|
|
33
|
-
column_order_counter:
|
|
34
|
+
column_order_counter: itertools.count | None = None,
|
|
34
35
|
) -> Iterator[OutputFieldSpecification]:
|
|
35
36
|
"""Serializes the parameter data paths of a Params class to JSON schema."""
|
|
36
37
|
if column_order_counter is None:
|
|
37
38
|
column_order_counter = itertools.count()
|
|
38
39
|
|
|
39
|
-
for field_name, field in params_class.
|
|
40
|
+
for field_name, field in params_class.model_fields.items():
|
|
41
|
+
annotation = field.annotation
|
|
42
|
+
if annotation is None:
|
|
43
|
+
continue
|
|
44
|
+
|
|
40
45
|
spec = OutputFieldSpecification(
|
|
41
46
|
data_path=f"action_result.parameter.{field_name}",
|
|
42
|
-
data_type=as_datatype(
|
|
47
|
+
data_type=as_datatype(annotation),
|
|
43
48
|
)
|
|
44
|
-
|
|
49
|
+
|
|
50
|
+
json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
|
|
51
|
+
|
|
52
|
+
if cef_types := json_schema_extra.get("cef_types"):
|
|
45
53
|
spec["contains"] = cef_types
|
|
46
54
|
|
|
47
|
-
column_name =
|
|
55
|
+
column_name = json_schema_extra.get("column_name")
|
|
48
56
|
|
|
49
57
|
if column_name is not None:
|
|
50
58
|
spec["column_name"] = column_name
|
|
@@ -56,7 +64,7 @@ class OutputsSerializer:
|
|
|
56
64
|
cls,
|
|
57
65
|
params_class: type[Params],
|
|
58
66
|
outputs_class: type[ActionOutput],
|
|
59
|
-
summary_class:
|
|
67
|
+
summary_class: type[ActionOutput] | None = None,
|
|
60
68
|
) -> list[OutputFieldSpecification]:
|
|
61
69
|
"""Serializes the data paths of an action to JSON schema."""
|
|
62
70
|
status = OutputFieldSpecification(
|
soar_sdk/cli/package/cli.py
CHANGED
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import asyncio
|
|
11
11
|
import time
|
|
12
|
-
from typing import
|
|
12
|
+
from typing import Annotated
|
|
13
13
|
from tqdm import tqdm
|
|
14
14
|
from rich.console import Console
|
|
15
15
|
from rich.panel import Panel
|
|
@@ -67,11 +67,11 @@ def build(
|
|
|
67
67
|
),
|
|
68
68
|
],
|
|
69
69
|
output_file: Annotated[
|
|
70
|
-
|
|
70
|
+
Path | None,
|
|
71
71
|
typer.Option("--output-file", "-o", show_default="derived from pyproject.toml"),
|
|
72
72
|
] = None,
|
|
73
73
|
with_sdk_wheel_from: Annotated[
|
|
74
|
-
|
|
74
|
+
Path | None,
|
|
75
75
|
typer.Option(
|
|
76
76
|
"--with-sdk-wheel-from",
|
|
77
77
|
"-w",
|
|
@@ -115,7 +115,7 @@ def build(
|
|
|
115
115
|
|
|
116
116
|
console.print(f"Generated manifest for app:[green] {app_name}[/]")
|
|
117
117
|
|
|
118
|
-
def filter_source_files(t: tarfile.TarInfo) ->
|
|
118
|
+
def filter_source_files(t: tarfile.TarInfo) -> tarfile.TarInfo | None:
|
|
119
119
|
if t.isdir() and "__pycache__" not in t.name:
|
|
120
120
|
return t
|
|
121
121
|
if t.isfile() and t.name.endswith(".py"):
|
|
@@ -125,7 +125,7 @@ def build(
|
|
|
125
125
|
with tarfile.open(output_file, "w:gz") as app_tarball:
|
|
126
126
|
# Collect all wheels from both Python versions
|
|
127
127
|
all_wheels = set(
|
|
128
|
-
app_meta.
|
|
128
|
+
app_meta.pip313_dependencies.wheel + app_meta.pip314_dependencies.wheel
|
|
129
129
|
)
|
|
130
130
|
|
|
131
131
|
# Run the async collection function within an event loop
|
|
@@ -188,8 +188,8 @@ def build(
|
|
|
188
188
|
input_file=wheel_archive_path,
|
|
189
189
|
input_file_aarch64=wheel_archive_path,
|
|
190
190
|
)
|
|
191
|
-
app_meta.pip39_dependencies.wheel.append(wheel_entry)
|
|
192
191
|
app_meta.pip313_dependencies.wheel.append(wheel_entry)
|
|
192
|
+
app_meta.pip314_dependencies.wheel.append(wheel_entry)
|
|
193
193
|
|
|
194
194
|
console.print("Writing manifest")
|
|
195
195
|
manifest_json = json.dumps(app_meta.to_json_manifest(), indent=4).encode()
|
soar_sdk/cli/package/utils.py
CHANGED
|
@@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator
|
|
|
6
6
|
@asynccontextmanager
|
|
7
7
|
async def phantom_get_login_session(
|
|
8
8
|
base_url: str, username: str, password: str
|
|
9
|
-
) -> AsyncGenerator[httpx.AsyncClient
|
|
9
|
+
) -> AsyncGenerator[httpx.AsyncClient]:
|
|
10
10
|
"""Contextmanager that creates an authenticated client with CSRF token handling."""
|
|
11
11
|
# Set longer timeouts for large file uploads
|
|
12
12
|
timeout = httpx.Timeout(30.0, read=60.0)
|