Provide Supplementary SBOMs for Unresolved Binaries
This guide describes how to write a supplementary SBOM and have devguard-scanner pick it up, so a binary it cannot resolve on its own — a statically linked Go tool, a vendored C library, redis-server itself — is assigned a real identity and dependency tree instead of appearing as an unresolved stub.
For the rationale behind this feature, see Supplementary SBOMs — Filling the Gaps Trivy Can't. This page covers implementation.
Prerequisites
devguard-scanner(from theghcr.io/l3montree-dev/devguard/scannerimage, or built locally)- Something to scan: a directory, or a container image / tarball
- Knowledge of the binary's actual dependencies — the build author has information the scanner cannot recover on its own
Step 1 — Find out what needs describing
Run a normal scan first and watch the logs. Any application-type component with no package identity that isn't already covered gets a warning with a ready-to-use example printed to stderr:
level=WARN msg="found an application component with no package identity (purl); it and everything
it depends on will be dropped from the dependency graph and reparented to the nearest ancestor with
a valid identity, producing an incomplete SBOM - provide a supplementary SBOM describing it (metadata.component.name set to the exact path the binary was found) under --sbomPath to fix this"
save this as a .json file under --sbomPath (default /sboms) to describe "/usr/local/bin/redis-server":
{
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"metadata": {
"component": {
"type": "application",
"bom-ref": "/usr/local/bin/redis-server",
"name": "/usr/local/bin/redis-server"
}
}
}
The name in the printed JSON skeleton is the identity the base scan already assigned that component (its in-image filesystem path, for a container scan). This must match verbatim in the supplementary SBOM you write — it is the only signal devguard-scanner uses to associate the supplementary SBOM with that existing component, rather than treat it as an unrelated addition.
Step 2 — Write the supplementary SBOM
Start from the printed skeleton and fill in what is known: version, purl, and the real dependency tree the scanner could not derive on its own.
What each part is doing:
metadata.componentis the root of the supplementary SBOM. Itsnameanchors it to the existing unresolved component; itsversion/purlbecome that component's identity, which is what makes it eligible for vulnerability matching.componentslists everything the binary depends on, each with a realpurlso those components are matchable as well.dependenciesconnects them: the rootdependsOnits direct children, givingdevguard-scannera real tree instead of a flat stub.
Exhaustively describing a binary's transitive tree is not required. Supplying only the root's version/purl, with no components/dependencies, is already an improvement, since it makes the binary itself matchable even if its own dependencies remain undescribed.
Step 3 — Place it under --sbomPath
devguard-scanner looks for supplementary SBOMs at --sbomPath (default /sboms), and merges every *.json file it finds there — one file per component, or one file bundling several components' subtrees at once.
For a directory/path scan, drop the file under <scan-path>/sboms/:
Use a custom directory name instead of the default with --sbomPath=./my-sboms.
Step 4 — Verify it worked
Run the scan again and check the logs for one of two lines per supplementary SBOM:
level=INFO msg="enrichment attached a new node under the scan root" path=/usr/local/bin/redis-server
level=INFO msg="enrichment replaced an existing component's subtree" path=/usr/local/bin/redis-server
- "attached a new node" — no existing component had that exact purl (it was listed as a binary); the supplementary SBOM's root was added as a new child of the scan root.
- "replaced an existing component's subtree" — the uncommon case: a purl was found, but it was enriched and the whole subtree was replaced.
Then confirm in the DevGuard UI: the component should show a real version and be checked against the vulnerability database, and any described dependencies should appear nested underneath it in the dependency graph rather than as flat siblings at the root.
Generating supplementary SBOMs at build time instead of by hand
A hand-written supplementary SBOM works, but goes stale as soon as the described binary's dependencies change and the file is not updated to match. If the build is under your control (a Nix derivation, a Bazel rule, a Dockerfile RUN step), generating the supplementary SBOM as part of the build is generally preferable:
- Run a source-level scan (e.g.
trivy fsagainst the vendored source / lockfile) — this resolves a real, versioned tree, because a source checkout has lockfile metadata the final compiled binary doesn't. - Retarget that scan's root component onto the binary's actual path inside the final image, tagged with the real version being built.
- Drop the result at
/sboms/<name>.jsonin the image.
This is exactly what DevGuard's own Nix build does for gitleaks, trivy, crane, and its own scanner binaries — see the "Where DevGuard's own supplementary SBOMs come from" section of Supplementary SBOMs for the concrete approach.
Related Documentation
- Supplementary SBOMs — Filling the Gaps Trivy Can't — the underlying problem and merge design
- Scan OCI Images
- Scan Source Code