The Post-Quantum Repository (PQR) service. A pure-Node HTTP API (no web
framework) that wraps the precogs-cbom engine so ECUs, CI pipelines, and other
tools can register projects and request CBOMs/QBOMs on demand instead of running
the CLI. Every scan is persisted per project and rolled up into a fleet view.
For backend developers: this is the service to deploy and (optionally) put
behind your gateway / swap the store for Postgres.
For frontend developers: the JSON contracts below are what the CISO
dashboard and ECU self-service UI consume. GET /api/v1/portfolio returns the
exact shape the bundled portfolio.html renders.
# from an install
CBOM_API_TOKEN=$(openssl rand -hex 24) cbom serve --port 8787 --data-dir /var/lib/pqr
# from the repo
CBOM_API_TOKEN=secret node ./bin/cbom.js serve --port 8787 --data-dir ./.cbom-serverFlags / env:
| Flag | Env | Default | Meaning |
|---|---|---|---|
--port |
CBOM_API_PORT |
8787 |
listen port |
--host |
— | 0.0.0.0 |
bind interface |
--data-dir |
— | .cbom-server |
JSON project/scan store (the repository) |
--token |
CBOM_API_TOKEN |
— | bearer token clients must present |
--no-auth |
— | — | disable auth (local/testing only) |
Air-gapped: source/firmware/certificate scans make no outbound calls. Only
/probe (reaches your targets) and /vault (reaches your cloud vault) need
network — and both are initiated by the server, never inbound.
Auth: when a token is set, every route except GET /health requires
Authorization: Bearer <token>. Comparison is constant-time.
Project ──< Scan (source | firmware | certificate | probe | vault)
└─ cbom (CycloneDX 1.6 CBOM)
└─ qbom (CycloneDX 1.6 QBOM + readiness)
└─ summary { score, grade, CRITICAL, HIGH, ... }
└─ pqcReport { totalVulnerableInstances, ... }
A project groups scans over time; the latest scan per project is its current
state, earlier scans drive trends. The store is <data-dir>/projects/*.json +
<data-dir>/scans/<projectId>/*.json — swap src/server/store.js for a
Postgres-backed module with the same five methods (listProjects, getProject,
createProject, addScan, listScans, latestScan) and nothing else changes.
All bodies are JSON unless noted. Errors are { "error": "..." } with a 4xx/5xx status.
No auth. { ok, service, version, projects }.
{ projects: [{ id, name, compliance, metadata, createdAt }] }.
Register (idempotent by name — same name returns the existing project).
{ "name": "ecu-telematics-42", "compliance": ["unece-r155","cnsa-2"], "metadata": {} }→ 201 { id, name, compliance, metadata, createdAt }. compliance frameworks
are auto-evaluated on every subsequent scan of this project.
Project + its scan index ({ ...project, scans: [...] }).
Two ways to supply code:
- Server-local path (JSON):
{ "path": "/builds/ecu-42/src" }— the server scans a directory it can already see (mounted build workspace). - Tarball upload (binary body,
Content-Type: application/x-tarorapplication/gzip): the server unpacks to a temp dir, scans, discards it.
→ 201 QBOM response (see below).
Raw binary body (ELF/PE/Mach-O/JAR). Optional ?name=ecu-fw.elf. Detects crypto
libraries and weak-algorithm symbols baked into the image. → 201 QBOM response.
Raw PEM/DER body. Optional ?name=device.pem. → 201 QBOM response.
Live TLS/SSH/LDAP/IPsec probe (server reaches the target):
{ "targets": [ { "host": "api.vehicle.example.com", "port": 443, "type": "tls" } ] }Single-target shorthand { "host": ..., "port": ..., "type": ... } also works.
Requires authorization to scan the target. → 201 QBOM response.
Live metadata-only key-store scan:
{ "provider": "aws", "region": "eu-west-2" }
{ "provider": "azure", "vaultUrl": "https://v.vault.azure.net", "tenantId": "...", "clientId": "...", "clientSecret": "..." }
{ "provider": "hashicorp", "address": "https://vault:8200", "token": "..." }→ 201 QBOM response.
Latest CycloneDX 1.6 CBOM / QBOM document for the project (404 if never scanned).
{ scans: [{ id, kind, timestamp, riskScore, riskGrade, findingCount }] }.
Fleet roll-up across all projects — same shape as portfolio.json (see
src/portfolio): { generatedAt, fleet, projects }. This is what the CISO
dashboard renders.
{
"scanId": "scan_...",
"kind": "source",
"timestamp": "2026-07-06T...",
"riskScore": 42, "riskGrade": "D",
"counts": { "critical": 3, "high": 2, "medium": 1, "low": 0 },
"quantum": { "shorVulnerable": 5, "groverWeakened": 1, "pqcSafe": 0 },
"exploitability": {
"actNow": 17, "scored": 25,
"top": [ { "priority": 84, "band": "CRITICAL", "asset": "RSA-1024",
"file": "...", "reasons": ["...", "..."], "factors": {...} } ]
},
"cbom": { /* full CycloneDX 1.6 CBOM */ },
"qbom": { /* full CycloneDX 1.6 QBOM */ }
}exploitability is the "which findings matter" ranking — actNow is the
count of Critical+High-exploitability findings (reachable, exposed, or protecting
long-lived data), and top is the ranked shortlist with a human-readable
reasons array per finding. Frontends should surface actNow as the headline
number and top as the remediation queue, not the raw counts.
TOKEN=... ; API=http://pqr.internal:8787
# 1. register once (idempotent — safe to call every build)
PID=$(curl -s -X POST $API/api/v1/projects -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{"name":"ecu-telematics-42","compliance":["unece-r155"]}' \
| node -pe 'JSON.parse(require("fs").readFileSync(0)).id')
# 2. request a QBOM for this ECU's firmware
curl -s -X POST "$API/api/v1/projects/$PID/scan/firmware?name=app.elf" \
-H "authorization: Bearer $TOKEN" --data-binary @build/app.elf | jq '.quantum'
# 3. CISO dashboard reads the fleet
curl -s $API/api/v1/portfolio -H "authorization: Bearer $TOKEN" > portfolio.jsonA 150-ECU simulation is just this loop over 150 names — each gets its own project and QBOM, and the portfolio aggregates them.
The npm main is src/engine/index.js. Everything the server does is available
programmatically:
const engine = require('precogs-cbom/src/engine');
const cyclonedx = require('precogs-cbom/src/output/cyclonedx');
const qbom = require('precogs-cbom/src/output/qbom');
const result = await engine.runScan('./project', { secretsDetect: true, pqcCheck: true, compliance: 'unece-r155' });
const cbomDoc = cyclonedx.generate(result);
const qbomDoc = qbom.generate(result);Vault connectors, the policy engine, the portfolio aggregator, the Armis
integration, and the advisor are all plain modules under src/ — see
docs/INTEGRATIONS.md.
- Persistence: the JSON store is fine for a PoC and for tens of thousands of
scans. For production concurrency, implement
src/server/store.jsagainst Postgres (same method surface) — the BOMs are already JSON columns. - Scale: scans are CPU-bound and synchronous per request; front with a
process manager (pm2/systemd) and N workers, or put a queue in front of
/scan/*for large firmware. - TLS: terminate TLS at your ingress/load balancer; the server speaks plain HTTP by design (keeps it dependency-free and air-gap friendly).
- Body size: default cap is 200 MB (firmware). Adjust
MAX_BODYinsrc/server/index.jsif needed.