splunk-soar-sdk 3.6.0__py3-none-any.whl → 3.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- soar_sdk/actions_manager.py +40 -0
- soar_sdk/app.py +52 -4
- soar_sdk/asset.py +49 -69
- soar_sdk/asset_state.py +13 -3
- soar_sdk/auth/__init__.py +41 -0
- soar_sdk/auth/client.py +540 -0
- soar_sdk/auth/factories.py +120 -0
- soar_sdk/auth/flows.py +172 -0
- soar_sdk/auth/httpx_auth.py +97 -0
- soar_sdk/auth/models.py +101 -0
- soar_sdk/cli/package/cli.py +35 -17
- soar_sdk/meta/dependencies.py +28 -3
- soar_sdk/shims/phantom/base_connector.py +7 -0
- {splunk_soar_sdk-3.6.0.dist-info → splunk_soar_sdk-3.7.0.dist-info}/METADATA +3 -1
- {splunk_soar_sdk-3.6.0.dist-info → splunk_soar_sdk-3.7.0.dist-info}/RECORD +18 -12
- {splunk_soar_sdk-3.6.0.dist-info → splunk_soar_sdk-3.7.0.dist-info}/WHEEL +0 -0
- {splunk_soar_sdk-3.6.0.dist-info → splunk_soar_sdk-3.7.0.dist-info}/entry_points.txt +0 -0
- {splunk_soar_sdk-3.6.0.dist-info → splunk_soar_sdk-3.7.0.dist-info}/licenses/LICENSE +0 -0
soar_sdk/auth/flows.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from soar_sdk.auth.client import (
|
|
9
|
+
AuthorizationRequiredError,
|
|
10
|
+
OAuthClientError,
|
|
11
|
+
SOARAssetOAuthClient,
|
|
12
|
+
TokenExpiredError,
|
|
13
|
+
)
|
|
14
|
+
from soar_sdk.auth.models import OAuthConfig, OAuthGrantType, OAuthToken
|
|
15
|
+
from soar_sdk.logging import getLogger
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from soar_sdk.asset_state import AssetState
|
|
19
|
+
|
|
20
|
+
logger = getLogger()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OAuthFlow(ABC):
|
|
24
|
+
"""Abstract base class for OAuth authentication flows."""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def authenticate(self) -> OAuthToken:
|
|
28
|
+
"""Execute the authentication flow and return a valid token."""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_token(self) -> OAuthToken:
|
|
32
|
+
"""Get a valid token, refreshing if necessary."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ClientCredentialsFlow(OAuthFlow):
|
|
36
|
+
"""OAuth 2.0 Client Credentials flow."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
auth_state: AssetState,
|
|
41
|
+
*,
|
|
42
|
+
client_id: str,
|
|
43
|
+
client_secret: str,
|
|
44
|
+
token_endpoint: str,
|
|
45
|
+
scope: str | list[str] | None = None,
|
|
46
|
+
extra_params: dict[str, Any] | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._auth_state = auth_state
|
|
49
|
+
self._extra_params = extra_params
|
|
50
|
+
|
|
51
|
+
self._config = OAuthConfig(
|
|
52
|
+
client_id=client_id,
|
|
53
|
+
client_secret=client_secret,
|
|
54
|
+
token_endpoint=token_endpoint,
|
|
55
|
+
scope=scope,
|
|
56
|
+
grant_type=OAuthGrantType.CLIENT_CREDENTIALS,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self._client = SOARAssetOAuthClient(self._config, auth_state)
|
|
60
|
+
|
|
61
|
+
def authenticate(self) -> OAuthToken:
|
|
62
|
+
"""Authenticate using client credentials."""
|
|
63
|
+
return self._client.fetch_token_with_client_credentials(
|
|
64
|
+
extra_params=self._extra_params,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def get_token(self) -> OAuthToken:
|
|
68
|
+
"""Get a valid token, fetching a new one if expired."""
|
|
69
|
+
try:
|
|
70
|
+
return self._client.get_valid_token(auto_refresh=False)
|
|
71
|
+
except (AuthorizationRequiredError, TokenExpiredError):
|
|
72
|
+
return self.authenticate()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AuthorizationCodeFlow(OAuthFlow):
|
|
76
|
+
"""OAuth 2.0 Authorization Code flow."""
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
auth_state: AssetState,
|
|
81
|
+
asset_id: str,
|
|
82
|
+
*,
|
|
83
|
+
client_id: str,
|
|
84
|
+
client_secret: str | None = None,
|
|
85
|
+
authorization_endpoint: str,
|
|
86
|
+
token_endpoint: str,
|
|
87
|
+
redirect_uri: str,
|
|
88
|
+
scope: str | list[str] | None = None,
|
|
89
|
+
use_pkce: bool = False,
|
|
90
|
+
extra_auth_params: dict[str, Any] | None = None,
|
|
91
|
+
extra_token_params: dict[str, Any] | None = None,
|
|
92
|
+
poll_timeout: int = 300,
|
|
93
|
+
poll_interval: int = 3,
|
|
94
|
+
) -> None:
|
|
95
|
+
self._auth_state = auth_state
|
|
96
|
+
self._asset_id = asset_id
|
|
97
|
+
self._use_pkce = use_pkce
|
|
98
|
+
self._extra_auth_params = extra_auth_params
|
|
99
|
+
self._extra_token_params = extra_token_params
|
|
100
|
+
self._poll_timeout = poll_timeout
|
|
101
|
+
self._poll_interval = poll_interval
|
|
102
|
+
|
|
103
|
+
self._config = OAuthConfig(
|
|
104
|
+
client_id=client_id,
|
|
105
|
+
client_secret=client_secret,
|
|
106
|
+
authorization_endpoint=authorization_endpoint,
|
|
107
|
+
token_endpoint=token_endpoint,
|
|
108
|
+
redirect_uri=redirect_uri,
|
|
109
|
+
scope=scope,
|
|
110
|
+
grant_type=OAuthGrantType.AUTHORIZATION_CODE,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._client = SOARAssetOAuthClient(self._config, auth_state)
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def client(self) -> SOARAssetOAuthClient:
|
|
117
|
+
"""Return the OAuth client."""
|
|
118
|
+
return self._client
|
|
119
|
+
|
|
120
|
+
def get_authorization_url(self) -> str:
|
|
121
|
+
"""Generate the authorization URL."""
|
|
122
|
+
auth_url, _ = self._client.create_authorization_url(
|
|
123
|
+
self._asset_id,
|
|
124
|
+
use_pkce=self._use_pkce,
|
|
125
|
+
extra_params=self._extra_auth_params,
|
|
126
|
+
)
|
|
127
|
+
return auth_url
|
|
128
|
+
|
|
129
|
+
def set_authorization_code(self, code: str) -> None:
|
|
130
|
+
"""Store authorization code in state (called by webhook)."""
|
|
131
|
+
self._client.set_authorization_code(code)
|
|
132
|
+
|
|
133
|
+
def exchange_code_for_token(self, code: str) -> OAuthToken:
|
|
134
|
+
"""Exchange an authorization code for tokens."""
|
|
135
|
+
return self._client.fetch_token_with_authorization_code(
|
|
136
|
+
code,
|
|
137
|
+
extra_params=self._extra_token_params,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
def wait_for_authorization(
|
|
141
|
+
self,
|
|
142
|
+
on_progress: Callable[[int], None] | None = None,
|
|
143
|
+
) -> OAuthToken:
|
|
144
|
+
"""Wait for user authorization to complete by polling state."""
|
|
145
|
+
start_time = time.time()
|
|
146
|
+
iteration = 0
|
|
147
|
+
|
|
148
|
+
while time.time() - start_time < self._poll_timeout:
|
|
149
|
+
time.sleep(self._poll_interval)
|
|
150
|
+
iteration += 1
|
|
151
|
+
|
|
152
|
+
if on_progress:
|
|
153
|
+
on_progress(iteration)
|
|
154
|
+
|
|
155
|
+
code = self._client.get_authorization_code(force_reload=True)
|
|
156
|
+
if code:
|
|
157
|
+
return self.exchange_code_for_token(code)
|
|
158
|
+
|
|
159
|
+
raise OAuthClientError(
|
|
160
|
+
f"Authorization timed out after {self._poll_timeout} seconds"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def authenticate(self) -> OAuthToken:
|
|
164
|
+
"""Execute the full authorization code flow."""
|
|
165
|
+
auth_url = self.get_authorization_url()
|
|
166
|
+
raise AuthorizationRequiredError(
|
|
167
|
+
f"User authorization required. Please visit: {auth_url}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def get_token(self) -> OAuthToken:
|
|
171
|
+
"""Get a valid token, refreshing if necessary."""
|
|
172
|
+
return self._client.get_valid_token(auto_refresh=True)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
from collections.abc import Generator
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from soar_sdk.auth.client import SOARAssetOAuthClient
|
|
9
|
+
from soar_sdk.auth.models import OAuthToken
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BasicAuth(httpx.Auth):
|
|
13
|
+
"""HTTPX authentication using HTTP Basic Authentication."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, username: str, password: str) -> None:
|
|
16
|
+
self._username = username
|
|
17
|
+
self._password = password
|
|
18
|
+
|
|
19
|
+
def auth_flow(
|
|
20
|
+
self,
|
|
21
|
+
request: httpx.Request,
|
|
22
|
+
) -> Generator[httpx.Request, httpx.Response]:
|
|
23
|
+
"""Add Basic authentication header to the request."""
|
|
24
|
+
credentials = f"{self._username}:{self._password}"
|
|
25
|
+
encoded = base64.b64encode(credentials.encode()).decode("ascii")
|
|
26
|
+
request.headers["Authorization"] = f"Basic {encoded}"
|
|
27
|
+
yield request
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class StaticTokenAuth(httpx.Auth):
|
|
31
|
+
"""HTTPX authentication using a static token."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
token: OAuthToken | str,
|
|
36
|
+
*,
|
|
37
|
+
token_type: str = "Bearer", # noqa: S107
|
|
38
|
+
header_name: str = "Authorization",
|
|
39
|
+
) -> None:
|
|
40
|
+
if isinstance(token, str):
|
|
41
|
+
self._access_token = token
|
|
42
|
+
else:
|
|
43
|
+
self._access_token = token.access_token
|
|
44
|
+
token_type = token.token_type or token_type
|
|
45
|
+
self._token_type = token_type
|
|
46
|
+
self._header_name = header_name
|
|
47
|
+
|
|
48
|
+
def auth_flow(
|
|
49
|
+
self,
|
|
50
|
+
request: httpx.Request,
|
|
51
|
+
) -> Generator[httpx.Request, httpx.Response]:
|
|
52
|
+
"""Add authentication header to the request."""
|
|
53
|
+
if self._token_type:
|
|
54
|
+
request.headers[self._header_name] = (
|
|
55
|
+
f"{self._token_type} {self._access_token}"
|
|
56
|
+
)
|
|
57
|
+
else:
|
|
58
|
+
request.headers[self._header_name] = self._access_token
|
|
59
|
+
yield request
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OAuthBearerAuth(httpx.Auth):
|
|
63
|
+
"""HTTPX authentication using OAuth Bearer tokens."""
|
|
64
|
+
|
|
65
|
+
requires_response_body = True
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
oauth_client: SOARAssetOAuthClient,
|
|
70
|
+
*,
|
|
71
|
+
auto_refresh: bool = True,
|
|
72
|
+
) -> None:
|
|
73
|
+
self._oauth_client = oauth_client
|
|
74
|
+
self._auto_refresh = auto_refresh
|
|
75
|
+
self._token: OAuthToken | None = None
|
|
76
|
+
|
|
77
|
+
def auth_flow(
|
|
78
|
+
self,
|
|
79
|
+
request: httpx.Request,
|
|
80
|
+
) -> Generator[httpx.Request, httpx.Response]:
|
|
81
|
+
"""Handle authentication flow for a request."""
|
|
82
|
+
if self._token is None or self._token.is_expired():
|
|
83
|
+
self._token = self._oauth_client.get_valid_token(
|
|
84
|
+
auto_refresh=self._auto_refresh
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
request.headers["Authorization"] = f"Bearer {self._token.access_token}"
|
|
88
|
+
response = yield request
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
response.status_code == 401
|
|
92
|
+
and self._auto_refresh
|
|
93
|
+
and self._token.refresh_token
|
|
94
|
+
):
|
|
95
|
+
self._token = self._oauth_client.refresh_token(self._token.refresh_token)
|
|
96
|
+
request.headers["Authorization"] = f"Bearer {self._token.access_token}"
|
|
97
|
+
yield request
|
soar_sdk/auth/models.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OAuthGrantType(StrEnum):
|
|
10
|
+
"""Supported OAuth 2.0 grant types."""
|
|
11
|
+
|
|
12
|
+
AUTHORIZATION_CODE = "authorization_code"
|
|
13
|
+
CLIENT_CREDENTIALS = "client_credentials"
|
|
14
|
+
REFRESH_TOKEN = "refresh_token" # noqa: S105
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OAuthToken(BaseModel):
|
|
18
|
+
"""OAuth 2.0 token response."""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(extra="allow")
|
|
21
|
+
|
|
22
|
+
_DEFAULT_TOKEN_TYPE = "Bearer" # noqa: S105
|
|
23
|
+
|
|
24
|
+
access_token: str
|
|
25
|
+
token_type: str = _DEFAULT_TOKEN_TYPE
|
|
26
|
+
expires_in: int | None = None
|
|
27
|
+
refresh_token: str | None = None
|
|
28
|
+
scope: str | None = None
|
|
29
|
+
expires_at: float | None = None
|
|
30
|
+
|
|
31
|
+
def model_post_init(self, __context: object) -> None:
|
|
32
|
+
"""Calculate expires_at if not provided but expires_in is available."""
|
|
33
|
+
if self.expires_at is None and self.expires_in is not None:
|
|
34
|
+
self.expires_at = time.time() + self.expires_in
|
|
35
|
+
|
|
36
|
+
def is_expired(self, leeway: int = 30) -> bool:
|
|
37
|
+
"""Check if the token is expired."""
|
|
38
|
+
if self.expires_at is None:
|
|
39
|
+
return False
|
|
40
|
+
return time.time() >= (self.expires_at - leeway)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OAuthConfig(BaseModel):
|
|
44
|
+
"""Configuration for OAuth 2.0 authentication."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(extra="forbid")
|
|
47
|
+
|
|
48
|
+
client_id: str
|
|
49
|
+
client_secret: str | None = None
|
|
50
|
+
authorization_endpoint: str | None = None
|
|
51
|
+
token_endpoint: str
|
|
52
|
+
redirect_uri: str | None = None
|
|
53
|
+
scope: str | list[str] | None = None
|
|
54
|
+
grant_type: OAuthGrantType = OAuthGrantType.AUTHORIZATION_CODE
|
|
55
|
+
|
|
56
|
+
def get_scope_string(self) -> str | None:
|
|
57
|
+
"""Return scope as a space-separated string."""
|
|
58
|
+
if self.scope is None:
|
|
59
|
+
return None
|
|
60
|
+
if isinstance(self.scope, list):
|
|
61
|
+
return " ".join(self.scope)
|
|
62
|
+
return self.scope
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class OAuthSession(BaseModel):
|
|
66
|
+
"""Represents an active OAuth authentication session."""
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(extra="forbid")
|
|
69
|
+
|
|
70
|
+
session_id: str
|
|
71
|
+
asset_id: str
|
|
72
|
+
auth_pending: bool = True
|
|
73
|
+
auth_complete: bool = False
|
|
74
|
+
auth_code: str | None = None
|
|
75
|
+
error: str | None = None
|
|
76
|
+
error_description: str | None = None
|
|
77
|
+
state: str | None = None
|
|
78
|
+
code_verifier: str | None = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class OAuthState(BaseModel):
|
|
82
|
+
"""State persisted in auth_state for OAuth authentication."""
|
|
83
|
+
|
|
84
|
+
model_config = ConfigDict(extra="forbid")
|
|
85
|
+
|
|
86
|
+
token: OAuthToken | None = None
|
|
87
|
+
session: OAuthSession | None = None
|
|
88
|
+
client_id: str | None = Field(
|
|
89
|
+
default=None,
|
|
90
|
+
description="Stored client_id to detect credential changes",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CertificateCredentials(BaseModel):
|
|
95
|
+
"""Certificate-based authentication credentials."""
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(extra="forbid")
|
|
98
|
+
|
|
99
|
+
certificate_thumbprint: str
|
|
100
|
+
private_key: str
|
|
101
|
+
tenant_id: str | None = None
|
soar_sdk/cli/package/cli.py
CHANGED
|
@@ -27,35 +27,53 @@ 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:
|
|
31
|
-
"""Asynchronously collect all wheels from the given
|
|
32
|
-
|
|
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(
|
|
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(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
54
|
-
*(
|
|
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
|
-
|
|
58
|
-
|
|
66
|
+
cache = dict(gathered_results)
|
|
67
|
+
|
|
68
|
+
for key, wheel_group in dedupe_map.items():
|
|
69
|
+
representative = wheel_group[0]
|
|
70
|
+
if representative.sdist is not None or representative.source_dir is not None:
|
|
71
|
+
for path, _ in cache[key]:
|
|
72
|
+
wheel_name = Path(path).name
|
|
73
|
+
for wheel in wheel_group:
|
|
74
|
+
wheel._record_built_wheel(wheel_name)
|
|
75
|
+
|
|
76
|
+
return list(chain.from_iterable(cache.values()))
|
|
59
77
|
|
|
60
78
|
|
|
61
79
|
@package.command()
|
|
@@ -124,13 +142,13 @@ def build(
|
|
|
124
142
|
|
|
125
143
|
with tarfile.open(output_file, "w:gz") as app_tarball:
|
|
126
144
|
# Collect all wheels from both Python versions
|
|
127
|
-
all_wheels =
|
|
145
|
+
all_wheels = (
|
|
128
146
|
app_meta.pip313_dependencies.wheel + app_meta.pip314_dependencies.wheel
|
|
129
147
|
)
|
|
130
148
|
|
|
131
149
|
# Run the async collection function within an event loop
|
|
132
150
|
console.print(
|
|
133
|
-
f"[yellow]Collecting [bold]{len(all_wheels)}[/bold] wheel{'' if len(all_wheels) == 1 else 's'} for package[/]"
|
|
151
|
+
f"[yellow]Collecting [bold]{len(all_wheels)}[/bold] wheel{'' if len(set(all_wheels)) == 1 else 's'} for package[/]"
|
|
134
152
|
)
|
|
135
153
|
wheel_data = asyncio.run(collect_all_wheels(all_wheels))
|
|
136
154
|
|
soar_sdk/meta/dependencies.py
CHANGED
|
@@ -219,18 +219,41 @@ class DependencyWheel(BaseModel):
|
|
|
219
219
|
sdist: UvSourceDistribution | None = Field(exclude=True, default=None)
|
|
220
220
|
source_dir: UvSourceDirectory | None = Field(exclude=True, default=None)
|
|
221
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)
|
|
242
|
+
|
|
222
243
|
async def collect_wheels(self) -> AsyncGenerator[tuple[str, bytes]]:
|
|
223
244
|
"""Collect a list of wheel files to fetch for this dependency across all platforms."""
|
|
224
245
|
if self.wheel is None and self.sdist is not None:
|
|
225
246
|
logger.info(f"Building sdist for {self.input_file}")
|
|
226
247
|
wheel_name, wheel_bytes = await self.sdist.fetch_and_build()
|
|
227
|
-
|
|
248
|
+
wheel_path = self._record_built_wheel(wheel_name)
|
|
249
|
+
yield (wheel_path, wheel_bytes)
|
|
228
250
|
return
|
|
229
251
|
|
|
230
252
|
if self.wheel is None and self.source_dir is not None:
|
|
231
253
|
logger.info(f"Building local sources for {self.input_file}")
|
|
232
254
|
wheel_name, wheel_bytes = self.source_dir.build()
|
|
233
|
-
|
|
255
|
+
wheel_path = self._record_built_wheel(wheel_name)
|
|
256
|
+
yield (wheel_path, wheel_bytes)
|
|
234
257
|
return
|
|
235
258
|
|
|
236
259
|
if self.wheel is None:
|
|
@@ -398,12 +421,14 @@ class UvPackage(BaseModel):
|
|
|
398
421
|
and UvLock.normalize_package_name(self.name) in DEPENDENCIES_TO_BUILD
|
|
399
422
|
):
|
|
400
423
|
wheel.sdist = self.sdist
|
|
424
|
+
wheel.set_placeholder_wheel_name(self.version)
|
|
401
425
|
|
|
402
426
|
if (
|
|
403
427
|
self.source.directory is not None
|
|
404
428
|
and UvLock.normalize_package_name(self.name) in DEPENDENCIES_TO_BUILD
|
|
405
429
|
):
|
|
406
430
|
wheel.source_dir = UvSourceDirectory(directory=self.source.directory)
|
|
431
|
+
wheel.set_placeholder_wheel_name(self.version)
|
|
407
432
|
|
|
408
433
|
try:
|
|
409
434
|
wheel_x86_64 = self._find_wheel(
|
|
@@ -429,7 +454,7 @@ class UvPackage(BaseModel):
|
|
|
429
454
|
wheel.input_file_aarch64 = f"{wheel_aarch64.basename}.whl"
|
|
430
455
|
wheel.wheel_aarch64 = wheel_aarch64
|
|
431
456
|
except FileNotFoundError:
|
|
432
|
-
if wheel.sdist is None:
|
|
457
|
+
if wheel.sdist is None and wheel.source_dir is None:
|
|
433
458
|
logger.warning(
|
|
434
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"
|
|
435
460
|
)
|
|
@@ -121,6 +121,13 @@ if TYPE_CHECKING or not _soar_is_available:
|
|
|
121
121
|
def get_config(self) -> dict:
|
|
122
122
|
return self.config
|
|
123
123
|
|
|
124
|
+
def get_app_id(self) -> str:
|
|
125
|
+
return self.__app_json.get("appid", "")
|
|
126
|
+
|
|
127
|
+
def get_state_dir(self) -> str:
|
|
128
|
+
phantom_home = os.getenv("PHANTOM_HOME", "/opt/phantom")
|
|
129
|
+
return f"{phantom_home}/local_data/app_states/{self.get_app_id()}/"
|
|
130
|
+
|
|
124
131
|
def save_state(self, state: dict) -> None:
|
|
125
132
|
self.__state = state
|
|
126
133
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: splunk-soar-sdk
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.0
|
|
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
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Classifier: Typing :: Typed
|
|
19
19
|
Requires-Python: <3.15,>=3.13
|
|
20
|
+
Requires-Dist: authlib>=1.3.0
|
|
20
21
|
Requires-Dist: beautifulsoup4>=4.10.0
|
|
21
22
|
Requires-Dist: bleach>=6.2.0
|
|
22
23
|
Requires-Dist: build>=1.3.0
|
|
@@ -29,6 +30,7 @@ Requires-Dist: humanize>=4.12.2
|
|
|
29
30
|
Requires-Dist: jinja2>=3.1.0
|
|
30
31
|
Requires-Dist: packaging>=25.0
|
|
31
32
|
Requires-Dist: pydantic<3,>=2
|
|
33
|
+
Requires-Dist: pyjwt[crypto]>=2.8.0
|
|
32
34
|
Requires-Dist: requests<3
|
|
33
35
|
Requires-Dist: setuptools>=80.9.0
|
|
34
36
|
Requires-Dist: toml<1,>=0.10.2
|
|
@@ -1,12 +1,12 @@
|
|
|
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=
|
|
5
|
-
soar_sdk/app.py,sha256=
|
|
4
|
+
soar_sdk/actions_manager.py,sha256=8IYOi2k8i9LHXEhQVZ0Ig3IS1gD3iAmOJ9q0bi14g-o,7179
|
|
5
|
+
soar_sdk/app.py,sha256=2bUWx1BgWS9Kwkp0aUzVWM8LTdVUq1JI2eXR0LhwEMU,37092
|
|
6
6
|
soar_sdk/app_cli_runner.py,sha256=K1ATWyGs0iNgPfIjMthsN72laOXqXCFZNEXfuzAMOM4,11645
|
|
7
7
|
soar_sdk/app_client.py,sha256=hbe1R2QwXDmoS4959a-ay9oylD1Qk-oPJvJRnxvICz0,6281
|
|
8
|
-
soar_sdk/asset.py,sha256=
|
|
9
|
-
soar_sdk/asset_state.py,sha256=
|
|
8
|
+
soar_sdk/asset.py,sha256=FI4HHtFdDXcOuad8sXB4ShkTzM8la42sZivCKjJK78o,11596
|
|
9
|
+
soar_sdk/asset_state.py,sha256=qh4n8IoabVObIZXRPyM0zznwC5LcJpbADcybmDdQABc,2318
|
|
10
10
|
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
|
|
@@ -32,6 +32,12 @@ soar_sdk/app_templates/basic_app/logo.svg,sha256=_JTop6spn5oPWPk-w6Tzumx_FTSBanO
|
|
|
32
32
|
soar_sdk/app_templates/basic_app/logo_dark.svg,sha256=PTxIs_1CKK9ZY3v-K1QoGwaUng9ZUL2MhUeO2jeHu_0,291
|
|
33
33
|
soar_sdk/app_templates/basic_app/uv.lock,sha256=AfgaIBg88KH-0iyXpCXacXAwHYKm0c-on2gWXjV9L-Y,80216
|
|
34
34
|
soar_sdk/app_templates/basic_app/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
+
soar_sdk/auth/__init__.py,sha256=Z6pSzWMd49aY4H-9IxdCeWxCEC8_K4kT7i4buyv03sw,896
|
|
36
|
+
soar_sdk/auth/client.py,sha256=QmQ2dcTpke7hI7aff4usgbM_LQ0Prw2PILEb56jQFII,17077
|
|
37
|
+
soar_sdk/auth/factories.py,sha256=lNDxhrJtQpEEVV_dGEarwHHdU64oNPCxcZTrv0L4GEY,3994
|
|
38
|
+
soar_sdk/auth/flows.py,sha256=ahSOdr129s-GQjGUK8e2kNaLmjLTRzkxEJG3IwUws78,5457
|
|
39
|
+
soar_sdk/auth/httpx_auth.py,sha256=b_ldXjRxqZZiCjCnR3eZvUiDWTqYZShZKhu9NBn1Vns,3009
|
|
40
|
+
soar_sdk/auth/models.py,sha256=vSgRqHxyga2W8FGQZLjqHtk0yEQHkbEREshDePfJXgo,2856
|
|
35
41
|
soar_sdk/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
42
|
soar_sdk/cli/cli.py,sha256=9OqjVFPoIBdsO-Zmijcf0yBnjk3ggBPEOaCxKfvvw6g,998
|
|
37
43
|
soar_sdk/cli/path_utils.py,sha256=aJy3fzw2MIU-WHZn21H021B4sjGkKxW61l02QIFVGJ8,1087
|
|
@@ -43,7 +49,7 @@ soar_sdk/cli/manifests/cli.py,sha256=cly5xVdj4bBIdZVMQPIWTXRgUfd1ON3qKO-76Fwql18
|
|
|
43
49
|
soar_sdk/cli/manifests/deserializers.py,sha256=kwgPAMgUEXtIn4AuQOh1nkLfWFqe4qnYPZ1czB-FQTU,16516
|
|
44
50
|
soar_sdk/cli/manifests/processors.py,sha256=soiRTbfLQuetstt1Xk7vKmWzOUhgVON5JxjWMvnGN7w,5141
|
|
45
51
|
soar_sdk/cli/manifests/serializers.py,sha256=ulpq3nS8g1YrIP371XoQC3_kpz-9v2Ln_mqPyMtpWn8,3632
|
|
46
|
-
soar_sdk/cli/package/cli.py,sha256=
|
|
52
|
+
soar_sdk/cli/package/cli.py,sha256=hdpVBRvVGWaYDYhpDILlA7LRhunfElLbNdh21jviKrM,10087
|
|
47
53
|
soar_sdk/cli/package/utils.py,sha256=fl6PMcrdC2zA7A16byQuxxPyAI2Z-BqBLfLlF2ZNnQ4,1712
|
|
48
54
|
soar_sdk/cli/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
55
|
soar_sdk/cli/test/cli.py,sha256=iDrthN8L7B1RplLhq0EI69MndaOhvAXn7bqv3XzlfpM,7655
|
|
@@ -72,7 +78,7 @@ soar_sdk/meta/actions.py,sha256=qOXFChYxOVTt1d0gBffyw8ahJ3E4w7GsVoD4yzJN8Vc,2189
|
|
|
72
78
|
soar_sdk/meta/adapters.py,sha256=KjSYIUtkCz2eesA_vhsNCjfi5C-Uz71tbSuDIjhuB8U,1112
|
|
73
79
|
soar_sdk/meta/app.py,sha256=KbDiq1JgRiH4hudyKr95wMfcPN7YLyOkRjf4JIoFihs,2815
|
|
74
80
|
soar_sdk/meta/datatypes.py,sha256=piR-oBVAATiRciXSdVE7XaqjUZTgSaOvTEqcOcNvCS0,795
|
|
75
|
-
soar_sdk/meta/dependencies.py,sha256=
|
|
81
|
+
soar_sdk/meta/dependencies.py,sha256=SXWOoUJRmyW9RE3CrgF2sQLQhfyPcXAg7KO0ONf3h_g,23244
|
|
76
82
|
soar_sdk/meta/webhooks.py,sha256=ILKP9pNalKG9DLdNQNoDlu5KUnm0m7PyA2O0fbkqVrA,1217
|
|
77
83
|
soar_sdk/models/__init__.py,sha256=YZVAcBguAlUsxAnBBL6jSguJEzf5PYCtdvbNyU1XfEU,380
|
|
78
84
|
soar_sdk/models/artifact.py,sha256=G8hv9wPPoRgrAQzIf-YlCSjAlkHEcIPF389T1bo4yHw,1087
|
|
@@ -83,7 +89,7 @@ soar_sdk/models/vault_attachment.py,sha256=sdRnQdPiwgaZDojpap4ohH7u1Q5TYGP-drs8K
|
|
|
83
89
|
soar_sdk/models/view.py,sha256=BUuz6VVVe78hg7irGgZCbvBcycOmuPqplkagdi3T4Dg,779
|
|
84
90
|
soar_sdk/shims/phantom/action_result.py,sha256=Nddc9oswAfHU7I2q0pLm3HZ2YiLUQZUEIqqAjToZWnM,1606
|
|
85
91
|
soar_sdk/shims/phantom/app.py,sha256=uvE7Hsz5KudARpwaye7cx9lEOTMmPsJlZdunmkV8_lY,303
|
|
86
|
-
soar_sdk/shims/phantom/base_connector.py,sha256=
|
|
92
|
+
soar_sdk/shims/phantom/base_connector.py,sha256=2Tbh5nHnP6euilRiQL9NaVtmrgQIiY0eIkkzu-ah8_Y,5152
|
|
87
93
|
soar_sdk/shims/phantom/connector_result.py,sha256=b6yrR1uUXBhfwpUf8HzESItPgfaiHxNTXF8FaGdQNsk,640
|
|
88
94
|
soar_sdk/shims/phantom/consts.py,sha256=eq6AIuDhb2Z-CJORwv98D3JbcIOW8CC673zx5dNPFKU,404
|
|
89
95
|
soar_sdk/shims/phantom/encryption_helper.py,sha256=20VqqSFuftjB8bMriP6mjgvYWpYqYZyokYzq_aydkqU,1503
|
|
@@ -110,8 +116,8 @@ soar_sdk/views/components/pie_chart.py,sha256=LVTeHVJN6nf2vjUs9y7PDBhS0U1fKW750l
|
|
|
110
116
|
soar_sdk/webhooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
111
117
|
soar_sdk/webhooks/models.py,sha256=j3kbvYmcOlcj3gQYKtrv7iS-lDavMKYNLdCNMy_I2Hc,4542
|
|
112
118
|
soar_sdk/webhooks/routing.py,sha256=OjezhuAb8wzW0MnbGSnIWeAH3uJcu-Sb7s3w9zoiPVM,6873
|
|
113
|
-
splunk_soar_sdk-3.
|
|
114
|
-
splunk_soar_sdk-3.
|
|
115
|
-
splunk_soar_sdk-3.
|
|
116
|
-
splunk_soar_sdk-3.
|
|
117
|
-
splunk_soar_sdk-3.
|
|
119
|
+
splunk_soar_sdk-3.7.0.dist-info/METADATA,sha256=etgn77ft4OQs4sPRMdUk88IMtxC7CPQoECTOPmrv9kk,7544
|
|
120
|
+
splunk_soar_sdk-3.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
121
|
+
splunk_soar_sdk-3.7.0.dist-info/entry_points.txt,sha256=CgBjo2ZWpYNkt9TgvToL26h2Tg1yt8FbvYTb5NVgNuc,51
|
|
122
|
+
splunk_soar_sdk-3.7.0.dist-info/licenses/LICENSE,sha256=gNCGrGhrSQb1PUzBOByVUN1tvaliwLZfna-QU2r2hQ8,11345
|
|
123
|
+
splunk_soar_sdk-3.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|