splunk-soar-sdk 2.3.6__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 +10 -13
- 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 +13 -5
- soar_sdk/shims/phantom/install_info.py +15 -2
- 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.6.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.6.dist-info/RECORD +0 -103
- {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/WHEEL +0 -0
- {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/entry_points.txt +0 -0
- {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
from typing import ClassVar
|
|
1
|
+
from typing import ClassVar
|
|
2
2
|
from collections.abc import Iterator
|
|
3
3
|
import typing
|
|
4
4
|
import ast
|
|
5
|
-
from
|
|
5
|
+
from pydantic_core import PydanticUndefined
|
|
6
6
|
|
|
7
7
|
from soar_sdk.action_results import ActionOutput
|
|
8
8
|
from soar_sdk.cli.utils import normalize_field_name
|
|
9
9
|
from soar_sdk.code_renderers.renderer import AstRenderer
|
|
10
10
|
from soar_sdk.meta.actions import ActionMeta
|
|
11
11
|
from soar_sdk.params import Params
|
|
12
|
+
from soar_sdk.field_utils import parse_json_schema_extra
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class ActionRenderer(AstRenderer[ActionMeta]):
|
|
@@ -201,7 +202,7 @@ class ActionRenderer(AstRenderer[ActionMeta]):
|
|
|
201
202
|
yield ast.fix_missing_locations(node)
|
|
202
203
|
|
|
203
204
|
def render_outputs_ast(
|
|
204
|
-
self, model:
|
|
205
|
+
self, model: type[ActionOutput] | None = None
|
|
205
206
|
) -> Iterator[ast.ClassDef]:
|
|
206
207
|
"""Generates the AST for the action outputs.
|
|
207
208
|
|
|
@@ -221,16 +222,24 @@ class ActionRenderer(AstRenderer[ActionMeta]):
|
|
|
221
222
|
|
|
222
223
|
field_defs: list[ast.stmt] = []
|
|
223
224
|
|
|
224
|
-
for field_name_str, field in model.
|
|
225
|
+
for field_name_str, field in model.model_fields.items():
|
|
225
226
|
annotation = field.annotation
|
|
227
|
+
if annotation is None:
|
|
228
|
+
continue
|
|
229
|
+
|
|
226
230
|
annotation_str = "{name}"
|
|
227
231
|
while typing.get_origin(annotation) is list:
|
|
228
232
|
annotation_str = f"list[{annotation_str}]"
|
|
229
233
|
annotation = typing.get_args(annotation)[0]
|
|
234
|
+
|
|
235
|
+
# Ensure annotation is a valid type after unwrapping
|
|
236
|
+
if not isinstance(annotation, type):
|
|
237
|
+
continue
|
|
238
|
+
|
|
230
239
|
annotation_str = annotation_str.format(name=annotation.__name__)
|
|
231
240
|
|
|
232
241
|
field_name = normalize_field_name(field_name_str)
|
|
233
|
-
if field.alias != field_name.normalized:
|
|
242
|
+
if field.alias is not None and field.alias != field_name.normalized:
|
|
234
243
|
field_name.original = field.alias
|
|
235
244
|
field_name.modified = True
|
|
236
245
|
|
|
@@ -240,9 +249,10 @@ class ActionRenderer(AstRenderer[ActionMeta]):
|
|
|
240
249
|
simple=1,
|
|
241
250
|
)
|
|
242
251
|
|
|
243
|
-
if issubclass(annotation, ActionOutput):
|
|
252
|
+
if isinstance(annotation, type) and issubclass(annotation, ActionOutput):
|
|
244
253
|
# If the field is a Pydantic model, recursively print its fields
|
|
245
|
-
|
|
254
|
+
# In Pydantic v2, use annotation directly (no field.type_)
|
|
255
|
+
for model_ast in self.render_outputs_ast(annotation):
|
|
246
256
|
model_tree[model_ast.name] = model_ast
|
|
247
257
|
|
|
248
258
|
if field_name.modified:
|
|
@@ -258,7 +268,9 @@ class ActionRenderer(AstRenderer[ActionMeta]):
|
|
|
258
268
|
)
|
|
259
269
|
else:
|
|
260
270
|
keywords = []
|
|
261
|
-
|
|
271
|
+
extras = {**parse_json_schema_extra(field.json_schema_extra)}
|
|
272
|
+
|
|
273
|
+
if extras or field_name.modified:
|
|
262
274
|
extras["example_values"] = extras.pop("examples", None)
|
|
263
275
|
if extras["example_values"] == [True, False]:
|
|
264
276
|
extras["example_values"] = None
|
|
@@ -315,7 +327,10 @@ class ActionRenderer(AstRenderer[ActionMeta]):
|
|
|
315
327
|
keywords=[],
|
|
316
328
|
)
|
|
317
329
|
|
|
318
|
-
for field_name, field_def in self.action_meta.parameters.
|
|
330
|
+
for field_name, field_def in self.action_meta.parameters.model_fields.items():
|
|
331
|
+
if field_def.annotation is None:
|
|
332
|
+
continue
|
|
333
|
+
|
|
319
334
|
field_type = ast.Name(id=field_def.annotation.__name__, ctx=ast.Load())
|
|
320
335
|
|
|
321
336
|
param = ast.Call(
|
|
@@ -324,29 +339,31 @@ class ActionRenderer(AstRenderer[ActionMeta]):
|
|
|
324
339
|
keywords=[],
|
|
325
340
|
)
|
|
326
341
|
|
|
327
|
-
|
|
342
|
+
json_schema_extra = parse_json_schema_extra(field_def.json_schema_extra)
|
|
343
|
+
|
|
344
|
+
if field_def.description:
|
|
328
345
|
param.keywords.append(
|
|
329
346
|
ast.keyword(
|
|
330
347
|
arg="description",
|
|
331
|
-
value=ast.Constant(value=field_def.
|
|
348
|
+
value=ast.Constant(value=field_def.description),
|
|
332
349
|
)
|
|
333
350
|
)
|
|
334
|
-
if not
|
|
351
|
+
if not json_schema_extra.get("required", True):
|
|
335
352
|
param.keywords.append(
|
|
336
353
|
ast.keyword(arg="required", value=ast.Constant(value=False))
|
|
337
354
|
)
|
|
338
|
-
if
|
|
355
|
+
if json_schema_extra.get("primary", False):
|
|
339
356
|
param.keywords.append(
|
|
340
357
|
ast.keyword(arg="primary", value=ast.Constant(value=True))
|
|
341
358
|
)
|
|
342
|
-
if
|
|
359
|
+
if field_def.default not in (PydanticUndefined, None):
|
|
343
360
|
param.keywords.append(
|
|
344
361
|
ast.keyword(
|
|
345
362
|
arg="default",
|
|
346
|
-
value=ast.Constant(value=field_def.
|
|
363
|
+
value=ast.Constant(value=field_def.default),
|
|
347
364
|
)
|
|
348
365
|
)
|
|
349
|
-
if value_list :=
|
|
366
|
+
if value_list := json_schema_extra.get("value_list"):
|
|
350
367
|
param.keywords.append(
|
|
351
368
|
ast.keyword(
|
|
352
369
|
arg="value_list",
|
|
@@ -356,7 +373,7 @@ class ActionRenderer(AstRenderer[ActionMeta]):
|
|
|
356
373
|
),
|
|
357
374
|
)
|
|
358
375
|
)
|
|
359
|
-
if cef_types :=
|
|
376
|
+
if cef_types := json_schema_extra.get("cef_types"):
|
|
360
377
|
param.keywords.append(
|
|
361
378
|
ast.keyword(
|
|
362
379
|
arg="cef_types",
|
|
@@ -366,7 +383,7 @@ class ActionRenderer(AstRenderer[ActionMeta]):
|
|
|
366
383
|
),
|
|
367
384
|
)
|
|
368
385
|
)
|
|
369
|
-
if
|
|
386
|
+
if json_schema_extra.get("allow_list", False):
|
|
370
387
|
param.keywords.append(
|
|
371
388
|
ast.keyword(arg="allow_list", value=ast.Constant(value=True))
|
|
372
389
|
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import ast
|
|
3
|
-
from typing import Union
|
|
4
3
|
from collections.abc import Iterator
|
|
5
4
|
|
|
6
5
|
|
|
@@ -50,7 +49,7 @@ class AppRenderer:
|
|
|
50
49
|
self.context = context
|
|
51
50
|
|
|
52
51
|
@staticmethod
|
|
53
|
-
def create_default_imports() -> Iterator[
|
|
52
|
+
def create_default_imports() -> Iterator[ast.Import | ast.ImportFrom]:
|
|
54
53
|
"""Create default imports for the App module.
|
|
55
54
|
|
|
56
55
|
Returns:
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
-
from typing import Optional, Union
|
|
3
2
|
import ast
|
|
4
3
|
from collections.abc import Iterator
|
|
5
4
|
|
|
@@ -12,12 +11,12 @@ class AssetContext:
|
|
|
12
11
|
"""Context for rendering individual configuration keys of an Asset class."""
|
|
13
12
|
|
|
14
13
|
name: str
|
|
15
|
-
description:
|
|
14
|
+
description: str | None
|
|
16
15
|
required: bool
|
|
17
|
-
default:
|
|
16
|
+
default: str | int | float | bool | None
|
|
18
17
|
data_type: str
|
|
19
|
-
value_list:
|
|
20
|
-
alias:
|
|
18
|
+
value_list: list[str] | None
|
|
19
|
+
alias: str | None = None
|
|
21
20
|
|
|
22
21
|
@property
|
|
23
22
|
def is_str(self) -> bool:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import abc
|
|
2
|
-
from typing import TypeVar, Generic
|
|
2
|
+
from typing import TypeVar, Generic
|
|
3
3
|
import jinja2 as j2
|
|
4
4
|
import ast
|
|
5
5
|
from collections.abc import Iterator
|
|
@@ -12,7 +12,7 @@ class Renderer(Generic[ContextT], abc.ABC):
|
|
|
12
12
|
"""Abstract base class for rendering code using Jinja2 templates."""
|
|
13
13
|
|
|
14
14
|
def __init__(
|
|
15
|
-
self, context: ContextT, jinja_env:
|
|
15
|
+
self, context: ContextT, jinja_env: j2.Environment | None = None
|
|
16
16
|
) -> None:
|
|
17
17
|
self.context = context
|
|
18
18
|
self.jinja_env = jinja_env or j2.Environment(
|
soar_sdk/compat.py
CHANGED
|
@@ -2,7 +2,7 @@ from enum import Enum
|
|
|
2
2
|
import functools
|
|
3
3
|
from packaging.version import Version
|
|
4
4
|
|
|
5
|
-
MIN_PHANTOM_VERSION = "
|
|
5
|
+
MIN_PHANTOM_VERSION = "7.0.0"
|
|
6
6
|
|
|
7
7
|
UPDATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
8
8
|
|
|
@@ -27,8 +27,8 @@ def remove_when_soar_newer_than(
|
|
|
27
27
|
class PythonVersion(str, Enum):
|
|
28
28
|
"""Enum to represent supported Python versions."""
|
|
29
29
|
|
|
30
|
-
PY_3_9 = "3.9"
|
|
31
30
|
PY_3_13 = "3.13"
|
|
31
|
+
PY_3_14 = "3.14"
|
|
32
32
|
|
|
33
33
|
def __str__(self) -> str:
|
|
34
34
|
"""Returns the string representation of the Python version."""
|
|
@@ -41,10 +41,10 @@ class PythonVersion(str, Enum):
|
|
|
41
41
|
Raises ValueError if the version is not supported.
|
|
42
42
|
"""
|
|
43
43
|
# "3" is a special case for connectors that don't properly define their Python version
|
|
44
|
-
if version_str in ("3", "3.
|
|
45
|
-
return cls.PY_3_9
|
|
46
|
-
if version_str == "3.13":
|
|
44
|
+
if version_str in ("3", "3.13"):
|
|
47
45
|
return cls.PY_3_13
|
|
46
|
+
if version_str == "3.14":
|
|
47
|
+
return cls.PY_3_14
|
|
48
48
|
|
|
49
49
|
raise ValueError(f"Unsupported Python version: {version_str}")
|
|
50
50
|
|
|
@@ -67,7 +67,7 @@ class PythonVersion(str, Enum):
|
|
|
67
67
|
@classmethod
|
|
68
68
|
def all(cls) -> list["PythonVersion"]:
|
|
69
69
|
"""Returns a list of all supported Python versions."""
|
|
70
|
-
return [cls.
|
|
70
|
+
return [cls.PY_3_13, cls.PY_3_14]
|
|
71
71
|
|
|
72
72
|
@classmethod
|
|
73
73
|
def all_csv(cls) -> str:
|
soar_sdk/decorators/action.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from functools import wraps
|
|
3
3
|
from collections.abc import Iterator
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Any, get_args, get_origin
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from collections.abc import AsyncGenerator
|
|
6
7
|
|
|
7
8
|
from soar_sdk.abstract import SOARClient
|
|
@@ -25,24 +26,22 @@ class ActionDecorator:
|
|
|
25
26
|
def __init__(
|
|
26
27
|
self,
|
|
27
28
|
app: "App",
|
|
28
|
-
name:
|
|
29
|
-
identifier:
|
|
30
|
-
description:
|
|
29
|
+
name: str | None = None,
|
|
30
|
+
identifier: str | None = None,
|
|
31
|
+
description: str | None = None,
|
|
31
32
|
verbose: str = "",
|
|
32
33
|
action_type: str = "generic",
|
|
33
34
|
read_only: bool = True,
|
|
34
|
-
params_class:
|
|
35
|
-
output_class:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
render_as: Optional[str] = None,
|
|
43
|
-
view_handler: Optional[Callable] = None,
|
|
35
|
+
params_class: type[Params] | None = None,
|
|
36
|
+
output_class: None
|
|
37
|
+
| type[ActionOutput]
|
|
38
|
+
| Iterator[type[ActionOutput]]
|
|
39
|
+
| AsyncGenerator[type[ActionOutput]]
|
|
40
|
+
| list[type[ActionOutput]] = None,
|
|
41
|
+
render_as: str | None = None,
|
|
42
|
+
view_handler: Callable | None = None,
|
|
44
43
|
versions: str = "EQ(*)",
|
|
45
|
-
summary_type:
|
|
44
|
+
summary_type: type[ActionOutput] | None = None,
|
|
46
45
|
enable_concurrency_lock: bool = False,
|
|
47
46
|
) -> None:
|
|
48
47
|
self.app = app
|
|
@@ -12,7 +12,8 @@ from soar_sdk.logging import getLogger
|
|
|
12
12
|
from functools import wraps
|
|
13
13
|
import traceback
|
|
14
14
|
|
|
15
|
-
from typing import TYPE_CHECKING,
|
|
15
|
+
from typing import TYPE_CHECKING, Any
|
|
16
|
+
from collections.abc import Callable
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from soar_sdk.app import App
|
|
@@ -24,7 +25,7 @@ class MakeRequestDecorator:
|
|
|
24
25
|
def __init__(
|
|
25
26
|
self,
|
|
26
27
|
app: "App",
|
|
27
|
-
output_class:
|
|
28
|
+
output_class: type[ActionOutput] | None = None,
|
|
28
29
|
) -> None:
|
|
29
30
|
self.app = app
|
|
30
31
|
self.output_class = output_class
|
|
@@ -97,7 +98,7 @@ class MakeRequestDecorator:
|
|
|
97
98
|
**kwargs: Any, # noqa: ANN401
|
|
98
99
|
) -> bool:
|
|
99
100
|
try:
|
|
100
|
-
action_params = validated_params_class.
|
|
101
|
+
action_params = validated_params_class.model_validate(params)
|
|
101
102
|
except Exception as e:
|
|
102
103
|
logger.info(f"Parameter validation error: {e!s}")
|
|
103
104
|
return self.app._adapt_action_result(
|
soar_sdk/decorators/on_poll.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from functools import wraps
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Any
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from collections.abc import Iterator
|
|
5
6
|
|
|
6
7
|
from soar_sdk.abstract import SOARClient
|
|
@@ -80,7 +81,7 @@ class OnPollDecorator:
|
|
|
80
81
|
try:
|
|
81
82
|
# Validate poll params
|
|
82
83
|
try:
|
|
83
|
-
action_params = validated_params_class.
|
|
84
|
+
action_params = validated_params_class.model_validate(params)
|
|
84
85
|
except Exception as e:
|
|
85
86
|
logger.info(f"Parameter validation error: {e!s}")
|
|
86
87
|
return self.app._adapt_action_result(
|
|
@@ -177,8 +178,8 @@ class OnPollDecorator:
|
|
|
177
178
|
|
|
178
179
|
# Custom ActionMeta class for on_poll (has no output)
|
|
179
180
|
class OnPollActionMeta(ActionMeta):
|
|
180
|
-
def
|
|
181
|
-
data = super().
|
|
181
|
+
def model_dump(self, *args: object, **kwargs: object) -> dict[str, Any]:
|
|
182
|
+
data = super().model_dump(*args, **kwargs)
|
|
182
183
|
# Poll actions have no output
|
|
183
184
|
data["output"] = []
|
|
184
185
|
return data
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from functools import wraps
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
4
|
|
|
5
5
|
from soar_sdk.abstract import SOARClient
|
|
6
6
|
from soar_sdk.action_results import ActionResult
|
|
@@ -48,7 +48,7 @@ class ConnectivityTestDecorator:
|
|
|
48
48
|
@action_protocol
|
|
49
49
|
@wraps(function)
|
|
50
50
|
def inner(
|
|
51
|
-
_param:
|
|
51
|
+
_param: dict | None = None,
|
|
52
52
|
soar: SOARClient = self.app.soar_client,
|
|
53
53
|
) -> bool:
|
|
54
54
|
kwargs = self.app._build_magic_args(function, soar=soar)
|