| Tested on | Kali GNU/Linux Rolling 2026.2 (kali-rolling) |
|---|---|
| Package | trivy 0.74.0grype 0.118.0syft 1.51.1 |
| Lab environment | Pentest lab |
| Cert prep | CompTIA PenTest+ · CKS |
| Privilege | Normal user for scans; sudo only to install binaries under /usr/local/bin |
| Scope | Side-by-side Trivy and Grype image scans on one pinned digest, warm-cache timing, why CVE counts differ, Syft and Trivy SBOM workflows, a short Trivy-only IaC example, actionable filters, and platform-neutral CI fail gates. Does not cover commercial SCA platforms, full SBOM compliance programs, runtime reachability analysis, or CI YAML for every platform. |
Choose Trivy if you want one tool for container vulnerabilities, IaC, secrets, licenses, Kubernetes, and SBOM workflows. Choose Grype if you primarily need vulnerability matching and prefer an SBOM-first workflow with Syft. For image CVEs alone, neither is universally “more accurate.” Their package inventories, feeds, and matching logic differ, which this comparison demonstrates on the same pinned image digest.
trivy config for Dockerfiles, Terraform, or Kubernetes manifests. Scan only images and configs you are authorized to test.
Trivy vs Grype: which tool does what?
Start with scope, not star counts:
| Capability | Trivy | Grype |
|---|---|---|
| Container image CVEs | Yes | Yes |
| Local filesystem / directory CVEs | Yes | Yes |
| Scan an existing SBOM | Yes (trivy sbom) |
Yes (grype sbom:…) |
| Generate an SBOM | Built in (--format cyclonedx / SPDX) |
Use Syft (companion tool) |
| IaC misconfigurations | Yes (trivy config) |
No |
| Secrets detection | Yes | No |
| License scanning | Yes | No |
| Kubernetes cluster / KBOM scanning | Yes (trivy k8s) |
No (images and SBOMs only) |
| Default table prioritization | Severity-oriented vulnerability rows | Risk-sorted using CVSS/severity plus EPSS/KEV context |
| Best fit | One binary for vulns + IaC + secrets | Focused vuln matching and SBOM pipelines |
For a practical choice:
| Choose | When |
|---|---|
| Trivy | You want one installer that also fails CI on Dockerfile, Terraform, or Kubernetes misconfigurations |
| Grype (+ Syft) | Inventory already comes from Syft and you want a dedicated matcher with risk-sorted triage |
| Both | High-value release images where a second opinion is worth the triage time |
Do not treat the union of findings as automatically more accurate than either tool alone.
Install Trivy, Grype, and Syft
Pin scanner versions the same way you pin application images. This lab uses Trivy 0.74.0, Grype 0.118.0, and Syft 1.51.1. Download each release with curl, then install the binary:
TRIVY_VERSION=0.74.0
curl -sfL "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" -o /tmp/trivy.tar.gz
tar xzf /tmp/trivy.tar.gz -C /tmp trivy && sudo mv /tmp/trivy /usr/local/bin/trivyInstall Grype the same way:
GRYPE_VERSION=0.118.0
curl -sfL "https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}/grype_${GRYPE_VERSION}_linux_amd64.tar.gz" -o /tmp/grype.tar.gz
tar xzf /tmp/grype.tar.gz -C /tmp grype && sudo mv /tmp/grype /usr/local/bin/grypeSyft is optional for image-only Grype scans, but you need it for the SBOM-first workflow later:
SYFT_VERSION=1.51.1
curl -sfL "https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/syft_${SYFT_VERSION}_linux_amd64.tar.gz" -o /tmp/syft.tar.gz
tar xzf /tmp/syft.tar.gz -C /tmp syft && sudo mv /tmp/syft /usr/local/bin/syftWarm each vulnerability database once before you compare tools, so cold-start downloads do not distort timing or counts:
trivy image --download-db-onlyConfirm Trivy and its DB metadata:
trivy -vVersion: 0.74.0
Vulnerability DB:
Version: 2
UpdatedAt: 2026-09-03 07:08:48.77266151 +0000 UTC
NextUpdate: 2026-09-04 07:08:48.77266125 +0000 UTC
DownloadedAt: 2026-09-03 10:20:06.835599719 +0000 UTCUpdate Grype’s database next:
grype db updateConfirm schema, build time, and status (the cache path varies by user home):
grype db statusSchema: v6.1.9
Built: 2026-09-03T06:30:55Z
Status: validRecord scanner versions, Trivy UpdatedAt, Grype DB Built, and the image digest with every comparison. Without those, “Tool A found more CVEs” is not reproducible.
Scan the same container image with Trivy and Grype
Use a digest, not a floating tag. This lab pins the amd64 manifest for python:3.12.8-slim-bookworm:
IMAGE='python@sha256:8859bd6ca943079262c27e38b7119cdacede77c463139a15651dd340087a6cc9'Trivy pulls the image by digest without a local Docker daemon. For Grype, use the registry: source so the pull path is explicit and does not depend on a Docker socket:
trivy image --scanners vuln --format table --table-mode summary "$IMAGE"Report Summary
┌────────────────────────────────────────────────────────────────────────────────┬────────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├────────────────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ python@sha256:8859bd6ca943079262c27e38b7119cdacede77c463139a15651dd340087a6cc9 │ debian │ 412 │
│ (debian 12.9) │ │ │
├────────────────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ usr/local/lib/python3.12/site-packages/pip-24.3.1.dist-info/METADATA │ python-pkg │ 6 │
└────────────────────────────────────────────────────────────────────────────────┴────────────┴─────────────────┘Trivy split the image into a Debian OS package target (412 findings) and one Python package path (6). Across severities that is 418 rows on this digest with Trivy 0.74.0 and the DB timestamp above.
Ask for HIGH and CRITICAL only when you want a shorter triage table:
trivy image --scanners vuln --severity HIGH,CRITICAL --format table --table-mode summary "$IMAGE"Report Summary
┌────────────────────────────────────────────────────────────────────────────────┬────────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├────────────────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ python@sha256:8859bd6ca943079262c27e38b7119cdacede77c463139a15651dd340087a6cc9 │ debian │ 69 │
│ (debian 12.9) │ │ │
├────────────────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ usr/local/lib/python3.12/site-packages/pip-24.3.1.dist-info/METADATA │ python-pkg │ 0 │
└────────────────────────────────────────────────────────────────────────────────┴────────────┴─────────────────┘Sixty-nine HIGH/CRITICAL OS findings still leave plenty of noise. Severity alone is not a remediation plan.
Scan the identical digest with Grype through the registry source:
grype "registry:$IMAGE" -o tableSample output (top of the default risk-sorted table):
NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK
libsqlite3-0 3.40.1-2+deb12u1 3.40.1-2+deb12u2 deb CVE-2025-6965 High 75.8% (99th) 57.6
libssl3 3.0.15-1~deb12u1 3.0.18-1~deb12u2 deb CVE-2025-15467 Critical 48.2% (98th) 45.3
openssl 3.0.15-1~deb12u1 3.0.18-1~deb12u2 deb CVE-2025-15467 Critical 48.2% (98th) 45.3
libgnutls30 3.7.9-2+deb12u3 deb CVE-2011-3389 Negligible 73.3% (99th) 3.7
libssl3 3.0.15-1~deb12u1 3.0.20-1~deb12u2 deb CVE-2026-45447 High 3.6% (88th) 2.8On the same digest, Grype 0.118.0 returned 426 matches (248 unique vulnerability IDs): Critical 19, High 115, Medium 165, Low 20, Negligible 83, Unknown 24. The default table leads with EPSS and a composite risk column, so the first rows differ from Trivy’s severity-sorted presentation even when the totals look close.
Save a machine-readable Trivy report for overlap analysis:
trivy image --scanners vuln --format json -o trivy-image.json "$IMAGE"Export Grype’s JSON with the same registry: source:
grype "registry:$IMAGE" -o json > grype-image.jsonBoth commands exit quietly when the write succeeds. Keep those files next to the digest and DB timestamps in your lab notes.
Warm-cache scan performance
After the vulnerability databases and image layers were already cached on this Kali host, I timed three consecutive summary scans with /usr/bin/time (%e wall seconds, %M peak RSS in KiB). Discard stdout so report rendering does not dominate the measurement. Results apply to this machine and this digest only.
Each Trivy run used:
/usr/bin/time -f '%e %M' trivy image --scanners vuln --format table --table-mode summary "$IMAGE" >/dev/nullEach Grype run used the same registry: source as the comparison scans:
/usr/bin/time -f '%e %M' grype "registry:$IMAGE" -o table >/dev/null| Scanner | Run 1 | Run 2 | Run 3 | Median wall time | Median peak RSS |
|---|---|---|---|---|---|
| Trivy | 2.02 s | 2.04 s | 1.87 s | 2.02 s | ~118 MiB |
Grype (registry:) |
14.04 s | 14.75 s | 16.79 s | 14.75 s | ~256 MiB |
On this warm-cache run Trivy finished faster and used less peak memory. That is not a universal ranking. Different images, CPU contention, cold registries, and output formats change both numbers. The registry: source may also differ from scanning a local Docker/Podman image already loaded on disk. Use these figures as a lab baseline when you size CI runners, not as a claim that one tool is always faster.
How to read container vulnerability results
Both tools follow the same high-level path even when the tables look different:
The diagram stops at known-vulnerability matching. It does not model exploitability or your network exposure.
Most tables share the same columns:
- Package / library name
- Installed version
- Fixed version (or empty / “won’t fix”)
- Vulnerability ID (CVE, GHSA, distro advisory)
- Severity
A scan “row” (or Grype “match”) is a package–vulnerability pairing. One CVE may therefore appear multiple times when several installed packages are affected, or when Trivy reports the same ID under more than one target. When this article compares scanner coverage later, it deduplicates by vulnerability ID so a CVE is counted once.
A row still means only that the scanner matched an advisory to a package it believes is present. It does not prove that the vulnerable code path runs in your deployment, that the service is reachable from the internet, or that your process has the privileges needed to trigger the bug.
Use the extra fields each tool exposes when you prioritize:
- Fix availability — prefer findings with a published fixed version for packages you actually ship
- Vendor severity — distro advisories often rate impact lower than NVD for the same CVE after backports
- EPSS / risk columns — Grype’s default sort combines severity with EPSS and KEV context; KEV can outweigh EPSS when an issue is actively exploited
- Grype match details — JSON
matchDetails[].typedistinguishes high-confidence package matches (exact-direct-match,exact-indirect-match) fromcpe-matchrows that deserve extra verification - VEX documents — both ecosystems can consume VEX to suppress known not-affected cases when your pipeline supports it
- Reachability and runtime context — outside either scanner; cover that in architecture review, not in the raw CVE dump
On this digest Grype’s JSON mix was about 305 exact-indirect, 69 exact-direct, and 52 CPE matches. One CPE example was the python binary row for CVE-2024-12254 (matcher: stock-matcher). Treat those as a triage queue, not as automatic CI failures.
For a longer Trivy-only gate and ignore workflow, use the dedicated Trivy image scan guide. This page stays on comparison and decision-making.
Why Trivy and Grype report different CVEs — is one more accurate?
A higher CVE count does not prove higher accuracy. Accuracy has to be evaluated finding-by-finding against package inventory, affected versions, vendor advisories, matching confidence, and runtime applicability.
Where do the CVEs actually come from? Both tools package their own vulnerability databases from overlapping upstream worlds, but the feeds, normalization, and matchers are not identical:
| Scanner | Vulnerability intelligence |
|---|---|
| Trivy | Trivy DB aggregates OS vendor advisories and language/ecosystem vulnerability sources into trivy-db |
| Grype | Grype DB aggregates distro/vendor and ecosystem vulnerability data for Anchore’s matchers into grype-db |
| Important | Feeds, normalization, update timing, and matching rules differ, so equal package inventories can still produce different results |
See Trivy’s vulnerability data sources and Anchore’s Grype DB plus supported ecosystems for the current feed lists.
Raw rows are not unique CVEs: the same CVE can appear against multiple installed packages or targets, so the overlap comparison deduplicates vulnerability IDs first. On this digest that unique-ID overlap was 190 CVEs. Trivy reported 211 unique IDs; Grype reported 248. That leaves 21 Trivy-only and 58 Grype-only IDs. Roughly 98 overlapping IDs still disagreed on severity labels (for example Trivy LOW vs Grype Negligible).
Neither tool is “wrong” in a simple accuracy sense. They inventory packages differently and match different feeds.
Common reasons the lists diverge:
- Package discovery — which OS packages, language ecosystems, and binary fingerprints each tool extracts
- Package identifiers — name and version normalization (Python binary vs
python-pkgmetadata vs deb package) - Vulnerability feeds — Trivy DB vs Grype DB providers and how each maps vendor advisories
- Feed update timing — even same-day databases are not identical snapshots
- Vendor advisories vs upstream — Debian/Ubuntu/RHEL backports change “fixed in” relative to upstream version numbers
- Severity sources — vendor impact vs NVD vs composite risk
- SBOM contents — when you scan an SBOM, missing components never get matched
- Matching logic — version ranges, “won’t fix”, CPE matches, and ignored states
Concrete signals from this lab:
- Grype-only samples included several
pythonbinary CVEs (CVE-2025-0938,CVE-2025-13462, …) that did not appear in Trivy’s unique-ID set - Trivy-only samples included rows such as
CVE-2023-45853and distro-style IDs likeDLA-4726-1 - Severity label mismatches on shared CVEs are normal when one tool follows distro severity and the other uses a different scale
Before you compare counts in a ticket or CI debate, check:
- Same image digest (not the same floating tag)?
- Same scan timestamp window?
- Current vulnerability databases on both tools?
- Same input type (image vs SBOM vs filesystem)?
- Same severity source / filters?
- Same ignore rules, VEX,
--ignore-unfixed, or--only-fixedsettings?
If any answer is no, the count comparison is not apples-to-apples.
Scan an SBOM with Grype and Trivy
An SBOM is the package inventory for a digest. Store it at release time, then rescan when the vulnerability database updates without pulling the image again.
Syft → Grype
Generate a CycloneDX SBOM with Syft from the same digest:
syft "$IMAGE" -o cyclonedx-json > syft-sbom.cdx.jsonSyft writes the file and prints progress to stderr. Confirm the file size is non-zero, then scan it with Grype:
grype sbom:./syft-sbom.cdx.json -o json > grype-sbom.jsonOn this digest the Syft SBOM path produced 426 Grype matches, the same total as scanning the image directly. That is the point of the SBOM-first design: inventory once, match many times as feeds change.
Trivy SBOM generate and rescan
Trivy can emit CycloneDX without Syft. --scanners vuln tells Trivy to include vulnerability information in the CycloneDX output; omit it when you want a package-only SBOM:
trivy image --scanners vuln --format cyclonedx -o trivy-sbom.cdx.json "$IMAGE"Rescan that file for HIGH and CRITICAL only:
trivy sbom --scanners vuln --severity HIGH,CRITICAL --format table --table-mode summary trivy-sbom.cdx.jsonReport Summary
┌───────────────────────────────────┬────────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├───────────────────────────────────┼────────────┼─────────────────┤
│ trivy-sbom.cdx.json (debian 12.9) │ debian │ 69 │
├───────────────────────────────────┼────────────┼─────────────────┤
│ …/pip-24.3.1.dist-info/METADATA │ python-pkg │ 0 │
└───────────────────────────────────┴────────────┴─────────────────┘Sixty-nine HIGH/CRITICAL rows match the filtered image scan earlier. That is expected when the SBOM came from the same Trivy inventory.
Scanning the Syft CycloneDX file with Trivy on this host returned only 76 findings across all severities, not 418. Different SBOM producers encode different package sets and identifiers, and Trivy’s vulnerability matching relies on properties it embeds in its own SBOM output. Keep the inventory source constant when you compare SBOM rescans. For Trivy-native SBOM lifecycle detail, see generate and scan container SBOMs with Trivy.
Scan IaC misconfigurations with Trivy
Grype does not scan Dockerfiles, Terraform, Kubernetes YAML, or Helm charts for misconfigurations. Keep that work on Trivy (or another dedicated IaC scanner). Create a small lab directory with an intentional Dockerfile mistake:
mkdir -p ~/trivy-grype-lab/iac && cd ~/trivy-grype-lab/iacWrite a final stage that still runs as root:
cat > Dockerfile <<'EOF'
FROM python:3.12.8-slim-bookworm
USER root
ADD app /app
EXPOSE 22
CMD ["python", "/app/main.py"]
EOFScan the Dockerfile for HIGH and CRITICAL misconfigurations:
trivy config --severity HIGH,CRITICAL DockerfileDockerfile (dockerfile)
=======================
Tests: 20 (SUCCESSES: 19, FAILURES: 1)
Failures: 1 (HIGH: 1, CRITICAL: 0)
DS-0002 (HIGH): Last USER command in Dockerfile should not be 'root'
════════════════════════════════════════
Running containers with 'root' user can lead to a container escape situation.
…
────────────────────────────────────────
Dockerfile:2
────────────────────────────────────────
2 | USER rootTrivy points at the offending line and links the check ID. Fix the Dockerfile by adding a non-root USER before the final stage ends. That matches the hygiene pattern in building secure minimal images.
The same trivy config command also covers Terraform, Kubernetes manifests, Helm charts, CloudFormation, and related formats. For Kubernetes YAML gates you already maintain separately, pair this with Kubernetes manifest security scanning.
Focus on actionable findings
Raw CRITICAL counts punish every base image equally. Prefer filters that match how your team remediates.
Show only Trivy findings that have a published fix, limited to HIGH and CRITICAL:
trivy image --scanners vuln --ignore-unfixed --severity HIGH,CRITICAL --format table --table-mode summary "$IMAGE"Report Summary
┌────────────────────────────────────────────────────────────────────────────────┬────────────┬─────────────────┐
│ Target │ Type │ Vulnerabilities │
├────────────────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ python@sha256:8859bd6ca943079262c27e38b7119cdacede77c463139a15651dd340087a6cc9 │ debian │ 41 │
│ (debian 12.9) │ │ │
├────────────────────────────────────────────────────────────────────────────────┼────────────┼─────────────────┤
│ usr/local/lib/python3.12/site-packages/pip-24.3.1.dist-info/METADATA │ python-pkg │ 0 │
└────────────────────────────────────────────────────────────────────────────────┴────────────┴─────────────────┘Sixty-nine HIGH/CRITICAL rows shrank to 41 fixable ones on Debian packages. That is a more honest CI input than “any CRITICAL fails.”
Grype’s equivalent filter is --only-fixed:
grype "registry:$IMAGE" --only-fixed -o json > grype-fixed.jsonOn this digest --only-fixed left 201 matches (Critical 9, High 89, Medium 88, Low 11, Negligible 4). Numbers still differ from Trivy because inventory and feeds differ. Compare policies, not absolute totals.
A practical policy sounds like: fail the build on fixable HIGH/CRITICAL vulnerabilities in packages that ship in the final image, with documented exceptions for deferred vendor fixes. Blindly failing on critical_count > 0 rejects healthy rebuilds when the only CRITICAL rows are will_not_fix or unreachable packages.
Use Trivy or Grype in CI/CD
Keep the gate platform-neutral: the same flags work in GitHub Actions, GitLab CI, Jenkins, or a shell step.
Fail Trivy when CRITICAL issues remain:
trivy image --scanners vuln --severity CRITICAL --exit-code 1 "$IMAGE"This lab returned exit code 1 because CRITICAL rows exist on the digest. Tighten the gate to fixable CRITICAL only:
trivy image --scanners vuln --severity CRITICAL --ignore-unfixed --exit-code 1 "$IMAGE"That still exited 1 here. Fixable CRITICAL packages remain (for example OpenSSL and GnuTLS rows with published Debian versions).
Grype uses --fail-on and returns exit code 2 when the threshold is met:
grype "registry:$IMAGE" --fail-on criticalNarrow the same gate to fixable CRITICAL findings only:
grype "registry:$IMAGE" --only-fixed --fail-on criticalBoth returned exit code 2 on this digest. Wire that code into your CI “fail the job” condition, and export JSON or SARIF as artifacts for human review. For a full promotion pipeline that also signs digests, see secure container image promotion in CI/CD.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Counts differ more than expected | Different digest, DB timestamp, package inventory, feeds, filters, or ignore/VEX settings | Verify digest and test conditions first, then compare individual matches |
| Overlap is high but severities disagree | Different severity sources / scales | Compare package + CVE + fix version, not severity labels alone |
| Unexpected language dependencies appear | Nested package metadata or embedded binaries | Inspect JSON package paths; rebuild from a slimmer base |
| Image scan and SBOM scan disagree | Different SBOM producer or incomplete inventory | Generate and scan with the same toolchain; do not mix Syft inventory into Trivy expectations without checking |
| First scan is slow | Vulnerability DB download or cold layer fetch | Warm DBs with trivy image --download-db-only and grype db update; pin digests so layers stay cached |
| Expecting Grype IaC results | Grype has no IaC misconfig scanner | Use trivy config or a dedicated IaC tool |
Summary
Trivy and Grype both scan container images for known vulnerabilities, but only Trivy covers IaC misconfigurations, secrets, and several adjacent targets in the same binary. Grype stays focused on vulnerability matching and pairs naturally with Syft when you want an SBOM you can rescan as feeds change.
The useful comparison is not “which tool found more CVEs.” On python@sha256:8859bd6… with Trivy 0.74.0 and Grype 0.118.0 on the same day, totals sat near each other (418 vs 426 rows) while unique-ID overlap was only 190, with 21 Trivy-only and 58 Grype-only IDs. Package discovery, feeds, vendor advisories, CPE matches, and filters explain that gap better than a simple accuracy ranking. Warm-cache timing on this host favored Trivy for this digest, but that result is local, not universal.
Treat CI gates as policy: prefer fixable HIGH/CRITICAL findings that affect packages you ship, record scanner and database versions beside the digest, and keep IaC checks on Trivy so the title of your pipeline matches what the tools actually do. From here, harden the image build, store an SBOM with the release, and only then argue about the remaining CVE rows that both scanners still agree on.
References
- Trivy documentation
- Trivy vulnerability scanning
- Trivy SBOM
- Trivy configuration (misconfiguration) scanning
- Grype CLI reference
- Grype DB architecture
- Grype supported ecosystems
- Understanding Grype results
- Filter Grype scan results
- Working with Grype JSON
- Syft documentation
Frequently Asked Questions
Why do Trivy and Grype show different CVE counts for the same image?
They build package inventories differently and match against different vulnerability databases. Digests, database timestamps, severity filters, and ignore rules must match before you compare totals. On this lab digest the unique-ID overlap was only a subset of each tool’s full list.
Why is vendor severity different from NVD severity?
Distro vendors rate impact for their packaged build, including backports and compile options. NVD scores the upstream issue more generically. Trivy often prefers vendor severity for OS packages; Grype may present a different label or risk score for the same CVE.
Why does scanning an SBOM change the result?
The SBOM is only as complete as the generator that wrote it. A Syft inventory and a Trivy CycloneDX file for the same digest can produce different match sets, and Trivy’s matching expects properties from its own SBOM output. Rescanning the same SBOM later also changes results when the vulnerability database updates.
Can Trivy scan IaC?
Yes. trivy config checks Dockerfiles, Terraform, Kubernetes manifests, Helm charts, and related formats for misconfigurations. That is separate from image CVE scanning.
Can Grype scan IaC?
No. Grype scans images, filesystems, and SBOMs for vulnerabilities. Use Trivy or another IaC scanner for misconfiguration checks.
Should I run Trivy and Grype together?
You can on high-value images when you have time to triage disagreements. Use them as independent signals, not as a vote that proves accuracy. Many teams pick one primary gate and keep the second tool for periodic audits.

