CERT-EU RSS Processor — Design

Overview

Fetches security advisories from the CERT-EU (Cybersecurity Service for the EU Institutions, Bodies, Offices and Agencies) RSS 2.0 feed and creates CVEMetadata rows (source=cert-eu) under the referenced CVE IDs.

Advisories are in English and cover critical and high severity vulnerabilities affecting widely-used products. Each advisory typically references one or more upstream CVE IDs in its full text.

Feed

PropertyValue
URLhttps://cert.europa.eu/publications/security-advisories-rss
AuthNone — fully public
FormatRSS 2.0 (no content:encoded namespace)
LanguageEnglish (en)
Items10 most-recent security advisories

JSON Endpoint

Each advisory has a structured JSON endpoint derived by appending json to the advisory URL:

https://cert.europa.eu/publications/security-advisories/2026-002/json
FieldContent
serial_numberAdvisory ID (2026-002)
titleAdvisory title
publish_datePublication date (26-02-2026 18:38:52)
descriptionShort HTML summary (same as RSS)
content_markdownFull advisory in Markdown — primary extraction source
content_htmlFull advisory in HTML

Parsing

The content_markdown field is parsed for all structured data:

PatternData Extracted
CVE-\d{4}-\d{4,}All CVE IDs (deduplicated, order-preserving)
CVE-NNNN[^.\n]{0,150}?CVSS score of N.NPer-CVE CVSS base score
CVSS score of N.N[^.\n]{0,150}?CVE-NNNNPer-CVE CVSS (reversed phrasing)
CVSS:\d+\.\d+/[A-Z]{2}:[A-Z]/...CVSS vector string (rare)
<https://...> in # References sectionExternal reference URLs
Advisory title keyword mapVendor + product names

Per-CVE CVSS Score Example

CERT-EU advisories describe individual CVE scores inline:

The vulnerability **CVE-2026-20127**, with the CVSS score of 10, is an
authentication bypass vulnerability in Cisco Catalyst SD-WAN Controller...

The vulnerability **CVE-2026-20129**, with a CVSS score of 9.8, is an
authentication bypass vulnerability in the API user authentication...

The regex CVE-(\d{4}-\d{4,})[^.\n]{0,150}?CVSS\s+score\s+of\s+([\d.]+) extracts both the CVE ID and its associated score from each sentence.

Reference URL Extraction

The # References section of the markdown lists URLs as:

[1] <https://sec.cloudapps.cisco.com/security/center/...>
[2] <https://sec.cloudapps.cisco.com/security/center/...>

Regex <(https?://[^>]+)> on the References section captures all external URLs.

ID Strategy

All CERT-EU advisories reference upstream CVE IDs — no VVD minting needed. Multi-CVE advisories create one CVEMetadata row per CVE, with all other CVEs from the same advisory stored as aliases.

Advisories without CVE IDs (very rare, informational-only) are skipped.

Storage

No new tables or columns. All tables already exist.

TableRows inserted
CVEMetadataOne per CVE ID; source="cert-eu", cveId=CVE-YYYY-NNNN
CVEDescriptionOne per CVE; containerType="cna", lang="en" (full markdown text)
CVEMetadataReferencesAdvisory URL (type="advisory") + all external reference URLs
CVEMetricPer-CVE CVSS score (+ vector string if available)
CVEAffectedVendor + product extracted from advisory title
CVEAliasCross-references between CVEs in the same advisory — only for two-CVE advisories; db.InsertAliases drops CVE↔CVE edges when a CVE-prefixed primary carries ≥2 CVE aliases (internal/db/cvealias.go:65). Same-cveId cross-source edges are always written.

The extracted CVSS vector, when one is present, is written to CVEMetric.vectorString but not to the denormalised CVEMetadata.vectorString column — the mapper never sets CVESourceData.VectorString.

Incremental Strategy

On startup, load all sourceAdvisoryRef values from CVEMetadata where source='cert-eu' into a map[string]bool. Per advisory: if the URL is in the set and --all/--force is false, skip. After successful processing, add the URL to the in-memory set.

The RSS feed returns only the 10 most recent items. A daily schedule means the processor evaluates ≤10 items per run, almost all already processed.

Flags

FlagDefaultDescription
--allfalseReprocess all advisories, not just new ones
--limit0Maximum advisories to process (0 = unlimited)
--forcefalseForce reprocessing even if URL is already stored

ECS Schedule

Runs once daily (cron(0 6 * * ? *)).

Error Alerting

Uses the notify.Notifier (SNS → Slack) pattern:

SituationActionSlack?
Feed unreachable / parse errornotifier.Errored(...) + os.Exit(1)Yes — immediate
Per-item JSON fetch failurenotifier.RecordError(...)Deferred to end
Per-CVE DB transaction failurenotifier.RecordError(...)Deferred to end
Any errors accumulatednotifier.Errored(...) at end, all details in one Slack messageYes — on exit
Clean runnotifier.Completed(...) with {stored, noCVEs, errors:0} statsYes — success
Partial success (some CVEs stored, some failed)notifier.Errored(...) + exit 0Yes — with details
Total failure (stored=0 && errors>0)notifier.Errored(...) + os.Exit(1)Yes — fatal

The deferred pattern (accumulate via RecordError, flush at end via Errored) ensures all per-item failures appear in a single Slack message rather than generating one alert per item. HasErrors() is used to decide whether to call Errored or Completed at the end of the run.

Key Files

FilePurpose
cmd/cert-eu-rss-processor/main.goMain processor — feed fetch, loop, DB writes, notify
internal/certeu/types.goRSS feed structs, AdvisoryJSON struct, Advisory struct
internal/certeu/parser.goRSS XML parsing, JSON endpoint fetch, markdown extraction
internal/certeu/mapper.goAdvisory → CVESourceData mapping

S3 Persistence

  • Archive path: cert-eu/files/{sha256}/{filename}
  • Quarantine path: failed-feeds/cert-eu-rss-processor/{YYYY-MM-DD}/{reason}/{filename}
  • Failure reasons emitted: parse-error, enrich-error

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

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

See the S3 Persistence Contract for the full reason taxonomy.