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

PropertyValue
URLhttps://portal.auscert.org.au/rss/bulletins/
AuthNone — fully public
FormatRSS 2.0 outer wrapper; <content:encoded> payload is AUSCERT’s own fixed-width preformatted text bulletin schema (not HTML)
LanguageEnglish (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

  1. HTML-unescape the content:encoded value (handles &lt;, &amp;, etc.)
  2. A second unescape pass handles double-encoded entities (rare)
  3. Strip the <pre> / </pre> wrapper
  4. Iterate lines with a state machine

Step 3 — State machine line parsing

StateTriggerAction
initial"AUSCERT Security Bulletin Summary"inSummary
inSummaryKey: value (≥2 spaces after colon)Store Product / Publisher / OS / Resolution
inSummaryCVE Names: ...Collect CVE IDs; enter inCVENames continuation
inCVENamesline starts with whitespaceAppend more CVE IDs
inCVENamesnon-indented lineExit continuation
any"Original Bulletin:"inOriginalBulletin
inOriginalBulletinnext non-empty http(s):// lineStore OriginalBulletinURL
any"Comment:"inComment; parse the same line
inCommentevery lineparseCommentLine()

Step 4 — Comment block extraction

parseCommentLine() applies three regexes to each line:

RegexCaptures
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

FieldSourceNotes
AdvisoryIDESB-\d{4}\.\d+ regex on full textHeader block
URL<link> (fallback <guid>)Canonical bulletin URL
Title<title>e.g., "389-ds-base: CVSS (Max): 7.2"
ProductProduct: key-value line
PublisherPublisher: key-value lineUsed as vendor
OperatingSystemOperating System: line
ResolutionResolution: linee.g., "Patch/Upgrade"
CVEIDsCVE Names: + continuation linesRegex CVE-\d{4}-\d{4,}
OriginalBulletinURLURL after Original Bulletin:Vendor advisory link
CVSSScoreComment: CVSS (Max):Float64
CVSSVectorParenthesised string on CVSS linee.g., CVSS:3.1/AV:N/...
CVSSSeverityDerived from scoreCRITICAL / HIGH / MEDIUM / LOW / NONE
CVSSSourceNameCVSS Source: linee.g., "Red Hat"
EPSSScoreEPSS (Max): lineFloat64 percentage
EPSSPercentileEPSS (Max): lineInteger (e.g., 64)
PubDateUnix<pubDate> RFC 2822 → Unix seconds
FileHashSHA1(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:

  1. Bulletins whose parsed CVEIDs list is empty are skipped before mapping (cmd/cert-au-rss-processor/main.go:131) — nothing is stored for them.
  2. Bulletins with no ESB ID return nil from MapAdvisory (internal/certau/mapper.go:16) and are counted as noCVEs.

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.

TableRows inserted
CVEMetadataOne per bulletin; source="cert-au", cveId=ESB-YYYY.NNNN
CVEDescriptionOne per bulletin; containerType="cna", lang="en" (full bulletin text)
CVEMetadataReferencesAUSCERT bulletin URL (type="advisory") + original vendor URL (type classified by domain)
CVEMetricCVSS score + vector when present (metricType="cvssV3_1")
CVEAffectedProduct as product name, Publisher as vendor
CVEAliasOne edge per (CVE in the bulletin, source holding that CVE) pair — see Alias fan-out

Alias fan-out. MapAdvisory passes the bulletin’s whole CVE list through as Aliases (internal/certau/mapper.go:34). db.InsertAliases then 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 in db.InsertAliases does not apply here because it only fires when the primary id is itself CVE-prefixed. cert-au accounts for ~373k CVEAlias rows as a result.

The parsed CVSS vector is written to CVEMetric.vectorString but not to the denormalised CVEMetadata.vectorString column — the mapper never sets CVESourceData.VectorString. Consumers that read the metadata column (rather than joining CVEMetric) see NULL for every cert-au row.

Reference type classification

The OriginalBulletinURL is classified by domain:

Domain patterntype
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.comvendor
cisa.gov, cert.*, ncsc*, github.com/advisoriesadvisory
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

FlagDefaultDescription
--allfalseReprocess all bulletins, not just new ones
--limit0Maximum bulletins to process (0 = unlimited)
--forcefalseForce 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

FilePurpose
cmd/cert-au-rss-processor/main.goMain processor
internal/certau/types.goRSS feed and advisory Go structs
internal/certau/parser.goRSS XML parsing, preformatted text state machine, CVE/CVSS/EPSS extraction
internal/certau/mapper.goAdvisory → 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).

flowchart LR SRC[Source feed] --> PROC[cert-au-rss-processor] PROC -->|success| ARCHIVE[("S3: cert-au/files/{sha256}/{filename}")] PROC -->|failure| Q[("S3: failed-feeds/cert-au-rss-processor/{date}/{reason}/{filename}")] PROC --> DB[(PostgreSQL)]

See the S3 Persistence Contract for the full reason taxonomy.