splunk-soar-sdk 3.4.0__py3-none-any.whl → 3.6.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 (76) hide show
  1. soar_sdk/abstract.py +7 -6
  2. soar_sdk/action_results.py +7 -7
  3. soar_sdk/actions_manager.py +7 -13
  4. soar_sdk/apis/artifact.py +3 -3
  5. soar_sdk/apis/container.py +2 -2
  6. soar_sdk/apis/es/findings.py +27 -0
  7. soar_sdk/apis/utils.py +3 -2
  8. soar_sdk/apis/vault.py +1 -0
  9. soar_sdk/app.py +24 -27
  10. soar_sdk/app_cli_runner.py +7 -6
  11. soar_sdk/app_client.py +3 -4
  12. soar_sdk/asset.py +7 -9
  13. soar_sdk/asset_state.py +1 -2
  14. soar_sdk/async_utils.py +1 -2
  15. soar_sdk/cli/cli.py +2 -2
  16. soar_sdk/cli/init/cli.py +5 -5
  17. soar_sdk/cli/manifests/deserializers.py +4 -3
  18. soar_sdk/cli/manifests/processors.py +4 -2
  19. soar_sdk/cli/manifests/serializers.py +4 -4
  20. soar_sdk/cli/package/cli.py +14 -14
  21. soar_sdk/cli/package/utils.py +3 -2
  22. soar_sdk/cli/path_utils.py +1 -1
  23. soar_sdk/code_renderers/action_renderer.py +5 -4
  24. soar_sdk/code_renderers/app_renderer.py +1 -1
  25. soar_sdk/code_renderers/asset_renderer.py +1 -1
  26. soar_sdk/code_renderers/renderer.py +2 -2
  27. soar_sdk/compat.py +2 -1
  28. soar_sdk/decorators/__init__.py +3 -3
  29. soar_sdk/decorators/action.py +7 -11
  30. soar_sdk/decorators/make_request.py +9 -11
  31. soar_sdk/decorators/on_es_poll.py +105 -136
  32. soar_sdk/decorators/on_poll.py +7 -11
  33. soar_sdk/decorators/test_connectivity.py +5 -6
  34. soar_sdk/decorators/view_handler.py +6 -7
  35. soar_sdk/decorators/webhook.py +3 -5
  36. soar_sdk/es_client.py +43 -0
  37. soar_sdk/extras/__init__.py +0 -0
  38. soar_sdk/extras/email/__init__.py +9 -0
  39. soar_sdk/extras/email/processor.py +1171 -0
  40. soar_sdk/extras/email/rfc5322.py +335 -0
  41. soar_sdk/extras/email/utils.py +178 -0
  42. soar_sdk/input_spec.py +4 -3
  43. soar_sdk/logging.py +5 -4
  44. soar_sdk/meta/actions.py +3 -3
  45. soar_sdk/meta/app.py +1 -0
  46. soar_sdk/meta/dependencies.py +47 -11
  47. soar_sdk/meta/webhooks.py +2 -1
  48. soar_sdk/models/__init__.py +1 -1
  49. soar_sdk/models/artifact.py +1 -0
  50. soar_sdk/models/attachment_input.py +1 -1
  51. soar_sdk/models/container.py +2 -1
  52. soar_sdk/models/finding.py +4 -6
  53. soar_sdk/models/vault_attachment.py +1 -0
  54. soar_sdk/models/view.py +2 -0
  55. soar_sdk/params.py +13 -7
  56. soar_sdk/shims/phantom/action_result.py +1 -1
  57. soar_sdk/shims/phantom/app.py +1 -1
  58. soar_sdk/shims/phantom/base_connector.py +3 -4
  59. soar_sdk/shims/phantom/connector_result.py +0 -1
  60. soar_sdk/shims/phantom/install_info.py +1 -1
  61. soar_sdk/shims/phantom/ph_ipc.py +2 -1
  62. soar_sdk/shims/phantom/vault.py +8 -6
  63. soar_sdk/shims/phantom_common/app_interface/app_interface.py +1 -0
  64. soar_sdk/types.py +1 -1
  65. soar_sdk/views/component_registry.py +0 -1
  66. soar_sdk/views/template_filters.py +4 -4
  67. soar_sdk/views/template_renderer.py +3 -2
  68. soar_sdk/views/view_parser.py +8 -6
  69. soar_sdk/webhooks/models.py +3 -3
  70. soar_sdk/webhooks/routing.py +3 -4
  71. {splunk_soar_sdk-3.4.0.dist-info → splunk_soar_sdk-3.6.0.dist-info}/METADATA +5 -1
  72. splunk_soar_sdk-3.6.0.dist-info/RECORD +117 -0
  73. splunk_soar_sdk-3.4.0.dist-info/RECORD +0 -110
  74. {splunk_soar_sdk-3.4.0.dist-info → splunk_soar_sdk-3.6.0.dist-info}/WHEEL +0 -0
  75. {splunk_soar_sdk-3.4.0.dist-info → splunk_soar_sdk-3.6.0.dist-info}/entry_points.txt +0 -0
  76. {splunk_soar_sdk-3.4.0.dist-info → splunk_soar_sdk-3.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,27 +1,30 @@
1
+ import asyncio
1
2
  import inspect
2
- from functools import wraps
3
- from typing import Any
4
3
  from collections.abc import Callable
5
- from collections.abc import Iterator
4
+ from functools import wraps
5
+ from typing import TYPE_CHECKING, Any, get_args
6
+
7
+ from pydantic import ValidationError
6
8
 
7
9
  from soar_sdk.abstract import SOARClient
8
10
  from soar_sdk.action_results import ActionResult
9
- from soar_sdk.params import OnESPollParams
10
- from soar_sdk.meta.actions import ActionMeta
11
- from soar_sdk.types import Action, action_protocol
11
+ from soar_sdk.es_client import ESClient
12
12
  from soar_sdk.exceptions import ActionFailure
13
- from soar_sdk.async_utils import run_async_if_needed
14
13
  from soar_sdk.logging import getLogger
15
- from soar_sdk.models.finding import Finding
16
- from soar_sdk.models.attachment_input import AttachmentInput
14
+ from soar_sdk.meta.actions import ActionMeta
17
15
  from soar_sdk.models.container import Container
18
-
19
- from typing import TYPE_CHECKING
16
+ from soar_sdk.models.finding import Finding
17
+ from soar_sdk.params import OnESPollParams
18
+ from soar_sdk.types import Action, action_protocol
20
19
 
21
20
  if TYPE_CHECKING:
22
21
  from soar_sdk.app import App
23
22
 
24
23
 
24
+ ESPollingYieldType = Finding
25
+ ESPollingSendType = int | None
26
+
27
+
25
28
  class OnESPollDecorator:
26
29
  """Class-based decorator for tagging a function as the special 'on es poll' action."""
27
30
 
@@ -29,12 +32,12 @@ class OnESPollDecorator:
29
32
  self.app = app
30
33
 
31
34
  def __call__(self, function: Callable) -> Action:
32
- """Decorator for the 'on es poll' action.
33
-
34
- 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.
35
36
 
36
37
  Usage:
37
- 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.
38
41
  """
39
42
  if self.app.actions_manager.get_action("on_es_poll"):
40
43
  raise TypeError(
@@ -43,16 +46,25 @@ class OnESPollDecorator:
43
46
 
44
47
  is_generator = inspect.isgeneratorfunction(function)
45
48
  is_async_generator = inspect.isasyncgenfunction(function)
46
- signature = inspect.signature(function)
47
49
 
48
- has_iterator_return = (
49
- signature.return_annotation != inspect.Signature.empty
50
- and getattr(signature.return_annotation, "__origin__", None) is Iterator
51
- )
50
+ generator_type = inspect.signature(function).return_annotation
51
+ generator_type_args = get_args(generator_type)
52
52
 
53
- if not (is_generator or is_async_generator or has_iterator_return):
53
+ if not (is_generator or is_async_generator) or len(generator_type_args) < 2:
54
54
  raise TypeError(
55
- "The on_es_poll function must be a generator (use 'yield') or return an Iterator."
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]
60
+
61
+ if yield_type != ESPollingYieldType:
62
+ raise TypeError(
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}."
56
68
  )
57
69
 
58
70
  action_identifier = "on_es_poll"
@@ -70,129 +82,85 @@ class OnESPollDecorator:
70
82
  **kwargs: Any, # noqa: ANN401
71
83
  ) -> bool:
72
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:
73
111
  try:
74
- action_params = validated_params_class.parse_obj(params)
75
- except Exception as e:
76
- logger.info(f"Parameter validation error: {e!s}")
112
+ item = polling_step(last_container_id)
113
+ except (StopIteration, StopAsyncIteration):
77
114
  return self.app._adapt_action_result(
78
115
  ActionResult(
79
- status=False, message=f"Invalid parameters: {e!s}"
116
+ status=True, message="Finding processing complete"
80
117
  ),
81
118
  self.app.actions_manager,
82
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
+ )
83
133
 
84
- kwargs = self.app._build_magic_args(function, soar=soar, **kwargs)
85
-
86
- result = function(action_params, *args, **kwargs)
87
- result = run_async_if_needed(result)
88
-
89
- for item in result:
90
- if not isinstance(item, tuple) or len(item) != 2:
91
- logger.info(
92
- f"Warning: Expected tuple of (Finding, list[AttachmentInput]), got: {type(item)}"
93
- )
94
- continue
95
-
96
- finding, attachments = item
97
-
98
- if not isinstance(finding, Finding):
99
- logger.info(
100
- f"Warning: First element must be Finding, got: {type(finding)}"
101
- )
102
- continue
103
-
104
- if not isinstance(attachments, list):
105
- logger.info(
106
- f"Warning: Second element must be list[AttachmentInput], got: {type(attachments)}"
107
- )
108
- continue
109
-
110
- for attachment in attachments:
111
- if not isinstance(attachment, AttachmentInput):
112
- logger.info(
113
- f"Warning: Attachment must be AttachmentInput, got: {type(attachment)}"
114
- )
115
- break
116
- else:
117
- finding_dict = finding.to_dict()
118
- logger.info(
119
- f"Processing finding: {finding_dict.get('rule_title', 'Unnamed finding')}"
120
- )
121
-
122
- # Send finding to ES and get finding_id back
123
- finding_id = self.app.actions_manager.send_finding_to_es(
124
- finding_dict
125
- )
126
-
127
- container = Container(
128
- name=finding.rule_title,
129
- description=finding.rule_description,
130
- severity=finding.urgency or "medium",
131
- status=finding.status,
132
- owner_id=finding.owner,
133
- sensitivity=finding.disposition,
134
- tags=finding.source,
135
- external_id=finding_id,
136
- data={
137
- "security_domain": finding.security_domain,
138
- "risk_score": finding.risk_score,
139
- "risk_object": finding.risk_object,
140
- "risk_object_type": finding.risk_object_type,
141
- },
142
- )
143
-
144
- ret_val, message, container_id = (
145
- self.app.actions_manager.save_container(container.to_dict())
146
- )
147
- logger.info(
148
- f"Creating container for finding: {finding.rule_title}"
149
- )
150
-
151
- if not ret_val:
152
- logger.info(f"Failed to create container: {message}")
153
- continue
154
-
155
- for attachment in attachments:
156
- try:
157
- if attachment.file_content is not None:
158
- vault_id = soar.vault.create_attachment(
159
- container_id=container_id,
160
- file_content=attachment.file_content,
161
- file_name=attachment.file_name,
162
- metadata=attachment.metadata,
163
- )
164
- else:
165
- vault_id = soar.vault.add_attachment(
166
- container_id=container_id,
167
- file_location=attachment.file_location,
168
- file_name=attachment.file_name,
169
- metadata=attachment.metadata,
170
- )
171
- logger.info(
172
- f"Added attachment {attachment.file_name} with vault_id: {vault_id}"
173
- )
174
- except Exception as e:
175
- logger.info(
176
- f"Failed to add attachment {attachment.file_name}: {e!s}"
177
- )
178
-
179
- return self.app._adapt_action_result(
180
- ActionResult(status=True, message="Finding processing complete"),
181
- self.app.actions_manager,
182
- )
183
- except ActionFailure as e:
184
- e.set_action_name(action_name)
185
- return self.app._adapt_action_result(
186
- ActionResult(status=False, message=str(e)),
187
- 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
+ },
188
157
  )
189
- except Exception as e:
190
- self.app.actions_manager.add_exception(e)
191
- logger.info(f"Error during finding processing: {e!s}")
192
- return self.app._adapt_action_result(
193
- ActionResult(status=False, message=str(e)),
194
- self.app.actions_manager,
158
+ ret_val, message, last_container_id = (
159
+ self.app.actions_manager.save_container(container.to_dict())
195
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}")
196
164
 
197
165
  inner.params_class = validated_params_class
198
166
 
@@ -214,5 +182,6 @@ class OnESPollDecorator:
214
182
  )
215
183
 
216
184
  self.app.actions_manager.set_action(action_identifier, inner)
185
+ self.app.actions_manager.supports_es_polling = True
217
186
  self.app._dev_skip_in_pytest(function, inner)
218
187
  return inner
@@ -1,20 +1,16 @@
1
1
  import inspect
2
+ from collections.abc import Callable, Iterator
2
3
  from functools import wraps
3
- from typing import Any
4
- from collections.abc import Callable
5
- from collections.abc import Iterator
4
+ from typing import TYPE_CHECKING, Any
6
5
 
7
6
  from soar_sdk.abstract import SOARClient
8
7
  from soar_sdk.action_results import ActionResult
9
- from soar_sdk.params import OnPollParams
10
- from soar_sdk.meta.actions import ActionMeta
11
- from soar_sdk.types import Action, action_protocol
12
- from soar_sdk.exceptions import ActionFailure
13
8
  from soar_sdk.async_utils import run_async_if_needed
9
+ from soar_sdk.exceptions import ActionFailure
14
10
  from soar_sdk.logging import getLogger
15
-
16
-
17
- from typing import TYPE_CHECKING
11
+ from soar_sdk.meta.actions import ActionMeta
12
+ from soar_sdk.params import OnPollParams
13
+ from soar_sdk.types import Action, action_protocol
18
14
 
19
15
  if TYPE_CHECKING:
20
16
  from soar_sdk.app import App
@@ -75,8 +71,8 @@ class OnPollDecorator:
75
71
  **kwargs: Any, # noqa: ANN401
76
72
  ) -> bool:
77
73
  # Lazy imports to avoid circular dependencies
78
- from soar_sdk.models.container import Container
79
74
  from soar_sdk.models.artifact import Artifact
75
+ from soar_sdk.models.container import Container
80
76
 
81
77
  try:
82
78
  # Validate poll params
@@ -1,16 +1,15 @@
1
1
  import inspect
2
- from functools import wraps
2
+ import traceback
3
3
  from collections.abc import Callable
4
+ from functools import wraps
5
+ from typing import TYPE_CHECKING
4
6
 
5
7
  from soar_sdk.abstract import SOARClient
6
8
  from soar_sdk.action_results import ActionResult
9
+ from soar_sdk.async_utils import run_async_if_needed
10
+ from soar_sdk.exceptions import ActionFailure
7
11
  from soar_sdk.meta.actions import ActionMeta
8
12
  from soar_sdk.types import Action, action_protocol
9
- from soar_sdk.exceptions import ActionFailure
10
- from soar_sdk.async_utils import run_async_if_needed
11
- import traceback
12
-
13
- from typing import TYPE_CHECKING
14
13
 
15
14
  if TYPE_CHECKING:
16
15
  from soar_sdk.app import App
@@ -1,19 +1,18 @@
1
1
  import inspect
2
- from functools import wraps
3
- from typing import Any
4
2
  from collections.abc import Callable
3
+ from functools import wraps
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from pydantic import BaseModel
5
7
 
6
8
  from soar_sdk.action_results import ActionResult
9
+ from soar_sdk.models.view import AllAppRuns, ResultSummary, ViewContext
7
10
  from soar_sdk.views.component_registry import COMPONENT_REGISTRY
8
- from pydantic import BaseModel
9
- from soar_sdk.models.view import ViewContext, AllAppRuns, ResultSummary
10
- from soar_sdk.views.view_parser import ViewFunctionParser
11
11
  from soar_sdk.views.template_renderer import (
12
12
  get_template_renderer,
13
13
  get_templates_dir,
14
14
  )
15
-
16
- from typing import TYPE_CHECKING
15
+ from soar_sdk.views.view_parser import ViewFunctionParser
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  from soar_sdk.app import App
@@ -1,13 +1,11 @@
1
1
  import inspect
2
2
  from functools import wraps
3
3
  from pathlib import Path
4
+ from typing import TYPE_CHECKING
4
5
 
5
- from soar_sdk.webhooks.models import WebhookRequest, WebhookResponse, WebhookHandler
6
- from soar_sdk.meta.webhooks import WebhookRouteMeta
7
6
  from soar_sdk.async_utils import run_async_if_needed
8
-
9
-
10
- from typing import TYPE_CHECKING
7
+ from soar_sdk.meta.webhooks import WebhookRouteMeta
8
+ from soar_sdk.webhooks.models import WebhookHandler, WebhookRequest, WebhookResponse
11
9
 
12
10
  if TYPE_CHECKING:
13
11
  from soar_sdk.app import App
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)
File without changes
@@ -0,0 +1,9 @@
1
+ from soar_sdk.extras.email.processor import EmailProcessor, ProcessEmailContext
2
+ from soar_sdk.extras.email.rfc5322 import RFC5322EmailData, extract_rfc5322_email_data
3
+
4
+ __all__ = [
5
+ "EmailProcessor",
6
+ "ProcessEmailContext",
7
+ "RFC5322EmailData",
8
+ "extract_rfc5322_email_data",
9
+ ]