Cisco CVRF Processor — Design Document

Overview

The Cisco CVRF Processor ingests Cisco security advisories published in CSAF-CVRF XML format (the documents declare the OASIS csaf-cvrf/v1.2 namespace). Advisories are enumerated from the Cisco Security Center pagination API, fetched, parsed, and stored in the VDB database using the standard CVEMetadata pipeline.

Cisco is the primary authority for cisco-sa-* advisory identifiers and the only feed that carries the Cisco Bug ID ↔ CVE mapping, which is what makes a Cisco advisory reachable from a customer’s CSCxx… bug reference.

Source

PropertyValue
Source namecisco
Enumeration APIhttps://sec.cloudapps.cisco.com/security/center/getCvrfPaginationRecords.x (JSON; cisco.PaginationAPI)
Advisory URLhttps://sec.cloudapps.cisco.com/security/center/contentxml/{typeName}/{identifier}/cvrf/{fileName}.xml
Advisory formatCSAF-CVRF v1.2 XML
Data typecvrf (structured CVRF XML over HTTP)

Naming

SystemValue
cmd/ directorycisco-cvrf-processor, cisco-cvrf-backfill
ECR image taggo-cisco-cvrf-processor-latest
ECS task familygo-cisco-cvrf-processor
EventBridge schedulego-cisco-cvrf-processor (daily at 11:00 UTC, cron(0 11 * * ? *))
Containerfile stagecisco-cvrf-processor
justfile backfillgo-cisco-cvrf-backfill
task-manager.toml key[tasks.cisco-cvrf-processor]
CloudWatch log group/ecs/vdb-scheduler/go-cisco-cvrf-processor

Data Flow

Cisco listing page (HTML)
   extract CVRF XML URLs
   for each URL: HTTP GET  raw XML bytes
   ParseCVRF (xml.Unmarshal)
   MapToSourceData (cisco SA ID as cveId, CVEs as aliases)
   processor.StoreCVESourceData (upsert CVEMetadata, descriptions, references, metrics, affected, aliases)
   S3 upload (application/xml)
   db.InsertArtifact + InsertLinkWithArtifact
   db.UpdateCVEMetadataFileLinkID

Resume / Idempotency

Two-level resume strategy:

  1. Listing-page tracker (ECS processor only): SHA256 of the listing page HTML is stored in BulkDataDumpTracker (source=cisco_listing). If unchanged, the processor exits early.

  2. Per-advisory hash: SHA256 of the raw CVRF XML bytes is stored in CVEMetadata.sourceFileHash. If the hash matches, the advisory is skipped on re-run.

The backfill ignores the listing-page tracker and always processes all URLs, using the per-advisory hash for resume unless --force is set.

Data Mapping

CVEMetadata (one row per advisory document)

CVRF FieldDB ColumnNotes
DocumentTracking.Identification.IDcveIde.g. cisco-sa-20231004-example
"cisco"source
DocumentTracking.Statusstate"Draft"→"DRAFT", else "PUBLISHED"
DocumentTracking.InitialReleaseDatedatePublishedUnix seconds
DocumentTracking.CurrentReleaseDatedateUpdatedUnix seconds (omitted if equal to datePublished)
DocumentTitletitle
First DocumentReferences.Reference[Type=Self].URLsourceAdvisoryRef
json.Marshal(doc)rawDataJSONJSON representation of parsed Document struct
SHA256(rawXML)sourceFileHashUsed for per-advisory resume
"Cisco"affectedVendorAlways “Cisco”
First FullProductName for first affected productaffectedProduct

CVEDescription (per DocumentNote + per Remediation)

SourcecontainerTypelang
DocumentNotes.Note.Value"cna""en"
"[Remediation: {Type}] {Description}" for each Remediation"cna""en"

CVEMetadataReferences

SourcetypereferenceSource
DocumentReferences.Reference[Type=Self]"advisory""cisco"
Bug tracker URLs (bst.cloudapps.cisco.com)"issue""cisco"
Other document/vulnerability references"advisory""cisco"
Remediation URLs with type Update/Fix/Patch"patch""cisco"
Other remediation URLs"vendor""cisco"

CVEAlias (per Vulnerability)

CVRF FieldStored as alias
Vulnerability.CVECVE identifier (e.g. CVE-2023-12345)
Vulnerability.ID[SystemName="Cisco Bug ID"]Cisco Bug ID (e.g. CSCxx12345)

CVEMetric (per CVSSScoreSet, deduplicated by vector string)

CVRF FieldDB ColumnNotes
ScoreSet.VectorvectorString
ScoreSet.BaseScorebaseScore
Detected from Vector prefixmetricTypeCVSS:3.1/→cvssV3_1, CVSS:3.0/→cvssV3_0, AV:→cvssV2_0
"cna"containerType

⚠ Broken today — no CVSS is actually stored. Cisco’s CSAF-CVRF v1.2 documents nest the score as <CVSSScoreSets><ScoreSetV3><BaseScoreV3>6.3</BaseScoreV3><VectorV3>CVSS:3.1/…</VectorV3></ScoreSetV3>, but internal/cisco/types.go:96 decodes CVSSScoreSets>ScoreSet with BaseScore/Vector child elements (internal/cisco/types.go:113-118). The element names never match, so vuln.CVSSScoreSets is always empty and internal/cisco/mapper.go:92-106 emits no metrics. Production evidence: 8,137 of 8,139 source='cisco' rows have vectorString IS NULL (the two exceptions predate the current mapper). Fix = add ScoreSetV3/ScoreSetV2 variants with BaseScoreV3/VectorV3 (and the V2 spellings) to the Vulnerability struct.

The shared pipeline still derives one containerType="vulnetix" CVSS v4.0 metric per record from the advisory description (internal/processor/pipeline.go:521-548), so severity is not entirely absent — it is simply Vulnetix-derived rather than Cisco-published.

CVEAffected (per ProductStatus)

FieldValue
vendor"Cisco"
productFullProductName.Name for the product ID (version embedded in name)
affectedHash`MD5(“Cisco

Placeholder and rejected rows under source='cisco'

Not every source='cisco' row is an advisory. Two side effects mint rows that carry no title, no rawDataJSON and datePublished = 0:

WriterRow shapeProduction count (2026-08-06)
db.InsertAliasesdb.EnsureMinimalCVEMetadata for each Cisco Bug ID alias (internal/db/cvealias.go:130-150)cveId="CSCxx12345", state="PUBLISHED", datePublished=02,918
db.StoreSkippedHash when ParseCVRF fails (cmd/cisco-cvrf-processor/main.go:191-195, internal/db/resume.go:67-81)state="REJECTED", hash-only row so the broken advisory is not refetched every runincluded above (both are title-less)
cisco.MapToSourceDatacveId="cisco-sa-…" real advisory4,286 (+935 legacy ids)

Exclude the alias/rejected shells (title IS NULL) from any coverage denominator for this source.

S3 Artifact

  • Key: cisco/advisories/{sha256hex}/{advisoryID}.xmlnon-canonical; the S3 Persistence Contract (and the generated section below) specify cisco/files/{sha256}/{filename}. cmd/cisco-cvrf-backfill writes the canonical path (cmd/cisco-cvrf-backfill/main.go:277), so the same source archives to two different prefixes depending on which binary ran.
  • ContentType: application/xml
  • BomFormat: cisco
  • Type: OTHER

Internal Package: internal/cisco/

FileResponsibility
types.goCVRF XML Go struct definitions + source constants + ListingEntry
parser.goFetchListingEntries (pagination API), FetchAdvisory, ParseCVRF
mapper.goMapToSourceData — Document → osv.CVESourceData

Reused Utilities

UtilityPackageUsage
db.NewPoolinternal/dbDB connection pool
db.GetTracker / db.UpsertTrackerinternal/dbListing SHA256 tracker
db.LoadProcessedHashesinternal/dbPer-advisory resume set
db.InsertArtifactinternal/dbS3 artifact record
db.InsertLinkWithArtifactinternal/dbLink → Artifact
db.UpdateCVEMetadataFileLinkIDinternal/dbLink CVEMetadata to artifact
db.WithTxinternal/dbTransaction wrapper with savepoints
processor.StoreCVESourceDatainternal/processorWrite all CVE sub-records

Local Testing

# Fetch up to 5 advisories from the Cisco listing page (uses .env)
just go-cisco-cvrf-backfill LIMIT=5

# Force reprocess all advisories
just go-cisco-cvrf-backfill FORCE=true

# Verify results
psql "$DATABASE_URL" -c 'SELECT "cveId", "title", "state" FROM "CVEMetadata" WHERE source='"'"'cisco'"'"' LIMIT 10;'
psql "$DATABASE_URL" -c 'SELECT "primaryCveId", "aliasCveId" FROM "CVEAlias" WHERE "primarySource"='"'"'cisco'"'"' LIMIT 10;'
psql "$DATABASE_URL" -c 'SELECT "metricType", "baseScore", "vectorString" FROM "CVEMetric" WHERE source='"'"'cisco'"'"' LIMIT 10;'
psql "$DATABASE_URL" -c 'SELECT "vendor", "product" FROM "CVEAffected" WHERE source='"'"'cisco'"'"' LIMIT 10;'

ECS Deployment

The ECS processor runs daily at 11:00 UTC via EventBridge Scheduler. It uses the listing-page SHA256 tracker to exit early when nothing has changed, keeping costs low.

CPU: 256 units (0.25 vCPU) Memory: 512 MB Architecture: ARM64 (Graviton)

S3 Persistence

  • Archive path: cisco/files/{sha256}/{filename}
  • Quarantine path: failed-feeds/cisco-cvrf-processor/{YYYY-MM-DD}/{reason}/{filename}
  • Failure reasons emitted: parse-error, schema-violation

Uses s3client.Uploader from internal/s3client/uploader.go. Skipped when S3_BUCKET_NAME is unset (local dev).

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

See the S3 Persistence Contract for the full reason taxonomy.