Teams that run both an image scanner and a software composition analysis (SCA) tool usually get the same CVE reported twice by two tools in two queues, while the gaps between the tools go unnoticed. Image scanning reads what is installed inside a built container image: operating system packages from the base, language runtimes and the application packages that ended up on disk. SCA reads what the source repository declares and resolves: manifests and lockfiles, before anything is built. They overlap on application dependencies and diverge everywhere else, including what they can see, when they run, how they prioritise and what they miss at runtime. This guide compares the two on coverage, reachability, pipeline timing, false positives and runtime drift, then shows how to deduplicate findings and decide when one tool is enough. It is for application security, platform and DevSecOps engineers who own the CI pipeline. Image scanning operations are covered in the Container Image Vulnerability Management Guide and SCA mechanics in the Software Composition Analysis (SCA) Guide; this page covers how the two fit together. Open source tools are named only to illustrate documented behaviour (Grype, govulncheck, OSV-Scanner, Syft and Trivy, listed alphabetically); naming is not a recommendation.
Key takeaways
- They read different evidence. SCA reads pre-build files such as package-lock.json and go.mod; image scanners read post-build evidence such as installed package metadata, JARs and Go binaries, plus the OS package database.
- Only the image scanner sees the base. OS packages inherited from the base image (glibc, OpenSSL, zlib) never appear in a repository manifest, so SCA alone cannot report them.
- Only SCA sees the source context. Lockfiles, dev dependencies, pull request diffs and code-level reachability live in the repository, where developers can fix them before a build.
- Deduplicate on package identity, not on CVE. Key findings on package URL, version and CVE, attribute each to an owner (base, runtime layer or application) and send it to one queue.
- Neither covers runtime drift. Packages installed into a running container, images pulled outside the pipeline and old digests still running need runtime inventory and admission controls.
The short answer
If you ship software as containers, you need image scanning, because it is the only one of the two that sees the operating system layer and what actually landed in the artefact. You also want SCA in the repository, because it finds dependency problems at pull request time, with the lockfile and code context a developer needs to fix them. One tool can be enough when the image is built from scratch or a minimal base and contains only a single compiled binary that the image scanner can read, or when the team is small and a single scanner covers both the repository and the image. In every other case, run both and design the overlap deliberately: SCA owns application dependencies before merge, the image scanner owns the base and runtime layers after build, and both feed one deduplicated backlog.
What each one sees
The clearest statement of the difference comes from scanner documentation itself. Trivy's language coverage page splits targets into pre-build (a code repository, where it analyses lock files) and post-build (a container image, where it analyses installed package metadata and binaries). Its coverage table shows the effect:
| Evidence file | Read in repository (SCA view) | Read in image (image scan view) |
|---|---|---|
| package-lock.json (Node.js) | Yes | No: installed package.json files are read instead |
| go.mod (Go) | Yes | No: Go binaries with embedded module information are read instead |
| requirements.txt, poetry.lock (Python) | Yes | No: installed egg and wheel metadata are read instead |
| Gemfile.lock (Ruby) | Yes | No: installed gemspec files are read instead |
| JAR, WAR, EAR (Java) | No | Yes |
| Cargo.lock (Rust) | Yes | Yes; Rust binaries are read only if built with cargo-auditable |
| OS package database (dpkg, rpm, apk) | No | Yes |
Three consequences follow. First, a build that fetches dependencies outside the lockfile, vendors code or copies in a prebuilt binary produces packages SCA never saw. Second, a multi-stage build discards everything not copied into the final stage: Docker's documentation describes the final image as containing only what the last stage includes, so dev dependencies and build tools flagged by SCA may not ship at all. Third, some software is invisible to both. Trivy's documentation states that it does not support third-party or self-compiled binaries, skips packages with unspecified versions unless a lockfile supplies them, and does not check files installed by an OS package manager (such as a JAR installed through apt) against upstream language advisories.
The image scanner's view
An image scanner unpacks the layers and reads the OS package database, then looks for language artefacts on disk. For OS packages it should use the distribution's own advisories: Trivy documents sources including the Debian Security Bug Tracker, Ubuntu CVE Tracker, Red Hat OVAL, Alpine secdb and Amazon Linux ALAS, because vendors backport fixes without changing the upstream version. Matching OS packages against upstream versions alone produces the false positives covered in the Backported Security Fixes Guide. Image scanners can also attribute each package to a layer: OSV-Scanner's image output includes "IN BASE IMAGE" and "INTRODUCED LAYER" columns, which is the data you need to route a finding to the platform team or the application team. MITRE D3FEND names this defensive technique Container Image Analysis; see D3-CIA Container Image Analysis.
The SCA view
SCA resolves the dependency tree from manifests and lockfiles, ideally including transitive dependencies, and runs where developers work: on the pull request, in the IDE and on a schedule against the default branch. It knows which manifest declared a package, whether it is a direct or transitive dependency, whether it is a dev or test dependency, and which line to change. Repository-native tools work the same way: GitHub's Dependabot documentation says updates can be configured for repositories that contain a dependency manifest or lock file for a supported package manager, and its docker ecosystem keeps image tags current, including tags in Kubernetes manifests. That keeps base image references fresh, but it does not inspect the contents of the image.
Side-by-side comparison
| Dimension | Container image scanning | SCA (repository) |
|---|---|---|
| Input | Built image layers (or an SBOM generated from them) | Manifests, lockfiles, sometimes source code |
| OS packages from base image | Yes, against distribution advisories | No |
| Application dependencies | What was installed in the final image | What was declared and resolved, including dev and test |
| Pipeline timing | After build, on push to registry, continuous registry rescan | Pull request, merge, scheduled rescan |
| Fix context | Layer and base image; developer must trace to source | Manifest file and line; automated pull requests |
| Reachability | Package presence; binary symbol analysis for some languages | Call graph analysis where the tool supports it |
| Main false positive source | Backported OS fixes; packages present but unused | Dev dependencies and packages that never ship |
| Owner | Platform team for base, application team for app layer | Application team |
| Runtime relevance | High: scans the artefact that runs | Indirect: scans the intent, not the artefact |
Reachability: where precision comes from
Most findings in both tools are "the vulnerable version is present", not "the vulnerable code can run". The two tools approach reachability from different evidence.
- Source call graph (SCA side): the Go project's govulncheck analyses the call graph and reports only vulnerabilities where your code transitively calls a vulnerable function, using a database that records affected symbols. Its documentation lists limits: calls made through reflection are invisible, the unsafe package may cause false negatives, and interface calls are analysed conservatively.
- Binary symbols (image side): the same tool's binary mode reads the symbol table of a compiled binary. The documentation says this is less precise: it cannot show call stacks, may report code that is present but unreachable, and falls back to reporting every dependency module when symbol information cannot be extracted.
- Presence only: for OS packages, the practical reachability questions are whether the package is in the final image at all, whether the vulnerable feature is used, and whether the workload is exposed. Record the answer as a VEX statement rather than a local ignore; see the VEX Guide and the Reachability Analysis Guide.
The practical rule: use source reachability to deprioritise application findings before merge, and use image presence plus runtime exposure to prioritise OS and runtime findings after build. Neither replaces exploitation signals such as CISA KEV listing and EPSS.
Timing in the pipeline
| Stage | Tool | What to gate on | Why here |
|---|---|---|---|
| Pull request | SCA | New critical or KEV-listed dependency findings introduced by the diff; malicious package advisories | Cheapest point to fix; developer has context |
| Build | Image scanner plus SBOM generation | New findings in the built image above policy threshold | First time the real artefact exists |
| Registry | Image scanner (continuous rescan) | Newly published CVEs in stored images | Catches disclosures after build |
| Deploy | Admission control | Unscanned or out-of-policy digests | Stops images that bypassed the pipeline |
| Runtime | Runtime inventory | Vulnerable digests actually running, by exposure | Turns findings into a deployed-risk view |
Generate the SBOM once at build time from the final image and store it with the digest. Syft generates SBOMs in CycloneDX and SPDX formats from images, filesystems and archives, and can create signed SBOM attestations using the in-toto specification; Grype can scan an SBOM instead of the image. Rescanning the stored SBOM against fresh vulnerability data is faster than pulling every image again. See the SBOM vs SCA guide for the inventory side. Admission policies are covered in the Kubernetes Admission Control Guide.
Deduplication and ownership
When both tools report the same CVE in the same application package, you have one problem and one owner. When the image scanner reports a CVE in an OS package, the owner is usually the platform team. Build the merge logic around this:
- Normalise identity: map every finding to a package URL (purl), version and vulnerability ID, with aliases (CVE, GHSA, OSV) resolved to one record.
- Attribute layer: tag each finding as base image, runtime layer (interpreter, JRE) or application, using the layer attribution from the image scanner and the manifest path from SCA.
- Collapse duplicates: where SCA and image findings share purl, version and vulnerability, keep one record, link both sources, and route it to the application repository where the fix belongs.
- Drop what does not ship: SCA findings in dev or test dependencies that the image scan confirms are absent from the final image can be downgraded, with the evidence recorded.
- Flag the gaps: packages in the image with no SCA record (vendored, fetched at build, copied binaries) and SCA packages missing from the image both indicate a build process the team does not fully understand.
- Fix once, at the lowest layer: a base image CVE across 200 services is one platform ticket and an automated rebuild, not 200 application tickets.
Runtime drift: what both miss
Both tools assess an artefact at a point in time. Production can diverge from it in ways neither sees:
- In-container changes: a package installed with a package manager inside a running container, or a file written by a compromised process, is not in the scanned image. Setting the Kubernetes container securityContext field readOnlyRootFilesystem to true, with writable volumes only where needed, reduces this drift. See the Container Runtime Security Guide.
- Images outside the pipeline: public images deployed directly, sidecars injected by operators and third-party Helm charts never pass your CI scanners. ATT&CK tracks adversary use of Deploy Container (T1610); admission control is the matching defensive gate.
- Stale digests: a fixed image in the registry does not help if an old digest still runs in a forgotten namespace. Runtime inventory has to map running digests back to scan results.
- Disclosure after deployment: a CVE published today affects images scanned clean last month. Continuous registry or SBOM rescanning closes this gap.
Where identity fits
Every stage above uses machine credentials: CI tokens that pull dependencies and push images, registry credentials used by scanners and clusters, and the signing keys behind SBOM attestations. A scanner with write access to the registry or broad repository scope is a high value non-human identity. Scope scanner credentials to read access, keep signing keys in a managed store and rotate CI tokens. The supply chain worms that harvested publishing tokens show why: see the Software Supply Chain Security Guide.
Decision criteria
| Situation | Recommended approach |
|---|---|
| Containerised services on a full distribution base (Debian, Ubuntu, RHEL family) | Both: image scanning for the OS layer, SCA for pull request gating |
| Single static binary on scratch or a minimal base | Image or binary scan may suffice if the scanner can read the binary's embedded module data; keep SCA if you want pull request feedback |
| Libraries and SDKs published to package registries, no images | SCA only |
| Third-party images you deploy but do not build | Image scanning, plus admission control |
| Small team, one scanner that covers repositories and images | One tool in two modes, with one backlog |
| Regulated product needing an SBOM per release | Both, with the SBOM generated from the final image and SCA evidence for the development process |
Worked scenario: a Python API on a Debian base
A team ships a Python API in an image built from a Debian slim base with a multi-stage build. SCA on the repository reports 41 findings across requirements files; the image scanner reports 63 on the final image.
- Overlap: 18 findings are the same purl, version and CVE in both. They collapse to 18 records owned by the application team, fixed by lockfile updates.
- SCA only: 23 findings are in test and lint dependencies. The image scan confirms none are installed in the final stage; they are downgraded with that evidence.
- Image only, OS layer: 39 findings are in Debian packages. Checked against Debian's own advisories, several are fixed by backports or marked unimportant. The remainder go to the platform team as one base image rebuild.
- Image only, application layer: 6 findings are in a wheel the Dockerfile installs directly with pip, outside the requirements file. The team moves it into the lockfile so SCA sees it next time.
The result is 18 application tickets, one base image rebuild and one build fix, instead of 104 alerts in two queues.
Detection and measurement
- Duplicate rate: share of findings reported by both tools that are collapsed into one record (should approach 100 percent of true overlaps).
- Coverage gap count: packages present in the image with no SCA record, by repository; trend towards zero.
- Base image rebuild latency: time from a fixed base digest being available to all dependent images running it.
- Running-vulnerable digests: number of running digests with KEV-listed or critical findings, from runtime inventory.
- Unscanned admissions: deployments admitted without a scan result, which should be zero where admission control is enforced.
Common failure patterns
- Two queues, one problem: developers receive the same CVE from two tools with different severities and close both as noise.
- SCA-only container programmes: no image scanning, so base image vulnerabilities are never reported. When the XZ Utils backdoor (CVE-2024-3094) was disclosed, the affected liblzma versions were OS packages from development and rolling distribution releases, which only an OS-aware image or host scan would list.
- Upstream matching of OS packages: treating distribution packages as upstream versions, producing backport false positives that bury real findings.
- Treating CVE scanning as malware defence: neither tool's CVE matching catches a freshly trojanised package. The Shai-Hulud npm worm ran through a preinstall script on install, before any image existed; malicious package advisories and install-time controls are needed in SCA.
- Scan once, never again: gating at build and never rescanning the registry or SBOM, so newly disclosed CVEs in running images go unreported.
- Ignoring copied binaries: downloading a tool with curl in the Dockerfile, which neither SCA nor the image scanner can identify reliably.
Framework mapping
| Framework | Reference | How the two tools contribute |
|---|---|---|
| NIST SP 800-53 Rev. 5 | RA-5 Vulnerability Monitoring and Scanning; SI-2 Flaw Remediation; CM-8 System Component Inventory; SA-11 Developer Testing and Evaluation | Image scanning and SCA both implement RA-5; SBOMs support CM-8; SCA in pull requests supports SA-11 |
| NIST CSF 2.0 | ID.RA-01; PR.PS-02 | Identify and record vulnerabilities; maintain and replace software by risk |
| CIS Controls v8.1 | Control 7 Continuous Vulnerability Management; Control 16 Application Software Security | Image scanning under 7, SCA and third-party component management under 16 |
| MITRE ATT&CK | T1195.001 Compromise Software Dependencies and Development Tools; T1610 Deploy Container | SCA and malware advisories address dependency compromise; admission control addresses unscanned container deployment |
| MITRE D3FEND | D3-CIA Container Image Analysis | The defensive technique image scanners implement |
| NIST SP 800-190 | Application Container Security Guide | Image vulnerabilities as a risk of the image itself |
Practitioner checklist
- Run SCA on every pull request and image scanning on every built image; do not drop either for containerised services on a full distribution base.
- Configure the image scanner to use distribution advisories for OS packages.
- Generate an SBOM from the final image at build, attest it and store it with the digest.
- Normalise findings to purl, version and vulnerability ID and merge them into one backlog.
- Route base image findings to the platform team as rebuilds, application findings to repositories.
- Rescan stored images or SBOMs continuously against new vulnerability data.
- Enforce admission control for unscanned or out-of-policy digests and map running digests to findings.
- Remove ad hoc downloads from Dockerfiles so every component is declared and scannable.
Standards and references
- NIST: SP 800-190 Application Container Security Guide
- Trivy: Language coverage
- Trivy: Vulnerability scanning
- Anchore: Grype documentation
- Anchore: Syft documentation
- OSV-Scanner: Scanning container images
- Go: govulncheck command documentation
- Docker: Multi-stage builds
Related NHI Mgmt Group resources: Container Image Security Guide · Container Security Guide · Container Image Vulnerability Management Guide · Software Composition Analysis (SCA) Guide · SBOM Guide