README
ΒΆ
β grype_me
An easy to use GitHub Action to scan the supply chain of your project for known vulnerabilities using Anchore Grype and generate badges with detailed reports.
Quick Start
- uses: actions/checkout@v4
with: { fetch-depth: 0, fetch-tags: true }
- uses: TomTonic/grype_me@v1
with:
scan: 'latest_release'
description: |
Nightly vulnerability scan of latest stable release.
Includes application dependencies from source manifests.
fail-build: false
gist-token: ${{ secrets.GIST_TOKEN }}
gist-id: ${{ vars.GRYPE_BADGE_GIST_ID }}
This scans your latest release, uploads a shields.io badge JSON and a detailed Markdown report to a GitHub Gist, and makes the badge URL available as a step output. Click a badge above to see a live example report.
For a full example see how this project runs a daily scan to update the two badges in this README.
Note: The default scan mode is
latest_release, which scans your highest semver tag. If your repo has no tags yet, usescan: 'head'instead.
Note: Due to automated daily updates of this action, pinning its version may yield unexpected behavior. See Daily tag updates.
Features
- π Uses the latest Grype version with a daily-updated vulnerability database (bundled in the action image)
- β‘ ~2Γ faster than installing Grype during a workflow run (no ~200 MB DB download)
- π¦ Multiple scan targets: repositories, container images, directories, or SBOMs
- π― Latest release scanning: Ideal for nightly scans of your published releases
- π Detailed vulnerability counts by severity (Critical, High, Medium, Low)
- π¨ Fail builds on vulnerabilities at or above a configurable threshold
- π§ Option to show only vulnerabilities with available fixes
- π·οΈ Dynamic badge generation with linked Markdown reportsβno extra action needed
How It Works
This action runs Grype with a pre-downloaded vulnerability database inside a Docker container. It supports two modes:
| Mode | Input | Description |
|---|---|---|
| Repository | scan |
Scans source code via dependency manifests (go.mod, package.json, requirements.txt, etc.) |
| Artifact | image / path / sbom |
Scans container images, directories, or SBOM files |
Repository mode
Grype reads dependency manifests directly from the repoβno build required. This works especially well for Go projects.
- β Detects source-declared dependencies without compiling
- β Great for nightly scans of tagged releases
- β Runtime-only or dynamically downloaded dependencies require artifact mode
Scan modes:
latest_releaseβ Scans your highest stable semver tag (default)headβ Scans the current working directory<tag/branch>β Scans a specific ref
Artifact mode
Use image, path, or sbom to scan build artifacts. These inputs are mutually exclusive with scan.
Usage
Nightly Release Scan with Badge
See .github/workflows/security-badge.yml for the workflow that generates the badges shown in this README. Here's the essential pattern:
name: Security Badge
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
update-badge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0, fetch-tags: true }
- uses: TomTonic/grype_me@v1
with:
scan: 'latest_release'
description: |
Nightly release scan for the public badge report.
Scans the latest semver tag in this repository.
fail-build: false
gist-token: ${{ secrets.GIST_TOKEN }}
gist-id: ${{ vars.GRYPE_BADGE_GIST_ID }}
gist-filename: 'my-project'
This writes three files to the gist:
my-project.jsonβ shields.io endpoint badge JSONmy-project.mdβ detailed Markdown report with CVE tablemy-project-grype.jsonβ raw Grype scan output
Container Image Scan
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- uses: TomTonic/grype_me@v1
with:
image: 'myapp:${{ github.sha }}'
image-source: 'registry'
description: |
PR build image scan for commit `${{ github.sha }}`.
Used as release gate for container publishing.
fail-build: true
severity-cutoff: 'high'
image-source: registry pulls directly from a registry and avoids Docker daemon access.
Using the Badge in Your README
After the first workflow run, add the badge to your README:
[](https://gist.github.com/YOUR_USER/YOUR_GIST_ID#file-my-project-md)
The badge links to the rendered gist report (not the raw file view). Clicking it shows the full CVE breakdown.
GitHub gist file anchors are based on rendered DOM IDs (for example, my_file.md β #file-my_file-md; underscores stay underscores).
Setup
- Create a GitHub Gist at gist.github.com with any initial file (e.g.,
init.txtwith content{}). Copy the Gist ID from the URL. - Create a Personal Access Token at GitHub Settings β Developer settings β Personal access tokens with
gistscope. - Add secrets/variables to your repository:
- Secret
GIST_TOKENβ the PAT from step 2 - Variable
GRYPE_BADGE_GIST_IDβ the gist ID from step 1
- Secret
Inputs
Scan Target (mutually exclusive)
| Input | Description | Default |
|---|---|---|
scan |
Repository scan: latest_release, head, or a tag/branch |
latest_release |
image |
Container image to scan (e.g., alpine:latest) |
β |
image-source |
Source for image scans: auto, registry, docker, podman, containerd |
auto |
path |
Directory or file to scan | β |
sbom |
SBOM file (Syft, CycloneDX, SPDX) | β |
Options
| Input | Description | Default |
|---|---|---|
fail-build |
Fail if vulnerabilities β₯ severity-cutoff |
false |
severity-cutoff |
Threshold: negligible, low, medium, high, critical |
medium |
output-file |
Save results to JSON file | β |
only-fixed |
Only report vulnerabilities with fixes available | false |
db-update |
Update DB before scanning (see Performance) | false |
strict-privilege-drop |
Fail instead of root fallback if GITHUB_OUTPUT cannot be pre-opened before UID/GID drop |
false |
description |
Optional free text (supports Markdown/line breaks) copied verbatim into report .md under Description: |
β |
Gist Integration
| Input | Description | Default |
|---|---|---|
gist-token |
GitHub PAT with gist scope (store as secret) |
β |
gist-id |
ID of the gist to update | β |
gist-filename |
Base filename for gist files (e.g., my-project) |
auto from scan mode |
Advanced inputs
| Input | Description | Default |
|---|---|---|
debug |
Print environment variables (may expose secrets) | false |
Outputs
| Output | Description |
|---|---|
cve-count |
Total vulnerabilities found |
critical / high / medium / low |
Count per severity |
grype-version |
Grype version used |
db-version |
Vulnerability database version |
json-output |
Path to output file (if output-file set) |
badge-url |
shields.io badge URL (dynamic endpoint when gist configured, static otherwise) |
report-url |
URL to the rendered gist report section (gist.github.com/...#file-...; underscores are preserved) |
runtime-privilege |
Effective privilege mode: already-non-root, dropped, or root-fallback |
runtime-privilege-detail |
Diagnostic reason for fallback/strict failures when privilege drop cannot be honored |
Privilege drop troubleshooting
The container starts as root and pre-opens GITHUB_OUTPUT before dropping to UID 10001. The inherited file descriptor remains valid after setuid/setgid (standard Unix behavior), so step outputs can be written without modifying mount ownership. If the pre-open fails, the action either:
- falls back to root (default,
strict-privilege-drop: false) and reportsruntime-privilege=root-fallback - fails fast (
strict-privilege-drop: true)
Use the runtime-privilege and runtime-privilege-detail outputs plus warning logs to detect this condition.
Performance
The action image is rebuilt daily with the latest Grype and vulnerability database. This eliminates the ~200 MB database download, making scans roughly 2Γ faster than running Grype manually in a GitHub Actions workflow.
| Scenario | Recommendation |
|---|---|
| Nightly scans | Use pre-baked DB (default) β fast and fresh enough |
| Security gates before release | Consider db-update: true for absolute freshness |
- uses: TomTonic/grype_me@v1
with:
scan: 'latest_release'
db-update: true # Download latest DB before scanning
Daily tag updates
The published container image is rebuilt daily to always contain the newest Grype release and the latest vulnerability database. As a result, moving tags are shifted to the new image every day: latest, v1, v1.2, and v1.2.3. By design, the patch level only refers to the patch level of this action, not including the vulnerability database.
Only the following tags remain immutable and stable:
v1.2.3-releasev1.2.3_grype-0.xyz.0_db-YYYY-MM-DDThh-mm-ssZ
This behavior is intentional but can be surprising if you try to pin to a patch level tag in CI or other automation. If you require an unchanging image, pin to one of the immutable tags (for example the db-specific ..._grype-..._db-... tag or the -release tag).
Badge
The action generates a dynamic shields.io badge that shows vulnerability counts with color-coding:
| Color | Meaning |
|---|---|
| No vulnerabilities | |
| Low severity only | |
| Medium severity | |
| High severity | |
| Critical severity |
When gist integration is configured, the badge is a shields.io endpoint badge that updates automatically. Clicking the badge opens the detailed Markdown report showing every CVE with package, version, fix status, and description.
Without gist integration, the badge-url output contains a static shields.io URL that can be displayed in workflow summaries:
- uses: TomTonic/grype_me@v1
id: grype
with: { scan: 'latest_release' }
- run: |
echo "" >> $GITHUB_STEP_SUMMARY
Alerting Examples
Create GitHub Issue
- uses: TomTonic/grype_me@v1
id: grype
with: { scan: 'latest_release' }
- if: steps.grype.outputs.critical > 0
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'π¨ Critical vulnerabilities detected',
body: `Found ${{ steps.grype.outputs.critical }} critical CVEs.\n\n[View report](${{ steps.grype.outputs.report-url }})`,
labels: ['security', 'critical']
});
Slack Notification
- uses: TomTonic/grype_me@v1
id: grype
with: { scan: 'latest_release' }
- if: steps.grype.outputs.cve-count > 0
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "π Scan: ${{ steps.grype.outputs.critical }} critical, ${{ steps.grype.outputs.high }} high CVEs"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
License
BSD 3-Clause License β see LICENSE.