Skip to content

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 sha256 recorded in manifest.json.
  • Authenticity: a detached manifest.json.sig signs the canonical manifest.json with 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-dataset last_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.