Skip to main content

BreachSpider Python SDK

Official Python client for the BreachSpider ICS/OT CVE intelligence API. Go from zero to working queries in about ten seconds from clone to first query (most of it pip install) instead of writing your own HTTP client.

  • Typed objects for CVEs, vendors, products, environments, and assets, not raw dicts.
  • Transparent pagination that absorbs the API's per-endpoint shapes.
  • Automatic 429 backoff so a naive loop over thousands of CVEs won't trip the edge limit.
  • Typed exceptions that surface the API's own helpful error messages.
  • The API key is never printed, logged, or included in an exception.
  • New in 0.3.0: cited source references on every CVE: vendor advisories, CISA ICS advisories and the CVE.org record. See the changelog.
  • New in 0.2.0: asset correlation (API v1) and Windows patch level (API v2), with the exposure-priority ranking, fix groups and fix plans. See the changelog: the API's default CVE order changed to priority.

Install

pip install breachspider

Requires Python 3.9+. The only dependency is requests.

60-second quickstart (no signup)

Client.demo() mints a read-only demo token (valid 24h, scoped to a public demo org). No API key or signup required.

import breachspider

bs = breachspider.Client.demo()

# Find a vendor. Read the slug from the response, never hand-craft it.
vendor = bs.catalog.vendor("schneider electric")
print(vendor.id, vendor.name, vendor.slug)   # 11529 Schneider-Electric schneider-electric

# List that vendor's products, each with a CVE count.
for product in bs.catalog.products(vendor_id=vendor.id)[:3]:
    print(f"  {product.id:>6}  {product.name}  ({product.cve_count} CVEs)")

# Critical, known-exploited CVEs for that vendor.
for cve in bs.cves.by_vendor(vendor.slug, severity="CRITICAL", kev=True):
    print(cve.cve_id, "-", cve.title)
    break   # (the loop would page through all of them)

Authentication

Use a real API key (bs_live_…) for production. Keys require the Professional tier or above and are created in the dashboard under Integrations → API Keys.

import os
import breachspider

# export BREACHSPIDER_API_KEY=bs_live_your_key_here
bs = breachspider.Client(os.environ.get("BREACHSPIDER_API_KEY", "bs_live_YOUR_KEY_HERE"))
print(bs)   # key is redacted: <breachspider.Client base_url='https://breachspider.com' auth=live key=***redacted***>

The key is held privately: it never appears in repr(), str(), logs, or any exception message.

The primary workflow: vendor → products → CVEs

import breachspider

bs = breachspider.Client.demo()

vendor = bs.catalog.vendor("schneider electric")
assert (vendor.id, vendor.slug) == (11529, "schneider-electric")

products = bs.catalog.products(vendor_id=vendor.id)
m340 = next(p for p in products if p.id == 25001)
print(m340.name, m340.cve_count)          # Modicon M340 41

# by_vendor takes the slug and supports the full /cves filter set.
criticals = bs.cves.by_vendor(vendor.slug, severity="CRITICAL", kev=True)
first = next(iter(criticals))
print(first.cve_id, first.severity, first.cvss_score, first.kev_flagged)

Searching CVEs

bs.cves.search(**filters) returns an iterator that transparently follows pages. Accepted filters:

q, vendor (slug), protocol, severity, kev, unpatched, has_exploit, patch_status, cvss_min, cvss_max, bcs_min, bcs_max, date_from, date_to, sort_by, ranked.

There is no product, version, or cpe filter on /cves today. To narrow to a product, filter by vendor and match on the result fields.

import itertools
import breachspider

bs = breachspider.Client.demo()

# First 5 KEV CVEs affecting Siemens, highest CVSS first.
for cve in itertools.islice(bs.cves.search(vendor="siemens", kev=True, sort_by="cvss"), 5):
    print(cve.cve_id, cve.cvss_score, cve.severity)

# One CVE by id (rich detail; nested scoring/exploitation/patch in .raw).
cve = bs.cves.get("CVE-2025-32433")
print(cve.cve_id, cve.severity, cve.cvss_score, cve.html_url)

Pagination

Every search/by_vendor/assets call is a lazy iterator: it fetches the next page only as you consume it, and stops at the end. Page size defaults to 100 (the maximum). Different endpoints paginate differently (per_page vs limit, _links.next vs page numbers vs offset); the SDK handles all of them.

import breachspider

bs = breachspider.Client.demo()

count = 0
for _cve in bs.cves.search(vendor="schneider-electric"):
    count += 1
    if count >= 250:      # 250 spans 3 pages of 100; the SDK paged for you
        break
print("iterated", count, "CVEs across multiple pages")

Do not call list() on a search iterator

list(bs.cves.search(...)) materializes every page of the result set. Siemens alone has over 5,000 CVEs; walking all of them will exhaust every page and can trigger the rate limit. Use itertools.islice when you only need the first N, or a for loop with a break.

import itertools
import breachspider

bs = breachspider.Client.demo()

# Safe: fetch exactly 10 results and stop. Never touches page 2 or beyond.
first_ten = list(itertools.islice(
    bs.cves.search(vendor="siemens", kev=True, sort_by="cvss"), 10))
for cve in first_ten:
    print(cve.cve_id, cve.cvss_score)

Rate-limit safety

The API sits behind a Cloudflare edge limit that trips at roughly 37 rapid requests from one IP. The client sleeps page_delay seconds (default 0.25) between pages and retries any 429 with exponential backoff. Tune it:

import breachspider

# Faster (riskier) or gentler pagination, and more patient retries.
bs = breachspider.Client.demo(page_delay=0.5, max_retries=8, backoff_factor=1.0)
print("page_delay:", bs.page_delay, "max_retries:", bs.max_retries)

Quota / usage headers

With a demo token (Client.demo()), bs.quota is always None; demo traffic is not metered.

With a bs_live_ key, every response carries X-RateLimit-* headers that the client parses into bs.quota after any call. Metering is observe-only today: usage is reported but nothing is rejected. Unlimited tiers report the string "unlimited".

import os
import breachspider
from breachspider import AuthenticationError

# export BREACHSPIDER_API_KEY=bs_live_your_key_here
bs = breachspider.Client(os.environ.get("BREACHSPIDER_API_KEY", "bs_live_YOUR_KEY_HERE"))

try:
    next(iter(bs.cves.search(vendor="siemens", per_page=1)), None)
    q = bs.quota
    print("limit:", q.limit, "used:", q.used, "remaining:", q.remaining)
    # On Professional: limit 25000 ... ; on Enterprise/api: limit 'unlimited'
except AuthenticationError:
    print("Set BREACHSPIDER_API_KEY to a live bs_live_… key to see quota.")

Error handling

Errors map to typed exceptions that keep the server's own message and structured detail, the most useful part.

import breachspider
from breachspider import UnknownParameterError, ValidationError, NotFoundError

bs = breachspider.Client.demo()

# 400 UNKNOWN_PARAMETER surfaces the accepted-parameter list.
try:
    list(bs.cves.search(color="red"))
except UnknownParameterError as e:
    print("accepted:", e.accepted_parameters)

# 422 for per_page > 100 (the real per-page maximum is 100 on every tier).
try:
    bs.request("GET", "/cves", params={"per_page": 101})
except ValidationError as e:
    print("validation:", e.fields)

# 404 for a CVE that doesn't exist.
try:
    bs.cves.get("CVE-0000-00000")
except NotFoundError as e:
    print("not found:", e.message)
Exception HTTP Notable attributes
UnknownParameterError 400 accepted_parameters, unknown_parameters
AuthenticationError 401
InsufficientScopeError 403 required_scope, current_scopes
CapExceededError 403 resource, limit, current, tier
NotFoundError 404 detail
ValidationError 422 fields
RateLimitError 429 retry_after (auto-retried first)

All inherit from breachspider.BreachSpiderError. Network failures raise APIConnectionError (never leaking the request or key).

Correlate assets (API v1)

Send your inventory (vendor, product, version); get each asset's CVEs ranked by what's exposed and what to fix first, the fix actions, and a fix plan. Nothing is stored server-side.

device = {"asset_id": "EXAMPLE-SWITCH-01", "vendor": "Moxa", "product": "EDS-518A", "version": "V3.5"}
resp = bs.correlate.correlate([device])          # sort="priority" by default; also "score", "exploit", "newest"
result = resp.results[0]
for cve in result.cves:
    print(cve.priority_rank, cve.cve_id, cve.priority_reason)
print(result.fix_groups[0].fix, result.fix_plan)
every_page = bs.correlate.all_cves(device)       # follows cves_page.has_more for you

Filters: confirmed_only, known_exploited_only, fix_available_only. result_hash never depends on sort, filters or page; use bs.correlate.check(...) to re-correlate only what changed. Guide: docs/correlate.md.

Windows patch level (API v2)

Send Windows build and KB facts; get confirmed open / cleared / needs review per CVE from Microsoft's own data, with fix actions such as "install KB5122876 (latest cumulative, build 10.0.17763.9245)". Submitting needs a write-scoped key.

hosts = [{"asset_id": "WIN-EXAMPLE-01", "os_product": "Windows Server 2019 Standard", "edition_id": "ServerStandard",
          "os_build": "10.0.17763.7792", "architecture": "x64", "installation_type": "Server",
          "collected_at": "2026-09-25T14:02:00Z"}]
resp = bs.windows.correlate(12, hosts, include_cleared=False)
for r in resp.rejected:                          # refused hosts (e.g. an identifying field); nothing stored
    print(r.asset_id, r.error_codes)
for batch in bs.windows.correlate_batched(12, many_hosts):   # calls of 100 hosts or fewer
    ...
bs.windows.correlate_csv(12, "hosts.csv")        # the same from a CSV file
bs.windows.results(12, asset_id="WIN-EXAMPLE-01", cve_page_size=50)

Guide: docs/windows-v2.md. Examples for every endpoint: examples/. OpenAPI document for these endpoints: openapi/breachspider-openapi.json.

Writes

Reads work with any key. Writes (environments.create, environments.add_asset, watchlist.add, environments.create_ticket, reports.generate) require a write-scoped bs_live_ key and are rejected for demo tokens. Session-only operations (API-key management, webhook delivery, billing, and account settings) are intentionally not part of this SDK; they can never be called with a key.

Honest limits

  • Per page: 100 results maximum on every paid tier (10 on Free). There is no pagination depth cap; you can page through the entire result set.
  • No product/version/CPE filter on /cves yet. Filter by vendor (slug).
  • Metering is observe-only. Usage is counted and returned in headers, but no request is rejected for exceeding a monthly allowance today.

Running the tests

Install the dev extras, then run the suite:

pip install ".[dev]"
pytest

The suite includes live integration tests that hit the real API using a demo token. To skip them when offline (or in CI without network access):

BREACHSPIDER_SKIP_INTEGRATION=1 pytest

The integration tests are marked @pytest.mark.integration and are skipped automatically if the API is unreachable or the env var is set. The correlate and Windows tests run against responses recorded from the live API (tests/fixtures/), so they need no key and no network.

License

MIT © CITED Relevance LLC

Metadata

Release files for breachspider 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for breachspider 0.3.0
File Size Uploaded
breachspider-0.3.0.tar.gz 38.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for breachspider 0.3.0
File Interpreter ABI Platform
breachspider-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 70.1 kB

Release files / breachspider-0.3.0.tar.gz

Download URL breachspider-0.3.0.tar.gz
Size 38.4 kB
Tags Source
SHA-256 checksum
How to use checksums
c74df2a0dde0ecc7e32030db0a80e212f388721dd08bdf0a22d50ed38595d139
BLAKE2b-256 checksum
How to use checksums
79aea3bb6c2640e1bc5cba1d311661b8ff6d6a7befea74416f5f3edbde52345c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release files / breachspider-0.3.0-py3-none-any.whl

Download URL breachspider-0.3.0-py3-none-any.whl
Size 31.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8ad4e2e00b69b2ab320a133d8e957617a3702e11898d8f5826b0c9a065b22a03
BLAKE2b-256 checksum
How to use checksums
3156291ab1e4fc4149315dee6127703edd995d8b9d20c97ed661af26dc8626cf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page