Grype Vulnerability Scanner Collector
Scans repository dependencies for known CVEs using Grype, Anchore's open-source vulnerability scanner. Writes normalized findings to .sca for use with the SCA policy. No secrets or vendor accounts required.
grype to your lunar-config.yml:uses: github://earthly/lunar-lib/collectors/grype@v1.0.5
What This Integration Collects
This integration includes 5 collectors that gather metadata from your systems.
auto
Auto-scans the repository filesystem for dependency vulnerabilities using Grype. Writes normalized findings to .sca with severity counts, CVE IDs, affected packages, and fix versions.
rescan
Re-runs the dependency (SCA) scan on a schedule and overwrites .sca so
the SCA policy re-evaluates against CVEs published since the commit was
first scanned — closing the gap where a branch passes at scan time but a
dependency picks up a new CVE later. Runs the same scan as the auto
collector (the sibling auto.sh) in the Grype collector image, and stamps
.sca.source.integration="cron" (vs "code" for the on-push scan) so it's
clear the data came from a scheduled re-scan.
Opt-in scan history: with scan_history_size > 0 the re-scan first snapshots the current .sca (counts + summary + source, including the scan timestamp) into a bounded .sca.history[] array before overwriting .sca, preserving point-in-time scan results for audit. The SCA policy keeps reading the current .sca and behaves identically whether history is on or off. Default 0 keeps today's overwrite-only behavior — no history, no schema change for existing consumers.
cicd
Detects Grype executions in CI pipelines. Captures the command,
version, and input arguments for audit and compliance tracking.
Routes results by scan target: an image scan (grype <image-ref>) is
normalized to .container_scan, while a directory or SBOM scan
(grype dir:., grype sbom:...) stays in .sca — so a container scan
already running in your pipeline is collected automatically with no
extra configuration.
container-scan
Automatically scans every container image this component published, as soon as they're detected. Via the after-json hook, resolves the pushed images from the docker collector's record (.containers.native.docker.cicd.cmds[]) and scans each with Grype into .container_scan (per-image detail in .container_scan.images[] and .findings[].image). Each scan is stamped with collected_at, the commit it ran at (collected_sha) and the resolved registry digest of every image scanned, so a consumer can tell a scan of the artifact it promotes from a scan of whatever the floating tag pointed at that day.
container-rescan
Re-scans every shipped container image on a schedule. Resolves the images
from what the component actually pushed — each docker push (or --push
build) in the docker collector's recorded commands
(.containers.native.docker.cicd.cmds[]), read via lunar component get-json. That's already-persisted data, so no collector dependency is
required. Pulls each image, scans it with Grype, and overwrites
.container_scan so shipped images are re-evaluated against CVEs published
after they were built. Override with the container_image input. No code
clone needed; runs in the Grype collector image, so the baked-in
vulnerability DB is reused.
Same provenance as container-scan (collected_at, collected_sha, per-image digest). With container_scan_history_size > 0 the re-scan also snapshots the current .container_scan into a bounded .container_scan.history[] before overwriting it, so the scan a release shipped with stays readable.
How Collectors Fit into Lunar
Lunar watches your code and CI/CD systems to collect SDLC data from config files, test results, IaC, deployment configurations, security scans, and more.
Collectors are the automatic data-gathering layer. They extract structured metadata from your repositories and pipelines, feeding it into Lunar's centralized database where guardrails evaluate it to enforce your engineering standards.
Learn How Lunar Works →Example Collected Data
This collector writes structured metadata to the Component JSON. Here's an example of the data it produces:
{
"sca": {
"source": {
"tool": "grype",
"version": "0.74.0",
"integration": "cron",
"collected_at": "2026-07-08T02:00:07Z"
},
"vulnerabilities": {
"critical": 0,
"high": 2,
"medium": 5,
"low": 3,
"total": 10
},
"findings": [
{
"severity": "high",
"package": "golang.org/x/net",
"version": "0.7.0",
"ecosystem": "go-module",
"cve": "CVE-2023-44487",
"title": "HTTP/2 Rapid Reset Attack",
"fix_version": "0.17.0",
"fixable": true
}
],
"summary": {
"has_critical": false,
"has_high": true,
"all_fixable": true
},
"history": [
{
"source": {"tool": "grype", "version": "0.74.0", "integration": "code", "collected_at": "2026-07-01T02:00:11Z"},
"vulnerabilities": {"critical": 0, "high": 1, "medium": 5, "low": 3, "total": 9},
"summary": {"has_critical": false, "has_high": true, "all_fixable": true}
},
{
"source": {"tool": "grype", "version": "0.74.0", "integration": "cron", "collected_at": "2026-07-05T02:00:09Z"},
"vulnerabilities": {"critical": 0, "high": 2, "medium": 5, "low": 3, "total": 10},
"summary": {"has_critical": false, "has_high": true, "all_fixable": true}
}
],
"rescan_count": 2,
"native": {
"grype": {
"cicd": {
"cmds": [
{"cmd": "grype dir:. -o json", "version": "0.74.0"}
]
},
"matches": [
{
"vulnerability": {
"id": "CVE-2023-44487",
"severity": "High",
"fix": {"versions": ["0.17.0"], "state": "fixed"},
"dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2023-44487",
"cvss": [{"metrics": {"baseScore": 7.5}}]
},
"artifact": {
"name": "golang.org/x/net",
"version": "0.7.0",
"type": "go-module",
"language": "go",
"purl": "pkg:golang/golang.org/x/net@0.7.0"
}
}
]
}
}
},
"container_scan": {
"source": {
"tool": "grype",
"version": "0.74.0",
"integration": "cron",
"collected_at": "2026-07-08T03:00:12Z",
"collected_sha": "7030ba7c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a"
},
"image": "myregistry.io/app:v1.2.3",
"digest": "sha256:de0eb0b3f2a47ba1eb89389859a9bd88b28e82f5826b6969ad604979713c2d4f",
"images": [
{
"image": "myregistry.io/app:v1.2.3",
"tool": "grype",
"digest": "sha256:de0eb0b3f2a47ba1eb89389859a9bd88b28e82f5826b6969ad604979713c2d4f",
"os": {"family": "alpine", "version": "3.19"},
"vulnerabilities": {"critical": 0, "high": 1, "medium": 4, "low": 7, "total": 12},
"summary": {"has_critical": false, "has_high": true, "all_fixable": true}
}
],
"history": [
{
"source": {"tool": "grype", "integration": "after-json", "collected_at": "2026-07-01T14:22:03Z", "collected_sha": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"},
"image": "myregistry.io/app:v1.2.3",
"digest": "sha256:9f2c5b1a7d3e4f60889ab0cc12de34f5678901234567890abcdef1234567890a",
"vulnerabilities": {"critical": 1, "high": 3, "medium": 4, "low": 7, "total": 15},
"summary": {"has_critical": true, "has_high": true, "all_fixable": false}
}
],
"os": {
"family": "alpine",
"version": "3.19"
},
"vulnerabilities": {
"critical": 0,
"high": 1,
"medium": 4,
"low": 7,
"total": 12
},
"findings": [
{
"severity": "high",
"package": "libssl3",
"version": "3.1.4-r5",
"ecosystem": "apk",
"cve": "CVE-2024-2511",
"title": "OpenSSL unbounded memory growth in TLSv1.3 session handling",
"fix_version": "3.1.4-r6",
"fixable": true
}
],
"summary": {
"has_critical": false,
"has_high": true,
"all_fixable": true
},
"native": {
"grype": {
"matches": [
{
"vulnerability": {
"id": "CVE-2024-2511",
"severity": "High",
"fix": {"versions": ["3.1.4-r6"], "state": "fixed"}
},
"artifact": {
"name": "libssl3",
"version": "3.1.4-r5",
"type": "apk",
"purl": "pkg:apk/alpine/libssl3@3.1.4-r5"
}
}
]
}
}
}
}
Configuration
Configure this collector in your lunar-config.yml.
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
scan_history_size
|
Optional |
0
|
Maximum number of prior SCA scans to retain in .sca.history[] on each cron re-scan. Snapshots are ordered oldest-first and the current scan stays in the main .sca fields, so the SCA policy is unaffected. Each entry carries its own source (integration + collected_at), so the release-time scan is identifiable as the integration="code" entry — that's .sca.history[0] when history is enabled from the component's first scan (the intended setup for release-time audit). Enabling it later on a component that has already been re-scanned captures the current .sca as the oldest entry, since the release-time scan was already overwritten. 0 (default) disables history entirely — .sca is overwritten each run exactly as today and no .sca.history is written, so existing consumers see no change. When the cap is reached the oldest entry (index 0) is preserved and the second-oldest is dropped. Only the rescan (cron) sub-collector maintains history; the on-push auto scan ignores this. |
max_rescans
|
Optional |
0
|
Stop re-scanning a component after this many scheduled re-scans; 0 (default) means unlimited. Counted independently via .sca.rescan_count (a monotonic tally of completed re-scans), so it stands alone and does not depend on scan_history_size — scan_history_size bounds how many scans you KEEP, max_rescans bounds how many you RUN. Once the limit is reached the cron skips the component, keeping the last scan and its history. |
db_auto_update
|
Optional |
false
|
Download the latest Grype vulnerability database at scan time so scans pick up CVEs published since the collector image was built. Disabled by default for now: enabling it fetches a runtime DB that decompresses to ~1.7GB, which needs the size: large profile the auto/rescan collectors declare — and that only helps on a Hub that honors size (ENG-983), so it's kept off until size-aware Hub builds ship everywhere. Set to "true" on a size-aware Hub to scan the fresh DB; left "false" it scans the lighter image-baked DB, whose freshness tracks the image rebuild cadence. |
container_image
|
Required | — | Pin the image(s) the container scans (container-scan + container-rescan) scan: one ref or a comma-separated list, e.g. "ghcr.io/acme/app:latest,ghcr.io/acme/worker:latest". Default: every image recorded as pushed in .containers.native.docker.cicd.cmds[] (by the docker collector), scanned in push order. |
container_scan_history_size
|
Optional |
0
|
Maximum number of prior container scans to keep in .container_scan.history[] on each cron re-scan. Each entry is {source, image, digest, vulnerabilities, summary} — dated and digest-pinned, so a release-time scan stays readable after later re-scans overwrite the live counts. findings[]/images[]/native are omitted: the hub concatenates those across collectors, so snapshotting them would double them. At the cap the oldest entry is kept and the second-oldest dropped. 0 (default) writes no history. Only the cron container-rescan maintains it — the on-push container-scan record is concatenated rather than replaced, so a history[] on it would grow on every push. |
Secrets
This collector requires the following secrets to be configured in Lunar:
| Secret | Description |
|---|---|
REGISTRY_USERNAME
|
Username for pulling private container images in the container-rescan cron. Optional — public images need no auth. REGISTRY_USER is accepted as an alias. |
REGISTRY_USER
|
Alias for REGISTRY_USERNAME (common registry/CI convention). Optional; used only when REGISTRY_USERNAME is unset. |
REGISTRY_PASSWORD
|
Password or token for pulling private container images in the container-rescan cron. Optional. |
Documentation
View on GitHubGrype Vulnerability Scanner Collector
Scans source code dependencies for known vulnerabilities using Grype.
Overview
This collector runs Grype — Anchore's open-source vulnerability scanner — against the repository to detect known CVEs in dependencies. It supports the ecosystems Grype covers (Go, Node.js, Python, Java, Rust, Ruby, PHP, .NET, and more) and writes normalized vulnerability data to .sca in the Component JSON, making results immediately consumable by the existing SCA policy. No secrets or vendor accounts are required. By default it scans against the vulnerability database baked into the collector image, so freshness tracks the image rebuild cadence; set db_auto_update: true — on a Hub that honors the size: large profile these collectors declare — to fetch the latest database at scan time and pick up CVEs published since the image was built.
Collected Data
This collector writes to the following Component JSON paths:
| Path | Type | Description |
|---|---|---|
.sca.source |
object | Source metadata (tool name, version, integration method, and collected_at scan timestamp) |
.sca.vulnerabilities |
object | Severity counts (critical, high, medium, low, total) |
.sca.findings[] |
array | Individual vulnerability findings with CVE, package, fix info |
.sca.summary |
object | Summary booleans (has_critical, has_high, all_fixable) |
.sca.history[] |
array | (opt-in) Bounded list of prior scan snapshots (source, counts, summary) for point-in-time audit; oldest first. [0] is the oldest retained scan — the release-time (integration="code") scan when history is enabled from the first scan. Absent unless scan_history_size > 0 |
.sca.rescan_count |
number | (opt-in) Monotonic tally of completed re-scans, used to enforce max_rescans independently of the (capped) .sca.history[] length. Present when scan history or max_rescans is enabled |
.sca.native.grype |
object | Raw Grype match output and CI command detection data |
.container_scan.source |
object | Source metadata for the container image scan: tool, version, integration, collected_at (when the scan ran) and collected_sha (the commit the run was bound to) |
.container_scan.image |
string | The primary (most recently pushed) scanned image reference (e.g. registry/app:tag) |
.container_scan.digest |
string | Registry digest of the primary image, resolved at scan time. Absent when the registry reports none |
.container_scan.vulnerabilities |
object | Severity counts across every scanned image (critical, high, medium, low, total) |
.container_scan.findings[] |
array | Individual image findings (OS and application packages), each with the image it was found in |
.container_scan.images[] |
array | Per-image breakdown: image, digest, tool, os, vulnerabilities, summary |
.container_scan.errors[] |
array | Pushed images that could not be pulled or scanned (image, error) |
.container_scan.os |
object | Detected base-image OS family and version of the primary image |
.container_scan.summary |
object | Summary booleans across every scanned image (has_critical, has_high, all_fixable) |
.container_scan.history[] |
array | (opt-in) Bounded list of prior container scans (source, image, digest, counts, summary), oldest first. Absent unless container_scan_history_size > 0 |
.container_scan.native.grype |
object | Raw Grype match output for the primary image |
Collectors
This integration provides the following collectors (use include to select a subset):
| Collector | Hook Type | Description |
|---|---|---|
auto |
code | Auto-scans the repository filesystem for dependency vulnerabilities → .sca |
cicd |
ci-after-command | Detects Grype executions in CI; routes image scans (grype <image>) to .container_scan and dir/SBOM scans to .sca |
rescan |
cron | Re-runs the auto scan on a schedule (daily by default) and overwrites .sca so the SCA policy re-evaluates against newly-published CVEs; optionally snapshots prior scans into .sca.history[] (opt-in via scan_history_size — see Scan history) |
container-scan |
after-json | Automatically scans every image the docker collector records as pushed (.containers.native.docker.cicd.cmds[]) as soon as they're published; writes .container_scan |
container-rescan |
cron | Re-scans every pushed image on a schedule, catching CVEs disclosed since they were built; optionally snapshots prior scans into .container_scan.history[] (opt-in via container_scan_history_size) |
Installation
Add to your lunar-config.yml:
collectors:
- uses: github://earthly/lunar-lib/collectors/grype@main
on: ["domain:your-domain"]
Zero configuration required. Works with any language Grype supports.
By default this also enables the rescan cron sub-collector, which re-runs the
scan daily on each component's default branch and overwrites .sca. To keep the
on-push (auto) and CI-detection (cicd) scans but turn the scheduled re-scan
off, exclude it:
collectors:
- uses: github://earthly/lunar-lib/collectors/grype@main
on: ["domain:your-domain"]
exclude: [rescan]
Scan history (point-in-time audit)
By default the rescan cron overwrites .sca each run — the SCA policy
always sees the latest scan and the previous result is discarded. Because an
unchanged artifact accrues newly-disclosed CVEs over time, you may want to
keep each scan as a point-in-time record (e.g. "here's what the scan looked
like when we shipped this release"). Opt in by keeping a bounded history:
collectors:
- uses: github://earthly/lunar-lib/collectors/grype@main
on: ["domain:your-domain"]
with:
scan_history_size: "30" # keep up to 30 prior scans in .sca.history[]
max_rescans: "0" # 0 = keep re-scanning forever (default)
With scan_history_size > 0, each re-scan snapshots the current .sca
(source + counts + summary, including the collected_at timestamp) into
.sca.history[] before overwriting .sca:
.sca(unchanged) — the current scan the SCA policy evaluates..sca.history[0]— the oldest retained scan, preserved even once the cap is reached. When history is enabled from the component's first scan (the intended setup) this is the release-time scan; each entry carries its ownsource(integration+collected_at), so the release-time scan is theintegration: "code"entry — not an assumption about position..sca.history[1..]— successive prior re-scans, oldest first.
Enabling history on an already-scanned component: if a component has already been re-scanning (overwriting
.sca) before you turn onscan_history_size, its release-time scan is already gone, so.sca.history[0]will be the current.scaat enable time (anintegration: "cron"entry), not the originalcodescan. For guaranteed release-time capture, enable history from the component's first scan.
The SCA policy never reads .sca.history, so enabling this changes nothing for
policy evaluation — .sca behaves identically whether history is on or off.
| Input | Default | Effect |
|---|---|---|
scan_history_size |
0 |
Max entries kept in .sca.history[]. 0 disables history (today's overwrite-only behavior). At the cap the oldest entry ([0]) is kept and the second-oldest is dropped. |
max_rescans |
0 |
Stop re-scanning a component after this many re-scans (0 = unlimited). Counted independently via .sca.rescan_count, so it stands alone — no dependency on scan_history_size. |
container_scan_history_size |
0 |
Max entries kept in .container_scan.history[] by the container-rescan cron. 0 disables it. Independent of scan_history_size, which covers .sca. |
Both default to off, so existing installs are unchanged. Only the rescan cron
maintains history — the on-push auto scan ignores these inputs.
Container image scanning. Beyond source dependencies, this collector scans built container images into the normalized .container_scan path (consumed by the container-scan policy). Three sub-collectors feed it, none installing Grype (or its ~1.7GB DB) in your pipeline:
cicd(detect) — if your pipeline already runsgrype <image>itself, that scan is captured to.container_scanautomatically. Agrype dir:/sbom:scan still routes to.sca. No install, no extra config.container-scan(on-push) — automatically scans every image as soon as it's published. Hooksafter-jsonon.containers.native.docker.cicd.cmds, so a scan fires the moment the docker collector records a push — no schedule lag. A pipeline that pushes several images (an app and its migrator, say) gets them all scanned into one.container_scan:image/osdescribe the most recently pushed one,images[]breaks the counts down per image and everyfindings[]entry names itsimage. On a pull request it scans the images that commit pushed, so PR-time scanning needs your pipeline to push on PR builds; otherwise there is nothing to scan until the merge. Because the scan is dispatched the instant the push is recorded, it can briefly run ahead of that record becoming readable; it retries for a few minutes rather than reporting "nothing was pushed", and fails the run if the record never arrives — the on-push scan fires once per commit, so a silent skip would leave that commit unscanned.container-rescan(scheduled re-scan) — a daily cron that re-scans those images, catching CVEs disclosed after they were built.
All three scan the shipped image itself in the Grype collector image (DB baked in) — no install on your side. Enable the docker collector alongside this one so its docker push commands are recorded:
collectors:
- uses: github://earthly/lunar-lib/collectors/docker@main
on: ["domain:your-domain"]
- uses: github://earthly/lunar-lib/collectors/grype@main
on: ["domain:your-domain"]
container-scanneeds a Hub with theafter-jsonhook. Without it,exclude: [container-scan]and rely on thecontainer-rescancron (the other sub-collectors work on any Hub).
Scan provenance. A container scan records what it scanned and when: source.collected_at, source.collected_sha (the commit the run was bound to) and the registry digest of each image, resolved from the scanner's own output. The digest is the field that matters — the recorded reference is usually a floating tag like registry/app:main, so counts alone cannot tell you whether a scan covers the artifact you are about to promote. Compare digest against the digest you are promoting; if they differ, the numbers describe different bytes. A digest the registry does not report is left absent rather than guessed.
collected_sha is provenance, not identity: on a cron re-scan it is the latest ingested default-branch commit, which need not be the commit that built the image.
Container scan history. Set container_scan_history_size above 0 and each cron re-scan snapshots the current .container_scan into a bounded .container_scan.history[] before overwriting it, so the scan a release shipped with stays readable after later re-scans replace the live counts:
collectors:
- uses: github://earthly/lunar-lib/collectors/grype@main
on: ["domain:your-domain"]
with:
container_scan_history_size: "30"
Entries hold source, image, digest, vulnerabilities and summary only — findings[], images[] and native are concatenated across collectors by the Hub, so snapshotting them would double them. At the cap the oldest entry is kept and the second-oldest dropped. Only the container-rescan cron maintains history; the on-push container-scan ignores the input. If the re-scan cannot read the current Component JSON it skips that tick rather than write a record without history. Running two scanners' crons on one component concatenates their histories, as it does images[] and findings[] — tell them apart by source.tool.
Private registries: the container-rescan cron pulls the images, so a private registry needs the REGISTRY_USERNAME (or REGISTRY_USER) / REGISTRY_PASSWORD secrets.
Resources: container-scan and container-rescan declare size: large, because they pull and unpack the shipped image inside the collector pod and the default profile's 1Gi ephemeral-storage limit is too small for real-world images. The cicd sub-collector runs natively on your CI runner, so no pod size applies to it.
Note: The
grypecollector writes to the same.scapaths as thesnykandtrivycollectors. Use one SCA scanner per component, not several, or they will overwrite each other's.scadata.
Re-scan freshness: By default (
db_auto_update: false), each cron re-scan uses the DB baked into the collector image, so freshness is tied to the image rebuild cadence — bumping the pinnedgrypecollector version (a newer image ships a newer DB) is what picks up new CVE data. Setdb_auto_update: true(on a Hub that honorssize: large) to have each re-scan fetch the latest vulnerability database instead, so CVEs published since the last scan surface on the next tick.
Open Source
This collector is open source and available on GitHub. Contribute improvements, report issues, or fork it for your own use.
Common Use Cases
Explore guardrails that use data from Grype Vulnerability Scanner Collector.
Ready to Automate Your Standards?
See how Lunar can turn your AGENTS.md, engineering wiki, compliance docs, or postmortem action items into automated guardrails with our 200+ built-in guardrails.