GPZ 0day ITW Google Sheets Processor — Design Document
1. Overview
Purpose: Fetch the Google Project Zero 0day in-the-wild (ITW) tracking spreadsheet and store exploit intelligence + CVE enrichment records.
Data source: Google Sheets CSV export — https://docs.google.com/spreadsheets/d/1lkNJ0uQwbeC1ZTRrxdtuPLCIl7mlUreoKfSIgajnSyY/export?format=csv&gid=1190662839
Schedule: Daily at 05:00 UTC (cron 0 5 * * *)
Timeout: 15 minutes
Resources: 256 CPU units, 512 MB memory
What it reads:
- Google Sheets CSV export (~555 entries, full re-fetch each run)
- Read replica:
BulkDataDumpTrackerrow forgoogle_project_zero_0day_itw(SHA256 freshness) - Read replica:
CVEMetadatafor the clone-source priority lookup - Read replica: Known CVE IDs for ExploitCVE junction creation
What it writes (all CVE-side rows use source slug gpz; the
google_project_zero_0day_itw string is the BulkDataDumpTracker key only):
CVEMetadata(source=gpz— cloned from the best available source, else created minimal)Exploit(source=gpz, category=0day-itw)ExploitCVE(junctions to priority-ordered sources + one undergpz)ExploitAffectedProduct(vendor + product)CVEMetadataReferences(advisory, analysis, root cause URLs;referenceSource="gpz")CVEProblemType(Type → CWE mapping)CVEDescription(combined description + 0day context)CVEImpact+CVEImpactDescription(“Exploited in the wild”)BulkDataDumpTracker(source=google_project_zero_0day_itw)
What it does not write: no CVEAlias. db.EnsureCVEMetadataForSource is a
direct CVEMetadata writer that never calls db.InsertAliases, so the gpz row
for a CVE is not linked into the alias graph and the same-cveId cross-source
backfill never runs for it (contrary to the AGENTS.md alias contract).
Environment variables:
DATABASE_URL— requiredDATABASE_URL_READ— optional
2. Business Logic
Freshness Check (SHA256-Based)
- Fetch CSV from Google Sheets
- Compute SHA256 hash of response body
- Compare against
BulkDataDumpTracker.sha256forgoogle_project_zero_0day_itw - If match → exit with “data unchanged”
- First run (no tracker row) always proceeds
GPZ CVEMetadata Cloning
For each CVE in the spreadsheet, ensure a CVEMetadata record exists with source=gpz
(db.EnsureCVEMetadataForSource, internal/db/cvemetadata.go:161-218):
- Check if
(cveId, "gpz")already exists → returnskipped(no field refresh) - If not, find best available source in priority order:
cve.org → nist-nvd → github → anchore_adp → any other - Clone key fields (dataVersion, state, datePublished, title, vectorString, affected vendor/product)
- If no source exists at all → create minimal record (state=“PUBLISHED”, dataVersion=“5.1”) using the fallback date, title and vendor/product passed by the processor
Known bug — step 4’s
fallbackDatePublishedis passed in milliseconds (main.go:274-284prefersDateDiscovered, thenDatePatched, both Unix ms, and multiplies the CVE-id-derived date by 1000), but the parameter is written straight intoCVEMetadata.datePublished, which isint4seconds. A millisecond value overflowsint4, so the insert fails for any CVE that no other source knows. In practice GPZ CVEs are almost always already known tocve.org, so the clone path at step 3 hides the defect.
Data Type Classification
New type: gsheet — data is fetched as a CSV export from a public Google Sheets spreadsheet at runtime.
Date Handling
Date Patched(column G): Primary temporal field →Exploit.datePublished(YYYY-MM-DD → Unix ms)Date Discovered(column F): Often “???” or blank (~66% unknown) → included in CVEDescription when known- Sentinel value “???” treated as nil/unknown
Type → CWE Mapping
Case-insensitive static map with Description-based refinement for “Memory Corruption”:
- Memory Corruption → CWE-119 (refined to CWE-416/CWE-122/CWE-787 etc. from Description)
- Logic/Design Flaw → CWE-840
- Information Leak/Disclosure → CWE-200
- XSS/UXSS → CWE-79
- Integer overflow → CWE-190
- Feature Bypass → CWE-863
- Unmapped types stored as description-only (no CWE ID)
Batch Processing
100 entries per transaction; ~406 rows currently parse cleanly, so ~5 batches.
Soft deadline checked between batches — note the fallback is a hardcoded
14 minutes applied even when EXPECTED_DURATION_MINUTES is unset
(main.go:62-66), so a local backfill is truncated at 14 minutes too.
Idempotency
Exploit:ON CONFLICT (exploitId, source) DO UPDATEExploitCVE:ON CONFLICT DO NOTHINGCVEMetadataReferences:ON CONFLICT DO NOTHINGCVEProblemType:ON CONFLICT DO NOTHINGCVEDescription: DELETE + re-INSERT (replace stale descriptions)CVEImpact:ON CONFLICT DO NOTHINGExploitAffectedProduct: DELETE + re-INSERT per exploit
3. Architecture Diagram
4. Deployment Diagram
go-ecr-deploy.yml] -->|push ARM64 image| ECR[ECR: go-processors
tag: go-gpz-0day-itw-gsheet-processor-sha-xxx] ECR --> TASKDEF[ECS Task Definition
go-gpz-0day-itw-gsheet-processor] TASKDEF --> EB[EventBridge Schedule
go-gpz-0day-itw-gsheet-processor
cron 0 5 * * ? *] EB -->|trigger| FARGATE[ECS Fargate Task
vdb-scheduler cluster
ARM64 ap-southeast-2] FARGATE --> CW[CloudWatch Logs
/ecs/vdb-scheduler/gpz-0day-itw-gsheet-processor] FARGATE -->|HTTPS GET| GSHEET[Google Sheets
CSV Export] FARGATE --> WRITE[RDS Write Proxy] FARGATE --> READ[RDS Read Replica]
5. Processing Flow
tracker?} FRESH -->|yes, not forced| EXIT0([Exit 0 — unchanged]) FRESH -->|no or forced| PARSE[gpz.ParseCSV → ~555 entries] PARSE -->|error| FAIL PARSE --> LOAD[Load known CVE IDs] LOAD --> BATCH[For each batch of 100 entries] BATCH --> ENTRY[For each entry in transaction] ENTRY --> VULNETIX[EnsureCVEMetadataForSource
clone or create source=gpz] VULNETIX --> EXPLOIT[UpsertExploit
source=gpz] EXPLOIT --> AFFECTED[Delete + Insert
ExploitAffectedProduct] AFFECTED --> JUNCTIONS[InsertExploitCVE
priority sources + gpz] JUNCTIONS --> REFINS[InsertReferences
advisory, analysis, root cause] REFINS --> PROBLEM[InsertProblemTypes
Type → CWE mapping] PROBLEM --> DESCINS[InsertDescriptions
combined description + context] DESCINS --> IMPACTINS[InsertImpacts
Exploited in the wild] IMPACTINS --> ENTRY ENTRY -->|done| DEADLINE{Soft deadline
reached?} DEADLINE -->|yes| TRACKER[UpsertTracker] DEADLINE -->|no| BATCH BATCH -->|done| TRACKER TRACKER --> ERRCHECK{errors > 0?} ERRCHECK -->|yes| FAIL2([Exit 1]) ERRCHECK -->|no| DONE([Exit 0])
6. Data Mapping
7. Google Sheets Columns
| Column | Header | Maps to |
|---|---|---|
| A | CVE | Exploit.exploitId, ExploitCVE.cveId, CVEMetadata lookup |
| B | Vendor | ExploitAffectedProduct.vendor, CVEMetadata.affectedVendor |
| C | Product | ExploitAffectedProduct.product, CVEMetadata.affectedProduct |
| D | Type | CVEProblemType (CWE mapping), description-based refinement |
| E | Description | Exploit.title component, CVEDescription.value |
| F | Date Discovered | CVEDescription (when known, ~34% of rows) |
| G | Date Patched | Exploit.datePublished, CVEMetadata.datePublished |
| H | Advisory | CVEMetadataReferences (type=“ADVISORY”) |
| I | Analysis URL | CVEMetadataReferences (type=“ARTICLE”) |
| J | Root Cause Analysis | CVEMetadataReferences (type=“ARTICLE”, title=“Root Cause Analysis”) |
| K | Reported By | Exploit.author, CVEImpactDescription attribution |
S3 filenames
The archive unit is the whole daily snapshot — gpz/files/{sha256}/gpz-0day-itw.csv,
uploaded once per changed spreadsheet after the batch loop (main.go:186), same
rationale as epss-csv-backfill. Quarantine has two granularities: the full CSV
body under fetch-error / parse-error (main.go:97, 116), and a synthesised
single-row CSV named {CVE}.csv under store-error / map-error for each failed
row, uploaded only after the transaction resolves (main.go:374-386).
S3 Persistence
- Archive path:
gpz/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/gpz-0day-itw-gsheet-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
fetch-error,parse-error,store-error,map-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.