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
| Property | Value |
|---|---|
| URL | https://www.cisa.gov/cybersecurity-advisories/all.xml |
| Auth | None — fully public |
| Format | RSS 2.0 with Dublin Core namespace (dc:creator) |
| Language | English (en) |
| Items | ~30 recent advisories per fetch |
Advisory Types
| Type | URL pattern | CVEs | CVSS vectors | CWEs | Products |
|---|---|---|---|---|---|
| ICS advisory | /ics-advisories/icsa-* | Yes | Yes | Yes | Sometimes |
| ICS medical | /ics-advisories/icsma-* | Yes | Yes | Yes | Sometimes |
| KEV catalog | /news-events/alerts/ | Yes | No | No | No |
| General alert | /news-events/alerts/ | Rare | No | No | No |
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 Heading | Data Extracted |
|---|---|
Summary | Impact/description text |
Affected Products | Vendor/product strings from <li> and <td> elements |
Vulnerability Overview | Per-CVE detail blocks containing vector links |
Mitigations | Remediation guidance text |
Recommended Practices | Additional mitigation text (fallback) |
Background | Critical infrastructure sectors from <li> elements |
Extracted fields
| Field | Source |
|---|---|
| Advisory URL | <link> element (fallback: <guid>) |
| Title | <title> |
| Published | <pubDate> parsed as RFC 2822 / RFC 1123Z |
| CVE IDs | Regex CVE-\d{4}-\d{4,} across full decoded description |
| CVSS vectors | Regex CVSS:\d+\.\d+/[A-Za-z0-9:/]+ across full text |
| CVSS scores | CISA table pattern v\d+(\.\d+)? (10|[1-9]\.\d) — max advisory score |
| CWE IDs | Regex 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 ID | ICSA/ICSMA pattern from URL path; fallback: last path segment |
| Content hash | SHA1(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.
| Table | Rows inserted |
|---|---|
CVEMetadata | One per CVE ID; source="cert-us", cveId=CVE-YYYY-NNNN |
CVEDescription | One per CVE; containerType="cna", lang="en" |
CVEMetadataReferences | Advisory URL (advisory) + extracted URLs (classified by domain) |
CVEMetric | CVSS vector + max score + severity when present (ICS advisories) |
CVEAffected | Products from Affected Products section when present |
CVEAlias | Same-cveId cross-source edges only (co-listed CVE↔CVE aliases are suppressed — see ID Strategy) |
CVEProblemtype | CWE IDs extracted from description |
Reference URL classification
| Domain pattern | Type |
|---|---|
cisa.gov, github.com/cisagov | advisory |
cve.org, nvd.nist.gov, first.org, cwe.mitre.org | technical |
github.com (other) | technical |
| Vendor domains (microsoft, cisco, siemens, schneider, etc.) | vendor |
| All other | technical |
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):
| Event | Trigger |
|---|---|
task.started | On processor startup |
task.errored | Feed fetch failure, feed parse failure |
task.errored | Per-item DB failures accumulated via RecordError |
task.overtime | Execution exceeds EXPECTED_DURATION_MINUTES |
task.completed | Normal completion with stored and noCVEs stats |
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 |
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
| File | Purpose |
|---|---|
cmd/cert-us-rss-processor/main.go | Main processor — fetch, parse, store, notify |
internal/certus/types.go | RSS feed structs, Advisory, CVSSVector |
internal/certus/parser.go | Feed parsing, HTML extraction (CVE IDs, CVSS, CWE, URLs, sections) |
internal/certus/mapper.go | Advisory → []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).
See the S3 Persistence Contract for the full reason taxonomy.