Zum Inhalt springen

Open source / Guard/Tripwire / sbom-diff-sentinel

sbom-diff-sentinel

SBOM diff gate: compares CycloneDX 1.4-1.6 / SPDX 2.3 SBOMs or git refs, reports added, removed, upgraded and downgraded components plus license, supplier and hash changes, checks OSV.dev for new vs fixed vulns, and enforces policy. Text/JSON/SARIF/Markdown, GitHub Action.

Auf npm
$ npm install sbom-diff-sentinel

README

sbom-diff-sentinel

npm version CI Node License: MIT

See exactly what a pull request changes in your software bill of materials, and block the risky changes.

sbom-diff-sentinel compares two SBOMs, or your project at a git ref against the working tree. It reports components that were added, removed, upgraded or downgraded, plus license, supplier and hash changes. It checks the added and changed components against OSV.dev and splits the results into new and fixed vulnerabilities, each with a severity. Then it applies a policy: fail on new vulnerabilities at or above a severity, downgrades, tampered hashes or oversized changes. Licence changes are reported (with license-sentinel categories) but not judged: put your licence policy in license-sentinel.

  • Reads CycloneDX 1.4–1.6 JSON and SPDX 2.3 JSON
  • Generates SBOMs from npm, pnpm and yarn lockfiles with license-sentinel
  • Text, JSON, SARIF and Markdown output (the Markdown suits PR comments and job summaries)
  • CLI, library and GitHub Action. Node.js 18+, one zero-dependency runtime dependency

Quick start

npx sbom-diff-sentinel --base origin/main             # project at origin/main vs working tree
npx sbom-diff-sentinel base.cdx.json head.spdx.json   # two SBOM files (formats can be mixed)
npx sbom-diff-sentinel base.cdx.json                  # SBOM file vs the current project
npx sbom-diff-sentinel --generate bom.cdx.json        # write a CycloneDX 1.5 SBOM for the project
SBOM diff: base.cdx.json (8) → head.spdx.json (10)

Added (3)
  + axios@0.21.0 MIT
  + copyleft-lib@1.0.0 AGPL-3.0-only
  + dual@2.0.0 MIT OR GPL-3.0-only

Removed (1)
  - left-pad@1.3.0

Upgraded (1)
  ↑ lodash 4.17.20 → 4.17.21

Downgraded (1)
  ↓ minimist 1.2.8 → 1.2.5

License changes (1)
  chalk: MIT → GPL-3.0-only

Supplier changes (1)
  debug: TJ Holowaychuk → Evil Corp

Hash changes (1)
  ! express@4.18.2

Fixed vulnerabilities (1)
  ✓ GHSA-29mw-wpgm-hmr9 lodash@4.17.20 (medium)

Policy findings (32)
  critical minimist@1.2.5: GHSA-xvch-5gv4-984h (CVE-2021-44906) Prototype Pollution in minimist [new-vulnerability]
           ↳ See https://osv.dev/vulnerability/GHSA-xvch-5gv4-984h for fixed versions
  high     axios@0.21.0: GHSA-cph5-m8f7-6c5x (CVE-2021-3749) axios Inefficient Regular Expression Complexity vulnerability [new-vulnerability]
  …
  high     express@4.18.2: hash changed for the same version [hash-changed]
  medium   debug: supplier "TJ Holowaychuk" → "Evil Corp" [supplier-changed]
  medium   minimist downgraded 1.2.8 → 1.2.5 [downgrade]
  low      chalk: license MIT → GPL-3.0-only [license-changed]

6 change(s): +3 -1 ↑1 ↓1; vulns: 26 new, 1 fixed (checked); 1 critical, 15 high, 14 medium, 2 low

VEX, OSV cache and PR comments

sbom-diff-sentinel --base origin/main --vex security/openvex.json --pr-comment pr.md
sbom-diff-sentinel base.cdx.json head.cdx.json --offline        # cached OSV results only
permissions: { contents: read, pull-requests: write }
steps:
  - uses: actions/checkout@v4
    with: { fetch-depth: 0 }
  - uses: MrLouisWright/sbom-diff-sentinel@v1
    with: { pr-comment: 'true', vex: 'security/openvex.json' }

Licence policy (copyleft and so on) belongs in license-sentinel; sbom-diff-sentinel only reports licence changes.

Module formats and types

Ships ESM and CommonJS (import and require both work, via exports), with TypeScript declarations generated from the source at build time. Tests run on Node 18/20/22 in GitHub Actions (test/), including require/import checks against the packed tarball; releases are published with npm provenance.

Rules

RuleDefault severityWhat it catches
new-vulnerabilitythe advisory's severityAn added or changed component has an OSV advisory that the base didn't have (matched by ID and aliases)
hash-changedhighSame name and version, different hash: a republished or tampered package, or a different mirror
too-many-changeshighMore component changes than --max-changes
osv-errorhighThe OSV lookup failed, so the check can't silently pass. Use --offline to skip it on purpose
downgrademedium (--deny-downgrades raises it to high)A component moved to a lower version
supplier-changedmediumThe supplier or publisher changed
license-changedlowAny license change
allowlist-expiredlowAn allowlist entry is past its expires date
unscanned-componentinfoNo purl or version, so it couldn't be checked on OSV
sbom-warninginfoSpec version outside CycloneDX 1.4–1.6 or SPDX 2.3 (parsed anyway)

Vulnerability severity comes from the CVSS v3 base score (computed from the vector), then the advisory's rating (GHSA MODERATE maps to medium). Advisories without either are rated medium and marked as estimated.

How components are matched

Components are matched by package URL without the version (pkg:npm/lodash, pkg:maven/org.example/core), falling back to the name. Qualifiers such as ?type=jar are ignored. When a package appears in several versions, versions on both sides are unchanged, the rest are paired highest-to-highest as upgrades or downgrades, and any leftovers count as added or removed. Hashes are compared only when both sides share an algorithm.

Options

--base <file|ref>      Base SBOM file, or a git ref to generate it from (e.g. origin/main, HEAD~1, v1.2.0)
--head <file|dir>      Head SBOM file or project directory (default: --project)
-C, --project <dir>    Project directory (default: .); may be a subdirectory of the repo
--include-dev          Include devDependencies when generating
--offline              Skip the OSV.dev lookup
--osv-url <url>        OSV API base URL (default: https://api.osv.dev)
--timeout <ms>         OSV request timeout (default: 15000)
--fail-on <sev>        critical | high | medium | low | info | none (default: high)
--vex <file>           CycloneDX VEX / OpenVEX (repeatable)
--cache-dir <dir>      OSV cache directory; --cache-ttl <s>, --no-cache
--pr-comment <file>    Short PR comment Markdown
--max-changes <n>      Fail when more than n components change
--deny-downgrades      Treat downgrades as high severity
--config <file>        Config file (default: .sbom-diff-sentinel.json or package.json "sbom-diff-sentinel")
--format <fmt>         text | json | sarif | markdown (default: text)
--json/--sarif/--markdown <file>   Also write that report to a file
--generate [file]      Write a CycloneDX 1.5 SBOM for the project (stdout if no file)
-q, --quiet            Only print the summary line

Exit codes: 0 pass, 1 findings at or above --fail-on, 2 usage or runtime error (bad SBOM, unknown git ref and so on).

With --base <ref>, the tool reads package.json and the lockfile at that ref with git show and builds both sides from lockfiles, so they're generated the same way. Your working tree is never checked out or changed.

Config

.sbom-diff-sentinel.json (or a "sbom-diff-sentinel" key in package.json):

{
  "failOn": "high",
  "vex": ["security/vex.json"],
  "maxChanges": 40,
  "rules": { "downgrade": "high", "license-changed": "off" },
  "allowlist": [
    { "id": "CVE-2021-3749", "reason": "server-side only, not reachable", "expires": "2027-01-31" },
    { "package": "@internal/*", "rule": "supplier-changed", "reason": "registry migration" },
    { "package": "chalk", "rule": "license-changed", "reason": "legal approved LEGAL-123" }
  ]
}

An allowlist entry matches a vulnerability ID or alias (id), a package name glob (package) and/or a rule. Matching findings are suppressed and reported as allowlisted. After expires, the finding comes back with a note, and unused expired entries are flagged.

GitHub Action

on: pull_request
permissions:
  contents: read
jobs:
  sbom-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0, persist-credentials: false }
      - uses: MrLouisWright/sbom-diff-sentinel@v1
        id: sbom
        with:
          fail-on: high          # base defaults to origin/<PR base branch>
          max-changes: '50'

The Markdown report is written to the job summary. Its path is in steps.sbom.outputs.markdown-file, so you can post it as a sticky PR comment (it starts with <!-- sbom-diff-sentinel -->). Inputs: base, head, project, fail-on, vex, pr-comment, github-token, max-changes, include-dev, offline, config, sarif, json, job-summary and args.

Library

import { readSbom, generateSbomAtRef, generateSbom, analyze, formatMarkdown, exitCode } from 'sbom-diff-sentinel';

const report = await analyze(generateSbomAtRef('.', 'origin/main'), generateSbom('.'), { maxChanges: 50 });
console.log(report.diff.upgraded, report.vulnerabilities.new);
process.exitCode = exitCode(report);

TypeScript types are included.

Support

If this project is useful to you, you can support its development with a donation: Buy me a coffee. It helps keep these tools maintained, documented and free for everyone.

Credit (optional)

Free to use under MIT, no attribution required. If sbom-diff-sentinel helps you, a link back is appreciated:

<a href="https://louiswright.co.uk">Built with sbom-diff-sentinel by Louis Wright</a>
[![Built with sbom-diff-sentinel by Louis Wright](https://img.shields.io/badge/built%20with-sbom--diff--sentinel-blue)](https://louiswright.co.uk)

License

MIT © Louis Wright (Louis Wright)