grype
On this page
- Control:
images - Tool: Anchore Grype,
grype registry:<ref> -o sarif - Status: ✅ implemented, opt-in (Trivy runs by default)
- Targets: container images
- License / terms: see license, terms and what is sent
What it does
Runs Grype against a container image and returns its native SARIF. Grype catalogs the packages in the image with Syft, then matches them against its vulnerability database.
It is a second scanner for a control Trivy already serves, and Trivy stays the default. Enable it per component, or for the project:
config:
controls:
images:
grype:
enabled: true
Both then run, and both sets of findings appear. That is the point rather than a side effect: two matchers over the same image disagree in ways that say something about coverage, and a qualification tool exists to be able to answer why the verdict is what it is.
Expect more findings, not a higher count. A flaw both scanners find is reported twice, once
under each tool's own rule identifier, because the disagreement between two scanners is the reason
to run two and a merge that keeps one opinion throws away what you were paying for. One of the pair
is counted and the other is evidence: the priority counts, the gate and draugr diff's gate all
skip the copy, and results.sarif marks it with properties.correlation.countedUnder so anything
reading the file can do the same.
What does rise is the number of rows in the report, which is the point. See writing an exclusion for the consequence that actually needs handling.
Choices worth knowing
The image is pulled from the registry, not from a local daemon. A bare reference makes Grype try a Docker daemon first and fall back to the registry. On a CI runner there is no daemon, so that is a failed connection before every scan. Where there is one, it scans whatever the daemon happens to hold under that tag rather than what the registry serves now, and scanning the wrong bytes is worse than scanning slowly. Draugr pins images to a digest wherever it can, and a digest means nothing to a daemon that never pulled it.
Findings are reported against the image reference. Grype locates an OS package finding at
alpine//lib/apk/db/installed, the image name joined to the package database inside it. That is a
real path in a filesystem nobody has, it reads like a source file in the console, and a component
shipping two images produces findings you cannot tell apart. The reference that was pulled is the
honest answer to "where is this".
--by-cve is on by default, which is a departure from Grype's own default. Grype reports a
language-ecosystem finding under the advisory that described it, GHSA-8q59-q68h-6hv4, where Trivy
reports the CVE for the same flaw. Left alone, one vulnerability arrives under two identities
depending on which scanner saw it, so it counts twice and an exclusion written against the CVE stops
matching. Set byCve: false to see the identifier Grype matched on.
No filtering flags. --fail-on, --only-fixed, --ignore-states and --exclude all drop
findings inside the tool, where Draugr cannot mark them suppressed or record who accepted them.
config.exclude in the Saga does that and keeps the evidence; the gate thresholds decide what
fails.
Each finding's package is read from Grype's JSON report. Grype's SARIF states a package once
per advisory and package name, and an image can hold one package at several versions, one per
node_modules directory, each a match of the same advisory. The version, fixed version and package
URL therefore come from the finding's own entry in the JSON report the same run writes, which is
what correlates it with Trivy's copy at that version. A finding with no entry there takes the
package the SARIF states only where it is that finding's version, and otherwise carries none.
Each finding keeps the identifiers Grype relates to its match in relatedVulnerabilities. A
finding reported by CVE names the GitHub advisory it was matched on, and one reported by GitHub
advisory names its CVE. Another scanner's copy keyed on any of them is counted once with this one, and a
reachability verdict reported under any of them reaches it.
Writing an exclusion that covers both
Grype's rule identifier is the vulnerability followed by the package it was found in.
CVE-2020-14343-pyyaml, where Trivy's is the bare CVE-2020-14343. An exclusion naming the CVE
exactly therefore suppresses Trivy's finding and not Grype's, which is the worst shape a gap can
take: a judgement someone recorded, applied to half the report, with nothing saying so.
Use a trailing *, which config.exclude supports:
config:
exclude:
- rules: ["CVE-2020-14343*"]
reason: "not reachable from any entry point, reviewed 2026-08-14"
Both findings then stay in the report marked suppressed, carrying that reason.
This is also the concrete reason --by-cve is on. Without it Grype reports the same flaw as
GHSA-8q59-q68h-6hv4-pyyaml, and no CVE-shaped pattern reaches it at all.
Exclusions
A match that an ignore: rule in Grype's own configuration set aside is reported suppressed with
origin: scanner and the rule's reason. Grype's SARIF leaves such a match out, so the scan also
asks for -o json into a temporary file, where Grype lists each match it ignored with the rules
that ignored it. A run that writes no JSON report is an error.
The source names .grype.yaml or .grype/config.yaml in the directory Draugr runs in, whichever
holds the rule. Grype also reads the file in the home directory, and a rule from there is reported
with no source. The rules Grype applies to kernel headers with no configuration at all are its
matching policy, and are not reported.
The database
Grype is a matcher with no vulnerability data of its own. It downloads a database, keeps it in a
local cache, and refuses to scan against one more than five days old, so a binary on PATH is
not yet a scanner that can run. draugr doctor asks Grype whether what it has is usable, so the
answer arrives before a scan rather than as an error in the middle of one.
Draugr downloads it once per run, before the scans fan out, rather than letting every concurrent job cold-start it.
Draugr then reads the database once with grype db providers before the first scan, offline
included, so a database an older Grype downloaded is migrated before any scan opens it.
For an air-gapped runner, see the air-gapped guide. With
--offline (or DRAUGR_OFFLINE) Draugr sets GRYPE_DB_AUTO_UPDATE=false, because Grype checks for
a newer database when a scan starts as well as at update time. Without it an offline run still
reaches out, once per job.
The staleness check is not disabled to keep things quiet. A scanner that reports a pass against a database that stopped being updated is the failure this whole tool exists to prevent.
License, terms and what is sent
Tool license: Apache-2.0. Draugr executes Grype as a subprocess and neither links nor bundles
it, so the license stays Anchore's. Releases are pinned by SHA-256 in draugr tools install, from
a checksums.txt verified out of band with cosign verify-blob against the anchore/grype release
workflow identity.
The database is a separate artifact and is not covered by that license. Anchore publishes it at
grype.anchore.io, without authentication and at no cost. Anchore's published legal documents,
terms of service, the master software license agreement, the privacy policy, govern their website
and their commercial platform; none of them purport to govern the open-source tools or this
database, and there is no separate agreement gating it. The instruments that do apply are the
Apache-2.0 license covering Grype and the pipeline that builds the database, and the terms of the
upstream sources it aggregates: NVD, GitHub Security Advisories, the distribution security trackers,
Bitnami, Chainguard, ECHO, MINIMOS and Wolfi, each of which carries its own.
Those upstream terms bind whoever redistributes the data. Draugr does not: the database is fetched by Grype, onto the machine running the scan, from Anchore. This is the same posture Draugr takes to Trivy's database, and it is the reason the project's rule is point, don't host. Mirroring the database ourselves would be distribution, and would attach every one of those terms to us.
What is sent: nothing. Grype uploads no code, no SBOM and no findings. Traffic is outbound downloads of the vulnerability database. No account, no key, no telemetry.
Links
- Grype: https://github.com/anchore/grype
- Data sources: https://oss.anchore.com/docs/reference/grype/data-sources/
- Database guide: https://oss.anchore.com/docs/guides/vulnerability/database/
- Sibling scanner over a repository:
grype-fs - The default for this control:
trivy
Data
The vulnerability database, from grype.anchore.io. Downloaded once before the jobs fan out,
then read once before the first scan.
An offline run reads the cache Grype keeps; GRYPE_DB_AUTO_UPDATE=false against a populated
cache is how to hold it to that.
Grype runs with GRYPE_CHECK_FOR_APP_UPDATE=false, so a scan never asks
toolbox-data.anchore.io whether a newer Grype exists.