VexHub
Status: Implemented Source: src/VexHub/ Owner: VEX Guild
Purpose
VexHub aggregates, validates, and distributes VEX statements from multiple upstream sources. It is the canonical VEX distribution layer for Stella Ops, providing deterministic outputs, conflict detection, and offline-friendly operation for internal services (such as VexLens) and external tools (Trivy/Grype).
Quick Links
- Architecture
- VexLens (consumer / consensus)
- Excititor (ingestion projection)
- IssuerDirectory (issuer trust)
Components
Services:
StellaOps.VexHub.WebService- Minimal API host for distribution endpoints, the admin conflict-resolution endpoint, health probes, rate limiting, and Router registration
There is no
StellaOps.VexHub.Workerproject. A background ingestion scheduler (VexIngestionScheduler) is implemented inStellaOps.VexHub.Corebut is not wired into the WebService host. In production, scheduled ingestion is handled by the Excititor Worker, which projects AOC-checked claims intovexhub.statements.
Libraries:
StellaOps.VexHub.Core- Normalization pipeline, schema validators (OpenVEX/CycloneDX/CSAF), signature verifier, statement flagging, export service, ingestion serviceStellaOps.VexHub.Persistence- PostgreSQL data source, repositories, and embedded SQL migrations (schemavexhub)
Configuration
Settings bind from the VexHub section (VexHubOptions); PostgreSQL binds from Postgres. Key groups:
- PostgreSQL connection (schema:
vexhub); migrations auto-apply on startup viaAddStartupMigrations - Top-level scalars:
EnableSignatureVerification(defaulttrue),RequireSignedStatements(defaultfalse),StaleDataThresholdDays(default90, drives statement flagging;0disables), plus page-size and polling defaults (DefaultApiPageSize,MaxApiPageSize,DefaultPollingIntervalSeconds,MaxConcurrentPolls) SignatureTrustRoots(map of signing-key fingerprint -> trusted DSSE HMAC root; empty means signed VEX fails closed as unavailable)Ingestion(EnableDeduplication,EnableConflictDetection,BatchSize,FetchTimeoutSeconds,MaxRetries)Distribution(EnableBulkExport,EnableWebhooks,CacheDurationSeconds,RateLimitPerMinute)SourceBackoff(exponential backoff for source polling failures:InitialBackoffSeconds,MaxBackoffSeconds,BackoffMultiplier,JitterFactor,MaxConsecutiveFailures)ApiKeys(external tool API keys; presented viaX-Api-Keyheader orapi_keyquery param)
API Surface
Read endpoints require scope vexhub:read; the conflict-resolution endpoint requires vexhub:admin.
GET /api/v1/vex/cve/{cveId}- Query VEX statements by CVE (limit/offset)GET /api/v1/vex/package/{purl}- Query VEX statements by PURL (URL-decoded)GET /api/v1/vex/source/{sourceId}- Query statements from a specific sourceGET /api/v1/vex/statement/{id:guid}- Single statement by idGET /api/v1/vex/search- Filtered search (sourceId,vulnerabilityId,productKey,status,isFlagged, pluslimit/offset);statusacceptsaffected/fixed/not_affected/under_investigationGET /api/v1/vex/stats- Dashboard statistics:totalStatements,verifiedStatements,flaggedStatements,byStatus,bySource(bucketed by issuer category),recentActivity(last 20), and a 7-daytrendswindowGET /api/v1/vex/export- Bulk OpenVEX feed (application/vnd.openvex+json)GET /api/v1/vex/index- JSON index manifest (version,lastUpdated,endpoints) advertising the by-CVE/by-package/by-source/search/stats/export routes (response body, not a static file)POST /api/v1/vex/conflicts/resolve- Resolve open conflicts for a vulnerability-product pair (admin; audited). Body:{ cveId, selectedStatementId, resolutionType, notes? }.resolutionType=deferrecords the conflicts assuppressedwith no winning statement; any other value marks themmanually_resolvedwith the selected statement as winner. Returns204(or400/404on validation/no-open-conflicts).GET /health,/health/liveness,/health/readiness- anonymous probes (return{ status, service })
Statements carry a SHA-256 content_digest for dedup; timestamps are UTC.
Rate limiting (sliding 1-minute window) is applied per client (API-key id, else forwarded/remote IP); /health* is exempt. Responses carry X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers, and exceeded limits return 429 with Retry-After. Authenticated clients receive 2x the configured Distribution.RateLimitPerMinute.
Dependencies
- PostgreSQL (schema:
vexhubwith tables:sources,statements,conflicts,provenance,ingestion_jobs,webhook_subscriptions; noexport_cursor/ notenant_id— statements are global) - Authority (bearer-token authentication; scopes
vexhub:read/vexhub:admin) - VexLens (consumes VexHub statements/provenance; hosts the consensus endpoint)
- Excititor Worker (projects AOC-checked VEX claims into
vexhub.statements)
Related Documentation
- Architecture:
./architecture.md - VexLens:
../vex-lens/ - IssuerDirectory:
../issuer-directory/ - Excititor:
../excititor/
Current Status
WebService implemented with the distribution API, admin conflict-resolution endpoint, API-key + bearer auth, in-process rate limiting, and startup migrations. Normalization, validation (OpenVEX/CycloneDX/CSAF), offline DSSE-HMAC signature verification, and OpenVEX export are in place. Scheduled ingestion is not run by a VexHub worker (none exists); statements are populated by the Excititor Worker’s projection into vexhub.statements. Prometheus metrics and dedicated tracing spans are not yet implemented (roadmap).
