Skip to content

Image & Dependency Scanning

Supply-Chain Security gave you an SBOM — a full inventory of every component in an artifact. This page is about what you do with that inventory: check every component against the world’s running list of known vulnerabilities, and stop the ones that matter from reaching production. Scanning is the operational, day-to-day face of supply-chain defense, and it’s where the shift-left economics of the cost curve pay off most concretely.

The premise is simple and a little uncomfortable: most of your attack surface is not your code. A container image is your application on top of a base image full of OS packages, libraries, and a runtime — and your application itself pulls in hundreds of dependencies. You wrote maybe 5% of what runs. Scanning is how you keep an eye on the other 95%.

“Scanning” is really two related jobs that look at different layers of the same artifact.

SCA (dependency scan)Container image scan
Looks atYour app’s libraries (the lockfile / SBOM)The whole image: OS packages + libs + your deps
Misses if used aloneOS-level packages in the base image(covers app deps too, but less language-aware)
Typical inputpackage-lock.json, go.sum, requirements.txta built image by digest
Runsearly, on source, in the PRafter build, on the artifact
  • SCASoftware Composition Analysis, the same tool you met in shift-left — reads your dependency manifest and flags libraries with known vulnerabilities. It runs early, on source, before you’ve even built anything.
  • Container image scanning looks at the assembled artifact: every layer, including the base image’s OS packages (openssl, glibc, the shell) that SCA never sees because you didn’t declare them.

You want both, because a vulnerability can live in either layer:

┌─────────────────────────────┐ ← your app code (you wrote this)
│ app dependencies │ ← SCA scans here
│ language runtime │ ← image scan
│ OS packages (base image) │ ← image scan ONLY (SCA can't see these)
└─────────────────────────────┘

A scanner has no opinion of its own. It works by cross-referencing the components it finds against public vulnerability databases — the CVE (Common Vulnerabilities and Exposures) feeds maintained by bodies like the NVD, plus ecosystem-specific advisories (GitHub Advisory Database, distro security trackers). A CVE is a stable identifier for one disclosed vulnerability, usually carrying a severity score (CVSS) and a list of affected versions.

So a scan is fundamentally a join: “here are the components in this artifact (from the SBOM); here are the components with known CVEs (from the feeds); show me the overlap.” This is why an SBOM is the prerequisite — and why a scan is only as current as its feed. A component isn’t “safe” because a scan passed; it’s safe as far as the feed knew at scan time. Yesterday’s clean image can light up red today because a new CVE was published overnight against something it already contained.

The common scanners — Trivy and Grype (open source) and Snyk (commercial) — differ in feeds, ergonomics, and language coverage, but all do this same join.

Terminal window
# Scan a built image for OS + library vulnerabilities, fail CI on high/critical.
trivy image --severity HIGH,CRITICAL --exit-code 1 \
ghcr.io/acme/app@sha256:9f86d0...

Base-image minimalism: the cheapest fix is less

Section titled “Base-image minimalism: the cheapest fix is less”

The single most effective way to reduce image vulnerabilities is to ship less. Every package in your base image is something that can have a CVE, whether or not your app uses it. A full ubuntu base carries a shell, a package manager, dozens of libraries — a large attack surface you inherited for free.

FAT base image MINIMAL base image
────────────── ──────────────────
ubuntu: shell, apt, curl, distroless: your app +
dozens of libs, all scannable runtime only — no shell,
→ many CVEs, big surface no apt → few CVEs, tiny surface

This is where distroless images shine — recall from Images & Layers that a distroless image contains only your app and its runtime: no shell, no package manager, almost nothing else. Fewer packages means fewer CVEs to triage and a smaller surface for an attacker who gets in (no shell to spawn). It’s a security win and a scanning-noise win at the same time — the components that aren’t there can’t generate findings.

Finding vulnerabilities is worthless unless something acts on the findings. Scanning enforces at two points, the same shape as the signing gate from the previous page:

  1. Scan-in-CI gate. The pipeline scans the built image and fails the build if it finds disqualifying vulnerabilities — so a vulnerable artifact never gets promoted. This is shift-left: catch it before it ships.
  2. Admission-time blocking. A Kubernetes admission controller checks images as they’re deployed and refuses to admit ones that haven’t been scanned or that carry critical CVEs. This is the backstop — it catches anything that bypassed CI, including images pulled from elsewhere. (The machinery for these gates is Compliance as Code.)
build ─► SCAN ─► pass? ─► push ─► [REGISTRY] ─► deploy ─► ADMISSION SCAN ─► run
│ │
└─ fail: stop the line └─ block at the gate
(CI gate, shift-left) (runtime backstop)

The eternal trade-off: CVE noise vs. real risk

Section titled “The eternal trade-off: CVE noise vs. real risk”

Here is where scanning gets genuinely hard, and it’s the same tension as shift-left’s false positives, sharpened. A scanner will happily report hundreds of CVEs against a normal image. Most of them don’t matter to you, and treating them all as equal is how teams drown.

A CVE’s raw severity is not the same as your actual risk. A “critical” CVE is harmless to you if:

  • the vulnerable code path is never reached by your application (you depend on the library but don’t call the affected function);
  • there’s no fix available yet, so failing the build just blocks every deploy for something you can’t remediate;
  • it’s in a component that isn’t exposed to untrusted input.
CVE severity ──► is the code path reachable? ──► is there a fix? ──► ACT
(CVSS score) (often: no) (sometimes: no)
"100 criticals" rarely means "100 things to fix tonight."
Reachability + fixability turn a wall of red into a short, real list.

The mature posture isn’t “zero CVEs” — that’s usually impossible and chasing it destroys trust in the tool. It’s a prioritized, fixable list plus a policy for accepting and documenting the rest. Reducing the base image (above) is the highest-leverage move precisely because it shrinks the noise at the source.

The manual, error-prone step scanning removes is a human trying to track every disclosed vulnerability against every component in every service — an impossible job done badly, usually meaning it isn’t done until something is exploited. A scanner does that cross-reference automatically on every build, and the gate stops a vulnerable artifact from shipping without anyone having to remember to check.

Production gets safer because known vulnerabilities — the easiest class for an attacker, since the exploit is often public — are caught at the pipeline and again at the admission gate, not in an incident. The cost is the triage discipline: tuning the gate so it blocks real, fixable risk without burying the team in noise that trains them to ignore it. Scanning tells you what is vulnerable; the next two pages limit the damage if something gets through — first by constraining what each workload and network path is even allowed to do: Identity, RBAC & Network Policy.

Five questions for scanning:

  • Why does it exist? Because most of your attack surface isn’t your code — you wrote maybe 5% of what runs, and the other 95% (base-image OS packages plus hundreds of dependencies) can carry known CVEs.
  • What problem does it solve? A human can’t track every disclosed vuln against every component: a scanner does the SBOM-vs-CVE-feed join automatically (SCA on app deps, image scan on the whole artifact) and gates both the pipeline and the admission controller.
  • What are the trade-offs? A scan is only as current as its feed — yesterday’s clean image fails today (Log4Shell), so you must re-scan deployed images — and a scanner that fails on every CVE regardless of reachability or fixability gets rubber-stamped or switched off.
  • When should I avoid it? Never skip scanning — but don’t block on unreachable or unfixable findings; report those with an audit trail and gate only on fixable, high-severity, reachable ones.
  • What breaks if I remove it? The easiest class of attack — a public, known exploit (Equifax/Struts, 147M records) — reaches prod uncaught, discovered in an incident rather than a PR.
  1. Why do you need both SCA and container image scanning? Give a concrete example of a vulnerability one would catch and the other would miss.
  2. A scan passed yesterday and fails today with no code change. Explain how that’s possible, and what practice it argues for.
  3. What is a scanner actually doing when it scans — and why does that make an SBOM a prerequisite?
  4. Why is reducing to a minimal/distroless base image described as both a security win and a scanning- noise win? Connect it to the earlier Images & Layers page.
  5. “We fail the build on every critical CVE.” Why can this posture backfire, and what does a mature gate block on instead? Tie it to the book’s thread.
Show answers
  1. SCA reads your app’s dependency manifest; image scanning reads the whole assembled image, including base-image OS packages you never declared. Example: a CVE in openssl or glibc from the base image is invisible to SCA (you didn’t list it) but caught by an image scan; conversely an image scan is less language-aware about deep transitive app dependencies. You want both layers covered.
  2. A scan is a join against vulnerability feeds, and the feeds move — a new CVE published overnight against a component already in your image makes a previously-clean scan fail. It argues for periodically re-scanning already-deployed images against the current feed, not just scanning at build time.
  3. It’s cross-referencing the components it finds (from the SBOM) against components with known CVEs (from the feeds) and reporting the overlap. You can’t check components you haven’t enumerated, so the SBOM/inventory is the prerequisite — and a “pass” only means “clean as far as the feed knew at scan time.”
  4. Every package present is something that can carry a CVE, whether or not you use it. A distroless base (app + runtime, no shell, no package manager) means far fewer packages — so fewer CVEs to triage (less noise) and a smaller attack surface, with no shell for an attacker to spawn. Components that aren’t there can’t generate findings.
  5. Failing on every critical CVE — regardless of whether the code path is reachable or a fix even exists — buries developers in unactionable noise, so they rubber-stamp overrides or disable the scan, leaving no coverage. A mature gate blocks on fixable, high-severity, reachable findings and merely reports the rest with an audit trail. Per the thread, the manual step removed is a human impossibly trying to track every CVE against every component; the gate must stay trustworthy to keep doing that job.