On 18 October 2026 we ran Syft 1.52.0 twice: once on a one-dependency Node project, and once on the public node:22-alpine image that such a project would typically be built on. The source SBOM listed 71 components. The image SBOM listed 497. Neither is wrong. They answer different questions, and CISA's 2025 draft minimum elements has a field, Generation Context, that exists to tell them apart.
Syft's latest release at the time of writing is v1.54.1, published 6 October 2026. Your counts will differ a little with the tool version and the image tag.
What's affected
Anyone who answers SBOM requests with whatever the first tool produced. A buyer reading "71 components" and a buyer reading "497" will draw different conclusions about the same product.
CISA's draft names three generation contexts: before build (source repositories and manifests), during build (the components that went into a releasable artifact), and after build (binary analysis of the result). Source and image SBOMs sit at opposite ends of that list.
How to check if you're exposed
Reproduce the comparison. The project had one dependency, express 4.21.2, and a lockfile created with npm install --package-lock-only.
# source: reads the lockfile
syft ./ -o cyclonedx-json=src.cdx.json
# image: pulls from the registry, no Docker daemon needed
syft registry:node:22-alpine -o cyclonedx-json=img.cdx.json
# compare
jq '.components | length' src.cdx.json img.cdx.json
jq -r '[.components[].type] | group_by(.) | map("\(.[0]) \(length)") | .[]' img.cdx.json
What came back in our run:
| Source (lockfile) | Image (node:22-alpine) | |
|---|---|---|
| Total components | 71 | 497 |
| Libraries | 70 | 216 |
| File entries | 1 (the lockfile) | 279 |
| Operating system entry | none | alpine 3.24.2 |
| Runtime entry | none | node 22.23.3 |
| Library types | 70 npm | 18 apk, 198 npm |
| Contains express 4.21.2 | yes | no |
Three points stand out.
First, the image has no express, because the image is only the base. Your application layer is what adds it. A source SBOM tells you what your code declares. An image SBOM tells you what is actually installed, including the operating system, the runtime and the package manager's own dependencies.
Second, the image's 198 npm packages are not your application. In our run, 197 sit under /usr/local/lib, 196 of them inside npm's own directory, and one is Yarn 1.22.22 under /opt. An application that never runs npm in production still carries them, and a vulnerability scanner will report on them.
Third, neither file recorded a lifecycle phase or a hash for any library. Both reported CycloneDX 1.7, and in both only the license field was largely filled in. CISA's draft lists hash and generation context as new elements, so you add those yourself or with a second tool.
What to do now
- Decide which SBOMs you will publish per release: source, image, or both. For a customer assessing exposure to a new vulnerability, the image SBOM is closer to what runs.
- Label every file with its generation context. In CycloneDX,
metadata.lifecyclescarries the phase. - Generate the image SBOM in CI from the exact digest you push, not from a tag you rebuild later.
- Keep the source SBOM too. It shows direct dependencies and is far easier to read and diff.
- When a customer sends a questionnaire, say in one line which of the two you are answering with.
How to avoid this trap
- Do not report a component count without saying what was scanned.
- Do not scan a base image alone and call it your product's SBOM. The application layer is missing.
- Do not mix counts from two tools in one table. Their detection rules differ.
Why it matters
The two SBOMs are complementary evidence. The source file is what you intended, the image file is what you shipped, and the gap between them is where surprises live: a package manager nobody uses, a library copied in by a Dockerfile step, a runtime that went end of life. Sending one without saying which it is invites a wrong reading in either direction.
Try it yourself
Pick one service. Generate both SBOMs, count libraries in each, and list five packages that appear only in the image. For each one, write down which Dockerfile line brought it in.
Click through it at your own pace, or open it full screen.
If you only have a manifest, the free dependency scan accepts package.json, requirements.txt, CycloneDX or SPDX files in the browser, with no login, and the file never leaves your machine. It handles up to 100 dependencies per scan.
Quick check
Why does the image SBOM not list express in this experiment?
The scanned image was the base image only. The application layer that installs express was not part of it.
Which CISA element tells a reader whether an SBOM came from source, a build or a finished artifact?
Generation Context, with the values before build, during build and after build.
Why can an image SBOM list hundreds of npm packages for an app with one dependency?
The base image ships its own tooling, including npm and its dependencies, and the scanner records everything installed. In our run, 196 of the 198 npm packages lived inside npm's own directory.