Red Hat Security Errata (RHSA) RSS Processor — Design

Overview

Fetches Red Hat Security Errata advisories from the public RSS 2.0 feed (https://security.access.redhat.com/data/meta/v1/rhsa.rss) and creates first-class CVEMetadata rows (source=rhsa) for every CVE ID embedded in each advisory.

Feed

PropertyValue
URLhttps://security.access.redhat.com/data/meta/v1/rhsa.rss
AuthNone — fully public
FormatRSS 2.0
ItemsRolling list of recent RHSA advisories

Parsing

FieldSource
Advisory IDLast path segment of <link>, e.g. RHSA-2026:4672
Advisory URL<link> (fallback: <guid>)
Title<title> (format: RHSA-YYYY:NNNN: Severity: product security update)
SeverityTitle prefix: Critical, Important, Moderate, Low
ProductTitle text after severity prefix, stripped of “security update” suffix
CVE IDsFetched from errata HTML page (RSS description contains no CVE IDs)
Published<pubDate> parsed as RFC 2822 → Unix seconds

Note: The RSS feed <description> does not contain CVE IDs — it only says “CVE link(s) in the References section”. Each advisory requires a follow-up HTTP fetch of the errata page HTML to extract CVE IDs via regex.

Storage

No new tables or columns. All tables already exist.

TableRows inserted
CVEMetadataOne per CVE per advisory; source="rhsa", dataVersion="5.1"
CVEDescriptionOne per CVE per advisory; containerType="cna", lang="en"
CVEMetadataReferencesOne per CVE per advisory; type="advisory", referenceSource="rhsa"
CVEAffectedOne per CVE per advisory; vendor="Red Hat", product from title
CVEMetricOne per CVE per advisory (when severity present); metricType="other", otherType="rhsa-severity"
CVEAliasFor multi-CVE advisories: (cveIDs[0], "rhsa") → each sibling; conflict = DO NOTHING

Incremental Strategy

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

Flags

FlagDefaultDescription
--forcefalseReprocess all advisories, not just new ones
--limit0Maximum advisories to process per run (0 = unlimited)

ECS Schedule

Runs every 6 hours (cron(0 */6 * * ? *)).

Key Files

FilePurpose
cmd/rhsa-rss-processor/main.goMain processor binary
internal/rhsa/types.goRSS XML structs and Advisory type
internal/rhsa/parser.goFeed parsing, title/CVE extraction
internal/rhsa/mapper.goAdvisory → DB row mapping utilities
schemas/rhsa_rss_advisory.schema.jsonJSON Schema Draft 7 for parsed advisory object

Verification Queries

-- Count ingested RHSA records
SELECT COUNT(*) FROM "CVEMetadata" WHERE source = 'rhsa';

-- Sample records
SELECT "cveId", "title", "sourceAdvisoryRef", "datePublished"
FROM "CVEMetadata" WHERE source = 'rhsa' ORDER BY "datePublished" DESC LIMIT 10;

-- References
SELECT COUNT(*) FROM "CVEMetadataReferences" WHERE "referenceSource" = 'rhsa';

-- Affected products
SELECT COUNT(*) FROM "CVEAffected" ca
JOIN "CVEMetadata" cm ON ca."cveId" = cm."cveId" AND ca.source = cm.source
WHERE cm.source = 'rhsa';

-- Aliases
SELECT COUNT(*) FROM "CVEAlias" WHERE "primarySource" = 'rhsa';

AI Enrichment (aienrich)

After each advisory’s transaction commits, the processor builds a target list from the CVE ids whose per-CVE processOneCVE returned nil (i.e. landed cleanly), then fires aienrich.RunBatch. Wire-in: cmd/rhsa-rss-processor/main.go, immediately after uploader.ArchiveRecord. Each target carries Source = "rhsa" and Aliases = adv.CVEIDs (the full multi-CVE bundle).

Four passes fire per CVE (no ghsapoc — self-gated to source = "github"):

PassPersists to
vulnetix.affectedCVEAffected.{modules, programFiles, programRoutines}
vulnetix.attackCVEAttackTechnique + children
vulnetix.cweCVEProblemType (descriptionType = "CWE", derivedBy = "vulnetix")
vulnetix.treesitterCVETreeSitterQuery + CVETreeSitterCapture + CVETreeSitterPredicate

RunBatch is invoked outside the parent transaction. See the aienrich overview for the full activity contract, environment variables, and operator runbook.

S3 Persistence

Not used. This processor does not currently archive payloads or quarantine failures to S3. Per the S3 Persistence Contract this is non-compliant — see the compliance matrix for the implementation roadmap.

Expected paths when implemented:

  • Archive: rhsa/files/{sha256}/{filename}
  • Quarantine: failed-feeds/rhsa-processor/{YYYY-MM-DD}/{reason}/{filename}
  • Likely reasons: parse-error