Design: Salesforce Advisories RSS Processor

Overview

Fetches Salesforce security advisories from the master RSS feed at security.salesforce.com/security-advisories/rss, parses CVE/GHSA aliases and bracketed product lists from each item, stores per-advisory rows under source="salesforce", and (when --emit-crit) stages CRIT candidate envelopes per (vulnID × Salesforce product).

Help articles (help.salesforce.com / kb.tableau.com / help.mulesoft.com) are SPA-rendered and not anonymously parseable; the RSS <description> is the canonical body — no per-article HTML enrichment fetch is needed.

Source identifier: salesforce Data type: rss ECS task name: go-salesforce-advisories-rss-processor Schedule: Runs weekly on Saturdays at 05:00 UTC (cron(0 5 ? * SAT *)). Pattern: A — inline per-record CRIT staging Phase: 1.7


Data Source

ItemValue
Master RSShttps://security.salesforce.com/security-advisories/rss
FormatRSS 2.0
AuthenticationNone
Volume~93 items (2017→present); ~6–28 advisories per year
LicensePublic

RSS item shape

<item>
  <title><![CDATA[[Vulnerability] CVE's for various vulnerabilities impacting some versions of Tableau Server and Tableau Desktop]]></title>
  <link>https://help.salesforce.com/s/articleView?id=005132575&amp;type=1</link>
  <guid>https://help.salesforce.com/s/articleView?id=005132575&amp;type=1</guid>
  <pubDate>Fri, 22 Aug 2025 00:00:00 GMT</pubDate>
  <description><![CDATA[[Tableau] We assigned the CVSSv3 score as 9.1. Affected CVE-2025-26496, CVE-2025-26497. We strongly encourage Tableau Server customers to apply the update. https://www.cve.org/CVERecord?id=CVE-2025-26496]]></description>
</item>

Article-ID extraction

Three URL shapes recur:

  • help.salesforce.com/s/articleView?id=NNNNNNNNN&type=1NNNNNNNNN (numeric)
  • status.salesforce.com/generalmessages/NNNNNNNNNstatus-NNNNNNNNN
  • kb.tableau.com/articles/Issue/{slug}tableau-{slug}
  • help.mulesoft.com/{slug}mulesoft-{slug}
  • Anything else → {host}-{lastPathSegment}

The article ID becomes CVEMetadata.cveId verbatim — no SFDC-YYYY-NNN scheme exists (precedent: AWS slug bulletins).


Architecture / Data Flow

sequenceDiagram participant ECS participant Proc as salesforce-advisories-rss-processor participant SF as security.salesforce.com participant DB as PostgreSQL participant S3 ECS->>Proc: cron(0 5 ? * SAT *) Proc->>SF: GET /security-advisories/rss SF-->>Proc: RSS 2.0 XML Proc->>Proc: xml.Unmarshal → RSSFeed (93 items) Proc->>DB: LoadProcessedHashes(source=salesforce) loop per RSS item Proc->>Proc: CombineRSS → ParsedEntry Proc->>Proc: hash = sha1(articleID | pubDate | sha256(bodyText)[:8]) alt hash unchanged AND not --force Proc->>Proc: skip else Proc->>DB: storeAdvisory (CVEMetadata + Aliases + References + Description) Proc->>S3: archive {sha256}/{articleID}.json opt --emit-crit AND product list resolves Proc->>Proc: stageCRIT (one envelope per vulnID × match) Proc->>S3: PUT crit-candidates/pending/... Proc->>DB: RegisterS3QueueObject(status=pending) end end end Proc->>Proc: critpublisher.DrainKeys (4 workers) Note over Proc,S3: see crit-publisher.design.md for drain detail

Source → Database Mappings

CVEMetadata

RSS fieldColumnNotes
ArticleID(<link>)cveIdVerbatim. Numeric (005132575), status-*, tableau-*, or mulesoft-*.
"salesforce"sourceConstant.
<pubDate> (RFC 2822)datePublishedUnix seconds. Falls back to time.Now() on parse failure.
<title> (CDATA)titleBracketed prefix retained ([Vulnerability] ...).
"PUBLISHED"stateConstant.
"5.0"dataVersionConstant.
<link>sourceAdvisoryRefArticle URL.
sha1(articleID | pubDate | sha256(bodyText)[:8])sourceFileHashResume detection.

CVEAlias

CVE-* and GHSA-* extracted from <title> + <description> via regex. Passed to db.InsertAliases. Empty list still triggers same-cveId backfill (per AGENTS.md alias contract).

GHSA canonicalisation: GHSA- prefix preserved in upper-case + body in lower-case (e.g. GHSA-5x4f-fvv8-wr65).

CVEMetadataReferences

  • Always include the article URL as the first reference (advisory type).
  • All https?://... URLs from <description> extracted via regex.
  • Filtered against isDenyHost (analytics + asset CDNs).
  • Deduped via urlSeen map.

Type classification:

  • self-link → advisory
  • contains cve.org / cve.mitre.org / /security/advisories/ghsa- / nvd.nist.gov/vuln/detail/cve-advisory
  • contains /releases/tag/patch
  • everything else → web

CVEDescription

Body text truncated to 1500 chars → one row with containerType="cna", lang="en".

S3 archive

salesforce/files/{sha256}/{articleID}.json

Service Mapping

service_map.go:Resolve walks the bracketed product list and resolves to spec/extended dict tuples.

Bracket extraction (rss.go:ExtractProducts + parser.go:filterCategoryLeaders)

The leading [..] of title or description contains either a category leader or a product list:

  • Category leaders (filtered out): Vulnerability, Security Advisory, Security Update, Security Notification, Security Enhancements, Tableau Security Advisory.
  • Product lists (kept): [Tableau, Slack, Service Cloud, ...] comma-separated.

Resolution order (more-specific first):

Bracket text contains (lc)ServiceResource typeDict
“tableau cloud” / “tableau online”tableaucloud-siteextended
“tableau server” / “tableau desktop” / “tableau prep” / “tableau”tableauserverextended
“marketing cloud account engagement”pardotaccountextended
“marketing cloud”marketing_cloudaccountextended
“pardot”pardotaccountextended
“sales cloud”sales_cloudorgextended
“service cloud”service_cloudorgextended
“commerce cloud” / “b2c commerce”commerce_cloudstoreextended
“experience cloud” / “community cloud”experience_cloudsiteextended
“salesforce platform” / “force.com”salesforce_platformorgextended
“hyperforce”hyperforceinstanceextended
“heroku”herokuappextended
“mulesoft” / “anypoint”mulesoftenvironmentextended
“slack”slackworkspaceextended
“quip”quipsiteextended
“clicksoftware”clicksoftwareinstanceextended
“agentforce”agentforceagentextended
“salesforce einstein” / “einstein”einsteincopilotextended

Extended-dict layer (internal/critutil/dictionaries/extended/salesforce.json): 17 entries with template_format=salesforce_url. Spec dict salesforce.json covers SObject record-level entries (crm/account, tooling/apex_class) — disjoint from this layer.


Business Rules

Resume / re-fetch (main.go:entryHash)

RuleConditionalRationale
R1 Re-fetch only when content changesif !*force && resumeSet[entry.ArticleID] == hash { continue }Hash combines articleID + pubDate + sha256(bodyText)[:8].
R2 Skip on missing article IDif entry.ArticleID == "" { continue }Logged + skipped.
R3 Skip when limit reachedif *limit > 0 && i >= *limit { break }Smoke-test cap.
R4 Soft-deadline guardif time.Now().After(softDeadline.Add(-5*time.Minute))ECS-only safety.

RSS parsing (rss.go:ExtractProducts, parser.go:filterCategoryLeaders)

RuleConditionalRationale
R5 Description bracket wins over title bracketpe.Products = filterCategoryLeaders(ExtractProducts(pe.BodyText)); if len(pe.Products) == 0 { ExtractProducts(pe.Title) }Title bracket is typically a category leader ([Vulnerability]); description bracket holds the actual product list.
R6 Filter category leaders from product listif categoryLeaders[lc] { continue }Words like “Vulnerability” / “Security Advisory” must not be mistaken for products.

CVSS extraction (parser.go:CombineRSS)

RuleConditional
R7 First CVSS in body winscvssRe.FindStringSubmatch(pe.BodyText) (single match).
R8 Severity from CVSSseverityFromCVSS — same thresholds as ServiceNow (≥9 Critical / ≥7 High / ≥4 Medium / >0 Low).

Service mapping (service_map.go:Resolve)

RuleConditional
R9 First synonym match per (service, resource_type) winsfor _, s := range productSynonyms { ... break } — order matters for Tableau Cloud-vs-Server and Pardot-vs-MarketingCloud.
R10 Unmapped products surfacedif !matched && lc != "" { unmappedProducts = append(...) } — logged so future extended dict entries can be added.
R11 Skip CRIT staging when no products resolveif len(matches) == 0 { return nil } — CVEMetadata + references still land.

fix_propagation inference (crit_mapper.go:inferFixPropagation)

Body signal (lc)vex_statusfix_propagationRule
“no action is required” / “no customer action” / “patch was applied to cloud instances” / “cloud customers do not need to take action” / “automatically updated” / “salesforce has applied”fixedautomaticR12 Hosted-fix language.
“rotate” + (“credential” | “key” | “token” | “secret”)fixedcredential_rotationR13
“transition from” + (“tls” | “rsa” | “cipher”)fixedconfig_changeR14 Crypto transition.
“configure” + “best practices” / “configuration change” / “review the recommended configuration”fixedconfig_changeR15
“we strongly encourage”+“update” / “we recommend”+“upgrade” / “users should upgrade” / “apply the update” / “apply the patch”fixedversion_updateR16
Default when CVSS ≥ 7.0fixedversion_updateR17
Default otherwisefixedautomaticR18

Confidence ladder (crit_mapper.go:confidenceFor)

RuleConditions
R19 low when no body aliases!bodyHasAliases — covers the ~89 of 93 informational notices without CVEs.
R20 high when text-pinned + non-zero CVSS + body aliasesfromText && cvssScore > 0 (with bodyHasAliases true).
R21 medium otherwise.

Reference filtering (parser.go:isDenyHost)

RulePattern
R22 Drop AWS asset CDNURL contains .awsstatic.com.
R23 Drop Adobe / Demdex analyticsURL contains amazonwebservicesinc.tt.omtrdc.net or .demdex.net.
R24 Drop generic analyticsURL contains google-analytics.com or doubleclick.net.
R25 Always include the RSS <link> firsturlSeen[pe.URL] = true; pe.RefURLs = append(pe.RefURLs, pe.URL).

vex_status / shared_responsibility coupling

Same as ServiceNow R23–R24:

  • R26 automaticprovider_only.
  • R27 existing_deployments_remain_vulnerable is false iff provider_only + automatic.

Drain (main.go)

  • R28 Drain skipped when CRIT_DISABLE_INPROCESS_DRAIN=true.
  • R29 Drain skipped when len(stagedKeys) == 0.
  • R30 Drain workers fixed at 4.

Verification Queries

SELECT count(*) FROM "CVEMetadata" WHERE source='salesforce';
-- expected: ~91-93

-- Article-ID primary keys (no CVE/GHSA pattern)
SELECT count(*) FROM "CVEMetadata"
 WHERE source='salesforce' AND "cveId" NOT LIKE 'CVE-%' AND "cveId" NOT LIKE 'GHSA-%';
-- expected: equal to row count

-- GHSA aliases extracted from body (recent advisories)
SELECT count(*) FROM "CVEAlias"
 WHERE "discoveredFrom"='salesforce' AND "aliasCveId" LIKE 'GHSA-%';
-- expected: non-zero

-- Reference coverage
SELECT count(*) FROM "CVEMetadataReferences"
 WHERE source='salesforce'
   AND "cveId" IN (SELECT "cveId" FROM "CVEMetadata" WHERE source='salesforce')
 GROUP BY "cveId" HAVING count(*) = 0;
-- expected: 0 rows

-- Service distribution (provider=salesforce)
SELECT service, count(*) FROM "CritRecord"
 WHERE provider='salesforce' GROUP BY 1 ORDER BY 2 DESC;
-- expected weighting: tableau dominant, then mulesoft / salesforce_platform / heroku / etc

-- Pending should be empty
SELECT "processingStatus", count(*) FROM "S3QueueObject"
 WHERE source='salesforce-advisories-rss-processor' GROUP BY 1;
-- expected: only `inserted`

Risk Surface

RiskGuard
Salesforce rotates the RSS endpointThe /security-advisories/rss path has been stable since 2017; processor fails fast if it 404s.
help/kb article rendered SPA-onlyWe don’t fetch the article — the RSS description is canonical.
Advisory carries an unmapped productCVEMetadata + references still land; CRIT skipped with unmappedProducts logged.
Spec dict’s record-level salesforce entries collide with extended product-levelDifferent (service, resource_type) keys → no collision.
GHSA canonicalisation driftTests in parser_test.go lock the form GHSA- + lowercase body.

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.

⚠ Not in the compliance matrix — status needs verification.

Expected paths when implemented:

  • Archive: salesforce-advisories/files/{sha256}/{filename}
  • Quarantine: failed-feeds/salesforce-advisories-rss-processor/{YYYY-MM-DD}/{reason}/{filename}
  • Likely reasons: (none documented)