Best practices

How to Triage Vulnerability Scan Results

Published August 12, 2026 · 9 min read

A scan report with a thousand findings is not a work plan. This guide covers a repeatable vulnerability triage process: ranking by real risk instead of raw severity, gating CI on the findings that matter, and assigning ownership so the backlog actually shrinks over time.

Triaging vulnerability scan results

Why severity alone is not a triage strategy

CVSS severity describes how bad a vulnerability could theoretically be, not how likely it is to be exploited or whether it is even reachable in your deployment. Treating every critical finding as equally urgent burns remediation capacity on findings that pose little real risk, while genuinely dangerous findings with a lower severity score wait in line behind them.

Step 1: Pull the raw findings into a structured list

Start from the scan report's JSON output rather than a human-formatted summary, so every downstream step in this process can be scripted:

curl -fsSL https://scanrook.io/install.sh | sh
scanrook scan --file myapp.tar --format json --out report.json

jq -r '.findings[] |
  "\(.severity)\t\(.package.name)\t\(.cve)\t\(.fixed_in // "none")"' report.json \
  | sort > findings.tsv

wc -l findings.tsv

Step 2: Enrich each finding with EPSS and KEV status

Exploitation probability and confirmed real-world exploitation are stronger prioritization signals than severity alone. Pull both for every high and critical finding:

while read -r severity package cve fixed; do
  epss=$(curl -s "https://api.first.org/data/v1/epss?cve=$cve" | jq -r '.data[0].epss // "0"')
  echo -e "$severity\t$package\t$cve\t$fixed\t$epss"
done < findings.tsv > enriched.tsv

# Cross-check against the CISA KEV catalog
curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json \
  -o kev.json
jq -r '.vulnerabilities[].cveID' kev.json > kev-ids.txt

Our guides to EPSS prioritization and the CISA KEV catalog explain what each signal means and where it comes from.

Step 3: Check reachability before committing remediation time

A finding in a package your application never actually invokes carries materially lower risk than one on a directly reachable code path. Confirm before ranking it high:

# Is the package imported anywhere in application code?
grep -rln "require\(['\"]<package>" src/ || echo "not directly imported"

# For OS packages, is the binary ever executed by the entrypoint or app?
grep -n "<binary_name>" entrypoint.sh Dockerfile 2>/dev/null

This is the reachability layer our piece on installed-state scanning describes — a package genuinely present but never loaded is a very different risk than one on your request path.

Step 4: Rank and bucket the findings

Combine severity, EPSS, KEV status, and reachability into a small number of action buckets rather than a single sorted list nobody reads past row twenty:

jq -r '
  .findings[] |
  if (.severity=="CRITICAL" or .severity=="HIGH") and .fixed_in != null then
    "fix_now"
  elif (.severity=="CRITICAL" or .severity=="HIGH") and .fixed_in == null then
    "mitigate_and_track"
  else
    "scheduled_remediation"
  end as $bucket |
  "\($bucket)\t\(.package.name)\t\(.cve)"
' report.json | sort

fix_now findings have a known fix and high severity — no ambiguity, just remediation work. Findings with no fix follow the process in our docs on handling unpatched vulnerabilities.

Step 5: Gate CI on the highest bucket only

Blocking a build on every new finding trains teams to disable the check. Gate strictly on the bucket that has both high severity and a known fix:

FIX_NOW=$(jq '[.findings[] |
  select((.severity=="CRITICAL" or .severity=="HIGH") and .fixed_in != null)] | length' report.json)

if [ "$FIX_NOW" -gt 0 ]; then
  echo "::error::$FIX_NOW fixable high/critical findings block this build"
  exit 1
fi

Step 6: Assign owners and track the backlog

A ranked list without an owner does not get fixed. Route each bucket to the team that controls the relevant manifest:

{
  "finding_id": "$CVE_ID",
  "package": "<package>",
  "bucket": "mitigate_and_track",
  "owner": "platform-team",
  "manifest": "Dockerfile",
  "opened": "2026-08-12",
  "review_date": "2026-09-11"
}

OS package findings typically route to whoever owns the base image; application dependency findings route to the service team owning the lockfile. Track both in the same system as regular engineering work, not a separate spreadsheet nobody checks.

Verifying the triage process is actually working

Re-run the same enrichment and bucketing on every scan, and track whether the fix_now bucket is shrinking over time rather than growing:

# Compare fix_now counts across two scan dates
jq '[.findings[] | select((.severity=="CRITICAL" or .severity=="HIGH")
  and .fixed_in != null)] | length' report-2026-08-01.json
jq '[.findings[] | select((.severity=="CRITICAL" or .severity=="HIGH")
  and .fixed_in != null)] | length' report-2026-08-12.json

A triage process that is working shows a stable or shrinking fix-now bucket alongside a visible, dated backlog for everything else — not a growing pile of untracked findings.

Where ScanRook fits

ScanRook returns fixed-in versions and confidence tiers directly in the scan JSON, so steps 1 and 4 above run against structured data instead of a PDF someone has to read manually. Pair the scan output with EPSS and KEV lookups to build the triage pipeline described here; see the docs for the full report schema.

Frequently asked questions

What is vulnerability triage?

Turning a raw scan report into a ranked, ownable list of work by combining severity with exploitability and reachability signals.

Should I triage by CVSS severity alone?

No — combine it with EPSS, CISA KEV, and reachability to avoid over- or under-prioritizing findings.

How often should I re-triage a scan report?

Every new scan, since EPSS and KEV status change independently of your image — automate the lookups to keep this fast.

Should CI fail on every new finding?

No — gate on high/critical findings with a known fix, and track the rest for scheduled remediation.

Build your triage pipeline on ScanRook

Get fixed-in versions and confidence tiers in structured JSON, ready to feed straight into an EPSS and KEV enrichment step.

Related Posts

More on this topic.