Runbook: Feed Connector - GitHub Security Advisories (GHSA) Failures
Purpose: recover the Concelier GHSA feed connector when GitHub advisory ingestion is failing or stale — typically GitHub API rate limiting, an expired token, malformed documents, or a stuck window cursor. Audience: Platform on-call engineers responding to a stale GitHub-ecosystem advisory feed.
Metadata
| Field | Value |
|---|---|
| Component | Concelier / GHSA Connector (StellaOps.Concelier.Connector.Ghsa) |
| Source id | ghsa (job kinds source:ghsa:fetch, source:ghsa:parse, source:ghsa:map) |
| Severity | High |
| On-call scope | Platform team |
| Last updated | 2026-05-30 |
Reconciliation note (2026-05-31): This runbook was reconciled against
src/Concelier/__Libraries/StellaOps.Concelier.Connector.Ghsa/and the CLI command tree insrc/Cli/StellaOps.Cli/. Several commands in the original draft (stella doctor --check ...,stella connector ...,stella offline load,stella upgrade,stella vuln query) do not exist; they have been replaced with the realstella db fetch,stella sources, andstella admin feedscommands or flagged as NOT IMPLEMENTED below. The metric names and alert names in the original draft were not found in source; the connector emits theghsa.*OpenTelemetry instruments documented under Telemetry.A previous reconciliation pass referenced
stella db connectors status/test/configure. Those subcommands exist only as unwired dead code (DbCommandGroup.BuildDbCommandis never added to the CLI root inCommandFactory.cs); the livestella dbcommand (BuildDatabaseCommand) exposes onlyfetch,merge, andexport. This pass replaces those references with the registeredstella sourcesandstella admin feedscommands, and points connector configuration at the Console / the/api/v1/advisory-sources/{id}/configurationAPI /concelier:sources:ghsaconfig keys.
Symptoms
- [ ] GHSA feed sync failing or stale (
source:ghsa:fetchjob erroring or not advancing its cursor) - [ ] GitHub returns
403 Forbidden/429 Too Many Requests(“rate limit exceeded”) - [ ] GitHub Advisory Database vulnerabilities missing
- [ ]
ghsa.ratelimit.exhaustedcounter increasing, orghsa.fetch.failuresincreasing - [ ]
ghsa.ratelimit.headroom_pct_currentgauge near zero
Note: The GHSA connector fetches from the GitHub REST advisories API (
GET advisories?..., media typeapplication/vnd.github+json). It does not use the GitHub GraphQL API for ingestion — GraphQL is only used by the per-connector credential dry-run probe (theviewerquery). Symptoms framed as “GraphQL query failed” in older runbooks do not apply to the fetch path.
Impact
| Impact Type | Description |
|---|---|
| User-facing | GitHub ecosystem vulnerabilities may be missed |
| Data integrity | Data becomes stale; no data loss (cursor preserves partial progress) |
| SLA impact | Vulnerability currency SLO violated for GitHub-ecosystem packages |
Built-in resilience: When a GitHub REST list fetch is rate limited, the connector automatically attempts a public OSV GHSA fallback (downloading
modified_id.csvand the matchingGHSA-*.jsondocuments fromhttps://osv-vulnerabilities.storage.googleapis.com/) so the window can still be ingested. This is controlled by theuseOsvFallbackoption (defaulttrue). The fallback is best-effort and caps documents per fetch viamaxOsvFallbackDocumentsPerFetch(default 25).
Diagnosis
Quick checks
Show advisory source configuration/status (all sources):
stella sources status # Optional: --jsonShow GHSA feed sync status:
stella admin feeds status --source ghsaTest GitHub connectivity for the GHSA connector:
stella sources check ghsa # Optional: --timeout 30 --json --auto-disablesources check <source>runs a real connectivity probe via the source registry (ISourceRegistry.CheckConnectivityAsync) and reports reachability. (The unwiredstella db connectors testis dead code and, even if reached, only simulates a probe — do not rely on it.)
Deep diagnosis
Review recent sync history for the source:
stella admin feeds history --source ghsa --limit 20Look for repeated
errorrows; apartialstatus means a window was only partly ingested (often a mid-window rate-limit deferral).Check rate-limit telemetry. The connector records GitHub
X-RateLimit-*headers on every fetch as theghsa.ratelimit.*instruments (see Telemetry). In your metrics backend:ghsa.ratelimit.remainingnear zero, orghsa.ratelimit.headroom_pct_currentbelow ~10%, orghsa.ratelimit.exhaustedincrementing indicates throttling. A warning is logged once whenremainingdrops at or belowrateLimitWarningThreshold(default 500).
Check service logs for the connector. Log lines are emitted by
GhsaConnector(StellaOps.Concelier.Connector.Ghsa). Look for:"GHSA list fetch was rate limited; attempting public OSV GHSA fallback""GHSA GitHub API rate limit deferred the window""GHSA rate limit exhausted for {Phase} {Resource}; delaying {Delay}""Malformed GHSA JSON for {DocumentId}"(parse quarantine)
Filter the Concelier service logs by the
StellaOps.Concelier.Connector.Ghsacategory through your normal log aggregation; there is nostella connector logsCLI command.Check for a GitHub outage at https://www.githubstatus.com/. (There is no
stella connector ghsa api-statuscommand.)
Resolution
Immediate mitigation
If rate limited, let the next scheduled run resume, or trigger a refresh after the reset window:
stella admin feeds refresh --source ghsaThe connector preserves already-fetched document ids and the current window in its cursor, so a re-run resumes rather than restarting. Detail-fetch throttling after a list page was read leaves pending docs for the next
source:ghsa:parse/source:ghsa:mapjobs.Trigger a specific stage manually (fetch, then parse, then map):
stella db fetch --source ghsa --stage fetch stella db fetch --source ghsa --stage parse stella db fetch --source ghsa --stage mapRely on the OSV fallback. If
useOsvFallbackis enabled (default), a rate-limited list fetch already pulls the affected window from the public OSV GHSA mirror automatically — confirm via the log line in step 3 above. There is no separatestella offline loadcommand for this path.
Root cause fix
If rate limit is consistently exceeded:
Configuration is persisted per-source (key path concelier:sources:ghsa) and edited via the Console (Configure Sources) or the /api/v1/advisory-sources/{id}/configuration API (PUT with { "values": { ... } }). There is no wired stella db connectors configure CLI command — that subcommand exists only as unreferenced code. Reduce request pressure by editing the persisted config or the compatibility concelier.yaml:
# concelier.yaml (compatibility fallback for the concelier:sources:ghsa keys)
concelier:
sources:
ghsa:
apiToken: "github_pat_xxx" # optional; raises GitHub's per-hour quota
pageSize: 10 # 1..100, default 10
maxPagesPerFetch: 1 # default 1
requestDelay: "00:00:00.500" # default 200ms (TimeSpan)
rateLimitWarningThreshold: 500
secondaryRateLimitBackoff: "00:02:00"
useOsvFallback: true # default true
maxOsvFallbackDocumentsPerFetch: 25 # default 25
The connector does not expose a
sync_intervalorincremental_syncconfig key; scheduling is fixed by the job cron (see Default schedule). Pacing is controlled bypageSize,maxPagesPerFetch,requestDelay, and the backoff options above. Authenticated requests use the persistedapiToken(sent asAuthorization: Bearer <token>withX-GitHub-Api-Version: 2022-11-28).
If a token is expired or invalid:
Generate a new GitHub PAT (fine-grained or classic) at https://github.com/settings/personal-access-tokens.
Update the persisted token via the Console (Configure Sources) or the configuration API:
PUT /api/v1/advisory-sources/ghsa/configuration { "values": { "apiToken": "<new-token>" } }(Or set
concelier:sources:ghsa:apiTokenin config.)Verify the credential is accepted. If the token is registered as a connector credential via Platform, the read-only dry-run endpoint exercises the auth path (a GitHub GraphQL
viewerprobe; requiresread:user):POST /api/v1/connectors/ghsa/dry-run-credential { "credentialId": "<uuid>" }(Requires scope
concelier:credentials:dry-run, policyConcelier.Credentials.DryRun. Returns 501 for connectors without a dry-run handler — GHSA has one.) Otherwise re-runstella sources check ghsa.
The token is optional: GitHub serves public global advisories anonymously, so the connector validates without a token. Configure one only to raise rate limits, route through enterprise GitHub, or satisfy an operator policy. There is no enforced GitHub scope list for the REST advisories path; the GraphQL dry-run probe needs
read:userforviewer.
If documents are quarantined (malformed JSON):
Quarantines surface as ghsa.parse.quarantine increments and "Malformed GHSA JSON for {DocumentId}" log lines; the affected document is marked Failed and skipped. Re-fetch the window after confirming upstream content is valid:
stella admin feeds refresh --source ghsa
If the window cursor appears stuck:
There is no stella connector ghsa reset-cursor command. The cursor is stored in Concelier source state and rolls forward automatically once a window completes without hasMore/rate-limit deferral. To force re-ingestion of a window, re-run the fetch stage; for a stuck source escalate to the Platform team to inspect/reset the persisted ghsa source state.
Verification
# Trigger a refresh
stella admin feeds refresh --source ghsa
# Re-check feed status and source health
stella admin feeds status --source ghsa
stella sources status
# Confirm recent sync history shows success
stella admin feeds history --source ghsa --limit 10
Looking up an individual advisory by GHSA id uses the vulnerability explorer, not a
stella vuln querycommand:stella vuln observations --tenant <tenant> --alias GHSA-xxxx-xxxx-xxxx
Telemetry
The connector emits metrics under the meter StellaOps.Concelier.Connector.Ghsa (defined in Internal/GhsaDiagnostics.cs):
| Instrument | Type | Notes |
|---|---|---|
ghsa.fetch.attempts | counter | List/detail fetch attempts |
ghsa.fetch.documents | counter | Documents persisted for parse/map |
ghsa.fetch.failures | counter | Fetch failures (incl. rate-limit exceptions) |
ghsa.fetch.unchanged | counter | 304 Not Modified responses |
ghsa.parse.success / ghsa.parse.failures / ghsa.parse.quarantine | counter | Parse outcomes |
ghsa.map.success | counter | Advisories mapped |
ghsa.map.canonical_metric_fallbacks | counter | CVSS-less advisories assigned a ghsa:severity/<level> canonical metric |
ghsa.ratelimit.limit / ghsa.ratelimit.remaining | histogram | Reported quota / remaining (tags: phase, resource) |
ghsa.ratelimit.reset_seconds | histogram | Seconds until reset |
ghsa.ratelimit.headroom_pct | histogram | Quota headroom % |
ghsa.ratelimit.headroom_pct_current | observable gauge | Latest headroom % per phase/resource |
ghsa.ratelimit.exhausted | counter | GitHub returned zero remaining quota |
Suggested alerts (define in your metrics backend — these are not shipped as named platform alerts):
ghsa.ratelimit.remainingbelowrateLimitWarningThresholdfor > 5 min.ghsa.ratelimit.headroom_pct_currentbelow 10% for longer than a reset window.increase(ghsa.ratelimit.exhausted[15m]) > 0.
Default job schedule
Defaults from GhsaDependencyInjectionRoutine:
| Job kind | Cron | Timeout | Lease |
|---|---|---|---|
source:ghsa:fetch | 1,11,21,31,41,51 * * * * | 6 min | 4 min |
source:ghsa:parse | 3,13,23,33,43,53 * * * * | 5 min | 4 min |
source:ghsa:map | 5,15,25,35,45,55 * * * * | 5 min | 4 min |
Authorization
| Action | Scope / policy |
|---|---|
Read advisories / canonical reads (policy Concelier.Advisories.Read) | advisory:read |
Manage / enable / sync / configure a source (policy Concelier.Sources.Manage) | integration:write or integration:operate (any-scope) |
Trigger Concelier jobs (policy Concelier.Jobs.Trigger) | configured Authority.RequiredScopes; documented scope constant concelier.jobs.trigger |
Connector credential dry-run (policy Concelier.Credentials.DryRun) | concelier:credentials:dry-run |
The policy → scope mappings are registered in
src/Concelier/StellaOps.Concelier.WebService/Program.cs(AddAuthorizationblock); scope constants live insrc/Authority/StellaOps.Authority/StellaOps.Auth.Abstractions/StellaOpsScopes.cs. There is noconcelier.sources.managescope —Concelier.Sources.Manageis an any-scope policy overintegration:write/integration:operate.
Prevention
- [ ] Authentication: Configure a GitHub token to raise the per-hour rate limit (optional but recommended for busy deployments).
- [ ] Monitoring: Alert on
ghsa.ratelimit.exhaustedand on stale sync history. - [ ] Redundancy: The built-in OSV GHSA fallback covers rate-limit windows automatically; keep
useOsvFallbackenabled. NVD/OSV connectors provide additional ecosystem coverage. - [ ] Token rotation: Rotate tokens before expiration; re-validate with
stella sources check ghsa.
Related Resources
- Connector architecture:
docs/modules/concelier/connectors.md - Connector operations (config keys, telemetry, throttling):
docs/modules/concelier/operations/connectors/ghsa.md - Related runbooks:
connector-nvd.md,connector-osv.md,connector-vendor-specific.md - GitHub REST advisories API: https://docs.github.com/en/rest/security-advisories
