Design: AWS ALAS RSS Processor
Overview
Fetches Amazon Linux Security Advisories (ALAS) from the three official RSS feeds (AL1, AL2, AL2023), parses package-level CVE references, stores per-advisory rows under source="amazon" keyed by ALAS-* / ALAS2-* / ALAS2023-* IDs, and stages CRIT candidate envelopes uniformly as (provider=aws, service=ec2, resource_type=instance) — every ALAS advisory affects EC2 instances running the corresponding Amazon Linux AMI.
This is the first Tier-1 CRIT producer to ship (Phase 1.1) and the first to operationalise the Pattern A inline contract that every later producer follows.
Source identifier: amazon
Data type: rss
ECS task name: go-alas-rss-processor
Schedule: every 6 hours — cron(0 */6 * * ? *)
(terraform/go-schedules.tf:909), expected_duration_minutes = 60
Pattern: A — inline per-record CRIT staging
Phase: 1.1
CRIT staging is off in ECS.
--emit-critdefaults tofalse(main.go:98) and the scheduled task passes only["/app/alas-rss-processor"](terraform/go-schedules.tf:910). Thego-alas-rss-backfilljustfile recipe defaultsEMIT_CRIT="true"(justfile:2523), so everything below about CRIT candidates,S3QueueObjectandCritRecorddescribes local backfills only. Advisory ingestion itself is unaffected.
This processor replaced a different source model on 2026-04-30 (commit
7003b49). Rows written before that date live underamazon-linux,amazon-linux-2andamazon-linux-2023and were not migrated — see alas-processor (superseded).
Data Source
| Item | Value |
|---|---|
| AL1 feed | https://alas.aws.amazon.com/alas.rss |
| AL2 feed | https://alas.aws.amazon.com/AL2/alas.rss |
| AL2023 feed | https://alas.aws.amazon.com/AL2023/alas.rss |
| Format | RSS 2.0 |
| Authentication | None |
| Volume | 8,136 advisory rows in production (2026-08-06): AL1 1,835, AL2 3,725, AL2023 2,578 by CVEAffected.collectionURL. The RSS feeds are rolling windows, not full history, so this grows forward rather than converging on Amazon’s whole 2011-onwards archive |
| License | Public |
RSS item shape
<item>
<title>Amazon Linux 2: ALAS2-2024-2475: medium priority package update for kernel</title>
<link>https://alas.aws.amazon.com/AL2/ALAS-2024-2475.html</link>
<guid>https://alas.aws.amazon.com/AL2/ALAS-2024-2475.html</guid>
<pubDate>Wed, 03 Jul 2024 21:14:00 GMT</pubDate>
<description><![CDATA[ ... CVE-2024-XXXX, CVE-2024-YYYY ... ]]></description>
</item>
The title encodes the advisory ID, severity (“medium priority package update”), and primary package name. CVEs are listed in the description body.
Architecture / Data Flow
Source → Database Mappings
CVEMetadata
| RSS field | Column | Notes |
|---|---|---|
Parsed advisory ID (ALAS-2024-2475 / ALAS2-2024-2475 / ALAS2023-2024-XXX) | cveId | Verbatim. Distinct prefixes per release line. |
"amazon" | source | Constant. Distinct from aws-security-bulletins (Phase 1.6). |
<pubDate> (RFC 2822) | datePublished | Unix seconds. |
Composed: "{advisoryID}: {pkgName} ({severity})" | title | |
"PUBLISHED" | state | Constant. |
"5.0" | dataVersion | Constant. |
<link> | sourceAdvisoryRef | |
sha1(advisoryURL | pubDate | description) | sourceFileHash |
CVEAlias
CVE-* IDs extracted from description via regex. Always passed through db.InsertAliases(ctx, tx, advisoryID, "amazon", cveIDs, logger). Empty list still triggers same-cveId backfill.
CVEMetadataReferences
- The
<link>URL → reference typeadvisory. - That’s the only reference; ALAS RSS descriptions don’t carry external URLs.
CVEAffected — one row per advisory when a package name was parsed
Written at main.go:448. vendor="Amazon", product and packageName both the
package from the title, and collectionURL carries the release line
(AL1 / AL2 / AL2023, main.go:446) — this is where the per-release-line
distinction lives now that all three feeds share one source. affectedHash is
the standard MD5(vendor|##|product|##|collectionURL|##|packageName).
Immediately after, db.EnrichAffectedWithDependency (main.go:460) runs inside
the same transaction under its own savepoint, so a stored advisory can also
create or update rows in the shared Dependency / registry tables and resolve an
upstream GitHub repository.
CVEMetric — severity
One metricType="other" row per advisory with otherType="alas-severity" and
otherContent = {"severity":"medium"} (main.go:472). ALAS publishes a
four-level severity (critical / important / medium / low) and no CVSS
vector, which is why vectorString is NULL for all 8,136 rows — an upstream
limitation, not a mapper gap.
CVEDescription — not written
Earlier revisions of this page claimed a description row per advisory. There is
none: writeAdvisory (main.go:401-491) writes CVEMetadata, the
sourceFileHash, one reference, one affected row (+ dependency enrichment), one
metric, and the aliases. The RSS <description> is used only to extract CVE ids
and to compute the resume hash.
S3 archive
amazon/files/{sha256}/{advisoryID}.json
The archive contains the raw RSS item JSON for downstream re-processing.
Service Mapping (none — uniform)
ALAS advisories ALWAYS map to (aws, ec2, instance) regardless of the package or feed. Amazon Linux is the AMI underlying EC2 Linux instances; every ALAS represents an EC2-affecting fix.
No synonym table, no per-advisory product extraction. The CRIT mapper hardcodes the natural key.
This uniformity is the distinction from aws-security-bulletins-json-processor (Phase 1.6) which produces heterogeneous service mappings (Bedrock, RDS, Lambda, EKS) for service-level AWS bulletins.
Business Rules
RSS parsing (main.go:parseTitle, parseCVEIDs)
| Rule | Conditional | Rationale |
|---|---|---|
| R1 Title parse must yield advisory ID + severity + pkg | advisoryID, severity, pkgName, ok := parseTitle(item.Title); if !ok { logger.Warn; continue } | Malformed RSS items are skipped, logged. |
R2 Advisory URL falls back to <link> when <guid> empty | if advisoryURL == "" { advisoryURL = item.Link } | Some legacy ALAS entries have empty guid. |
| R3 Hash combines URL + pubDate + description | itemHash(advisoryURL, item.PubDate, item.Description) | Resume detection — content-aware. |
| R4 Resume cache hit short-circuits | if !*force && resumeSet[advisoryID] == hash { continue } | Skips unchanged advisories. |
Per-advisory transaction (main.go:storeAdvisory)
| Rule | Conditional |
|---|---|
| R5 Quarantine on transaction failure | if !txOK { uploader.QuarantineRecord(ctx, "store-error", payload); continue } — failed stores end up in crit-quarantine/store-error/ for inspection. |
| R6 Archive only on success | if txOK { uploader.ArchiveRecord(...) } — success path puts the payload to amazon/files/{sha256}/.... |
CRIT staging (main.go inline + crit_mapper.go:mapALASToCRIT)
| Rule | Conditional |
|---|---|
| R7 vulnIDs default to advisory CVE list | vulnIDs := cveIDs; if len(vulnIDs) == 0 { vulnIDs = []string{advisoryID} } — ALAS without CVE refs (rare) keys CRIT to the ALAS-id. |
| R8 Reject empty vulnID | if vulnID == "" { return ok=false } — defensive. |
| R9 Prefer canonical CVE pubdate | if lookup != nil && strings.HasPrefix(vulnID, "CVE-") { canonicalPubDate, found = lookup(vulnID) } — falls back to ALAS pubdate. |
| R10 ALAS pubdate IS the provider_fix_date | if pubDateSec > 0 { fixDate = time.Unix(pubDateSec, 0).UTC().Format("2006-01-02") } — ALAS only publishes when a fix is available; the RSS pubDate equals the fix release date. |
| R11 ServiceAvailableDate fallback to EC2 launch (2006-08-21) | if !found { saDate = "2006-08-21" } — never produce a candidate without a service_available_date. |
| R12 Hardcoded natural key | Provider="aws", Service="ec2", ResourceType="instance" — every ALAS becomes an EC2-instance candidate. |
R13 Hardcoded resource_lifecycle = stateful_customer | EC2 instances are customer-managed (the customer chooses AMI version + reboots). |
R14 Hardcoded shared_responsibility = customer_action_required | The customer must rebuild AMIs / restart instances. |
R15 Hardcoded existing_deployments_remain_vulnerable = true | Pre-existing instances stay on the old AMI until rebuilt. |
R16 fix_propagation from helper suggestion | helpers.SuggestFixPropagation("stateful_customer", "customer_action_required", true) — typically returns version_update. |
R17 vex_status from helper | helpers.SuggestVEXStatus("PUBLISHED", true, false) — returns fixed for ALAS (always published with a fix). |
| R18 Detection placeholder | pre_fix detection with pending_reason="query_in_development". |
Confidence (crit_mapper.go)
| Rule | Conditions |
|---|---|
R19 Always high for ALAS | All inputs are structural (the AMI provider IS Amazon, the service IS EC2). No ambiguity. |
Drain (main.go)
- R20 Drain skipped on
CRIT_DISABLE_INPROCESS_DRAIN. - R21 4 workers.
Verification Queries
-- Advisory count (cumulative across feeds)
SELECT count(*) FROM "CVEMetadata" WHERE source='amazon';
-- expected: 13,000+ rows (covers AL1, AL2, AL2023 since 2011)
-- Distinct prefix coverage (one count per release line)
SELECT
count(*) FILTER (WHERE "cveId" LIKE 'ALAS-%') AS al1,
count(*) FILTER (WHERE "cveId" LIKE 'ALAS2-%') AS al2,
count(*) FILTER (WHERE "cveId" LIKE 'ALAS2023-%') AS al2023
FROM "CVEMetadata" WHERE source='amazon';
-- CVE alias edges
SELECT count(*) FROM "CVEAlias"
WHERE "discoveredFrom"='amazon' AND "primaryCveId" LIKE 'CVE-%';
-- expected: very high — ALAS rows reference 1+ CVEs each
-- All ALAS CRIT records share natural key (aws, ec2, instance)
SELECT service, "resourceType", count(*) FROM "CritRecord"
WHERE provider='aws'
AND "critJSON"->'provider_advisory'->>'advisory_id' LIKE 'ALAS%'
GROUP BY 1, 2;
-- expected: only one row, ec2/instance
-- Pending should be empty
SELECT "processingStatus", count(*) FROM "S3QueueObject"
WHERE source='alas-rss-processor' GROUP BY 1;
Risk Surface
| Risk | Guard |
|---|---|
| ALAS RSS endpoint changes | Each feed is fetched independently; one failing doesn’t block the others. Backoff on HTTP errors. |
| Title format drift | parseTitle regex tested against historical samples; breakage logs and skips the item. |
| New AMI release line (AL2024 or beyond) | Add a new feed entry to allFeeds; everything else is uniform. |
| Per-package severity inflation across CVEs in one advisory | We use the title-level severity for all CVEs in the advisory — acceptable given EC2-instance natural key is the same. |
| Massive volume (~13k rows + per-CVE CRIT envelopes) | Hourly cadence + resume-by-hash skips unchanged; full backfill drains in ~10 minutes. |
| Consumer can’t distinguish ALAS-style EC2 from service-level EC2 (AWS Security Bulletins) | The provider_advisory.advisory_id field encodes which producer staged the row (ALAS-* vs AWS-YYYY-NNN). |
S3 Persistence
Compliant. The generator printed “Not used” here because
scripts/docs/s3-status.yaml recorded status: none for this slug; that entry
was wrong (cf. ORCH-09) and has been corrected.
- Archive:
amazon/files/{sha256}/{advisoryID}.json—uploader.ArchiveRecord(ctx, amazonSource, payload)atmain.go:258, on the success path only, with the payload built bys3client.MarshalRecordatmain.go:246. - Quarantine:
failed-feeds/alas-rss-processor/{YYYY-MM-DD}/store-error/{advisoryID}.json—uploader.QuarantineRecordatmain.go:255. - Failure reasons emitted:
store-erroronly.parse-erroris not emitted: a title that failsparseTitleis logged and skipped atmain.go:225with no S3 write, and a feed whose XML fails to parse is abandoned wholesale atmain.go:195without quarantining the body. Both are gaps against the contract, not reasons in use.
Skipped entirely when S3_BUCKET_NAME is unset — s3client.NewFromEnv returns a
nil uploader and both methods are nil-receiver no-ops.