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.
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 +5 -7
  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/finding.py +54 -0
  40. soar_sdk/models/vault_attachment.py +6 -6
  41. soar_sdk/models/view.py +10 -13
  42. soar_sdk/params.py +57 -39
  43. soar_sdk/shims/phantom/action_result.py +4 -4
  44. soar_sdk/shims/phantom/base_connector.py +5 -5
  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.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/METADATA +5 -6
  54. splunk_soar_sdk-3.1.0.dist-info/RECORD +105 -0
  55. splunk_soar_sdk-2.3.7.dist-info/RECORD +0 -103
  56. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/WHEEL +0 -0
  57. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/entry_points.txt +0 -0
  58. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,7 +3,7 @@ import inspect
3
3
  import json
4
4
  from pathlib import Path
5
5
  import typing
6
- from typing import Optional, Any
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: Optional[list[str]] = None) -> argparse.Namespace:
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.parse_obj(params_json)
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.dict()))
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.dict(),
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.json()
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.parse_obj(asset_json),
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: Optional[list[str]] = None) -> None:
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, Union, Optional
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: Optional[BasicAuth] = None
44
+ self.basic_auth: BasicAuth | None = None
45
45
 
46
- self._summary: Optional[SummaryType] = None
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: Optional[Union[dict[str, Any], httpx.QueryParams]] = None,
136
- headers: Optional[dict[str, str]] = None,
137
- cookies: Optional[dict[str, str]] = None,
138
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
139
- timeout: Optional[httpx.Timeout] = None,
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: Optional[Mapping[str, Any]] = None,
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) -> Optional[SummaryType]:
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, Optional, Union
1
+ from typing import Any
2
2
  from zoneinfo import ZoneInfo
3
- from pydantic import BaseModel, root_validator
4
- from pydantic.fields import Field, Undefined
3
+ from pydantic import BaseModel, model_validator, ConfigDict, Field
4
+ from pydantic_core import PydanticUndefined
5
5
 
6
- from typing_extensions import NotRequired, TypedDict
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: Optional[str] = None,
21
+ description: str | None = None,
20
22
  required: bool = True,
21
- default: Optional[Any] = None, # noqa: ANN401
22
- value_list: Optional[list] = None,
23
+ default: Any | None = None, # noqa: ANN401
24
+ value_list: list | None = None,
23
25
  sensitive: bool = False,
24
- alias: Optional[str] = None,
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=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[Union[str, int, float, bool]]
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
- class Config:
110
- """Pydantic configuration for BaseAsset.
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
- @root_validator(pre=True)
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.__fields__:
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.__fields__.items()):
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
- if field.field_info.extra.get("sensitive", False):
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.field_info.description):
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=field.field_info.extra.get("required", True),
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.field_info.default) and default != Undefined:
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 := field.field_info.extra.get("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.__fields__.items()
259
- if field.field_info.extra.get("sensitive", False)
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.__fields__.items()
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, None]) -> list[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, None]) -> list[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, Optional, cast
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: Optional[str] = None,
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: Optional[str] = None,
110
+ product: str | None = None,
111
111
  fips_compliant: bool = False,
112
112
  overwrite: bool = False,
113
- app_content: Optional[list[ast.stmt]] = None,
114
- asset_class: Optional[ast.ClassDef] = None,
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
- Optional[Path],
243
+ Path | None,
246
244
  typer.Argument(
247
245
  file_okay=False,
248
246
  dir_okay=True,
@@ -1,7 +1,7 @@
1
1
  import dataclasses
2
2
  import json
3
3
  from pathlib import Path
4
- from typing import Any, NamedTuple, Optional, TypeVar, Union, cast
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: Optional[list[str]] = None
98
- example_values: Optional[list[Union[str, float, bool]]] = None
99
- column_name: Optional[str] = None
100
- column_order: Optional[int] = None
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.parse_obj(action),
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, Union[list, dict, OutputFieldModel]]:
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, Union[list, dict, OutputFieldModel]],
226
+ field_struct: dict[str, list | dict | OutputFieldModel],
227
227
  path_parts: list[str],
228
228
  field_spec: OutputFieldModel,
229
- ) -> Union[list, dict, OutputFieldModel]:
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, Union[list, dict, OutputFieldModel]],
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=Union[list, dict, OutputFieldModel])
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, Union[list, dict, OutputFieldModel]] = {}
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, Union[list, dict, OutputFieldModel]],
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, Union[dict, list, OutputFieldModel]],
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: Union[list, dict, OutputFieldModel]
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, timezone
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 remove_when_soar_newer_than, UPDATE_TIME_FORMAT
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(timezone.utc).strftime(
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.pip39_dependencies, app_meta.pip313_dependencies = (
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, Optional
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.__fields__.keys())
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: Optional[itertools.count] = None,
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.__fields__.items():
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(field.annotation),
47
+ data_type=as_datatype(annotation),
43
48
  )
44
- if cef_types := field.field_info.extra.get("cef_types"):
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 = field.field_info.extra.get("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: Optional[type[ActionOutput]] = None,
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(
@@ -9,7 +9,7 @@ import json
9
9
  from pathlib import Path
10
10
  import asyncio
11
11
  import time
12
- from typing import Optional, Annotated
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
- Optional[Path],
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
- Optional[Path],
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) -> Optional[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.pip39_dependencies.wheel + app_meta.pip313_dependencies.wheel
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()
@@ -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, None]:
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)