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
| Property | Value |
|---|---|
| URL | https://cert.europa.eu/publications/security-advisories-rss |
| Auth | None — fully public |
| Format | RSS 2.0 (no content:encoded namespace) |
| Language | English (en) |
| Items | 10 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
| Field | Content |
|---|---|
serial_number | Advisory ID (2026-002) |
title | Advisory title |
publish_date | Publication date (26-02-2026 18:38:52) |
description | Short HTML summary (same as RSS) |
content_markdown | Full advisory in Markdown — primary extraction source |
content_html | Full advisory in HTML |
Parsing
The content_markdown field is parsed for all structured data:
| Pattern | Data Extracted |
|---|---|
CVE-\d{4}-\d{4,} | All CVE IDs (deduplicated, order-preserving) |
CVE-NNNN[^.\n]{0,150}?CVSS score of N.N | Per-CVE CVSS base score |
CVSS score of N.N[^.\n]{0,150}?CVE-NNNN | Per-CVE CVSS (reversed phrasing) |
CVSS:\d+\.\d+/[A-Z]{2}:[A-Z]/... | CVSS vector string (rare) |
<https://...> in # References section | External reference URLs |
| Advisory title keyword map | Vendor + 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.
| Table | Rows inserted |
|---|---|
CVEMetadata | One per CVE ID; source="cert-eu", cveId=CVE-YYYY-NNNN |
CVEDescription | One per CVE; containerType="cna", lang="en" (full markdown text) |
CVEMetadataReferences | Advisory URL (type="advisory") + all external reference URLs |
CVEMetric | Per-CVE CVSS score (+ vector string if available) |
CVEAffected | Vendor + product extracted from advisory title |
CVEAlias | Cross-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
| Flag | Default | Description |
|---|---|---|
--all | false | Reprocess all advisories, not just new ones |
--limit | 0 | Maximum advisories to process (0 = unlimited) |
--force | false | Force reprocessing even if URL is already stored |
ECS Schedule
Runs once daily (cron(0 6 * * ? *)).
Error Alerting
Uses the notify.Notifier (SNS → Slack) pattern:
| Situation | Action | Slack? |
|---|---|---|
| Feed unreachable / parse error | notifier.Errored(...) + os.Exit(1) | Yes — immediate |
| Per-item JSON fetch failure | notifier.RecordError(...) | Deferred to end |
| Per-CVE DB transaction failure | notifier.RecordError(...) | Deferred to end |
| Any errors accumulated | notifier.Errored(...) at end, all details in one Slack message | Yes — on exit |
| Clean run | notifier.Completed(...) with {stored, noCVEs, errors:0} stats | Yes — success |
| Partial success (some CVEs stored, some failed) | notifier.Errored(...) + exit 0 | Yes — 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
| File | Purpose |
|---|---|
cmd/cert-eu-rss-processor/main.go | Main processor — feed fetch, loop, DB writes, notify |
internal/certeu/types.go | RSS feed structs, AdvisoryJSON struct, Advisory struct |
internal/certeu/parser.go | RSS XML parsing, JSON endpoint fetch, markdown extraction |
internal/certeu/mapper.go | Advisory → 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).
See the S3 Persistence Contract for the full reason taxonomy.