CERT.at RSS Processor — Design
Overview
Fetches security warnings from the Austrian national CERT (CERT.at) Atom 1.0
feed and creates CVEMetadata rows (source=cert-at) under original CVE IDs.
Advisories are in German with structured HTML content containing CVE IDs, CVSS scores, affected products, and reference URLs organized under standard section headings.
Feed
| Property | Value |
|---|---|
| URL | https://www.cert.at/cert-at.de.warnings.atom_1.0.xml |
| Auth | None — fully public |
| Format | Atom 1.0 with Dublin Core extensions |
| Language | German (de) |
| Items | ~18 security warnings |
Parsing
The <summary type="html"> content is HTML-entity-encoded. After decoding,
it contains structured sections under <h2> headings:
| Section Heading | Data Extracted |
|---|---|
Beschreibung | Description text, CVE IDs (CVE-Nummer(n):), CVSS score (CVSS Base Score:) |
Auswirkungen | Impact text |
Betroffene Systeme | Affected products from <li> items |
Abhilfe | Remedy/mitigation text |
Informationsquelle(n) | Reference URLs from <a href> tags |
| Field | Source |
|---|---|
| Advisory URL | <link rel="alternate" href="..."> |
| Title | <title> (German) |
| Published | <published> or <dc:date> parsed as ISO 8601 |
| Updated | <updated> parsed as ISO 8601 |
| CVE IDs | Regex CVE-\d{4}-\d{4,} from Beschreibung |
| CVSS Score | Regex CVSS Base Score: (bis zu )?\d+.\d+ |
| Affected Products | <li> elements in Betroffene Systeme |
| Reference URLs | <a href> elements in Informationsquelle(n) |
| Content Hash | SHA1(id|title|updated) |
ID Strategy
All items contain upstream CVE IDs — no VVD minting needed. Multi-CVE
advisories create one CVEMetadata row per CVE with cross-references as aliases.
Items without CVE IDs (e.g., phishing warnings) are skipped.
Storage
No new tables or columns. All tables already exist.
| Table | Rows inserted |
|---|---|
CVEMetadata | One per CVE ID; source="cert-at", cveId=CVE-YYYY-NNNN |
CVEDescription | One per CVE; containerType="cna", lang="de" |
CVEMetadataReferences | Advisory URL (type="advisory") + vendor/technical refs |
CVEMetric | CVSS score when available (metricType="cvssV3_1") |
CVEAffected | Products from “Betroffene Systeme” section |
CVEAlias | Cross-references between CVEs in the same advisory — only for two-CVE advisories (see below) |
db.InsertAliases treats a CVE-prefixed primary id carrying two or more
CVE-prefixed aliases as bundling and drops the CVE entries
(internal/db/cvealias.go:65). A three-or-more-CVE CERT.at warning therefore
produces no CVE↔CVE edges at all; a two-CVE warning does, because each record
then carries exactly one CVE alias. Every row still gets same-cveId
cross-source edges linking cert-at to every other source holding that CVE, so
the record is never orphaned in the alias graph.
CVEMetadata.vectorString is NULL for every row — correct here, because the
feed publishes a base score and no vector.
Incremental Strategy
On startup, load all sourceAdvisoryRef values from CVEMetadata where
source='cert-at' 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.
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 content unchanged |
Error Alerting
| Situation | Action |
|---|---|
| Feed fetch failure (after 3 retries) | notifier.Errored + os.Exit(1) — Slack |
| Feed parse failure | quarantine + notifier.Errored + os.Exit(1) — Slack |
| Per-CVE transaction failure | notifier.RecordError, advisory quarantined, loop continues |
| End of run | always notifier.Completed |
Because the run always ends in Completed, and notify.Completed publishes only
Stats — it does not flush the messages accumulated by RecordError
(internal/notify/notify.go:213) — per-advisory store failures never reach Slack.
They are visible in the task logs and the EMF record-error metric only, and are
not reflected in the reported stats either. Sibling processors (cert-it,
cert-eu) use notifier.HasErrors() to choose Errored over Completed; this
one does not.
ECS Schedule
Runs once daily (cron(0 6 * * ? *)), schedule_enabled = true,
expected_duration_minutes = 30.
The soft deadline is EXPECTED_DURATION_MINUTES - 10, and the loop guard fires a
further 5 minutes early — so an ECS run stops after 15 minutes. A local backfill
recipe unsets EXPECTED_DURATION_MINUTES, which does not remove the deadline:
the hard-coded 10-minute default applies, leaving a 5-minute effective budget.
Key Files
| File | Purpose |
|---|---|
cmd/cert-at-rss-processor/main.go | Main processor |
internal/certat/types.go | Atom feed and advisory Go structs |
internal/certat/parser.go | Feed parsing, HTML section extraction, CVE/CVSS extraction |
internal/certat/mapper.go | Advisory → CVESourceData mapping |
schemas/cert_at_rss_advisory.schema.json | JSON Schema Draft 7 for parsed advisory object |
S3 Persistence
- Archive path:
cert-at/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/cert-at-rss-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).
See the S3 Persistence Contract for the full reason taxonomy.