Beating Alert Fatigue: How to Filter and Prioritize Vulnerabilities

Sep 17, 2026 min read

You wire Trivy into the pipeline on a Friday afternoon, feeling pretty good about yourself. Monday morning the first scan lands: 450 vulnerabilities. Twelve critical, sixty-one high, the rest a wall of yellow and blue you don’t have the patience to read. You post the report in the team channel. Someone reacts with the skull emoji. Nobody opens the report. By Wednesday, the scan is just a step that runs and gets ignored, the same way everyone stopped reading cookie banners a decade ago.

That’s not a tooling failure. That’s Alert Fatigue, and it’s the actual threat to your DevSecOps program — not the CVEs themselves. A scanner nobody reads provides exactly the same protection as no scanner at all, except now it also burns CI minutes and slows down every pull request. Once your team learns that “vulnerability report” means “wall of noise I can’t act on,” they stop looking, and the one CVE in that pile that genuinely matters gets buried with the 449 that don’t.

Fixing this isn’t about scanning harder. It’s about scanning smarter — filtering the report down to the handful of things a developer can actually do something about, and handling everything else through a documented, auditable process instead of a shrug.

The Problem with CVSS Scores

Here’s the part that trips people up: a CVSS score measures how bad a vulnerability could be, not how much danger it poses to your specific application. A 9.8 CRITICAL rating on a library means the bug is severe in the abstract — full compromise, no privileges required, network-exploitable. It says nothing about whether your code ever calls the vulnerable function.

This is the gap between a theoretical vulnerability and an exploitable one. Trivy’s free image scanning tells you a package with a known CVE is present in your image. It does not tell you whether your application actually loads that code path — that kind of reachability analysis (tracing whether the vulnerable function is called at runtime) is a deeper capability that most open-source scanners don’t do out of the box, and it’s the feature commercial tools like Checkmarx or Ox Security sell as their headline differentiator.

Put a CRITICAL CVE in a Python testing library that only runs in your CI container and never ships to production, and its operational risk to your live service is close to zero — even though the report renders it in the same alarming red as a CVE in your public-facing web server. Treating every red row the same way is what trains your team to stop distinguishing between them.

Strategy 1: Filter Unfixed Vulnerabilities

A big chunk of that 450-item report is CVEs with no available patch. The OS maintainer — Debian, Alpine, whoever packages your base image — knows about the bug but hasn’t shipped a fix yet. Sometimes that’s because the fix is genuinely hard. Sometimes it’s a low-severity issue nobody’s prioritized. Either way, the outcome for your developer is identical: there is no version bump, no patch, no PR that resolves it.

Failing a build over something a developer is structurally incapable of fixing is one of the fastest ways to burn goodwill in a security program. It doesn’t make the container safer. It just teaches people that the pipeline blocks them for reasons outside their control, which is exactly the setup that leads to a git commit --no-verify culture where people route around checks instead of respecting them.

Trivy has a flag for exactly this:

trivy image --ignore-unfixed myapp:latest

--ignore-unfixed drops any CVE where the vendor hasn’t published a patched package yet. It doesn’t hide the vulnerability from the world — it’s still sitting in the CVE database and it’ll show up again automatically the moment a fix ships and your scan catches an unpatched image. It just stops it from cluttering the list of things a human needs to act on today. This one flag is often the single biggest reduction you’ll see in report size, because base image maintainers move slower than the CVE feed.

Strategy 2: Systematic Risk Acceptance

Filtering unfixed CVEs still leaves you with a stack of vulnerabilities that do have a fix available but that your team has decided, deliberately, not to act on right now. Maybe it’s a CVE in a dev-only dependency that never ships. Maybe it’s a low-severity bug in a library you’re two sprints from removing entirely. That’s a legitimate call — but it needs to be a documented one, not a mental note that lives in one engineer’s head until they leave the company.

This is what .trivyignore is for. It’s a plain-text file, checked into the same repository as your Dockerfile, that tells Trivy which CVE IDs to suppress and — critically — lets you attach a reason and an expiration date to each one:

# .trivyignore
# Review cadence: quarterly, owned by @security-champions

# Local-only privilege escalation via chroot; container runs as non-root
# and has no interactive shell access in prod. Reassess if runtime changes.
CVE-2016-2781 exp:2026-12-15

# Legacy apt metadata parsing bug — package present as a base image
# transitive dependency only, never invoked at runtime. No fix planned
# by Debian maintainers; monitored for exploit activity.
CVE-2011-3374 exp:2026-12-15

# Dev-container tooling only (pytest-cov). Confirmed absent from the
# production image via multi-stage build — see distroless article.
CVE-2024-33663 exp:2026-10-01

Notice what each entry has: the CVE ID, a comment explaining why it’s safe to ignore, and an exp: date. That expiration is doing real work — when it passes, Trivy stops honoring the suppression and the CVE reappears in your next scan, forcing someone to actually look at it again instead of it silently disappearing into the ignore file forever. A .trivyignore entry without a review date isn’t risk acceptance. It’s just risk you’ve stopped tracking.

Version-controlling this file means every addition goes through a pull request, which means every risk acceptance has an author, a timestamp, and — if you require sign-off — a reviewer’s name attached to it. When an auditor eventually asks “why is this CVE in production,” you have an answer that isn’t “I don’t remember.”

Practical Example: Building a Sustainable Policy

Put the two strategies together and you get a pipeline that only fails builds for things a developer can genuinely fix. The rules of engagement look like this:

  1. Only gate on HIGH and CRITICAL severity. MEDIUM and LOW findings get logged, not enforced — nobody’s holiday plans get ruined over a LOW.
  2. Drop anything without an available fix. --ignore-unfixed handles this automatically.
  3. Require a pull request to add anything to .trivyignore. No direct commits to the ignore file, same as any other production config.

The combined CLI invocation looks like this:

trivy image \
  --severity HIGH,CRITICAL \
  --ignore-unfixed \
  --exit-code 1 \
  --ignorefile .trivyignore \
  myapp:latest

--exit-code 1 is what actually fails the CI job when Trivy finds something matching your criteria; without it, Trivy scans, prints a report, and exits 0 regardless of what it found — a mistake that quietly turns your entire gate into a no-op. Wire this into GitHub Actions (the companion article in this series, Integrating Trivy Vulnerability Scanning into GitHub Actions, covers the workflow YAML in detail) and the build only turns red when a developer can bump a package version and turn it back green. That’s the whole point: a failing pipeline should always come with an obvious next step.

The Vulnerability Triage Funnel

This diagram illustrates how a massive list of raw CVEs is filtered down into a small, actionable set of failing checks for developers.

flowchart TD A[(Raw Scan Output\n450+ CVEs)] --> B{Has Patch?} B -->|No| C[Ignore\n--ignore-unfixed] B -->|Yes| D{In .trivyignore?} D -->|Yes, Unexpired| E[Ignore\nDocumented Risk] D -->|Yes, Expired| F D -->|No| F{Severity?} F -->|LOW / MEDIUM| G[Log Only\nNo Block] F -->|HIGH / CRITICAL| H[Fail Build\nDeveloper Action Required]

Visual Notes:

  • The funnel drastically reduces noise before a human has to look at the pipeline result.
  • Expired suppressions automatically fall back into the active evaluation path.

Common Mistakes That Undo All of This

Even a well-designed policy erodes if you let these habits slide:

  • Ignoring by package instead of by CVE. .trivyignore works on CVE IDs, not package names — a broad suppression that silences “everything in openssl” instead of one specific finding will blind you to the next CVE in that package, which might be the one that matters.
  • No expiration date. An ignore entry without exp: is permanent by default. Packages get more exploitable over time as proof-of-concept exploits get published; a CVE that was low-risk in January can be trivially weaponized by June.
  • Letting the ignore file become a dumping ground. If .trivyignore grows to 80 entries with no periodic review, you’ve just rebuilt the 450-item wall of noise one suppression at a time, except now it’s invisible.
  • Gating on unfixed CVEs “to be safe.” This is the mistake that started the whole problem. It produces builds that fail for reasons nobody can resolve, and teaches engineers to distrust — or bypass — the gate entirely.
  • Treating .trivyignore merges as rubber-stamp approvals. If a security champion doesn’t actually read the justification comment before approving, you’ve turned an audit trail into theater.

None of these mistakes look dangerous in isolation. They accumulate quietly, the same way the original 450-item report did.

Do’s and Don’ts

Do:

  • Treat .trivyignore like production code — PR required, reviewed by a security champion.
  • Set an exp: date on every ignored CVE and put the review on a calendar, not just in the file.
  • Re-run the ignored list quarterly and drop anything that’s no longer relevant.

Don’t:

  • Ignore MEDIUM or LOW findings forever without revisiting them.
  • Block a build on a CVE with no available fix.
  • Let risk-acceptance decisions live outside version control.

Accepting a vulnerability is still accepting risk, even when the paperwork says it’s fine. Get sign-off from whoever owns that risk on your team before the pattern becomes company policy — a .trivyignore file with quarterly review is a process, not a permission slip.

Key Takeaways

Alert fatigue is what happens when a scanner reports everything and prioritizes nothing — and it costs you the one alert that actually mattered, buried under 449 that didn’t. Fixing it isn’t about better dashboards. It’s --ignore-unfixed removing what nobody can act on, .trivyignore documenting what you’ve decided not to act on yet, and a severity gate that only stops the pipeline for things a developer can actually fix. Do that consistently, and a 450-item report turns into five things worth a Slack message instead of a shrug.

Next, check whether your current scanner configuration is already gating on unfixed CVEs — that’s usually the fastest fix available today. And if your base image is contributing half that noise in the first place, the next article in this series, Hardening from the Ground Up: Using Distroless Base Images, covers cutting the attack surface before Trivy ever gets a chance to complain about it. Pair it with the pre-commit hook and Checkov linting articles in this playbook to catch issues before they ever reach a scan at all.

Sources