crit-emit-post-processor — Design (RETIRED)
Retired 2026-08-10
This processor has been removed from the fleet. It was a no-op in production for its
whole life: the crit-candidates/pending/ prefix it scanned was empty, because every
producer already correlates these four signals into the envelope at staging time. A
scheduled second pass over the same pending keys therefore bought nothing and added a
second writer to objects a producer may be restaging concurrently.
The four correlation rules are not deleted — they moved to
internal/critsignals (Load + Correlate), so a producer can run them inside its own
staging step, which is where the envelope is built and where the lookup belongs. The
unit tests moved with them.
Accepted cost: a signal that lands after an envelope is staged no longer reaches
it. Nothing re-correlates a pending envelope today. If late-arriving KEV or EPSS
evidence on already-staged candidates turns out to matter, the fix is to call
critsignals from crit-publisher’s drain path — not to reinstate a standalone
scheduled scanner.
The document is kept because the correlation rules, their idempotency contract and the restage/supersede semantics still describe live code.
Status: RETIRED 2026-08-10 — schedule, task definition, image target and
cmd/directory all removed; rules preserved ininternal/critsignalsSource: not a feed — read CRIT candidate envelopes already staged in S3 undercrit-candidates/pending/Type: post-pass enricher (S3 in, S3 out; the database is read-only except forS3QueueObjectbookkeeping) Source slug: none — this processor wrote noCVEMetadatarow and minted no identifier Schedule: was daily at 11:00 UTC (cron(0 11 * * ? *)), 256 CPU / 512 MB,expected_duration_minutes = 15.
Overview
CRIT candidates are produced by ~40 Phase-1.x processors at ingest time, when the only thing known about a vulnerability is what its own advisory said. Exploitation intelligence arrives later and from different feeds: CISA adds a CVE to the KEV catalog with a federal due date, VulnCheck lists it weeks earlier, FIRST publishes an EPSS score, CrowdSec starts reporting in-the-wild sightings. Without a post-pass, a candidate reviewed on day 3 is judged on the day-0 evidence set.
This processor closes that gap. It walks the pending prefix, runs four
correlation rules per envelope against the current database state, and — when
anything fired — restages an updated envelope and routes the original to
crit-candidates/superseded/ for audit. Reviewers in the S3 queue GUI then see
kev_listed, vulncheck_kev_listed, epss_score and crowdsec_sightings
evidence plus a populated customer_deadline_date, which is what lets
high-signal candidates be auto-approved instead of hand-triaged.
It is deliberately independent of producer --emit-crit gating: it operates on
whatever is in pending/, whoever staged it.
⚠ Currently a no-op in production (verified 2026-08-06). The premise — that candidates sit in
pending/long enough for a post-pass to enrich them — no longer holds. Every Phase-1.x producer callscritpublisher.DrainKeysat end-of-run, so each envelope is inserted or rejected before the producer exits.s3://vdb-manager-artifacts/crit-candidates/contains onlyapproved/,rejected/andaudit-log/— there is nopending/prefix and nosuperseded/prefix, andS3QueueObjectholds 100,018inserted+ 50,267rejectedrows and zerosupersededrows. This processor therefore lists an empty prefix on every scheduled run and has never restaged an envelope; the KEV deadlines, EPSS scores and CrowdSec sightings it exists to attach are not reaching any publishedCritRecord.Fixing it means changing where it runs, not what it computes: either producers must invoke the correlation pass before their in-process drain, or the pass must be re-pointed at published
CritRecordrows / thecrit-candidates/approved/prefix and re-publish through the normal publisher path.
Correlation rules
| Rule key | Fires when | Envelope mutation |
|---|---|---|
kev_listed | A Kev row with source='CISA' exists for the envelope’s CVE (internal/db/kev.go:29) | Appends evidence source=CISA date_added=… known_ransomware=…, adds a cisa-kev supporting source, sets Temporal.CustomerDeadlineDate to the CISA dueDate (or dateAdded + 14 days when the due date is absent) with CustomerDeadlineSource="CISA-KEV" |
vulncheck_kev_listed | Any VulnCheckKEV row joins the CVE (internal/db/kev.go:65) | Appends evidence with vendor/product/date, adds a vulncheck-kev supporting source, and sets a dateAdded + 14 days deadline only if CISA-KEV did not already set one |
epss_score | An EpssScore row exists (latest by dateString) | Appends score=… percentile=… evidence once. Score changes are deliberately not chased — otherwise every EPSS publication would restage the whole queue |
crowdsec_sightings | CrowdSecSighting count > 0 for the CVE | Appends sightings=N last_seen=… evidence and a crowdsec supporting source. Re-evaluated on Sightings > 0, not on the exact count, so re-runs stay stable |
Every rule is idempotent: it checks for its own marker before appending, and the
post-pass preserves the envelope’s original GeneratedAt. A re-run with no new
correlations produces a byte-identical envelope, whose content SHA maps to the
same pending key, and critpublisher.Restage then skips the move entirely
(internal/critpublisher/publisher.go:393-396) — a true no-op.
Records produced
| Condition | Effect |
|---|---|
| Rules fired and the envelope changed | New object at crit-candidates/pending/{…}/{sha}.json; an S3QueueObject row registered for it (internal/critpublisher/publisher.go:400-409) |
| Same | The original object is moved to crit-candidates/superseded/{YYYY}/{MM}/{DD}/{cve}/{provider}/{service}/{resourceType}/…, its S3QueueObject key remapped and marked superseded with the new key as the reason (internal/critpublisher/publisher.go:238-259) |
| No rules fired, or the envelope was SHA-stable | Counted as unchanged; nothing written to S3 or the database |
--dry-run | Correlations computed and logged, nothing staged or moved |
No CVEMetadata, CVEAlias, Kev, EpssScore or CritRecord row is ever
written by this binary. Kev, VulnCheckKEV, EpssScore and
CrowdSecSighting are read from the replica; S3QueueObject is the only table
it mutates. Publication into CritRecord remains the
crit-publisher’s job.
Failure modes
| Failure | Behaviour |
|---|---|
--bucket / S3_BUCKET_NAME unset, DATABASE_URL unset, pool or uploader init failure, List failure | task.errored to SNS + exit 1 (cmd/crit-emit-post-processor/main.go:128-132) |
One object fails to Get or parse | Logged at WARN, counted in errored, scan continues |
Envelope has an empty cveId | Lookup returns an error for that object only (cmd/crit-emit-post-processor/main.go:199-201) |
| Any lookup query fails | That object is counted as errored; the run still reports task.completed |
| SIGTERM / overtime | notify.SetOvertimeCancel cancels the context and the scan stops between envelopes rather than mid-object (cmd/crit-emit-post-processor/main.go:96-100) |
Note that per-object errors do not change the exit code and do not raise
task.errored; a run where every envelope failed to parse still ends as
task.completed with errors: N in the payload.
Flags
| Flag | Default | Description |
|---|---|---|
--prefix | crit-candidates/pending/ | S3 prefix to scan |
--bucket | $S3_BUCKET_NAME | Bucket override |
--limit | 0 | Cap on envelopes processed (0 = no cap) |
--dry-run | false | Compute correlations without staging or moving objects |
Local run: just go-crit-emit-post-backfill prod — add DRY_RUN=true to
inspect what would change.
Key files
| File | Purpose |
|---|---|
cmd/crit-emit-post-processor/main.go | Flags, S3 listing, per-object orchestration, lookup loading |
cmd/crit-emit-post-processor/correlate.go | The four pure correlation rules + idempotency helpers |
cmd/crit-emit-post-processor/correlate_test.go | Rule unit tests with injected Lookups |
internal/critpublisher/publisher.go | Restage, moveAndMark, superseded key layout |
internal/db/kev.go, internal/db/crowdsec.go | LookupKEV, LookupVulnCheckKEV, EpssLatest, CrowdSecCount |
.repo/CRIT-pipeline-plan.md §15.7 | Phase 6 specification |
On the generated section below: the S3 Persistence Contract covers feed archival — raw upstream payloads plus a quarantine path. This is not a feed-archival processor: S3 is its input and output medium (the CRIT candidate queue), so it has no upstream payload to archive and nothing to quarantine. The same applies to
crit-publisherandcrit-inference. Read the block below as “not applicable”, not as a gap.
On the generated section below: the S3 Persistence Contract covers feed archival — raw upstream payloads plus a quarantine path. This is not a feed-archival processor: S3 is its input and output medium (the CRIT candidate queue), so it has no upstream payload to archive and nothing to quarantine. The same applies to
crit-publisherandcrit-inference. Read the block below as “not applicable”, not as a gap.
S3 Persistence
Not used. This processor does not currently archive payloads or quarantine failures to S3. Per the S3 Persistence Contract this is non-compliant — see the compliance matrix for the implementation roadmap.
⚠ Not in the compliance matrix — status needs verification.
Expected paths when implemented:
- Archive:
crit-emit-post/files/{sha256}/{filename} - Quarantine:
failed-feeds/crit-emit-post-processor/{YYYY-MM-DD}/{reason}/{filename} - Likely reasons: (none documented)