aisoc-sdk 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aisoc_sdk/__init__.py +52 -0
- aisoc_sdk/client.py +292 -0
- aisoc_sdk/models.py +208 -0
- aisoc_sdk-4.0.0.dist-info/METADATA +109 -0
- aisoc_sdk-4.0.0.dist-info/RECORD +6 -0
- aisoc_sdk-4.0.0.dist-info/WHEEL +4 -0
aisoc_sdk/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""aisoc-sdk — Python client for AiSOC.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from aisoc_sdk import AiSOCClient
|
|
6
|
+
|
|
7
|
+
async with AiSOCClient(base_url="https://soc.example.com", token="aisoc_...") as client:
|
|
8
|
+
alerts = await client.alerts.list(severity="critical")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .client import AiSOCClient, AiSOCError
|
|
12
|
+
from .models import (
|
|
13
|
+
Alert,
|
|
14
|
+
AlertFilters,
|
|
15
|
+
AlertSeverity,
|
|
16
|
+
AlertStatus,
|
|
17
|
+
ApiKey,
|
|
18
|
+
ApiKeyCreateRequest,
|
|
19
|
+
ApiKeyCreateResponse,
|
|
20
|
+
Case,
|
|
21
|
+
CaseFilters,
|
|
22
|
+
CasePriority,
|
|
23
|
+
CaseStatus,
|
|
24
|
+
Connector,
|
|
25
|
+
DetectionRule,
|
|
26
|
+
Page,
|
|
27
|
+
Playbook,
|
|
28
|
+
PlaybookRun,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"AiSOCClient",
|
|
33
|
+
"AiSOCError",
|
|
34
|
+
"Alert",
|
|
35
|
+
"AlertFilters",
|
|
36
|
+
"AlertSeverity",
|
|
37
|
+
"AlertStatus",
|
|
38
|
+
"ApiKey",
|
|
39
|
+
"ApiKeyCreateRequest",
|
|
40
|
+
"ApiKeyCreateResponse",
|
|
41
|
+
"Case",
|
|
42
|
+
"CaseFilters",
|
|
43
|
+
"CasePriority",
|
|
44
|
+
"CaseStatus",
|
|
45
|
+
"Connector",
|
|
46
|
+
"DetectionRule",
|
|
47
|
+
"Page",
|
|
48
|
+
"Playbook",
|
|
49
|
+
"PlaybookRun",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
__version__ = "4.0.0"
|
aisoc_sdk/client.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""AiSOCClient — async httpx-based client for the AiSOC REST API.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
async with AiSOCClient(base_url="https://soc.example.com", token="aisoc_...") as c:
|
|
6
|
+
page = await c.alerts.list(severity="critical")
|
|
7
|
+
case = await c.cases.create(title="Incident", priority="high")
|
|
8
|
+
run = await c.playbooks.run("isolate-host", trigger_data={"host": "srv-42"})
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any, Optional, Type, TypeVar
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
from pydantic import TypeAdapter
|
|
17
|
+
|
|
18
|
+
from .models import (
|
|
19
|
+
Alert,
|
|
20
|
+
AlertFilters,
|
|
21
|
+
ApiKey,
|
|
22
|
+
ApiKeyCreateRequest,
|
|
23
|
+
ApiKeyCreateResponse,
|
|
24
|
+
Case,
|
|
25
|
+
CaseFilters,
|
|
26
|
+
Connector,
|
|
27
|
+
DetectionRule,
|
|
28
|
+
Page,
|
|
29
|
+
Playbook,
|
|
30
|
+
PlaybookRun,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
T = TypeVar("T")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ─── Error ────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AiSOCError(Exception):
|
|
40
|
+
"""Raised when the AiSOC API returns a non-2xx response."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, status_code: int, detail: str) -> None:
|
|
43
|
+
self.status_code = status_code
|
|
44
|
+
self.detail = detail
|
|
45
|
+
super().__init__(f"AiSOC API {status_code}: {detail}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ─── Base resource client ─────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _ResourceClient:
|
|
52
|
+
def __init__(self, http: httpx.AsyncClient) -> None:
|
|
53
|
+
self._http = http
|
|
54
|
+
|
|
55
|
+
async def _get(
|
|
56
|
+
self,
|
|
57
|
+
path: str,
|
|
58
|
+
params: Optional[dict[str, Any]] = None,
|
|
59
|
+
model: Optional[Type[T]] = None,
|
|
60
|
+
) -> Any:
|
|
61
|
+
r = await self._http.get(path, params=self._clean(params))
|
|
62
|
+
self._raise(r)
|
|
63
|
+
if model is not None:
|
|
64
|
+
return TypeAdapter(model).validate_python(r.json())
|
|
65
|
+
return r.json()
|
|
66
|
+
|
|
67
|
+
async def _post(self, path: str, body: Any, model: Optional[Type[T]] = None) -> Any:
|
|
68
|
+
r = await self._http.post(path, json=body)
|
|
69
|
+
self._raise(r)
|
|
70
|
+
if model is not None:
|
|
71
|
+
return TypeAdapter(model).validate_python(r.json())
|
|
72
|
+
return r.json()
|
|
73
|
+
|
|
74
|
+
async def _patch(self, path: str, body: Any, model: Optional[Type[T]] = None) -> Any:
|
|
75
|
+
r = await self._http.patch(path, json=body)
|
|
76
|
+
self._raise(r)
|
|
77
|
+
if model is not None:
|
|
78
|
+
return TypeAdapter(model).validate_python(r.json())
|
|
79
|
+
return r.json()
|
|
80
|
+
|
|
81
|
+
async def _put(self, path: str, body: Any, model: Optional[Type[T]] = None) -> Any:
|
|
82
|
+
r = await self._http.put(path, json=body)
|
|
83
|
+
self._raise(r)
|
|
84
|
+
if model is not None:
|
|
85
|
+
return TypeAdapter(model).validate_python(r.json())
|
|
86
|
+
return r.json()
|
|
87
|
+
|
|
88
|
+
async def _delete(self, path: str) -> None:
|
|
89
|
+
r = await self._http.delete(path)
|
|
90
|
+
self._raise(r)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _clean(params: Optional[dict[str, Any]]) -> dict[str, Any]:
|
|
94
|
+
if params is None:
|
|
95
|
+
return {}
|
|
96
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def _raise(r: httpx.Response) -> None:
|
|
100
|
+
if not r.is_success:
|
|
101
|
+
try:
|
|
102
|
+
detail = r.json().get("detail", r.text)
|
|
103
|
+
except Exception:
|
|
104
|
+
detail = r.text
|
|
105
|
+
raise AiSOCError(r.status_code, detail)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ─── Resource sub-clients ─────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AlertsClient(_ResourceClient):
|
|
112
|
+
async def list(self, filters: Optional[AlertFilters] = None, **kwargs: Any) -> Page[Alert]:
|
|
113
|
+
params = filters.model_dump(exclude_none=True) if filters else self._clean(kwargs)
|
|
114
|
+
return await self._get("/api/v1/alerts", params, Page[Alert])
|
|
115
|
+
|
|
116
|
+
async def get(self, alert_id: str) -> Alert:
|
|
117
|
+
return await self._get(f"/api/v1/alerts/{alert_id}", model=Alert)
|
|
118
|
+
|
|
119
|
+
async def update(self, alert_id: str, **data: Any) -> Alert:
|
|
120
|
+
return await self._patch(f"/api/v1/alerts/{alert_id}", data, Alert)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CasesClient(_ResourceClient):
|
|
124
|
+
async def list(self, filters: Optional[CaseFilters] = None, **kwargs: Any) -> Page[Case]:
|
|
125
|
+
params = filters.model_dump(exclude_none=True) if filters else self._clean(kwargs)
|
|
126
|
+
return await self._get("/api/v1/cases", params, Page[Case])
|
|
127
|
+
|
|
128
|
+
async def get(self, case_id: str) -> Case:
|
|
129
|
+
return await self._get(f"/api/v1/cases/{case_id}", model=Case)
|
|
130
|
+
|
|
131
|
+
async def create(self, **data: Any) -> Case:
|
|
132
|
+
return await self._post("/api/v1/cases", data, Case)
|
|
133
|
+
|
|
134
|
+
async def update(self, case_id: str, **data: Any) -> Case:
|
|
135
|
+
return await self._patch(f"/api/v1/cases/{case_id}", data, Case)
|
|
136
|
+
|
|
137
|
+
# There is no `delete`. The API serves no DELETE on a case — a case is
|
|
138
|
+
# closed by patching its status, and the method that used to be here
|
|
139
|
+
# called a route `services/api` has never declared.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class DetectionsClient(_ResourceClient):
|
|
143
|
+
# The route is `/detection/rules`, singular, and this client asked for
|
|
144
|
+
# `/detections` — so every method here answered 404 against a real
|
|
145
|
+
# deployment while its mocked test passed.
|
|
146
|
+
async def list(self, page: int = 1, page_size: int = 20) -> Page[DetectionRule]:
|
|
147
|
+
return await self._get(
|
|
148
|
+
"/api/v1/detection/rules", {"page": page, "page_size": page_size}, Page[DetectionRule]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def get(self, rule_id: str) -> DetectionRule:
|
|
152
|
+
return await self._get(f"/api/v1/detection/rules/{rule_id}", model=DetectionRule)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ConnectorsClient(_ResourceClient):
|
|
156
|
+
async def list(self, page: int = 1, page_size: int = 20) -> Page[Connector]:
|
|
157
|
+
return await self._get(
|
|
158
|
+
"/api/v1/connectors", {"page": page, "page_size": page_size}, Page[Connector]
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def get(self, connector_id: str) -> Connector:
|
|
162
|
+
return await self._get(f"/api/v1/connectors/{connector_id}", model=Connector)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class PlaybooksClient(_ResourceClient):
|
|
166
|
+
async def list(self, page: int = 1, page_size: int = 20) -> Page[Playbook]:
|
|
167
|
+
return await self._get(
|
|
168
|
+
"/api/v1/playbooks", {"page": page, "page_size": page_size}, Page[Playbook]
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def get(self, playbook_id: str) -> Playbook:
|
|
172
|
+
return await self._get(f"/api/v1/playbooks/{playbook_id}", model=Playbook)
|
|
173
|
+
|
|
174
|
+
async def create(self, **data: Any) -> Playbook:
|
|
175
|
+
return await self._post("/api/v1/playbooks", data, Playbook)
|
|
176
|
+
|
|
177
|
+
# PUT, not PATCH: `playbooks.py` declares `@router.put("/{playbook_id}")`
|
|
178
|
+
# and no patch route, so the previous verb returned 405.
|
|
179
|
+
async def update(self, playbook_id: str, **data: Any) -> Playbook:
|
|
180
|
+
return await self._put(f"/api/v1/playbooks/{playbook_id}", data, Playbook)
|
|
181
|
+
|
|
182
|
+
async def delete(self, playbook_id: str) -> None:
|
|
183
|
+
return await self._delete(f"/api/v1/playbooks/{playbook_id}")
|
|
184
|
+
|
|
185
|
+
async def run(
|
|
186
|
+
self,
|
|
187
|
+
playbook_id: str,
|
|
188
|
+
trigger_data: Optional[dict[str, Any]] = None,
|
|
189
|
+
) -> PlaybookRun:
|
|
190
|
+
return await self._post(
|
|
191
|
+
f"/api/v1/playbooks/{playbook_id}/run",
|
|
192
|
+
{"trigger_data": trigger_data or {}},
|
|
193
|
+
PlaybookRun,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def get_run(self, run_id: str) -> PlaybookRun:
|
|
197
|
+
return await self._get(f"/api/v1/playbooks/runs/{run_id}", model=PlaybookRun)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class ApiKeysClient(_ResourceClient):
|
|
201
|
+
async def list(self) -> Page[ApiKey]:
|
|
202
|
+
return await self._get("/api/v1/api-keys", model=Page[ApiKey])
|
|
203
|
+
|
|
204
|
+
async def create(self, req: ApiKeyCreateRequest) -> ApiKeyCreateResponse:
|
|
205
|
+
return await self._post(
|
|
206
|
+
"/api/v1/api-keys",
|
|
207
|
+
req.model_dump(exclude_none=True),
|
|
208
|
+
ApiKeyCreateResponse,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
async def revoke(self, key_id: str) -> None:
|
|
212
|
+
return await self._delete(f"/api/v1/api-keys/{key_id}")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ─── Main client ─────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class AiSOCClient:
|
|
219
|
+
"""Async Python client for the AiSOC REST API.
|
|
220
|
+
|
|
221
|
+
Must be used as an async context manager::
|
|
222
|
+
|
|
223
|
+
async with AiSOCClient(base_url="...", token="...") as client:
|
|
224
|
+
alerts = await client.alerts.list()
|
|
225
|
+
|
|
226
|
+
Or manage the lifecycle manually::
|
|
227
|
+
|
|
228
|
+
client = AiSOCClient(base_url="...", token="...")
|
|
229
|
+
await client.__aenter__()
|
|
230
|
+
try:
|
|
231
|
+
...
|
|
232
|
+
finally:
|
|
233
|
+
await client.__aexit__(None, None, None)
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(
|
|
237
|
+
self,
|
|
238
|
+
base_url: str,
|
|
239
|
+
token: str,
|
|
240
|
+
*,
|
|
241
|
+
timeout: float = 30.0,
|
|
242
|
+
headers: Optional[dict[str, str]] = None,
|
|
243
|
+
) -> None:
|
|
244
|
+
self._base_url = base_url.rstrip("/")
|
|
245
|
+
self._token = token
|
|
246
|
+
self._timeout = timeout
|
|
247
|
+
self._extra_headers = headers or {}
|
|
248
|
+
self._http: Optional[httpx.AsyncClient] = None
|
|
249
|
+
|
|
250
|
+
# Placeholders — initialised in __aenter__
|
|
251
|
+
self.alerts: AlertsClient
|
|
252
|
+
self.cases: CasesClient
|
|
253
|
+
self.detections: DetectionsClient
|
|
254
|
+
self.connectors: ConnectorsClient
|
|
255
|
+
self.playbooks: PlaybooksClient
|
|
256
|
+
self.api_keys: ApiKeysClient
|
|
257
|
+
|
|
258
|
+
async def __aenter__(self) -> "AiSOCClient":
|
|
259
|
+
self._http = httpx.AsyncClient(
|
|
260
|
+
base_url=self._base_url,
|
|
261
|
+
headers={
|
|
262
|
+
"Authorization": f"Bearer {self._token}",
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
**self._extra_headers,
|
|
265
|
+
},
|
|
266
|
+
timeout=self._timeout,
|
|
267
|
+
)
|
|
268
|
+
self.alerts = AlertsClient(self._http)
|
|
269
|
+
self.cases = CasesClient(self._http)
|
|
270
|
+
self.detections = DetectionsClient(self._http)
|
|
271
|
+
self.connectors = ConnectorsClient(self._http)
|
|
272
|
+
self.playbooks = PlaybooksClient(self._http)
|
|
273
|
+
self.api_keys = ApiKeysClient(self._http)
|
|
274
|
+
return self
|
|
275
|
+
|
|
276
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
277
|
+
if self._http is not None:
|
|
278
|
+
await self._http.aclose()
|
|
279
|
+
self._http = None
|
|
280
|
+
|
|
281
|
+
async def graphql(
|
|
282
|
+
self,
|
|
283
|
+
query: str,
|
|
284
|
+
variables: Optional[dict[str, Any]] = None,
|
|
285
|
+
) -> dict[str, Any]:
|
|
286
|
+
"""Execute a GraphQL query against the /graphql endpoint."""
|
|
287
|
+
if self._http is None:
|
|
288
|
+
raise RuntimeError("Use AiSOCClient as an async context manager")
|
|
289
|
+
r = await self._http.post("/graphql", json={"query": query, "variables": variables})
|
|
290
|
+
if not r.is_success:
|
|
291
|
+
raise AiSOCError(r.status_code, r.text)
|
|
292
|
+
return r.json() # type: ignore[return-value]
|
aisoc_sdk/models.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Pydantic models mirroring the AiSOC OpenAPI schema."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Generic, List, Optional, TypeVar
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ── Enums ─────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AlertSeverity(str, Enum):
|
|
16
|
+
CRITICAL = "critical"
|
|
17
|
+
HIGH = "high"
|
|
18
|
+
MEDIUM = "medium"
|
|
19
|
+
LOW = "low"
|
|
20
|
+
INFO = "info"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AlertStatus(str, Enum):
|
|
24
|
+
OPEN = "open"
|
|
25
|
+
IN_PROGRESS = "in_progress"
|
|
26
|
+
CLOSED = "closed"
|
|
27
|
+
FALSE_POSITIVE = "false_positive"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CasePriority(str, Enum):
|
|
31
|
+
CRITICAL = "critical"
|
|
32
|
+
HIGH = "high"
|
|
33
|
+
MEDIUM = "medium"
|
|
34
|
+
LOW = "low"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CaseStatus(str, Enum):
|
|
38
|
+
OPEN = "open"
|
|
39
|
+
INVESTIGATING = "investigating"
|
|
40
|
+
RESOLVED = "resolved"
|
|
41
|
+
CLOSED = "closed"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── Core models ───────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
_M = ConfigDict(populate_by_name=True, from_attributes=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Alert(BaseModel):
|
|
50
|
+
model_config = _M
|
|
51
|
+
|
|
52
|
+
id: str
|
|
53
|
+
tenant_id: str
|
|
54
|
+
title: str
|
|
55
|
+
severity: AlertSeverity
|
|
56
|
+
status: AlertStatus
|
|
57
|
+
source: str
|
|
58
|
+
source_ref: Optional[str] = None
|
|
59
|
+
mitre_tactics: List[str] = []
|
|
60
|
+
ai_score: Optional[float] = None
|
|
61
|
+
case_id: Optional[str] = None
|
|
62
|
+
created_at: datetime
|
|
63
|
+
updated_at: datetime
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Case(BaseModel):
|
|
67
|
+
model_config = _M
|
|
68
|
+
|
|
69
|
+
id: str
|
|
70
|
+
tenant_id: str
|
|
71
|
+
case_number: str
|
|
72
|
+
title: str
|
|
73
|
+
status: CaseStatus
|
|
74
|
+
priority: CasePriority
|
|
75
|
+
assignee: Optional[str] = None
|
|
76
|
+
mitre_tactics: List[str] = []
|
|
77
|
+
alert_ids: List[str] = []
|
|
78
|
+
created_at: datetime
|
|
79
|
+
updated_at: datetime
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DetectionRule(BaseModel):
|
|
83
|
+
model_config = _M
|
|
84
|
+
|
|
85
|
+
id: str
|
|
86
|
+
tenant_id: str
|
|
87
|
+
name: str
|
|
88
|
+
description: Optional[str] = None
|
|
89
|
+
rule_language: str
|
|
90
|
+
severity: AlertSeverity
|
|
91
|
+
enabled: bool
|
|
92
|
+
created_at: datetime
|
|
93
|
+
updated_at: datetime
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Connector(BaseModel):
|
|
97
|
+
model_config = _M
|
|
98
|
+
|
|
99
|
+
id: str
|
|
100
|
+
tenant_id: str
|
|
101
|
+
name: str
|
|
102
|
+
connector_type: str
|
|
103
|
+
is_enabled: bool
|
|
104
|
+
health_status: str
|
|
105
|
+
events_ingested: int = 0
|
|
106
|
+
created_at: datetime
|
|
107
|
+
updated_at: datetime
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class PlaybookStep(BaseModel):
|
|
111
|
+
model_config = _M
|
|
112
|
+
|
|
113
|
+
id: str
|
|
114
|
+
name: str
|
|
115
|
+
type: str
|
|
116
|
+
action: Optional[str] = None
|
|
117
|
+
parameters: Optional[dict[str, Any]] = None
|
|
118
|
+
next_steps: List[str] = []
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Playbook(BaseModel):
|
|
122
|
+
model_config = _M
|
|
123
|
+
|
|
124
|
+
id: str
|
|
125
|
+
name: str
|
|
126
|
+
description: Optional[str] = None
|
|
127
|
+
version: str
|
|
128
|
+
steps: List[PlaybookStep] = []
|
|
129
|
+
trigger_conditions: Optional[dict[str, Any]] = None
|
|
130
|
+
created_at: datetime
|
|
131
|
+
updated_at: datetime
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class PlaybookRun(BaseModel):
|
|
135
|
+
model_config = _M
|
|
136
|
+
|
|
137
|
+
run_id: str
|
|
138
|
+
playbook_id: str
|
|
139
|
+
status: str
|
|
140
|
+
started_at: datetime
|
|
141
|
+
completed_at: Optional[datetime] = None
|
|
142
|
+
trigger_data: Optional[dict[str, Any]] = None
|
|
143
|
+
step_results: Optional[dict[str, Any]] = None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ApiKey(BaseModel):
|
|
147
|
+
model_config = _M
|
|
148
|
+
|
|
149
|
+
id: str
|
|
150
|
+
name: str
|
|
151
|
+
prefix: str
|
|
152
|
+
scopes: List[str]
|
|
153
|
+
expires_at: Optional[datetime] = None
|
|
154
|
+
last_used_at: Optional[datetime] = None
|
|
155
|
+
created_at: datetime
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ── Pagination ────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
T = TypeVar("T")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Page(BaseModel, Generic[T]):
|
|
164
|
+
model_config = _M
|
|
165
|
+
|
|
166
|
+
items: List[T]
|
|
167
|
+
total: int
|
|
168
|
+
page: int
|
|
169
|
+
page_size: int
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ── Request / response helpers ────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class AlertFilters(BaseModel):
|
|
176
|
+
model_config = _M
|
|
177
|
+
|
|
178
|
+
severity: Optional[AlertSeverity] = None
|
|
179
|
+
status: Optional[AlertStatus] = None
|
|
180
|
+
case_id: Optional[str] = None
|
|
181
|
+
search: Optional[str] = None
|
|
182
|
+
page: int = 1
|
|
183
|
+
page_size: int = 20
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class CaseFilters(BaseModel):
|
|
187
|
+
model_config = _M
|
|
188
|
+
|
|
189
|
+
status: Optional[CaseStatus] = None
|
|
190
|
+
priority: Optional[CasePriority] = None
|
|
191
|
+
assignee: Optional[str] = None
|
|
192
|
+
page: int = 1
|
|
193
|
+
page_size: int = 20
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ApiKeyCreateRequest(BaseModel):
|
|
197
|
+
model_config = _M
|
|
198
|
+
|
|
199
|
+
name: str
|
|
200
|
+
scopes: List[str]
|
|
201
|
+
expires_at: Optional[datetime] = None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class ApiKeyCreateResponse(BaseModel):
|
|
205
|
+
model_config = _M
|
|
206
|
+
|
|
207
|
+
key: ApiKey
|
|
208
|
+
raw_key: str
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aisoc-sdk
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: Python client SDK for AiSOC — typed httpx client
|
|
5
|
+
Project-URL: Homepage, https://github.com/beenuar/AiSOC
|
|
6
|
+
Project-URL: Documentation, https://beenuar.github.io/AiSOC
|
|
7
|
+
Project-URL: Repository, https://github.com/beenuar/AiSOC
|
|
8
|
+
Project-URL: Issues, https://github.com/beenuar/AiSOC/issues
|
|
9
|
+
Author-email: AiSOC Contributors <oss@aisoc.io>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: aisoc,client,sdk,security,soc
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: httpx>=0.27.0
|
|
14
|
+
Requires-Dist: pydantic>=2.0.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: mypy<3,>=2.3.1; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff<0.17,>=0.16.8; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# aisoc-sdk
|
|
24
|
+
|
|
25
|
+
[](../../LICENSE)
|
|
26
|
+
[](https://github.com/beenuar/AiSOC/blob/main/CHANGELOG.md)
|
|
27
|
+
|
|
28
|
+
Async Python client SDK for [AiSOC](https://github.com/beenuar/AiSOC).
|
|
29
|
+
|
|
30
|
+
> **Status — monorepo today, not yet on PyPI.** `pip install aisoc-sdk` does not resolve; install from the monorepo source path below. The import path (`aisoc_sdk`) and API surface stay identical once it ships.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Today (from this monorepo):
|
|
36
|
+
git clone https://github.com/beenuar/AiSOC.git
|
|
37
|
+
cd AiSOC && pip install -e packages/sdk-py
|
|
38
|
+
|
|
39
|
+
# Not yet on PyPI — the upload is blocked on registry credentials,
|
|
40
|
+
# which is an account action rather than a code change. Until then, install
|
|
41
|
+
# from source with the command above.
|
|
42
|
+
# pip install aisoc-sdk
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import asyncio
|
|
49
|
+
from aisoc_sdk import AiSOCClient
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def main():
|
|
53
|
+
async with AiSOCClient(
|
|
54
|
+
base_url="https://your-aisoc.example.com",
|
|
55
|
+
token="aisoc_...",
|
|
56
|
+
) as client:
|
|
57
|
+
# List critical open alerts
|
|
58
|
+
alerts = await client.alerts.list(severity="critical", status="open")
|
|
59
|
+
print(f"Found {alerts.total} critical alerts")
|
|
60
|
+
|
|
61
|
+
# Create a case
|
|
62
|
+
case = await client.cases.create(
|
|
63
|
+
title="Suspicious lateral movement",
|
|
64
|
+
priority="high",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Trigger a playbook
|
|
68
|
+
run = await client.playbooks.run(
|
|
69
|
+
"isolate-host",
|
|
70
|
+
trigger_data={"host_id": "srv-prod-42", "case_id": case.id},
|
|
71
|
+
)
|
|
72
|
+
print("Playbook run:", run.run_id)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
asyncio.run(main())
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## GraphQL
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
async with AiSOCClient(base_url="...", token="...") as client:
|
|
82
|
+
result = await client.graphql("""
|
|
83
|
+
query {
|
|
84
|
+
alerts(pageSize: 10, status: "open") {
|
|
85
|
+
items { id title severity }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
""")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## API reference
|
|
92
|
+
|
|
93
|
+
All resource methods are `async` and return typed Pydantic models.
|
|
94
|
+
|
|
95
|
+
| Attribute | Methods |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `client.alerts` | `list(filters?)`, `get(id)`, `update(id, **data)` |
|
|
98
|
+
| `client.cases` | `list(filters?)`, `get(id)`, `create(**data)`, `update(id, **data)`, `delete(id)` |
|
|
99
|
+
| `client.detections` | `list(page, page_size)`, `get(id)` |
|
|
100
|
+
| `client.connectors` | `list(page, page_size)`, `get(id)` |
|
|
101
|
+
| `client.playbooks` | `list(page, page_size)`, `get(id)`, `create(**data)`, `update(id, **data)`, `delete(id)`, `run(id, trigger_data?)`, `get_run(run_id)` |
|
|
102
|
+
| `client.api_keys` | `list()`, `create(req)`, `revoke(id)` |
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install -e ".[dev]"
|
|
108
|
+
pytest
|
|
109
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
aisoc_sdk/__init__.py,sha256=cF4BWhjS-orfVyWEXsKUQRBjdoo6MrDXXcaURQoINWQ,955
|
|
2
|
+
aisoc_sdk/client.py,sha256=VBp10g4i6qKOa_zMXqrBbvhcR9TEkuPfYG_ckGwaCoU,10630
|
|
3
|
+
aisoc_sdk/models.py,sha256=iWCoAr3ZRL3HuZ2NPxbxwcvYbSSUyTz-rLybpl-94G0,4677
|
|
4
|
+
aisoc_sdk-4.0.0.dist-info/METADATA,sha256=h0pX-Pou7jm_ZhajJ52-mltlu0RGe4dSfn1br_mSubI,3485
|
|
5
|
+
aisoc_sdk-4.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
6
|
+
aisoc_sdk-4.0.0.dist-info/RECORD,,
|