0day-today-fetch-backfill
Status: Live (local-only, on demand) Source: a local clone of the 0day.today archive (
--repo, default/data/0day.today.archive) Type:fetch(flat-file archive walk) Source slug:0day-today(inExploit.source— this processor writes noCVEMetadata) Schedule: None. There is no ECS task definition, no ECR target and no EventBridge schedule. Run it by hand withjust go-0day-today-fetch-backfill.
Overview
0day.today is one of the largest public exploit archives and the only one of its
size with no machine-readable API. The archive is mirrored as a git repo of flat
.txt files — one exploit per file, grouped into five category directories —
plus an index.json carrying the metadata the file headers omit (platform,
canonical link, raw URL).
This backfill turns that into Exploit rows so the exploit-intelligence
surface (/exploits, exploitation-maturity signals, SSVC inputs, the CWSS
priority calculation) can cite 0day.today alongside ExploitDB, Metasploit,
Nuclei and Nmap NSE. It is a backfill rather than a scheduled processor because
the upstream is a git mirror that a human refreshes; there is nothing to poll.
Without it, roughly 39k exploits — the single largest non-ExploitDB corpus we hold — are invisible to exploit-maturity scoring, and the exploit bodies are not in S3 for offline static analysis.
Archive layout
0day.today.archive/
index.json # [{exploit_id, cve[], platform, original_link, github_raw_url, title, author}]
web-applications/{id}.txt
dos-poc/{id}.txt
remote-exploits/{id}.txt
local-exploits/{id}.txt
shellcode/{id}.txt
Only those five category directories are walked (categories in main.go:47);
files are sorted by category/filename so ordering — and therefore statefile
resume — is deterministic. A missing category directory is skipped, not an
error.
Each .txt file has a fixed five-field header followed by a blank line and the
exploit body:
id: 41235
date: 03/14/2026
title: Example CMS 2.1 - Remote Code Execution
author: someone
cves: CVE-2026-1234, CVE-2026-1235
# Exploit Title: Example CMS 2.1 - RCE
...
The title: field may span multiple lines (everything up to author: is
folded into it). A trailing # 0day.today [...] promo footer line and any
trailing blank lines are stripped from the body before hashing. NUL bytes are
removed and invalid UTF-8 sequences dropped (main.go:471-474) — without this,
PostgreSQL rejects the insert with SQLSTATE 22021.
Records produced
| Condition | Records |
|---|---|
| Every parsed exploit | Exploit (source="0day-today", exploitId = the archive filename stem, category = directory, bodyContentHash = SHA-256 of the stripped body, fileSize, cveIds JSON) |
index.json has original_link | Link (PLAIN_TEXT) — its id is stored as Exploit.fileLinkId |
| S3 configured and body non-empty | Exploit.r2Bucket / r2Key pointing at 0day-today/{exploitId}.txt |
| Each extracted CVE ID | one ExploitCVE row per source that already knows that CVE (db.ResolveExploitCVESources) |
| Vendor / product / version recovered | ExploitAffectedProduct |
| Parse failure | S3 quarantine at failed-feeds/0day-today-fetch-backfill/{date}/parse-error/{filename} + notifier.RecordError |
datePublished comes from the date: header parsed as MM/DD/YYYY and is
stored in milliseconds; an unparseable date yields nil, never 0
(main.go:592-599).
CVE extraction
CVE IDs are merged from three sources in priority order (mergeCVEs,
main.go:612-634), deduplicated case-insensitively:
index.jsoncve[]for this exploit id- the file’s
cves:header line (comma-separated) - every
CVE-\d{4}-\d{4,}match anywhere in the exploit body
Source 3 is deliberately broad — exploit authors routinely cite the CVE only in
a code comment — but it also picks up CVEs merely mentioned in prose (“similar
to CVE-2019-0708”). Treat ExploitCVE rows from this source as
exploit-adjacent evidence rather than a proof of exploitability for that exact
CVE.
Affected-product extraction
Four header conventions are tried in order (extractAffectedProducts,
main.go:648-699); the first that yields anything wins:
- ExploitDB style —
# Exploit Title:,# Version:,# Software Link:,# Vendor Homepage:,# Software Web Page: - Advisory style (LiquidWorm / zeroscience) —
Vendor:,Product Web Page:,Affected version: - Metasploit module — detected via
class … < Msf::/MetasploitModule, product taken from the'Name' =>field - Title fallback —
{product} {version}split out of the exploit title
Resume
Two independent layers, both bypassed by --force:
| Layer | Mechanism | Scope |
|---|---|---|
| DB-driven | db.LoadProcessedExploitIDs(source) — an exploit id already in Exploit is skipped without being read | across runs, authoritative |
| Statefile | .repo/0day-today.state records the last processed category/filename; processor.ResumeIndex restarts the walk there | local only, deleted on a clean finish |
db.LoadKnownCVEIDs is also loaded up front so ExploitCVE junctions can be
written against every (cveId, source) pair that exists. That query scans
CVEMetadata for all CVE-% ids and takes minutes on production — it is the
dominant startup cost of the run (see AGENTS.md “Known Issues”).
Failure modes
| Symptom | Cause | Handling |
|---|---|---|
no exploit files found | --repo points somewhere without the five category directories | notifier.NoWork, exit 0 |
parse failed: missing {id,date,title,author,cves} header | file is not in archive header format | quarantined as parse-error, counted in errors, run continues |
file too short | fewer than 6 lines | same |
batch transaction failed | the 500-record transaction rolled back | all 500 counted as errors; the statefile is not advanced past them, so a re-run retries |
exit 1 with N errors during processing | any per-file error | the statefile is preserved so the next run resumes rather than restarting |
Flags
| Flag | Default | Description |
|---|---|---|
--repo | /data/0day.today.archive | Path to the archive clone |
--state-dir | .repo | Where the resume statefile lives |
--force | false | Reprocess exploits already present in Exploit, ignore the statefile |
--skip-s3 | false | Never upload bodies even when S3_BUCKET_NAME is set |
The justfile recipe adds NO_PULL (default false), which git -C {repo} pulls
the archive before the walk, and resolves REPO to an absolute path because the
binary runs from scripts/go-processors.
Local development
# Refresh the archive clone and backfill against production
just go-0day-today-fetch-backfill prod
# Re-run without touching the clone, no S3 uploads
just go-0day-today-fetch-backfill prod NO_PULL=true SKIP_S3=true
psql "$DATABASE_URL" -c "
SELECT count(*) AS exploits,
count(\"datePublished\") AS with_date,
count(\"r2Key\") AS with_s3
FROM \"Exploit\" WHERE source = '0day-today';"
S3 Persistence
- Archive path:
0day-today/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/0day-today-fetch-backfill/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
parse-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.