Skip to content

Investigate SBOM CPE normalisation before Dependency-Track import #155

Description

@mrjertop

Summary

I believe we need to investigate adding an SBOM identity normalisation step before importing Syft-generated SBOMs into DependencyTrack.

I have identified there are cases where DependencyTrack can miss vulnerabilities because the CycloneDX SBOM contains a stale or superseded top level CPE, while a perhaps "better" candidate CPE is present, but only as a Syft-specific property DependencyTrack can not operate upon.

An immediate example is Redis, which will generate the following output in the final SBOM artifact:

     "type" : "application",
      "bom-ref" : "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "name" : "redis",
      "version" : "7.2.14",
      "cpe" : "cpe:2.3:a:redislabs:redis:7.2.14:*:*:*:*:*:*:*",
      "purl" : "pkg:generic/redis@7.2.14",
      "properties" : [
        {
          "name" : "syft:cpe23",
          "value" : "cpe:2.3:a:redis:redis:7.2.14:*:*:*:*:*:*:*"

DependencyTrack uses the top-level component cpe for NVD/internal analyser matching. It does not treat Syft’s syft:cpe23 properties as authoritative alternative CPE identity.

The redislabs:redis CPE is stale/superseded for current Redis vulnerability tracking. The redis:redis CPE appears to be the actively maintained identity and is the one that matches newer Redis CVEs in the 7.2.x range.

This results in false negatives: the component is present, the vulnerability data exists, but the component identity used for matching does not line up with the actively maintained CPE used in current vulnerability records.

Why is this a problem?

This is not just a Redis specific edge case. It is a broader issue with CPE-based matching in general as far as I can tell, at least when utilising NVD as a source.

  • CPEs are tied to NVD naming conventions.
  • NVD may retain older CPE names in older CVEs and use newer names in newer CVEs.
  • CycloneDX has one main component-level CPE field.
  • Syft can generate multiple candidate CPEs, but only one can occupy the top-level CycloneDX cpe field.
  • DependencyTrack does not generator-specific candidate CPE properties as equivalent identities.

This means the correctness of DependencyTrack results depends heavily on which CPE Syft promote to that top level field.

Options considered / explored

  1. Usage of DependencyTrack fuzzy CPE matching

DependencyTrack has fuzzy CPE matching options, including fuzzy matching for components with PURLs. This may help expose some missed vulnerabilities caused by inconsistent CPE data, but it is not deterministic. Its also highly suggested that it increase false positives, especially where wildcard or approximate CPE matching causes unrelated components to match the same vulnerability data.

Current Finding: It may be worth testing as a compensating control, but is not suitable as the primary fix. It's largely intended to address other mismatches, not our finding.

  1. Enable additional vulnerability sources

Additional sources such as GitHub Advisories, OSV, etc may reduce reliance on NVD/CPE matching. This is still worth doing I suspect for other reasons, but it does not fully solve the problem for us because many components use generic PURLs, for example:

pkg:generic/<name>@<version>

PURL based matching only helps when the PURL accurately maps to the ecosystem/package identity used by the vulnerability source. A generic PURL may not match GitHub Advisories, OSV, etc in the same way an ecosystem specific PURL would.

Current finding: We could enable/test additional sources, but tasting from a raw level with the SBOMs already suggests this is likely not a resolution.

  1. Use DependencyTrack private vulnerability repository

We could add internal vulnerability mappings for known cases. This may be useful for urgent exceptions or internal advisories, but it would require us to maintain vulnerability mappings ourselves. This does not scale for public CVEs and should not become a replacement for correct component identity.

Current finding: Useful as an exception mechanism only.

  1. Run Grype/similar separately

Running Grype or similar against the SBOM/image can help validate DependencyTrack output and detect mismatches. This is valuable as an independent verification layer, however, it does not fix the data imported into DependencyTrack, and DependencyTrack would still remain misleading unless the SBOM identity is corrected before import.

Current finding: Useful as a validation layer, but not a replacement for fixing the SBOM sent to DependencyTrack.

  1. Wait for upstream DependencyTrack improvements

There have been/are many upstream discussions/issues around deprecated this issue such as:

DependencyTrack/dependency-track#5626
DependencyTrack/dependency-track#2290

The outcome from most seems to be "working as intended" or "working around it would induce other issues."

This may somehow be improved in future DependencyTrack versions, but I would not be holding my breath for it given what we know. Even if it was to improve via this layer, the solution may not cover all generator specific candidate fields, or generic package identity cases.

Current finding: Worth keeping an eye on, not worth blocking a solution in hope of.

Recommended apporach based on current understanding

So far, my best recommendation is a controlled SBOM normalisation step before DependencyTrack import.

  1. inspect Syft-generated SBOMs before upload
  2. detect known-bad or stale top-level CPEs
  3. replace them with the preferred CPE for DependencyTrack matching
  4. preserve the original CPE in a property or pipeline log for auditability
  5. warn or fail when multiple candidate CPEs exist and no policy exists for choosing between them
  6. keep the override rules explicit and version controlled.

Example override logic:

cpe_overrides: 
    - description: "Redis Labs CPE is stale/superseded for current Redis CVE matching"
        match: 
            name: redis top_level_cpe_vendor: redislabs 
            top_level_cpe_product: redis 
        replace: 
            cpe_vendor: redis 
            cpe_product: redis

Why normalisation is likely the best path

Normalisation gives us deterministic control over the identity data DependencyTrack receives.

Fuzzy matching and additional vulnerability sources may improve coverage, but both rely on matching heuristics and upstream data alignment. They may help in some cases and fail in others.

SBOM normalisation is more explicit:

  • we know which bad identity we are correcting;
  • we know which replacement identity we expect DependencyTrack to use;
  • the rule is visible in code review;
  • the original value can be retained for audit;
  • the same corrected SBOM can be reused consistently;
  • the outcome does not depend on DependencyTrack guessing correctly.

This also avoids relying too heavily on NVD/CPE naming behaviour. DependencyTrack appears to be constrained by how NVD represents vulnerable products, and NVD/CPE naming is not always ideal for modern package identity. Normalising before import lets us compensate for known CPE data issues without enabling broad fuzzy matching likely to create false positives.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions