Runbook: Feed Connector - NVD Connector Failures

Purpose: recover the Concelier NVD connector when the NIST CVE 2.0 feed is failing or stale — transport/endpoint problems, schema-validation quarantines, or a window cursor that needs widening. Audience: Platform on-call engineers responding to a stale or failing NVD advisory feed.

Doc reconciliation (2026-05-30): This runbook was reconciled against the implementation in src/Concelier/__Libraries/StellaOps.Concelier.Connector.Nvd. Several commands and config keys in the original draft did not exist in code and have been corrected. The NVD connector is API-key-free — it pulls the public NIST CVE 2.0 REST feed via a sliding modified-window with no authentication. Sections that describe API-key/rate-limit handling have been removed or marked NOT IMPLEMENTED.

Metadata

FieldValue
ComponentConcelier / NVD Connector (StellaOps.Concelier.Connector.Nvd, source id nvd)
SeverityHigh
On-call scopePlatform team
Last updated2026-05-30
Doctor checkNOT IMPLEMENTED — no check.connector.nvd-health Doctor check exists (verified against src/Doctor). Use the freshness API and job-run endpoints below instead.

How the connector works (ground truth)

Understanding the pipeline makes diagnosis deterministic:


Symptoms

The original draft referenced an alert ConnectorNvdSyncFailed and a metric connector_sync_failures_total{source="nvd"}. Neither exists in source. The connector emits counters under the meter StellaOps.Concelier.Connector.Nvd (see Metrics). Wire alerts off those counters or off the freshness API.


Impact

Impact TypeDescription
User-facingVulnerability scans may miss recent CVEs
Data integrityData becomes stale; no data loss (failures back off and retry; cursor only advances on success)
SLA impactSource freshness SLA exceeded (freshnessSlaSeconds on the freshness record)

Diagnosis

All HTTP examples target the Concelier WebService. Read endpoints (/jobs*, /api/v1/advisory-sources*) and the trigger endpoint require a bearer token. See Authorization for the required scopes.

Quick checks

  1. Check source freshness (last sync, last success, last error):

    curl -s -H "Authorization: Bearer $TOKEN" \
      https://<concelier-host>/api/v1/advisory-sources/nvd/freshness | jq
    

    Look for: lastSyncAt, lastSuccessAt, lastError, errorCount, freshnessStatus (healthy / warning / stale / unavailable).

  2. Check the aggregate source dashboard counters:

    curl -s -H "Authorization: Bearer $TOKEN" \
      https://<concelier-host>/api/v1/advisory-sources/summary | jq
    
  3. Check the NVD job definitions and recent runs:

    curl -s -H "Authorization: Bearer $TOKEN" \
      https://<concelier-host>/jobs/definitions/source:nvd:fetch | jq
    curl -s -H "Authorization: Bearer $TOKEN" \
      "https://<concelier-host>/jobs/definitions/source:nvd:fetch/runs?limit=10" | jq
    

    Repeat for source:nvd:parse and source:nvd:map. Look for Failed outcomes and error messages.

Deep diagnosis

  1. Inspect Concelier service logs for the NVD source. Grep for the structured log messages emitted by the connector:

    • NVD fetch failed for {Uri} — transport/HTTP failure (followed by a 5-minute backoff).
    • NVD window {Start} - {End} returned 304 — upstream unchanged (not an error).
    • NVD schema validation failed ... quarantined — payload failed schema (nvd.parse.quarantine).
    • Failed to parse NVD JSON payload — malformed JSON (nvd.parse.failures).
  2. Check connector metrics (meter StellaOps.Concelier.Connector.Nvd, exported via OTLP/Prometheus — see Metrics):

    • nvd.fetch.failures rising → fetch/transport problem.
    • nvd.fetch.unchanged high → upstream returning 304 (usually benign).
    • nvd.parse.quarantine / nvd.parse.failures rising → schema/parse problem.
  3. Check for an NVD outage / endpoint problem. Confirm the base endpoint is reachable from the Concelier host:

    curl -s -o /dev/null -w '%{http_code}\n' \
      'https://services.nvd.nist.gov/rest/json/cves/2.0?resultsPerPage=1'
    

    Also check: https://nvd.nist.gov/general/news

  4. Confirm the active endpoint and pacing. The runtime overlay (NvdRuntimeOverlay) merges DB-persisted baseEndpoint, windowSize, windowOverlap, and initialBackfill over the configured values, so an operator override may have changed the effective endpoint. Inspect the advisory-source record (Console Admin → Advisory Sources, or the /api/v1/advisory-sources list) to see the active configuration.


Resolution

Immediate mitigation

  1. Force a fetch/parse/map cycle (e.g. after a transient outage clears). Trigger the three jobs in order:

    curl -s -X POST -H "Authorization: Bearer $TOKEN" \
      -H 'Content-Type: application/json' -d '{"trigger":"runbook"}' \
      https://<concelier-host>/jobs/source:nvd:fetch
    # then, after fetch completes:
    curl -s -X POST -H "Authorization: Bearer $TOKEN" \
      -H 'Content-Type: application/json' -d '{"trigger":"runbook"}' \
      https://<concelier-host>/jobs/source:nvd:parse
    curl -s -X POST -H "Authorization: Bearer $TOKEN" \
      -H 'Content-Type: application/json' -d '{"trigger":"runbook"}' \
      https://<concelier-host>/jobs/source:nvd:map
    

    Responses: 202 Accepted (queued, Location: /jobs/{runId}), 404 (job not registered), 423 Locked (job disabled), 409 Conflict (a run is already active).

  2. If you see persistent 304s with stale data, the cursor is at the head of the window — this is normal when there are genuinely no new modifications. Widen recovery by temporarily increasing initialBackfill only if the cursor was reset / data was lost.

  3. Air-gap / disaster recovery: load from an offline feed snapshot. The supported offline path is the deterministic feed-snapshot bundle, not a per-source loader:

    stella feeds snapshot import <bundle-file> --validate
    

    (See stella feedsfor the full snapshot command set.)

Root cause fix

If the upstream endpoint changed or you need a private NVD mirror:

Override the base endpoint via the advisory-source runtime configuration (persisted setting key baseEndpoint, surfaced in Console Admin → Advisory Sources). The runtime overlay re-projects it onto NvdOptions on the next resolution; no redeploy is required. Configuration file equivalent (concelier:sources:nvd):

concelier:
  sources:
    nvd:
      baseEndpoint: "https://<your-nvd-mirror>/rest/json/cves/2.0"
      windowSize: "PT4H"      # 4h sliding modified-window
      windowOverlap: "PT5M"   # must be < windowSize
      initialBackfill: "P7D"  # look-back on first run / empty state

NOT IMPLEMENTED: auth / api-key, maxDocumentsPerFetch, fetchTimeout, requestDelay, sync_interval, delta_sync, and proxy config keys referenced in older docs/drafts. NvdOptions exposes only baseEndpoint, windowSize, windowOverlap, and initialBackfill. The HTTP timeout (30s) is fixed in NvdServiceCollectionExtensions. There is no NVD API-key authentication.

If hammering the upstream causes throttling:

Increase windowSize (fewer, larger windows) and/or rely on the built-in 5-minute failure backoff. The connector does not perform per-request rate-limit accounting; pacing is governed entirely by the job cron schedule and the modified-window size.

If network/firewall is the issue:

  1. Verify outbound HTTPS connectivity to the configured baseEndpoint host from the Concelier host (the HTTP client restricts AllowedHosts to that host, so a redirected/mirrored host must match the configured endpoint).
  2. Egress proxy configuration is a deployment/runtime concern (container env / HTTPS_PROXY), not an NVD connector config key.

If data parsing failures / quarantine spikes:

  1. The connector validates against an embedded NVD CVE 2.0 JSON schema (NvdSchemaProvider). A spike in nvd.parse.quarantine usually means an upstream schema change. Quarantined documents are marked Failed; the window cursor still advances for recoverable payloads.
  2. If NVD changed its schema, the connector must be updated in code (NvdSchemaProvider / NvdMapper) and redeployed. There is no stella connector ... schema-check or stella upgrade --component command.

Verification

# Re-trigger the pipeline (see Immediate mitigation) and poll the run:
RUN=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' -d '{"trigger":"runbook"}' \
  https://<concelier-host>/jobs/source:nvd:fetch -D - | awk -F'/jobs/' '/Location/{print $2}' | tr -d '\r')
curl -s -H "Authorization: Bearer $TOKEN" \
  https://<concelier-host>/jobs/$RUN | jq '.status'

# Confirm freshness recovered (lastSuccessAt advanced, errorCount stable):
curl -s -H "Authorization: Bearer $TOKEN" \
  https://<concelier-host>/api/v1/advisory-sources/nvd/freshness | jq

# Confirm no NVD failure log lines in the last hour (service log search).

Prevention


Authorization

OperationEndpointRequired scope (claim)
Trigger NVD jobsPOST /jobs/source:nvd:{fetch,parse,map}concelier.jobs.trigger (policy Concelier.Jobs.Trigger)
List jobs / definitions / runsGET /jobs*concelier.jobs.read or concelier.jobs.trigger (policy Concelier.Jobs.Read; trigger implies read)
Read source freshnessGET /api/v1/advisory-sources*policy Concelier.Advisories.Read (advisory read)

Scope constants live in src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs (ConcelierJobsRead = "concelier.jobs.read", ConcelierJobsTrigger = "concelier.jobs.trigger"). The advisory-source read endpoints are tenant-scoped (RequireTenant()).


Metrics

Meter: StellaOps.Concelier.Connector.Nvd (registered in Concelier WebService telemetry). All counters are long:

MetricMeaning
nvd.fetch.attemptsFetch operations attempted (including paginated windows)
nvd.fetch.documentsDocuments fetched and persisted
nvd.fetch.failuresFetch attempts that errored or returned no document
nvd.fetch.unchangedFetch attempts returning 304 Not Modified
nvd.parse.successDocuments validated and converted to DTOs
nvd.parse.failuresDocuments that failed parsing (missing content / read error)
nvd.parse.quarantineDocuments quarantined on schema validation failure
nvd.map.successCanonical advisories produced by mapping

CLI surface

The primary NVD-relevant CLI is the deterministic feed-snapshot group (FeedsCommandGroup, wired as stella feeds snapshot ...):

stella feeds snapshot create [--label <l>] [--sources nvd ...] [--json]
stella feeds snapshot list [--limit <n>] [--json]
stella feeds snapshot export <snapshot-id> --output <file> [--compression zstd|gzip|none]
stella feeds snapshot import <input-file> [--validate]
stella feeds snapshot validate <snapshot-id>

Feed administration is also available via the admin feeds group (AdminCommandGroup.BuildFeedsCommand, backed by AdminCommandHandlers):

stella admin feeds list [--format table|json]
stella admin feeds status [--source <id>]          # e.g. --source nvd
stella admin feeds refresh [--source <id>] [--force]
stella admin feeds history --source <id> [--limit <n>]

Note — stella config feeds list|status is a stub. A second feed group exists under config (CommandFactory.BuildConfigFeedsCommand), but it prints hardcoded sample data (fixed timestamps, fabricated advisory counts) — do not use it for live diagnosis. Use stella admin feeds status or the freshness API instead.

NOT IMPLEMENTED (verified against src/Cli):

  • stella connector test|credentials|logs|nvd ... and stella connector config set — there is no top-level connector command group at all.
  • stella upgrade --component — there is no upgrade command group.
  • stella vuln query — the vuln group does exist, but its subcommands are observations, list, show, assign, comment, accept-risk, verify-fix, target-fix, reopen, simulate, export; there is no query subcommand.
  • stella offline load — the offline group does exist, but the kit loader is stella offline import (with status); there is no load subcommand.

stella doctor [run] --check <id> exists, but there is no NVD connector check id to pass it (no NVD check is registered under src/Doctor).