OSV File Processor — Design Document
1. Overview
Purpose: Download OSV vulnerability data from Google Cloud Storage public buckets for every ecosystem OSV publishes, deduplicate by SHA256, and process records directly into CVEMetadata and related tables using the shared processing pipeline.
Data source: GCS public bucket — https://osv-vulnerabilities.storage.googleapis.com/<Ecosystem>/all.zip
Ecosystem discovery: The processor lists the GCS bucket at startup (?delimiter=/, with marker pagination) and processes every top-level prefix that has an all.zip. The list is not hardcoded, so any ecosystem OSV adds (e.g. MinimOS, Echo, SwiftURL, future additions) is picked up automatically on the next run. Filtered out:
- Versioned subprefixes (anything containing
:, e.g.Alpine:v3.10,Ubuntu:22.04:LTS). - Sentinel / non-ecosystem folders:
all,icons,[EMPTY],Root.
Ecosystems handled by a dedicated processor are not skipped. OSV is the canonical aggregator and we want every OSV record on disk regardless of whether another processor also writes the same advisory. Collision is impossible because every row in CVEMetadata is keyed by (cveId, source) and this processor always writes with source = "osv". A dedicated processor (e.g. chainguard-json-processor) writes its own (cveId, source="chainguard") row from the upstream feed; the OSV row and the dedicated row coexist and are linked through CVEAlias.
Schedule: Daily (cron 0 0 * * *)
Timeout: see ECS task definition (3 hour soft deadline; configurable via EXPECTED_DURATION_MINUTES)
Resources: 256 CPU units, 1024 MB memory
What it reads:
- GCS bucket listing (
/?delimiter=/) — for ecosystem discovery - GCS ZIP archives (one per ecosystem; cached to
/tmp/YYYYMMDD_{ecosystem-slug}.zipwithin the run) - Read replica:
OsvFileper-file SHA256 dedup check - Read replica:
BulkDataDumpTrackerper ecosystem for archive-level SHA256 dedup
What it writes:
CVEMetadata(one row per advisory;source = "osv"uniformly,cveId = adv.ID)CVEDescription,CVEMetadataReferences,CVEMetric,CVEProblemType,CVEAffected,CVEAffectedVersionCVEAlias(viadb.InsertAliasesfrom the central pipeline — see §2 Identifier & Alias Policy)OsvFile(per-file tracking record: status, r2Path, r2StoredAt, cveCount, advisoryCount)Artifact(one per file if S3 upload succeeded)Link(one per file if S3 upload succeeded, with artifactUuid FK)CVEMetadata.fileLinkId→ set to Link.id if availableBulkDataDumpTracker(one row per ecosystem:osv_pypi,osv_minimos,osv_github_actions, etc.)
Environment variables:
DATABASE_URL— requiredDATABASE_URL_READ— optionalS3_BUCKET_NAME— optional; if absent, S3 upload, Artifact, and Link records are skipped (local dev mode)EXPECTED_DURATION_MINUTES— optional; sets soft deadline (default 3 h)
2. Business Logic
Identifier & Alias Policy (CRITICAL)
OSV advisories carry an OSV-native id (e.g. MINI-jhg8-3g7g-q6mp, GHSA-xxxx-xxxx-xxxx, PYSEC-2024-1234, RUSTSEC-2024-0001) and may list aliases[] containing CVE-* identifiers. This processor never uses a CVE-* alias for CVEMetadata.cveId:
| Field | Value |
|---|---|
CVEMetadata.cveId | adv.ID — always the native OSV identifier, never a CVE-* alias |
CVEMetadata.source | "osv" — uniform across every ecosystem |
CVEAlias rows | Every adv.aliases[], adv.related[], adv.upstream[] entry — including any CVE-* values — is written to CVEAlias via db.InsertAliases |
Why: The CVE-* identifier (when present) is the cross-DB pivot, not a substitute identifier. If we used the CVE-* as cveId, the OSV record would either collide with a dedicated processor’s row or silently overwrite the native OSV identity. Keeping cveId = adv.ID preserves the OSV identity and makes the CVE-* available as a navigable alias edge.
db.InsertAliases handles cross-source linking automatically: when this processor writes (MINI-jhg8-3g7g-q6mp, source=osv) with alias CVE-2025-5889, it backfills edges to every other source that already carries CVE-2025-5889 (e.g. NIST NVD, GHSA, MITRE). Subsequent runs of any processor that writes the same cveId will likewise backfill an edge to the OSV row. This is what the project means by “the relations carry the canonical source”: the CVEMetadata.source is uniformly "osv" for OSV-written rows, while the alias edges (in CVEAlias.discoveredFrom / peer rows) carry the per-source canonical identity.
This is implemented by calling osv.MapAdvisoryKeepID (which sets mapped.CveID = adv.ID and propagates aliases[]/related[]/upstream[] to mapped.Aliases), and passing the result through processor.StoreCVESourceDataDeferred → db.InsertAliases.
/tmp Download Caching
At startup, files matching /tmp/????????_*.zip that do NOT start with today’s date (YYYYMMDD) are deleted to reclaim space. Each ecosystem ZIP is cached to /tmp/YYYYMMDD_{ecosystem-slug}.zip on first download and reused for the rest of the run. Ecosystem names with spaces or punctuation are slugified (GitHub Actions → github_actions, Azure Linux → azure_linux).
Per-Ecosystem Processing
Ecosystems are processed sequentially. Each ecosystem is independent — failure in one does not stop others. A soft deadline (default 3 h) bounds the total run; if exceeded, the loop stops cleanly between ecosystems.
Two-Level SHA256 Dedup
- Archive level: Compute SHA256 of the downloaded ZIP bytes. Compare against
BulkDataDumpTracker.sha256forosv_<ecosystem-slug>. If unchanged → skip the entire ecosystem. - File level: For each file inside the ZIP, check
OsvFilefor(archiveSha256, filename)→ iffileSha256matches → skip this file.
Direct Processing (No Queue)
Each file is parsed with osv.ParseJSON, mapped with osv.MapAdvisoryKeepID, and stored directly via processor.StoreCVESourceDataDeferred — all within a database transaction with per-file SAVEPOINTs for rollback isolation. Deferred enrichment (Dependency / PackageVersion upserts) runs in independent short transactions after the main batch commits, to avoid cross-worker deadlocks.
Batch Processing
Files are split into batches of 200 and processed by 4 concurrent workers. Each batch is one transaction with per-file SAVEPOINTs. A 10-minute per-batch context bounds slow batches.
S3 Upload (Conditional)
If S3_BUCKET_NAME is set, each file is uploaded to osv/files/{fileSHA256}/{filename} before the DB transaction writes. S3 keys are content-addressed so re-uploads are idempotent if the DB write fails and the file is re-processed next run.
Tracker Source Key
osv_<slugified ecosystem> — e.g. osv_pypi, osv_minimos, osv_github_actions, osv_azure_linux, osv_bellsoft_hardened_containers.
URL Encoding
Ecosystem names with spaces (GitHub Actions, Azure Linux, BellSoft Hardened Containers) are URL-encoded for the GCS download path via url.PathEscape.
Malicious Package Detection
osv.MapAdvisoryKeepID returns nil for malicious packages (MAL- prefix on the primary id, on any alias/related/upstream, or ossf-malicious-packages source). These are silently skipped (no DB write). Malicious packages are handled by oss-malware-git-processor.
3. OsvFile Status Lifecycle
(new file) → processing → completed
↘ failed
(sha unchanged) → skipped (no DB write)
4. S3 Key Convention
osv/files/{fileSHA256}/{filename}
Example: osv/files/abc123.../MINI-jhg8-3g7g-q6mp.json
GCS source URL stored in the Link record:
https://osv-vulnerabilities.storage.googleapis.com/{Ecosystem}/{filename}
5. Local vs ECS Behaviour
| Behaviour | Local (no S3_BUCKET_NAME) | ECS (S3_BUCKET_NAME set) |
|---|---|---|
| Ecosystem discovery | ✅ same | ✅ same |
| /tmp caching | ✅ same | ✅ same |
| Past-day cleanup | ✅ same | ✅ same |
| Parse + MapAdvisoryKeepID | ✅ always | ✅ always |
| StoreCVESourceData | ✅ always | ✅ always |
| S3 upload | ⬜ skipped (warn) | ✅ performed |
| Artifact record | ⬜ skipped | ✅ created |
| Link record | ⬜ skipped | ✅ created |
| CVEMetadata.fileLinkId | null | set |
6. Architecture Diagram
• canonicalises edge direction
• cross-source backfill on cveId
• bundle suppression] ARTIFACT[artifact.go — InsertArtifact / InsertLinkWithArtifact / UpdateCVEMetadataFileLinkID] BATCH[batch.go — WithTx] end MAIN --> CLEANUP MAIN --> DISCOVER DISCOVER -->|GCS bucket listing| MAIN MAIN -->|for each discovered ecosystem| PROC_ECO PROC_ECO --> CACHE PROC_ECO --> TRACK PROC_ECO --> S3MOD PROC_ECO -->|batches of 200, 4 workers| PROC_BATCH PROC_BATCH --> PARSE PROC_BATCH --> MAP PROC_BATCH --> STORE STORE --> ALIAS PROC_BATCH --> ARTIFACT PROC_BATCH --> BATCH PROC_BATCH --> ENRICH
7. Deployment Diagram
go-ecr-deploy.yml] -->|push ARM64 image| ECR[ECR: vdb-manager
tag: go-osv-json-processor-latest] ECR --> TASKDEF[ECS Task Definition
go-osv-json-processor] TASKDEF --> EB[EventBridge Schedule
go-osv-json-processor
cron 0 0 * * ? *] EB -->|trigger| FARGATE[ECS Fargate Task
vdb-scheduler cluster
ARM64 ap-southeast-2] FARGATE --> CW[CloudWatch Logs
/ecs/vdb-scheduler/go-osv-json-processor] FARGATE -->|HTTPS GET bucket listing| GCS_LIST[osv-vulnerabilities GCS
/?delimiter=/] FARGATE -->|HTTPS GET N ZIPs| GCS[osv-vulnerabilities/
all non-versioned ecosystems] FARGATE -->|PutObject| S3[S3 Bucket
osv/files/{sha256}/{filename}] S3 --> WRITE FARGATE --> WRITE[RDS Write Proxy
CVEMetadata source=osv + CVEAlias + OsvFile + Artifact + Link + BulkDataDumpTracker] FARGATE --> READ[RDS Read Replica
OsvFile sha check + tracker read]
8. Processing Flow
GET GCS /?delimiter=/
filter sentinels and versioned subprefixes] DISCOVER -->|error| FAIL DISCOVER --> ECOSYSTEMS[For each discovered ecosystem] ECOSYSTEMS --> CACHE_CHECK{/tmp today ZIP exists?} CACHE_CHECK -->|yes| USE_CACHE[Use cached bytes] CACHE_CHECK -->|no| DOWNLOAD[GET gcsBase/EscapedEcosystem/all.zip → cache to /tmp] DOWNLOAD -->|error| ECO_ERR[count ecosystem error
continue to next] USE_CACHE --> SHA256 DOWNLOAD --> SHA256[sha256.Sum256 ZIP bytes] SHA256 --> TRACKER{osv_slug sha256
== zipSHA256?} TRACKER -->|yes| NEXT_ECO[Skip ecosystem — unchanged] TRACKER -->|no| S3_INIT{S3_BUCKET_NAME set?} S3_INIT -->|yes| BUILD_S3[buildS3Uploader] S3_INIT -->|no| OPEN_ZIP BUILD_S3 --> OPEN_ZIP[zip.NewReader from bytes] OPEN_ZIP -->|error| ECO_ERR OPEN_ZIP -->|ok| BATCH_FAN[Split into batches of 200] BATCH_FAN --> WORKERS[4 concurrent workers process batches] WORKERS --> UPD_TRACKER[UpsertTracker osv_slug
only if no error and ctx not cancelled] subgraph processBatch["processBatch (per file in TX)"] SP[SAVEPOINT sp_file] SKIP{OsvFile fileSha256
matches?} PARSE[osv.ParseJSON] MAPF[osv.MapAdvisoryKeepID
cveId=adv.ID source=osv] MALICIOUS{mapped == nil?} S3UP[s3Upload osv/files/sha/name] UPSERT_OSV[Upsert OsvFile status=processing] ART[InsertArtifact] LINK[InsertLinkWithArtifact] STORE[StoreCVESourceDataDeferred
→ writes CVEMetadata source=osv
→ writes CVEAlias for adv.aliases incl CVE-*] LINKID[UpdateCVEMetadataFileLinkID] DONE_OSV[UPDATE OsvFile status=completed] REL[RELEASE SAVEPOINT] SP --> SKIP SKIP -->|yes| REL SKIP -->|no| PARSE PARSE --> MAPF MAPF --> MALICIOUS MALICIOUS -->|yes| REL MALICIOUS -->|no| S3UP S3UP --> UPSERT_OSV UPSERT_OSV --> ART ART --> LINK LINK --> STORE STORE --> LINKID LINKID --> DONE_OSV DONE_OSV --> REL end WORKERS --> processBatch UPD_TRACKER --> NEXT_ECO NEXT_ECO --> ECOSYSTEMS ECOSYSTEMS -->|all done| SUMMARY[Log totals] SUMMARY --> ERRCHECK{totalErrors > 0?} ERRCHECK -->|yes| FAIL2([Exit 1]) ERRCHECK -->|no| DONE([Exit 0])
9. Data Mapping
NEVER a CVE-* alias" string source PK "osv (uniform for all ecosystems written by this processor)" int fileLinkId FK "→ Link.id" } CVEAlias { string cveId FK "OSV native id" string source FK "osv" string aliasCveId "every entry from adv.aliases[] / related[] / upstream[]
including CVE-* — used for cross-source linking" string aliasSource "peer source carrying the same aliasCveId
(backfilled by db.InsertAliases)" bigint discoveredAt "Unix ms" string discoveredFrom "osv" } OsvFile ||--o| Artifact : "r2Path" Artifact ||--|| Link : "artifactUuid" Link ||--o| CVEMetadata : "fileLinkId" CVEMetadata ||--o{ CVEAlias : "cveId,source"
10. AI Enrichment (aienrich)
Targets are collected inside the batch transaction (one per file that
clears RELEASE SAVEPOINT sp_file) into enrichTargets. After the batch
tx commits successfully and processor.RunDeferredEnrichment finishes,
aienrich.RunBatch(ctx, enrichTargets) fires once with the whole batch.
The slice is reset on every retry attempt so retries can’t double-count.
Wire-in: cmd/osv-json-processor/main.go, Phase 4 right before
processBatch returns.
Four passes fire per target (ghsapoc self-gates to GHSA-shaped ids,
which is rare for OSV but kept for GHSA:* records that flow through
this processor):
| Pass | Persists to |
|---|---|
vulnetix.affected | CVEAffected.{modules, programFiles, programRoutines} |
vulnetix.attack | CVEAttackTechnique + children |
vulnetix.cwe | CVEProblemType (descriptionType = "CWE", derivedBy = "vulnetix") |
vulnetix.treesitter | CVETreeSitterQuery + CVETreeSitterCapture + CVETreeSitterPredicate |
Target.Source = "osv" matches CVEMetadata.source. See the
aienrich overview for the
full activity contract.
S3 Persistence
Not used. This processor does not currently archive payloads or quarantine failures to S3. Per the S3 Persistence Contract this is non-compliant — see the compliance matrix for the implementation roadmap.
⚠ Not in the compliance matrix — status needs verification.
Expected paths when implemented:
- Archive:
osv-file/files/{sha256}/{filename} - Quarantine:
failed-feeds/osv-file-processor/{YYYY-MM-DD}/{reason}/{filename} - Likely reasons: (none documented)