fix(dpkg): build the dependency graph for packages from /var/lib/dpkg/status.d
Description
In images that store dpkg metadata in /var/lib/dpkg/status.d/ (one file per package, e.g. distroless), all OS packages have an empty DependsOn.
As a result, the SBOM shows every package as a direct dependency of the OS.
Reproduced on v0.74.0:
trivy image --format cyclonedx --output base.cdx.json gcr.io/distroless/base-debian12libssl3 is expected to depend on libc6, but its dependsOn is empty.
If the files from status.d/ are merged into a single /var/lib/dpkg/status, the graph is built correctly.
Discussed in #11260
Root cause
parseDpkgStatus is called separately for each status file, and consolidateDependencies resolves names from Depends into IDs (name@version) only against packages from the same file.
Names that aren't found are dropped.
status holds all packages in one file, while each file in status.d/ holds exactly one package, so dependencies are never found.
Implementation challenges
1. Packages live in different layers
In distroless images each package usually sits in its own layer.
For example, libc6 and libssl3 are in different layers of gcr.io/distroless/base-debian12.
The analyzer (including PostAnalyze) works on a single layer and doesn't see packages from other layers.
So resolving across all status.d/ files inside PostAnalyze would only fix single-layer images and rootfs/fs, not real distroless images.
Dependencies have to be linked after the layers are merged, i.e. in ApplyLayers (pkg/fanal/applier/docker.go) or later.
2. The analyzer has to keep unresolved dependencies
Today the analyzer drops names that aren't found in the file, so they never reach the applier. The analyzer therefore has to keep them, and we need to decide where to store them:
- In the existing
DependsOn. No changes totypes.Packageor protobuf, but the field gets a dual meaning: package names in the cache and RPC, IDs in the report. Debian package names can't contain@, so a name can be told apart from an ID, but this is an implicit contract. - In a new field. The meaning is explicit, but it changes
types.Package,rpc/common/service.protoand the converters inpkg/rpc/convert.go. The field has to be cleared after resolution, otherwise it leaks into the JSON report.
3. Where and in which order to resolve
ApplyLayersalready has dpkg-specific logic (merging licenses), so there is a precedent. The alternative ispkg/scan/local, but thenApplyLayerswould return an incomplete result.- Resolution must only touch packages found by the dpkg analyzer (
AnalyzedBy). Packages from embedded SBOMs (AnalyzedBy: sbom) already have IDs inDependsOnand must stay unchanged. - We need to define the order relative to
filterMismatchedOSPkgsand package de-duplication (#8298), so that thename → IDmap is built from the final package list. - Names that still can't be resolved (package not installed, virtual package) have to be dropped, so that
DependsOnin the report keeps containing only IDs.
4. Ambiguous name → ID mapping
- The same package can be present in several
status.d/files with different file names and in different layers (#8273, #8298). If the versions match, it's a single ID, but if they differ, we need a rule: the last layer wins, all versions, or something else. - Multi-arch:
libc6:amd64andlibc6:i386share the same name, and the ID doesn't include the architecture. The current code doesn't distinguish them either, but the expected behavior should be stated.
5. Cache
Analysis results are cached per layer.
Without bumping analyzerVersion, already cached layers stay without dependencies, and the fix will look like it doesn't work.
Bumping the version triggers re-analysis of images for all users.
6. Client/server
The analyzer runs on the client, while ApplyLayers runs on the server.
With mismatched versions (a new client and an old server), unresolved package names may end up in DependsOn in the report (with the existing-DependsOn option).
Client/server version compatibility is outside the compatibility policy, but this behavior is worth keeping in mind when choosing the approach.
7. Tests
Expectations in pkg/fanal/analyzer/pkg/dpkg/dpkg_test.go and pkg/fanal/applier/docker_test.go will change, and most likely the integration test golden files too (integration/testdata/distroless-base.json.golden and SBOM outputs).
Before implementation
Before implementing, the approach (including items 2–4) should be discussed and agreed on in this issue, and only then should the implementation start.
Source: aquasecurity/trivy