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.
Files changed (32) hide show
  1. breachspider-0.3.0/LICENSE +21 -0
  2. breachspider-0.3.0/PKG-INFO +346 -0
  3. breachspider-0.3.0/README.md +318 -0
  4. breachspider-0.3.0/breachspider/__init__.py +97 -0
  5. breachspider-0.3.0/breachspider/_version.py +3 -0
  6. breachspider-0.3.0/breachspider/client.py +486 -0
  7. breachspider-0.3.0/breachspider/exceptions.py +164 -0
  8. breachspider-0.3.0/breachspider/models.py +615 -0
  9. breachspider-0.3.0/breachspider/py.typed +0 -0
  10. breachspider-0.3.0/breachspider/resources/__init__.py +15 -0
  11. breachspider-0.3.0/breachspider/resources/catalog.py +51 -0
  12. breachspider-0.3.0/breachspider/resources/correlate.py +107 -0
  13. breachspider-0.3.0/breachspider/resources/cves.py +64 -0
  14. breachspider-0.3.0/breachspider/resources/environments.py +68 -0
  15. breachspider-0.3.0/breachspider/resources/reports.py +33 -0
  16. breachspider-0.3.0/breachspider/resources/watchlist.py +43 -0
  17. breachspider-0.3.0/breachspider/resources/windows.py +195 -0
  18. breachspider-0.3.0/breachspider.egg-info/PKG-INFO +346 -0
  19. breachspider-0.3.0/breachspider.egg-info/SOURCES.txt +30 -0
  20. breachspider-0.3.0/breachspider.egg-info/dependency_links.txt +1 -0
  21. breachspider-0.3.0/breachspider.egg-info/requires.txt +6 -0
  22. breachspider-0.3.0/breachspider.egg-info/top_level.txt +1 -0
  23. breachspider-0.3.0/pyproject.toml +46 -0
  24. breachspider-0.3.0/setup.cfg +4 -0
  25. breachspider-0.3.0/tests/test_correlate.py +131 -0
  26. breachspider-0.3.0/tests/test_errors.py +115 -0
  27. breachspider-0.3.0/tests/test_integration.py +81 -0
  28. breachspider-0.3.0/tests/test_openapi.py +23 -0
  29. breachspider-0.3.0/tests/test_pagination.py +71 -0
  30. breachspider-0.3.0/tests/test_quota.py +53 -0
  31. breachspider-0.3.0/tests/test_redaction.py +64 -0
  32. 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