eukev-json-processor

Status: Live Source: enisaeu/CNW — advisories/eukev/eukev.json Type: json (single flat array, no pagination, no auth) Source slug: n/a — writes no CVEMetadata. Kev.source = "enisa"; tracker source is enisa_eukev. Schedule: Runs daily at 08:30 UTC (cron(30 8 * * ? *)).

Overview

The EU counterpart to the CISA KEV catalog. ENISA publishes its own known-exploited-vulnerabilities list inside the Cybersecurity News Watch (CNW) repository; this processor lands it in the same Kev table the CISA catalog uses, under source='enisa', so every KEV consumer (SSVC exploitation input, KEV urgency badges, kev-watch, the KEV API) picks up EU-designated entries with no additional plumbing. Three sources currently coexist in Kev: CISA, enisa, and vulnetix.

Overlap with CISA is partial and deliberate — ENISA lists vulnerabilities exploited against EU targets that CISA has not added, and vice versa. Losing this processor means EU-only exploitation designations silently disappear from urgency scoring.

Records produced

ConditionRecords
Every entry with a non-empty cveIDKev row, upsert on ("cveID","source") with source='enisa'
Every runBulkDataDumpTracker row enisa_eukev (sha256 written empty — freshness is time-based)
Every stored entryS3 archive; failures quarantined (see § S3 Persistence)

Entries with an empty cveID are skipped outright. Records are written 500 at a time, each batch in one transaction with a per-entry SAVEPOINT sp_kev.

Field mapping

ENISA’s schema does not line up 1:1 with CISA’s, so several Kev columns carry ENISA-specific semantics. Consumers that assume CISA semantics for these columns will misread source='enisa' rows.

Kev columnENISA sourceNote
dateAddeddateReportedCOALESCEd to fetchedAt on first insert only; on conflict the stored value is preserved when the upstream date is unparseable, so a scheduled run can never roll the date forward to the run day
dueDatedateReportedENISA publishes no remediation deadline — dueDate is set to the same value as dateAdded, not a CISA-style compliance deadline
requiredActionpatchedSince, or the literal "Apply vendor patch when available"
knownRansomwareCampaignUsethreatActorsExploitingHolds an actor name, not CISA’s "Known"/"Unknown" enum
notesnotes + "Exploitation type: " + exploitationTypejoined with "; "
cwesJSONcwescomma-separated string split into a JSON array
catalogVersion, catalogReleaseDatealways NULL; ENISA publishes no catalog version
euvdIDnot persisted — the field is parsed off the wire but never written to any column

Every string field is normalised through normalizeDash, which maps the literal "-" (ENISA’s “not applicable” marker) and whitespace-only values to empty.

parseEUKEVDate tries 2006/01/02, 2006-01-02, 02/01/2006, 02/01/06 in that order — four-digit-year layouts first so 2026/07/10 can never be misread as day-first. "", "-" and "TBC" are treated as absent, and the caller deliberately does not substitute time.Now(), which is what would otherwise rewrite every record’s date on every run.

Freshness gate

The run is skipped with task.no_work when now - tracker.lastProcessedAt < tracker.frequency × 1000.

Known defect — the daily run skips every second day. db.UpsertTracker hardcodes frequency = 86400 and writes lastProcessedAt at the end of a successful run. The next 08:30 fire therefore sees elapsed < 86400000 and exits early without fetching. The freshness window must be strictly shorter than the schedule interval. (cisa-kev-json-processor escapes this only because its live tracker row predates the helper and still carries 21600.)

Deadline

EXPECTED_DURATION_MINUTES − 10, falling back to a hardcoded 15 minutes when the env var is unset — the state the just go-eukev-json-backfill recipe creates. At ~35 entries the bound is never reached, but it contradicts the backfill-has-no-deadline rule; the deadline should be left at its zero value when the env var is absent.

Local execution

just go-eukev-json-backfill          # local DB
just go-eukev-json-backfill prod     # .env.production

S3 Persistence

  • Archive path: eukev/files/{sha256}/{filename}
  • Quarantine path: failed-feeds/eukev-json-processor/{YYYY-MM-DD}/{reason}/{filename}
  • Failure reasons emitted: parse-error, store-error

Uses s3client.Uploader from internal/s3client/uploader.go. Skipped when S3_BUCKET_NAME is unset (local dev).

flowchart LR SRC[Source feed] --> PROC[eukev-json-processor] PROC -->|success| ARCHIVE[("S3: eukev/files/{sha256}/{filename}")] PROC -->|failure| Q[("S3: failed-feeds/eukev-json-processor/{date}/{reason}/{filename}")] PROC --> DB[(PostgreSQL)]

See the S3 Persistence Contract for the full reason taxonomy.