ghsa-git-processor
Status: Live Source: github/advisory-database (git) Type:
git(runtime clone/pull via pure-Go go-git — no baked data stage) Source slug:githubSchedule: Runs daily at 04:30 UTC (cron(30 4 * * ? *)). CPU 512 / memory 2048 MB /expected_duration_minutes = 60.
Overview
The GitHub Advisory Database is the reviewed, ecosystem-keyed advisory corpus
behind Dependabot. It is the single richest source of package-level affected
ranges (npm, PyPI, Maven, Go, RubyGems, NuGet, Packagist, crates.io, Pub, Hex,
Composer, Swift, GitHub Actions) and it is where a large fraction of
non-CVE-numbered ecosystem vulnerabilities live. Losing this feed would leave
lockfile matching dependent on OSV mirrors and would break every GHSA-*
lookup, the GHSA PoC enrichment pass, and the alias bridge between GHSA and CVE
namespaces.
Note on the source slug. This processor writes
source='github', not'ghsa'. Rows that appear undersource='ghsa'inCVEMetadataare alias-shell placeholders minted by theCVEAliasforeign key when some other processor links a GHSA alias — they are not written here.
Each run:
- Refreshes the repo.
processor.PullOrCloneclones (or pulls)/data/advisory-databaseat depth 1. The final image stage isscratchand carries no baked advisory data, so the clone runs through go-git. - Short-circuits on an unchanged HEAD against
BulkDataDumpTracker.sha256for sourceghsa_advisory; a match without--forcereportstask.noworkand exits 0.--start-batchalso bypasses this check (an operator resuming an interrupted run). - Narrows to changed files with
processor.DetectChangedFiles(3-day window) when a tracker exists.--force, a first run, or--start-batchforces a full scan. - Walks
advisories/recursively forGHSA-*.json(bothreviewed/andunreviewed/trees), sorted deterministically so--start-batchis stable. - Maps and stores in transactions of
--batch-size(default 200), oneSAVEPOINTper file.
The mapper override is osv.MapAdvisoryKeepID — the GHSA-xxxx-xxxx-xxxx ID is
always the cveId, and any CVE-* alias is written as a CVEAlias edge rather
than absorbing the record into a CVE row. Advisories that OSV flags as
malicious-package records (including the ossf/malicious-packages mirrors GitHub
republishes) are skipped — oss-malware-git-processor owns those.
Records produced
| Condition | Records |
|---|---|
| Every stored advisory | CVEMetadata (source="github", cveId = GHSA-…, sourceFileHash = file SHA1), CVEDescription (summary + details), CVEMetadataReferences (referenceSource="GitHub"), CVEProblemType (upstream cwe_ids), CVEAffected + CVEAffectedVersion (per ecosystem, with introduced/fixed/last_affected ranges expanded) |
| Advisory carries a CVSS vector | CVEMetric per severity entry (top-level and per-affected, deduplicated; Ubuntu-typed severities skipped) |
| Advisory carries descriptions | one derived CVEMetric (containerType="vulnetix", cvssV4_0) |
| Always | CVEAlias edges via db.InsertAliases — CVE aliases, related[], upstream[], plus same-cveId cross-source edges |
| Every affected package | Dependency, DependencyRegistry, PackageVersion, PackageVersionCVE (+ GitHubRepoDependency when a GitHub repo is resolvable) via db.EnrichAffectedWithDependency |
| Clean run | BulkDataDumpTracker row ghsa_advisory |
Resume
Three layers, because the repo is ~350k files:
- Repo-level — the
ghsa_advisorytracker SHA. - Per-file —
db.LoadProcessedHashes("github")builds a{cveId → sourceFileHash}skip set from the read replica. On production this is the largest such scan in the fleet and has a 5-minute budget; on timeout the run logsfailed to load resume hashes, processing all filesand re-upserts. - Batch-level —
--start-batch Nresumes from the Nth batch of the deterministically sorted path list, bypassing the tracker SHA check.
The tracker write is skipped when the run context was cancelled mid-way, so an overtime run re-scans next time.
Throughput — plan a cold load in advance
Measured on a production backfill (2026-08-06): ~1.1 s per advisory, i.e. 3.8 minutes per 200-file batch, 1,774 batches, ~112 hours for a full pass over 354,736 advisories.
The cost is serial per-record round trips to the RDS write proxy, not CPU. Per
stored advisory the store path issues, in order: the CVEMetadata upsert plus six
statements for its datePublished cross-source self-heal, an UpdateSourceFileHash,
one multi-row insert each for descriptions / references / metrics / problem types,
then — per affected entry — an affected upsert, a versions insert, and three
single-row upserts (Dependency, PackageVersion, PackageVersionCVE) for each
version boundary, plus two savepoint statements. An advisory spanning several
ecosystems with many introduced/fixed pairs therefore costs tens of round trips.
Practical consequences:
- With
expected_duration_minutes = 60the soft deadline is 50 minutes ≈ 13 batches. A scheduled run cannot cold-load the catalogue; it only keeps up once the per-file resume skip set is healthy (a skip costs a file read and a SHA1, no round trip). - A cold or forced load should be driven manually in windows with
--start-batch, not left to the schedule. - If
db.LoadProcessedHashesfails, the run logsfailed to load resume hashes, processing all files, continues with an empty skip set, and re-upserts everything — turning a routine daily run into the 112-hour path. Check that line before concluding a run is merely slow. The known cause is a missing or invalidCVEMetadata(source, "sourceFileHash")index in the target database.
Alerting
Individual file failures in a 350k-file repo are expected, so this processor does
not fail on the first error. After the walk it computes
successRate = 1 − errored/processed and pages (task.errored, exit 1) only
below 95 %; otherwise it reports task.completed with the rate attached.
AI enrichment
Wired to aienrich. Beyond the shared per-record passes (affected routines,
ATT&CK, CWE inference, TreeSitter), GHSA is the only source that triggers the
GHSA-specific PoC pass (RunGHSAPoC, its own 180 s per-record budget), which is
gated internally on this source slug. Inference is time-boxed against
EXPECTED_DURATION_MINUTES, so on a large catch-up run not every stored record
is enriched — unreached records keep their CVE row and are simply un-enriched.
Local just runs are inference-free unless AIENRICH=true.
Failure modes
| Symptom | Cause |
|---|---|
repository unchanged, skipping processing | tracker SHA equals HEAD |
failed to load resume hashes, processing all files | read-replica scan of the largest source in the fleet exceeded 5 minutes; the run still completes but re-upserts |
soft deadline reached, stopping early | EXPECTED_DURATION_MINUTES − 10 elapsed; use --start-batch to continue where it stopped |
success rate … below 95% threshold | systemic parse/store failure — the run exits 1 and pages |
Flags
| Flag | Default | Meaning |
|---|---|---|
--force | false | Ignore the tracker SHA and the per-file hash skip set |
--batch-size | 200 | Files per transaction |
--repo | /data/advisory-database | Clone location |
--data-dir | (repo + /advisories) | Override the advisory directory |
--start-batch | 0 | Resume from this 1-based batch, bypassing the tracker SHA check |
Local run: just go-ghsa-git-backfill (append prod for production; pass
START_BATCH= to resume).
S3 Persistence
- Archive path:
github/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/ghsa-git-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
parse-error,store-error
Uses s3client.Uploader from internal/s3client/uploader.go. Skipped when S3_BUCKET_NAME is unset (local dev).
See the S3 Persistence Contract for the full reason taxonomy.