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.
Files changed (58) hide show
  1. soar_sdk/abstract.py +38 -41
  2. soar_sdk/action_results.py +41 -18
  3. soar_sdk/actions_manager.py +10 -13
  4. soar_sdk/apis/utils.py +3 -3
  5. soar_sdk/apis/vault.py +10 -10
  6. soar_sdk/app.py +58 -51
  7. soar_sdk/app_cli_runner.py +8 -8
  8. soar_sdk/app_client.py +10 -10
  9. soar_sdk/asset.py +45 -33
  10. soar_sdk/async_utils.py +2 -2
  11. soar_sdk/cli/init/cli.py +7 -9
  12. soar_sdk/cli/manifests/deserializers.py +15 -15
  13. soar_sdk/cli/manifests/processors.py +4 -10
  14. soar_sdk/cli/manifests/serializers.py +16 -8
  15. soar_sdk/cli/package/cli.py +6 -6
  16. soar_sdk/cli/package/utils.py +1 -1
  17. soar_sdk/code_renderers/action_renderer.py +35 -18
  18. soar_sdk/code_renderers/app_renderer.py +1 -2
  19. soar_sdk/code_renderers/asset_renderer.py +4 -5
  20. soar_sdk/code_renderers/renderer.py +2 -2
  21. soar_sdk/code_renderers/templates/pyproject.toml.jinja +1 -1
  22. soar_sdk/compat.py +6 -6
  23. soar_sdk/decorators/action.py +14 -15
  24. soar_sdk/decorators/make_request.py +4 -3
  25. soar_sdk/decorators/on_poll.py +5 -4
  26. soar_sdk/decorators/test_connectivity.py +2 -2
  27. soar_sdk/decorators/view_handler.py +11 -17
  28. soar_sdk/decorators/webhook.py +1 -2
  29. soar_sdk/exceptions.py +1 -4
  30. soar_sdk/field_utils.py +8 -0
  31. soar_sdk/input_spec.py +13 -17
  32. soar_sdk/logging.py +3 -3
  33. soar_sdk/meta/actions.py +6 -22
  34. soar_sdk/meta/app.py +10 -7
  35. soar_sdk/meta/dependencies.py +48 -42
  36. soar_sdk/meta/webhooks.py +12 -12
  37. soar_sdk/models/artifact.py +20 -23
  38. soar_sdk/models/container.py +30 -33
  39. soar_sdk/models/vault_attachment.py +6 -6
  40. soar_sdk/models/view.py +10 -13
  41. soar_sdk/params.py +57 -39
  42. soar_sdk/shims/phantom/action_result.py +4 -4
  43. soar_sdk/shims/phantom/base_connector.py +13 -5
  44. soar_sdk/shims/phantom/install_info.py +15 -2
  45. soar_sdk/shims/phantom/ph_ipc.py +3 -3
  46. soar_sdk/shims/phantom/vault.py +35 -34
  47. soar_sdk/types.py +3 -2
  48. soar_sdk/views/template_filters.py +4 -4
  49. soar_sdk/views/template_renderer.py +2 -2
  50. soar_sdk/views/view_parser.py +3 -4
  51. soar_sdk/webhooks/models.py +7 -6
  52. soar_sdk/webhooks/routing.py +4 -3
  53. {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/METADATA +5 -6
  54. splunk_soar_sdk-3.0.0.dist-info/RECORD +104 -0
  55. splunk_soar_sdk-2.3.6.dist-info/RECORD +0 -103
  56. {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/WHEEL +0 -0
  57. {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/entry_points.txt +0 -0
  58. {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/licenses/LICENSE +0 -0
soar_sdk/abstract.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from abc import abstractmethod
2
- from typing import Any, Optional, Union, Generic, TypeVar
2
+ from typing import Any, Generic, TypeVar
3
3
  from collections.abc import Mapping, Iterable, AsyncIterable
4
4
 
5
5
  from soar_sdk.apis.vault import Vault
@@ -8,9 +8,9 @@ from soar_sdk.apis.container import Container
8
8
  from soar_sdk.action_results import ActionOutput
9
9
  import httpx
10
10
  from pydantic.dataclasses import dataclass
11
- from pydantic import validator
11
+ from pydantic import field_validator
12
12
 
13
- JSONType = Union[dict[str, Any], list[Any], str, int, float, bool, None]
13
+ JSONType = dict[str, Any] | list[Any] | str | int | float | bool | None
14
14
  SummaryType = TypeVar("SummaryType", bound=ActionOutput)
15
15
 
16
16
 
@@ -23,7 +23,8 @@ class SOARClientAuth:
23
23
  password: str = ""
24
24
  user_session_token: str = ""
25
25
 
26
- @validator("base_url")
26
+ @field_validator("base_url")
27
+ @classmethod
27
28
  def validate_phantom_url(cls, value: str) -> str:
28
29
  """Validate and format the base URL for the SOAR API."""
29
30
  return (
@@ -74,13 +75,13 @@ class SOARClient(Generic[SummaryType]):
74
75
  self,
75
76
  endpoint: str,
76
77
  *,
77
- params: Optional[Union[dict[str, Any], httpx.QueryParams]] = None,
78
- headers: Optional[dict[str, str]] = None,
79
- cookies: Optional[dict[str, str]] = None,
80
- timeout: Optional[httpx.Timeout] = None,
81
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
78
+ params: dict[str, Any] | httpx.QueryParams | None = None,
79
+ headers: dict[str, str] | None = None,
80
+ cookies: dict[str, str] | None = None,
81
+ timeout: httpx.Timeout | None = None,
82
+ auth: httpx.Auth | tuple[str, str] | None = None,
82
83
  follow_redirects: bool = False,
83
- extensions: Optional[Mapping[str, Any]] = None,
84
+ extensions: Mapping[str, Any] | None = None,
84
85
  ) -> httpx.Response:
85
86
  """Perform a GET request to the specific endpoint using the SOAR client."""
86
87
  response = self.client.get(
@@ -100,19 +101,17 @@ class SOARClient(Generic[SummaryType]):
100
101
  self,
101
102
  endpoint: str,
102
103
  *,
103
- content: Optional[
104
- Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
105
- ] = None,
106
- data: Optional[Mapping[str, Any]] = None,
107
- files: Optional[dict[str, Any]] = None,
108
- json: Optional[JSONType] = None,
109
- params: Optional[dict[str, Any]] = None,
110
- headers: Optional[dict[str, str]] = None,
111
- cookies: Optional[dict[str, str]] = None,
112
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
113
- timeout: Optional[Union[float, httpx.Timeout]] = None,
104
+ content: str | bytes | Iterable[bytes] | AsyncIterable[bytes] | None = None,
105
+ data: Mapping[str, Any] | None = None,
106
+ files: dict[str, Any] | None = None,
107
+ json: JSONType | None = None,
108
+ params: dict[str, Any] | None = None,
109
+ headers: dict[str, str] | None = None,
110
+ cookies: dict[str, str] | None = None,
111
+ auth: httpx.Auth | tuple[str, str] | None = None,
112
+ timeout: float | httpx.Timeout | None = None,
114
113
  follow_redirects: bool = True,
115
- extensions: Optional[Mapping[str, Any]] = None,
114
+ extensions: Mapping[str, Any] | None = None,
116
115
  ) -> httpx.Response:
117
116
  """Perform a POST request to the specific endpoint using the SOAR client."""
118
117
  headers = headers or {}
@@ -138,19 +137,17 @@ class SOARClient(Generic[SummaryType]):
138
137
  self,
139
138
  endpoint: str,
140
139
  *,
141
- content: Optional[
142
- Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
143
- ] = None,
144
- data: Optional[Mapping[str, Any]] = None,
145
- files: Optional[dict[str, Any]] = None,
146
- json: Optional[JSONType] = None,
147
- params: Optional[dict[str, Any]] = None,
148
- headers: Optional[dict[str, str]] = None,
149
- cookies: Optional[dict[str, str]] = None,
150
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
151
- timeout: Optional[Union[float, httpx.Timeout]] = None,
140
+ content: str | bytes | Iterable[bytes] | AsyncIterable[bytes] | None = None,
141
+ data: Mapping[str, Any] | None = None,
142
+ files: dict[str, Any] | None = None,
143
+ json: JSONType | None = None,
144
+ params: dict[str, Any] | None = None,
145
+ headers: dict[str, str] | None = None,
146
+ cookies: dict[str, str] | None = None,
147
+ auth: httpx.Auth | tuple[str, str] | None = None,
148
+ timeout: float | httpx.Timeout | None = None,
152
149
  follow_redirects: bool = True,
153
- extensions: Optional[Mapping[str, Any]] = None,
150
+ extensions: Mapping[str, Any] | None = None,
154
151
  ) -> httpx.Response:
155
152
  """Perform a PUT request to the specific endpoint using the SOAR client."""
156
153
  headers = headers or {}
@@ -176,13 +173,13 @@ class SOARClient(Generic[SummaryType]):
176
173
  self,
177
174
  endpoint: str,
178
175
  *,
179
- params: Optional[Union[dict[str, Any], httpx.QueryParams]] = None,
180
- headers: Optional[dict[str, str]] = None,
181
- cookies: Optional[dict[str, str]] = None,
182
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
183
- timeout: Optional[httpx.Timeout] = None,
176
+ params: dict[str, Any] | httpx.QueryParams | None = None,
177
+ headers: dict[str, str] | None = None,
178
+ cookies: dict[str, str] | None = None,
179
+ auth: httpx.Auth | tuple[str, str] | None = None,
180
+ timeout: httpx.Timeout | None = None,
184
181
  follow_redirects: bool = False,
185
- extensions: Optional[Mapping[str, Any]] = None,
182
+ extensions: Mapping[str, Any] | None = None,
186
183
  ) -> httpx.Response:
187
184
  """Perform a DELETE request to the specific endpoint using the SOAR client."""
188
185
  headers = headers or {}
@@ -229,7 +226,7 @@ class SOARClient(Generic[SummaryType]):
229
226
  pass
230
227
 
231
228
  @abstractmethod
232
- def get_summary(self) -> Optional[SummaryType]:
229
+ def get_summary(self) -> SummaryType | None:
233
230
  """Get the summary for the action run."""
234
231
  pass
235
232
 
@@ -1,12 +1,15 @@
1
- from typing import Optional, Union, get_origin, get_args, Any
1
+ from typing import Union, get_origin, get_args, Any
2
2
  from collections.abc import Iterator
3
- from typing_extensions import NotRequired, TypedDict
4
- from pydantic import BaseModel, Field
3
+ from typing_extensions import TypedDict
4
+ from typing import NotRequired
5
+ from pydantic import BaseModel, Field, ConfigDict
5
6
  import itertools
7
+ import types
6
8
 
7
9
  from soar_sdk.compat import remove_when_soar_newer_than
8
10
  from soar_sdk.shims.phantom.action_result import ActionResult as PhantomActionResult
9
11
  from soar_sdk.meta.datatypes import as_datatype
12
+ from soar_sdk.field_utils import parse_json_schema_extra
10
13
 
11
14
  remove_when_soar_newer_than(
12
15
  "7.0.0", "NotRequired from typing_extensions is in typing in Python 3.11+"
@@ -40,7 +43,7 @@ class ActionResult(PhantomActionResult):
40
43
  self,
41
44
  status: bool,
42
45
  message: str,
43
- param: Optional[dict] = None,
46
+ param: dict | None = None,
44
47
  ) -> None:
45
48
  """Initialize an ActionResult with status, message, and optional parameters.
46
49
 
@@ -82,16 +85,16 @@ class OutputFieldSpecification(TypedDict):
82
85
  data_path: str
83
86
  data_type: str
84
87
  contains: NotRequired[list[str]]
85
- example_values: NotRequired[list[Union[str, float, bool]]]
88
+ example_values: NotRequired[list[str | float | bool]]
86
89
  column_name: NotRequired[str]
87
90
  column_order: NotRequired[int]
88
91
 
89
92
 
90
93
  def OutputField(
91
- cef_types: Optional[list[str]] = None,
92
- example_values: Optional[list[Union[str, float, bool]]] = None,
93
- alias: Optional[str] = None,
94
- column_name: Optional[str] = None,
94
+ cef_types: list[str] | None = None,
95
+ example_values: list[str | float | bool] | None = None,
96
+ alias: str | None = None,
97
+ column_name: str | None = None,
95
98
  ) -> Any: # noqa: ANN401
96
99
  """Define metadata for an action output field.
97
100
 
@@ -121,11 +124,18 @@ def OutputField(
121
124
  ... )
122
125
  ... count: int = OutputField(example_values=[1, 5, 10])
123
126
  """
127
+ json_schema_extra: dict[str, Any] = {}
128
+ if cef_types is not None:
129
+ json_schema_extra["cef_types"] = cef_types
130
+ if example_values is not None:
131
+ json_schema_extra["examples"] = example_values
132
+ if column_name is not None:
133
+ json_schema_extra["column_name"] = column_name
134
+
124
135
  return Field(
125
- examples=example_values,
136
+ default=...,
126
137
  alias=alias,
127
- cef_types=cef_types,
128
- column_name=column_name,
138
+ json_schema_extra=json_schema_extra if json_schema_extra else None,
129
139
  )
130
140
 
131
141
 
@@ -158,11 +168,14 @@ class ActionOutput(BaseModel):
158
168
  Nested ActionOutput classes are supported for complex data structures.
159
169
  """
160
170
 
171
+ # Allow instantiation with both field names and aliases for backward compatibility
172
+ model_config = ConfigDict(populate_by_name=True)
173
+
161
174
  @classmethod
162
175
  def _to_json_schema(
163
176
  cls,
164
177
  parent_datapath: str = "action_result.data.*",
165
- column_order_counter: Optional[itertools.count] = None,
178
+ column_order_counter: itertools.count | None = None,
166
179
  ) -> Iterator[OutputFieldSpecification]:
167
180
  """Convert the ActionOutput class to SOAR-compatible JSON schema.
168
181
 
@@ -191,15 +204,18 @@ class ActionOutput(BaseModel):
191
204
  if column_order_counter is None:
192
205
  column_order_counter = itertools.count()
193
206
 
194
- for _field_name, field in cls.__fields__.items():
207
+ for _field_name, field in cls.model_fields.items():
195
208
  field_name = alias if (alias := field.alias) else _field_name
196
209
 
197
210
  field_type = field.annotation
211
+ if field_type is None:
212
+ continue
213
+
198
214
  datapath = parent_datapath + f".{field_name}"
199
215
 
200
216
  # Handle lists and optional types, even nested ones
201
217
  origin = get_origin(field_type)
202
- while origin in [list, Union, Optional]:
218
+ while origin in [list, Union, types.UnionType]:
203
219
  type_args = [
204
220
  arg
205
221
  for arg in get_args(field_type)
@@ -221,6 +237,11 @@ class ActionOutput(BaseModel):
221
237
  field_type = type_args[0]
222
238
  origin = get_origin(field_type)
223
239
 
240
+ if not isinstance(field_type, type):
241
+ raise TypeError(
242
+ f"Output field {field_name} has invalid type annotation: {field_type}"
243
+ )
244
+
224
245
  if issubclass(field_type, ActionOutput):
225
246
  # If the field is another ActionOutput, recursively call _to_json_schema
226
247
  yield from field_type._to_json_schema(datapath, column_order_counter)
@@ -237,15 +258,17 @@ class ActionOutput(BaseModel):
237
258
  data_path=datapath, data_type=type_name
238
259
  )
239
260
 
240
- if cef_types := field.field_info.extra.get("cef_types"):
261
+ json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
262
+
263
+ if cef_types := json_schema_extra.get("cef_types"):
241
264
  schema_field["contains"] = cef_types
242
- if examples := field.field_info.extra.get("examples"):
265
+ if examples := json_schema_extra.get("examples"):
243
266
  schema_field["example_values"] = examples
244
267
 
245
268
  if field_type is bool:
246
269
  schema_field["example_values"] = [True, False]
247
270
 
248
- column_name = field.field_info.extra.get("column_name")
271
+ column_name = json_schema_extra.get("column_name")
249
272
 
250
273
  if column_name is not None:
251
274
  schema_field["column_name"] = column_name
@@ -1,6 +1,7 @@
1
- from typing import Optional, Any
1
+ from typing import Any
2
2
  import os
3
3
 
4
+ from soar_sdk.compat import remove_when_soar_newer_than
4
5
  from soar_sdk.input_spec import InputSpecification
5
6
  from soar_sdk.shims.phantom.base_connector import BaseConnector
6
7
 
@@ -8,13 +9,9 @@ from soar_sdk.meta.actions import ActionMeta
8
9
  from soar_sdk.types import Action
9
10
  from pydantic import ValidationError
10
11
  from soar_sdk.shims.phantom.action_result import ActionResult as PhantomActionResult
12
+ from soar_sdk.shims.phantom.install_info import is_onprem_broker_install
11
13
  from soar_sdk.logging import getLogger
12
14
 
13
- try:
14
- from phantom_common import install_info
15
- except ImportError:
16
- install_info = None
17
-
18
15
 
19
16
  _INGEST_STATE_KEY = "ingestion_state"
20
17
  _AUTH_STATE_KEY = "auth_state"
@@ -34,7 +31,7 @@ class ActionsManager(BaseConnector):
34
31
  self.auth_state: dict = {}
35
32
  self.asset_cache: dict = {}
36
33
 
37
- def get_action(self, identifier: str) -> Optional[Action]:
34
+ def get_action(self, identifier: str) -> Action | None:
38
35
  """Convenience method for getting an Action callable from its identifier.
39
36
 
40
37
  Returns None if there are no actions managed by this object matching the given
@@ -63,14 +60,12 @@ class ActionsManager(BaseConnector):
63
60
  """
64
61
  self._actions[action_identifier] = wrapped_function
65
62
 
66
- def handle(
67
- self, input_data: InputSpecification, handle: Optional[int] = None
68
- ) -> str:
63
+ def handle(self, input_data: InputSpecification, handle: int | None = None) -> str:
69
64
  """Runs handling of the input data on connector."""
70
65
  action_id = input_data.identifier
71
66
  if self.get_action(action_id):
72
67
  self.print_progress_message = True
73
- return self._handle_action(input_data.json(), handle or 0)
68
+ return self._handle_action(input_data.model_dump_json(), handle or 0)
74
69
  else:
75
70
  raise RuntimeError(
76
71
  f"Action {action_id} not recognized"
@@ -89,7 +84,7 @@ class ActionsManager(BaseConnector):
89
84
 
90
85
  if handler := self.get_action(action_id):
91
86
  try:
92
- params = handler.meta.parameters.parse_obj(param)
87
+ params = handler.meta.parameters.model_validate(param)
93
88
  except (ValueError, ValidationError) as e:
94
89
  self.save_progress(
95
90
  f"Validation Error - the params data for action could not be parsed: {e!s}"
@@ -150,8 +145,10 @@ class ActionsManager(BaseConnector):
150
145
 
151
146
  Returns APP_HOME directly on brokers, which contains the correct SDK app path.
152
147
  """
148
+ # Remove when 7.1.0 is the min supported broker version
149
+ remove_when_soar_newer_than("7.1.1")
153
150
  # On AB, APP_HOME is set by spawn to the full app path at runtime
154
- if install_info and install_info.is_onprem_broker_install():
151
+ if is_onprem_broker_install():
155
152
  return os.getenv("APP_HOME", "")
156
153
 
157
154
  # For non-broker just proceed as we did before
soar_sdk/apis/utils.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import httpx
2
- from typing import Optional, Any
2
+ from typing import Any
3
3
  from collections.abc import Generator
4
4
 
5
5
 
@@ -11,9 +11,9 @@ def is_client_authenticated(client: httpx.Client) -> bool:
11
11
  def get_request_iter_pages(
12
12
  client: httpx.Client,
13
13
  endpoint: str,
14
- params: Optional[dict] = None,
14
+ params: dict | None = None,
15
15
  page_size: int = 50,
16
- ) -> Generator[Any, None, None]:
16
+ ) -> Generator[Any]:
17
17
  """Iterate through REST JSON results using the provided paging."""
18
18
  params = params or {}
19
19
 
soar_sdk/apis/vault.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Optional, Union
1
+ from typing import TYPE_CHECKING
2
2
  from soar_sdk.models.vault_attachment import VaultAttachment
3
3
  from soar_sdk.shims.phantom.vault import PhantomVault, VaultBase
4
4
 
@@ -47,9 +47,9 @@ class Vault:
47
47
  def create_attachment(
48
48
  self,
49
49
  container_id: int,
50
- file_content: Union[str, bytes],
50
+ file_content: str | bytes,
51
51
  file_name: str,
52
- metadata: Optional[dict[str, str]] = None,
52
+ metadata: dict[str, str] | None = None,
53
53
  ) -> str:
54
54
  """Create a vault attachment from file content.
55
55
 
@@ -87,7 +87,7 @@ class Vault:
87
87
  container_id: int,
88
88
  file_location: str,
89
89
  file_name: str,
90
- metadata: Optional[dict[str, str]] = None,
90
+ metadata: dict[str, str] | None = None,
91
91
  ) -> str:
92
92
  """Add an existing file to the vault as an attachment.
93
93
 
@@ -121,9 +121,9 @@ class Vault:
121
121
 
122
122
  def get_attachment(
123
123
  self,
124
- vault_id: Optional[str] = None,
125
- file_name: Optional[str] = None,
126
- container_id: Optional[int] = None,
124
+ vault_id: str | None = None,
125
+ file_name: str | None = None,
126
+ container_id: int | None = None,
127
127
  ) -> list[VaultAttachment]:
128
128
  """Retrieve attachment(s) from the vault.
129
129
 
@@ -161,9 +161,9 @@ class Vault:
161
161
 
162
162
  def delete_attachment(
163
163
  self,
164
- vault_id: Optional[str] = None,
165
- file_name: Optional[str] = None,
166
- container_id: Optional[int] = None,
164
+ vault_id: str | None = None,
165
+ file_name: str | None = None,
166
+ container_id: int | None = None,
167
167
  remove_all: bool = False,
168
168
  ) -> list[str]:
169
169
  """Delete attachment(s) from the vault.