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 in internal/critsignals Source: not a feed — read CRIT candidate envelopes already staged in S3 under crit-candidates/pending/ Type: post-pass enricher (S3 in, S3 out; the database is read-only except for S3QueueObject bookkeeping) Source slug: none — this processor wrote no CVEMetadata row 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 calls critpublisher.DrainKeys at end-of-run, so each envelope is inserted or rejected before the producer exits. s3://vdb-manager-artifacts/crit-candidates/ contains only approved/, rejected/ and audit-log/ — there is no pending/ prefix and no superseded/ prefix, and S3QueueObject holds 100,018 inserted + 50,267 rejected rows and zero superseded rows. 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 published CritRecord.

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 CritRecord rows / the crit-candidates/approved/ prefix and re-publish through the normal publisher path.

Correlation rules

Rule keyFires whenEnvelope mutation
kev_listedA 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_listedAny 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_scoreAn 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_sightingsCrowdSecSighting count > 0 for the CVEAppends 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

ConditionEffect
Rules fired and the envelope changedNew object at crit-candidates/pending/{…}/{sha}.json; an S3QueueObject row registered for it (internal/critpublisher/publisher.go:400-409)
SameThe 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-stableCounted as unchanged; nothing written to S3 or the database
--dry-runCorrelations 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

FailureBehaviour
--bucket / S3_BUCKET_NAME unset, DATABASE_URL unset, pool or uploader init failure, List failuretask.errored to SNS + exit 1 (cmd/crit-emit-post-processor/main.go:128-132)
One object fails to Get or parseLogged at WARN, counted in errored, scan continues
Envelope has an empty cveIdLookup returns an error for that object only (cmd/crit-emit-post-processor/main.go:199-201)
Any lookup query failsThat object is counted as errored; the run still reports task.completed
SIGTERM / overtimenotify.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

FlagDefaultDescription
--prefixcrit-candidates/pending/S3 prefix to scan
--bucket$S3_BUCKET_NAMEBucket override
--limit0Cap on envelopes processed (0 = no cap)
--dry-runfalseCompute 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

FilePurpose
cmd/crit-emit-post-processor/main.goFlags, S3 listing, per-object orchestration, lookup loading
cmd/crit-emit-post-processor/correlate.goThe four pure correlation rules + idempotency helpers
cmd/crit-emit-post-processor/correlate_test.goRule unit tests with injected Lookups
internal/critpublisher/publisher.goRestage, moveAndMark, superseded key layout
internal/db/kev.go, internal/db/crowdsec.goLookupKEV, LookupVulnCheckKEV, EpssLatest, CrowdSecCount
.repo/CRIT-pipeline-plan.md §15.7Phase 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-publisher and crit-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-publisher and crit-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)