MITRE CVE Processor — Design Document
1. Overview
Purpose: Download the MITRE cvelistV5 GitHub archive (tar.gz), stream-extract all CVE-*.json files, and enqueue them for downstream processing.
Data source: GitHub release archive — https://github.com/CVEProject/cvelistV5/archive/refs/heads/main.tar.gz
Schedule: Daily (cron 0 2 * * *)
Timeout: 180 minutes (the archive is ~1GB+ compressed)
Resources: 512 CPU units, 2048 MB memory
What it reads:
- MITRE cvelistV5 tar.gz archive (streamed directly from HTTP response — no temp file)
What it writes:
QueueMessage(source=mitre-cve, batches of 100)MitreCveFile(single summary record per archive)BulkDataDumpTracker(tracker key:mitre_cve_org)
Environment variables:
DATABASE_URL— requiredDATABASE_URL_READ— optional
2. Business Logic
Streaming Architecture (No Temp File)
The HTTP response body is streamed through a io.TeeReader that simultaneously:
- Feeds bytes to
gzip.NewReader→tar.NewReaderfor extraction - Hashes the entire stream with
sha256.Newto compute the archive SHA256
This means the archive SHA256 is only available after the stream is fully consumed. Files are enqueued during streaming; the tracker is updated at the end with the completed hash.
Archive SHA256 Computation
io.TeeReader(resp.Body, hasher) — every byte read from resp.Body is also written to the hasher. After all tar entries are processed, io.Copy(io.Discard, teeReader) drains any remaining bytes (e.g., gzip footer) to ensure the hash covers the complete stream.
archiveSHA256 = fmt.Sprintf("%x", hasher.Sum(nil))
File Filter
Within the tar archive: path.Base(hdr.Name) — filter for files where basename starts with CVE- and ends with .json. The archive includes many non-CVE files (README, delta files, etc.) which are skipped.
JSON Validation
json.Valid(data) — each file’s bytes are validated as JSON before enqueueing. Invalid files increment the error counter and are skipped.
Batch Enqueueing
Records accumulate in a slice; when it reaches 100 entries, InsertQueueMessages is called. fileName for all messages is cvelistV5-main.tar.gz.
MitreCveFile Summary Record
After processing the full archive, a single summary record is written to MitreCveFile with:
archiveSha256— computed from streamfilename = 'cvelistV5-main.tar.gz'cveCount— total files processedstatus = 'completed'
This is an ON CONFLICT (archiveSha256, filename) DO UPDATE — each archive run is idempotent.
No SHA256 Dedup Check at Start
Unlike other processors, mitre-cve-processor does NOT check the tracker SHA256 before downloading. The archive must be streamed to compute its SHA256. This means every run downloads the full archive.
3. Architecture Diagram
io.TeeReader + tar + sha256] MITRE_FILES[upsertMitreCveFiles
MitreCveFile summary] end subgraph "internal/queue/" QUEUE[producer.go — InsertQueueMessages] end subgraph "internal/db/" POOL[pool.go — Pool] TRACK[tracker.go — UpsertTracker] end MAIN --> PROCESS MAIN --> TRACK MAIN --> POOL PROCESS -->|batches of 100| QUEUE PROCESS --> MITRE_FILES MITRE_FILES -->|SQL| WRITE[pool.Write.Exec]
4. Deployment Diagram
go-ecr-deploy.yml] -->|push ARM64 image| ECR[ECR: vdb-manager
tag: mitre-cve-processor-sha-xxx] ECR --> TASKDEF[ECS Task Definition
go-processor-mitre-cve-processor] TASKDEF --> EB[EventBridge Schedule
vdb-mitre-cve-daily
cron 0 2 * * ? *] EB -->|trigger| FARGATE[ECS Fargate Task
vdb-scheduler cluster
ARM64 ap-southeast-2] FARGATE --> CW[CloudWatch Logs
/ecs/vdb-scheduler/mitre-cve-daily] FARGATE -->|HTTPS GET streaming| GITHUB[github.com/CVEProject/cvelistV5
archive/refs/heads/main.tar.gz] FARGATE --> WRITE[RDS Write Proxy
QueueMessage + MitreCveFile + BulkDataDumpTracker]
5. Processing Flow
streaming HTTP response] DOWNLOAD -->|error| FAIL DOWNLOAD -->|ok| STREAM[processArchive
io.TeeReader → hasher + gzip → tar] STREAM --> TARLOOP[For each tar entry] TARLOOP --> FILTER{basename starts with CVE-
and ends with .json?} FILTER -->|no| TARLOOP FILTER -->|yes| READ[io.ReadAll tar entry] READ -->|error| ERR[count error
continue] READ -->|ok| VALIDATE{json.Valid?} VALIDATE -->|no| ERR VALIDATE -->|yes| ACCUMULATE[Append to records slice] ACCUMULATE --> BATCH{records >= 100?} BATCH -->|yes| FLUSH[InsertQueueMessages
source=mitre-cve
fileName=cvelistV5-main.tar.gz] BATCH -->|no| TARLOOP FLUSH --> TARLOOP TARLOOP -->|EOF| DRAIN[io.Copy Discard — drain remaining bytes for hash] DRAIN --> FLUSH_FINAL[Flush remaining records] FLUSH_FINAL --> COMPUTE[archiveSHA256 = hasher.Sum nil] COMPUTE --> MITRE_FILE[upsertMitreCveFiles
archiveSHA256 + fileCount] MITRE_FILE --> TRACKER[UpsertTracker mitre_cve_org
sha256=archiveSHA256] TRACKER --> ERRCHECK{errors > 0?} ERRCHECK -->|yes| FAIL2([Exit 1]) ERRCHECK -->|no| DONE([Exit 0]) ERR --> TARLOOP
6. Data Mapping
S3 Persistence
- Archive path:
cve.org/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/mitre-cve-processor/{YYYY-MM-DD}/{reason}/{filename}⚠ not yet wired - Failure reasons emitted:
schema-violation,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.