Secure the PipelineContainerStep 23 of 31

Grype versus Trivy on the Same Image

difficulty intermediatehands-on 20 min hands-on

74% complete

prereqs · trivy-image-cve-scan

concepts · scanner comparison · severity models · vulnerability databases · gate thresholds

Two mature, widely used, free image scanners. One image. Run within minutes of each other.

text
Trivy   7696  (CRITICAL: 229,  HIGH: 1622, MEDIUM: 4253, LOW: 1536, UNKNOWN: 56)
Grype   3995  (Critical: 267,  High: 1167, Medium: 1379, Low: 237,
               Negligible: 915, Unknown: 30)

Trivy reports 93% more findings. Grype reports 17% more criticals.

If you have ever been handed two scan reports that disagree and been asked which is right, this lesson is that situation.

Verified against the Range image — Trivy 0.72.0 and Grype 0.117.0, same image ID, same afternoon. Output copied from a real terminal.

Step 1 — Reproduce it

bash
git clone https://github.com/jaybilgaye/aiopsone-range
cd aiopsone-range
docker build -f docker/Dockerfile -t range-app:vulnerable .

brew install grype

trivy image --scanners vuln range-app:vulnerable
grype range-app:vulnerable

Terminal showing Trivy reporting 7696 vulnerabilities and Grype reporting 3995 for the identical image

Step 2 — Why the totals differ

Three causes, and none of them is one tool being broken.

Different severity vocabularies. Grype has a Negligible band — 915 findings here — that Trivy does not. Those 915 are not missing from Trivy; they are distributed into LOW and UNKNOWN, or dropped. Comparing bucket to bucket across the two tools is meaningless before you reconcile the vocabularies.

Different data sources and different severity assignment. Both consume the NVD, distribution advisories and language ecosystem feeds, but they weight them differently. When Debian rates a CVE lower than NVD does — common, because Debian assesses it against the version they ship — the tools can land on different severities for the identical package.

Different matching strictness. Trivy counts more total findings; Grype promotes more of what it does find to Critical. That is a defensible design difference: report everything and let the user filter, versus report less and be more confident in each.

The practical consequence. "We have 229 critical vulnerabilities" is not a fact about your image. It is a fact about your image and your scanner and the day you ran it. Any metric built on raw counts across tools — or across time, as databases update — will mislead whoever reads it.

Step 3 — Compare something that means something

The totals are not the comparison worth making. This is:

bash
# same image, both tools, JSON out
trivy image --scanners vuln -f json -o trivy.json range-app:vulnerable
grype range-app:vulnerable -o json --file grype.json

# what does each find that the other does not?
python3 - <<'PY'
import json
t = {v['VulnerabilityID'] for r in json.load(open('trivy.json')).get('Results', [])
     for v in (r.get('Vulnerabilities') or [])}
g = {m['vulnerability']['id'] for m in json.load(open('grype.json'))['matches']}
print(f"both:        {len(t & g)}")
print(f"trivy only:  {len(t - g)}")
print(f"grype only:  {len(g - t)}")
PY

Run that on your own images. The interesting number is grype only — the findings you would never have seen if you had standardised on Trivy alone, and vice versa. That set is what a second scanner actually buys you.

Step 4 — Choose, and gate on one

Running both in a blocking gate means two databases to keep current, two ignore files, and two sets of arguments about which is right. For most teams that is not worth it.

Gate on one. Audit with the other.

yaml
# blocking: one scanner, one threshold
- name: Trivy (gate)
  uses: aquasecurity/trivy-action@0.28.0
  with:
    image-ref: app:${{ github.sha }}
    severity: HIGH,CRITICAL
    ignore-unfixed: true
    exit-code: 1

# non-blocking: second opinion, weekly, report only
- name: Grype (audit)
  if: github.event_name == 'schedule'
  run: grype app:${{ github.sha }} --fail-on high || true

The scheduled job answers "is our primary scanner missing a class of thing?" without adding a second way for a pull request to fail at 5pm on a Friday.

Step 5 — Pick a threshold you can defend

Whichever you gate on, --ignore-unfixed (Trivy) and --only-fixed (Grype) matter more than the tool choice. A vulnerability with no available patch is not a task; blocking on it teaches people to bypass the gate.

bash
grype range-app:vulnerable --only-fixed --fail-on critical

That is a gate someone can pass by doing work, which is the only kind that survives.

Step 6 — The thing neither tool tells you

Both report on packages present in the image. Neither knows whether your application calls the vulnerable function.

A CVE in a code path you never execute is a lower risk than one in your request handler, and no image scanner can tell them apart. Reachability analysis is a different capability — it is what the commercial tiers of Snyk and Semgrep sell, and it is the honest answer to "why would I pay for this when Trivy is free". Until you have it, severity is a proxy for risk, not a measure of it.

What you learned

  • 7,696 versus 3,995 on an identical image. Different severity vocabularies, different data sources, different matching strictness.
  • Grype's Negligible band has no Trivy equivalent — bucket-to-bucket comparison is meaningless without reconciling them first.
  • A raw CVE count is a fact about your scanner and the date, not about your image. Do not build a metric on it.
  • Compare the set difference, not the totals. grype only is what a second scanner buys you.
  • Gate on one, audit with the other on a schedule. Two blocking gates is two arguments.
  • Neither tool knows whether the vulnerable code is reachable. That is what reachability analysis sells, and why severity is a proxy for risk rather than a measure of it.