GHSA RSS (Atom) Advisory Processor
Overview
Fetches the GitHub Security Advisory Atom feed hourly and seeds CVEMetadata
records for new advisories. Complements the daily ghsa-git-processor by
providing near-real-time visibility into newly published GHSA advisories.
Uses source="github" (same as the git processor) so that the git processor’s
daily run upserts full OSV data over the lightweight records created here.
Feed
- URL:
https://github.com/security-advisories.atom - Format: Atom 1.0 (
xmlns="http://www.w3.org/2005/Atom") - Size: ~70 most recent entries (no pagination)
- Auth: None required (public feed)
Entry Structure
<entry>
<id>tag:github.com,2008:GHSA-xxxx-xxxx-xxxx</id>
<published>2026-03-23T04:05:36Z</published>
<updated>2026-03-23T04:05:36Z</updated>
<title type="html">[GHSA-xxxx-xxxx-xxxx] Summary text</title>
<category term="NPM"/>
<content type="html">
<!-- HTML-encoded advisory body containing:
- Description paragraph(s)
- Affected Packages (<dl> with <dt>/<dd>)
- References (<ul>)
- CWEs (<ul>)
- CVSS vector and score -->
</content>
</entry>
HTML Content Fields
| Field | HTML Pattern | Example |
|---|---|---|
| Description | First <p> before “Affected Packages” | Advisory summary text |
| Package name | <dt> | proxy.py, uipathisfun |
| Ecosystem | <dd>Ecosystem: X</dd> | pip, npm, maven |
| Severity | <dd>Severity: X</dd> | critical, high, moderate, low |
| Versions | <dd>Versions: X</dd> | < 2.3.1, >= 0 |
| Fixed in | <dd>Fixed in: X</dd> | 2.3.1 (optional) |
| References | <li> under “References” | NVD, GitHub advisory URLs |
| CWEs | <li>CWE-NNN - Description</li> | CWE-506, CWE-287 |
| CVSS vector | After <strong>CVSS:</strong> | CVSS:4.0/AV:N/... |
| CVSS score | After <strong>CVSS score:</strong> | 8.7 |
Package Layout
cmd/ghsa-rss-processor/
main.go — single-file processor (Atom fetch, HTML parse, DB store)
No internal package needed — the HTML parsing is specific to this feed format
and not reusable. Shared DB operations come from internal/db/.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--force | bool | false | Reprocess all entries even if hash unchanged |
--limit | int | 0 | Max entries to process (0 = unlimited) |
Processing Flow
- Connect to DB (read + write pools)
- Check concurrency lock (
BulkDataDumpTrackerkeyghsa_rss) - Acquire lock; defer release
- Fetch Atom feed with retry (3 attempts)
- Load resume set (
sourceFileHashper CVE for source=“github”) - For each
<entry>:- Extract GHSA ID from
<id>tag - Parse HTML content for all fields
- Extract CVE IDs via regex
- Select primary ID: first CVE ID found, else GHSA ID
- Detect malware: “Malware in” title or CWE-506
- Compute hash: SHA1(ghsaID|updated|content)
- Skip if hash matches and
--forcenot set - Store in transaction with retry:
- UpsertCVEMetadata (source=“github”)
- UpdateSourceFileHash
- InsertDescriptions
- InsertReferences
- InsertMetrics (CVSS)
- InsertProblemTypes (CWEs)
- UpsertAffected + InsertVersions (per package)
- InsertAliases (GHSA ↔ CVE)
- EnrichAffectedWithDependency
- Extract GHSA ID from
- Release lock
DB Impact
Tables written (all existing — no migrations needed):
CVEMetadata— primary record (cveId, source=“github”)CVEMetadataReferences— advisory and reference URLsCVEDescription— advisory description textCVEMetric— CVSS vector/scoreCVEProblemType— CWE entriesCVEAffected— affected packagesCVEAffectedVersion— version rangesCVEAlias— GHSA ↔ CVE linkageBulkDataDumpTracker— concurrency lockDependency,PackageVersion,PackageVersionCVE— enrichment
Upsert compatibility: Records use source="github" and the same table
schemas as ghsa-git-processor. The git processor’s daily upsert will
overwrite all fields with full OSV data, preserving the primary key.
Malware Detection
Two signals from the Atom feed:
- Title pattern: contains “Malware in” (case-insensitive)
- CWE-506: Embedded Malicious Code
Either signal sets isMaliciousPackage = true on the CVEMetadata record.
Malware Threat-Actor Enrichment
When an entry is flagged malicious — a “Malware in” title, CWE-506
(Embedded Malicious Code), or a MAL-* alias — the record qualifies for
threat-actor attribution under source='github'. After the advisory’s store
transaction commits, a first-time-only post-commit pass invokes the shared
attribution engine (internal/actorintel), which resolves the malware author
from package/repo identity plus upstream registry, GitHub, and Docker Hub
lookups. Results land in the generic, shared MalwareThreatActor edge
(cveId, cveSource='github' → ThreatActor) and MalwareAttribution
status tables — not the OSM-only OsmThreat* tables. A (cveId, source)
already carrying a MalwareAttribution row is never re-enriched.
| Aspect | Behaviour |
|---|---|
| Detection rule | “Malware in” title, CWE-506, or a MAL-* alias |
| Attribution tables | MalwareThreatActor, MalwareAttribution, shared ThreatActor + ThreatActorKey |
| Attribution basis | repo owner / container namespace / registry maintainer / Go-module repo / commit author, per the engine’s identity heuristics |
| Hijack handling | compromised-account advisories mark the maintainer as hijack-victim-excluded (the victim is NOT attributed) |
| Impersonation trap | for non-Go packages, a declared repository is often the dependency-confusion / typosquat target (e.g. a legit org’s repo) — captured as claimedRepo*, NEVER attributed as an actor |
| GitHub key harvest | public SSH-auth, SSH-signing, and GPG keys → ThreatActorKey (OpenSSH SHA256 fingerprint / GPG key id); a reused fingerprint links operators across accounts |
The existing backlog of malicious records is cleared by the one-time
cmd/malware-actor-backfill (just go-malware-actor-backfill), which selects
isMaliciousPackage=true AND source<>'osm' across all malware sources.
Decisions
- source=“github” rather than a separate source — enables seamless upsert by the git processor without creating duplicate records.
- Atom XML parsing via encoding/xml — the feed is well-structured Atom 1.0; no external library needed.
- HTML parsing via strings/regexp — the HTML content is machine-generated by GitHub with a consistent structure. A full HTML parser would be overkill.
- Concurrency lock — prevents overlapping hourly runs via
BulkDataDumpTracker(same pattern as enrichment tasks). - 256 CPU / 512 MB — the feed is small (~70 entries) and processing is lightweight (no git clone, no large JSON parsing).
AI Enrichment (aienrich)
After each advisory’s transaction commits successfully, the processor
fires a single-target aienrich.RunBatch for the primary GHSA id.
Wire-in: cmd/ghsa-rss-processor/main.go immediately after
uploader.ArchiveRecord. The target carries CveID = primaryID,
Source = "github", Aliases = adv.cveIDs.
Five passes fire (all gated by enricher.Enabled()):
| Pass | Persists to |
|---|---|
vulnetix.affected | CVEAffected.{modules, programFiles, programRoutines} |
vulnetix.attack | CVEAttackTechnique + children |
vulnetix.cwe | CVEProblemType (descriptionType = "CWE", derivedBy = "vulnetix") |
vulnetix.treesitter | CVETreeSitterQuery + CVETreeSitterCapture + CVETreeSitterPredicate |
vulnetix.ghsapoc | Exploit (source=vulnetix) + Markdown PoC in S3 |
RunBatch is invoked outside the parent transaction. See the aienrich overview for the full activity contract, environment variables, and operator runbook.
S3 Persistence
Not used. This processor does not currently archive payloads or quarantine failures to S3. Per the S3 Persistence Contract this is non-compliant — see the compliance matrix for the implementation roadmap.
Expected paths when implemented:
- Archive:
ghsa/files/{sha256}/{filename} - Quarantine:
failed-feeds/ghsa-rss-processor/{YYYY-MM-DD}/{reason}/{filename} - Likely reasons:
parse-error