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%.
Two scans, two layers of the stack
Section titled “Two scans, two layers of the stack”“Scanning” is really two related jobs that look at different layers of the same artifact.
| SCA (dependency scan) | Container image scan | |
|---|---|---|
| Looks at | Your app’s libraries (the lockfile / SBOM) | The whole image: OS packages + libs + your deps |
| Misses if used alone | OS-level packages in the base image | (covers app deps too, but less language-aware) |
| Typical input | package-lock.json, go.sum, requirements.txt | a built image by digest |
| Runs | early, on source, in the PR | after build, on the artifact |
- SCA — Software 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) └─────────────────────────────┘What a scanner actually knows: CVE feeds
Section titled “What a scanner actually knows: CVE feeds”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.
# 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 surfaceThis 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.
The two gates: CI and admission
Section titled “The two gates: CI and admission”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:
- 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.
- 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 thread: what manual step disappears
Section titled “The thread: what manual step disappears”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.
The architect’s lens
Section titled “The architect’s lens”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.
Check your understanding
Section titled “Check your understanding”- 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.
- A scan passed yesterday and fails today with no code change. Explain how that’s possible, and what practice it argues for.
- What is a scanner actually doing when it scans — and why does that make an SBOM a prerequisite?
- 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.
- “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
- 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
opensslorglibcfrom 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. - 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.
- 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.”
- 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.
- 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.