Runbook: Feed Connector - OSV (Open Source Vulnerabilities) Failures
Audience: Platform on-call engineers diagnosing and recovering the OSV advisory feed connector in Concelier (the vulnerability-feed ingestion service of the Stella Ops release control plane). Use this runbook when OSV ingestion is failing, stale, or producing incomplete open-source ecosystem coverage.
Sprint: SPRINT_20260117_029_DOCS_runbook_coverage Task: RUN-006 - Feed Connector Runbooks
Doc reconciliation (2026-05-30): This runbook was reconciled against the implementation in
src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv, the CLI insrc/Cli/StellaOps.Cli, the Doctor checks insrc/Doctor, and the scope catalogStellaOpsScopes.cs. Several commands, config keys, metrics, and the Doctor check in the original draft did not exist in code and have been corrected or marked NOT IMPLEMENTED. Key correction: the OSV connector is anonymous, public-bucket HTTP — it downloads per-ecosystemall.ziparchives over plain HTTPS GET fromosv-vulnerabilities.storage.googleapis.com. There is no GCS authentication, no service-account credentials, no proxy config key, and no separate “OSV API” vs “GCS bulk” path — it is one path.
Metadata
| Field | Value |
|---|---|
| Component | Concelier / OSV Connector (StellaOps.Concelier.Connector.Osv, source id osv) |
| Severity | High |
| On-call scope | Platform team |
| Last updated | 2026-05-30 |
| Doctor check | NOT IMPLEMENTED — no check.connector.osv-health Doctor check exists (verified against src/Doctor; there is no Concelier/connector Doctor plugin at all). Use stella admin feeds status --source osv, the job-run endpoints, and the connector metric below instead. |
How the connector works (ground truth)
Understanding the pipeline makes diagnosis deterministic:
- The connector runs as three separate jobs wired through the Concelier job scheduler (
OsvDependencyInjectionRoutine):source:osv:fetch— downloads each ecosystem’sall.ziparchive (cron0,20,40 * * * *, i.e. every 20 min; timeout 15 min; lease 10 min).source:osv:parse— deserializes raw OSV records into DTOs (cron5,25,45 * * * *; timeout 20 min).source:osv:map— maps DTOs into canonical advisories (cron10,30,50 * * * *; timeout 20 min). Map is time-budgeted (MapRunTimeBudget, default 15 min): it checkpoints and resumes on the next run rather than running unbounded.
- No authentication. Fetch is a plain
HttpClientGET (client namesource.osv) against the public Google Cloud Storage buckethttps://osv-vulnerabilities.storage.googleapis.com/(configurableBaseUri). There are no GCS credentials, no service account, and no “anonymous-vs-authenticated” toggle — the bucket is world-readable. - Per-ecosystem bulk archives. For each configured ecosystem the connector fetches
<Ecosystem>/<ArchiveFileName>whereArchiveFileNamedefaults toall.zip. The default ecosystem list (fromOsvOptions.Ecosystems) is:PyPI,npm,Maven,Go,crates.io,Hex,Packagist,RubyGems,Chainguard,Wolfi. Audit-DB buckets (PYSEC-*/RUSTSEC-*/GO-*/RSEC-*) are attributed to their native source DB at map time viaOsvSourceAttribution, so they need no separate ecosystem entry. - Conditional fetch. Each ecosystem archive request sends
If-None-Match(cached ETag) andIf-Modified-Since(cachedLast-Modified); a304 Not Modifiedshort-circuits with zero new documents. A per-ecosystem resume window (cursorlastModified±ModifiedTolerance, default 10 min; first run looks backInitialBackfill, default 14 days) skips records already seen. - Memory-bounded download. Large archives (e.g.
npm/all.zip) are streamed to aDeleteOnClosetemp file before opening theZipArchive, becauseZipArchiveneeds a seekable stream and buffering into memory OOMs the Concelier container. SeeDownloadResponseToTempFileAsync/SPRINT_20260505_030_Concelier_osv_streaming_zip.md. - Failure handling. On a fetch exception for any ecosystem, the connector records a failure via the source-state repository with a 10-minute retry backoff and re-throws (the whole fetch run fails). Individual records that fail to deserialize are skipped/quarantined (document status
Failed) without aborting the run. - Capacity cap. A single fetch run persists at most
MaxAdvisoriesPerFetchnew documents (default 250) across all ecosystems, so a large backlog drains over several runs.
Symptoms
- [ ] OSV feed sync failing or stale (
stella admin feeds status --source osv) - [ ] One of the
source:osv:fetch|parse|mapjob runs reportingFailed - [ ] Errors in Concelier logs:
OSV fetch failed for ecosystem {Ecosystem},Failed to parse OSV entry,Failed to deserialize OSV document, orOSV document {DocumentId} produced empty payload - [ ] OSV vulnerabilities missing or outdated for an ecosystem
- [ ] Connector metric
osv.map.canonical_metric_fallbacksspiking (severity-only advisories, not necessarily a failure — see Metrics)
The original draft referenced an alert
ConnectorOsvSyncFailedand a metricconnector_sync_failures_total{source="osv"}. Neither exists in source. The only OSV-specific metric isosv.map.canonical_metric_fallbacksunder the meterStellaOps.Concelier.Connector.Osv. Wire alerts off that counter or off the feeds-status / job-run state.
Impact
| Impact Type | Description |
|---|---|
| User-facing | Open source ecosystem vulnerabilities may be missed |
| Data integrity | Data becomes stale; no data loss (failures back off 10 min and retry; the per-ecosystem cursor only advances on processed records) |
| SLA impact | Vulnerability currency SLO violated for affected ecosystems |
Diagnosis
Job-trigger and job-run endpoints target the Concelier WebService and require a bearer token. See Authorization for the required scopes.
Quick checks
Check OSV feed sync status (CLI):
stella admin feeds status --source osvThis calls
GET /api/v1/admin/feeds/osv/statuson the backend. (If the command returns a transport/404 error, the server-side admin-feeds endpoint may not be wired in your build; fall back to the job-run checks below, which exercise the connector directly.)Check the OSV job definitions and recent runs:
curl -s -H "Authorization: Bearer $TOKEN" \ https://<concelier-host>/jobs/definitions/source:osv:fetch | jq curl -s -H "Authorization: Bearer $TOKEN" \ "https://<concelier-host>/jobs/definitions/source:osv:fetch/runs?limit=10" | jqRepeat for
source:osv:parseandsource:osv:map. Look forFailedoutcomes and error messages.Test OSV bucket connectivity from the Concelier host:
curl -s -o /dev/null -w '%{http_code}\n' \ 'https://osv-vulnerabilities.storage.googleapis.com/PyPI/all.zip'Also check: https://osv.dev/ for an upstream outage.
NOT IMPLEMENTED:
stella doctor --check check.connector.osv-health,stella connector test osv, andstella connector osv api-status. There is noconnectorCLI command group and no OSV Doctor check.stella doctor [run] --check <id>exists, but there is no OSV connector check id to pass it. To list the real checks:stella doctor list.
Deep diagnosis
Inspect Concelier service logs for the OSV source. Grep for the structured log messages emitted by the connector:
OSV fetch failed for ecosystem {Ecosystem}— transport/HTTP failure for that ecosystem’s archive (followed by a 10-minute failure backoff).Failed to parse OSV entry {Entry} for ecosystem {Ecosystem}— a single record in the archive failed to deserialize (skipped, not fatal).Failed to deserialize OSV document {DocumentId}/OSV document {DocumentId} produced empty payload— parse-stage quarantine (document markedFailed).OSV map checkpoint paused after {N} documents— map hit its time budget and will resume next run (informational, not an error).
The collected/diagnostic log view is available via
stella admin diagnostics logs --service <svc> --level error --tail 200.Check the OSV connector metric (meter
StellaOps.Concelier.Connector.Osv, exported via OTLP/Prometheus — see Metrics):osv.map.canonical_metric_fallbacksrising → OSV is publishing severity-only / CVSS v4-only advisories that hit the canonical-metric fallback path. Usually benign; alert only on large spikes (see the connector ops doc thresholds).
Confirm the active base URI and ecosystem list. The connector binds
concelier:sources:osvtoOsvOptions. If an operator overrodeBaseUri(e.g. to a private mirror) or trimmedEcosystems, a “missing ecosystem” symptom is configuration, not an outage. Inspect the effective configuration (Console Admin → Advisory Sources, or the configuration file underconcelier:sources:osv).
Resolution
Immediate mitigation
Force a fetch/parse/map cycle (e.g. after a transient outage clears). The supported, code-backed trigger is the Concelier
db fetchCLI, which queues thesource:osv:<stage>jobs:stella db fetch --source osv --stage fetch # then, after fetch completes: stella db fetch --source osv --stage parse stella db fetch --source osv --stage mapEquivalent raw job triggers:
curl -s -X POST -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"trigger":"runbook"}' \ https://<concelier-host>/jobs/source:osv:fetchResponses:
202 Accepted(queued,Location: /jobs/{runId}),404(job not registered),423 Locked(job disabled),409 Conflict(a run already active).The
stella admin feeds refresh --source osv [--force]CLI also exists and posts to the backend admin-feeds refresh endpoint; preferdb fetchwhen you need stage-level control.NOT IMPLEMENTED:
stella admin feeds refresh --source osv --ecosystem npm. Therefreshcommand accepts only--sourceand--force— there is no--ecosystemoption. Fetch always iterates the full configured ecosystem list (capped byMaxAdvisoriesPerFetch). Also NOT IMPLEMENTED:stella connector osv sync-from-gcs(there is no separate GCS sync — the normal fetch is the GCS download).If you see persistent
304 Not Modifiedwith stale data, the cached ETag /Last-Modifiedis at the head of the archive — normal when there are genuinely no new modifications. Only widen recovery (increaseInitialBackfill) if the cursor was reset / data was lost.Air-gap / disaster recovery: load from an offline kit. The supported offline path is the offline-kit import, not a per-source loader. The kit is passed via
--bundle(the bundle is a.tar.zstpayload); the manifest defaults tomanifest.jsonnext to the bundle:stella offline import --bundle <kit-bundle.tar.zst> [--manifest <manifest.json>] [--dry-run]By default
importverifies the DSSE signature and Rekor receipt (--verify-dsse/--verify-rekor, both on by default);--dry-runvalidates the kit without activating it. Check current kit state withstella offline status.NOT IMPLEMENTED:
stella offline load --source osv --package <file>, andstella offline import <kit-file>as a positional argument. There is nooffline loadcommand and no--source/--packageoptions; the command isstella offline importand it takes the kit via the required--bundle(-b) option, not a positional path. The onlyofflinesubcommands areimportandstatus.
Root cause fix
If the bucket / endpoint is unreachable or you need a private mirror:
Override the base URI via the advisory-source configuration (concelier:sources:osv); the connector re-binds OsvOptions on resolution. Example configuration:
concelier:
sources:
osv:
baseUri: "https://<your-osv-mirror>/" # must be an absolute URI
archiveFileName: "all.zip" # per-ecosystem archive name
ecosystems: ["PyPI", "npm", "Maven", "Go", "crates.io",
"Hex", "Packagist", "RubyGems", "Chainguard", "Wolfi"]
maxAdvisoriesPerFetch: 250
initialBackfill: "14.00:00:00" # look-back on first/empty run
modifiedTolerance: "00:10:00"
requestDelay: "00:00:00.250"
httpTimeout: "00:03:00"
mapRunTimeBudget: "00:15:00"
NOT IMPLEMENTED:
osv.proxy,osv.gcs_auth,osv.gcs_credentials,osv.ecosystems.disabled, andsync_intervalconfig keys referenced in older drafts.OsvOptionsexposes onlyBaseUri,Ecosystems,InitialBackfill,ModifiedTolerance,MaxAdvisoriesPerFetch,ArchiveFileName,RequestDelay,HttpTimeout, andMapRunTimeBudget. The bucket is anonymous/public; there is no GCS authentication. Egress proxy is a deployment concern (container env /HTTPS_PROXY), not an OSV connector config key.
If a specific ecosystem is failing:
Temporarily remove the offending ecosystem from concelier:sources:osv:ecosystems (the list must keep at least one entry, or OsvOptions.Validate() throws). There is no stella connector config set osv.ecosystems.disabled command and no stella connector osv ecosystem-check command — edit the configuration and re-resolve.
If parsing / deserialization failures spike:
A spike in parse-stage Failed documents usually means an upstream OSV schema change. The connector tolerates per-record parse failures (skips them); a systemic change requires updating the DTO / mapper (OsvVulnerabilityDto, OsvMapper) in code and redeploying. There is no stella connector osv schema-check and no stella upgrade --component connector-osv command.
Verification
# Re-trigger the pipeline (see Immediate mitigation):
stella db fetch --source osv --stage fetch
stella db fetch --source osv --stage parse
stella db fetch --source osv --stage map
# Poll the most recent run state:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://<concelier-host>/jobs/definitions/source:osv:map/runs?limit=1" | jq '.[0].status'
# Confirm feed status recovered:
stella admin feeds status --source osv
# Spot-check a recent OSV advisory via the vuln observations API:
stella vuln observations --tenant <tenant> --alias OSV-2026-xxxx --json
# Confirm no OSV failure log lines in the last hour (service log search).
NOT IMPLEMENTED:
stella vuln query OSV-2026-xxxxandstella admin feeds status --source osv --watch. Thevulncommand exposesobservations(filter by--alias/--purl/etc.), notquery, andfeeds statushas no--watchflag (--watchexists onstella doctor).
Prevention
- [ ] Monitoring: Alert on
source:osv:*job runs reportingFailed, on stalestella admin feeds status --source osv, and on large spikes ofosv.map.canonical_metric_fallbacks. - [ ] Redundancy: Run overlapping connectors — NVD and GHSA provide overlapping coverage for major ecosystems (GHSA takes merge precedence over OSV for shared severity data).
- [ ] Offline: Maintain a periodic offline kit for air-gap / disaster recovery.
- [ ] Capacity: If a large backfill is draining slowly, raise
maxAdvisoriesPerFetch(default 250) so each run persists more documents.
Authorization
| Operation | Endpoint | Required scope (claim) |
|---|---|---|
Trigger OSV jobs (stella db fetch) | POST /jobs/source:osv:{fetch,parse,map} | concelier.jobs.trigger (policy Concelier.Jobs.Trigger) |
| List jobs / definitions / runs | GET /jobs, GET /jobs/definitions/{kind}, GET /jobs/definitions/{kind}/runs, GET /jobs/{runId}, GET /jobs/active | concelier.jobs.read or concelier.jobs.trigger (policy Concelier.Jobs.Read; trigger implies read) |
Run canonical merge (stella db merge) | POST /jobs/merge:reconcile | concelier.jobs.trigger (policy Concelier.Jobs.Trigger) — not concelier.merge (see note) |
Read advisory observations (stella vuln observations) | GET /concelier/observations, GET /advisories/observations | vuln:view (policy Concelier.Observations.Read), tenant-scoped |
Corrected (2026-05-30): Earlier drafts claimed
stella db mergerequired theconcelier.mergescope and that observations neededadvisory:readatGET /api/v1/vuln/observations*. Verified againstsrc/Concelier/StellaOps.Concelier.WebService/Program.cs:
- Merge is dispatched through the same catch-all job-trigger endpoint (
POST /jobs/{*jobKind}withjobKind=merge:reconcile), which is guarded by theConcelier.Jobs.Triggerpolicy — i.e. the required scope isconcelier.jobs.trigger, the same as every other job trigger. TheStellaOpsScopes.ConcelierMergeconstant ("concelier.merge") exists in the catalog but is not wired to any Concelier endpoint — it does not gate the merge job.- Observations are served by Concelier at
GET /concelier/observationsandGET /advisories/observations(the CLIstella vuln observationscalls the former; seeConcelierObservationsClient.BuildRequestUri). Both are guarded by theConcelier.Observations.Readpolicy, which is registered with scopeStellaOpsScopes.VulnView("vuln:view") — notadvisory:read. There is no/api/v1/vuln/observations*route on Concelier.
Scope constants live in src/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs (ConcelierJobsRead = "concelier.jobs.read", ConcelierJobsTrigger = "concelier.jobs.trigger", VulnView = "vuln:view", AdvisoryRead = "advisory:read"; ConcelierMerge = "concelier.merge" exists but is unused by Concelier endpoints).
Metrics
Meter: StellaOps.Concelier.Connector.Osv (version 1.0.0), exported via OTLP/Prometheus.
| Metric | Type | Meaning |
|---|---|---|
osv.map.canonical_metric_fallbacks | counter (advisories) | Incremented at map time when a mapped advisory has empty CVSS metrics but still has a non-empty canonical metric id (OsvMapper.BuildSeverityCanonicalMetricId). The id has the form <resolved-source>:severity/<severity>, where <resolved-source> is the per-ecosystem alias the record was attributed to — e.g. npm:severity/HIGH, pypi:severity/..., rustsec:severity/..., or osv:severity/... when the ecosystem is unmapped (not always literally osv:). Tags: canonical_metric_id, severity (falls back to unknown when the advisory has no severity), ecosystem, reason=no_cvss. |
This is the only OSV-connector-specific metric. The
connector_sync_failures_total{source="osv"}metric in the original draft does not exist. For failure signal use thesource:osv:*job-run outcomes and feed status.
See docs/modules/concelier/operations/connectors/osv.md for canonical-metric fallback dashboard/alert guidance (baseline volume is currently <5/day; alert when the 1-hour sum exceeds 50 for any ecosystem).
CLI surface
| Command | Purpose | Status |
|---|---|---|
stella db fetch --source osv --stage {fetch,parse,map} [--mode <m>] | Trigger a connector stage (queues source:osv:<stage>) | Implemented |
stella db merge | Run canonical merge reconciliation | Implemented |
stella admin feeds status --source osv | Show OSV feed sync status (GET /api/v1/admin/feeds/osv/status) | CLI implemented (server endpoint may be a gap in some builds) |
stella admin feeds refresh --source osv [--force] | Trigger an OSV feed refresh | CLI implemented |
stella admin feeds list | history --source osv | List feeds / show sync history | CLI implemented |
stella sources check osv [--json] | Check connectivity to an advisory source | Implemented |
stella sources status / list / enable osv / disable osv | Source configuration management | Implemented |
stella vuln observations --tenant <t> --alias OSV-... | Inspect raw advisory observations (calls GET /concelier/observations, scope vuln:view) | Implemented |
stella offline import --bundle <kit-bundle.tar.zst> [--manifest <m>] [--dry-run] | Import an offline kit (required --bundle; no positional path) | Implemented |
stella offline status | Show current offline kit status | Implemented |
NOT IMPLEMENTED (invented in the earlier draft):
stella connector test osv,stella connector osv ecosystems status,stella connector osv api-status,stella connector osv gcs-status,stella connector osv sync-from-gcs,stella connector osv api-test,stella connector osv gcs-test,stella connector osv ecosystem-check,stella connector osv schema-check,stella connector logs osv,stella connector config set osv.*,stella offline load --source osv,stella vuln query, andstella upgrade --component connector-osv. There is noconnectorcommand group, noupgradecommand, and no per-source offline loader.
Related Resources
- Connector code:
src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/ - Job registration / cron:
src/Concelier/__Libraries/StellaOps.Concelier.Connector.Osv/OsvDependencyInjectionRoutine.cs - Architecture:
docs/modules/concelier/connectors.md - Connector ops doc:
docs/modules/concelier/operations/connectors/osv.md - Related runbooks:
connector-nvd.md,connector-ghsa.md - OSV upstream docs: https://osv.dev/docs/
- Dashboard: Grafana > Stella Ops > Feed Connectors
