hsec-git-processor
Status: Live Source: haskell/security-advisories (git) Type:
git(repo baked by thehsec-git-datastage; the final image isalpinewith a git binary soPullOrCloneuses system git at startup) Source slug:haskellSchedule: Runs weekly on Mondays at 04:00 UTC (cron(0 4 ? * MON *)). CPU 256 / memory 512 MB /expected_duration_minutes = 45.
Overview
Hackage has no CNA and no OSV export. haskell/security-advisories (the
HSEC-* namespace, maintained by the Haskell Security Response Team) is the only
authoritative record of Haskell package and GHC-component vulnerabilities. It is
also the source of the HSEC↔CVE alias edges, without which an HSEC- identifier
quoted in a Haskell advisory or cabal audit output resolves to nothing in the
VDB.
Volume is small (tens of advisories), so this is a coverage-completeness feed: it
exists so a cabal.project.freeze gets a real answer instead of silence.
Each run:
- Refreshes the repo with
processor.PullOrCloneon/data/haskell-advisories(alpine image, system git).--no-pullreads the baked HEAD instead. - Short-circuits on an unchanged HEAD against
BulkDataDumpTracker.sha256for sourcehsec_advisory; a match without--forcereportstask.noworkand exits 0. - Loads the resume skip set —
db.LoadProcessedHashes("haskell"). - Walks
advisories/published/recursively forHSEC-*.md, then applies the 3-day changed-file filter when a tracker exists. - Loads known CVE IDs —
db.LoadKnownCVEIDsreads every(cveId, source)pair matchingCVE-%soExploitCVEjunctions attach to the sources that actually hold the CVE. This scan dominates start-up time. - Stores in transactions of
--batch-size(default 50), oneSAVEPOINTper file.
Advisory format
Each advisory is a Markdown file whose leading fenced block is TOML:
| Section | Fields |
|---|---|
[advisory] | id (HSEC-YYYY-NNNN), cwe (integers), capec, keywords, aliases, related |
[[references]] | type, url |
[[affected]] | package, ghc-component, os, cvss, declarations, nested [[affected.versions]] ranges |
The Markdown body supplies the title (H1), the description, body links, and any code blocks.
hsec.ExtractCVEID keys the primary row on the first CVE-YYYY-NNNN+ alias when
present, otherwise on the HSEC-* ID.
datePublished comes from git. The TOML carries no publication date, so the
processor calls processor.FileFirstCommitDate and uses the advisory file’s
first-commit timestamp. When git history is unavailable (shallow clone boundary)
the field is left unset and the UpsertCVEMetadata earliest-wins guard preserves
whatever another source already knows.
One row per affected package
hsec.MapAdvisories returns one CVESourceData per [[affected]] entry, not
one per file:
- affected index 0 takes the primary ID (CVE alias, or the
HSEC-*ID); - every further index is minted as a synthetic clone id
"{HSEC-ID}-{n+1}"(e.g.HSEC-2025-0006-2); - all clone ids are cross-linked to each other as aliases.
So a multi-package Haskell advisory produces several CVEMetadata rows under
source='haskell', and identifiers of the form HSEC-YYYY-NNNN-2 are
Vulnetix-minted, not upstream identifiers.
Records produced
| Condition | Records |
|---|---|
| Every stored record | CVEMetadata (source="haskell", sourceFileHash = file SHA1), CVEDescription, CVEMetadataReferences (referenceSource="Haskell"), CVEProblemType from [advisory].cwe, CVEAffected + CVEAffectedVersion (collection URL https://hackage.haskell.org) |
[[affected]].cvss present | CVEMetric with the upstream vector |
| Descriptions present | one derived CVEMetric (containerType="vulnetix", cvssV4_0) |
| Always | CVEAlias edges via db.InsertAliases (aliases, related, sibling clone ids, plus same-cveId cross-source edges) |
| Every affected package | Dependency, DependencyRegistry, PackageVersion, PackageVersionCVE via db.EnrichAffectedWithDependency |
Row keyed on an HSEC- id | GcveIssuance (gcveId = "GCVE-110-" + cveId, GNA 110, year/sequenceNumber parsed from the HSEC id) + GcveAlias rows for every alias and for the upstream HSEC id |
Advisory contains a code block in a PoC language (c, ocaml, python, bash, sh, ruby, go, rust, java, javascript, ml, haskell) | Exploit (source="haskell", exploitId = the HSEC id, category="poc", originalURL = the advisory’s GitHub blob URL) + ExploitCVE junctions for every CVE alias |
| End of run | BulkDataDumpTracker row hsec_advisory |
The exploit heuristic is language-based, not behaviour-based — any fenced
block tagged with one of those languages marks the advisory as carrying a PoC.
Treat source='haskell' exploit rows as “advisory contains code”, not as
verified weaponisation.
Resume
- Repo-level — the
hsec_advisorytracker SHA. - Per-file —
db.LoadProcessedHashes("haskell")skip set, matched against the primary id only.
--force bypasses both.
Failure modes
| Symptom | Cause |
|---|---|
repository unchanged, skipping processing | tracker SHA equals HEAD — normal most weeks |
git date extraction failed | first-commit lookup failed; datePublished is left to the DB guard |
failed to load known CVE IDs | the scan failed; ExploitCVE junctions are limited |
gcve issuance failed | logged as a warning — the CVE rows still land, only the GCVE identifier is missing |
soft deadline reached, stopping early | EXPECTED_DURATION_MINUTES − 10 elapsed. The tracker still advances, so unreached files wait for the next --force |
%d files errored + exit 1 | any errored file fails the run after the tracker has advanced |
Flags
| Flag | Default | Meaning |
|---|---|---|
--force | false | Ignore the tracker SHA and the per-file hash skip set |
--batch-size | 50 | Files per transaction |
--repo | /data/haskell-advisories | Clone location |
--data-dir | (repo + /advisories/published) | Override the advisory directory |
--no-pull | false | Use the existing clone’s HEAD instead of pulling |
Local run: just go-hsec-git-backfill (append prod for production).
S3 Persistence
- Archive path:
haskell/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/hsec-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.