Skip to main content

The SBOM, evidence, and cdxgen

Learning objective

After this chapter you will understand why an accurate, evidence-bearing CycloneDX SBOM is the prerequisite for trustworthy reachability, which cdxgen features produce the evidence dep-scan consumes, and how to recognize a BOM that is too thin to support reachability so you can fix it before scanning.

Why the BOM is the foundation

dep-scan's reachability engine is purl-keyed and evidence-driven. It needs two things from the BOM that a bare manifest scan will not provide: versioned package URLs for every dependency, and evidence that records where each dependency appears in code. When either is missing, the engine degrades gracefully but visibly: occurrences vanish so direct_purls is empty, or purls are versionless so the slicer cannot reconcile a flow onto a BOM component. The symptom is usually a slice file that exists but carries no usable purls, which is the most common cause of "no reachable flows were identified." The reachability pipeline is only as good as the BOM that feeds it, which is why this chapter comes before the language guides.

cdxgen and the slice files

cdxgen is the SBOM generator dep-scan bundles with. Under the --profile research setting (the documented way to turn reachability on), cdxgen runs its evinse layer, which performs source-level slicing and persists per-language slice files alongside the BOM. The files you will see in the reports directory include deps, usages, data-flow, reachables, and semantics, named per language (for example java-reachables.slices.json or rust-reachables.slices.json). The reachability engine reads the *-reachables.slices.json file, and the native rusi, golem, and dosai reports are folded into the *-semantics.slices.json path when present. For most project types --profile research is sufficient; for some, notably Docker images and OS packages, --deep is what persuades cdxgen to dig into the layers that carry the real dependencies.

  cdxgen --profile research
├─▶ sbom-<type>.cdx.json component.purl + evidence.occurrences ─┐
└─▶ evinse slicing │
├─▶ *-reachables.slices.json (flows with purls) ─────────────┤
└─▶ *-semantics.slices.json (raw rusi/golem/dosai report) ──┤

reachability engine (reachability.py)
direct_purls + reached_purls

The three pieces of evidence that matter

Three CycloneDX evidence constructs drive the engine, and learning to spot them in a BOM makes reachability debugging straightforward.

The purl (component.purl) is the join key for everything. It must be versioned (for example pkg:cargo/time@0.1.45, not pkg:cargo/time) because the vulnerability feed and the slicer output both key on the versioned purl. Native slicers often emit versionless purls, which is why each converter has a reconciler that maps by name onto the BOM's versioned purls. A BOM whose components lack versions will silently break reachability even when the slices are perfect.

component.evidence.occurrences records where a dependency appears in code. Each occurrence carries a location (typically a file path), and cdxgen populates it from real imports, requires, and using directives. The engine builds direct_purls from these occurrences, so a component with occurrences is "imported in source" while a component without them is "merely declared in a manifest." This is the distinction that lets dep-scan separate a dependency that is wired into the application from one that is transitively pulled but never referenced.

component.evidence.callstack.frames is the richer, call-stack form of evidence that some project types emit. When present, it gives the engine a structured path from an entry point to the component, which is what the semantic analyzer uses to attribute endpoint and service reachability. Not every project type produces callstack frames, and that is acceptable: FrameworkReachability works from occurrences alone.

Recognizing and fixing a thin BOM

A reachability-hostile BOM has recognizable symptoms. The *-reachables.slices.json file is empty or very small, which means the slicer found no flows to trace. The file exists but its flow objects have empty purls arrays, which usually means the BOM lacked the occurrence and purl information needed for the slicer to attribute a flow to a component. Or the components in the BOM have no evidence block at all, which tells you cdxgen ran without the research profile or the deep option.

The fixes follow from the causes. Run with --profile research so cdxgen persists slices and evidence, and add --deep for image and OS-package scans. Make sure the source tree is in the state cdxgen expects for the language: a restored .NET tree (so project.assets.json and *.deps.json are present), a built Java tree with resolved Maven or Gradle dependencies, a Go module cache warm enough for golem to load packages, or a Cargo.lock that resolves real versions. The language chapters call out the per-language prerequisite in each case. When a large project produces no slices at all, the cause is often resource exhaustion during slicing, which is covered in the troubleshooting notes of the Framework reachability chapter.

Summary

Reachability is evidence-driven, and the evidence comes from cdxgen. The three things to verify before chasing a reachability problem are that components carry versioned purls, that those components have evidence.occurrences, and that the *-reachables.slices.json file is non-trivial. With those in place, the engine and every language guide behave predictably. From here, the natural next step is the guide for your language under Language Guides, or the Framework reachability chapter for the analyzer internals.