gitlab-json-processor
Status: Live Source: gitlab.com/gitlab-org/cves — tarball
…/-/archive/master/cves-master.tar.gzType:json(single gzipped tar of CVE Record Format 5.0 files) Source slug:gitlab— shared withgemnasium-git-processor, see the warning below Schedule: Runs daily at 08:00 UTC (cron(0 8 * * ? *)), ECS command["/app/gitlab-json-processor", "--emit-crit=true"].
⚠
source='gitlab'has two writers, and this one owns the small share.gemnasium-git-processor(cmd/gemnasium-git-processor/main.go:32,source = "gitlab") writes the GitLab gemnasium-db advisory corpus under the same slug. Measured in production, of 39,191source='gitlab'rows:
dataVersionRows Writer Shape 1.037,935 gemnasium-git-processor UUID sourceAdvisoryRef, gemnasium YAML-derived payload (8,176 carryrawDataJSON)5.0/5.1/5.21,256 this processor sourceAdvisoryRef = cveId, CVE Record Format payloadConsequences:
- Any per-source field-coverage measurement of
gitlabmixes both writers. Split ondataVersion LIKE '5.%'to isolate this processor’s output. (rawDataJSON IS NOT NULLdoes not work — gemnasium populates it for 8,176 of its own rows.)db.LoadProcessedHashes("gitlab")(step 2 below) loads both writers'sourceFileHashvalues into one keyspace. For a CVE both processors know about, each run sees the other’s hash, the resume check misses, and the record is needlessly re-stored — and whichever runs last owns the row, becausedb.UpsertCVEMetadataassignsrawDataJSON,dataVersionandvectorStringunconditionally fromEXCLUDED(internal/db/cvemetadata.go:59,72). The last full run of this processor stored 1,288 records (BulkDataDumpTracker.gitlab.totalCVEs) yet only 1,256 rows still carry a5.xdataVersion— the difference is records gemnasium has since overwritten.The trackers are distinct (
gitlabvsgemnasium_advisory), so the freshness gates do not interfere. Fixing the collision means giving one writer its own slug.
1. Overview
- Purpose: ingest GitLab’s first-party CVE database (one CVE 5.0 JSON
per advisory, year-organised) and emit
CVEMetadatarows undersource="gitlab"plus optional CRIT candidates for the GitLab.com SaaS and self-managed (CE/EE) SKUs. - Source URL:
https://gitlab.com/gitlab-org/cves(public repo). Tarball:https://gitlab.com/gitlab-org/cves/-/archive/master/cves-master.tar.gz. - Owner: GitLab Security (CNA
assignerShortName="GitLab"). - Licence: repo licence (CC0/CC-BY — confirm at runtime; no scrape restrictions imposed).
- Schedule:
cron(0 8 * * ? *)daily 08:00 UTC. GitLab publishes security releases on Tuesdays; daily run keeps cadence within 24 h while avoiding the 06:00–07:00 slot already occupied by twilio/fastly. - ECS resources:
cpu_units=512,memory_mb=1024,expected_duration_minutes=30(per blueprint sizing for full-dataset JSON tarball download + ~1500 records). - Reads: GitLab archive endpoint (one HTTP fetch),
BulkDataDumpTracker.gitlabfor freshness,LoadProcessedHashesfor per-record resume. - Writes:
CVEMetadata,CVEDescription,CVEMetadataReferences,CVEMetric,CVEAffected,CVEAffectedVersion,CVEProblemType,CVEAlias,BulkDataDumpTracker,S3archive, optionalCritRecordviacrit-candidates/pending/.
2. Source contract (Phase 0 recon)
| Question | Answer | Evidence |
|---|---|---|
| Cadence | Daily-ish; cluster around Tuesdays | GitLab security release schedule (public) |
| Volume | ~1,290 records (2020–2026) | BulkDataDumpTracker.gitlab.totalCVEs = 1288 on the last full pass; 1,256 surviving rows match source='gitlab' AND dataVersion LIKE '5.%'. An earlier revision of this page claimed ~9,400 by counting rawDataJSON IS NOT NULL, which wrongly includes 8,176 gemnasium rows. |
| Identifier | Native CVE-YYYY-NNNN | cveMetadata.cveId field on every record; assignerShortName="GitLab" |
| Backfillability | Full archive in one tarball | https://gitlab.com/gitlab-org/cves/-/archive/master/cves-master.tar.gz returns 200 + gzip stream |
| Anti-bot | None | Public read, no auth, no Cloudflare challenge |
| Pagination | None | Tarball contains everything |
| Freshness signal | Tarball SHA256 | Computed at download; stored in BulkDataDumpTracker.sha256 |
| Schema shape | CVE 5.0 (cvelistv5) | Confirmed via raw fetch of 2024/CVE-2024-0199.json — has dataVersion: "5.0", cveMetadata, containers.cna |
The schema match means we can reuse internal/cvelistv5 for parsing and
storage end-to-end — no new mapper code.
3. Architecture
tarball] --> HTTP[httpclient.New] HTTP --> Walk[gitlab.WalkTarball
gzip+tar streaming] Walk --> Parse[cvelistv5.ParseRecord] Parse --> Tx[(pgx Tx)] Tx --> Store[cvelistv5.StoreRecordWithSource
source=gitlab] Store --> CVEMetadata Store --> CVEAlias[(db.InsertAliases)] Store --> Children[Description / Reference / Metric / Affected] Tx -.commit.-> S3[s3client.ArchiveRecord] Parse --> CRIT[mapGitLabToCRITs
SaaS + self-managed +
Runner / DepProxy / Registry] CRIT --> Stage[critutil.StageCandidate] Stage --> Drain[critpublisher.DrainKeys] Drain --> CritRecord
4. Source → DB field mapping
Handled entirely by cvelistv5.StoreRecordWithSource(ctx, tx, rec, fileHash, "gitlab", cveID, logger). The cvelistv5 mapper persists every
field of the CVE 5.0 record into the appropriate row (descriptions,
references, metrics, affected, problem-types). No bespoke mapping table
is required for this processor.
The only override versus default cvelistv5 ingestion: source="gitlab"
instead of the default "cvelistv5". This drives downstream
attribution and ensures GitLab’s CVE rows are distinguishable from
copies that may also exist in the broader cvelistv5 corpus.
5. Identifier policy
CVEMetadata.cveId: nativeCVE-YYYY-NNNNfromcveMetadata.cveId. GitLab is a CNA — every advisory has a CVE identifier; no minted IDs needed.CVEAlias: written automatically bycvelistv5.StoreRecordWithSourceviadb.InsertAliases(mapper.go:279). Empty alias list still triggers same-cveId cross-source backfill so agitlab-sourced row is automatically aliased to peernist-nvd/cve.orgrows when those exist.- No bundle suppression required — GitLab CVEs are 1:1 with their published CVE IDs.
6. CRIT / VEX
Service taxonomy
The six services below ship in the CRIT spec dictionary
(github.com/Vulnetix/ietf-crit-spec → dictionaries/gitlab.json), loaded by
critutil.LoadSpecDictionaries(). There is no internal/critutil/dictionaries/extended/
directory; the local extended layer is internal/critutil/dictionaries/local/ and carries
no GitLab entries. A triple with no dictionary template resolves to nil and stages
nothing, so the dictionary is the hard gate on emission. A record that stages zero
envelopes is offered to CRIT inference via critprep.Hook instead.
| Service | Resource type | Trigger | Lifecycle / responsibility |
|---|---|---|---|
gitlab_saas | instance | always emitted | provider_only / automatic |
gitlab_self_managed | instance | always emitted | customer_action_required / version_update |
gitlab_dedicated | instance | affected.product mentions “Dedicated” | provider_only / automatic |
runner | agent | affected.product mentions “Runner” | customer_action_required / version_update |
dependency_proxy | proxy | title/description mentions “Dependency Proxy” | provider_only / automatic |
container_registry | registry | title/description mentions “Container Registry” | provider_only / automatic |
Default emission: two envelopes per CVE (gitlab_saas + gitlab_self_managed). Service-specific keywords extend with additional envelopes; they don’t replace the default pair.
VEX status derivation
- All emitted candidates use
vex_status="fixed". GitLab only publishes CVEs against shipped releases — by the time the CVE-*.json file appears in the public repo, fixed versions are out. temporal.provider_fix_date=cveMetadata.datePublished(parsed to YYYY-MM-DD).temporal.vuln_published_date= canonical lookup (db.LoadCanonicalDatePublished) with fallback tocveMetadata.datePublished.temporal.service_available_date = "2014-03-01"(GitLab.com GA).
Provenance
provenance.primary_source.kind = "gitlab",advisory_id = cveID,advisory_url = "https://gitlab.com/gitlab-org/cves/-/blob/master/{year}/{cveID}.json",s3_key = gitlab/files/{sha256}/{path}.- Evidence rules:
source-scoped-provider: “GitLab CNA-issued advisory implies provider=gitlab”service-text-match: hint keyword → service tripledictionary-resolved:(gitlab,…) ∈ extended dictcve-publish-date-lookup: canonical-date trace
7. S3 / source-file archive layout
- Bucket:
vdb-manager-artifacts(or whateverS3_BUCKET_NAMEresolves at runtime). - Archive prefix (success):
gitlab/files/{sha256}/{year}/{CVE-YYYY-NNNN}.json— content-addressed; the{path}is the tarball-relative path (preserves year directory). - Quarantine prefix (failure):
failed-feeds/gitlab-json-processor/{YYYY-MM-DD}/{reason}/{path}. - Quarantine reasons:
parse-error(cvelistv5 unmarshal failed),store-error(DB transaction failed after retries),decode-error(gzip/tar extraction failed — terminates the run with a fatal).
8. Error handling & Slack
- Per-record failures are non-fatal: log warn, increment
failedcounter, quarantine the bytes, continue to next record. - Tarball fetch failure is fatal: exit 1 with
ErroredSlack event. - Retry profile: HTTP fetch retries 3 times with exponential backoff
via the standard
httpclient.Newclient. Per-record DB writes are attempted once — failures go to quarantine. - Slack events:
Startedat process entry;Erroredon fatal exits (DB pool init, tarball fetch);NoWorkwhen tracker is fresh and not--force;Completedwith stats{fetched, stored, skipped, failed, critStaged}on clean exit.
9. Performance
- Concurrency: sequential walk of tarball entries; the bottleneck
is per-record DB write. ~1,290 records at ~10 ms each is comfortably
under the 30-min soft deadline once resume is warm, but a
--forcecold pass is the run most likely to hit it. - Per-request timeout: httpclient default (30 s) for tarball fetch with internal retries.
- Soft deadline: only when
EXPECTED_DURATION_MINUTESenv var is set; backfill runs unset it (per blueprint “Backfill must not have a deadline”). - Resume strategy:
BulkDataDumpTracker.gitlabchecked at startup; if fresh and not--force, exitNoWork.LoadProcessedHashes(source="gitlab")returns{cveId → sourceFileHash}. Records whose tarball-entry SHA1 matches the tracker’s prior hash are skipped (cvelistv5 already stored them).
10. Backfill
cmd/gitlab-json-processoritself is the backfill — same binary, same flow. The justfile recipego-gitlab-json-backfilladds:unset EXPECTED_DURATION_MINUTESso the soft deadline does not fire.--force={{FORCE}}to bypass tracker freshness.--limit={{LIMIT}}for sampling during development.--emit-crit={{EMIT_CRIT}}— the recipe’sEMIT_CRITdefault is"true", even though the binary’s own flag default isfalse. The ECS command also passes--emit-crit=true.
- Expected runtime: a few minutes on prod data once resume is warm; most entries skip
via
sourceFileHashafter the first run.
11. Tests
internal/gitlab/parse_test.gowalks a checked-in mini-tarball fixture (testdata/cves-mini.tar.gz, 3 records) and asserts each entry parses withcvelistv5.ParseRecord.cmd/gitlab-json-processor/crit_mapper_test.gocovers:- dual SaaS+self-managed emission for a generic GitLab CVE
- Runner extra envelope when
affected.product="GitLab Runner" - Dependency Proxy extra envelope when description mentions it
- Container Registry extra envelope
- empty CVE id rejection
- S3 key shape
12. Definition of done
See plan §17. Every box must tick before merge:
- Phase 0 recon documented (this doc §2).
go vet+go testclean acrossinternal/gitlab/andcmd/gitlab-json-processor/.- All 6 registration files updated (Containerfile, terraform,
task-manager.toml, post-push-ecr.sh, ecr-build targets.go, justfile)
with the locked name
gitlab-json-processor. cvelistv5.StoreRecordWithSourceis the only path to CVEMetadata.- S3 archive on success + quarantine on failure wired.
- Slack events confirmed for at least one local backfill run.
- §16 verification executed end-to-end against
.env.production. - One CloudWatch firing observed with exit code 0.
S3 Persistence
- Archive path:
gitlab/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/gitlab-json-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.