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
| Property | Value |
|---|---|
| Source name | cisco |
| Enumeration API | https://sec.cloudapps.cisco.com/security/center/getCvrfPaginationRecords.x (JSON; cisco.PaginationAPI) |
| Advisory URL | https://sec.cloudapps.cisco.com/security/center/contentxml/{typeName}/{identifier}/cvrf/{fileName}.xml |
| Advisory format | CSAF-CVRF v1.2 XML |
| Data type | cvrf (structured CVRF XML over HTTP) |
Naming
| System | Value |
|---|---|
| cmd/ directory | cisco-cvrf-processor, cisco-cvrf-backfill |
| ECR image tag | go-cisco-cvrf-processor-latest |
| ECS task family | go-cisco-cvrf-processor |
| EventBridge schedule | go-cisco-cvrf-processor (daily at 11:00 UTC, cron(0 11 * * ? *)) |
| Containerfile stage | cisco-cvrf-processor |
| justfile backfill | go-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:
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.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 Field | DB Column | Notes |
|---|---|---|
DocumentTracking.Identification.ID | cveId | e.g. cisco-sa-20231004-example |
"cisco" | source | |
DocumentTracking.Status | state | "Draft"→"DRAFT", else "PUBLISHED" |
DocumentTracking.InitialReleaseDate | datePublished | Unix seconds |
DocumentTracking.CurrentReleaseDate | dateUpdated | Unix seconds (omitted if equal to datePublished) |
DocumentTitle | title | |
First DocumentReferences.Reference[Type=Self].URL | sourceAdvisoryRef | |
json.Marshal(doc) | rawDataJSON | JSON representation of parsed Document struct |
SHA256(rawXML) | sourceFileHash | Used for per-advisory resume |
"Cisco" | affectedVendor | Always “Cisco” |
| First FullProductName for first affected product | affectedProduct |
CVEDescription (per DocumentNote + per Remediation)
| Source | containerType | lang |
|---|---|---|
DocumentNotes.Note.Value | "cna" | "en" |
"[Remediation: {Type}] {Description}" for each Remediation | "cna" | "en" |
CVEMetadataReferences
| Source | type | referenceSource |
|---|---|---|
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 Field | Stored as alias |
|---|---|
Vulnerability.CVE | CVE 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 Field | DB Column | Notes |
|---|---|---|
ScoreSet.Vector | vectorString | |
ScoreSet.BaseScore | baseScore | |
| Detected from Vector prefix | metricType | CVSS: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>, butinternal/cisco/types.go:96decodesCVSSScoreSets>ScoreSetwithBaseScore/Vectorchild elements (internal/cisco/types.go:113-118). The element names never match, sovuln.CVSSScoreSetsis always empty andinternal/cisco/mapper.go:92-106emits no metrics. Production evidence: 8,137 of 8,139source='cisco'rows havevectorString IS NULL(the two exceptions predate the current mapper). Fix = addScoreSetV3/ScoreSetV2variants withBaseScoreV3/VectorV3(and the V2 spellings) to theVulnerabilitystruct.
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)
| Field | Value |
|---|---|
vendor | "Cisco" |
product | FullProductName.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:
| Writer | Row shape | Production count (2026-08-06) |
|---|---|---|
db.InsertAliases → db.EnsureMinimalCVEMetadata for each Cisco Bug ID alias (internal/db/cvealias.go:130-150) | cveId="CSCxx12345", state="PUBLISHED", datePublished=0 | 2,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 run | included above (both are title-less) |
cisco.MapToSourceData | cveId="cisco-sa-…" real advisory | 4,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}.xml— non-canonical; the S3 Persistence Contract (and the generated section below) specifycisco/files/{sha256}/{filename}.cmd/cisco-cvrf-backfillwrites 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/
| File | Responsibility |
|---|---|
types.go | CVRF XML Go struct definitions + source constants + ListingEntry |
parser.go | FetchListingEntries (pagination API), FetchAdvisory, ParseCVRF |
mapper.go | MapToSourceData — Document → osv.CVESourceData |
Reused Utilities
| Utility | Package | Usage |
|---|---|---|
db.NewPool | internal/db | DB connection pool |
db.GetTracker / db.UpsertTracker | internal/db | Listing SHA256 tracker |
db.LoadProcessedHashes | internal/db | Per-advisory resume set |
db.InsertArtifact | internal/db | S3 artifact record |
db.InsertLinkWithArtifact | internal/db | Link → Artifact |
db.UpdateCVEMetadataFileLinkID | internal/db | Link CVEMetadata to artifact |
db.WithTx | internal/db | Transaction wrapper with savepoints |
processor.StoreCVESourceData | internal/processor | Write 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).
See the S3 Persistence Contract for the full reason taxonomy.