Blog archive

Scan Dependency CVEs with Syft and Grype in GitHub Actions

Use SBOM-based scanning to identify vulnerable dependencies before running code locally or building images in CI.

Published · Republished on Medium

Software Supply ChainSBOMGitHub ActionsSyftGrype

Picture by Frantzou Fleurine on Unsplash

What this check answers

Syft can inventory packages found in a source directory and emit a CycloneDX or SPDX Software Bill of Materials. Grype can consume that SBOM and match the packages against its vulnerability database.

Placed before dependency installation and image building, this creates an early checkpoint:

text

repository manifests and lockfiles
  -> Syft package inventory
  -> versioned SBOM
  -> Grype vulnerability matches
  -> policy result and retained evidence

The checkpoint is useful, but narrow. It does not prove that dependencies are exploitable, that the repository is safe to execute, or that the final image and runtime are clean.

Demo scope

The example repository contains two Node.js targets:

  • app/ intentionally produces vulnerability findings.
  • app-clean/ provides a cleaner comparison for the same workflow.

Screenshot from How to Scan Dependency CVEs Early with Syft and Grype in GitHub Actions

The vulnerable target is demonstration material. Do not install it on a developer workstation, reuse it as a starter, publish it as an image, or place it on a networked runtime.

Why an SBOM is more useful than package.json alone

A manifest expresses dependency intent. A lockfile records a concrete resolution produced by package tooling. An SBOM normalizes discovered components, versions, package URLs, relationships, and source evidence into a format other tools can consume.

SBOM quality still depends on the source and cataloger. A source-directory scan can identify packages represented by supported manifests, lockfiles, and files, but it cannot inventory operating-system packages or artifacts that will be introduced later by a container base image or build stage.

Do not hand-write a lockfile and call it resolved truth. In a real project, generate and validate lockfiles with the package manager through a separately controlled workflow. For this early scan, review the committed lockfile without running lifecycle scripts.

Source, image, and runtime coverage

Use complementary scan stages:

StageUseful coverageImportant gap
Source directoryDeclared/resolved application packages detectable from repository dataBase image, installed OS packages, generated artifacts
Built image by digestApplication artifacts, base image, OS and language packages actually packagedRuntime mounts, deployed configuration, later mutation
Deployed/runtime inventoryWhat is present in the running environmentMay occur too late to prevent release

The source scan is an early warning, not a substitute for scanning the final immutable image digest.

Pin the scanning toolchain

At the time this article was reviewed, the tested examples use Syft 1.50.0 and Grype 0.116.1. Treat those as explicit example pins. Review release notes and security advisories, then update them through a pull request with regression tests.

GitHub Action major tags such as @v0, @v7, and @v4 are convenient but mutable. For a hardened workflow, replace them with the full reviewed commit SHA from the action release and let Dependabot or Renovate propose SHA updates. Keep the version comment next to the SHA for readability.

A bounded GitHub Actions workflow

yaml

name: sbom-vulnerability-scan

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  scan-source:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4 # Replace with a reviewed full commit SHA.
        with:
          persist-credentials: false

      - name: Install Syft 1.50.0
        id: syft
        uses: anchore/sbom-action/download-syft@v0 # Pin the action SHA.
        with:
          syft-version: 1.50.0

      - name: Generate CycloneDX SBOM
        shell: bash
        run: |
          set -euo pipefail
          "${{ steps.syft.outputs.cmd }}" dir:app \
            --output cyclonedx-json=sbom.cdx.json
          test -s sbom.cdx.json
          jq -e '.bomFormat == "CycloneDX" and (.components | type == "array")' \
            sbom.cdx.json >/dev/null

      - name: Install Grype 0.116.1
        id: grype
        uses: anchore/scan-action/download-grype@v7 # Pin the action SHA.
        with:
          grype-version: 0.116.1
          cache-db: true

      - name: Record vulnerability database status
        shell: bash
        run: |
          set -euo pipefail
          "${{ steps.grype.outputs.cmd }}" db status -o json > grype-db.json
          test -s grype-db.json

      - name: Produce machine-readable findings
        shell: bash
        run: |
          set -euo pipefail
          "${{ steps.grype.outputs.cmd }}" sbom:sbom.cdx.json \
            --output json > grype-findings.json
          test -s grype-findings.json

      - name: Enforce reviewed severity policy
        shell: bash
        run: |
          set -euo pipefail
          "${{ steps.grype.outputs.cmd }}" sbom:sbom.cdx.json \
            --fail-on high

      - name: Upload scan evidence
        if: always()
        uses: actions/upload-artifact@v4 # Replace with a reviewed full commit SHA.
        with:
          name: source-sbom-and-grype-${{ github.run_id }}-${{ github.run_attempt }}
          path: |
            sbom.cdx.json
            grype-db.json
            grype-findings.json
          if-no-files-found: error
          retention-days: 30

The explicit tool versions make the catalog and matcher behavior reviewable. The database status captures which vulnerability data informed the result. if: always() preserves evidence even when the policy gate fails.

The workflow still requires network access to download actions, binaries, and the Grype database. For restricted runners, mirror and verify those artifacts, import an approved database archive, and define how stale data fails the build.

Understand Grype database behavior

Grype uses a local vulnerability database and checks for updates when it runs. Current Anchore documentation says the scan fails when the database is more than five days old by default. Keep age and hash validation enabled.

Decide explicitly what happens when the update service is unavailable:

  • A release gate can fail closed when it cannot check for current vulnerability data.
  • A developer feedback job might continue with a known-age cached database while raising a visible infrastructure warning.
  • An air-gapped environment should import a signed or checksum-verified database through a controlled promotion process.

Never turn a database download failure, invalid database, or missing result file into a clean security result.

Define policy beyond --fail-on high

--fail-on high is a minimum severity gate, not a complete risk decision. The policy should also define:

  • Whether Critical and High findings without a known fix block release.
  • How exploitability, reachability, exposure, and compensating controls affect priority.
  • How end-of-life operating systems or ecosystems are handled.
  • Maximum remediation time by severity.
  • Who can approve an exception and when it expires.

Do not globally enable “only fixed” reporting just to make the pipeline green. That can hide severe findings without vendor fixes. Preserve all findings, then distinguish actionable upgrades from accepted or mitigated risk in policy and reporting.

Triage matches and false positives

A scanner match is evidence to investigate, not proof of exploitability. For each disputed result, retain:

  1. Vulnerability identifier and package coordinates.
  2. Exact package location and catalog evidence.
  3. Vulnerability provider and database build timestamp.
  4. Installed/resolved version and fixed-version claim.
  5. Reachability or runtime evidence where available.
  6. Decision owner, rationale, approval, and expiry.

Use a narrow Grype ignore rule or a VEX document when the conclusion is supportable. Scope exceptions to the specific package, version, vulnerability, product, and time window. Re-evaluate when the component, scanner, or vulnerability data changes.

Protect and retain the SBOM

An SBOM can reveal proprietary package names, versions, internal paths, and components useful to an attacker. Keep GitHub artifact permissions narrow, set an intentional retention period, and avoid publishing SBOMs from private products as public release assets by accident.

For release evidence, associate the SBOM and scan with:

  • source commit and workflow commit;
  • Syft and Grype versions;
  • Grype database build/status;
  • configuration and ignore/VEX revisions;
  • final image digest, when scanning the built artifact;
  • file hashes or attestations for integrity.

Scan the final image separately

After building and pushing to a controlled registry, scan the immutable digest rather than a mutable tag:

bash

syft registry:ghcr.io/example/app@sha256:<digest> \
  -o cyclonedx-json=image-sbom.cdx.json

grype sbom:image-sbom.cdx.json --fail-on high

This stage catches packages inherited from the base image and files introduced by the build. It should use registry credentials with pull-only access and must not print credentials or embed them in artifacts.

Safe validation cases

Test more than one expected vulnerable package:

  1. Known vulnerable target produces the expected match and non-zero gate exit.
  2. Cleaner target completes, while still producing a valid SBOM and report.
  3. Missing or empty SBOM fails.
  4. Invalid or stale vulnerability database fails according to policy.
  5. An expired exception stops suppressing a finding.
  6. Tool/action upgrades reproduce the regression corpus.
  7. Source scan and image scan show the expected coverage difference.
  8. Artifact upload failure is visible and does not silently produce PASS.

A “clean” result means no matches under that scanner, database, configuration, and inventory. It is not evidence that the software has no vulnerabilities.

Repository examples

Official references

Conclusion

Syft and Grype provide a useful early dependency control when the inventory, tools, vulnerability database, policy, and evidence are all versioned. Keep the source scan before installation, retain every result—including infrastructure failures—and complement it with an immutable image scan before release.