CERT-CA RSS Processor — Design

Overview

Fetches security advisories from the Canadian Centre for Cyber Security (CCCS) Atom 1.0 feeds in English and French. EN is the primary language; FR entries are merged by advisory ID matching. Records stored under original CVE IDs (source=cert-ca).

Feeds

FeedURLLang
ENhttps://www.cyber.gc.ca/api/cccs/atom/v1/get?feed=alerts_advisories&lang=enen
FRhttps://www.cyber.gc.ca/api/cccs/atom/v1/get?feed=alerts_advisories&lang=frfr

Both feeds: Auth=none, Format=Atom 1.0, ~42 entries each.

Parsing

The <content> CDATA contains HTML with:

  • Serial number and date in first <p> (<strong>Serial number:</strong> AV26-258)
  • Affected products in <ul><li> items
  • CVE IDs in text and <a href> links
  • Occasional CVSS scores in text (e.g., “CVSS 10.0”)
  • Reference URLs in <ul class="list-unstyled"><li><a href> at bottom
  • <summary> is always empty — use <content> only

EN/FR Merge

Match by advisory ID extracted from URL: regex (av\d+-\d+).

  • EN: /en/alerts-advisories/...-av26-258av26-258
  • FR: /fr/alertes-avis/...-av26-258av26-258

EN primary. Matched FR entries add lang=fr description.

Storage

TableDetails
CVEMetadatacveId=CVE-YYYY-NNNN, source=cert-ca
CVEDescriptionlang=en + lang=fr (when merged)
CVEMetadataReferencesAdvisory URL + vendor refs
CVEMetricOccasional CVSS score
CVEAffectedProducts from <ul><li>
CVEAliasCross-refs between CVEs — 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 occasional CVSS score is written to CVEMetric; CVEMetadata.vectorString is NULL for every cert-ca row, correctly — the CCCS content carries a bare score and no vector.

Flags

--all, --limit, --force

Error Alerting

A failed EN feed fetch/parse is fatal (notifier.Errored + os.Exit(1)); a failed FR feed is a warning and the run proceeds English-only. Per-CVE transaction failures call notifier.RecordError and quarantine the advisory, then the loop continues — but the run always ends in notifier.Completed, which does not flush RecordError messages (internal/notify/notify.go:213). Those failures therefore 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 15-minute default (10-minute effective budget) rather than no deadline.

Key Files

FilePurpose
cmd/cert-ca-rss-processor/main.goMain processor with EN/FR merge
internal/certca/types.goAtom feed and advisory structs
internal/certca/parser.goFeed parsing, HTML extraction, EN/FR merge
internal/certca/mapper.goAdvisory → CVESourceData mapping
schemas/cert_ca_rss_advisory.schema.jsonJSON Schema Draft 7

S3 Persistence

  • Archive path: cert-ca/files/{sha256}/{filename}
  • Quarantine path: failed-feeds/cert-ca-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-ca-rss-processor] PROC -->|success| ARCHIVE[("S3: cert-ca/files/{sha256}/{filename}")] PROC -->|failure| Q[("S3: failed-feeds/cert-ca-rss-processor/{date}/{reason}/{filename}")] PROC --> DB[(PostgreSQL)]

See the S3 Persistence Contract for the full reason taxonomy.