enrich-nuclei Design
Enriches CVE records with Nuclei template and ExploitDB exploit data.
Overview
- Batch size: 1000 CVEs per run
- Schedule: Every hour (
cron(0 * * * ? *)), 512 CPU / 1024 MB,EXPECTED_DURATION_MINUTES=45; soft deadline isbudget − 10minutes (60 min when unset, i.e. local runs) - Concurrency: Lock via
BulkDataDumpTracker(source=enrich_nuclei); exits if < 8 min since last run; lock refreshed every 5 min via heartbeat goroutine during long runs - State: Per-CVE rows in
BulkDataDumpTracker(source=enrich_nuclei:{cveId}) - Allowed prefixes:
CVE-,GHSA-(configured viaallowedPrefixesvar inmain.go)
Template path history (why this mattered)
nuclei-templates reorganised its CVE tree from a flat cves/{year}/ into
per-protocol directories (http/cves/{year}/, network/cves/{year}/,
javascript/cves/{year}/). The old flat path returns 404 for every CVE in
existence, and because a 404 is a legitimate “no template” answer the breakage was
completely silent: the task kept reporting nucleiFound=0 run after run.
Production evidence at the time of the fix — 943,751 CVEs marked processed under
the enrich_nuclei: prefix, and zero Exploit rows with source='nuclei',
zero CVEMetadataReferences with referenceSource='NUCLEI'.
Because phase 1 permanently claims a CVE the first time it is seen, the ~944k
already-claimed CVEs recover only via the phase-2 reprocess pass (oldest-first,
1000/run, hourly), which needs roughly 39 days for a full sweep. If a faster
recovery is wanted, clear the enrich_nuclei: tracker rows.
Retry/Backoff
HTTP requests to raw.githubusercontent.com (Nuclei templates) and exploit-db.com (ExploitDB) use exponential backoff:
- Up to 4 retries per request
- Backoff starts at 1s, doubles each attempt, capped at 60s, with up to half-backoff random jitter
- Retries on HTTP 429 (rate limited) and network errors
- Request function rebuilt fresh each attempt (new
http.NewRequestWithContextcall)
GHSA ID Handling
For GHSA- prefixed IDs, extractYear returns "" (GHSA IDs have no 4-digit year at position 1), so CheckNucleiTemplate is skipped. FetchExploitDBByCVE and LinkExistingExploits still run normally for GHSA IDs.
Processing Flow
- Check lock (
enrich_nuclei) — exit 0 if recent - Acquire lock
- Start heartbeat goroutine (refreshes lock every 5 min)
- Find next batch (1000 CVEs,
CVE-andGHSA-prefixes only): unprocessed first, then oldest reprocess - Claim batch (upsert per-CVE tracker rows)
- For each CVE:
CheckNucleiTemplate: HEADraw.githubusercontent.com/.../nuclei-templates/main/{protocol}/cves/{year}/{cveId}.yaml, probingprotocolinhttp,network,javascriptuntil one returns 200 (skipped entirely for non-CVE IDs, whereextractYearyields"")- If 200: GET the YAML, upload to
nuclei-templates/{cveId}.yamlin S3, then upsertCVEMetadataReferences(type=exploit, referenceSource=NUCLEI) +Exploit(source=nuclei, withr2Bucket/r2Key) +ExploitCVE - An error is returned only when every probe failed at the transport level; an honest set of 404s means “no template exists” and is not a failure
- If 200: GET the YAML, upload to
FetchExploitDBByCVE: GETexploit-db.com/search?cve={cveId}(JSON)- For each result: fetch raw file, parse header, upsert
CVEMetadataReferences+Exploit+ExploitCVE
- For each result: fetch raw file, parse header, upsert
LinkExistingExploits: queryExploitWHEREcveIds @> [cveId], insert missingExploitCVEjunctions- Update
CVEMetadata.lastEnriched
Tables Written
CVEMetadataReferences— inserted (ON CONFLICT DO NOTHING) withtype=exploitandreferenceSourceofNUCLEIorEXPLOIT_DB. Thesourcecolumn carries the CVE’s first known source (MIN(source)fromCVEMetadata), so the reference is attached to whichever authority sorts first alphabetically — not to every source that knows the CVE.Exploit— exploit records (source=nucleiorsource=exploit-db), withr2Bucket/r2Keywhen S3 is configuredExploitCVE— CVE-to-exploit junctionCVEMetadata.lastEnriched— enrichment timestampBulkDataDumpTracker— lock + per-CVE state (lock row refreshed every 5 min by heartbeat)
S3 content archive
| Path | When |
|---|---|
nuclei-templates/{cveId}.yaml | A Nuclei template was found and fetched |
exploit-db/{exploitId}.txt | An ExploitDB search hit resolved to raw exploit content |
Skipped when S3_BUCKET_NAME is unset. Nothing is quarantined — a fetch failure
is logged and the CVE is left for the next reprocess pass.
S3 Persistence
- Archive: ⚠ Not yet implemented — requires record reconstruction (DB row → canonical JSON).
- Quarantine: ⚠ Not yet implemented — same reason.
- Likely reasons when implemented:
fetch-error,enrich-error
This is an enrichment processor; it reads from CVEMetadata rather than ingesting raw feeds, so there is no original payload to archive verbatim. See S3 Persistence Contract § Processors whose unit-of-work is not a file.