Safeguard
Supply Chain Security•

Source SBOM vs container SBOM: a 71 versus 497 component comparison

We generated both SBOMs for one tiny Node app with Syft and compared them. The numbers show why a customer who asks for an SBOM needs to know which one you mean.

Safeguard Security Team
5 min read

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 components71497
Libraries70216
File entries1 (the lockfile)279
Operating system entrynonealpine 3.24.2
Runtime entrynonenode 22.23.3
Library types70 npm18 apk, 198 npm
Contains express 4.21.2yesno

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

  1. 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.
  2. Label every file with its generation context. In CycloneDX, metadata.lifecycles carries the phase.
  3. Generate the image SBOM in CI from the exact digest you push, not from a tag you rebuild later.
  4. Keep the source SBOM too. It shows direct dependencies and is far easier to read and diff.
  5. 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.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.