blackshield-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of blackshield-sdk might be problematic. Click here for more details.

Files changed (44) hide show
  1. blackshield/__init__.py +47 -0
  2. blackshield/_async_client.py +130 -0
  3. blackshield/_base_client.py +420 -0
  4. blackshield/_client.py +156 -0
  5. blackshield/_exceptions.py +74 -0
  6. blackshield/_types.py +14 -0
  7. blackshield/_version.py +3 -0
  8. blackshield/resources/__init__.py +1 -0
  9. blackshield/resources/ai_gateway.py +64 -0
  10. blackshield/resources/alerts.py +127 -0
  11. blackshield/resources/api_keys.py +61 -0
  12. blackshield/resources/audit.py +95 -0
  13. blackshield/resources/compliance.py +112 -0
  14. blackshield/resources/connectors.py +40 -0
  15. blackshield/resources/dashboard.py +76 -0
  16. blackshield/resources/easm.py +108 -0
  17. blackshield/resources/findings.py +221 -0
  18. blackshield/resources/identity.py +87 -0
  19. blackshield/resources/ingestion.py +178 -0
  20. blackshield/resources/policies.py +242 -0
  21. blackshield/resources/remediation.py +175 -0
  22. blackshield/resources/reports.py +109 -0
  23. blackshield/resources/security_reviews.py +158 -0
  24. blackshield/resources/threat_intel.py +97 -0
  25. blackshield/types/__init__.py +86 -0
  26. blackshield/types/ai_gateway.py +47 -0
  27. blackshield/types/alerts.py +73 -0
  28. blackshield/types/api_keys.py +30 -0
  29. blackshield/types/audit.py +24 -0
  30. blackshield/types/compliance.py +50 -0
  31. blackshield/types/connectors.py +60 -0
  32. blackshield/types/dashboard.py +77 -0
  33. blackshield/types/easm.py +64 -0
  34. blackshield/types/findings.py +113 -0
  35. blackshield/types/identity.py +76 -0
  36. blackshield/types/ingestion.py +134 -0
  37. blackshield/types/policies.py +78 -0
  38. blackshield/types/remediation.py +87 -0
  39. blackshield/types/reports.py +42 -0
  40. blackshield/types/security_reviews.py +47 -0
  41. blackshield/types/threat_intel.py +62 -0
  42. blackshield_sdk-0.1.0.dist-info/METADATA +153 -0
  43. blackshield_sdk-0.1.0.dist-info/RECORD +44 -0
  44. blackshield_sdk-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,47 @@
1
+ """BlackShield Python SDK — Official client for the BlackShield Security Platform.
2
+
3
+ Quick start::
4
+
5
+ import blackshield
6
+
7
+ client = blackshield.BlackShield(api_key="bsk_live_...")
8
+ page = client.findings.list(severity=["critical", "high"])
9
+ for finding in page.items:
10
+ print(f"{finding.severity.value} | {finding.title}")
11
+
12
+ For async usage::
13
+
14
+ from blackshield import AsyncBlackShield
15
+
16
+ async with AsyncBlackShield(api_key="bsk_live_...") as client:
17
+ page = await client.findings.list()
18
+ """
19
+
20
+ from blackshield._async_client import AsyncBlackShield
21
+ from blackshield._client import BlackShield
22
+ from blackshield._exceptions import (
23
+ APIError,
24
+ AuthenticationError,
25
+ BlackShieldError,
26
+ ConflictError,
27
+ NotFoundError,
28
+ PermissionDeniedError,
29
+ RateLimitError,
30
+ ValidationError,
31
+ )
32
+ from blackshield._version import __version__
33
+
34
+ __all__ = [
35
+ "APIError",
36
+ "AsyncBlackShield",
37
+ "AuthenticationError",
38
+ "BlackShield",
39
+ # Exceptions
40
+ "BlackShieldError",
41
+ "ConflictError",
42
+ "NotFoundError",
43
+ "PermissionDeniedError",
44
+ "RateLimitError",
45
+ "ValidationError",
46
+ "__version__",
47
+ ]
@@ -0,0 +1,130 @@
1
+ """Asynchronous BlackShield client — async/await variant."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from blackshield._base_client import AsyncHTTPClient
6
+ from blackshield.resources.ai_gateway import AsyncAiGatewayResource
7
+ from blackshield.resources.alerts import AsyncAlertsResource
8
+ from blackshield.resources.api_keys import AsyncApiKeysResource
9
+ from blackshield.resources.audit import AsyncAuditResource
10
+ from blackshield.resources.compliance import AsyncComplianceResource
11
+ from blackshield.resources.connectors import AsyncConnectorsResource
12
+ from blackshield.resources.dashboard import AsyncDashboardResource
13
+ from blackshield.resources.easm import AsyncEasmResource
14
+ from blackshield.resources.findings import AsyncFindingsResource
15
+ from blackshield.resources.identity import AsyncIdentityResource
16
+ from blackshield.resources.ingestion import AsyncIngestionResource
17
+ from blackshield.resources.policies import AsyncPoliciesResource
18
+ from blackshield.resources.remediation import AsyncRemediationResource
19
+ from blackshield.resources.reports import AsyncReportsResource
20
+ from blackshield.resources.security_reviews import AsyncSecurityReviewsResource
21
+ from blackshield.resources.threat_intel import AsyncThreatIntelResource
22
+
23
+
24
+ class AsyncBlackShield:
25
+ """Asynchronous client for the BlackShield Security Platform API."""
26
+
27
+ def __init__(
28
+ self,
29
+ api_key: str,
30
+ *,
31
+ base_url: str = "https://api.blackshield.security",
32
+ timeout: float = 60.0,
33
+ max_retries: int = 3,
34
+ ) -> None:
35
+ self._http = AsyncHTTPClient(
36
+ base_url=base_url,
37
+ api_key=api_key,
38
+ timeout=timeout,
39
+ max_retries=max_retries,
40
+ )
41
+ self._findings = AsyncFindingsResource(self._http)
42
+ self._ingestion = AsyncIngestionResource(self._http)
43
+ self._dashboard = AsyncDashboardResource(self._http)
44
+ self._compliance = AsyncComplianceResource(self._http)
45
+ self._reports = AsyncReportsResource(self._http)
46
+ self._easm = AsyncEasmResource(self._http)
47
+ self._threat_intel = AsyncThreatIntelResource(self._http)
48
+ self._connectors = AsyncConnectorsResource(self._http)
49
+ self._api_keys = AsyncApiKeysResource(self._http)
50
+ self._policies = AsyncPoliciesResource(self._http)
51
+ self._security_reviews = AsyncSecurityReviewsResource(self._http)
52
+ self._audit = AsyncAuditResource(self._http)
53
+ self._alerts = AsyncAlertsResource(self._http)
54
+ self._identity = AsyncIdentityResource(self._http)
55
+ self._remediation = AsyncRemediationResource(self._http)
56
+ self._ai_gateway = AsyncAiGatewayResource(self._http)
57
+
58
+ @property
59
+ def findings(self) -> AsyncFindingsResource:
60
+ return self._findings
61
+
62
+ @property
63
+ def ingestion(self) -> AsyncIngestionResource:
64
+ return self._ingestion
65
+
66
+ @property
67
+ def dashboard(self) -> AsyncDashboardResource:
68
+ return self._dashboard
69
+
70
+ @property
71
+ def compliance(self) -> AsyncComplianceResource:
72
+ return self._compliance
73
+
74
+ @property
75
+ def reports(self) -> AsyncReportsResource:
76
+ return self._reports
77
+
78
+ @property
79
+ def easm(self) -> AsyncEasmResource:
80
+ return self._easm
81
+
82
+ @property
83
+ def threat_intel(self) -> AsyncThreatIntelResource:
84
+ return self._threat_intel
85
+
86
+ @property
87
+ def connectors(self) -> AsyncConnectorsResource:
88
+ return self._connectors
89
+
90
+ @property
91
+ def api_keys(self) -> AsyncApiKeysResource:
92
+ return self._api_keys
93
+
94
+ @property
95
+ def policies(self) -> AsyncPoliciesResource:
96
+ return self._policies
97
+
98
+ @property
99
+ def security_reviews(self) -> AsyncSecurityReviewsResource:
100
+ return self._security_reviews
101
+
102
+ @property
103
+ def audit(self) -> AsyncAuditResource:
104
+ return self._audit
105
+
106
+ @property
107
+ def alerts(self) -> AsyncAlertsResource:
108
+ return self._alerts
109
+
110
+ @property
111
+ def identity(self) -> AsyncIdentityResource:
112
+ return self._identity
113
+
114
+ @property
115
+ def remediation(self) -> AsyncRemediationResource:
116
+ return self._remediation
117
+
118
+ @property
119
+ def ai_gateway(self) -> AsyncAiGatewayResource:
120
+ return self._ai_gateway
121
+
122
+ async def close(self) -> None:
123
+ """Close the underlying async HTTP client."""
124
+ await self._http.close()
125
+
126
+ async def __aenter__(self) -> AsyncBlackShield:
127
+ return self
128
+
129
+ async def __aexit__(self, *_args: object) -> None:
130
+ await self.close()
@@ -0,0 +1,420 @@
1
+ """Shared HTTP transport — retry logic, auth, and error mapping.
2
+
3
+ Both the sync and async clients delegate to these helpers so retry
4
+ semantics and error classification live in one place.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import random
11
+ import time
12
+ from typing import Any
13
+
14
+ import httpx
15
+
16
+ from blackshield._exceptions import (
17
+ APIError,
18
+ AuthenticationError,
19
+ BlackShieldError,
20
+ ConflictError,
21
+ NotFoundError,
22
+ PermissionDeniedError,
23
+ RateLimitError,
24
+ ValidationError,
25
+ )
26
+ from blackshield._types import Headers, JSONDict, QueryParams
27
+
28
+ logger = logging.getLogger("blackshield")
29
+
30
+ _RETRYABLE_STATUS_CODES = frozenset({429, 500, 502, 503, 504})
31
+ _DEFAULT_BASE_URL = "https://api.blackshield.security"
32
+ _DEFAULT_TIMEOUT = 60.0
33
+ _DEFAULT_MAX_RETRIES = 3
34
+ _RETRY_BASE_DELAY = 0.5
35
+ _RETRY_MAX_DELAY = 8.0
36
+ _JITTER_FACTOR = 0.1
37
+
38
+
39
+ def _build_auth_headers(api_key: str) -> Headers:
40
+ """Construct default headers for every request."""
41
+ return {
42
+ "X-API-Key": api_key,
43
+ "Content-Type": "application/json",
44
+ "User-Agent": _user_agent(),
45
+ }
46
+
47
+
48
+ def _user_agent() -> str:
49
+ from blackshield._version import __version__
50
+
51
+ return f"blackshield-python/{__version__}"
52
+
53
+
54
+ def _retry_delay(attempt: int) -> float:
55
+ """Exponential backoff with jitter, capped at ``_RETRY_MAX_DELAY``."""
56
+ base = _RETRY_BASE_DELAY * (2 ** max(0, attempt - 1))
57
+ capped = min(base, _RETRY_MAX_DELAY)
58
+ jitter = capped * _JITTER_FACTOR * random.random()
59
+ return capped + jitter
60
+
61
+
62
+ def _raise_for_status(response: httpx.Response) -> None:
63
+ """Map HTTP error status codes to typed SDK exceptions."""
64
+ if response.is_success:
65
+ return
66
+
67
+ status_code = response.status_code
68
+ try:
69
+ body: dict[str, Any] = response.json()
70
+ except Exception:
71
+ body = {}
72
+
73
+ detail = body.get("detail", response.text or "Unknown error")
74
+ message = f"HTTP {status_code}: {detail}"
75
+
76
+ exc_map: dict[int, type[BlackShieldError]] = {
77
+ 401: AuthenticationError,
78
+ 403: PermissionDeniedError,
79
+ 404: NotFoundError,
80
+ 409: ConflictError,
81
+ 422: ValidationError,
82
+ }
83
+
84
+ if status_code == 429:
85
+ retry_after_raw = response.headers.get("retry-after")
86
+ retry_after = float(retry_after_raw) if retry_after_raw else None
87
+ raise RateLimitError(
88
+ message,
89
+ status_code=status_code,
90
+ response_body=body,
91
+ retry_after=retry_after,
92
+ )
93
+
94
+ exc_cls = exc_map.get(status_code)
95
+ if exc_cls:
96
+ raise exc_cls(message, status_code=status_code, response_body=body)
97
+
98
+ if status_code >= 500:
99
+ raise APIError(message, status_code=status_code, response_body=body)
100
+
101
+ # Fallback for any other 4xx we didn't map.
102
+ raise BlackShieldError(message, status_code=status_code, response_body=body)
103
+
104
+
105
+ def _is_retryable(status_code: int) -> bool:
106
+ return status_code in _RETRYABLE_STATUS_CODES
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Sync transport
111
+ # ---------------------------------------------------------------------------
112
+
113
+
114
+ class SyncHTTPClient:
115
+ """Synchronous HTTP client with retry support."""
116
+
117
+ def __init__(
118
+ self,
119
+ *,
120
+ base_url: str = _DEFAULT_BASE_URL,
121
+ api_key: str,
122
+ timeout: float = _DEFAULT_TIMEOUT,
123
+ max_retries: int = _DEFAULT_MAX_RETRIES,
124
+ ) -> None:
125
+ self._base_url = base_url.rstrip("/")
126
+ self._max_retries = max(1, max_retries)
127
+ self._client = httpx.Client(
128
+ timeout=timeout,
129
+ headers=_build_auth_headers(api_key),
130
+ )
131
+
132
+ # -- public interface --------------------------------------------------
133
+
134
+ def request(
135
+ self,
136
+ method: str,
137
+ path: str,
138
+ *,
139
+ json: JSONDict | list[JSONDict] | None = None,
140
+ params: QueryParams | None = None,
141
+ ) -> httpx.Response:
142
+ """Execute an HTTP request with automatic retries."""
143
+ url = f"{self._base_url}{path}"
144
+ clean_params = _strip_none_params(params)
145
+ last_exc: Exception | None = None
146
+
147
+ for attempt in range(1, self._max_retries + 1):
148
+ try:
149
+ response = self._client.request(
150
+ method,
151
+ url,
152
+ json=json,
153
+ params=clean_params,
154
+ )
155
+ if response.is_success:
156
+ return response
157
+ if not _is_retryable(response.status_code):
158
+ _raise_for_status(response)
159
+ last_exc = None # will retry
160
+ if attempt < self._max_retries:
161
+ _sleep_for_retry(attempt, response)
162
+ continue
163
+ # Final attempt — raise
164
+ _raise_for_status(response)
165
+ except BlackShieldError:
166
+ raise
167
+ except (
168
+ httpx.TimeoutException,
169
+ httpx.ConnectError,
170
+ httpx.NetworkError,
171
+ httpx.TransportError,
172
+ ) as exc:
173
+ last_exc = exc
174
+ if attempt == self._max_retries:
175
+ raise APIError(
176
+ f"Request failed after {self._max_retries} attempts: {exc}",
177
+ status_code=None,
178
+ ) from exc
179
+ delay = _retry_delay(attempt)
180
+ logger.warning(
181
+ "Retrying %s %s (attempt %d/%d, sleep %.2fs): %s",
182
+ method,
183
+ path,
184
+ attempt,
185
+ self._max_retries,
186
+ delay,
187
+ exc,
188
+ )
189
+ time.sleep(delay)
190
+
191
+ # Unreachable in practice but satisfies the type checker.
192
+ assert last_exc is not None
193
+ raise APIError(f"Request failed: {last_exc}", status_code=None)
194
+
195
+ def get(
196
+ self,
197
+ path: str,
198
+ *,
199
+ params: QueryParams | None = None,
200
+ ) -> httpx.Response:
201
+ return self.request("GET", path, params=params)
202
+
203
+ def post(
204
+ self,
205
+ path: str,
206
+ *,
207
+ json: JSONDict | list[JSONDict] | None = None,
208
+ params: QueryParams | None = None,
209
+ ) -> httpx.Response:
210
+ return self.request("POST", path, json=json, params=params)
211
+
212
+ def put(
213
+ self,
214
+ path: str,
215
+ *,
216
+ json: JSONDict | None = None,
217
+ params: QueryParams | None = None,
218
+ ) -> httpx.Response:
219
+ return self.request("PUT", path, json=json, params=params)
220
+
221
+ def patch(
222
+ self,
223
+ path: str,
224
+ *,
225
+ json: JSONDict | None = None,
226
+ params: QueryParams | None = None,
227
+ ) -> httpx.Response:
228
+ return self.request("PATCH", path, json=json, params=params)
229
+
230
+ def delete(
231
+ self,
232
+ path: str,
233
+ *,
234
+ params: QueryParams | None = None,
235
+ ) -> httpx.Response:
236
+ return self.request("DELETE", path, params=params)
237
+
238
+ def close(self) -> None:
239
+ self._client.close()
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # Async transport
244
+ # ---------------------------------------------------------------------------
245
+
246
+
247
+ class AsyncHTTPClient:
248
+ """Asynchronous HTTP client with retry support."""
249
+
250
+ def __init__(
251
+ self,
252
+ *,
253
+ base_url: str = _DEFAULT_BASE_URL,
254
+ api_key: str,
255
+ timeout: float = _DEFAULT_TIMEOUT,
256
+ max_retries: int = _DEFAULT_MAX_RETRIES,
257
+ ) -> None:
258
+ self._base_url = base_url.rstrip("/")
259
+ self._max_retries = max(1, max_retries)
260
+ self._client = httpx.AsyncClient(
261
+ timeout=timeout,
262
+ headers=_build_auth_headers(api_key),
263
+ )
264
+
265
+ async def request(
266
+ self,
267
+ method: str,
268
+ path: str,
269
+ *,
270
+ json: JSONDict | list[JSONDict] | None = None,
271
+ params: QueryParams | None = None,
272
+ ) -> httpx.Response:
273
+ """Execute an async HTTP request with automatic retries."""
274
+ import asyncio
275
+
276
+ url = f"{self._base_url}{path}"
277
+ clean_params = _strip_none_params(params)
278
+ last_exc: Exception | None = None
279
+
280
+ for attempt in range(1, self._max_retries + 1):
281
+ try:
282
+ response = await self._client.request(
283
+ method,
284
+ url,
285
+ json=json,
286
+ params=clean_params,
287
+ )
288
+ if response.is_success:
289
+ return response
290
+ if not _is_retryable(response.status_code):
291
+ _raise_for_status(response)
292
+ last_exc = None
293
+ if attempt < self._max_retries:
294
+ await _async_sleep_for_retry(attempt, response)
295
+ continue
296
+ _raise_for_status(response)
297
+ except BlackShieldError:
298
+ raise
299
+ except (
300
+ httpx.TimeoutException,
301
+ httpx.ConnectError,
302
+ httpx.NetworkError,
303
+ httpx.TransportError,
304
+ ) as exc:
305
+ last_exc = exc
306
+ if attempt == self._max_retries:
307
+ raise APIError(
308
+ f"Request failed after {self._max_retries} attempts: {exc}",
309
+ status_code=None,
310
+ ) from exc
311
+ delay = _retry_delay(attempt)
312
+ logger.warning(
313
+ "Retrying %s %s (attempt %d/%d, sleep %.2fs): %s",
314
+ method,
315
+ path,
316
+ attempt,
317
+ self._max_retries,
318
+ delay,
319
+ exc,
320
+ )
321
+ await asyncio.sleep(delay)
322
+
323
+ assert last_exc is not None
324
+ raise APIError(f"Request failed: {last_exc}", status_code=None)
325
+
326
+ async def get(
327
+ self,
328
+ path: str,
329
+ *,
330
+ params: QueryParams | None = None,
331
+ ) -> httpx.Response:
332
+ return await self.request("GET", path, params=params)
333
+
334
+ async def post(
335
+ self,
336
+ path: str,
337
+ *,
338
+ json: JSONDict | list[JSONDict] | None = None,
339
+ params: QueryParams | None = None,
340
+ ) -> httpx.Response:
341
+ return await self.request("POST", path, json=json, params=params)
342
+
343
+ async def put(
344
+ self,
345
+ path: str,
346
+ *,
347
+ json: JSONDict | None = None,
348
+ params: QueryParams | None = None,
349
+ ) -> httpx.Response:
350
+ return await self.request("PUT", path, json=json, params=params)
351
+
352
+ async def patch(
353
+ self,
354
+ path: str,
355
+ *,
356
+ json: JSONDict | None = None,
357
+ params: QueryParams | None = None,
358
+ ) -> httpx.Response:
359
+ return await self.request("PATCH", path, json=json, params=params)
360
+
361
+ async def delete(
362
+ self,
363
+ path: str,
364
+ *,
365
+ params: QueryParams | None = None,
366
+ ) -> httpx.Response:
367
+ return await self.request("DELETE", path, params=params)
368
+
369
+ async def close(self) -> None:
370
+ await self._client.aclose()
371
+
372
+
373
+ # ---------------------------------------------------------------------------
374
+ # Helpers
375
+ # ---------------------------------------------------------------------------
376
+
377
+
378
+ def _strip_none_params(params: QueryParams | None) -> dict[str, str | int | float | bool]:
379
+ """Remove None-valued query params so httpx doesn't send ``key=None``."""
380
+ if not params:
381
+ return {}
382
+ return {k: v for k, v in params.items() if v is not None}
383
+
384
+
385
+ def _sleep_for_retry(attempt: int, response: httpx.Response) -> None:
386
+ retry_after = response.headers.get("retry-after")
387
+ if retry_after:
388
+ try:
389
+ delay = float(retry_after)
390
+ except ValueError:
391
+ delay = _retry_delay(attempt)
392
+ else:
393
+ delay = _retry_delay(attempt)
394
+ logger.warning(
395
+ "Retrying after %d (attempt %d, sleep %.2fs)",
396
+ response.status_code,
397
+ attempt,
398
+ delay,
399
+ )
400
+ time.sleep(delay)
401
+
402
+
403
+ async def _async_sleep_for_retry(attempt: int, response: httpx.Response) -> None:
404
+ import asyncio
405
+
406
+ retry_after = response.headers.get("retry-after")
407
+ if retry_after:
408
+ try:
409
+ delay = float(retry_after)
410
+ except ValueError:
411
+ delay = _retry_delay(attempt)
412
+ else:
413
+ delay = _retry_delay(attempt)
414
+ logger.warning(
415
+ "Retrying after %d (attempt %d, sleep %.2fs)",
416
+ response.status_code,
417
+ attempt,
418
+ delay,
419
+ )
420
+ await asyncio.sleep(delay)