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
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>
| Component | Position | Description | Default |
|---|---|---|---|
cpe:2.3 | prefix | Fixed CPE 2.3 identifier | - |
part | 1 | a (application), o (OS), h (hardware) | a |
vendor | 2 | Vendor/publisher name | required |
product | 3 | Product name | required |
version | 4 | Version string | * (any) |
update | 5 | Update/patch level | * |
edition | 6 | Edition designation | * |
language | 7 | Language tag (RFC 5646) | * |
sw_edition | 8 | Software edition | * |
target_sw | 9 | Target software environment | * |
target_hw | 10 | Target hardware platform | * |
other | 11 | Any other designation | * |
* = “any value” (wildcard). - = “not applicable”.
Normalisation Rules
'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 Vendor | Input Product | Generated CPE |
|---|---|---|
apache | http_server | cpe:2.3:a:apache:http_server:*:*:*:*:*:*:*:* |
Microsoft | Windows 10 | cpe:2.3:a:microsoft:windows_10:*:*:*:*:*:*:*:* |
Vendor (Inc.) | My Product! | cpe:2.3:a:vendor_\(inc.\):my_product\!:*:*:*:*:*:*:*:* |
AWS | CloudWatch | cpe:2.3:a:aws:cloudwatch:*:*:*:*:*:*:*:* |
amazon | workspaces | cpe: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):
| Suffix | Example | Result |
|---|---|---|
_inc, _inc. | Amazon_Inc. → amazon | |
_llc | Google_LLC → google | |
_ltd, _ltd. | Acme_Ltd. → acme | |
_corp, _corp., _corporation | Microsoft_Corporation → microsoft | |
_co, _co., _company | Acme_Co. → acme | |
_gmbh, _ag, _sa, _bv | SAP_AG → sap |
Generation Conditions
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:
- v2_affected endpoint — for each CVEAffected row where
cpesis null/empty - package_search endpoint — from the vendor/product fields in enrichment data
- v2_cloud_locators endpoint — standalone CPE generation from query parameters
- 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:
- Parse existing
cpesJSON array from CVEAffected - Track seen CPE strings in a set
- Preserve insertion order (first occurrence wins)
- Append generated CPE only if not already present
- Return the deduplicated array
[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:
- Read current
cpesvalue from DB - Parse existing JSON array
- Check if generated CPE already present (dedup)
- Append if new, marshal to JSON
- UPDATE the row
This runs in a fire-and-forget goroutine so the API response is not blocked.
CPE Dictionary Sources
| Source | Coverage | Notes |
|---|---|---|
| NVD (NIST) | 1,610,458 CPE names | Official CPE dictionary, primary source |
| EUVD | ~830K affected with CPEs | European Vulnerability Database |
| cve.org | ~100K affected with CPEs | MITRE CVE records |
| anchore-adp | ~24K affected with CPEs | Container-focused CPE data |
| Generated (VDB) | Any CVEAffected without CPE | Derived from vendor+product fields |
Key Files
| File | Project | Purpose |
|---|---|---|
internal/shared/cpe.go | vdb-api | BuildCPE23, ParseCPE23, NormalizeCPEField, StripVendorSuffix |
internal/shared/cpe_test.go | vdb-api | 10 unit tests for CPE builder |
internal/shared/enrichment_writeback.go | vdb-api | persistCpe (async write-back), DeduplicateCpes |
internal/handler/v2_affected.go | vdb-api | CPE generation + dedup in affected endpoint |
internal/handler/v2_cloud_locators.go | vdb-api | CPE generation in cloud-locators endpoint |
internal/handler/package_search.go | vdb-api | CPE generation in package search |