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-crit defaults to false (main.go:98) and the scheduled task passes only ["/app/alas-rss-processor"] (terraform/go-schedules.tf:910). The go-alas-rss-backfill justfile recipe defaults EMIT_CRIT="true" (justfile:2523), so everything below about CRIT candidates, S3QueueObject and CritRecord describes 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 under amazon-linux, amazon-linux-2 and amazon-linux-2023 and were not migrated — see alas-processor (superseded).


Data Source

ItemValue
AL1 feedhttps://alas.aws.amazon.com/alas.rss
AL2 feedhttps://alas.aws.amazon.com/AL2/alas.rss
AL2023 feedhttps://alas.aws.amazon.com/AL2023/alas.rss
FormatRSS 2.0
AuthenticationNone
Volume8,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
LicensePublic

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

sequenceDiagram participant ECS participant Proc as alas-rss-processor participant ALAS as alas.aws.amazon.com participant DB as PostgreSQL participant S3 ECS->>Proc: cron(0 */6 * * ? *) loop per feed (AL1, AL2, AL2023) Proc->>ALAS: GET /alas.rss (AL1) / /AL2/alas.rss / /AL2023/alas.rss ALAS-->>Proc: RSS 2.0 XML Proc->>DB: LoadProcessedHashes(source=amazon) — re-run per feed, main.go:203 loop per item Proc->>Proc: parseTitle → (advisoryID, severity, pkgName) Proc->>Proc: parseCVEIDs(description) Proc->>Proc: hash = sha1(advisoryURL | pubDate | description) alt hash unchanged AND not --force Proc->>Proc: skip else Proc->>DB: storeAdvisory (CVEMetadata + Aliases + Reference) Proc->>S3: archive opt --emit-crit Proc->>Proc: stageCRIT (one envelope per CVE × (aws,ec2,instance)) Proc->>S3: PUT crit-candidates/pending/... Proc->>DB: RegisterS3QueueObject end end end end Proc->>Proc: critpublisher.DrainKeys

Source → Database Mappings

CVEMetadata

RSS fieldColumnNotes
Parsed advisory ID (ALAS-2024-2475 / ALAS2-2024-2475 / ALAS2023-2024-XXX)cveIdVerbatim. Distinct prefixes per release line.
"amazon"sourceConstant. Distinct from aws-security-bulletins (Phase 1.6).
<pubDate> (RFC 2822)datePublishedUnix seconds.
Composed: "{advisoryID}: {pkgName} ({severity})"title
"PUBLISHED"stateConstant.
"5.0"dataVersionConstant.
<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 type advisory.
  • 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.

CVEDescriptionnot 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)

RuleConditionalRationale
R1 Title parse must yield advisory ID + severity + pkgadvisoryID, 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> emptyif advisoryURL == "" { advisoryURL = item.Link }Some legacy ALAS entries have empty guid.
R3 Hash combines URL + pubDate + descriptionitemHash(advisoryURL, item.PubDate, item.Description)Resume detection — content-aware.
R4 Resume cache hit short-circuitsif !*force && resumeSet[advisoryID] == hash { continue }Skips unchanged advisories.

Per-advisory transaction (main.go:storeAdvisory)

RuleConditional
R5 Quarantine on transaction failureif !txOK { uploader.QuarantineRecord(ctx, "store-error", payload); continue } — failed stores end up in crit-quarantine/store-error/ for inspection.
R6 Archive only on successif txOK { uploader.ArchiveRecord(...) } — success path puts the payload to amazon/files/{sha256}/....

CRIT staging (main.go inline + crit_mapper.go:mapALASToCRIT)

RuleConditional
R7 vulnIDs default to advisory CVE listvulnIDs := cveIDs; if len(vulnIDs) == 0 { vulnIDs = []string{advisoryID} } — ALAS without CVE refs (rare) keys CRIT to the ALAS-id.
R8 Reject empty vulnIDif vulnID == "" { return ok=false } — defensive.
R9 Prefer canonical CVE pubdateif lookup != nil && strings.HasPrefix(vulnID, "CVE-") { canonicalPubDate, found = lookup(vulnID) } — falls back to ALAS pubdate.
R10 ALAS pubdate IS the provider_fix_dateif 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 keyProvider="aws", Service="ec2", ResourceType="instance" — every ALAS becomes an EC2-instance candidate.
R13 Hardcoded resource_lifecycle = stateful_customerEC2 instances are customer-managed (the customer chooses AMI version + reboots).
R14 Hardcoded shared_responsibility = customer_action_requiredThe customer must rebuild AMIs / restart instances.
R15 Hardcoded existing_deployments_remain_vulnerable = truePre-existing instances stay on the old AMI until rebuilt.
R16 fix_propagation from helper suggestionhelpers.SuggestFixPropagation("stateful_customer", "customer_action_required", true) — typically returns version_update.
R17 vex_status from helperhelpers.SuggestVEXStatus("PUBLISHED", true, false) — returns fixed for ALAS (always published with a fix).
R18 Detection placeholderpre_fix detection with pending_reason="query_in_development".

Confidence (crit_mapper.go)

RuleConditions
R19 Always high for ALASAll 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

RiskGuard
ALAS RSS endpoint changesEach feed is fetched independently; one failing doesn’t block the others. Backoff on HTTP errors.
Title format driftparseTitle 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 advisoryWe 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}.jsonuploader.ArchiveRecord(ctx, amazonSource, payload) at main.go:258, on the success path only, with the payload built by s3client.MarshalRecord at main.go:246.
  • Quarantine: failed-feeds/alas-rss-processor/{YYYY-MM-DD}/store-error/{advisoryID}.jsonuploader.QuarantineRecord at main.go:255.
  • Failure reasons emitted: store-error only. parse-error is not emitted: a title that fails parseTitle is logged and skipped at main.go:225 with no S3 write, and a feed whose XML fails to parse is abandoned wholesale at main.go:195 without 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.