CERT-LV RSS Processor — Design

Overview

Fetches vulnerability advisories from the Latvian national CERT (CERT-LV / cert.lv) RSS feed. The feed republishes CERT/CC VU# notes and CISA alerts with English-language HTML descriptions containing structured sections.

Records stored under original CVE IDs (source=cert-lv). Items without CVE IDs are skipped.

Feed

PropertyValue
URLhttps://cert.lv/rss/attacks.xml
AuthNone — fully public
FormatRSS 2.0
Languagelv (feed metadata), en (content)
Items~15 advisories

Content Types

Two item types in the feed:

  1. CERT/CC VU# notes: Structured HTML with <h3 id="..."> sections (Overview, Description, Impact, Solution, Acknowledgements, References) and CVE IDs in <table> rows
  2. CISA alerts: Simpler HTML with <p> and <div> elements, sometimes containing CVE IDs in <a> links

Parsing

FieldSource
Advisory IDLast path segment of <link> (e.g., “294418”)
Title<title>
Overview<h3 id="overview"> section text
Description<h3 id="description"> section text
Impact<h3 id="impact"> section text
Solution<h3 id="solution"> section text
CVE IDsCVE-\d{4}-\d{4,} regex from full HTML
Reference URLs<a href> from References section
Published<pubDate> (format: YYYY-MM-DD HH:MM:SS)
Content HashSHA1(link|title|pubDate)

Storage

No new tables or columns.

TableRows inserted
CVEMetadataOne per CVE ID; source="cert-lv"
CVEDescriptionOne per CVE; lang="en"
CVEMetadataReferencesAdvisory URL + extracted reference URLs
CVEAliasCross-references between CVEs in 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.

No Metrics (feed has no CVSS), no ProblemTypes (no CWE), no Affected (no product data). CVEMetadata.affectedVendor / affectedProduct / vectorString are therefore NULL for every cert-lv row — expected, given what the feed carries.

Incremental Strategy

URL-based dedup via sourceAdvisoryRef in CVEMetadata.

Flags

FlagDefaultDescription
--allfalseReprocess all advisories
--limit0Maximum advisories (0 = unlimited)
--forcefalseForce reprocessing

Error Alerting

Feed fetch or parse failure is fatal (notifier.Errored + os.Exit(1)). Per-CVE transaction failures call notifier.RecordError and quarantine the advisory, but the run always ends in notifier.Completed, which does not flush RecordError messages (internal/notify/notify.go:213) — so those failures never reach Slack.

ECS Schedule

Daily (cron(0 6 * * ? *)), schedule_enabled = true, expected_duration_minutes = 30. Soft deadline is EXPECTED_DURATION_MINUTES - 10 and the loop guard fires 5 minutes earlier still, so an ECS run stops after 15 minutes. Unsetting EXPECTED_DURATION_MINUTES for a local backfill falls back to a hard-coded 10-minute default (5-minute effective budget) rather than no deadline.

Key Files

FilePurpose
cmd/cert-lv-rss-processor/main.goMain processor
internal/certlv/types.goRSS feed and advisory structs
internal/certlv/parser.goFeed parsing, HTML section extraction
internal/certlv/mapper.goAdvisory → CVESourceData mapping
schemas/cert_lv_rss_advisory.schema.jsonJSON Schema Draft 7

S3 Persistence

  • Archive path: cert-lv/files/{sha256}/{filename}
  • Quarantine path: failed-feeds/cert-lv-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).

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

See the S3 Persistence Contract for the full reason taxonomy.