CERT-US (CISA) RSS Processor — Design

Overview

Fetches cybersecurity advisories from the CISA (Cybersecurity and Infrastructure Security Agency) RSS 2.0 feed and creates CVEMetadata rows (source=cert-us) under original CVE IDs.

The feed contains two advisory types:

  • ICS advisories (/ics-advisories/) — structured HTML with per-CVE CVSS vectors, CWE IDs, affected products, and critical infrastructure sector tags
  • KEV catalog alerts (/news-events/alerts/) — minimal HTML citing newly added entries to the CISA Known Exploited Vulnerabilities catalog

Only advisories with at least one CVE ID are stored. Items without CVE IDs (e.g., general threat briefings) are skipped.

Feed

PropertyValue
URLhttps://www.cisa.gov/cybersecurity-advisories/all.xml
AuthNone — fully public
FormatRSS 2.0 with Dublin Core namespace (dc:creator)
LanguageEnglish (en)
Items~30 recent advisories per fetch

Advisory Types

TypeURL patternCVEsCVSS vectorsCWEsProducts
ICS advisory/ics-advisories/icsa-*YesYesYesSometimes
ICS medical/ics-advisories/icsma-*YesYesYesSometimes
KEV catalog/news-events/alerts/YesNoNoNo
General alert/news-events/alerts/RareNoNoNo

Parsing

The RSS <description> field is HTML-entity-encoded. After double-unescaping, it contains two structural layers:

Summary table (all ICS advisories)

A <div class="csaf-table"> near the top of the description contains a summary row with the aggregate max CVSS score for the advisory:

CVSS      | Vendor              | Equipment        | Vulnerabilities
v3 9.9    | Schneider Electric  | Plant iT/...     | Use After Free, ...

Score format: v3 X.X, v3.1 X.X, v4 X.X, or v3 10 (integer 10 = 10.0).

Sections by <h2> heading

Section HeadingData Extracted
SummaryImpact/description text
Affected ProductsVendor/product strings from <li> and <td> elements
Vulnerability OverviewPer-CVE detail blocks containing vector links
MitigationsRemediation guidance text
Recommended PracticesAdditional mitigation text (fallback)
BackgroundCritical infrastructure sectors from <li> elements

Extracted fields

FieldSource
Advisory URL<link> element (fallback: <guid>)
Title<title>
Published<pubDate> parsed as RFC 2822 / RFC 1123Z
CVE IDsRegex CVE-\d{4}-\d{4,} across full decoded description
CVSS vectorsRegex CVSS:\d+\.\d+/[A-Za-z0-9:/]+ across full text
CVSS scoresCISA table pattern v\d+(\.\d+)? (10|[1-9]\.\d) — max advisory score
CWE IDsRegex CWE-\d+ across full decoded description
Reference URLs<a href> elements (http/https only)
Affected products<li> + <td> elements in Affected Products section
Sectors<li> elements in Background section
Advisory IDICSA/ICSMA pattern from URL path; fallback: last path segment
Content hashSHA1(guid|title|pubDate)

CVSS score strategy

The CISA RSS description embeds one aggregate max score per advisory in the summary table, not individual per-CVE scores. Individual CVSS vectors appear in a separate Vulnerability Overview section.

Because the per-CVE scores are not embedded in the RSS (only on the full advisory page), the max summary score is assigned to all vectors extracted from the same advisory. This is conservative — the actual scores for individual vectors may differ — but ensures severity data is present for downstream consumers without requiring a follow-up HTTP fetch per advisory.

ID Strategy

All stored items use upstream CVE IDs — no VDB identifier minting. Multi-CVE advisories create one CVEMetadata row per CVE ID.

The mapper hands the advisory’s other CVE IDs to the store call as Aliases (internal/certus/mapper.go:29-34), but they are not written as CVEAlias rows: an ICS advisory listing several CVEs is a bundle, not an identity assertion, so db.InsertAliases drops every CVE-prefixed entry when the alias list holds more than one (internal/db/cvealias.go:65-88). What does get written is the same-cveId cross-source edge set — (CVE-x, cert-us) linked to every other source already carrying CVE-x.

Storage

No new tables or columns required. All tables already exist.

TableRows inserted
CVEMetadataOne per CVE ID; source="cert-us", cveId=CVE-YYYY-NNNN
CVEDescriptionOne per CVE; containerType="cna", lang="en"
CVEMetadataReferencesAdvisory URL (advisory) + extracted URLs (classified by domain)
CVEMetricCVSS vector + max score + severity when present (ICS advisories)
CVEAffectedProducts from Affected Products section when present
CVEAliasSame-cveId cross-source edges only (co-listed CVE↔CVE aliases are suppressed — see ID Strategy)
CVEProblemtypeCWE IDs extracted from description

Reference URL classification

Domain patternType
cisa.gov, github.com/cisagovadvisory
cve.org, nvd.nist.gov, first.org, cwe.mitre.orgtechnical
github.com (other)technical
Vendor domains (microsoft, cisco, siemens, schneider, etc.)vendor
All othertechnical

Incremental Strategy

On startup, load all sourceAdvisoryRef values from CVEMetadata where source='cert-us' into a map[string]bool using the read replica. Per advisory: if the URL is already in the set and neither --all nor --force is set, skip. After successful storage, add the URL to the in-memory set.

Error Alerting

Uses the shared internal/notify package (SNS → Lambda → Slack pipeline):

EventTrigger
task.startedOn processor startup
task.erroredFeed fetch failure, feed parse failure
task.erroredPer-item DB failures accumulated via RecordError
task.overtimeExecution exceeds EXPECTED_DURATION_MINUTES
task.completedNormal completion with stored and noCVEs stats

Flags

FlagDefaultDescription
--allfalseReprocess all advisories, not just new ones
--limit0Maximum advisories to process (0 = unlimited)
--forcefalseForce reprocessing even if content unchanged

ECS Schedule

Runs once daily at 05:00 UTC (cron(0 5 * * ? *)). CPU: 256 units, Memory: 512 MB, expected duration: 30 minutes.

Local Testing

# Process all current feed items against prod DB
just go-cert-us-rss-backfill "prod"

# Process first 20 items only
just go-cert-us-rss-backfill "prod" "false" "20"

# Force reprocess all items
just go-cert-us-rss-backfill "prod" "true"

# Verify idempotency (should show processed=0 after a full run)
just go-cert-us-rss-backfill "prod"

Note: just uses positional arguments — pass values in order: TARGET ALL LIMIT FORCE.

Key Files

FilePurpose
cmd/cert-us-rss-processor/main.goMain processor — fetch, parse, store, notify
internal/certus/types.goRSS feed structs, Advisory, CVSSVector
internal/certus/parser.goFeed parsing, HTML extraction (CVE IDs, CVSS, CWE, URLs, sections)
internal/certus/mapper.goAdvisory[]CVESourceData (one record per CVE ID)

S3 Persistence

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

See the S3 Persistence Contract for the full reason taxonomy.