Vulnerability scanners in 2026: revisit Trivy, Grype and OSV-Scanner before you trust another green scan

Picture a small software company in Bhubaneswar that builds claims and pre-authorisation software for hospitals that deal with health insurance TPAs (an invented example, not a real client). Its hospital customers are in Cuttack, Puri, Berhampur, Sambalpur and Rourkela. The product runs as four container images: a Django API on python:3.12-slim, an older report service on node:18-bookworm-slim that nobody has had time to upgrade, an nginx proxy on Alpine, and a forgotten cron image still on alpine:3.18. The team is three developers and one DevOps engineer who also looks after the hospital VPN links.

Two things happened this year. In March their CI used aquasecurity/trivy-action by tag, and one pipeline ran inside the window when those tags pointed at an infostealer, so they spent a weekend rotating every secret the runner could see. Then, in September, an insurer’s security team scanned their images with Grype as part of a vendor review and sent back a list of “critical” findings, while the team’s own Trivy reports for the same images were green. Three proposals are now on the table: stay with Trivy but pin everything by digest and commit SHA; move to Syft for SBOMs and Grype for matching, because that is what the insurer uses; or add OSV-Scanner on pull requests for the Python and npm lockfiles. Everyone agrees on one thing: nobody can explain why two scanners disagree.

Here the three are compared as they stood on 20 October 2026: what each one matches against, distro advisories and backports, false positives and false negatives, ignore files and VEX, SBOM generation and SBOM input, database delivery and offline CI, exit codes, release integrity, licences and governance, and the supply chain incidents of 2026. It is about scanning images and dependency files in CI. The earlier Revisit.Tech post on container registries met Trivy only as the scanner inside Harbor; here the scanner is the subject, and so is the trust you place in it.

Facts come from GitHub release and advisory APIs, CVE records, official docs, repository discussions and a drill that ran Trivy 0.76.0, Grype 0.121.0 with Syft 1.55.0 and OSV-Scanner 2.7.0 against the same four public images and the same two lockfiles on one shared box. The drill checks behaviour, not speed. Where a point has no primary source, I say “Not documented”. On licences, this is my own reading of the licence texts and not legal advice.

The short version

  • A green scan is a statement about inputs, not about your image. It says: this inventory, matched by this logic, against this database build, filtered by this policy, found nothing above the line. Change one and the answer changes. On the same python:3.12-slim digest, Trivy reported 79 unique vulnerability IDs, Grype 80 and OSV-Scanner 75, and all three agreed on only 73.
  • Disagreement is mostly about what each tool counts, not bugs. Grype’s extras on the drill images were CPE matches on binaries (the CPython interpreter, the node binary, OpenSSL libraries on Alpine), which its own docs say need checking. Trivy’s extras were Debian tracker items such as TEMP- IDs. On nginx:1.29-alpine, Trivy showed 0 CRITICAL and Grype 26, because they take severity from different places.
  • An end-of-life base image can look perfectly clean. On alpine:3.18, Trivy’s table said “Clean (no security findings detected)” and OSV-Scanner found 0; the end-of-support notice from Trivy was only a log line. Grype found 6 CPE matches and printed a warning that results for 15 packages may be incomplete. trivy --exit-on-eol 1 turns that log line into a failing build.
  • SBOMs travel less well than “standard” suggests. Grype read Trivy’s CycloneDX SBOM of the Python image and found 24 IDs instead of 80; Trivy read Syft’s and found 26 instead of 79, with most Debian rows gone. Scan with the tool that made the SBOM, or test the pair first.
  • VEX works only when the IDs line up. A VEX statement naming CVE-2025-8869 for pip hid the finding in Trivy but not in Grype, which matched the same pip issue under its GitHub advisory ID. Naming both IDs worked in both.
  • The scanner is part of your supply chain. In March 2026 a malicious Trivy v0.69.4 was published and 76 of 77 trivy-action tags were force-pushed to credential-stealing code (CVE-2026-33634). Pin actions by full commit SHA, pin binaries and images by checksum and digest, and verify signatures. All four binaries in the drill verified.

Where things stand on 20 October 2026

All times are IST. Release times are the GitHub published_at values; where a project’s notes give another date, I mention it. Licences come from each repository’s licence file. Stars were read on 20 October and are only a rough sign of attention.

Trivy

  • Latest: v0.76.0 (14 October 2026, 5:15 PM), after v0.75.0 (1 October, 7:04 PM). The 0.76 notes add experimental CBOM security levels, Echo-patched Maven and npm packages, RapidFort advisories for more operating systems and a fix for a CSAF VEX panic.
  • Release rhythm: 11 GitHub releases dated 2026, v0.69.2 to v0.76.0. Releases v0.27.0 to v0.69.1 are missing because they were deleted in the February incident. Releases have been immutable since 3 March.
  • Licence and governance: Apache-2.0. The repository sits in the aquasecurity organisation; Aqua’s own comparison page describes Aqua Security as “the home of Trivy” and lists what the commercial Aqua platform adds (among others a web UI, RBAC, SSO and dashboards).
  • Database: trivy-db, an OCI artefact at schema version 2, pulled first from mirror.gcr.io/aquasec/trivy-db and then from ghcr.io. The build workflow in the trivy-db repository runs every six hours.
  • Attention: 38,521 stars.

Grype and Syft

  • Latest Grype: v0.121.0 (20 October 2026, 2:02 AM), whose end-of-life warnings now take the distro’s own support end date into account. Before it, v0.120.1 (7 October, 2:16 AM) and v0.120.0 (2 October), which began signing the release checksums file with a Sigstore bundle and fixed a false positive where --add-cpes-if-none mapped tomcat-embed-el to Tomcat.
  • Latest Syft: v1.55.0 (20 October 2026, 12:31 AM).
  • Release rhythm: 23 Grype releases and 24 Syft releases dated 2026. Both use immutable releases.
  • Licence and governance: both Apache-2.0. The Grype README says development is “sponsored by Anchore” and points to Anchore for commercial support.
  • Database: schema v6, served from grype.anchore.io and built by the vunnel tool from providers such as Debian, Alpine, Red Hat, Ubuntu, GitHub advisories, NVD, EPSS, CISA KEV and end-of-life data. Anchore stopped publishing schema v5 updates on 6 March 2026, so Grype older than v0.88.0 now scans against a frozen database.
  • Attention: Grype 13,053 stars, Syft 9,701.

OSV-Scanner

  • Latest: v2.7.0 (12 October 2026, 7:02 AM), which adds purls to JSON output, severity in CycloneDX output, Bitnami SPDX support and a fix for offline mode when databases are downloaded in the same run. Before it, v2.6.0 (14 September, 8:25 AM).
  • Release rhythm: 10 GitHub releases dated 2026. Releases are not marked immutable; v2.7.0 ships SLSA provenance (multiple.intoto.jsonl).
  • Licence and governance: Apache-2.0, in the google organisation. The README calls it “an officially supported frontend” to the OSV database at osv.dev, built on the OSV-SCALIBR extraction library.
  • Database: the OSV.dev API by default; per-ecosystem zip exports for offline use.
  • Attention: 11,212 stars.

The GitHub Actions around them

  • aquasecurity/trivy-action: v0.36.0 (22 April 2026, 3:09 PM), immutable. After the March incident the project deleted the old tags and published new ones with a v prefix.
  • aquasecurity/setup-trivy: v0.3.1 (3 June 2026), immutable.
  • anchore/scan-action: v7.4.2 (29 August 2026, 1:13 AM), immutable. It includes a fix to pin the Grype install.sh script to the release tag being installed (pull request #750).
  • google/osv-scanner-action: v2.7.0 (12 October 2026, 7:09 AM), not marked immutable.

Timeline

  • 17 March 2025: OSV-Scanner v2.0.0 adds container image scanning.
  • 28 October 2025: GitHub immutable releases become generally available (public preview from 26 August).
  • 24 November 2025: Grype v0.104.1 fixes CVE-2025-65965 (registry credentials in JSON output).
  • 18 February 2026: trivy-action command injection advisory (CVE-2026-26189).
  • 27 February 2026: a pull_request_target workflow in the Trivy repository is abused and organisation secrets are stolen, per the project’s own incident discussion.
  • 1 March 2026: Trivy releases v0.27.0 to v0.69.1 are deleted in the takeover; the workflow is removed the same afternoon (pull request #10259, 3:45 PM).
  • 6 March 2026: Grype database schema v5 stops receiving updates.
  • 19 and 20 March 2026: malicious Trivy v0.69.4, trivy-action and setup-trivy tags (CVE-2026-33634). Advisory published 21 March, 9:21 PM.
  • 22 and 23 March 2026: malicious Trivy 0.69.5 and 0.69.6 images on Docker Hub.
  • June 2026: three more Trivy advisories, fixed in 0.71.0, 0.71.1 and 0.72.0.
  • 12, 14 and 20 October 2026: OSV-Scanner v2.7.0, Trivy v0.76.0, Grype v0.121.0 and Syft v1.55.0.

First question: what does a green scan actually claim?

Every scanner here does the same four things in the same order, and each step can quietly change the answer.

  1. Inventory. The scanner works out which packages are present: from the distro package database (/var/lib/dpkg/status, /lib/apk/db/installed, the RPM database), from language metadata (site-packages records, node_modules, lockfiles, JAR manifests) and, in some tools, from fingerprints of bare binaries.
  2. Matching. Each package is compared with advisories. The big choice is which source wins for which package: the distro’s own tracker (which knows about backported fixes), an ecosystem database such as the GitHub Advisory Database or PyPI’s, or NVD with CPE names (which knows little about how a distro patched a package).
  3. Database build. The advisories come from a database that was built at some moment and copied to your runner. A CVE published after that build does not exist for your scan.
  4. Policy. Severity thresholds, “fixed only” filters, ignore files and VEX statements decide what reaches the exit code. A pipeline can be green because nothing matched, or because everything matched was filtered.

So the honest reading of a green result is a sentence with blanks:

“For the packages this tool found, matched with this tool’s rules against the database built at this time, nothing passed this policy.”

If your CI logs cannot fill every blank, the green tick is weaker than it looks.

What each scanner actually is

Trivy 0.76.0

Trivy is a single Go binary that scans container images, filesystems, Git repositories, root filesystems, VM images, Kubernetes clusters and SBOMs. Vulnerabilities are one of several scanners; it also looks for misconfigurations, secrets, licences and, newer, cryptographic assets. For an image the default scanners are vuln and secret. It generates SBOMs itself (CycloneDX and SPDX), reads SBOMs as input and supports VEX, which its docs still mark experimental. The vulnerability database is downloaded as an OCI artefact on first use and refreshed when it is older than its NextUpdate time; Java JAR scanning uses a second database, trivy-java-db.

The docs state its matching stance plainly: for OS packages, trust the vendor’s advisories, because a distro often backports a fix without changing the upstream version. They also admit the cost. Packages from third-party repositories (EPEL, Docker’s apt repository and similar) are skipped, found by a heuristic that can be wrong either way, and the default --detection-priority precise can miss issues the vendor has not assessed. comprehensive lets upstream advisories in, with more false positives.

Grype 0.121.0 with Syft 1.55.0

Grype is Anchore’s vulnerability matcher; Syft is Anchore’s SBOM generator, and Grype uses Syft’s catalogers as a library when you hand it an image. You can run Grype on an image directly, or run Syft first, keep the SBOM as a build artefact and feed it to Grype later with grype sbom:./sbom.json. Syft writes Syft JSON, CycloneDX (JSON and XML), SPDX (JSON and tag-value) and GitHub’s dependency format.

Grype’s database is a SQLite file built by vunnel from distro trackers, the GitHub Advisory Database, NVD and auxiliary feeds. Every match carries a type: exact-direct-match (the package matched an advisory for that package), exact-indirect-match (matched through its source package, such as a Debian source package) and cpe-match (matched through a CPE name, usually from NVD). The interpretation guide says CPE matches are more prone to false positives. Grype also shows EPSS, the CISA KEV flag and its own risk score next to each finding, which helps ordering but is not a severity.

OSV-Scanner 2.7.0

OSV-Scanner is Google’s frontend to the OSV database and to the OSV-SCALIBR extraction library. It scans source directories and lockfiles (requirements.txt, package-lock.json, go.mod, pom.xml and many more), container images and SBOMs. By default it sends package names and versions to the OSV.dev API and gets matching OSV records back; with --offline it uses per-ecosystem zip exports on disk instead. OSV records come from many upstream databases, including GitHub’s, PyPI’s, Go’s, RustSec, Ubuntu, the OpenSSF malicious packages list and converted Debian, Alpine and NVD data.

Two features set it apart. It resolves transitive dependencies for some manifests (on by default; --no-resolve turns it off), and it has guided remediation (osv-scanner fix), which proposes version bumps for npm and Maven projects. Image scanning needs a local Docker daemon or a docker save tarball passed with --archive. Licence checks (--licenses) use licence data from deps.dev. Go projects can get experimental call analysis, which I did not test.

Side by side

Where a project’s own docs say nothing on a point, I write “Not documented”. Counts in this section come from the drill further down.

Licence and governance

  • Trivy: Apache-2.0, maintained by Aqua Security, which sells a commercial platform built around it.
  • Grype and Syft: Apache-2.0, sponsored by Anchore, which offers commercial support.
  • OSV-Scanner: Apache-2.0, in the Google GitHub organisation. The OSV data sources carry their own licences, listed on the OSV.dev data page (for example CC-BY 4.0 for the GitHub Advisory Database and CC-BY-SA 4.0 for Ubuntu).
  • Data licences for the Trivy and Grype databases: Not documented on the pages I read. If you redistribute a database in an air-gapped bundle to customers, read the upstream terms. This is not legal advice.

What each one matches against

  • Trivy: for OS packages, the distro vendor’s advisories only, by default, so a backported fix is not flagged. For language packages, the GitHub Advisory Database and GitLab advisories per ecosystem. Severity comes from the vendor first, NVD as a fallback.
  • Grype: distro providers for OS packages, matched directly and through source packages, so backports are respected there; GitHub advisories for language packages; NVD CPE matching, which knows nothing of backports, where a package has no better data or is a bare binary.
  • OSV-Scanner: OSV records for every ecosystem, including Debian and Alpine in OSV form. Results carry OSV IDs such as GHSA-..., PYSEC-..., DLA-... or DEBIAN-CVE-..., with CVE IDs in an aliases field.

Packages outside the package manager

  • Trivy: language packages it can see in metadata; third-party OS repository packages skipped by design; binaries without metadata mostly not matched (Go binaries are read through their build information).
  • Grype: Syft’s binary classifiers recognise interpreters and runtimes such as CPython, Node.js and OpenSSL by fingerprint, and Grype matches them by CPE. On the drill images this was the biggest single source of disagreement.
  • OSV-Scanner: in the drill, the Node image inventory held only Debian packages, so the npm packages bundled with Node were not checked, although the docs list node_modules as supported. I did not find the reason; one guess is that npm’s bundled tree has no .package-lock.json. Syft found 204 npm packages there.

False positives and false negatives

  • Trivy: the docs name both risks: heuristics may misclassify third-party packages, and precise mode may miss unassessed issues.
  • Grype: the docs say CPE matches can be false positives; v0.120.0 fixed one such case. Ignore rules can target a whole match type.
  • OSV-Scanner: false negatives came from an inventory gap (the Node case above); extra results came from transitive resolution, which added idna and setuptools versions that were not in my requirements.txt.

Severity and prioritisation

  • Trivy: one severity per finding, vendor first. On nginx:1.29-alpine it had 0 CRITICAL.
  • Grype: a severity, plus EPSS, KEV and a risk score. On the same image it had 26 CRITICAL. Of the 113 CVEs that both reported, the severity label differed on 49.
  • OSV-Scanner: shows CVSS from the OSV records, grouped by package.
  • So: a “fail on critical” gate is not portable between tools. Pick one severity source per pipeline.

Ignore files and VEX

  • Trivy: .trivyignore with optional expiry (exp:2026-12-31), a YAML variant with reasons and paths, Rego policies, and VEX from a file, a VEX repository, OCI attestations or SBOM references (experimental). VEX formats: OpenVEX, CycloneDX and CSAF.
  • Grype: ignore rules in .grype.yaml by vulnerability, package, fix state or match type, and VEX documents (OpenVEX and CSAF) with --vex. An expiry date on ignore rules: Not documented. Ignored findings move to an ignoredMatches list in JSON, which is good for audit.
  • OSV-Scanner: osv-scanner.toml with IgnoredVulns entries that carry ignoreUntil and reason, and package overrides. Expired ignores come back and are reported. VEX input: Not documented.
  • The trap: an ignore or VEX statement names an ID. Trivy reported the pip finding under its CVE ID; Grype reported it under the GitHub advisory ID it matched; OSV-Scanner uses OSV IDs throughout. One statement does not cover all three unless it lists aliases.

SBOM generation and SBOM input

  • Trivy: trivy image --format cyclonedx or spdx-json; trivy sbom file.json to scan one.
  • Grype and Syft: syft <image> -o cyclonedx-json; grype sbom:file.json.
  • OSV-Scanner: reads SPDX and CycloneDX SBOMs; can write CycloneDX 1.4 to 1.7 and SPDX 2.3 as report formats.
  • Interchange in practice: each tool read its own SBOM back with its image result; across tools, Debian findings collapsed (numbers in the drill). A likely cause is where each writer puts the Debian source package name, which distro advisories are keyed on: Syft in the purl (upstream=glibc), Trivy in a tool-specific property (aquasecurity:trivy:SrcName).

Database delivery, freshness and offline CI

  • Trivy: DB at mirror.gcr.io/aquasec/trivy-db:2, then ghcr.io/aquasecurity/trivy-db:2; --db-repository sets a mirror. In CI, cache the directory and use --skip-db-update; --offline-scan stops lookups such as Maven Central for JAR files. The JSON report records the Trivy version but not the DB build time. With a deliberately old metadata date and --skip-db-update, Trivy printed no warning.
  • Grype: DB at grype.anchore.io; GRYPE_DB_AUTO_UPDATE=false for offline use; grype db import for a downloaded archive. validate-age is on, with a maximum built age of 120 hours, so a stale database fails the scan instead of passing it. The JSON report records the DB schema, build time and each provider’s capture time.
  • OSV-Scanner: online by default. Offline needs --download-offline-databases once and --offline-vulnerabilities (or --offline) after. Freshness of the local zips: Not documented in the report output.

Exit codes and CI gates

  • Trivy: exits 0 with findings unless you pass --exit-code. --ignore-unfixed hides findings with no fixed version. --exit-on-eol fails on an unsupported OS.
  • Grype: exits 0 with findings unless you pass --fail-on <severity>, which returns 2. --only-fixed hides unfixed findings.
  • OSV-Scanner: exited 1 whenever it found anything, 0 when it found nothing. There is no severity threshold flag; policy goes in osv-scanner.toml.

Release integrity

  • Trivy: immutable GitHub releases, a Sigstore bundle (.sigstore.json) per archive and a checksums file.
  • Grype and Syft: immutable releases; the checksums file carries a Sigstore bundle.
  • OSV-Scanner: not immutable on GitHub; SLSA provenance for slsa-verifier.
  • All three verified in the drill.

Worked example for the Bhubaneswar scenario

The real problem is not which scanner is best. Nobody can say what their green reports meant, and their CI trusted a moving tag.

Step 1: write down what CI runs today. trivy-action by a version tag, the default DB download, no exit code. Three unknowns follow: which code ran (tags can move, as March showed), which database build it used (not in the report) and what would have failed the job (nothing; Trivy exits 0 by default).

Step 2: pin the scanner like any other dependency. For actions, pin to a full 40-character commit SHA and let a bot propose updates. For the binary, check the SHA-256 and Sigstore bundle once and store the checksum in the repository. These are the real values from the drill for Trivy 0.76.0 on Linux amd64:

TRIVY_VERSION=0.76.0
TRIVY_TGZ=trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz
TRIVY_SHA256=e24b1c51293c1b15e4ba8d45bbc7891ba053f90278ec2698f3562c993e145fe7
curl -sSfLO "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/${TRIVY_TGZ}"
curl -sSfLO "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/${TRIVY_TGZ}.sigstore.json"
echo "${TRIVY_SHA256}  ${TRIVY_TGZ}" | sha256sum -c -
cosign verify-blob "${TRIVY_TGZ}" --bundle "${TRIVY_TGZ}.sigstore.json" \
  --certificate-identity-regexp '^https://github.com/aquasecurity/trivy/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
trivy version   # prints the DB UpdatedAt and NextUpdate; keep this line in the job log

Step 3: scan the digest you deploy, and keep the SBOM with it. Build, push, read the digest back, and scan image@sha256:... rather than a tag. Generate the SBOM at build time with the same tool family that will scan it later (Trivy SBOM for Trivy, Syft SBOM for Grype) and store it as a build artefact. Crossing families lost most Debian findings in the drill.

Step 4: pick one gate and one second opinion. The team knows Trivy, so Trivy stays the gate for now: --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 --exit-on-eol 1. Grype runs weekly on the same digests as a second opinion, because that is what the insurer runs. When Grype reports something Trivy does not, the first question is the match type in Grype’s JSON. If it is cpe-match on a binary, check the upstream advisory against the version in the image and record a decision.

Step 5: fix the images that the gate cannot see. The alpine:3.18 cron image looked clean in Trivy and OSV-Scanner; only a log line and Grype’s warning said the distro is out of support. Move it to a supported Alpine release. Plan the Node 18 upgrade, and until then scan its npm tree separately, because one tool did not see it inside the image.

Step 6: add OSV-Scanner on pull requests for lockfiles. It handles requirements.txt and package-lock.json quickly, supports ignoreUntil with a reason, and exits 1 on any finding, which suits a pull request check better than a release gate. Use --no-resolve for files that already pin every version, because default resolution added two packages that were not in the file. Keep the OSV-Scanner action pinned by commit SHA too; its releases are not immutable.

Step 7: answer the insurer with evidence, not a green tick. Send the digest, the SBOM, the scanner version, the DB build time, the policy and a VEX document for findings judged not affected, listing both CVE and GHSA IDs. Then the two runs can be compared line by line, and the disagreement shrinks to a short list of CPE matches.

None of this needs a new tool, only the four blanks filled in for every scan.

Security and hygiene

A scanner runs with your registry credentials, your build workspace and often your cloud tokens. In 2026 that stopped being theory.

Trivy. The large incident is GHSA-69fq-xp46-6x23, CVE-2026-33634, critical (CVSS 4.0 score 9.4), published 21 March 2026 at 9:21 PM. The advisory calls it a continuation of the late February attack: credential rotation after the first disclosure was not atomic, and a still-valid token was enough. Three things followed. A malicious Trivy v0.69.4 went out through the usual channels, including GitHub releases, container registries, deb and rpm packages and get.trivy.dev (19 March 11:52 PM to about 3:12 AM on 20 March). 76 of 77 trivy-action tags were force-pushed to code that dumped the runner process memory and swept the disk for credentials (about 11:13 PM on 19 March to about 11:10 AM on 20 March). All 7 setup-trivy tags were replaced (until about 3:14 AM). Later, images 0.69.5 and 0.69.6 reached Docker Hub through separately compromised Docker Hub credentials (22 March 9:13 PM to about 7:10 AM on 23 March). Unaffected, per the advisory: v0.69.2 and v0.69.3, trivy-action 0.35.0 (immutable releases were enabled on 4 March) and images referenced by digest. Its advice: rotate every secret the affected pipelines could reach, look for repositories named tpcp-docs in your GitHub account (the malware created one when its normal exfiltration failed), and pin actions to full commit SHAs.

The earlier incident is described in the maintainers’ conclusion discussion (30 March 2026, 8:42 PM): on 27 February a pull_request_target workflow was abused to steal organisation secrets, and on 1 March releases v0.27.0 to v0.69.1 were deleted and the repository was renamed. Their hardening list includes a full credential reset, no more pull_request_target, SHA-pinned actions, immutable releases, tag protection, SLSA provenance with Sigstore signing, and SSO with an IP allow-list.

Other 2026 Trivy advisories, all fixed: CVE-2026-26189 in trivy-action (18 February, 11:17 AM; command injection through the environment file; fixed in 0.34.1); CVE-2026-54448 (4 June, 1:11 PM; a crafted Helm archive could exhaust memory; fixed in 0.71.0); CVE-2026-55092 (15 June, 3:35 PM; path traversal through an OCI artefact title annotation, reachable through a hostile --db-repository; fixed in 0.71.1); and CVE-2026-63328 (30 June, 5:44 PM; path traversal in the plugin manager; fixed in 0.72.0).

Grype and Syft. No Grype repository advisory was published in 2026. The most recent is CVE-2025-65965 (24 November 2025, 11:01 PM; registry credentials could appear in JSON output from v0.68.0 to v0.104.0; fixed in v0.104.1). Syft had CVE-2026-33481 (20 March 2026, 8:27 PM; temporary files left behind; fixed in v1.42.3) and GHSA-mw2c-m758-9v5q (18 September 2026, 2:30 AM; crafted input could cause very large memory allocations; fixed in v1.52.0; no CVE listed). For the action, issue #632 (24 March 2026) pointed out that scan-action fetched Grype’s install script from the main branch, so pinning the action by SHA did not pin the script. Release v7.4.2 pins it to the release tag; the issue was closed on 11 September.

OSV-Scanner. No repository security advisories are listed. osv-scalibr issue #1867 (7 March 2026) reported that osv-scanner fix on a malicious pom.xml could send Maven credentials to a server the file named; the maintainers planned to put the risky behaviour behind an explicit unsafe-plugins switch. The README now warns that guided remediation can run package manager scripts or follow registries named in the project, so run it only on code you trust.

Habits that matter more than the counts

  • Pin every scanner action by full commit SHA, every scanner image by digest, and every binary by checksum. Tags were the attack path in March.
  • Verify signatures or provenance once per upgrade, in a separate step, and keep the result in the job log.
  • Give the scan job read access to the registry and nothing more: no deploy keys, no cloud admin token.
  • Do not run scanners or fix commands with secrets on pull requests from forks. pull_request_target was the entry point in February.
  • Treat the database as an input with a source: mirror it if you can and log its build time with every scan.
  • Watch the security advisories of the scanner repositories you use.

Decision guide

Lean towards Trivy if

  • You want one binary for images, filesystems, Kubernetes, IaC misconfiguration, secrets and SBOMs.
  • You prefer the vendor’s view of OS packages and fewer CPE-style false positives, and accept that unassessed issues may not appear.
  • You will pin it properly after March: binary by checksum, actions by SHA, images by digest.
  • You can add --exit-on-eol and a DB age check of your own.

Lean towards Grype (with Syft) if

  • You want the SBOM as a first-class build artefact and scanning as a separate, repeatable step.
  • You want each finding to say how it was matched, plus EPSS and KEV to help ordering.
  • You want the scan to refuse a stale database by default.
  • You have time to review CPE matches on binaries.

Lean towards OSV-Scanner if

  • Your main surface is source code and lockfiles in many ecosystems, checked on every pull request.
  • You want ignores that expire by themselves and a report that lists unused ones.
  • You will test its image scanning on your own images before trusting it there.

Be careful whichever you pick

  • Do not compare raw counts between tools. Fold IDs to CVEs and compare sets.
  • Do not move a “fail on critical” rule from one tool to another without testing it on real images.
  • Do not assume VEX or ignore entries carry across tools; list every ID alias.
  • Do not trust a green result on an end-of-life base image.

Bhubaneswar (invented) pick. Keep Trivy as the release gate, installed from a checksum-pinned and signature-checked binary, with --exit-on-eol 1 and the DB time logged. Generate a Syft SBOM at build time and run Grype weekly as the second opinion the insurer expects. Add OSV-Scanner on pull requests for the two lockfiles. Replace the Alpine 3.18 cron image this month and plan the Node 18 upgrade. Three tools for three jobs, not three tools for one job.

Checklist

  1. Every scanner action is pinned by full commit SHA, every image by digest, every binary by checksum.
  2. Signatures or provenance were verified once for the version in use, and the command is in the repository.
  3. The job log shows the scanner version and the database build time for every scan.
  4. The gate scans the digest that will be deployed, not a tag.
  5. An end-of-life base image fails the build (--exit-on-eol in Trivy, or Grype’s warning treated as a failure).
  6. The SBOM is made at build time and scanned by the same tool family that made it.
  7. Every ignore entry has a reason and an expiry date, or lives in a tool that supports one.
  8. VEX statements list the CVE and every alias the tools report, such as the GHSA ID.
  9. The scan job has read-only registry access and no deploy credentials.
  10. Someone owns the weekly second-opinion run and the short list of disagreements.

Common mistakes

  1. Reading a green tick without knowing the exit code was 0 by default.
  2. Comparing finding counts between tools instead of comparing ID sets.
  3. Treating --ignore-unfixed or --only-fixed as “no risk”; the Python image went green with 172 findings underneath.
  4. Downloading the DB from the public endpoint on every job, then skipping the scan when the download is slow.
  5. Caching the DB forever with --skip-db-update and never checking its age.
  6. Writing VEX with only a CVE ID and expecting every scanner to honour it.
  7. Ignoring a finding with no end date, so it stays hidden after the fix ships.
  8. Feeding one tool’s SBOM to another and reading the smaller number as progress.
  9. Trusting an end-of-life base image because its scan is quiet.
  10. Giving the scan job the same secrets as the deploy job.

Drill on this box, 20 October 2026

The box is a shared VM running Debian 13, with 8 vCPUs, 15 GiB of RAM, other jobs alongside and no Docker. Downloads and verification ran from about 6:19 to 6:22 AM IST, scans from about 6:23 to 6:33 AM, with signature checks repeated at 6:39 AM. Single runs throughout; no number here is a speed claim.

Tools and checks. Trivy 0.76.0, Grype 0.121.0, Syft 1.55.0 and OSV-Scanner 2.7.0 from GitHub releases; all four SHA-256 values matched the published checksums. cosign 3.1.3 verified the Trivy tarball’s Sigstore bundle (signed by the project’s reusable-release.yaml workflow at tag v0.76.0, logged in Rekor at 5:13 PM IST on 14 October) and the signed checksum files of Grype and Syft; slsa-verifier 2.7.1 passed the OSV-Scanner binary against its provenance for tag v2.7.0.

Databases. Trivy pulled mirror.gcr.io/aquasec/trivy-db:2: 128.87 MiB compressed, a 1,637,736,448-byte trivy.db, UpdatedAt 20 October 12:38 AM IST and NextUpdate 24 hours later. Grype pulled schema v6.1.10, built 19 October 12:09 PM IST: 191,935,956 bytes compressed, 3,287,412,736 bytes unpacked. OSV-Scanner’s offline download for PyPI and npm was 36,469,854 and 218,597,937 bytes. The files landed under an osv-scalibr/ folder inside the cache, not the osv-scanner/ path shown in the docs.

Images. Pulled for linux/amd64 with crane as tarballs: python:3.12-slim (Debian 13.7, created 6 October 2026), node:18-bookworm-slim (Debian 12.11, created 27 March 2025), alpine:3.18 (3.18.12, created 14 February 2025) and nginx:1.29-alpine (Alpine 3.23.4, created 15 April 2026). Unique IDs are counted after folding OSV and GHSA records to CVEs through their aliases.

  • python:3.12-slim: Trivy 79 IDs (172 rows), Grype 80 (172), OSV-Scanner 75 (188). All three agreed on 73. Grype’s extra 7 were CPE matches on the CPython 3.12.15 interpreter; Trivy’s extra 6 were Debian tracker items; OSV-Scanner had 2 of its own. The image still carries pip 25.0.1 with 6 fixed CVEs.
  • node:18-bookworm-slim: Trivy 201 IDs (349 rows, 37 from npm packages), Grype 222 (369 rows: 38 npm, 28 on the node binary), OSV-Scanner 158 (312 rows, Debian packages only). OSV-Scanner’s inventory had 88 Debian packages and no npm packages.
  • alpine:3.18: Trivy 0 and OSV-Scanner 0. Grype 14 rows (6 IDs), all CPE matches, plus a warning that 15 packages come from an end-of-life distro.
  • nginx:1.29-alpine: Trivy 113, Grype 133, OSV-Scanner 109. Grype’s extra 20 were CPE matches on libcrypto3, libssl3, c-ares, nginx and a few others. Severity differed on 49 of the 113 shared CVEs; Trivy had 0 CRITICAL, Grype 26.

Trivy on Alpine 3.18, log lines and summary (table borders removed):

WARN  This OS version is no longer supported by the distribution  family="alpine" version="3.18.12"
WARN  The vulnerability detection may be insufficient because security updates are not provided
Report Summary
  img/alpine318.tar (alpine 3.18.12)   alpine   0
Legend:
- '-': Not scanned
- '0': Clean (no security findings detected)

Grype on the same image ended with: 15 packages from EOL distro "alpine 3.18.12" - vulnerability data may be incomplete or outdated; consider upgrading to a supported version. With --exit-on-eol 1 the same scan exited 1.

Exit codes and gates. Default runs: Trivy 0 and Grype 0 despite findings; OSV-Scanner 1, and 0 on Alpine 3.18. With Trivy --ignore-unfixed --severity HIGH,CRITICAL --exit-code 1 and Grype --only-fixed --fail-on high: the Python image passed both (Trivy 0 findings left, Grype 13 matches, none high or critical), Alpine 3.18 passed both, and nginx failed both (Trivy exit 1 with 45 findings, Grype exit 2 with 117 high or critical matches).

Lockfiles. A requirements.txt with seven old pins (Django 4.2.0, requests 2.28.1, cryptography 39.0.0 and others) and an npm project with 93 lockfile entries. Trivy and Grype each found the same 149 IDs. OSV-Scanner by default added a second result with idna 3.9.0 and setuptools 9.1.0, which were not in the file; with --no-resolve it reported 151 IDs in 238 entries (GHSA and PYSEC records for the same issues). 148 IDs were common to all three. Offline OSV-Scanner gave exactly the online result. For licences, Trivy read 91 from the npm lockfile only, while OSV-Scanner’s --licenses (deps.dev) covered 98 packages including Python and flagged cryptography (PSF-2.0 AND (Apache-2.0 OR BSD-3-Clause)) against my allow-list.

SBOM crossover, Python image. Grype on Syft’s CycloneDX: 80 IDs, as for the image; Trivy on it: 26 (Debian rows fell from 166 to 20). Trivy on its own CycloneDX: 79; Grype on it: 24 (Debian rows 159 to 18). OSV-Scanner on either SBOM found no Debian packages, only the 6 pip IDs. The Node image showed the same pattern, except that OSV-Scanner on either SBOM found the 38 npm IDs its image scan had missed.

VEX, pip 25.0.1 in the Python image. An OpenVEX not_affected statement naming CVE-2025-8869 removed it from Trivy (172 to 171) but not from Grype, which reported it as GHSA-4xh5-x5gv-qwph; product IDs by image name, digest or purl did not change that. Naming the GHSA worked for Grype (moved to ignoredMatches) and not for Trivy. Naming the CVE with the GHSA in aliases worked for both. Grype with --by-cve honoured the CVE-only statement.

Ignore expiry and DB age. A .trivyignore entry with exp:2026-10-01 came back; one with exp:2026-12-31 stayed hidden. In osv-scanner.toml, a past ignoreUntil came back and was listed as an unused ignore. With GRYPE_DB_MAX_ALLOWED_BUILT_AGE=12h, Grype refused to scan: “the vulnerability database was built 18 hours ago (max allowed age is 12 hours)”. For Trivy I changed only the DB metadata to 1 March 2026 and ran with --skip-db-update: no warning, exit 0. That is a metadata-only simulation, not a truly old database.

What this box could not test: the GitHub Actions themselves (no runner), a genuinely old database, Java JAR scanning and the Java DB, Kubernetes and VM scans, private registries, VEX repositories and OCI attestations, CSAF VEX, OSV-Scanner call analysis and guided remediation, Windows and macOS, a real network cut-off (offline modes were tested by flags only) and any speed comparison. Nothing outside the drill folder was touched.

Unlearn

  • Unlearn that a green scan means “no vulnerabilities”.
  • Unlearn that two scanners should give the same count.
  • Unlearn that a version tag on an action is a version.
  • Unlearn that an SBOM is portable because its format is standard.
  • Unlearn that quiet means safe on an old base image.

Re-learn

  • Re-learn the four parts of a result: inventory, matcher, database build, policy.
  • Re-learn your exit codes and write them into the job.
  • Re-learn VEX as a statement about IDs, aliases included.
  • Re-learn pinning: SHA for actions, digest for images, checksum and signature for binaries.
  • Re-learn your second opinion, and give it an owner.

Closing

Vulnerability scanners in 2026 differ less in whether they find problems than in what they count and what they trust. Trivy 0.76.0 trusts the distro and covers the widest set of targets in one binary, and after March it deserves the same pinning you would give any build dependency. Grype 0.121.0 with Syft 1.55.0 separates inventory from matching, shows how each match was made and refuses a stale database, at the cost of CPE noise on binaries that you must review. OSV-Scanner 2.7.0 is strongest on lockfiles and pull requests, with honest expiring ignores, and its image scanning needs testing on your own images. The post on container image builds is where the SBOM should be born, and the post on Kubernetes admission policy is where a scan result can finally block a deploy. Learn the four parts of a result. Unlearn the green tick. Re-learn your pins, your IDs and your exit codes. Practise them. Apply them before you trust another green scan.

Sources

Trivy

Grype and Syft

OSV-Scanner

Incident and security records

GitHub and supply chain

Internal

comments powered by Disqus

Releted Posts

Lightweight Kubernetes in 2026: revisit k3s, k0s and Talos Linux before you stand up another small cluster

Picture a software company in Visakhapatnam that builds gate and yard software for container freight stations around the Visakhapatnam and Gangavaram ports, with two more customers at Kakinada and one inland depot near Vizianagaram (an invented example, not a real client).

Read more

Kubernetes secrets delivery in 2026: revisit External Secrets Operator, Sealed Secrets and SOPS before you commit another Secret YAML

Picture a platform team in Jaipur that runs a multi-tenant SaaS product on Kubernetes across Rajasthan and Delhi NCR (an invented example, not a real client).

Read more

Container registries in 2026: revisit Harbor, Zot and CNCF Distribution before you push another image

Picture a product engineering team in Bengaluru that runs about forty microservices on Kubernetes for a B2B payments product used by NBFCs across Karnataka and Maharashtra (an invented example, not a real client).

Read more