Skip to main content

CI/CD · troubleshooting

The vulnerability scan blocked the build

A scanner found something and stopped the pipeline. The question is not how to make it pass — it is whether this finding is reachable, exploitable and fixable, and those are three separate checks.

Run this first

step 1 of 4
git log -1 --stat -- package-lock.json go.sum requirements.txt Dockerfile

Look for No manifest change in the failing commit

Establishes whether your change caused this at all. A failing scan on an unchanged manifest means a new advisory, which is a different conversation from a dependency you just added.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

Did the failing commit change a dependency manifest or the base image?

If not, an advisory was published rather than anything of yours changing.

Cause space

6 of 6 still possible

  • A new advisory landed against your base imageCommon
  • The vulnerable code is present but never calledCommon
  • It is in a build or test dependency, not in the shipped artifactCommon
  • There is no fixed version yetOccasional
  • It is real, reachable and fixableOccasional
  • The match itself is wrongRare

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Check it with a tool

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    git log -1 --stat -- package-lock.json go.sum requirements.txt Dockerfile

    Establishes whether your change caused this at all. A failing scan on an unchanged manifest means a new advisory, which is a different conversation from a dependency you just added.

    Look for No manifest change in the failing commit

  2. Step 2

    trivy image --severity HIGH,CRITICAL --ignore-unfixed <image>

    Removes the two largest noise categories at once — low severity, and anything nobody can fix. What remains is usually small enough to read.

    Look for How many findings survive. This number, not the original total, is the size of the problem

  3. Step 3

    npm ls <package> --omit=dev

    Separates what ships from what builds. A finding present only in the dev tree is a different risk with a different owner.

    Look for Absence from the production dependency tree

  4. Step 4

    grep -rn "<package-name>" --include='*.ts' --include='*.js' --include='*.py' --include='*.go' src/

    Crude reachability. Not conclusive — a transitive call path will not show up — but a package your code never imports is a much weaker finding.

    Look for No import anywhere in your own source

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

A new advisory landed against your base image

Common

Nothing in your change caused it. An advisory was published against a package in the base image, and the same commit that passed yesterday fails today. This is the single most common reason a scan starts failing, and it is why 'what did I change' is the wrong first question here.

Confirm

git log -1 --stat -- package-lock.json go.sum requirements.txt Dockerfile

No dependency or base-image change in the failing commit. If the manifest is untouched, the advisory moved rather than your code

Fix

  • Rebuild against a current base image tag first — most OS-level findings clear without touching your code at all.
  • Rebuild on a schedule rather than only when code changes, so this arrives on a Tuesday rather than during a release.
  • If no fixed base exists yet, record the acceptance with an expiry date rather than an open-ended ignore.
docker pull <base>:<tag> && docker build --no-cache -t <image> .

Forces a fresh base layer. Without --no-cache the old layer is reused and nothing changes.

The vulnerable code is present but never called

Common

The package is in the image and your application does not import it, or imports it and never calls the affected function. A scanner working from the package inventory cannot tell the difference. This is the largest category by volume and the reason severity alone is a poor priority signal.

Confirm

grep -rn "<package-name>" --include='*.ts' --include='*.js' --include='*.py' --include='*.go' src/ || echo 'not imported anywhere in src'

No import at all, which makes it a transitive dependency of something else rather than code you call. Where your ecosystem supports reachability analysis, prefer that over grep

Fix

  • Record the reachability finding with the evidence, so the next person does not repeat the analysis.
  • Prefer scanners and flags that do reachability analysis where your ecosystem supports it — it removes this category automatically rather than one ticket at a time.
  • Do not mark it fixed. It is accepted, which is a different state and should expire.

It is in a build or test dependency, not in the shipped artifact

Common

A vulnerability in a test framework or a build tool is not in production unless your build environment is itself a target — which is a real concern, but a different one with a different owner. Scanning the repository rather than the final image conflates them.

Confirm

npm ls <package> --omit=dev   # or: pip list --not-required / go mod why <module>

The package absent from the production dependency tree while present in the full one

Fix

  • Scan the built artifact rather than the source tree, so what ships is what is judged.
  • Use a multi-stage build so build-time tooling does not reach the final image.
  • Treat build-environment vulnerabilities as a pipeline-security concern in its own right rather than ignoring them outright.

There is no fixed version yet

Occasional

The advisory is real, the code is reachable, and the maintainer has not released a fix. Assigning this to somebody produces a ticket that cannot be closed, which is how teams learn that security tickets are noise.

Confirm

trivy image --ignore-unfixed <image>   # or the equivalent flag for your scanner

The finding disappearing, which confirms no fixed version is published

Fix

  • Accept it explicitly, with an owner, a stated mitigation if one exists, and a date to re-check — not an indefinite ignore.
  • Consider whether a control elsewhere reduces the exposure: a WAF rule, an input constraint, removing the feature that reaches the code.
  • Configure the gate to distinguish fixable from unfixable. Blocking on something nobody can fix is the fastest route to the gate being disabled.

It is real, reachable and fixable

Occasional

The case the gate exists for. Reachable from your code, plausibly reachable from untrusted input, and a fixed version is available. These are the minority of findings and the entire justification for the other four categories of work.

Confirm

npm audit --omit=dev   # or: trivy image --severity HIGH,CRITICAL --ignore-unfixed <image>

A fixed version, against a package your code demonstrably calls on a path reachable from input

Fix

  • Upgrade, test, and ship — and ship it as its own change rather than bundled into a release, so a regression is attributable.
  • Read what changed rather than only bumping the number; a major-version fix can carry behaviour you did not want.
  • If the upgrade is blocked by an incompatibility, that is a scheduling problem to escalate, not a finding to ignore.

The match itself is wrong

Rare

Package naming is inconsistent across ecosystems and advisory databases, so a scanner can match the wrong component, or a version range can be recorded incorrectly upstream. Genuine, and rarer than it feels when you are the one blocked.

Confirm

# Compare the advisory's affected range against the version you actually ship

Your version falling outside the stated range, or the matched package being a different project with a similar name

Fix

  • Report it upstream to the scanner or the advisory source; it is wrong for everyone else too.
  • Suppress it narrowly — that CVE, that package, with a comment explaining why — never by relaxing severity thresholds.
  • Re-check periodically. Advisory data gets corrected.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

What is actually happening

A scanner compares the components in your artifact against advisory databases and reports matches. That is all it does. It does not know whether your code calls the vulnerable function, whether untrusted input can reach it, or whether the fix exists — so a finding is a starting point rather than a verdict.

Two responses are common and both are bad. Bumping versions until the scan passes fixes whatever happened to be flagged and teaches nobody anything. Adding an ignore entry, or disabling the gate, removes the signal along with the noise — and it is almost always done under release pressure, which is precisely when the judgement is worst.

The useful move is a sequence: is it reachable, is it exploitable, is there a fix. Most findings fail the first test, which is why a scan reporting hundreds of issues usually contains a handful worth acting on.

One thing worth saying plainly: a blocked build is the control working. The problem is almost never that the gate fired — it is that the gate was configured to fire on more than anyone can triage.

How to tell this is your problem
WhereWhat you see
The pipelineA scan step exiting non-zero with a list of CVEs and severities
The reportFar more findings than the change introduced — most come from the base image
TimingOften on a build with no dependency change, because a new advisory was published

How to know it is actually fixed

  • The scan passes without any new suppression having been added.
  • Where something was accepted rather than fixed, it has an owner, a reason and an expiry date recorded.
  • Re-run against a deliberately vulnerable image to confirm the gate still fires. A gate that passes everything is indistinguishable from a gate that is off.
  • The fix shipped as its own change, so a regression can be attributed to it.

Stopping it happening again

  • Scan the built artifact rather than the source tree, so the judgement is about what ships.
  • Rebuild on a schedule, not only on code change. Most base-image findings clear on a rebuild and arrive far more calmly on a Tuesday.
  • Gate on fixable findings only. Blocking on something with no available fix guarantees the gate gets disabled.
  • Run a new scanner in warn mode for a few weeks before it can block anything, so you learn its false-positive rate on your own codebase first.
  • Use a minimal base image — fewer packages means fewer advisories to triage, which is the cost that actually recurs.
  • Give acceptances an expiry. An indefinite ignore is a decision nobody will revisit.

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Related