splunk-soar-sdk 3.5.0__py3-none-any.whl → 3.6.1__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.
@@ -24,6 +24,7 @@ class ActionsManager(BaseConnector):
24
24
 
25
25
  self._actions: dict[str, Action] = {}
26
26
  self.__app_dir: Path | None = None
27
+ self.supports_es_polling: bool = False
27
28
 
28
29
  def get_action(self, identifier: str) -> Action | None:
29
30
  """Convenience method for getting an Action callable from its identifier.
@@ -123,13 +124,6 @@ class ActionsManager(BaseConnector):
123
124
  # For non-broker just proceed as we did before
124
125
  return super().get_app_dir()
125
126
 
126
- def send_finding_to_es(self, finding: dict[str, Any]) -> str:
127
- """Send finding to ES.
128
-
129
- Returns finding_id: ID for the finding in ES.
130
- """
131
- return ""
132
-
133
127
  @classmethod
134
128
  def get_soar_base_url(cls) -> str:
135
129
  """Get the base URL of the Splunk SOAR instance this app is running on."""
@@ -0,0 +1,27 @@
1
+ import httpx
2
+ from pydantic import Field
3
+
4
+ from soar_sdk.models.finding import Finding
5
+
6
+
7
+ class CreateFindingResponse(Finding):
8
+ """The return type from creating a Finding."""
9
+
10
+ time: str = Field(alias="_time")
11
+ finding_id: str
12
+
13
+
14
+ class Findings:
15
+ """Client for ES Findings API."""
16
+
17
+ def __init__(self, client: httpx.Client) -> None:
18
+ self._client = client
19
+
20
+ def create(self, finding: Finding) -> CreateFindingResponse:
21
+ """Create a new Finding."""
22
+ res = self._client.post(
23
+ "/services/public/v2/findings",
24
+ data=finding.model_dump(),
25
+ )
26
+ res.raise_for_status()
27
+ return CreateFindingResponse(**res.json())
@@ -74,6 +74,8 @@ class ManifestProcessor:
74
74
  f"{module_name}.{app_instance_name}.handle_webhook"
75
75
  )
76
76
 
77
+ app_meta.supports_es_polling = app.actions_manager.supports_es_polling
78
+
77
79
  return app_meta
78
80
 
79
81
  def create(self) -> None:
@@ -27,35 +27,51 @@ package = typer.Typer()
27
27
  console = Console() # For printing lots of pretty colors and stuff
28
28
 
29
29
 
30
- async def collect_all_wheels(wheels: set[DependencyWheel]) -> list[tuple[str, bytes]]:
31
- """Asynchronously collect all wheels from the given set of DependencyWheel objects."""
32
- # Create progress bar for tracking wheel collection
30
+ async def collect_all_wheels(wheels: list[DependencyWheel]) -> list[tuple[str, bytes]]:
31
+ """Asynchronously collect all wheels from the given list of DependencyWheel objects.
32
+
33
+ Downloads/builds each unique wheel once while updating every DependencyWheel instance
34
+ so the manifest records the final wheel filenames.
35
+ """
36
+ dedupe_map: dict[int, list[DependencyWheel]] = {}
37
+ for wheel in wheels:
38
+ key = hash(wheel)
39
+ dedupe_map.setdefault(key, []).append(wheel)
40
+
33
41
  progress = tqdm(
34
- total=len(wheels),
42
+ total=len(dedupe_map),
35
43
  desc="Downloading wheels",
36
44
  unit="wheel",
37
45
  colour="green",
38
46
  ncols=80,
39
47
  )
40
48
 
41
- async def collect_from_wheel(wheel: DependencyWheel) -> list[tuple[str, bytes]]:
42
- result = []
43
- # This actually is covered, but pytest-cov branch coverage
44
- # has a bug with the end of async for loops
49
+ async def collect_from_wheel(
50
+ cache_key: int, wheel: DependencyWheel
51
+ ) -> tuple[int, list[tuple[str, bytes]]]:
52
+ result: list[tuple[str, bytes]] = []
45
53
  async for path, data in wheel.collect_wheels(): # pragma: no cover
46
54
  result.append((path, data))
47
- # Update progress bar after each wheel is processed
48
55
  progress.update(1)
49
- return result
56
+ return cache_key, result
50
57
 
51
- # Use asyncio.gather to truly run all wheel collections concurrently
52
58
  with contextlib.closing(progress):
53
- wheel_data_lists = await asyncio.gather(
54
- *(collect_from_wheel(wheel) for wheel in wheels)
59
+ gathered_results = await asyncio.gather(
60
+ *(
61
+ collect_from_wheel(key, wheel_group[0])
62
+ for key, wheel_group in dedupe_map.items()
63
+ )
55
64
  )
56
65
 
57
- # Use itertools.chain to flatten the list of lists
58
- return list(chain.from_iterable(wheel_data_lists))
66
+ cache = dict(gathered_results)
67
+
68
+ for key, wheel_group in dedupe_map.items():
69
+ for path, _ in cache[key]:
70
+ wheel_name = Path(path).name
71
+ for wheel in wheel_group:
72
+ wheel._record_built_wheel(wheel_name)
73
+
74
+ return list(chain.from_iterable(cache.values()))
59
75
 
60
76
 
61
77
  @package.command()
@@ -124,13 +140,13 @@ def build(
124
140
 
125
141
  with tarfile.open(output_file, "w:gz") as app_tarball:
126
142
  # Collect all wheels from both Python versions
127
- all_wheels = set(
143
+ all_wheels = (
128
144
  app_meta.pip313_dependencies.wheel + app_meta.pip314_dependencies.wheel
129
145
  )
130
146
 
131
147
  # Run the async collection function within an event loop
132
148
  console.print(
133
- f"[yellow]Collecting [bold]{len(all_wheels)}[/bold] wheel{'' if len(all_wheels) == 1 else 's'} for package[/]"
149
+ f"[yellow]Collecting [bold]{len(all_wheels)}[/bold] wheel{'' if len(set(all_wheels)) == 1 else 's'} for package[/]"
134
150
  )
135
151
  wheel_data = asyncio.run(collect_all_wheels(all_wheels))
136
152
 
@@ -1,15 +1,17 @@
1
+ import asyncio
1
2
  import inspect
2
- from collections.abc import Callable, Iterator
3
+ from collections.abc import Callable
3
4
  from functools import wraps
4
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING, Any, get_args
6
+
7
+ from pydantic import ValidationError
5
8
 
6
9
  from soar_sdk.abstract import SOARClient
7
10
  from soar_sdk.action_results import ActionResult
8
- from soar_sdk.async_utils import run_async_if_needed
11
+ from soar_sdk.es_client import ESClient
9
12
  from soar_sdk.exceptions import ActionFailure
10
13
  from soar_sdk.logging import getLogger
11
14
  from soar_sdk.meta.actions import ActionMeta
12
- from soar_sdk.models.attachment_input import AttachmentInput
13
15
  from soar_sdk.models.container import Container
14
16
  from soar_sdk.models.finding import Finding
15
17
  from soar_sdk.params import OnESPollParams
@@ -19,6 +21,10 @@ if TYPE_CHECKING:
19
21
  from soar_sdk.app import App
20
22
 
21
23
 
24
+ ESPollingYieldType = Finding
25
+ ESPollingSendType = int | None
26
+
27
+
22
28
  class OnESPollDecorator:
23
29
  """Class-based decorator for tagging a function as the special 'on es poll' action."""
24
30
 
@@ -26,12 +32,12 @@ class OnESPollDecorator:
26
32
  self.app = app
27
33
 
28
34
  def __call__(self, function: Callable) -> Action:
29
- """Decorator for the 'on es poll' action.
30
-
31
- The decorated function must be a generator (using yield) or return an Iterator that yields tuples of (Finding, list[AttachmentInput]). Only one on_es_poll action is allowed per app.
35
+ """Decorator for the 'on es poll' action. The decorated function must be a Generator or AsyncGenerator. Only one on_es_poll action is allowed per app.
32
36
 
33
37
  Usage:
34
- Each yielded tuple creates a Container from the Finding metadata. All AttachmentInput items in the list are added as vault attachments to that container.
38
+ The generator should yield a `Finding`. Upon receiving an event from the generator, the SDK will submit the Finding to Splunk Enterprise Security and create a linked SOAR Container.
39
+ The generator should accept a "send type" of `int | None`. When a Finding is successfully delivered to ES and linked to a Container, the SDK will send the Container ID back into the generator. The Container is useful for storing large attachments included with the Finding.
40
+ If the Finding cannot be successfully delivered to ES, the SDK will stop polling and return a failed result for the action run.
35
41
  """
36
42
  if self.app.actions_manager.get_action("on_es_poll"):
37
43
  raise TypeError(
@@ -40,16 +46,25 @@ class OnESPollDecorator:
40
46
 
41
47
  is_generator = inspect.isgeneratorfunction(function)
42
48
  is_async_generator = inspect.isasyncgenfunction(function)
43
- signature = inspect.signature(function)
44
49
 
45
- has_iterator_return = (
46
- signature.return_annotation != inspect.Signature.empty
47
- and getattr(signature.return_annotation, "__origin__", None) is Iterator
48
- )
50
+ generator_type = inspect.signature(function).return_annotation
51
+ generator_type_args = get_args(generator_type)
52
+
53
+ if not (is_generator or is_async_generator) or len(generator_type_args) < 2:
54
+ raise TypeError(
55
+ "The on_es_poll function must be a Generator or AsyncGenerator (use 'yield')."
56
+ )
57
+
58
+ yield_type = generator_type_args[0]
59
+ send_type = generator_type_args[1]
49
60
 
50
- if not (is_generator or is_async_generator or has_iterator_return):
61
+ if yield_type != ESPollingYieldType:
51
62
  raise TypeError(
52
- "The on_es_poll function must be a generator (use 'yield') or return an Iterator."
63
+ f"@on_es_poll generator should have yield type {ESPollingYieldType}."
64
+ )
65
+ if send_type != ESPollingSendType:
66
+ raise TypeError(
67
+ f"@on_es_poll generator should have send type {ESPollingSendType}."
53
68
  )
54
69
 
55
70
  action_identifier = "on_es_poll"
@@ -67,129 +82,85 @@ class OnESPollDecorator:
67
82
  **kwargs: Any, # noqa: ANN401
68
83
  ) -> bool:
69
84
  try:
85
+ action_params = validated_params_class.model_validate(params)
86
+ except ValidationError as e:
87
+ logger.info(f"Parameter validation error: {e!s}")
88
+ return self.app._adapt_action_result(
89
+ ActionResult(status=False, message=f"Invalid parameters: {e!s}"),
90
+ self.app.actions_manager,
91
+ )
92
+ es = ESClient(params.es_base_url, params.es_session_key)
93
+ kwargs = self.app._build_magic_args(function, soar=soar, **kwargs)
94
+ generator = function(action_params, *args, **kwargs)
95
+
96
+ if is_async_generator:
97
+
98
+ def polling_step(
99
+ last_container_id: ESPollingSendType,
100
+ ) -> ESPollingYieldType:
101
+ return asyncio.run(generator.asend(last_container_id))
102
+ else:
103
+
104
+ def polling_step(
105
+ last_container_id: ESPollingSendType,
106
+ ) -> ESPollingYieldType:
107
+ return generator.send(last_container_id)
108
+
109
+ last_container_id = None
110
+ while True:
70
111
  try:
71
- action_params = validated_params_class.parse_obj(params)
72
- except Exception as e:
73
- logger.info(f"Parameter validation error: {e!s}")
112
+ item = polling_step(last_container_id)
113
+ except (StopIteration, StopAsyncIteration):
74
114
  return self.app._adapt_action_result(
75
115
  ActionResult(
76
- status=False, message=f"Invalid parameters: {e!s}"
116
+ status=True, message="Finding processing complete"
77
117
  ),
78
118
  self.app.actions_manager,
79
119
  )
120
+ except ActionFailure as e:
121
+ e.set_action_name(action_name)
122
+ return self.app._adapt_action_result(
123
+ ActionResult(status=False, message=str(e)),
124
+ self.app.actions_manager,
125
+ )
126
+ except Exception as e:
127
+ self.app.actions_manager.add_exception(e)
128
+ logger.info(f"Error during finding processing: {e!s}")
129
+ return self.app._adapt_action_result(
130
+ ActionResult(status=False, message=str(e)),
131
+ self.app.actions_manager,
132
+ )
80
133
 
81
- kwargs = self.app._build_magic_args(function, soar=soar, **kwargs)
82
-
83
- result = function(action_params, *args, **kwargs)
84
- result = run_async_if_needed(result)
85
-
86
- for item in result:
87
- if not isinstance(item, tuple) or len(item) != 2:
88
- logger.info(
89
- f"Warning: Expected tuple of (Finding, list[AttachmentInput]), got: {type(item)}"
90
- )
91
- continue
92
-
93
- finding, attachments = item
94
-
95
- if not isinstance(finding, Finding):
96
- logger.info(
97
- f"Warning: First element must be Finding, got: {type(finding)}"
98
- )
99
- continue
100
-
101
- if not isinstance(attachments, list):
102
- logger.info(
103
- f"Warning: Second element must be list[AttachmentInput], got: {type(attachments)}"
104
- )
105
- continue
106
-
107
- for attachment in attachments:
108
- if not isinstance(attachment, AttachmentInput):
109
- logger.info(
110
- f"Warning: Attachment must be AttachmentInput, got: {type(attachment)}"
111
- )
112
- break
113
- else:
114
- finding_dict = finding.to_dict()
115
- logger.info(
116
- f"Processing finding: {finding_dict.get('rule_title', 'Unnamed finding')}"
117
- )
118
-
119
- # Send finding to ES and get finding_id back
120
- finding_id = self.app.actions_manager.send_finding_to_es(
121
- finding_dict
122
- )
123
-
124
- container = Container(
125
- name=finding.rule_title,
126
- description=finding.rule_description,
127
- severity=finding.urgency or "medium",
128
- status=finding.status,
129
- owner_id=finding.owner,
130
- sensitivity=finding.disposition,
131
- tags=finding.source,
132
- external_id=finding_id,
133
- data={
134
- "security_domain": finding.security_domain,
135
- "risk_score": finding.risk_score,
136
- "risk_object": finding.risk_object,
137
- "risk_object_type": finding.risk_object_type,
138
- },
139
- )
140
-
141
- ret_val, message, container_id = (
142
- self.app.actions_manager.save_container(container.to_dict())
143
- )
144
- logger.info(
145
- f"Creating container for finding: {finding.rule_title}"
146
- )
147
-
148
- if not ret_val:
149
- logger.info(f"Failed to create container: {message}")
150
- continue
151
-
152
- for attachment in attachments:
153
- try:
154
- if attachment.file_content is not None:
155
- vault_id = soar.vault.create_attachment(
156
- container_id=container_id,
157
- file_content=attachment.file_content,
158
- file_name=attachment.file_name,
159
- metadata=attachment.metadata,
160
- )
161
- else:
162
- vault_id = soar.vault.add_attachment(
163
- container_id=container_id,
164
- file_location=attachment.file_location,
165
- file_name=attachment.file_name,
166
- metadata=attachment.metadata,
167
- )
168
- logger.info(
169
- f"Added attachment {attachment.file_name} with vault_id: {vault_id}"
170
- )
171
- except Exception as e:
172
- logger.info(
173
- f"Failed to add attachment {attachment.file_name}: {e!s}"
174
- )
175
-
176
- return self.app._adapt_action_result(
177
- ActionResult(status=True, message="Finding processing complete"),
178
- self.app.actions_manager,
179
- )
180
- except ActionFailure as e:
181
- e.set_action_name(action_name)
182
- return self.app._adapt_action_result(
183
- ActionResult(status=False, message=str(e)),
184
- self.app.actions_manager,
134
+ if type(item) is not ESPollingYieldType:
135
+ logger.info(
136
+ f"Warning: expected {ESPollingYieldType}, got {type(item)}, skipping"
137
+ )
138
+ continue
139
+ finding = es.findings.create(item)
140
+ logger.info(f"Created finding {finding.finding_id}")
141
+
142
+ container = Container(
143
+ name=finding.rule_title,
144
+ description=finding.rule_description,
145
+ severity=finding.urgency or "medium",
146
+ status=finding.status,
147
+ owner_id=finding.owner,
148
+ sensitivity=finding.disposition,
149
+ tags=finding.source,
150
+ external_id=finding.finding_id,
151
+ data={
152
+ "security_domain": finding.security_domain,
153
+ "risk_score": finding.risk_score,
154
+ "risk_object": finding.risk_object,
155
+ "risk_object_type": finding.risk_object_type,
156
+ },
185
157
  )
186
- except Exception as e:
187
- self.app.actions_manager.add_exception(e)
188
- logger.info(f"Error during finding processing: {e!s}")
189
- return self.app._adapt_action_result(
190
- ActionResult(status=False, message=str(e)),
191
- self.app.actions_manager,
158
+ ret_val, message, last_container_id = (
159
+ self.app.actions_manager.save_container(container.to_dict())
192
160
  )
161
+ logger.info(f"Creating container for finding: {finding.rule_title}")
162
+ if not ret_val:
163
+ raise ActionFailure(f"Failed to create container: {message}")
193
164
 
194
165
  inner.params_class = validated_params_class
195
166
 
@@ -211,5 +182,6 @@ class OnESPollDecorator:
211
182
  )
212
183
 
213
184
  self.app.actions_manager.set_action(action_identifier, inner)
185
+ self.app.actions_manager.supports_es_polling = True
214
186
  self.app._dev_skip_in_pytest(function, inner)
215
187
  return inner
soar_sdk/es_client.py ADDED
@@ -0,0 +1,43 @@
1
+ import httpx
2
+ from httpx_retries import Retry, RetryTransport
3
+ from httpx_retries.retry import HTTPMethod, HTTPStatus
4
+
5
+ from soar_sdk.apis.es.findings import Findings
6
+
7
+ RETRYABLE_METHODS = [
8
+ HTTPMethod.GET,
9
+ HTTPMethod.PUT,
10
+ HTTPMethod.POST,
11
+ HTTPMethod.PATCH,
12
+ HTTPMethod.DELETE,
13
+ ]
14
+ RETRYABLE_STATUSES = [
15
+ HTTPStatus.TOO_MANY_REQUESTS,
16
+ HTTPStatus.BAD_GATEWAY,
17
+ HTTPStatus.SERVICE_UNAVAILABLE,
18
+ HTTPStatus.GATEWAY_TIMEOUT,
19
+ ]
20
+
21
+
22
+ class ESClient:
23
+ """A client for accessing Splunk Enterprise Security APIs."""
24
+
25
+ def __init__(self, base_url: str, session_key: str, verify: bool = True) -> None:
26
+ transport = RetryTransport(
27
+ transport=httpx.HTTPTransport(verify=verify),
28
+ retry=Retry(
29
+ allowed_methods=RETRYABLE_METHODS,
30
+ status_forcelist=RETRYABLE_STATUSES,
31
+ total=5,
32
+ ),
33
+ )
34
+ self._client = httpx.Client(
35
+ base_url=base_url,
36
+ transport=transport,
37
+ headers={"Authorization": f"Splunk {session_key}"},
38
+ )
39
+
40
+ @property
41
+ def findings(self) -> Findings:
42
+ """The ES /public/v2/findings API."""
43
+ return Findings(self._client)
soar_sdk/meta/app.py CHANGED
@@ -45,6 +45,7 @@ class AppMeta(BaseModel):
45
45
  pip314_dependencies: DependencyList = Field(default_factory=DependencyList)
46
46
 
47
47
  webhook: WebhookMeta | None = None
48
+ supports_es_polling: bool = False
48
49
 
49
50
  @field_validator("python_version", mode="before")
50
51
  @classmethod
@@ -52,7 +52,8 @@ remove_when_soar_newer_than(
52
52
  "If the Splunk SDK is available as a wheel now, remove it, and remove all of the code for building wheels from source.",
53
53
  )
54
54
  DEPENDENCIES_TO_BUILD = {
55
- "splunk_sdk", # https://github.com/splunk/splunk-sdk-python/pull/656
55
+ "splunk_sdk", # https://github.com/splunk/splunk-sdk-python/pull/656,
56
+ "splunk_soar_sdk", # Useful to build from source when developing the SDK
56
57
  }
57
58
 
58
59
 
@@ -189,6 +190,23 @@ class UvSourceDistribution(BaseModel):
189
190
  return Path(wheel_path).name, f.read()
190
191
 
191
192
 
193
+ class UvSourceDirectory(BaseModel):
194
+ """Represents a Python dependency to be built from a source directory on the local filesystem."""
195
+
196
+ directory: str
197
+
198
+ def build(self) -> tuple[str, bytes]:
199
+ """Build a wheel from a local source directory."""
200
+ with TemporaryDirectory() as build_dir:
201
+ builder = build.ProjectBuilder(
202
+ self.directory,
203
+ runner=UvSourceDistribution._builder_runner,
204
+ )
205
+ wheel_path = builder.build("wheel", build_dir)
206
+ with open(wheel_path, "rb") as f:
207
+ return Path(wheel_path).name, f.read()
208
+
209
+
192
210
  class DependencyWheel(BaseModel):
193
211
  """Represents a Python package dependency with all the information required to fetch its wheel(s) from the CDN."""
194
212
 
@@ -199,13 +217,43 @@ class DependencyWheel(BaseModel):
199
217
  wheel: UvWheel | None = Field(exclude=True, default=None)
200
218
  wheel_aarch64: UvWheel | None = Field(exclude=True, default=None)
201
219
  sdist: UvSourceDistribution | None = Field(exclude=True, default=None)
220
+ source_dir: UvSourceDirectory | None = Field(exclude=True, default=None)
221
+
222
+ def _set_wheel_paths(self, wheel_name: str) -> str:
223
+ """Assign the final wheel path (with any existing prefix) to both arches."""
224
+ base_path = Path(self.input_file or "wheels/shared")
225
+ # If there's already a filename component, replace it instead of nesting it
226
+ if base_path.suffix == ".whl":
227
+ base_path = base_path.parent
228
+ wheel_path = (base_path / wheel_name).as_posix()
229
+ self.input_file = wheel_path
230
+ self.input_file_aarch64 = wheel_path
231
+ return wheel_path
232
+
233
+ def set_placeholder_wheel_name(self, version: str) -> None:
234
+ """Populate a clearly placeholder wheel path when we expect to build from source."""
235
+ # Use only a filename here; platform-specific prefixes are added later.
236
+ self.input_file = "<to_be_built>.whl"
237
+ self.input_file_aarch64 = "<to_be_built>.whl"
238
+
239
+ def _record_built_wheel(self, wheel_name: str) -> str:
240
+ """Fill in missing wheel paths once a wheel has been built from source."""
241
+ return self._set_wheel_paths(wheel_name)
202
242
 
203
243
  async def collect_wheels(self) -> AsyncGenerator[tuple[str, bytes]]:
204
244
  """Collect a list of wheel files to fetch for this dependency across all platforms."""
205
245
  if self.wheel is None and self.sdist is not None:
206
246
  logger.info(f"Building sdist for {self.input_file}")
207
247
  wheel_name, wheel_bytes = await self.sdist.fetch_and_build()
208
- yield (f"wheels/shared/{wheel_name}", wheel_bytes)
248
+ wheel_path = self._record_built_wheel(wheel_name)
249
+ yield (wheel_path, wheel_bytes)
250
+ return
251
+
252
+ if self.wheel is None and self.source_dir is not None:
253
+ logger.info(f"Building local sources for {self.input_file}")
254
+ wheel_name, wheel_bytes = self.source_dir.build()
255
+ wheel_path = self._record_built_wheel(wheel_name)
256
+ yield (wheel_path, wheel_bytes)
209
257
  return
210
258
 
211
259
  if self.wheel is None:
@@ -247,6 +295,13 @@ class UvDependency(BaseModel):
247
295
  name: str
248
296
 
249
297
 
298
+ class UvSource(BaseModel):
299
+ """Represents the source of a Python package in the uv lock."""
300
+
301
+ registry: str | None = None
302
+ directory: str | None = None
303
+
304
+
250
305
  class UvPackage(BaseModel):
251
306
  """Represents a Python package loaded from the uv lock."""
252
307
 
@@ -258,6 +313,7 @@ class UvPackage(BaseModel):
258
313
  )
259
314
  wheels: list[UvWheel] = []
260
315
  sdist: UvSourceDistribution | None = None
316
+ source: UvSource
261
317
 
262
318
  def _find_wheel(
263
319
  self,
@@ -365,6 +421,14 @@ class UvPackage(BaseModel):
365
421
  and UvLock.normalize_package_name(self.name) in DEPENDENCIES_TO_BUILD
366
422
  ):
367
423
  wheel.sdist = self.sdist
424
+ wheel.set_placeholder_wheel_name(self.version)
425
+
426
+ if (
427
+ self.source.directory is not None
428
+ and UvLock.normalize_package_name(self.name) in DEPENDENCIES_TO_BUILD
429
+ ):
430
+ wheel.source_dir = UvSourceDirectory(directory=self.source.directory)
431
+ wheel.set_placeholder_wheel_name(self.version)
368
432
 
369
433
  try:
370
434
  wheel_x86_64 = self._find_wheel(
@@ -373,7 +437,7 @@ class UvPackage(BaseModel):
373
437
  wheel.input_file = f"{wheel_x86_64.basename}.whl"
374
438
  wheel.wheel = wheel_x86_64
375
439
  except FileNotFoundError as e:
376
- if wheel.sdist is None:
440
+ if wheel.sdist is None and wheel.source_dir is None:
377
441
  raise FileNotFoundError(
378
442
  f"Could not find a suitable x86_64 wheel or source distribution for {self.name}"
379
443
  ) from e
@@ -390,7 +454,7 @@ class UvPackage(BaseModel):
390
454
  wheel.input_file_aarch64 = f"{wheel_aarch64.basename}.whl"
391
455
  wheel.wheel_aarch64 = wheel_aarch64
392
456
  except FileNotFoundError:
393
- if wheel.sdist is None:
457
+ if wheel.sdist is None and wheel.source_dir is None:
394
458
  logger.warning(
395
459
  f"Could not find a suitable aarch64 wheel for {self.name=}, {self.version=}, {abi_precedence=}, {python_precedence=} -- the built package might not work on ARM systems"
396
460
  )
@@ -1,6 +1,6 @@
1
1
  from typing import Any
2
2
 
3
- from pydantic import BaseModel
3
+ from pydantic import BaseModel, ConfigDict
4
4
 
5
5
 
6
6
  class DrilldownSearch(BaseModel):
@@ -27,10 +27,7 @@ class Finding(BaseModel):
27
27
  for investigation workflow.
28
28
  """
29
29
 
30
- class Config:
31
- """Pydantic config."""
32
-
33
- extra = "forbid"
30
+ model_config = ConfigDict(extra="forbid")
34
31
 
35
32
  rule_title: str
36
33
  rule_description: str
@@ -52,4 +49,4 @@ class Finding(BaseModel):
52
49
 
53
50
  def to_dict(self) -> dict[str, Any]:
54
51
  """Convert the finding to a dictionary."""
55
- return self.dict(exclude_none=True)
52
+ return self.model_dump(exclude_none=True)
soar_sdk/params.py CHANGED
@@ -207,17 +207,24 @@ class OnESPollParams(Params):
207
207
  description="Start of time range, in epoch time (milliseconds).",
208
208
  required=False,
209
209
  )
210
-
211
210
  end_time: int = Param(
212
211
  description="End of time range, in epoch time (milliseconds).",
213
212
  required=False,
214
213
  )
215
-
216
214
  container_count: int = Param(
217
215
  description="Maximum number of container records to query for.",
218
216
  required=False,
219
217
  )
220
218
 
219
+ es_base_url: str = Param(
220
+ description="Base URL for the Splunk Enterprise Security API",
221
+ required=True,
222
+ )
223
+ es_session_key: str = Param(
224
+ description="Session token for the Splunk Enterprise Security API",
225
+ required=True,
226
+ )
227
+
221
228
 
222
229
  class MakeRequestParams(Params):
223
230
  """Canonical parameters for the special make request action."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splunk-soar-sdk
3
- Version: 3.5.0
3
+ Version: 3.6.1
4
4
  Summary: The official framework for developing and testing Splunk SOAR Apps
5
5
  Project-URL: Homepage, https://github.com/phantomcyber/splunk-soar-sdk
6
6
  Project-URL: Documentation, https://github.com/phantomcyber/splunk-soar-sdk
@@ -22,6 +22,8 @@ Requires-Dist: bleach>=6.2.0
22
22
  Requires-Dist: build>=1.3.0
23
23
  Requires-Dist: click<8.2.0,>=8.0.0
24
24
  Requires-Dist: distro>=1.8.0
25
+ Requires-Dist: hatchling>=1.28.0
26
+ Requires-Dist: httpx-retries>=0.4.5
25
27
  Requires-Dist: httpx>=0.28.1
26
28
  Requires-Dist: humanize>=4.12.2
27
29
  Requires-Dist: jinja2>=3.1.0
@@ -1,7 +1,7 @@
1
1
  soar_sdk/__init__.py,sha256=RzAng-ARqpK01SY82lNy4uYJFVG0yW6Q3CccEqbToJ4,726
2
2
  soar_sdk/abstract.py,sha256=GycJhTrSNDa7eDg8hOD7hJjIt5eHEykZhpza-jh_Veo,7787
3
3
  soar_sdk/action_results.py,sha256=eL4qBj2nXDKurzs733z_nnpNREc0SLLYJP2lPTpMKf0,11911
4
- soar_sdk/actions_manager.py,sha256=DUCaNaKxI-nl-WBNb-fn7o4HyNV9-gF5jlQsfdQKY54,5820
4
+ soar_sdk/actions_manager.py,sha256=jfTTa8Rq06GXpJ_UHCA0MFli5bLgYFbcjpgbAW-ZSKo,5684
5
5
  soar_sdk/app.py,sha256=1tPd1bFe9abSzNJCqAmw7sexeuSHwSKGdggyrPjkVQA,35122
6
6
  soar_sdk/app_cli_runner.py,sha256=K1ATWyGs0iNgPfIjMthsN72laOXqXCFZNEXfuzAMOM4,11645
7
7
  soar_sdk/app_client.py,sha256=hbe1R2QwXDmoS4959a-ay9oylD1Qk-oPJvJRnxvICz0,6281
@@ -11,11 +11,12 @@ soar_sdk/async_utils.py,sha256=Dz7RagIRjyIagA9vivHWSb18S96J2WOuDB8B5Zy64AE,1428
11
11
  soar_sdk/colors.py,sha256=--i_iXqfyITUz4O95HMjfZQGbwFZ34bLmBhtfpXXqlQ,1095
12
12
  soar_sdk/compat.py,sha256=N4bG1wqISICV92K1jLx7v5JGrHC08Bdn3Gx3Cx1lEmE,3062
13
13
  soar_sdk/crypto.py,sha256=qiBMHUQqgn5lPI1DbujSj700s89FuLJrkQgCO9_eBn4,392
14
+ soar_sdk/es_client.py,sha256=U2NhPwGcciCfZM7ofBnfbkbHivna7rkTScwXwa39_bg,1204
14
15
  soar_sdk/exceptions.py,sha256=413-AcIM7IMixoyVk_0yDaqsUhommb784uH5vSv18lU,2129
15
16
  soar_sdk/field_utils.py,sha256=Jb0HteUPd-CtuDM7rNXVLy4uRxl419zeDxY_oOpU8GM,287
16
17
  soar_sdk/input_spec.py,sha256=vvmE8AWk2VFRgsvh-Bn1eIMDAHkV-1Y73_8xM_8qVZI,4678
17
18
  soar_sdk/logging.py,sha256=Y_5RLT1Z0UcRTxDZpwXwDae7ZtwOs91Hs-UU4UOiMuk,11425
18
- soar_sdk/params.py,sha256=IpBRYI5aNXesjU_vs7_YPFINeJEHWEpNc3p92D5F-0Q,10569
19
+ soar_sdk/params.py,sha256=pvcQwzEJSSOM95RQ-JTfQrExzJMIKCKOVFdka6P9yYQ,10836
19
20
  soar_sdk/paths.py,sha256=XhpanQCAiTXaulRx440oKu36mnll7P05TethHXgMpgQ,239
20
21
  soar_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
22
  soar_sdk/types.py,sha256=wN-zV27IamDR67hj4QDqOWA04EVnJwcFhWOighMYEJc,616
@@ -24,6 +25,7 @@ soar_sdk/apis/artifact.py,sha256=d2-5U5DSBMpTYf8CJsd2y5rTaDjN3jMG8jDg9mbkRqU,471
24
25
  soar_sdk/apis/container.py,sha256=99Y6ZZaQR_wSFuy5824_Lpfa5z3pS9mCG62jbsXz4JU,8747
25
26
  soar_sdk/apis/utils.py,sha256=HQkfmaFixyIGXRrGKc7mYNHflYNopOxBM77KBEW9cvQ,958
26
27
  soar_sdk/apis/vault.py,sha256=m7xcu5xzPizrSM6sDUyhRNaJiCeKB1X7kPo2yZ9dco8,8519
28
+ soar_sdk/apis/es/findings.py,sha256=02drgxFclcSfmwbpz1hxFTwKJZfwwsscAn41_BLB1e8,685
27
29
  soar_sdk/app_templates/basic_app/.gitignore,sha256=CwSrCn9YISq7Q2EJVc7o1kdkXqysZngGJfz-4pOMMdY,3517
28
30
  soar_sdk/app_templates/basic_app/.pre-commit-config.yaml,sha256=0sFjnoC-scXm80ZgA4TPyc6BN1VaF4eY9p8qwEchB38,2526
29
31
  soar_sdk/app_templates/basic_app/logo.svg,sha256=_JTop6spn5oPWPk-w6Tzumx_FTSBanOYra3vE2FO-6c,285
@@ -39,9 +41,9 @@ soar_sdk/cli/init/cli.py,sha256=HymS_a-5iNuYLI9FTSqboZt4lnrxXQmIaHqcSA0TBzw,1461
39
41
  soar_sdk/cli/manifests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
42
  soar_sdk/cli/manifests/cli.py,sha256=cly5xVdj4bBIdZVMQPIWTXRgUfd1ON3qKO-76Fwql18,524
41
43
  soar_sdk/cli/manifests/deserializers.py,sha256=kwgPAMgUEXtIn4AuQOh1nkLfWFqe4qnYPZ1czB-FQTU,16516
42
- soar_sdk/cli/manifests/processors.py,sha256=mX7ArmZDxb8ieZBXS933xVByXDN1kRnv7xuuQ0-lNKM,5061
44
+ soar_sdk/cli/manifests/processors.py,sha256=soiRTbfLQuetstt1Xk7vKmWzOUhgVON5JxjWMvnGN7w,5141
43
45
  soar_sdk/cli/manifests/serializers.py,sha256=ulpq3nS8g1YrIP371XoQC3_kpz-9v2Ln_mqPyMtpWn8,3632
44
- soar_sdk/cli/package/cli.py,sha256=Vb6vdDszWLEOuBrAe4BLoxzdmr8luwA0_FbsvjjYhhM,9578
46
+ soar_sdk/cli/package/cli.py,sha256=_SlMNfqEyjAAWQy8AU4KhDpqxbV0jnjTQX1c3uULAtk,9945
45
47
  soar_sdk/cli/package/utils.py,sha256=fl6PMcrdC2zA7A16byQuxxPyAI2Z-BqBLfLlF2ZNnQ4,1712
46
48
  soar_sdk/cli/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
49
  soar_sdk/cli/test/cli.py,sha256=iDrthN8L7B1RplLhq0EI69MndaOhvAXn7bqv3XzlfpM,7655
@@ -55,7 +57,7 @@ soar_sdk/code_renderers/templates/pyproject.toml.jinja,sha256=uH7SJhFD9_-kYzGHob
55
57
  soar_sdk/decorators/__init__.py,sha256=h-9GesqZRySCUNP-ktAzjVEW55e5W6l95NTBYKz6cQE,580
56
58
  soar_sdk/decorators/action.py,sha256=7NNHjq-8yZBkgcvOrCpVQftd9ecunNDc7iJQxnlgs9Y,6569
57
59
  soar_sdk/decorators/make_request.py,sha256=sKLbg5l3yXSryrxBiCl4uIDmVdnpyOhf1iBPwqQAfOM,5942
58
- soar_sdk/decorators/on_es_poll.py,sha256=SBWMdpoZ3L0XiiHTCzqhDUIbMhPFc67QkmNdMPhBHXU,9391
60
+ soar_sdk/decorators/on_es_poll.py,sha256=27LdzjTIdIJej0_POEWBi5FCYBDGJ5C02p_iP7Bo3iU,7899
59
61
  soar_sdk/decorators/on_poll.py,sha256=2glXawGJcdbu1s5yaI-SSJtp0HGu60NWEaY1E9LX1uU,8229
60
62
  soar_sdk/decorators/test_connectivity.py,sha256=agoMP2-iFsfBUJH6_uc907o1duQVUlmC_Jgu4s-ou-Y,3563
61
63
  soar_sdk/decorators/view_handler.py,sha256=OOALnXJrLTAs_GsYUYSgKVjWrmVXWop4pH-56aCDjbY,7057
@@ -68,15 +70,15 @@ soar_sdk/extras/email/utils.py,sha256=njTYBCaVs2F3DlEHV2R1YSbVf5qJS86j6swHED6dAM
68
70
  soar_sdk/meta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
71
  soar_sdk/meta/actions.py,sha256=qOXFChYxOVTt1d0gBffyw8ahJ3E4w7GsVoD4yzJN8Vc,2189
70
72
  soar_sdk/meta/adapters.py,sha256=KjSYIUtkCz2eesA_vhsNCjfi5C-Uz71tbSuDIjhuB8U,1112
71
- soar_sdk/meta/app.py,sha256=eZlM8GIY1B_o-RzJrRNCNVEQSx0sFupxZqCM7sIWGv4,2777
73
+ soar_sdk/meta/app.py,sha256=KbDiq1JgRiH4hudyKr95wMfcPN7YLyOkRjf4JIoFihs,2815
72
74
  soar_sdk/meta/datatypes.py,sha256=piR-oBVAATiRciXSdVE7XaqjUZTgSaOvTEqcOcNvCS0,795
73
- soar_sdk/meta/dependencies.py,sha256=xPZQjJVNTGbFTjJlevpcyCopbRZoSsm6vyHPTjhneAw,20443
75
+ soar_sdk/meta/dependencies.py,sha256=SXWOoUJRmyW9RE3CrgF2sQLQhfyPcXAg7KO0ONf3h_g,23244
74
76
  soar_sdk/meta/webhooks.py,sha256=ILKP9pNalKG9DLdNQNoDlu5KUnm0m7PyA2O0fbkqVrA,1217
75
77
  soar_sdk/models/__init__.py,sha256=YZVAcBguAlUsxAnBBL6jSguJEzf5PYCtdvbNyU1XfEU,380
76
78
  soar_sdk/models/artifact.py,sha256=G8hv9wPPoRgrAQzIf-YlCSjAlkHEcIPF389T1bo4yHw,1087
77
79
  soar_sdk/models/attachment_input.py,sha256=s2mkEsRVb52yqHtb4Q7FzC9j8A4-Q8W4wCDqMJQZ8cc,1043
78
80
  soar_sdk/models/container.py,sha256=Cnn-Grha8qUFHHBxLUcEvo81sC3z483oItJ4GhRiTmg,1528
79
- soar_sdk/models/finding.py,sha256=HRH94akM57jEyaybT5TydjWLP-c4F4T_opq3oTI8e9Q,1415
81
+ soar_sdk/models/finding.py,sha256=Evga9Jrp3TfSVdAQlAkZ7UHDkUjaQYicYYY1S5bIruY,1404
80
82
  soar_sdk/models/vault_attachment.py,sha256=sdRnQdPiwgaZDojpap4ohH7u1Q5TYGP-drs8Ko4p_aU,1073
81
83
  soar_sdk/models/view.py,sha256=BUuz6VVVe78hg7irGgZCbvBcycOmuPqplkagdi3T4Dg,779
82
84
  soar_sdk/shims/phantom/action_result.py,sha256=Nddc9oswAfHU7I2q0pLm3HZ2YiLUQZUEIqqAjToZWnM,1606
@@ -108,8 +110,8 @@ soar_sdk/views/components/pie_chart.py,sha256=LVTeHVJN6nf2vjUs9y7PDBhS0U1fKW750l
108
110
  soar_sdk/webhooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
109
111
  soar_sdk/webhooks/models.py,sha256=j3kbvYmcOlcj3gQYKtrv7iS-lDavMKYNLdCNMy_I2Hc,4542
110
112
  soar_sdk/webhooks/routing.py,sha256=OjezhuAb8wzW0MnbGSnIWeAH3uJcu-Sb7s3w9zoiPVM,6873
111
- splunk_soar_sdk-3.5.0.dist-info/METADATA,sha256=snxUdZt6O1aN7Kqyxg3i0szCCdTXRnh_r0LAYWZTsZo,7409
112
- splunk_soar_sdk-3.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
113
- splunk_soar_sdk-3.5.0.dist-info/entry_points.txt,sha256=CgBjo2ZWpYNkt9TgvToL26h2Tg1yt8FbvYTb5NVgNuc,51
114
- splunk_soar_sdk-3.5.0.dist-info/licenses/LICENSE,sha256=gNCGrGhrSQb1PUzBOByVUN1tvaliwLZfna-QU2r2hQ8,11345
115
- splunk_soar_sdk-3.5.0.dist-info/RECORD,,
113
+ splunk_soar_sdk-3.6.1.dist-info/METADATA,sha256=1Y4gKBFsYMfCL1vPPxCCLCDvz16iQ9EmjhdjY0prJg0,7478
114
+ splunk_soar_sdk-3.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
115
+ splunk_soar_sdk-3.6.1.dist-info/entry_points.txt,sha256=CgBjo2ZWpYNkt9TgvToL26h2Tg1yt8FbvYTb5NVgNuc,51
116
+ splunk_soar_sdk-3.6.1.dist-info/licenses/LICENSE,sha256=gNCGrGhrSQb1PUzBOByVUN1tvaliwLZfna-QU2r2hQ8,11345
117
+ splunk_soar_sdk-3.6.1.dist-info/RECORD,,