Vulnerability Feed API Both¶
The Vulnerability Feed Broker is SentriKat's centralized, read-only feed of the public vulnerability landscape — CISA KEV, NVD (CVSS + CPE applicability), EPSS and the NVD CPE dictionary — plus a beyond-NVD exploit-intel layer. SentriKat operates it so that client installations (especially on-premises and air-gapped) pull this data from one place instead of each hammering the upstream feeds.
What it is — and isn't
The broker is a cache/mirror of authoritative public data, not a detection engine. It serves raw, upstream-faithful records. It does no matching against your inventory, so it produces no false positives of its own — confidence scoring and false-positive suppression happen later, in the SentriKat detection engine on the client side.
Coverage scope: the broker carries NVD / CISA-KEV / EPSS / CPE / exploit-intel only. Vendor-advisory, distro and OSV package feeds stay direct on the client and are not brokered — so coverage is never lost if you rely on the broker.
Base path: /api/v1/vuln-feed · Contract version: 0.1.0 (returned in the Contract-Version response header).
Authentication¶
Every endpoint except /health requires the same Bearer scheme as KB Sync:
Authorization: Bearer <first 64 chars of your signed license token>
X-Installation-ID: SK-INST-XXXXXXXX
The licence's edition maps to a feed tier, and each endpoint requires a minimum tier:
| Edition | Feed tier | Can call |
|---|---|---|
| Community / Demo | community | /manifest, /cpe-dictionary |
| Professional | professional | all of the above + /vulnerabilities, /cve/{id}, /exploit-intel, /bundle |
Requests are rate-limited per installation (by X-Installation-ID), not per IP, so a NAT'd fleet sharing one egress IP isn't throttled as a single client.
Error envelope¶
All errors return a consistent body:
{
"error": "tier_insufficient",
"message": "this endpoint requires the professional tier or higher",
"documentation_url": "https://docs.sentrikat.com/api/vuln-feed"
}
Common codes: auth_invalid_signature, auth_installation_unknown, auth_installation_suspended, tier_insufficient, not_found, bad_request.
Endpoints¶
GET /health¶
Public (no auth). Liveness and per-dataset freshness so a monitor or status page can alert when ingestion silently stops.
{
"status": "ok",
"contract_version": "0.1.0",
"datasets": {
"vulnerabilities": { "count": 1342, "last_modified": "2026-06-26T05:15:00", "age_hours": 1.2, "stale": false },
"cpe_dictionary": { "count": 89234, "last_modified": "2026-06-22T04:30:00", "age_hours": 96.0, "stale": false },
"exploit_intel": { "count": 412, "last_modified": "2026-06-26T05:15:00", "age_hours": 1.2, "stale": false }
},
"checked_at": "2026-06-26T06:25:00+00:00"
}
status is ok, stale (a dataset is older than its budget — 48h for KEV/EPSS/exploit, 240h for the CPE dictionary), or empty (returns HTTP 503).
GET /manifest¶
Tier: community. Dataset sizes + cursors so a client can plan incremental pulls, plus your tier and the coverage note.
GET /vulnerabilities¶
Tier: professional. Paginated CVE records (CISA KEV ∪ NVD-enriched).
| Query param | Default | Notes |
|---|---|---|
page | 1 | |
page_size | 100 | max 500 |
since | – | ISO 8601; only rows modified after this time |
Incremental pulls: pass ?since=<last_modified you saw> or the If-Modified-Since header. If nothing changed, the broker returns 304 Not Modified. Response is a pagination envelope:
{
"contract_version": "0.1.0",
"page": 1, "page_size": 100, "total": 1342,
"next_page": "/api/v1/vuln-feed/vulnerabilities?page=2&page_size=100",
"data": [ { "cve_id": "CVE-2024-3400", "severity": "CRITICAL", "is_actively_exploited": true, "epss_score": 0.97, "last_modified": "2026-06-26T05:15:00" } ]
}
GET /cve/{cve_id}¶
Tier: professional. A single CVE with its CPE applicability (cpe_data).
GET /cpe-dictionary¶
Tier: community. Paginated CPE dictionary (?since= / 304 supported).
GET /exploit-intel¶
Tier: professional. Paginated beyond-NVD exploit signal per CVE.
GET /bundle¶
Tier: professional. Downloads the latest signed offline bundle (.tar.gz) for air-gapped installs. See below.
Offline bundle (air-gapped)¶
A self-contained, signed snapshot for installations with no internet access.
- Archive:
.tar.gz(gzip). - Datasets: NDJSON (one JSON object per line), field names identical to the REST output — so the client uses one parser for both API and bundle.
- Integrity: each dataset has a
sha256recorded inmanifest.json. - Authenticity: a detached
manifest.json.sigsigns the canonicalmanifest.jsonwith the same RSA keypair as licences (RSA-PKCS1v15-SHA256, base64).
Archive layout:
manifest.json
manifest.json.sig
vulnerabilities.jsonl
cpe_data.jsonl
exploit_intel.jsonl
cpe_dictionary.jsonl
kev_history.jsonl
manifest.json:
{
"contract_version": "0.1.0",
"format_version": 1,
"generated_at": "2026-06-26T05:45:00Z",
"datasets": [
{"name": "vulnerabilities", "filename": "vulnerabilities.jsonl", "record_count": 1342, "sha256": "…"},
{"name": "cpe_data", "filename": "cpe_data.jsonl", "record_count": 5821, "sha256": "…"}
],
"signature": {"algorithm": "RSA-PKCS1v15-SHA256", "format": "detached", "file": "manifest.json.sig", "signed": "manifest.json"}
}
Verifying a bundle¶
Verify the signature over the raw manifest.json bytes (do not re-serialise), then the per-dataset sha256:
import tarfile, json, hashlib, base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
with tarfile.open("sentrikat-feed-bundle.tar.gz", "r:gz") as t:
manifest_bytes = t.extractfile("manifest.json").read() # raw bytes
sig = base64.b64decode(t.extractfile("manifest.json.sig").read())
licensing_public_key.verify(sig, manifest_bytes, # same key as licences
padding.PKCS1v15(), hashes.SHA256())
manifest = json.loads(manifest_bytes)
for d in manifest["datasets"]:
data = t.extractfile(d["filename"]).read()
assert hashlib.sha256(data).hexdigest() == d["sha256"]
rows = [json.loads(line) for line in data.splitlines()] # NDJSON
Data freshness & reliability¶
The feed is refreshed continuously from authoritative public sources — you don't manage any of that. What you can rely on as a client:
- Check freshness any time via
GET /health(per-datasetlast_modified,age_hours,stale). - Graceful behaviour during upstream outages: the broker keeps serving the last known-good data rather than going blank, so a temporary problem at an upstream shows up as staleness, not an empty response.
- Keep your own direct feeds as a safety net. The broker covers NVD/CISA-KEV/EPSS/CPE/exploit-intel only; your installation should continue to ingest vendor-advisory, distro and OSV package feeds directly. If the broker is unreachable, fall back to direct upstreams — coverage is never lost.
Security¶
- Access is limited to licensed installations (Bearer + tier).
- The API is read-only and rate-limited per installation.
- Offline bundles are RSA-signed and per-dataset hashed, so a tampered bundle is rejected before import (fail-closed).
- The feed data itself is public vulnerability intelligence — the guarantees here are integrity and access control, not secrecy.