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/params.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
from typing import
|
|
2
|
-
from typing_extensions import
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
from typing_extensions import TypedDict
|
|
3
|
+
from typing import NotRequired
|
|
3
4
|
|
|
4
|
-
from pydantic
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
from pydantic_core import PydanticUndefined
|
|
5
7
|
from pydantic.main import BaseModel
|
|
6
8
|
|
|
7
9
|
from soar_sdk.compat import remove_when_soar_newer_than
|
|
8
10
|
from soar_sdk.meta.datatypes import as_datatype
|
|
11
|
+
from soar_sdk.field_utils import parse_json_schema_extra
|
|
9
12
|
|
|
10
13
|
remove_when_soar_newer_than(
|
|
11
14
|
"7.0.0", "NotRequired from typing_extensions is in typing in Python 3.11+"
|
|
@@ -13,16 +16,16 @@ remove_when_soar_newer_than(
|
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
def Param(
|
|
16
|
-
description:
|
|
19
|
+
description: str | None = None,
|
|
17
20
|
required: bool = True,
|
|
18
21
|
primary: bool = False,
|
|
19
|
-
default:
|
|
20
|
-
value_list:
|
|
21
|
-
cef_types:
|
|
22
|
+
default: Any | None = None, # noqa: ANN401
|
|
23
|
+
value_list: list | None = None,
|
|
24
|
+
cef_types: list | None = None,
|
|
22
25
|
allow_list: bool = False,
|
|
23
26
|
sensitive: bool = False,
|
|
24
|
-
alias:
|
|
25
|
-
column_name:
|
|
27
|
+
alias: str | None = None,
|
|
28
|
+
column_name: str | None = None,
|
|
26
29
|
) -> Any: # noqa: ANN401
|
|
27
30
|
"""Representation of a single complex action parameter.
|
|
28
31
|
|
|
@@ -53,20 +56,30 @@ def Param(
|
|
|
53
56
|
:param column_name: Optional name for the parameter when displayed in an output table.
|
|
54
57
|
:return: returns the FieldInfo object as pydantic.Field
|
|
55
58
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
json_schema_extra: dict[str, Any] = {}
|
|
60
|
+
if required is not None:
|
|
61
|
+
json_schema_extra["required"] = required
|
|
62
|
+
if primary is not None:
|
|
63
|
+
json_schema_extra["primary"] = primary
|
|
64
|
+
if value_list:
|
|
65
|
+
json_schema_extra["value_list"] = value_list
|
|
66
|
+
if cef_types is not None:
|
|
67
|
+
json_schema_extra["cef_types"] = cef_types
|
|
68
|
+
if allow_list is not None:
|
|
69
|
+
json_schema_extra["allow_list"] = allow_list
|
|
70
|
+
if sensitive is not None:
|
|
71
|
+
json_schema_extra["sensitive"] = sensitive
|
|
72
|
+
if column_name is not None:
|
|
73
|
+
json_schema_extra["column_name"] = column_name
|
|
74
|
+
|
|
75
|
+
# Use ... for required fields
|
|
76
|
+
field_default: Any = ... if default is None and required else default
|
|
58
77
|
|
|
59
78
|
return Field(
|
|
60
|
-
default=
|
|
79
|
+
default=field_default,
|
|
61
80
|
description=description,
|
|
62
|
-
required=required,
|
|
63
|
-
primary=primary,
|
|
64
|
-
value_list=value_list,
|
|
65
|
-
cef_types=cef_types,
|
|
66
|
-
allow_list=allow_list,
|
|
67
|
-
sensitive=sensitive,
|
|
68
81
|
alias=alias,
|
|
69
|
-
|
|
82
|
+
json_schema_extra=json_schema_extra if json_schema_extra else None,
|
|
70
83
|
)
|
|
71
84
|
|
|
72
85
|
|
|
@@ -82,7 +95,7 @@ class InputFieldSpecification(TypedDict):
|
|
|
82
95
|
primary: bool
|
|
83
96
|
value_list: NotRequired[list[str]]
|
|
84
97
|
allow_list: bool
|
|
85
|
-
default: NotRequired[
|
|
98
|
+
default: NotRequired[str | int | float | bool]
|
|
86
99
|
column_name: NotRequired[str]
|
|
87
100
|
column_order: NotRequired[int]
|
|
88
101
|
|
|
@@ -103,9 +116,12 @@ class Params(BaseModel):
|
|
|
103
116
|
def _to_json_schema(cls) -> dict[str, InputFieldSpecification]:
|
|
104
117
|
params: dict[str, InputFieldSpecification] = {}
|
|
105
118
|
|
|
106
|
-
for field_order, (field_name, field) in enumerate(cls.
|
|
119
|
+
for field_order, (field_name, field) in enumerate(cls.model_fields.items()):
|
|
107
120
|
field_type = field.annotation
|
|
108
121
|
|
|
122
|
+
if field_type is None:
|
|
123
|
+
raise TypeError(f"Parameter {field_name} has no type annotation")
|
|
124
|
+
|
|
109
125
|
try:
|
|
110
126
|
type_name = as_datatype(field_type)
|
|
111
127
|
except TypeError as e:
|
|
@@ -113,14 +129,16 @@ class Params(BaseModel):
|
|
|
113
129
|
f"Failed to serialize action parameter {field_name}: {e}"
|
|
114
130
|
) from None
|
|
115
131
|
|
|
116
|
-
|
|
132
|
+
json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
|
|
133
|
+
|
|
134
|
+
if json_schema_extra.get("sensitive", False):
|
|
117
135
|
if field_type is not str:
|
|
118
136
|
raise TypeError(
|
|
119
137
|
f"Sensitive parameter {field_name} must be type str, not {field_type.__name__}"
|
|
120
138
|
)
|
|
121
139
|
type_name = "password"
|
|
122
140
|
|
|
123
|
-
if not (description := field.
|
|
141
|
+
if not (description := field.description):
|
|
124
142
|
description = cls._default_field_description(field_name)
|
|
125
143
|
|
|
126
144
|
params_field = InputFieldSpecification(
|
|
@@ -128,19 +146,19 @@ class Params(BaseModel):
|
|
|
128
146
|
name=field_name,
|
|
129
147
|
description=description,
|
|
130
148
|
data_type=type_name,
|
|
131
|
-
required=
|
|
132
|
-
primary=
|
|
133
|
-
allow_list=
|
|
149
|
+
required=bool(json_schema_extra.get("required", True)),
|
|
150
|
+
primary=bool(json_schema_extra.get("primary", False)),
|
|
151
|
+
allow_list=bool(json_schema_extra.get("allow_list", False)),
|
|
134
152
|
)
|
|
135
153
|
|
|
136
|
-
if cef_types :=
|
|
154
|
+
if cef_types := json_schema_extra.get("cef_types"):
|
|
137
155
|
params_field["contains"] = cef_types
|
|
138
|
-
if (default := field.
|
|
156
|
+
if (default := field.default) not in (PydanticUndefined, None):
|
|
139
157
|
params_field["default"] = default
|
|
140
|
-
if value_list :=
|
|
158
|
+
if value_list := json_schema_extra.get("value_list"):
|
|
141
159
|
params_field["value_list"] = value_list
|
|
142
160
|
|
|
143
|
-
params[field.alias] = params_field
|
|
161
|
+
params[field.alias or field_name] = params_field
|
|
144
162
|
|
|
145
163
|
return params
|
|
146
164
|
|
|
@@ -189,21 +207,21 @@ class MakeRequestParams(Params):
|
|
|
189
207
|
"verify_ssl",
|
|
190
208
|
}
|
|
191
209
|
|
|
192
|
-
def __init_subclass__(cls, **kwargs:
|
|
210
|
+
def __init_subclass__(cls, **kwargs: Any) -> None: # noqa: ANN401
|
|
193
211
|
"""Validate that subclasses only define allowed fields."""
|
|
194
212
|
super().__init_subclass__(**kwargs)
|
|
195
|
-
cls._validate_make_request_fields()
|
|
196
213
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
214
|
+
def model_post_init(self, __context: Any) -> None: # noqa: ANN401
|
|
215
|
+
"""Ensure model fields are validated after instance is created."""
|
|
216
|
+
super().model_post_init(__context)
|
|
200
217
|
# Check if any fields are not in the allowed set
|
|
201
|
-
invalid_fields =
|
|
202
|
-
|
|
218
|
+
invalid_fields = (
|
|
219
|
+
set(self.__class__.model_fields.keys()) - self.__class__._ALLOWED_FIELDS
|
|
220
|
+
)
|
|
203
221
|
if invalid_fields:
|
|
204
222
|
raise TypeError(
|
|
205
|
-
f"MakeRequestParams subclass '{
|
|
206
|
-
f"{sorted(
|
|
223
|
+
f"MakeRequestParams subclass '{self.__class__.__name__}' can only define these fields: "
|
|
224
|
+
f"{sorted(self.__class__._ALLOWED_FIELDS)}. Invalid fields: {sorted(invalid_fields)}"
|
|
207
225
|
)
|
|
208
226
|
|
|
209
227
|
http_method: str = Param(
|
|
@@ -5,12 +5,12 @@ try:
|
|
|
5
5
|
except ImportError:
|
|
6
6
|
_soar_is_available = False
|
|
7
7
|
|
|
8
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING or not _soar_is_available:
|
|
11
11
|
|
|
12
12
|
class ActionResult: # type: ignore[no-redef]
|
|
13
|
-
def __init__(self, param:
|
|
13
|
+
def __init__(self, param: dict | None = None) -> None:
|
|
14
14
|
self.status = False
|
|
15
15
|
self.message = ""
|
|
16
16
|
self.summary: dict[str, Any] = {}
|
|
@@ -24,9 +24,9 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
24
24
|
|
|
25
25
|
def set_status(
|
|
26
26
|
self,
|
|
27
|
-
status_code:
|
|
27
|
+
status_code: bool | int,
|
|
28
28
|
_status_message: str = "",
|
|
29
|
-
_exception:
|
|
29
|
+
_exception: Exception | None = None,
|
|
30
30
|
) -> bool:
|
|
31
31
|
self.status = bool(status_code)
|
|
32
32
|
self.message = _status_message
|
|
@@ -19,7 +19,7 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
19
19
|
from soar_sdk.shims.phantom.action_result import ActionResult
|
|
20
20
|
from soar_sdk.shims.phantom.connector_result import ConnectorResult
|
|
21
21
|
|
|
22
|
-
from typing import
|
|
22
|
+
from typing import Any
|
|
23
23
|
from contextlib import suppress
|
|
24
24
|
|
|
25
25
|
class BaseConnector: # type: ignore[no-redef]
|
|
@@ -71,14 +71,14 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
71
71
|
def error_print(
|
|
72
72
|
self,
|
|
73
73
|
_tag: str,
|
|
74
|
-
_dump_object:
|
|
74
|
+
_dump_object: str | list | dict | ActionResult | Exception = "",
|
|
75
75
|
) -> None:
|
|
76
76
|
print(_tag, _dump_object)
|
|
77
77
|
|
|
78
78
|
def debug_print(
|
|
79
79
|
self,
|
|
80
80
|
_tag: str,
|
|
81
|
-
_dump_object:
|
|
81
|
+
_dump_object: str | list | dict | ActionResult | Exception = "",
|
|
82
82
|
) -> None:
|
|
83
83
|
print(_tag, _dump_object)
|
|
84
84
|
|
|
@@ -109,12 +109,12 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
109
109
|
|
|
110
110
|
def save_container(
|
|
111
111
|
self, container: dict, fail_on_duplicate: bool = False
|
|
112
|
-
) -> tuple[bool, str,
|
|
112
|
+
) -> tuple[bool, str, int | None]:
|
|
113
113
|
return True, "Container saved successfully", 1
|
|
114
114
|
|
|
115
115
|
def save_artifacts(
|
|
116
116
|
self, artifacts: list[dict]
|
|
117
|
-
) -> tuple[bool, str,
|
|
117
|
+
) -> tuple[bool, str, int | None | list[int]]:
|
|
118
118
|
return True, "Artifacts saved successfully", [1]
|
|
119
119
|
|
|
120
120
|
def get_config(self) -> dict:
|
soar_sdk/shims/phantom/ph_ipc.py
CHANGED
|
@@ -5,7 +5,7 @@ try:
|
|
|
5
5
|
except ImportError:
|
|
6
6
|
_soar_is_available = False
|
|
7
7
|
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING or not _soar_is_available:
|
|
11
11
|
from soar_sdk.shims.phantom.install_info import get_product_version
|
|
@@ -34,12 +34,12 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
34
34
|
|
|
35
35
|
@staticmethod
|
|
36
36
|
def sendstatus(
|
|
37
|
-
handle:
|
|
37
|
+
handle: int | None, status: int, message: str, flag: bool
|
|
38
38
|
) -> None:
|
|
39
39
|
print(message)
|
|
40
40
|
|
|
41
41
|
@staticmethod
|
|
42
|
-
def debugprint(handle:
|
|
42
|
+
def debugprint(handle: int | None, message: str, level: int) -> None:
|
|
43
43
|
print(message)
|
|
44
44
|
|
|
45
45
|
@staticmethod
|
soar_sdk/shims/phantom/vault.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING, Any
|
|
1
|
+
from typing import TYPE_CHECKING, Any
|
|
2
2
|
|
|
3
3
|
if TYPE_CHECKING:
|
|
4
4
|
from soar_sdk.abstract import SOARClient
|
|
5
5
|
from abc import abstractmethod
|
|
6
6
|
from soar_sdk.exceptions import SoarAPIError
|
|
7
|
+
from datetime import UTC
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class VaultBase:
|
|
@@ -19,9 +20,9 @@ class VaultBase:
|
|
|
19
20
|
def create_attachment(
|
|
20
21
|
self,
|
|
21
22
|
container_id: int,
|
|
22
|
-
file_content:
|
|
23
|
+
file_content: str | bytes,
|
|
23
24
|
file_name: str,
|
|
24
|
-
metadata:
|
|
25
|
+
metadata: dict[str, str] | None = None,
|
|
25
26
|
) -> str:
|
|
26
27
|
"""Creates a vault attachment from file content. This differs from add_attachment because it doesn't require the file to exist locally."""
|
|
27
28
|
pass
|
|
@@ -32,7 +33,7 @@ class VaultBase:
|
|
|
32
33
|
container_id: int,
|
|
33
34
|
file_location: str,
|
|
34
35
|
file_name: str,
|
|
35
|
-
metadata:
|
|
36
|
+
metadata: dict[str, str] | None = None,
|
|
36
37
|
) -> str:
|
|
37
38
|
"""Add an attachment to vault. This requires the file to exist locally."""
|
|
38
39
|
pass
|
|
@@ -40,9 +41,9 @@ class VaultBase:
|
|
|
40
41
|
@abstractmethod
|
|
41
42
|
def get_attachment(
|
|
42
43
|
self,
|
|
43
|
-
vault_id:
|
|
44
|
-
file_name:
|
|
45
|
-
container_id:
|
|
44
|
+
vault_id: str | None = None,
|
|
45
|
+
file_name: str | None = None,
|
|
46
|
+
container_id: int | None = None,
|
|
46
47
|
download_file: bool = True,
|
|
47
48
|
) -> list[dict[str, Any]]:
|
|
48
49
|
"""Returns vault attachments based on the provided query parameters."""
|
|
@@ -51,9 +52,9 @@ class VaultBase:
|
|
|
51
52
|
@abstractmethod
|
|
52
53
|
def delete_attachment(
|
|
53
54
|
self,
|
|
54
|
-
vault_id:
|
|
55
|
-
file_name:
|
|
56
|
-
container_id:
|
|
55
|
+
vault_id: str | None = None,
|
|
56
|
+
file_name: str | None = None,
|
|
57
|
+
container_id: int | None = None,
|
|
57
58
|
remove_all: bool = False,
|
|
58
59
|
) -> list[str]:
|
|
59
60
|
"""Deletes vault attachments based on the provided query parameters."""
|
|
@@ -79,9 +80,9 @@ if _soar_is_available:
|
|
|
79
80
|
def create_attachment(
|
|
80
81
|
self,
|
|
81
82
|
container_id: int,
|
|
82
|
-
file_content:
|
|
83
|
+
file_content: str | bytes,
|
|
83
84
|
file_name: str,
|
|
84
|
-
metadata:
|
|
85
|
+
metadata: dict[str, str] | None = None,
|
|
85
86
|
) -> str:
|
|
86
87
|
resp_json = Vault.create_attachment(
|
|
87
88
|
file_content, container_id, file_name, metadata
|
|
@@ -96,7 +97,7 @@ if _soar_is_available:
|
|
|
96
97
|
container_id: int,
|
|
97
98
|
file_location: str,
|
|
98
99
|
file_name: str,
|
|
99
|
-
metadata:
|
|
100
|
+
metadata: dict[str, str] | None = None,
|
|
100
101
|
) -> str:
|
|
101
102
|
resp_json = vault_add(container_id, file_location, file_name, metadata)
|
|
102
103
|
if not resp_json.get("succeeded"):
|
|
@@ -106,9 +107,9 @@ if _soar_is_available:
|
|
|
106
107
|
|
|
107
108
|
def get_attachment(
|
|
108
109
|
self,
|
|
109
|
-
vault_id:
|
|
110
|
-
file_name:
|
|
111
|
-
container_id:
|
|
110
|
+
vault_id: str | None = None,
|
|
111
|
+
file_name: str | None = None,
|
|
112
|
+
container_id: int | None = None,
|
|
112
113
|
download_file: bool = True,
|
|
113
114
|
) -> list[dict[str, Any]]:
|
|
114
115
|
success, _, attachment = vault_info(
|
|
@@ -120,9 +121,9 @@ if _soar_is_available:
|
|
|
120
121
|
|
|
121
122
|
def delete_attachment(
|
|
122
123
|
self,
|
|
123
|
-
vault_id:
|
|
124
|
-
file_name:
|
|
125
|
-
container_id:
|
|
124
|
+
vault_id: str | None = None,
|
|
125
|
+
file_name: str | None = None,
|
|
126
|
+
container_id: int | None = None,
|
|
126
127
|
remove_all: bool = False,
|
|
127
128
|
) -> list[str]:
|
|
128
129
|
success, message, deleted_file_names = vault_delete(
|
|
@@ -143,7 +144,7 @@ else:
|
|
|
143
144
|
from pathlib import Path
|
|
144
145
|
import secrets
|
|
145
146
|
import random
|
|
146
|
-
from datetime import datetime
|
|
147
|
+
from datetime import datetime
|
|
147
148
|
import hashlib
|
|
148
149
|
from soar_sdk.logging import getLogger
|
|
149
150
|
from soar_sdk.models.vault_attachment import VaultAttachment
|
|
@@ -162,9 +163,9 @@ else:
|
|
|
162
163
|
def create_attachment(
|
|
163
164
|
self,
|
|
164
165
|
container_id: int,
|
|
165
|
-
file_content:
|
|
166
|
+
file_content: str | bytes,
|
|
166
167
|
file_name: str,
|
|
167
|
-
metadata:
|
|
168
|
+
metadata: dict[str, str] | None = None,
|
|
168
169
|
) -> str:
|
|
169
170
|
if is_client_authenticated(self.soar_client.client):
|
|
170
171
|
data = {
|
|
@@ -203,7 +204,7 @@ else:
|
|
|
203
204
|
container_id=container_id,
|
|
204
205
|
container="test_container",
|
|
205
206
|
created_via="upload",
|
|
206
|
-
create_time=datetime.now(
|
|
207
|
+
create_time=datetime.now(UTC).isoformat(),
|
|
207
208
|
user="Phantom User",
|
|
208
209
|
vault_document=doc_id,
|
|
209
210
|
vault_id=vault_id,
|
|
@@ -224,7 +225,7 @@ else:
|
|
|
224
225
|
container_id: int,
|
|
225
226
|
file_location: str,
|
|
226
227
|
file_name: str,
|
|
227
|
-
metadata:
|
|
228
|
+
metadata: dict[str, str] | None = None,
|
|
228
229
|
) -> str:
|
|
229
230
|
metadata = metadata or {}
|
|
230
231
|
|
|
@@ -265,7 +266,7 @@ else:
|
|
|
265
266
|
container_id=container_id,
|
|
266
267
|
container="test_container",
|
|
267
268
|
created_via="upload",
|
|
268
|
-
create_time=datetime.now(
|
|
269
|
+
create_time=datetime.now(UTC).isoformat(),
|
|
269
270
|
user="Phantom User",
|
|
270
271
|
vault_document=doc_id,
|
|
271
272
|
vault_id=vault_id,
|
|
@@ -283,9 +284,9 @@ else:
|
|
|
283
284
|
|
|
284
285
|
def get_attachment(
|
|
285
286
|
self,
|
|
286
|
-
vault_id:
|
|
287
|
-
file_name:
|
|
288
|
-
container_id:
|
|
287
|
+
vault_id: str | None = None,
|
|
288
|
+
file_name: str | None = None,
|
|
289
|
+
container_id: int | None = None,
|
|
289
290
|
download_file: bool = True,
|
|
290
291
|
) -> list[dict[str, Any]]:
|
|
291
292
|
if not any([vault_id, file_name, container_id]):
|
|
@@ -295,7 +296,7 @@ else:
|
|
|
295
296
|
|
|
296
297
|
results = []
|
|
297
298
|
if is_client_authenticated(self.soar_client.client):
|
|
298
|
-
query_params: dict[str,
|
|
299
|
+
query_params: dict[str, str | int] = {"pretty": ""}
|
|
299
300
|
if vault_id:
|
|
300
301
|
query_params["_filter_vault_document__hash"] = (
|
|
301
302
|
f'"{vault_id.lower()}"'
|
|
@@ -319,22 +320,22 @@ else:
|
|
|
319
320
|
if vault_id:
|
|
320
321
|
res = self.__storage.get(vault_id)
|
|
321
322
|
if res:
|
|
322
|
-
results.append(res.
|
|
323
|
+
results.append(res.model_dump())
|
|
323
324
|
|
|
324
325
|
if any((container_id, file_name)):
|
|
325
326
|
for _, res in self.__storage.items():
|
|
326
327
|
if (
|
|
327
328
|
file_name and file_name in res.file_path
|
|
328
329
|
) or container_id == res.container_id:
|
|
329
|
-
results.append(res.
|
|
330
|
+
results.append(res.model_dump())
|
|
330
331
|
|
|
331
332
|
return results
|
|
332
333
|
|
|
333
334
|
def delete_attachment(
|
|
334
335
|
self,
|
|
335
|
-
vault_id:
|
|
336
|
-
file_name:
|
|
337
|
-
container_id:
|
|
336
|
+
vault_id: str | None = None,
|
|
337
|
+
file_name: str | None = None,
|
|
338
|
+
container_id: int | None = None,
|
|
338
339
|
remove_all: bool = False,
|
|
339
340
|
) -> list[str]:
|
|
340
341
|
vault_enteries = self.get_attachment(vault_id, file_name, container_id)
|
soar_sdk/types.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import typing
|
|
2
|
-
from typing import Any,
|
|
2
|
+
from typing import Any, Protocol
|
|
3
|
+
from collections.abc import Callable
|
|
3
4
|
|
|
4
5
|
from soar_sdk.meta.actions import ActionMeta
|
|
5
6
|
from soar_sdk.params import Params
|
|
@@ -9,7 +10,7 @@ class Action(Protocol):
|
|
|
9
10
|
"""Type interface for an action definition."""
|
|
10
11
|
|
|
11
12
|
meta: ActionMeta
|
|
12
|
-
params_class:
|
|
13
|
+
params_class: type[Params] | None = None
|
|
13
14
|
|
|
14
15
|
def __call__(self, *args: Any, **kwargs: Any) -> bool: # noqa: ANN401
|
|
15
16
|
"""Execute the action function."""
|
|
@@ -10,7 +10,7 @@ import uuid
|
|
|
10
10
|
import humanize
|
|
11
11
|
import bleach # type: ignore[import-untyped]
|
|
12
12
|
from datetime import datetime, timedelta
|
|
13
|
-
from typing import
|
|
13
|
+
from typing import TypeVar
|
|
14
14
|
from collections.abc import Iterable
|
|
15
15
|
from collections.abc import Iterator
|
|
16
16
|
from jinja2 import Environment
|
|
@@ -77,7 +77,7 @@ def by_key(dictionary: dict, key: str) -> str:
|
|
|
77
77
|
return dictionary.get(key, "")
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
def by_nested_key(dictionary: dict, key: str) ->
|
|
80
|
+
def by_nested_key(dictionary: dict, key: str) -> str | None:
|
|
81
81
|
"""Get nested dictionary value by space-separated key."""
|
|
82
82
|
split_key = key.split()
|
|
83
83
|
src = dictionary.get(split_key[0], None)
|
|
@@ -97,7 +97,7 @@ def typeof(item: object) -> type:
|
|
|
97
97
|
return type(item)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
def safe_intcomma(value:
|
|
100
|
+
def safe_intcomma(value: str | int) -> str:
|
|
101
101
|
"""Format integer with commas, safely handling non-integers."""
|
|
102
102
|
try:
|
|
103
103
|
return f"{int(value):,}"
|
|
@@ -143,7 +143,7 @@ def to_json(obj: object) -> str:
|
|
|
143
143
|
return json.dumps(obj)
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
def absval(obj:
|
|
146
|
+
def absval(obj: int | float) -> int | float:
|
|
147
147
|
"""Get absolute value."""
|
|
148
148
|
return abs(obj)
|
|
149
149
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any
|
|
4
4
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
5
5
|
from soar_sdk.views.template_filters import setup_jinja_env
|
|
6
6
|
from soar_sdk.paths import SDK_TEMPLATES
|
|
@@ -107,7 +107,7 @@ class JinjaTemplateRenderer(TemplateRenderer):
|
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
def get_template_renderer(
|
|
110
|
-
engine:
|
|
110
|
+
engine: str | None = None, templates_dir: str | None = None
|
|
111
111
|
) -> TemplateRenderer:
|
|
112
112
|
"""Factory function to get the appropriate template renderer.
|
|
113
113
|
|
soar_sdk/views/view_parser.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
from typing import (
|
|
2
2
|
Any,
|
|
3
|
-
Callable,
|
|
4
3
|
TypeVar,
|
|
5
|
-
Union,
|
|
6
4
|
get_origin,
|
|
7
5
|
get_args,
|
|
8
6
|
Generic,
|
|
9
7
|
cast,
|
|
10
8
|
)
|
|
9
|
+
from collections.abc import Callable
|
|
11
10
|
import inspect
|
|
12
11
|
from pydantic import BaseModel
|
|
13
12
|
from soar_sdk.action_results import ActionOutput
|
|
@@ -68,7 +67,7 @@ class ViewFunctionParser(Generic[T]):
|
|
|
68
67
|
for result in action_results:
|
|
69
68
|
for data_item in result.get_data():
|
|
70
69
|
try:
|
|
71
|
-
parsed_output = self.output_class.
|
|
70
|
+
parsed_output = self.output_class.model_validate(data_item)
|
|
72
71
|
parsed_outputs.append(parsed_output)
|
|
73
72
|
except Exception as e:
|
|
74
73
|
output_class_name = self.output_class.__name__
|
|
@@ -85,7 +84,7 @@ class ViewFunctionParser(Generic[T]):
|
|
|
85
84
|
context: ViewContext,
|
|
86
85
|
*args: Any, # noqa: ANN401
|
|
87
86
|
**kwargs: Any, # noqa: ANN401
|
|
88
|
-
) ->
|
|
87
|
+
) -> str | dict | BaseModel:
|
|
89
88
|
"""Wrapper around the object's view function which massages platform inputs as necessary.
|
|
90
89
|
|
|
91
90
|
Takes the JSON list of AppRun results provided by Splunk SOAR, parses that into
|
soar_sdk/webhooks/models.py
CHANGED
|
@@ -2,7 +2,8 @@ import json
|
|
|
2
2
|
import base64
|
|
3
3
|
import mimetypes
|
|
4
4
|
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TypeVar, Generic, Any, IO
|
|
6
|
+
from collections.abc import Callable
|
|
6
7
|
from pydantic import BaseModel, Field
|
|
7
8
|
|
|
8
9
|
from soar_sdk.asset import BaseAsset
|
|
@@ -18,7 +19,7 @@ class WebhookRequest(BaseModel, Generic[AssetType]):
|
|
|
18
19
|
headers: dict[str, str]
|
|
19
20
|
path_parts: list[str]
|
|
20
21
|
query: dict[str, list[str]]
|
|
21
|
-
body:
|
|
22
|
+
body: str | None
|
|
22
23
|
asset: AssetType
|
|
23
24
|
soar_base_url: str
|
|
24
25
|
soar_auth_token: str
|
|
@@ -65,7 +66,7 @@ class WebhookResponse(BaseModel):
|
|
|
65
66
|
def text_response(
|
|
66
67
|
content: str,
|
|
67
68
|
status_code: int = 200,
|
|
68
|
-
extra_headers:
|
|
69
|
+
extra_headers: dict[str, Any] | None = None,
|
|
69
70
|
) -> "WebhookResponse":
|
|
70
71
|
"""Build a WebhookResponse object given raw textual content.
|
|
71
72
|
|
|
@@ -83,7 +84,7 @@ class WebhookResponse(BaseModel):
|
|
|
83
84
|
def json_response(
|
|
84
85
|
content: dict,
|
|
85
86
|
status_code: int = 200,
|
|
86
|
-
extra_headers:
|
|
87
|
+
extra_headers: dict[str, Any] | None = None,
|
|
87
88
|
) -> "WebhookResponse":
|
|
88
89
|
"""Build a WebhookResponse object given a dictionary, to be interpreted as JSON.
|
|
89
90
|
|
|
@@ -101,9 +102,9 @@ class WebhookResponse(BaseModel):
|
|
|
101
102
|
def file_response(
|
|
102
103
|
fd: IO,
|
|
103
104
|
filename: str,
|
|
104
|
-
content_type:
|
|
105
|
+
content_type: str | None = None,
|
|
105
106
|
status_code: int = 200,
|
|
106
|
-
extra_headers:
|
|
107
|
+
extra_headers: dict[str, Any] | None = None,
|
|
107
108
|
) -> "WebhookResponse":
|
|
108
109
|
"""Build a webhook response using the data in a given open file-like object.
|
|
109
110
|
|
soar_sdk/webhooks/routing.py
CHANGED
|
@@ -5,7 +5,8 @@ This module provides a router for mapping URL patterns to handler functions.
|
|
|
5
5
|
|
|
6
6
|
import re
|
|
7
7
|
from dataclasses import dataclass
|
|
8
|
-
from typing import
|
|
8
|
+
from typing import TypeVar, Generic
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from collections.abc import Sequence
|
|
10
11
|
|
|
11
12
|
from soar_sdk.webhooks.models import WebhookRequest, WebhookResponse
|
|
@@ -45,7 +46,7 @@ class Router(Generic[AssetType]):
|
|
|
45
46
|
self,
|
|
46
47
|
pattern: str,
|
|
47
48
|
handler: Callable[[WebhookRequest], WebhookResponse],
|
|
48
|
-
methods:
|
|
49
|
+
methods: Sequence[str] | None = None,
|
|
49
50
|
) -> None:
|
|
50
51
|
"""Register a new route with the router.
|
|
51
52
|
|
|
@@ -146,7 +147,7 @@ class Router(Generic[AssetType]):
|
|
|
146
147
|
return False
|
|
147
148
|
|
|
148
149
|
# Check each segment
|
|
149
|
-
for part1, part2 in zip(parts1, parts2):
|
|
150
|
+
for part1, part2 in zip(parts1, parts2, strict=False):
|
|
150
151
|
is_param1 = part1.startswith("<") and part1.endswith(">")
|
|
151
152
|
is_param2 = part2.startswith("<") and part2.endswith(">")
|
|
152
153
|
|