CPE (Common Platform Enumeration) Generation

CPE 2.3 identifiers are generated for CVEAffected entries that lack upstream CPE data. They follow the NIST IR 7695 specification for Well-Formed Names and enable consumers to match vulnerabilities against product inventories using standard CPE matching algorithms.

Architecture

flowchart TB subgraph Sources["CPE Sources"] Upstream["Upstream CPEs
CVEAffected.cpes JSON array
(from NVD, EUVD, cve.org)"] Generated["Generated CPEs
BuildCPE23() from vendor+product
(when upstream is null/empty)"] end subgraph Builder["CPE 2.3 Builder"] Normalise["NormalizeCPEField()
lowercase
spaces → underscores
escape special chars"] Assemble["Assemble 13-component URI
cpe:2.3:{part}:{vendor}:{product}
:{version}:{update}:{edition}
:{language}:{sw_edition}
:{target_sw}:{target_hw}:{other}"] end subgraph Dedup["Deduplication"] Merge["DeduplicateCpes()
merge upstream + generated
preserve order
remove duplicates"] end subgraph Persist["Persistence"] WriteBack["UPDATE CVEAffected
SET cpes = merged_array
(async, fire-and-forget)"] end Upstream -->|"exists"| Merge Generated -->|"when upstream null/empty"| Merge Merge --> WriteBack subgraph When["When Generated"] Check{"CVEAffected.cpes
null or []?"} Yes["BuildCPE23()
Part: 'a'
Vendor: from CVEAffected
Product: from CVEAffected"] No["Use upstream as-is"] end Check -->|"yes"| Yes Check -->|"no"| No Yes --> Builder Builder --> Normalise --> Assemble --> Generated

CPE 2.3 Format

The CPE 2.3 formatted string has exactly 13 colon-separated components:

cpe:2.3:<part>:<vendor>:<product>:<version>:<update>:<edition>:<language>:<sw_edition>:<target_sw>:<target_hw>:<other>
ComponentPositionDescriptionDefault
cpe:2.3prefixFixed CPE 2.3 identifier-
part1a (application), o (OS), h (hardware)a
vendor2Vendor/publisher namerequired
product3Product namerequired
version4Version string* (any)
update5Update/patch level*
edition6Edition designation*
language7Language tag (RFC 5646)*
sw_edition8Software edition*
target_sw9Target software environment*
target_hw10Target hardware platform*
other11Any other designation*

* = “any value” (wildcard). - = “not applicable”.

Normalisation Rules

flowchart LR Raw["Raw String
'Vendor (Inc.)'"] Lower["Lowercase
'vendor (inc.)'"] Spaces["Spaces → Underscores
'vendor_(inc.)'"] Escape["Escape Special Chars
'vendor_\\(inc.\\)'"] Result["CPE Field
'vendor_\\(inc.\\)'"] Raw --> Lower --> Spaces --> Escape --> Result

Character Escaping

The following characters are backslash-escaped per NIST IR 7695 section 6.2:

! " # $ % & ' ( ) * + , / : ; < = > ? @ [ \ ] ^ ` { | } ~

Characters that are NOT escaped (commonly appear in valid CPE values):

  • Period . — common in version strings (e.g., 2.4.51)
  • Hyphen - — common in product names (e.g., http-server)
  • Underscore _ — used as space replacement

Examples

Input VendorInput ProductGenerated CPE
apachehttp_servercpe:2.3:a:apache:http_server:*:*:*:*:*:*:*:*
MicrosoftWindows 10cpe:2.3:a:microsoft:windows_10:*:*:*:*:*:*:*:*
Vendor (Inc.)My Product!cpe:2.3:a:vendor_\(inc.\):my_product\!:*:*:*:*:*:*:*:*
AWSCloudWatchcpe:2.3:a:aws:cloudwatch:*:*:*:*:*:*:*:*
amazonworkspacescpe:2.3:a:amazon:workspaces:*:*:*:*:*:*:*:*
(empty)(empty)"" (empty — both required)

Vendor Suffix Stripping

Before CPE generation, vendor names have corporate suffixes removed for better matching against NVD’s CPE dictionary (1.6M+ entries):

SuffixExampleResult
_inc, _inc.Amazon_Inc.amazon
_llcGoogle_LLCgoogle
_ltd, _ltd.Acme_Ltd.acme
_corp, _corp., _corporationMicrosoft_Corporationmicrosoft
_co, _co., _companyAcme_Co.acme
_gmbh, _ag, _sa, _bvSAP_AGsap

Generation Conditions

flowchart TD Start["CVEAffected row
vendor + product"] CheckCpes{"cpes field
null or empty []?"} HasUpstream["Use upstream CPEs
no generation needed"] CheckVP{"vendor AND product
both non-empty?"} Skip["Skip generation
(insufficient data)"] Generate["BuildCPE23()
Part: 'a'
Vendor: normalised vendor
Product: normalised product"] Dedup["DeduplicateCpes()
append generated to array
check for duplicates"] Start --> CheckCpes CheckCpes -->|"has CPEs"| HasUpstream CheckCpes -->|"null or []"| CheckVP CheckVP -->|"both present"| Generate CheckVP -->|"either empty"| Skip Generate --> Dedup

CPE generation runs in these contexts:

  1. v2_affected endpoint — for each CVEAffected row where cpes is null/empty
  2. package_search endpoint — from the vendor/product fields in enrichment data
  3. v2_cloud_locators endpoint — standalone CPE generation from query parameters
  4. Synthesized entries — when no CVEAffected exists, generated from title-derived services

Parsing (Round-trip)

ParseCPE23() decomposes a CPE 2.3 string back into its components, handling backslash-escaped colons within field values:

Input:  cpe:2.3:a:vendor\:name:product:1.0:*:*:*:*:*:*:*
Output: CPEComponents{
    Part:    "a",
    Vendor:  "vendor\:name",  ← escaped colon preserved
    Product: "product",
    Version: "1.0",
    ...remaining fields: "*"
}

The parser splits on unescaped colons (colons not preceded by \) to correctly handle vendor/product names containing literal colons.

Deduplication

DeduplicateCpes() is applied as the last step before response and persistence:

  1. Parse existing cpes JSON array from CVEAffected
  2. Track seen CPE strings in a set
  3. Preserve insertion order (first occurrence wins)
  4. Append generated CPE only if not already present
  5. Return the deduplicated array
flowchart LR Upstream["Upstream CPEs
[cpe:2.3:a:apache:struts:2.5.30:...,
cpe:2.3:a:apache:struts:2.5.30:...,
cpe:2.3:a:apache:struts:*:...]"] Generated["Generated CPE
cpe:2.3:a:apache:struts:*:..."] Dedup["DeduplicateCpes()"] Result["Result
[cpe:2.3:a:apache:struts:2.5.30:...,
cpe:2.3:a:apache:struts:*:...]"] Upstream --> Dedup Generated --> Dedup Dedup --> Result

The generated CPE (* version) is only added if not already present. Upstream CPEs with specific versions are preserved and take precedence.

Persistence

Generated CPEs are written back to CVEAffected.cpes asynchronously:

  1. Read current cpes value from DB
  2. Parse existing JSON array
  3. Check if generated CPE already present (dedup)
  4. Append if new, marshal to JSON
  5. UPDATE the row

This runs in a fire-and-forget goroutine so the API response is not blocked.

CPE Dictionary Sources

SourceCoverageNotes
NVD (NIST)1,610,458 CPE namesOfficial CPE dictionary, primary source
EUVD~830K affected with CPEsEuropean Vulnerability Database
cve.org~100K affected with CPEsMITRE CVE records
anchore-adp~24K affected with CPEsContainer-focused CPE data
Generated (VDB)Any CVEAffected without CPEDerived from vendor+product fields

Key Files

FileProjectPurpose
internal/shared/cpe.govdb-apiBuildCPE23, ParseCPE23, NormalizeCPEField, StripVendorSuffix
internal/shared/cpe_test.govdb-api10 unit tests for CPE builder
internal/shared/enrichment_writeback.govdb-apipersistCpe (async write-back), DeduplicateCpes
internal/handler/v2_affected.govdb-apiCPE generation + dedup in affected endpoint
internal/handler/v2_cloud_locators.govdb-apiCPE generation in cloud-locators endpoint
internal/handler/package_search.govdb-apiCPE generation in package search