gemnasium-git-processor
Status: Live Source: gitlab-org/security-products/gemnasium-db (git) Type:
git(runtime clone/pull via pure-Go go-git — no baked data stage) Source slugs:gitlab(provenance) plus the upstream source implied by each advisory identifier —cve.org,github,gms, and others Schedule: Runs daily at 03:00 UTC (cron(0 3 * * ? *)). CPU 256 / memory 512 MB /expected_duration_minutes = 45.
Overview
Gemnasium is the advisory database behind GitLab Dependency Scanning. It
independently curates affected ranges for npm, PyPI, Maven, Go, RubyGems, NuGet,
Packagist, Cargo, Pub, Conan and Swift, and it carries GMS-* advisories that
exist nowhere else. Its practical value here is range quality: Gemnasium
frequently states a tighter, GitLab-verified affected_range than the CVE record
does, and it is the second independent opinion the alias graph reconciles against
GHSA and NVD.
Each run:
- Refreshes the repo.
processor.PullOrCloneclones (or pulls)/data/gemnasium-dbat depth 1 through go-git — the final image stage isscratchand ships no baked advisory data.--no-pullreads the existing HEAD instead. - Short-circuits on an unchanged HEAD against
BulkDataDumpTracker.sha256for sourcegemnasium_advisory; a match without--forcereportstask.noworkand exits 0. - Loads the resume skip set with
db.LoadProcessedHashes("gitlab")(30-second budget — this is deliberately short; on timeout the run proceeds without resume and re-upserts). - Walks the repo root recursively for
.ymlfiles, skipping.gitlab-ci.yml, then applies the 3-day changed-file filter when a tracker exists. - Stores in transactions of
--batch-size(default 200) with oneSAVEPOINTper file; each batch runs underdb.WithBatchTxwith its own 5-minute context so an overtime cancel cannot poison an in-flight commit. - Attributes actors —
malwareactor.PostPassfor sourcegitlab.
The two-row write — read this before querying source='gitlab'
Gemnasium is a republisher, not an issuing authority, so the processor
deliberately splits each advisory across two CVEMetadata rows:
- Provenance row —
db.UpsertCVEMetadatawithsource='gitlab', carrying onlycveId,title,sourceAdvisoryRef,datePublished,dateUpdated,isMaliciousPackage,aliasesandsourceFileHash. No descriptions, metrics, affected rows or references hang off it. Its job is to record that gemnasium-db has this advisory and to carry the per-file resume hash. - Full row —
mapped.Sourceis then reassigned tosourceident.FromPrefix(adv.Identifier)and the record is stored throughprocessor.StoreCVESourceData. So aCVE-*advisory writes its full payload undersource='cve.org', aGHSA-*advisory undersource='github', aGMS-*advisory undersource='gms', and an unrecognised prefix falls back togitlab.
Consequences worth knowing:
count(*) WHERE source='gitlab'counts advisories gemnasium knows about, not rows with data. Mostgitlabrows have a nullaffectedProductby design.- The bulk of this processor’s
CVEDescription/CVEMetadataReferences/CVEAffectedoutput lands undersource='cve.org', where it shares rows withmitre-cve-json-processor. BecauseUpsertCVEMetadataoverwritestitle,vectorString,rawDataJSON,affectedVendor/affectedProductandstatefrom the incoming record, whichever of the two processors ran last owns those columns for a given CVE. - Reference rows written by this processor are identifiable by
referenceSource='GitLab'regardless of whichsourcerow they hang off.
datePublished for the provenance row is the mapper’s upstream date when
present, else derived from the CVE ID’s year (cveid.DateFromID) — never
time.Now(), so the UpsertCVEMetadata earliest-wins guard can preserve a
better value from another source.
Records produced
| Condition | Records |
|---|---|
| Every stored advisory | CVEMetadata provenance row (source="gitlab", sourceFileHash = file SHA1) and CVEMetadata full row under the identifier-derived source |
| Full row | CVEDescription, CVEMetadataReferences (referenceSource="GitLab"), CVEProblemType (cwe_ids), CVEAffected + CVEAffectedVersion from the parsed affected_range / fixed_versions |
cvss_v2 / cvss_v3 present | CVEMetric (cvssV2_0, cvssV3_0/cvssV3_1 auto-detected by prefix, deduplicated) |
| Descriptions present | one derived CVEMetric (containerType="vulnetix", cvssV4_0) |
| Always | CVEAlias edges via db.InsertAliases for identifier + identifiers[], plus same-cveId cross-source edges |
| Every affected package | Dependency, DependencyRegistry, PackageVersion, PackageVersionCVE via db.EnrichAffectedWithDependency |
| Malicious advisory (CWE-506, or “malware”/“malicious” in title/description) | the row is stored with isMaliciousPackage=true — not skipped — and becomes eligible for the actor post-pass |
| Post-pass, per attributed record | ThreatActor, ThreatActorKey, MalwareThreatActor, MalwareAttribution |
| End of run | BulkDataDumpTracker row gemnasium_advisory |
Parsing
Gemnasium YAML is not OSV, so internal/gemnasium has its own schema:
identifier, identifiers[], package_slug, affected_range,
fixed_versions, cvss_v2, cvss_v3, cwe_ids, urls.
ParsePackageSlugnormalisesecosystem/packageto the OSV ecosystem vocabulary:npm/lodash→("npm","lodash"),pypi/requests→("PyPI","requests"),maven/org.example/artifact→("Maven","org.example:artifact"),go/github.com/u/r→("Go","github.com/u/r").ParseAffectedRangeexpands constraint strings into version rows:>=X,<Y,>=X <Y,||-combined sub-ranges,=X, bare<X/<=X(lower bound assumed0), and Maven interval notation(,X]/[X,Y).ExtractCVEIDprefers aCVE-YYYY-NNNN+match inidentifier, then inidentifiers[], and falls back to the primaryidentifier— this is the value used both as the row key and for the resume lookup.
Threat-actor attribution
malwareactor.PostPass(source="gitlab") runs after the walk. It is
first-time-only (a (cveId, source) with a MalwareAttribution row is skipped)
and bounded to MALWARE_ACTOR_BATCH (default 150) records per run;
MALWARE_ACTORS=false disables it. It writes to the generic
MalwareThreatActor / MalwareAttribution / ThreatActor tables. Note the pass
selects on source='gitlab', i.e. the provenance rows.
Failure modes
| Symptom | Cause |
|---|---|
repository unchanged, skipping processing | tracker SHA equals HEAD |
failed to load resume hashes, processing all files | the 30-second read-replica budget expired; the run re-upserts every advisory |
soft deadline reached, stopping early | EXPECTED_DURATION_MINUTES − 10 elapsed. The tracker is still advanced, so files the run never reached wait for the next --force |
batch %d: transaction failed | the whole batch is counted errored and its successful files are quarantined as tx-rollback |
%d files errored + exit 1 | any errored file fails the run after the tracker has already advanced |
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/gemnasium-db | Clone location |
--data-dir | (repo root) | Override the advisory directory |
--no-pull | false | Use the existing clone’s HEAD instead of pulling |
Local run: just go-gemnasium-git-backfill (append prod for production).
S3 Persistence
- Archive path:
gitlab/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/gemnasium-git-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
store-error,tx-rollback
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.