CERT-AU (AUSCERT) RSS Processor — Design
Overview
Fetches security bulletins from the AUSCERT (Australian Computer Emergency
Response Team) RSS 2.0 feed and creates CVEMetadata rows
(source=cert-au) under original upstream CVE IDs.
AUSCERT is a non-profit CERT that redistributes advisories from upstream vendors (Red Hat, Ubuntu, SUSE, Debian, Cisco, Microsoft, etc.) with standardised summaries including CVSS and EPSS data. Each bulletin always references one or more upstream CVE IDs. Items without CVE IDs are skipped.
Feed
| Property | Value |
|---|---|
| URL | https://portal.auscert.org.au/rss/bulletins/ |
| Auth | None — fully public |
| Format | RSS 2.0 outer wrapper; <content:encoded> payload is AUSCERT’s own fixed-width preformatted text bulletin schema (not HTML) |
| Language | English (en) |
| Items | ~100 most-recent bulletins (rolling window) |
Payload Format
The <content:encoded> field is not HTML content. It contains an
HTML-escaped <pre> block whose inner text is AUSCERT’s proprietary
fixed-width bulletin format:
===========================================================================
AUSCERT External Security Bulletin Redistribution
ESB-2026.3146
389-ds-base security update
31 March 2026
===========================================================================
AUSCERT Security Bulletin Summary
---------------------------------
Product: 389-ds-base
Publisher: Red Hat
Operating System: Red Hat
Resolution: Patch/Upgrade
CVE Names: CVE-2025-14905
Original Bulletin:
https://access.redhat.com/errata/RHSA-2026:6220
Comment: CVSS (Max): 7.2 CVE-2025-14905 (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)
CVSS Source: Red Hat
Calculator: https://www.first.org/cvss/calculator/3.1#CVSS:3.1/...
EPSS (Max): 0.5% (64th) CVE-2025-14905 2026-03-30
Multi-CVE bulletins have a multi-line CVE Names: block:
CVE Names: CVE-2025-14905 CVE-2025-14906
CVE-2025-14907
Continuation lines start with whitespace and contain additional CVE IDs until a blank line or the next key-value field.
Parsing
Step 1 — RSS XML unmarshalling
Standard RSS 2.0 XML. The <content:encoded> field is bound via the
http://purl.org/rss/1.0/modules/content/ namespace.
Step 2 — Payload extraction
- HTML-unescape the
content:encodedvalue (handles<,&, etc.) - A second unescape pass handles double-encoded entities (rare)
- Strip the
<pre>/</pre>wrapper - Iterate lines with a state machine
Step 3 — State machine line parsing
| State | Trigger | Action |
|---|---|---|
| initial | "AUSCERT Security Bulletin Summary" | → inSummary |
inSummary | Key: value (≥2 spaces after colon) | Store Product / Publisher / OS / Resolution |
inSummary | CVE Names: ... | Collect CVE IDs; enter inCVENames continuation |
inCVENames | line starts with whitespace | Append more CVE IDs |
inCVENames | non-indented line | Exit continuation |
| any | "Original Bulletin:" | → inOriginalBulletin |
inOriginalBulletin | next non-empty http(s):// line | Store OriginalBulletinURL |
| any | "Comment:" | → inComment; parse the same line |
inComment | every line | parseCommentLine() |
Step 4 — Comment block extraction
parseCommentLine() applies three regexes to each line:
| Regex | Captures |
|---|---|
CVSS \(Max\):\s+([\d.]+)\s+(CVE-[^\s]+)\s+\(([^)]+)\) | score, primary CVE ID, vector string |
CVSS Source:\s*(.+) | CVSS scoring authority (e.g., "Red Hat") |
EPSS \(Max\):\s+([\d.]+)%\s+\((\d+) | EPSS percentage, percentile integer |
Field summary
| Field | Source | Notes |
|---|---|---|
AdvisoryID | ESB-\d{4}\.\d+ regex on full text | Header block |
URL | <link> (fallback <guid>) | Canonical bulletin URL |
Title | <title> | e.g., "389-ds-base: CVSS (Max): 7.2" |
Product | Product: key-value line | |
Publisher | Publisher: key-value line | Used as vendor |
OperatingSystem | Operating System: line | |
Resolution | Resolution: line | e.g., "Patch/Upgrade" |
CVEIDs | CVE Names: + continuation lines | Regex CVE-\d{4}-\d{4,} |
OriginalBulletinURL | URL after Original Bulletin: | Vendor advisory link |
CVSSScore | Comment: CVSS (Max): | Float64 |
CVSSVector | Parenthesised string on CVSS line | e.g., CVSS:3.1/AV:N/... |
CVSSSeverity | Derived from score | CRITICAL / HIGH / MEDIUM / LOW / NONE |
CVSSSourceName | CVSS Source: line | e.g., "Red Hat" |
EPSSScore | EPSS (Max): line | Float64 percentage |
EPSSPercentile | EPSS (Max): line | Integer (e.g., 64) |
PubDateUnix | <pubDate> RFC 2822 → Unix seconds | |
FileHash | SHA1(guid|title|pubDate) | Dedup key |
ID Strategy
One CVEMetadata row is created per AUSCERT bulletin, using the ESB
advisory ID (e.g. ESB-2026.3146) as the primary cveId. All CVE IDs
referenced in the bulletin (CVE-YYYY-NNNN) are stored as CVEAlias
entries pointing back to the ESB ID.
Two skip conditions apply, in this order:
- Bulletins whose parsed
CVEIDslist is empty are skipped before mapping (cmd/cert-au-rss-processor/main.go:131) — nothing is stored for them. - Bulletins with no ESB ID return
nilfromMapAdvisory(internal/certau/mapper.go:16) and are counted asnoCVEs.
Because the primary id is ESB-… rather than CVE-…, the multi-CVE bundle
suppression in db.InsertAliases does not apply: every CVE listed in the
bulletin gets an alias edge. Edges are only written for CVE ids that already
have a CVEMetadata row from some other source — a CVE that no other source has
seen yet is skipped rather than materialised as a placeholder.
Storage
No new tables or columns. All tables already exist.
| Table | Rows inserted |
|---|---|
CVEMetadata | One per bulletin; source="cert-au", cveId=ESB-YYYY.NNNN |
CVEDescription | One per bulletin; containerType="cna", lang="en" (full bulletin text) |
CVEMetadataReferences | AUSCERT bulletin URL (type="advisory") + original vendor URL (type classified by domain) |
CVEMetric | CVSS score + vector when present (metricType="cvssV3_1") |
CVEAffected | Product as product name, Publisher as vendor |
CVEAlias | One edge per (CVE in the bulletin, source holding that CVE) pair — see Alias fan-out |
Alias fan-out.
MapAdvisorypasses the bulletin’s whole CVE list through asAliases(internal/certau/mapper.go:34).db.InsertAliasesthen writes one edge per (alias CVE, source that carries it) pair, so a bulletin listing N CVEs produces roughly N × (number of sources holding those CVEs) rows. AUSCERT redistribution bulletins routinely list hundreds of CVEs — the largest currently stored lists 1,210 — and the multi-CVE bundle guard indb.InsertAliasesdoes not apply here because it only fires when the primary id is itself CVE-prefixed.cert-auaccounts for ~373kCVEAliasrows as a result.
The parsed CVSS vector is written to
CVEMetric.vectorStringbut not to the denormalisedCVEMetadata.vectorStringcolumn — the mapper never setsCVESourceData.VectorString. Consumers that read the metadata column (rather than joiningCVEMetric) see NULL for everycert-aurow.
Reference type classification
The OriginalBulletinURL is classified by domain:
| Domain pattern | type |
|---|---|
redhat.com, ubuntu.com, suse.com, debian.org, cisco.com, microsoft.com, fortiguard.com, oracle.com, sonicwall.com, ivanti.com, vmware.com, citrix.com, broadcom.com, canonical.com | vendor |
cisa.gov, cert.*, ncsc*, github.com/advisories | advisory |
nvd.nist.gov, cve.org, github.com (other) | technical |
| (default) | advisory |
Incremental Strategy
On startup, load all sourceAdvisoryRef values from CVEMetadata where
source='cert-au' into a map[string]bool. Per bulletin: if the URL is in
the set and --all/--force is false, skip. After successful processing,
add the URL to the in-memory set.
Resume is URL-based (not hash-based) because AUSCERT bulletin URLs are
stable and permanent. The --force flag re-stores all bulletins regardless
of whether they’ve been seen before.
Flags
| Flag | Default | Description |
|---|---|---|
--all | false | Reprocess all bulletins, not just new ones |
--limit | 0 | Maximum bulletins to process (0 = unlimited) |
--force | false | Force reprocessing even if content unchanged |
HTTP Client
Standard httpclient.New(30s) with browser headers. The AUSCERT portal is
publicly accessible without IP restrictions; no special HTTP/1.1 forcing is
required. Feed is fetched before database connection. Up to 3 retries with
attempt * 2s backoff.
ECS Schedule
Runs once daily (cron(0 6 * * ? *)). The rolling-window feed returns ~100
items; deduplication ensures only new bulletins are stored on each run.
Key Files
| File | Purpose |
|---|---|
cmd/cert-au-rss-processor/main.go | Main processor |
internal/certau/types.go | RSS feed and advisory Go structs |
internal/certau/parser.go | RSS XML parsing, preformatted text state machine, CVE/CVSS/EPSS extraction |
internal/certau/mapper.go | Advisory → CVESourceData mapping, reference classification |
S3 Persistence
- Archive path:
cert-au/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/cert-au-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.