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
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()
|
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,
|