Aller au contenu

Open source / Guard/Tripwire / package-tripwire

package-tripwire

Fail CI when dependency changes look suspicious: new install scripts, publisher/maintainer changes, freshly published versions, typosquats, deprecated packages, integrity swaps and git/URL deps. Diffs package-lock, pnpm and yarn lockfiles against a git ref or baseline. Text, JSON, SARIF; GitHub Action included.

Sur npm
$ npm install package-tripwire

README

package-tripwire

CI npm version license: MIT Node

Fail CI when dependency changes look suspicious.

package-tripwire diffs your lockfile against a git ref (e.g. the PR base branch) or a saved baseline, looks up every new or changed package in the npm registry, and fails the build when something looks like a supply-chain attack. It works as a CLI, a library and a GitHub Action, and has zero runtime dependencies.

Supported lockfiles: package-lock.json / npm-shrinkwrap.json (v1–v3), pnpm-lock.yaml (v5–v9), yarn.lock (classic v1 and Berry).

Tests live under test/ and run in GitHub Actions on Node 18, 20 and 22 (including ESM import and CommonJS require against the built bundle).

What it flags

RuleDefaultWhat it catches
install-scripterrorNew preinstall / install / postinstall scripts (or implicit node-gyp), or scripts that changed since the previous version
disallowed-install-scripterrorPackage runs install scripts but is not listed in allowScripts when enforcement is on
integrity-changeerrorSame name@version with a different integrity hash or tarball source, or a lockfile hash that doesn't match the registry
registry-signature-invaliderrorRegistry tarball ECDSA signature does not verify (same rules as npm audit signatures)
unsigned-packagewarnRegistry metadata has no tarball signature while the registry publishes signing keys
provenance-losterrornpm provenance attestation was present on the previous version but missing on the new one
provenance-missingnoteResolved version has no provenance attestation (--require-provenance promotes to a failure)
disallowed-host / disallowed-scheme / name-url-mismatcherrorlockfile-lint style tarball URL policy (opt-in via lockfileLint)
typosquaterrorNew names close to popular packages (edit distance, -/_/. confusion, homoglyphs like rn→m, 0→o, scoped copies, -js/node- affixes)
publisher-changeerrorThe new version was published by a different npm user than the previous one
recent-publisherrorThe resolved version was published less than --min-age ago (default 72h)
non-registry-sourcewarnGit, GitHub, HTTP tarball or file: dependencies
maintainer-changewarnThe maintainer list changed between versions
deprecatedwarnThe resolved version is deprecated
unknown-package / registry-errorwarnNot in the registry, or the lookup failed
new-dependencynoteEvery newly added package (direct or transitive)

Diff rules only query the registry for added/changed packages, so runs stay fast. When signature verification is enabled, every registry package in the lockfile is checked against /-/npm/v1/keys. --offline skips the registry and keeps lockfile-only checks (integrity, typosquat, git/URL, hasInstallScript, optional lockfileLint).

Install

npm install --save-dev package-tripwire
# or run it once
npx package-tripwire --base origin/main

Requires Node.js 18 or later.

CLI

package-tripwire                          # compare working-tree lockfile against HEAD
package-tripwire --base origin/main       # compare against a branch / tag / commit
package-tripwire --save-baseline .tripwire/baseline.json   # snapshot the current lockfile
package-tripwire --baseline .tripwire/baseline.json         # compare against the snapshot
package-tripwire --min-age 7d --fail-on warn --sarif tripwire.sarif --json tripwire.json
OptionDescription
--base <ref>Git ref to compare against (default HEAD)
--baseline <file>Compare against a saved baseline (or any lockfile) instead of git
--save-baseline <file>Write a baseline snapshot of the current lockfile and exit
--lockfile <path>Lockfile to check (auto-detected by default)
--min-age <dur>Minimum publish age: 90m, 72h, 3d, 1w, 0
--offlineNo registry lookups
--registry <url>Registry URL (default https://registry.npmjs.org)
--no-devIgnore dev dependencies
--no-signaturesSkip registry tarball signature verification
--require-provenanceTreat missing provenance attestations as findings
--enforce-allow-scriptsFail when install scripts run outside allowScripts
--write-allow-scripts <file>Write the configured allowScripts list to a JSON policy file
--config <file>Config file path
--fail-on <level>error (default), warn, note or none
--json [file]JSON report to a file, or stdout
--sarif <file>SARIF 2.1.0 report for GitHub code scanning
--markdown <file>Markdown report for PR comments and job summaries
--quiet, --no-colorOutput tweaks

Exit codes: 0 passed · 1 findings at or above --fail-on · 2 usage or runtime error.

Sample output

package-tripwire — 2 → 6 packages (4 added, 1 updated, 0 removed)
  ERROR [install-script] esbuild@0.24.0 runs install scripts → postinstall: node install.js
  ERROR [typosquat] expresss looks like a typosquat: edit distance 1 from "express"
  WARN  [maintainer-change] debug@4.4.3 maintainers changed -thebigredgeek,-tjholowaychuk
  WARN  [unknown-package] expresss@0.0.1 not found in registry
  WARN  [non-registry-source] my-fork@1.0.0 is installed from a git source: git+ssh://git@github.com/someone/my-fork.git#3f2c1a9
  WARN  [deprecated] request@2.88.2 is deprecated: request has been deprecated, see https://github.com/request/request/issues/3142
  NOTE  [new-dependency] esbuild@0.24.0 added
  ...

2 error(s), 4 warning(s), 4 note(s).
✖ Failing (fail-on: error).

Configuration and allowlist

Put the config in .package-tripwire.json (or .package-tripwirerc.json, or a "package-tripwire" key in package.json):

{
  "minAge": "72h",
  "failOn": "error",
  "includeDev": true,
  "registry": "https://registry.npmjs.org",
  "rules": { "new-dependency": "off", "deprecated": "error" },
  "popular": ["my-company-ui"],
  "allow": [
    "@my-company/*",
    "esbuild@0.24.0",
    { "package": "sharp", "rules": ["install-script"], "reason": "prebuilt binaries" },
    { "package": "left-pad", "version": "1.3.0", "rules": ["recent-publish"], "expires": "2026-12-31" }
  ],
  "allowScripts": ["esbuild", "sharp", "@my-org/*"],
  "enforceAllowScripts": true,
  "verifySignatures": true,
  "requireProvenance": false,
  "lockfileLint": {
    "allowedHosts": ["registry.npmjs.org", "registry.yarnpkg.com", "npm.pkg.github.com"],
    "allowedSchemes": ["https:"],
    "validatePackageNames": true
  }
}

Wire install-script enforcement in CI the same way as @lavamoat/allow-scripts: keep allowScripts in config, turn on enforceAllowScripts, and optionally commit the policy with --write-allow-scripts .package-tripwire-allow-scripts.json.

  • rules sets each rule's level to error, warn, note or off.
  • allow entries can be name, name@version or a glob like @scope/*, or an object with package, version, rules, reason and expires. An expired entry stops matching, so exceptions don't stay forever.
  • popular adds your own names to the list used for typosquat checks.

Allowlisted findings are counted in the summary and listed under suppressed in the JSON.

GitHub Action

name: Dependency tripwire
on: pull_request
permissions:
  contents: read
  security-events: write   # only needed for SARIF upload
jobs:
  tripwire:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: MrLouisWright/package-tripwire@v1
        with:
          min-age: 72h
          fail-on: error
          sarif: tripwire.sarif
      - if: always()
        uses: github/codeql-action/upload-sarif@v3
        with: { sarif_file: tripwire.sarif }

On a pull request it compares against origin/<base branch> by default. Otherwise it uses HEAD~1.

InputDefault
basePR base / HEAD~1Git ref to compare against
baselineBaseline file, used instead of a git ref
lockfileautoLockfile path
min-age72hMinimum publish age
fail-onerrorerror / warn / note / none
configConfig file path
offlinefalseSkip registry lookups
sarif, jsonReport paths
job-summarytrueWrite the Markdown report to the job summary
argsExtra CLI arguments

Library

Works with ESM and CommonJS (Node 18+):

import { run, analyze, parseAny, createRegistry, formatSarif, lintLockfile, findDisallowedInstallScripts } from 'package-tripwire';
// const { run } = require('package-tripwire');

const result = await run({ base: 'origin/main', config: { minAge: '3d', enforceAllowScripts: true, allowScripts: ['esbuild'] } });
console.log(result.findings);

TypeScript declarations are generated from source into dist/ when you run npm run build (also run automatically before npm test / publish).

Limitations

  • Typosquat detection is a heuristic built on a list of popular packages. Expect the occasional false positive, and allowlist it.
  • For git, URL and file: dependencies there is no registry metadata, so only the source is flagged.
  • Yarn Berry stores its own checksum format, so the check that compares lockfile integrity with the registry is skipped for Berry. The same-version change check still runs.
  • Signature verification depends on registry signing keys and dist.signatures metadata (private registries must expose the same /-/npm/v1/keys convention as the public npm registry).
  • enforceAllowScripts blocks disallowed packages at scan time; combine with your package manager's lifecycle hooks if you need install-time enforcement like LavaMoat.

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 package-tripwire helps you, a link back is appreciated:

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

License

MIT © Louis Wright (Louis Wright)