breachspider 0.3.0__tar.gz
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.
- breachspider-0.3.0/LICENSE +21 -0
- breachspider-0.3.0/PKG-INFO +346 -0
- breachspider-0.3.0/README.md +318 -0
- breachspider-0.3.0/breachspider/__init__.py +97 -0
- breachspider-0.3.0/breachspider/_version.py +3 -0
- breachspider-0.3.0/breachspider/client.py +486 -0
- breachspider-0.3.0/breachspider/exceptions.py +164 -0
- breachspider-0.3.0/breachspider/models.py +615 -0
- breachspider-0.3.0/breachspider/py.typed +0 -0
- breachspider-0.3.0/breachspider/resources/__init__.py +15 -0
- breachspider-0.3.0/breachspider/resources/catalog.py +51 -0
- breachspider-0.3.0/breachspider/resources/correlate.py +107 -0
- breachspider-0.3.0/breachspider/resources/cves.py +64 -0
- breachspider-0.3.0/breachspider/resources/environments.py +68 -0
- breachspider-0.3.0/breachspider/resources/reports.py +33 -0
- breachspider-0.3.0/breachspider/resources/watchlist.py +43 -0
- breachspider-0.3.0/breachspider/resources/windows.py +195 -0
- breachspider-0.3.0/breachspider.egg-info/PKG-INFO +346 -0
- breachspider-0.3.0/breachspider.egg-info/SOURCES.txt +30 -0
- breachspider-0.3.0/breachspider.egg-info/dependency_links.txt +1 -0
- breachspider-0.3.0/breachspider.egg-info/requires.txt +6 -0
- breachspider-0.3.0/breachspider.egg-info/top_level.txt +1 -0
- breachspider-0.3.0/pyproject.toml +46 -0
- breachspider-0.3.0/setup.cfg +4 -0
- breachspider-0.3.0/tests/test_correlate.py +131 -0
- breachspider-0.3.0/tests/test_errors.py +115 -0
- breachspider-0.3.0/tests/test_integration.py +81 -0
- breachspider-0.3.0/tests/test_openapi.py +23 -0
- breachspider-0.3.0/tests/test_pagination.py +71 -0
- breachspider-0.3.0/tests/test_quota.py +53 -0
- breachspider-0.3.0/tests/test_redaction.py +64 -0
- breachspider-0.3.0/tests/test_windows.py +119 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CITED Relevance LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: breachspider
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Official Python SDK for the BreachSpider ICS/OT CVE intelligence API
|
|
5
|
+
Author-email: CITED Relevance LLC <joshua@citedrelevance.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://breachspider.com
|
|
8
|
+
Project-URL: Documentation, https://breachspider.com/docs
|
|
9
|
+
Project-URL: Source, https://github.com/citedrelevance/breachspider-python
|
|
10
|
+
Keywords: breachspider,cve,ics,ot,scada,vulnerability,security
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: requests>=2.25
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: responses>=0.23; extra == "dev"
|
|
26
|
+
Requires-Dist: openapi-spec-validator>=0.7; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# BreachSpider Python SDK
|
|
30
|
+
|
|
31
|
+
Official Python client for the [BreachSpider](https://breachspider.com) ICS/OT
|
|
32
|
+
CVE intelligence API. Go from zero to working queries in about ten seconds from
|
|
33
|
+
clone to first query (most of it `pip install`) instead of writing your own
|
|
34
|
+
HTTP client.
|
|
35
|
+
|
|
36
|
+
- Typed objects for CVEs, vendors, products, environments, and assets, not raw dicts.
|
|
37
|
+
- Transparent pagination that absorbs the API's per-endpoint shapes.
|
|
38
|
+
- Automatic 429 backoff so a naive loop over thousands of CVEs won't trip the edge limit.
|
|
39
|
+
- Typed exceptions that surface the API's own helpful error messages.
|
|
40
|
+
- The API key is never printed, logged, or included in an exception.
|
|
41
|
+
- **New in 0.3.0:** cited source references on every CVE: vendor advisories, CISA ICS advisories and the
|
|
42
|
+
CVE.org record. See the [changelog](CHANGELOG.md).
|
|
43
|
+
- **New in 0.2.0:** asset correlation (API v1) and Windows patch level (API v2), with the
|
|
44
|
+
exposure-priority ranking, fix groups and fix plans. See the [changelog](CHANGELOG.md): the API's default
|
|
45
|
+
CVE order changed to `priority`.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install breachspider
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires Python 3.9+. The only dependency is `requests`.
|
|
54
|
+
|
|
55
|
+
## 60-second quickstart (no signup)
|
|
56
|
+
|
|
57
|
+
`Client.demo()` mints a read-only demo token (valid 24h, scoped to a public
|
|
58
|
+
demo org). No API key or signup required.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import breachspider
|
|
62
|
+
|
|
63
|
+
bs = breachspider.Client.demo()
|
|
64
|
+
|
|
65
|
+
# Find a vendor. Read the slug from the response, never hand-craft it.
|
|
66
|
+
vendor = bs.catalog.vendor("schneider electric")
|
|
67
|
+
print(vendor.id, vendor.name, vendor.slug) # 11529 Schneider-Electric schneider-electric
|
|
68
|
+
|
|
69
|
+
# List that vendor's products, each with a CVE count.
|
|
70
|
+
for product in bs.catalog.products(vendor_id=vendor.id)[:3]:
|
|
71
|
+
print(f" {product.id:>6} {product.name} ({product.cve_count} CVEs)")
|
|
72
|
+
|
|
73
|
+
# Critical, known-exploited CVEs for that vendor.
|
|
74
|
+
for cve in bs.cves.by_vendor(vendor.slug, severity="CRITICAL", kev=True):
|
|
75
|
+
print(cve.cve_id, "-", cve.title)
|
|
76
|
+
break # (the loop would page through all of them)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Authentication
|
|
80
|
+
|
|
81
|
+
Use a real API key (`bs_live_…`) for production. Keys require the Professional
|
|
82
|
+
tier or above and are created in the dashboard under **Integrations → API Keys**.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import os
|
|
86
|
+
import breachspider
|
|
87
|
+
|
|
88
|
+
# export BREACHSPIDER_API_KEY=bs_live_your_key_here
|
|
89
|
+
bs = breachspider.Client(os.environ.get("BREACHSPIDER_API_KEY", "bs_live_YOUR_KEY_HERE"))
|
|
90
|
+
print(bs) # key is redacted: <breachspider.Client base_url='https://breachspider.com' auth=live key=***redacted***>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The key is held privately: it never appears in `repr()`, `str()`, logs, or any
|
|
94
|
+
exception message.
|
|
95
|
+
|
|
96
|
+
## The primary workflow: vendor → products → CVEs
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import breachspider
|
|
100
|
+
|
|
101
|
+
bs = breachspider.Client.demo()
|
|
102
|
+
|
|
103
|
+
vendor = bs.catalog.vendor("schneider electric")
|
|
104
|
+
assert (vendor.id, vendor.slug) == (11529, "schneider-electric")
|
|
105
|
+
|
|
106
|
+
products = bs.catalog.products(vendor_id=vendor.id)
|
|
107
|
+
m340 = next(p for p in products if p.id == 25001)
|
|
108
|
+
print(m340.name, m340.cve_count) # Modicon M340 41
|
|
109
|
+
|
|
110
|
+
# by_vendor takes the slug and supports the full /cves filter set.
|
|
111
|
+
criticals = bs.cves.by_vendor(vendor.slug, severity="CRITICAL", kev=True)
|
|
112
|
+
first = next(iter(criticals))
|
|
113
|
+
print(first.cve_id, first.severity, first.cvss_score, first.kev_flagged)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Searching CVEs
|
|
117
|
+
|
|
118
|
+
`bs.cves.search(**filters)` returns an iterator that transparently follows
|
|
119
|
+
pages. Accepted filters:
|
|
120
|
+
|
|
121
|
+
`q`, `vendor` (slug), `protocol`, `severity`, `kev`, `unpatched`,
|
|
122
|
+
`has_exploit`, `patch_status`, `cvss_min`, `cvss_max`, `bcs_min`, `bcs_max`,
|
|
123
|
+
`date_from`, `date_to`, `sort_by`, `ranked`.
|
|
124
|
+
|
|
125
|
+
> There is **no** `product`, `version`, or `cpe` filter on `/cves` today. To
|
|
126
|
+
> narrow to a product, filter by vendor and match on the result fields.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
import itertools
|
|
130
|
+
import breachspider
|
|
131
|
+
|
|
132
|
+
bs = breachspider.Client.demo()
|
|
133
|
+
|
|
134
|
+
# First 5 KEV CVEs affecting Siemens, highest CVSS first.
|
|
135
|
+
for cve in itertools.islice(bs.cves.search(vendor="siemens", kev=True, sort_by="cvss"), 5):
|
|
136
|
+
print(cve.cve_id, cve.cvss_score, cve.severity)
|
|
137
|
+
|
|
138
|
+
# One CVE by id (rich detail; nested scoring/exploitation/patch in .raw).
|
|
139
|
+
cve = bs.cves.get("CVE-2025-32433")
|
|
140
|
+
print(cve.cve_id, cve.severity, cve.cvss_score, cve.html_url)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Pagination
|
|
144
|
+
|
|
145
|
+
Every `search`/`by_vendor`/`assets` call is a lazy iterator: it fetches the
|
|
146
|
+
next page only as you consume it, and stops at the end. Page size defaults to
|
|
147
|
+
100 (the maximum). Different endpoints paginate differently (`per_page` vs
|
|
148
|
+
`limit`, `_links.next` vs page numbers vs offset); the SDK handles all of them.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
import breachspider
|
|
152
|
+
|
|
153
|
+
bs = breachspider.Client.demo()
|
|
154
|
+
|
|
155
|
+
count = 0
|
|
156
|
+
for _cve in bs.cves.search(vendor="schneider-electric"):
|
|
157
|
+
count += 1
|
|
158
|
+
if count >= 250: # 250 spans 3 pages of 100; the SDK paged for you
|
|
159
|
+
break
|
|
160
|
+
print("iterated", count, "CVEs across multiple pages")
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Do not call list() on a search iterator
|
|
164
|
+
|
|
165
|
+
`list(bs.cves.search(...))` materializes **every page** of the result set.
|
|
166
|
+
Siemens alone has over 5,000 CVEs; walking all of them will exhaust every page
|
|
167
|
+
and can trigger the rate limit. Use `itertools.islice` when you only need the
|
|
168
|
+
first N, or a `for` loop with a `break`.
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
import itertools
|
|
172
|
+
import breachspider
|
|
173
|
+
|
|
174
|
+
bs = breachspider.Client.demo()
|
|
175
|
+
|
|
176
|
+
# Safe: fetch exactly 10 results and stop. Never touches page 2 or beyond.
|
|
177
|
+
first_ten = list(itertools.islice(
|
|
178
|
+
bs.cves.search(vendor="siemens", kev=True, sort_by="cvss"), 10))
|
|
179
|
+
for cve in first_ten:
|
|
180
|
+
print(cve.cve_id, cve.cvss_score)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Rate-limit safety
|
|
184
|
+
|
|
185
|
+
The API sits behind a Cloudflare edge limit that trips at roughly 37 rapid
|
|
186
|
+
requests from one IP. The client sleeps `page_delay` seconds (default `0.25`)
|
|
187
|
+
between pages and retries any `429` with exponential backoff. Tune it:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
import breachspider
|
|
191
|
+
|
|
192
|
+
# Faster (riskier) or gentler pagination, and more patient retries.
|
|
193
|
+
bs = breachspider.Client.demo(page_delay=0.5, max_retries=8, backoff_factor=1.0)
|
|
194
|
+
print("page_delay:", bs.page_delay, "max_retries:", bs.max_retries)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Quota / usage headers
|
|
198
|
+
|
|
199
|
+
With a demo token (`Client.demo()`), `bs.quota` is always `None`; demo
|
|
200
|
+
traffic is not metered.
|
|
201
|
+
|
|
202
|
+
With a `bs_live_` key, every response carries `X-RateLimit-*` headers that the
|
|
203
|
+
client parses into `bs.quota` after any call. Metering is **observe-only** today:
|
|
204
|
+
usage is reported but nothing is rejected. Unlimited tiers report the string
|
|
205
|
+
`"unlimited"`.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
import os
|
|
209
|
+
import breachspider
|
|
210
|
+
from breachspider import AuthenticationError
|
|
211
|
+
|
|
212
|
+
# export BREACHSPIDER_API_KEY=bs_live_your_key_here
|
|
213
|
+
bs = breachspider.Client(os.environ.get("BREACHSPIDER_API_KEY", "bs_live_YOUR_KEY_HERE"))
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
next(iter(bs.cves.search(vendor="siemens", per_page=1)), None)
|
|
217
|
+
q = bs.quota
|
|
218
|
+
print("limit:", q.limit, "used:", q.used, "remaining:", q.remaining)
|
|
219
|
+
# On Professional: limit 25000 ... ; on Enterprise/api: limit 'unlimited'
|
|
220
|
+
except AuthenticationError:
|
|
221
|
+
print("Set BREACHSPIDER_API_KEY to a live bs_live_… key to see quota.")
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Error handling
|
|
225
|
+
|
|
226
|
+
Errors map to typed exceptions that keep the server's own message and structured
|
|
227
|
+
detail, the most useful part.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
import breachspider
|
|
231
|
+
from breachspider import UnknownParameterError, ValidationError, NotFoundError
|
|
232
|
+
|
|
233
|
+
bs = breachspider.Client.demo()
|
|
234
|
+
|
|
235
|
+
# 400 UNKNOWN_PARAMETER surfaces the accepted-parameter list.
|
|
236
|
+
try:
|
|
237
|
+
list(bs.cves.search(color="red"))
|
|
238
|
+
except UnknownParameterError as e:
|
|
239
|
+
print("accepted:", e.accepted_parameters)
|
|
240
|
+
|
|
241
|
+
# 422 for per_page > 100 (the real per-page maximum is 100 on every tier).
|
|
242
|
+
try:
|
|
243
|
+
bs.request("GET", "/cves", params={"per_page": 101})
|
|
244
|
+
except ValidationError as e:
|
|
245
|
+
print("validation:", e.fields)
|
|
246
|
+
|
|
247
|
+
# 404 for a CVE that doesn't exist.
|
|
248
|
+
try:
|
|
249
|
+
bs.cves.get("CVE-0000-00000")
|
|
250
|
+
except NotFoundError as e:
|
|
251
|
+
print("not found:", e.message)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
| Exception | HTTP | Notable attributes |
|
|
255
|
+
|---|---|---|
|
|
256
|
+
| `UnknownParameterError` | 400 | `accepted_parameters`, `unknown_parameters` |
|
|
257
|
+
| `AuthenticationError` | 401 | |
|
|
258
|
+
| `InsufficientScopeError` | 403 | `required_scope`, `current_scopes` |
|
|
259
|
+
| `CapExceededError` | 403 | `resource`, `limit`, `current`, `tier` |
|
|
260
|
+
| `NotFoundError` | 404 | `detail` |
|
|
261
|
+
| `ValidationError` | 422 | `fields` |
|
|
262
|
+
| `RateLimitError` | 429 | `retry_after` (auto-retried first) |
|
|
263
|
+
|
|
264
|
+
All inherit from `breachspider.BreachSpiderError`. Network failures raise
|
|
265
|
+
`APIConnectionError` (never leaking the request or key).
|
|
266
|
+
|
|
267
|
+
## Correlate assets (API v1)
|
|
268
|
+
|
|
269
|
+
Send your inventory (vendor, product, version); get each asset's CVEs ranked by what's exposed and what to fix
|
|
270
|
+
first, the fix actions, and a fix plan. Nothing is stored server-side.
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
device = {"asset_id": "EXAMPLE-SWITCH-01", "vendor": "Moxa", "product": "EDS-518A", "version": "V3.5"}
|
|
274
|
+
resp = bs.correlate.correlate([device]) # sort="priority" by default; also "score", "exploit", "newest"
|
|
275
|
+
result = resp.results[0]
|
|
276
|
+
for cve in result.cves:
|
|
277
|
+
print(cve.priority_rank, cve.cve_id, cve.priority_reason)
|
|
278
|
+
print(result.fix_groups[0].fix, result.fix_plan)
|
|
279
|
+
every_page = bs.correlate.all_cves(device) # follows cves_page.has_more for you
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Filters: `confirmed_only`, `known_exploited_only`, `fix_available_only`. `result_hash` never depends on sort, filters
|
|
283
|
+
or page; use `bs.correlate.check(...)` to re-correlate only what changed. Guide: [docs/correlate.md](docs/correlate.md).
|
|
284
|
+
|
|
285
|
+
## Windows patch level (API v2)
|
|
286
|
+
|
|
287
|
+
Send Windows build and KB facts; get confirmed open / cleared / needs review per CVE from Microsoft's own data, with
|
|
288
|
+
fix actions such as "install KB5122876 (latest cumulative, build 10.0.17763.9245)". Submitting needs a
|
|
289
|
+
**write-scoped** key.
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
hosts = [{"asset_id": "WIN-EXAMPLE-01", "os_product": "Windows Server 2019 Standard", "edition_id": "ServerStandard",
|
|
293
|
+
"os_build": "10.0.17763.7792", "architecture": "x64", "installation_type": "Server",
|
|
294
|
+
"collected_at": "2026-09-25T14:02:00Z"}]
|
|
295
|
+
resp = bs.windows.correlate(12, hosts, include_cleared=False)
|
|
296
|
+
for r in resp.rejected: # refused hosts (e.g. an identifying field); nothing stored
|
|
297
|
+
print(r.asset_id, r.error_codes)
|
|
298
|
+
for batch in bs.windows.correlate_batched(12, many_hosts): # calls of 100 hosts or fewer
|
|
299
|
+
...
|
|
300
|
+
bs.windows.correlate_csv(12, "hosts.csv") # the same from a CSV file
|
|
301
|
+
bs.windows.results(12, asset_id="WIN-EXAMPLE-01", cve_page_size=50)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Guide: [docs/windows-v2.md](docs/windows-v2.md). Examples for every endpoint: [examples/](examples/). OpenAPI
|
|
305
|
+
document for these endpoints: [openapi/breachspider-openapi.json](openapi/breachspider-openapi.json).
|
|
306
|
+
|
|
307
|
+
## Writes
|
|
308
|
+
|
|
309
|
+
Reads work with any key. Writes (`environments.create`, `environments.add_asset`,
|
|
310
|
+
`watchlist.add`, `environments.create_ticket`, `reports.generate`) require a
|
|
311
|
+
**write-scoped** `bs_live_` key and are rejected for demo tokens. Session-only
|
|
312
|
+
operations (API-key management, webhook delivery, billing, and account
|
|
313
|
+
settings) are intentionally **not** part of this SDK; they can never be called
|
|
314
|
+
with a key.
|
|
315
|
+
|
|
316
|
+
## Honest limits
|
|
317
|
+
|
|
318
|
+
- **Per page:** 100 results maximum on every paid tier (10 on Free). There is no
|
|
319
|
+
pagination depth cap; you can page through the entire result set.
|
|
320
|
+
- **No product/version/CPE filter** on `/cves` yet. Filter by `vendor` (slug).
|
|
321
|
+
- **Metering is observe-only.** Usage is counted and returned in headers, but no
|
|
322
|
+
request is rejected for exceeding a monthly allowance today.
|
|
323
|
+
|
|
324
|
+
## Running the tests
|
|
325
|
+
|
|
326
|
+
Install the dev extras, then run the suite:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
pip install ".[dev]"
|
|
330
|
+
pytest
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The suite includes live integration tests that hit the real API using a demo
|
|
334
|
+
token. To skip them when offline (or in CI without network access):
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
BREACHSPIDER_SKIP_INTEGRATION=1 pytest
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The integration tests are marked `@pytest.mark.integration` and are skipped
|
|
341
|
+
automatically if the API is unreachable or the env var is set. The correlate and Windows tests run against
|
|
342
|
+
responses recorded from the live API (`tests/fixtures/`), so they need no key and no network.
|
|
343
|
+
|
|
344
|
+
## License
|
|
345
|
+
|
|
346
|
+
MIT © CITED Relevance LLC
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# BreachSpider Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the [BreachSpider](https://breachspider.com) ICS/OT
|
|
4
|
+
CVE intelligence API. Go from zero to working queries in about ten seconds from
|
|
5
|
+
clone to first query (most of it `pip install`) instead of writing your own
|
|
6
|
+
HTTP client.
|
|
7
|
+
|
|
8
|
+
- Typed objects for CVEs, vendors, products, environments, and assets, not raw dicts.
|
|
9
|
+
- Transparent pagination that absorbs the API's per-endpoint shapes.
|
|
10
|
+
- Automatic 429 backoff so a naive loop over thousands of CVEs won't trip the edge limit.
|
|
11
|
+
- Typed exceptions that surface the API's own helpful error messages.
|
|
12
|
+
- The API key is never printed, logged, or included in an exception.
|
|
13
|
+
- **New in 0.3.0:** cited source references on every CVE: vendor advisories, CISA ICS advisories and the
|
|
14
|
+
CVE.org record. See the [changelog](CHANGELOG.md).
|
|
15
|
+
- **New in 0.2.0:** asset correlation (API v1) and Windows patch level (API v2), with the
|
|
16
|
+
exposure-priority ranking, fix groups and fix plans. See the [changelog](CHANGELOG.md): the API's default
|
|
17
|
+
CVE order changed to `priority`.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install breachspider
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Requires Python 3.9+. The only dependency is `requests`.
|
|
26
|
+
|
|
27
|
+
## 60-second quickstart (no signup)
|
|
28
|
+
|
|
29
|
+
`Client.demo()` mints a read-only demo token (valid 24h, scoped to a public
|
|
30
|
+
demo org). No API key or signup required.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import breachspider
|
|
34
|
+
|
|
35
|
+
bs = breachspider.Client.demo()
|
|
36
|
+
|
|
37
|
+
# Find a vendor. Read the slug from the response, never hand-craft it.
|
|
38
|
+
vendor = bs.catalog.vendor("schneider electric")
|
|
39
|
+
print(vendor.id, vendor.name, vendor.slug) # 11529 Schneider-Electric schneider-electric
|
|
40
|
+
|
|
41
|
+
# List that vendor's products, each with a CVE count.
|
|
42
|
+
for product in bs.catalog.products(vendor_id=vendor.id)[:3]:
|
|
43
|
+
print(f" {product.id:>6} {product.name} ({product.cve_count} CVEs)")
|
|
44
|
+
|
|
45
|
+
# Critical, known-exploited CVEs for that vendor.
|
|
46
|
+
for cve in bs.cves.by_vendor(vendor.slug, severity="CRITICAL", kev=True):
|
|
47
|
+
print(cve.cve_id, "-", cve.title)
|
|
48
|
+
break # (the loop would page through all of them)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Authentication
|
|
52
|
+
|
|
53
|
+
Use a real API key (`bs_live_…`) for production. Keys require the Professional
|
|
54
|
+
tier or above and are created in the dashboard under **Integrations → API Keys**.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import os
|
|
58
|
+
import breachspider
|
|
59
|
+
|
|
60
|
+
# export BREACHSPIDER_API_KEY=bs_live_your_key_here
|
|
61
|
+
bs = breachspider.Client(os.environ.get("BREACHSPIDER_API_KEY", "bs_live_YOUR_KEY_HERE"))
|
|
62
|
+
print(bs) # key is redacted: <breachspider.Client base_url='https://breachspider.com' auth=live key=***redacted***>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The key is held privately: it never appears in `repr()`, `str()`, logs, or any
|
|
66
|
+
exception message.
|
|
67
|
+
|
|
68
|
+
## The primary workflow: vendor → products → CVEs
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import breachspider
|
|
72
|
+
|
|
73
|
+
bs = breachspider.Client.demo()
|
|
74
|
+
|
|
75
|
+
vendor = bs.catalog.vendor("schneider electric")
|
|
76
|
+
assert (vendor.id, vendor.slug) == (11529, "schneider-electric")
|
|
77
|
+
|
|
78
|
+
products = bs.catalog.products(vendor_id=vendor.id)
|
|
79
|
+
m340 = next(p for p in products if p.id == 25001)
|
|
80
|
+
print(m340.name, m340.cve_count) # Modicon M340 41
|
|
81
|
+
|
|
82
|
+
# by_vendor takes the slug and supports the full /cves filter set.
|
|
83
|
+
criticals = bs.cves.by_vendor(vendor.slug, severity="CRITICAL", kev=True)
|
|
84
|
+
first = next(iter(criticals))
|
|
85
|
+
print(first.cve_id, first.severity, first.cvss_score, first.kev_flagged)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Searching CVEs
|
|
89
|
+
|
|
90
|
+
`bs.cves.search(**filters)` returns an iterator that transparently follows
|
|
91
|
+
pages. Accepted filters:
|
|
92
|
+
|
|
93
|
+
`q`, `vendor` (slug), `protocol`, `severity`, `kev`, `unpatched`,
|
|
94
|
+
`has_exploit`, `patch_status`, `cvss_min`, `cvss_max`, `bcs_min`, `bcs_max`,
|
|
95
|
+
`date_from`, `date_to`, `sort_by`, `ranked`.
|
|
96
|
+
|
|
97
|
+
> There is **no** `product`, `version`, or `cpe` filter on `/cves` today. To
|
|
98
|
+
> narrow to a product, filter by vendor and match on the result fields.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import itertools
|
|
102
|
+
import breachspider
|
|
103
|
+
|
|
104
|
+
bs = breachspider.Client.demo()
|
|
105
|
+
|
|
106
|
+
# First 5 KEV CVEs affecting Siemens, highest CVSS first.
|
|
107
|
+
for cve in itertools.islice(bs.cves.search(vendor="siemens", kev=True, sort_by="cvss"), 5):
|
|
108
|
+
print(cve.cve_id, cve.cvss_score, cve.severity)
|
|
109
|
+
|
|
110
|
+
# One CVE by id (rich detail; nested scoring/exploitation/patch in .raw).
|
|
111
|
+
cve = bs.cves.get("CVE-2025-32433")
|
|
112
|
+
print(cve.cve_id, cve.severity, cve.cvss_score, cve.html_url)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Pagination
|
|
116
|
+
|
|
117
|
+
Every `search`/`by_vendor`/`assets` call is a lazy iterator: it fetches the
|
|
118
|
+
next page only as you consume it, and stops at the end. Page size defaults to
|
|
119
|
+
100 (the maximum). Different endpoints paginate differently (`per_page` vs
|
|
120
|
+
`limit`, `_links.next` vs page numbers vs offset); the SDK handles all of them.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
import breachspider
|
|
124
|
+
|
|
125
|
+
bs = breachspider.Client.demo()
|
|
126
|
+
|
|
127
|
+
count = 0
|
|
128
|
+
for _cve in bs.cves.search(vendor="schneider-electric"):
|
|
129
|
+
count += 1
|
|
130
|
+
if count >= 250: # 250 spans 3 pages of 100; the SDK paged for you
|
|
131
|
+
break
|
|
132
|
+
print("iterated", count, "CVEs across multiple pages")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Do not call list() on a search iterator
|
|
136
|
+
|
|
137
|
+
`list(bs.cves.search(...))` materializes **every page** of the result set.
|
|
138
|
+
Siemens alone has over 5,000 CVEs; walking all of them will exhaust every page
|
|
139
|
+
and can trigger the rate limit. Use `itertools.islice` when you only need the
|
|
140
|
+
first N, or a `for` loop with a `break`.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import itertools
|
|
144
|
+
import breachspider
|
|
145
|
+
|
|
146
|
+
bs = breachspider.Client.demo()
|
|
147
|
+
|
|
148
|
+
# Safe: fetch exactly 10 results and stop. Never touches page 2 or beyond.
|
|
149
|
+
first_ten = list(itertools.islice(
|
|
150
|
+
bs.cves.search(vendor="siemens", kev=True, sort_by="cvss"), 10))
|
|
151
|
+
for cve in first_ten:
|
|
152
|
+
print(cve.cve_id, cve.cvss_score)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Rate-limit safety
|
|
156
|
+
|
|
157
|
+
The API sits behind a Cloudflare edge limit that trips at roughly 37 rapid
|
|
158
|
+
requests from one IP. The client sleeps `page_delay` seconds (default `0.25`)
|
|
159
|
+
between pages and retries any `429` with exponential backoff. Tune it:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
import breachspider
|
|
163
|
+
|
|
164
|
+
# Faster (riskier) or gentler pagination, and more patient retries.
|
|
165
|
+
bs = breachspider.Client.demo(page_delay=0.5, max_retries=8, backoff_factor=1.0)
|
|
166
|
+
print("page_delay:", bs.page_delay, "max_retries:", bs.max_retries)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Quota / usage headers
|
|
170
|
+
|
|
171
|
+
With a demo token (`Client.demo()`), `bs.quota` is always `None`; demo
|
|
172
|
+
traffic is not metered.
|
|
173
|
+
|
|
174
|
+
With a `bs_live_` key, every response carries `X-RateLimit-*` headers that the
|
|
175
|
+
client parses into `bs.quota` after any call. Metering is **observe-only** today:
|
|
176
|
+
usage is reported but nothing is rejected. Unlimited tiers report the string
|
|
177
|
+
`"unlimited"`.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
import os
|
|
181
|
+
import breachspider
|
|
182
|
+
from breachspider import AuthenticationError
|
|
183
|
+
|
|
184
|
+
# export BREACHSPIDER_API_KEY=bs_live_your_key_here
|
|
185
|
+
bs = breachspider.Client(os.environ.get("BREACHSPIDER_API_KEY", "bs_live_YOUR_KEY_HERE"))
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
next(iter(bs.cves.search(vendor="siemens", per_page=1)), None)
|
|
189
|
+
q = bs.quota
|
|
190
|
+
print("limit:", q.limit, "used:", q.used, "remaining:", q.remaining)
|
|
191
|
+
# On Professional: limit 25000 ... ; on Enterprise/api: limit 'unlimited'
|
|
192
|
+
except AuthenticationError:
|
|
193
|
+
print("Set BREACHSPIDER_API_KEY to a live bs_live_… key to see quota.")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Error handling
|
|
197
|
+
|
|
198
|
+
Errors map to typed exceptions that keep the server's own message and structured
|
|
199
|
+
detail, the most useful part.
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
import breachspider
|
|
203
|
+
from breachspider import UnknownParameterError, ValidationError, NotFoundError
|
|
204
|
+
|
|
205
|
+
bs = breachspider.Client.demo()
|
|
206
|
+
|
|
207
|
+
# 400 UNKNOWN_PARAMETER surfaces the accepted-parameter list.
|
|
208
|
+
try:
|
|
209
|
+
list(bs.cves.search(color="red"))
|
|
210
|
+
except UnknownParameterError as e:
|
|
211
|
+
print("accepted:", e.accepted_parameters)
|
|
212
|
+
|
|
213
|
+
# 422 for per_page > 100 (the real per-page maximum is 100 on every tier).
|
|
214
|
+
try:
|
|
215
|
+
bs.request("GET", "/cves", params={"per_page": 101})
|
|
216
|
+
except ValidationError as e:
|
|
217
|
+
print("validation:", e.fields)
|
|
218
|
+
|
|
219
|
+
# 404 for a CVE that doesn't exist.
|
|
220
|
+
try:
|
|
221
|
+
bs.cves.get("CVE-0000-00000")
|
|
222
|
+
except NotFoundError as e:
|
|
223
|
+
print("not found:", e.message)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| Exception | HTTP | Notable attributes |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `UnknownParameterError` | 400 | `accepted_parameters`, `unknown_parameters` |
|
|
229
|
+
| `AuthenticationError` | 401 | |
|
|
230
|
+
| `InsufficientScopeError` | 403 | `required_scope`, `current_scopes` |
|
|
231
|
+
| `CapExceededError` | 403 | `resource`, `limit`, `current`, `tier` |
|
|
232
|
+
| `NotFoundError` | 404 | `detail` |
|
|
233
|
+
| `ValidationError` | 422 | `fields` |
|
|
234
|
+
| `RateLimitError` | 429 | `retry_after` (auto-retried first) |
|
|
235
|
+
|
|
236
|
+
All inherit from `breachspider.BreachSpiderError`. Network failures raise
|
|
237
|
+
`APIConnectionError` (never leaking the request or key).
|
|
238
|
+
|
|
239
|
+
## Correlate assets (API v1)
|
|
240
|
+
|
|
241
|
+
Send your inventory (vendor, product, version); get each asset's CVEs ranked by what's exposed and what to fix
|
|
242
|
+
first, the fix actions, and a fix plan. Nothing is stored server-side.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
device = {"asset_id": "EXAMPLE-SWITCH-01", "vendor": "Moxa", "product": "EDS-518A", "version": "V3.5"}
|
|
246
|
+
resp = bs.correlate.correlate([device]) # sort="priority" by default; also "score", "exploit", "newest"
|
|
247
|
+
result = resp.results[0]
|
|
248
|
+
for cve in result.cves:
|
|
249
|
+
print(cve.priority_rank, cve.cve_id, cve.priority_reason)
|
|
250
|
+
print(result.fix_groups[0].fix, result.fix_plan)
|
|
251
|
+
every_page = bs.correlate.all_cves(device) # follows cves_page.has_more for you
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Filters: `confirmed_only`, `known_exploited_only`, `fix_available_only`. `result_hash` never depends on sort, filters
|
|
255
|
+
or page; use `bs.correlate.check(...)` to re-correlate only what changed. Guide: [docs/correlate.md](docs/correlate.md).
|
|
256
|
+
|
|
257
|
+
## Windows patch level (API v2)
|
|
258
|
+
|
|
259
|
+
Send Windows build and KB facts; get confirmed open / cleared / needs review per CVE from Microsoft's own data, with
|
|
260
|
+
fix actions such as "install KB5122876 (latest cumulative, build 10.0.17763.9245)". Submitting needs a
|
|
261
|
+
**write-scoped** key.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
hosts = [{"asset_id": "WIN-EXAMPLE-01", "os_product": "Windows Server 2019 Standard", "edition_id": "ServerStandard",
|
|
265
|
+
"os_build": "10.0.17763.7792", "architecture": "x64", "installation_type": "Server",
|
|
266
|
+
"collected_at": "2026-09-25T14:02:00Z"}]
|
|
267
|
+
resp = bs.windows.correlate(12, hosts, include_cleared=False)
|
|
268
|
+
for r in resp.rejected: # refused hosts (e.g. an identifying field); nothing stored
|
|
269
|
+
print(r.asset_id, r.error_codes)
|
|
270
|
+
for batch in bs.windows.correlate_batched(12, many_hosts): # calls of 100 hosts or fewer
|
|
271
|
+
...
|
|
272
|
+
bs.windows.correlate_csv(12, "hosts.csv") # the same from a CSV file
|
|
273
|
+
bs.windows.results(12, asset_id="WIN-EXAMPLE-01", cve_page_size=50)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Guide: [docs/windows-v2.md](docs/windows-v2.md). Examples for every endpoint: [examples/](examples/). OpenAPI
|
|
277
|
+
document for these endpoints: [openapi/breachspider-openapi.json](openapi/breachspider-openapi.json).
|
|
278
|
+
|
|
279
|
+
## Writes
|
|
280
|
+
|
|
281
|
+
Reads work with any key. Writes (`environments.create`, `environments.add_asset`,
|
|
282
|
+
`watchlist.add`, `environments.create_ticket`, `reports.generate`) require a
|
|
283
|
+
**write-scoped** `bs_live_` key and are rejected for demo tokens. Session-only
|
|
284
|
+
operations (API-key management, webhook delivery, billing, and account
|
|
285
|
+
settings) are intentionally **not** part of this SDK; they can never be called
|
|
286
|
+
with a key.
|
|
287
|
+
|
|
288
|
+
## Honest limits
|
|
289
|
+
|
|
290
|
+
- **Per page:** 100 results maximum on every paid tier (10 on Free). There is no
|
|
291
|
+
pagination depth cap; you can page through the entire result set.
|
|
292
|
+
- **No product/version/CPE filter** on `/cves` yet. Filter by `vendor` (slug).
|
|
293
|
+
- **Metering is observe-only.** Usage is counted and returned in headers, but no
|
|
294
|
+
request is rejected for exceeding a monthly allowance today.
|
|
295
|
+
|
|
296
|
+
## Running the tests
|
|
297
|
+
|
|
298
|
+
Install the dev extras, then run the suite:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
pip install ".[dev]"
|
|
302
|
+
pytest
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The suite includes live integration tests that hit the real API using a demo
|
|
306
|
+
token. To skip them when offline (or in CI without network access):
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
BREACHSPIDER_SKIP_INTEGRATION=1 pytest
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The integration tests are marked `@pytest.mark.integration` and are skipped
|
|
313
|
+
automatically if the API is unreachable or the env var is set. The correlate and Windows tests run against
|
|
314
|
+
responses recorded from the live API (`tests/fixtures/`), so they need no key and no network.
|
|
315
|
+
|
|
316
|
+
## License
|
|
317
|
+
|
|
318
|
+
MIT © CITED Relevance LLC
|