SCA

CPE enrichment for C++ third-party libraries

Solving the CPE/PURL gap in Conan-based builds to enable reliable NVD vulnerability matching.

Software composition analysis (SCA) works well for npm, Maven or PyPI. For C and C++ it has lagged behind for years. Libraries were vendored into source trees, installed from the operating system or built with custom scripts, and nothing recorded which versions ended up in a product.

Conan changed that. It gives C and C++ projects a resolved, versioned dependency graph, and with it the basis for a real SBOM. But there’s a gap between knowing which libraries you use and getting alerted when one of them has a vulnerability. In most Conan pipelines I’ve seen, that gap is the missing CPE identifier. The SBOM exists, the vulnerability scanner runs, the dashboard is green, and the CVEs in your OpenSSL or libcurl version never show up.

This article explains why that happens and how to fix it with a small, maintainable enrichment step.

Generate the SBOM from the build graph

For C++, the SBOM should come from the dependency graph that Conan resolves at build time. That’s the authoritative record of what went into the build: every direct and transitive dependency, with its exact version.

Binary scanning is no substitute. It works by recognizing version strings, file hashes or byte patterns in compiled code, and C++ release builds defeat it in several ways:

  • Release builds are usually stripped, so there are no symbols to go on.
  • Static linking merges library code into your binary, and the component boundaries disappear.
  • Libraries built with patches or unusual compiler flags no longer look like the reference binaries the scanner knows.

Binary scanning is still a useful second layer for code that bypasses Conan, such as vendored sources or system libraries. But the build graph is your ground truth.

Two tools can produce the SBOM:

  • cdxgen reads conan.lock (Conan 1 and 2) or conanfile.txt and writes a CycloneDX SBOM. It doesn’t read dependencies declared only in conanfile.py, so create and commit a lockfile with conan lock create.
  • Conan 2 has its own experimental SBOM tools, cyclonedx_1_4 and cyclonedx_1_6, which you call from a hook. They work from the full dependency graph without a lockfile, but the feature is still marked as subject to breaking changes.

The gap: no CPE

Here’s what cdxgen produces for a Conan component today:

{
  "type": "library",
  "name": "openssl",
  "version": "3.3.2",
  "purl": "pkg:conan/[email protected]"
}

It has a Package URL (PURL), but no CPE. That matters because vulnerability databases don’t look up components by name. Each source uses its own identifier:

Vulnerability sourceMatches onCovers Conan packages?
NIST National Vulnerability Database (NVD)CPEYes, through CPE. This is where most C and C++ CVEs live.
OSVPURLPartly, through its ConanCenter ecosystem
Sonatype OSS IndexPURLPartly, for packages it knows
GitHub Advisory DatabasePURLNo. It doesn't cover the Conan ecosystem.

For most other ecosystems, a PURL gets you good coverage. For C++, the vulnerabilities in libraries like OpenSSL, zlib, libcurl or libxml2 are recorded in the NVD against CPE identifiers, and the PURL-based sources only cover part of them. Without a CPE, your SBOM can’t be matched against the source that matters most.

OWASP Dependency-Track shows this clearly. It matches NVD data against the CPE in your SBOM. Version 4 had a fuzzy matching option that tried to guess CPEs from component names. Dependency-Track 5 dropped it, so a component without a CPE gets no NVD findings at all. If you’re running Dependency-Track, our guide on deploying Dependency-Track 5 with Docker Compose covers the setup.

The failure is silent. The scan completes, no vulnerabilities are reported, and the dashboard looks clean. Not because your dependencies are safe, but because the tool couldn’t look them up.

Why the CPE can’t be generated automatically

A CPE 2.3 identifier for a library looks like this:

cpe:2.3:a:<vendor>:<product>:<version>:*:*:*:*:*:*:*

The a stands for application. The vendor and product names come from the NVD’s CPE dictionary, which NIST builds independently of any package registry. Many of these names were set years before Conan existed, and they often don’t match the Conan package name:

Conan packageNVD vendorNVD product
opensslopensslopenssl
expatlibexpatexpat
libcurlhaxxlibcurl
libxml2xmlsoftlibxml2
libjpeg-turbolibjpeg-turbo, and d.r.commander for older versionslibjpeg-turbo

There’s no rule that gets you from libcurl to haxx. And a keyword search in the dictionary returns distribution packages too: searching for libpng first returns Red Hat’s redhat:libpng entries, not the upstream libpng:libpng. The only reliable way is for a person to check the NVD CPE dictionary once per library, look at which vendor and product the recent CVEs use, and record the result.

That’s also why SBOM generators don’t do it. For npm or Maven, a heuristic mapping works well enough. For C++, it would produce wrong CPEs, and a wrong CPE is worse than none: it fails just as silently.

Two more things to know about CPEs:

  • A library with no CVE history may have no CPE yet. NIST mostly creates CPE entries while analyzing CVEs. That’s fine: there’s nothing to match. But the mapping needs to be revisited when the first CVE appears.
  • The NVD is enriching fewer CVEs. Since April 2026, NIST adds CPE data mainly to CVEs in CISA’s Known Exploited Vulnerabilities catalog and to software used by the US federal government; other CVEs are marked “Not Scheduled” and may never get CPE data. CPE matching remains essential for C++, but it’s no longer enough on its own, which is why you add PURLs and more than one vulnerability source as well.

The fix: curated CPE metadata plus a post-processing step

The approach has two parts:

  1. Record the verified CPE vendor and product for each third-party library, once.
  2. After the SBOM is generated, add the CPE to each component, together with a PURL if the generator didn’t add one.

Store only the vendor and product, never the full CPE string. The post-processor adds the version from the SBOM. When you upgrade from OpenSSL 3.3.2 to 3.4.1, nothing in your CPE metadata needs to change, and the CPE can’t drift out of sync with the version you actually ship.

Where to keep the metadata

If you maintain your own Conan recipes, put the metadata in the recipe. It already holds the component’s name, homepage and license, and the CPE belongs next to them:

class OpensslConan(ConanFile):
    name = "openssl"
    homepage = "https://www.openssl.org"
    license = "Apache-2.0"

    # Verified against the NVD CPE dictionary
    cpe_vendor = "openssl"
    cpe_product = "openssl"

Conan ignores attributes it doesn’t know, so this doesn’t affect the build. A short script can collect the values from all recipes without executing them:

"""Collect cpe_vendor/cpe_product from Conan recipes without executing them."""
import ast
import json
import pathlib
import sys

def recipe_metadata(path):
    tree = ast.parse(path.read_text(encoding="utf-8"))
    for node in tree.body:
        if isinstance(node, ast.ClassDef):
            values = {}
            for stmt in node.body:
                if isinstance(stmt, ast.Assign) and isinstance(stmt.value, ast.Constant):
                    for target in stmt.targets:
                        if isinstance(target, ast.Name):
                            values[target.id] = stmt.value.value
            if "name" in values:
                return values
    return None

mapping = {}
for recipe in pathlib.Path(sys.argv[1]).rglob("conanfile.py"):
    meta = recipe_metadata(recipe)
    if meta and meta.get("cpe_vendor") and meta.get("cpe_product"):
        mapping[meta["name"]] = {"cpe_vendor": meta["cpe_vendor"], "cpe_product": meta["cpe_product"]}

json.dump(mapping, sys.stdout, indent=2, sort_keys=True)

If you consume recipes straight from ConanCenter, you can’t add attributes to them. Keep a mapping file in a repository instead, reviewed like code:

{
  "expat":   { "cpe_vendor": "libexpat", "cpe_product": "expat" },
  "libcurl": { "cpe_vendor": "haxx",     "cpe_product": "libcurl" },
  "libxml2": { "cpe_vendor": "xmlsoft",  "cpe_product": "libxml2" }
}

If you proxy ConanCenter through a private remote, for example in Artifactory, maintain one central mapping file for everything that passes through it, so every project benefits. Mixed setups work too: merge the values from your own recipes with the mapping file for public packages.

The post-processor

The enrichment step runs right after the SBOM is generated and before it’s uploaded anywhere:

"""Add CPE and Conan PURL identifiers to a CycloneDX SBOM."""
import json
import sys

sbom_path, map_path, out_path = sys.argv[1:4]
sbom = json.load(open(sbom_path, encoding="utf-8"))
cpe_map = json.load(open(map_path, encoding="utf-8"))

missing = []
for comp in sbom.get("components", []):
    name, version = comp.get("name"), comp.get("version")
    if not name or not version:
        continue
    comp.setdefault("purl", f"pkg:conan/{name}@{version}")
    entry = cpe_map.get(name)
    if entry:
        comp["cpe"] = f"cpe:2.3:a:{entry['cpe_vendor']}:{entry['cpe_product']}:{version}:*:*:*:*:*:*:*"
    elif "cpe" not in comp:
        missing.append(f"{name}@{version}")

json.dump(sbom, open(out_path, "w", encoding="utf-8"), indent=2)

for item in missing:
    print(f"WARNING: no CPE mapping for {item}", file=sys.stderr)

After enrichment, the component looks like this:

{
  "type": "library",
  "name": "openssl",
  "version": "3.3.2",
  "purl": "pkg:conan/[email protected]",
  "cpe": "cpe:2.3:a:openssl:openssl:3.3.2:*:*:*:*:*:*:*"
}

Validate the result against the CycloneDX schema before you upload it, for example with the CycloneDX CLI or cyclonedx-python-lib.

Two practical details. First, Conan versions don’t always match the NVD’s. ConanCenter uses versions like cci.20230101 for libraries without releases, and the NVD sometimes writes versions differently from upstream tags. Check a few CVEs per library to confirm the version formats line up, and add a version override to the mapping where they don’t. Second, treat every warning as a gap to close: add the mapping, or record why the library has none.

The full pipeline

Put together, the pipeline looks like this:

  1. Conan resolves the dependency graph, pulling packages through your private remote.
  2. The build compiles and links.
  3. cdxgen, or Conan’s SBOM hook, generates the base SBOM.
  4. The post-processor adds CPEs and PURLs, warns about unmapped components and validates the result.
  5. The binaries and the enriched SBOM are stored together in your artifact repository.
  6. Dependency-Track, or another SCA platform, ingests the SBOM, matches it against NVD, OSV and other sources, and keeps watching it.

That last point is easy to underestimate. When a CVE is published for a library you shipped six months ago, Dependency-Track matches it against the SBOMs it already has and alerts you, but only if the CPE was there when the SBOM was uploaded. That retroactive matching is exactly what the EU Cyber Resilience Act expects: identifying the components in your product and monitoring them for vulnerabilities throughout the support period. Our CRA guide for SMBs covers those obligations.

Add a build gate as well. Fail the build when a component with a critical vulnerability is introduced, warn on high, and report the rest. Developers then learn about a vulnerable dependency in the same pipeline run that introduced it, when the fix is cheapest. Continuous monitoring catches what wasn’t known at build time. You need both.

Edge cases

Internal libraries that other teams in your organization build and publish as Conan packages have no NVD entries and shouldn’t get a CPE. Give them a PURL with a repository_url qualifier pointing to your private remote, so they stay visible in the inventory and in license checks.

Dependencies outside the Conan graph are the real blind spot: vendored source code, libraries installed from the build machine’s package manager, and anything pulled in with CMake’s FetchContent or ExternalProject. They never reach the SBOM. Move them into Conan recipes where you can. Where you can’t, add them to the SBOM in the post-processing step with a hand-curated CPE, and audit for new ones regularly.

For C++ libraries that don’t live in any registry, there’s no good PURL type yet; pkg:generic exists but few vulnerability sources match on it. The PURL community is discussing a new type for non-packaged software, and PURL itself became an international standard, ECMA-427, in December 2025. Until a type for unpackaged software lands, CPE remains the most reliable identifier for those libraries.

Design the program around the people who use it

Enrichment fixes the data. Whether the SCA program works depends on whether it answers the right questions at each stage of development:

Design
  • How do we help developers pick secure components?
Implementation
  • How do we stop developers from importing bad components?
Build
  • How do we make it easy to create an accurate SBOM for existing software?
Verification
  • How can QA verify that there are no new SCA findings before release?
Operation
  • How do we meet post-release security requirements?

And it has to serve every team that depends on it, not only the security team:

TeamWhat they need from the SCA program
GRC and legalA view of the overall risk from third-party components across the company
AppSecA dashboard to monitor, triage and follow up, and answers for questions from other departments
DevOpsNotifications about new findings, and a clear process for adding new repositories
DevelopersNotifications about new findings, and an SCA tool every developer can use to assess and triage them
SupportSBOM exports on customer request, recorded decisions on vulnerabilities to answer questions, and VEX documents
SalesA clear description of the SCA approach for tenders, SBOMs with an acceptable number of vulnerabilities, and recent VEX documents

A program that only produces findings for the security team will stall. One that gives support a VEX document to send and sales an answer for a tender questionnaire gets used.

Rolling it out

A realistic plan for an existing codebase takes about eight weeks:

  1. Weeks 1 and 2, audit. Run your SBOM generator on the current build and check how many components have a CPE. Inventory your recipes, check which libraries exist in the NVD CPE dictionary, and start with the riskiest: cryptography, networking and parsers.
  2. Weeks 3 to 5, enrich. Add the CPE vendor and product to every third-party recipe or to the mapping file, verified against the NVD. Make CPE metadata a review requirement for every new recipe, and document internal components separately.
  3. Weeks 6 and 7, build the post-processor. Add it to the pipeline after SBOM generation, validate the output against the CycloneDX schema, and test end to end: upload an enriched SBOM and confirm that known CVEs appear.
  4. Week 8, enable monitoring. Ingest the enriched SBOMs into your SCA platform, configure alerts for critical and high vulnerabilities, add the build gate, and hand the process over to the teams that will run it.

Key takeaways

  • Generate C++ SBOMs from the Conan dependency graph, and treat binary scanning as a second layer.
  • SBOM generators add a pkg:conan PURL at best. They don’t add CPEs, and for C++ the CPE is what connects you to the NVD.
  • Verify CPE vendor and product names by hand against the NVD dictionary, once per library, and store them without the version.
  • Add the CPE in a post-processing step, and treat every unmapped component as a gap to close.
  • Don’t rely on the NVD alone: since 2026 it enriches fewer CVEs, so combine CPE matching with PURL-based sources.
  • Track what bypasses Conan: vendored code, system libraries and CMake-fetched dependencies.

If you want help designing or reviewing your SCA program, book a free call.