supply-chain-guardrail
v1.0.1
Published
npm supply chain security guardian — detects ghost dependencies, scores install script behavior, catches the axios blind spot, and blocks known malicious packages before they execute
Maintainers
Readme
GuardRail
GuardRail is a zero-dependency npm supply chain security tool that detects attacks before any advisory exists. It was built in response to the March 2026 axios supply chain compromise (GHSA-fw8c-xr5c-95f9), where a trusted package silently added a malicious dependency that ran a postinstall hook to deploy a RAT. No CVE existed at the time of the attack. Traditional audit tools were blind to it. GuardRail catches this class of attack -- and others like it -- by analyzing behavioral signals rather than waiting for someone to file an advisory.
The core insight: no existing tool catches "ghost dependencies" -- packages added to package.json that are never imported in source code. These packages exist solely to run their postinstall hooks. GuardRail cross-references every declared dependency against the actual import graph of the package source, and flags any dependency that is declared but never used. Combined with behavioral scoring of install scripts, baseline drift detection, and IOC matching, GuardRail provides defense-in-depth for any npm project.
Quick Start
npm install -g supply-chain-guardrail
cd your-project
guardrail scan
guardrail ioc list
guardrail audit-tokensHow Detection Works
Ghost Dependency Detection
GuardRail builds an import map by statically analyzing all source files in a package (.js, .ts, .mjs, .cjs, and related extensions). It extracts every require() call and import statement, then cross-references those against the dependencies declared in package.json. If a dependency is declared but never imported by any source file, it is flagged as a ghost dependency. This is the single most reliable signal for postinstall dropper attacks, because the malicious package does not need to be imported -- it only needs to be installed.
Script Behavioral Scoring
Every lifecycle script (preinstall, install, postinstall, prepare, prepublish) across the full dependency tree is analyzed against a weighted multi-rule system. Each rule detects a specific behavioral pattern: network calls, process spawning, file writes, base64/hex payloads, eval usage, self-delete behavior, persistence paths, and shell launchers. Individual rule matches produce a base score, and compound bonuses are added when multiple dangerous patterns appear together (for example, a script that both fetches from a URL and spawns a child process scores higher than either signal alone). The final score is compared against a configurable threshold to determine severity.
Axios Blind Spot (OIDC + Token Coexistence)
The axios attack succeeded in part because the compromised maintainer account still had static npm publish tokens alongside OIDC trusted publishing. GuardRail scans for this exact configuration: if a project uses OIDC trusted publishing but static publish tokens also exist (in .npmrc, environment variables, or CI workflow files), it flags the mixed-mode setup as a critical risk. An attacker who compromises a static token can publish releases that bypass the trusted-publisher pipeline entirely.
Baseline Drift
GuardRail creates an Ed25519-signed snapshot of every package's dependency set, lifecycle scripts, and manifest contents. On subsequent scans, it compares the current state against the signed baseline. Any change -- new dependencies, modified scripts, altered metadata -- is detected and reported. Because the baseline is cryptographically signed, an attacker cannot silently modify it. The --update-baseline flag lets you intentionally advance the baseline after reviewing changes.
Advisory-Independent
GuardRail does not depend on CVE databases or advisory feeds. All detection is based on behavioral analysis and structural comparison. This means GuardRail catches zero-day supply chain attacks the moment they appear in a package release, not days or weeks later when an advisory is published. The built-in IOC database provides an additional layer for known threats, but the core detection engine works without it.
CLI Reference
| Command | Description |
|---------|-------------|
| guardrail scan | Full project scan: ghost dependencies, script scoring, baseline drift, IOC matching |
| guardrail monitor | Watch the npm change feed for new releases of your dependencies |
| guardrail audit-tokens | Find static publish tokens and mixed-mode publishing risk |
| guardrail verify <package@version> | Verify a specific package version against registry metadata and provenance signals |
| guardrail incident <package@version> --from ISO --to ISO | Build an incident checklist and optionally scan GitHub Actions logs |
| guardrail ioc list\|add\|remove\|check | Manage the local IOC (Indicators of Compromise) database |
scan
guardrail scan [options]| Flag | Description |
|------|-------------|
| --root-dir <path> | Project root directory (default: current directory) |
| --fail-fast | Exit with non-zero status on first high-severity finding |
| --threshold <number> | Script risk score threshold (default: from config or 70) |
| --sarif <path> | Write findings in SARIF format for GitHub Code Scanning |
| --update-baseline | Update the signed baseline after scan |
| --generate-workflow | Generate a GitHub Actions workflow file |
| --install-pre-commit | Install a Git pre-commit hook that runs GuardRail |
| --output <path> | Write scan results to a file |
| --json | Output results as JSON |
| --quiet | Suppress non-essential output |
monitor
guardrail monitor [options]| Flag | Description |
|------|-------------|
| --root-dir <path> | Project root directory |
| --interval-ms <ms> | Poll interval in milliseconds |
| --slack-webhook <url> | Slack webhook URL for alerts |
| --webhook <url> | Generic webhook URL for alerts |
| --once | Run one check and exit instead of continuous monitoring |
audit-tokens
guardrail audit-tokens [options]| Flag | Description |
|------|-------------|
| --root-dir <path> | Project root directory |
| --revoke-stale | Suggest revocation of stale tokens |
| --stale-after-days <days> | Number of days after which a token is considered stale |
verify
guardrail verify <package@version> [options]| Flag | Description |
|------|-------------|
| --root-dir <path> | Project root directory |
| --fail-fast | Exit with non-zero status on verification failure |
incident
guardrail incident <package@version> --from <ISO> --to <ISO> [options]| Flag | Description |
|------|-------------|
| --from <ISO> | Start of the incident time window (required) |
| --to <ISO> | End of the incident time window (required) |
| --github-owner <owner> | GitHub repository owner for Actions log scanning |
| --github-repo <repo> | GitHub repository name |
| --github-token <token> | GitHub personal access token |
ioc
guardrail ioc <list|add|remove|check> [options]| Flag | Description |
|------|-------------|
| --reason <text> | Reason for adding an IOC entry |
| --advisory <id> | Advisory identifier (GHSA, CVE) |
IOC Database
GuardRail maintains a database of known malicious packages (Indicators of Compromise). The database has two layers: built-in entries that ship with GuardRail, and custom entries that you manage per-project via guardrail.config.json.
List all IOCs
guardrail ioc listShows both built-in and custom IOC entries with their reasons and advisory links.
Add a custom IOC
guardrail ioc add evil-package --reason "Exfiltrates env vars via postinstall" --advisory "GHSA-xxxx-xxxx-xxxx"Adds a package to the custom IOC list in your project config. Future scans will flag this package if it appears in any dependency tree.
Check a specific package
guardrail ioc check suspicious-packageChecks whether a package name matches any built-in or custom IOC entry.
Remove a custom IOC
guardrail ioc remove evil-packageRemoves a package from the custom IOC list. Built-in IOCs cannot be removed.
Contributing IOCs
The community can contribute new malicious package entries to the built-in IOC database. To submit a new IOC:
- Open a GitHub issue using the "New IOC Submission" template.
- Provide the package name, affected versions, advisory link (GHSA or CVE), and a description of the malicious behavior.
- Alternatively, submit a pull request that adds the entry directly to the
BUILTIN_IOCSarray insrc/commands/ioc.tsand thebuiltinIocsobject insrc/commands/scan.ts.
All submissions require evidence: a published advisory or a reproducible technical analysis demonstrating malicious behavior.
CI/CD Integration
GuardRail integrates into your CI/CD pipeline in three ways:
- GitHub Actions workflow: Run
guardrail scan --generate-workflowto create a.github/workflows/guardrail.ymlfile that runs on every push and pull request. - SARIF output: Run
guardrail scan --sarif guardrail.sarifto produce SARIF output compatible with GitHub Code Scanning. Upload the SARIF file using thegithub/codeql-action/upload-sarifaction to see GuardRail findings directly in the Security tab. - Pre-commit hook: Run
guardrail scan --install-pre-committo install a Git hook that runs GuardRail before every commit, blocking commits that introduce high-severity findings.
Configuration
GuardRail reads guardrail.config.json from the project root. The file supports JSON-with-comments (lines starting with // and /* ... */ blocks are stripped before parsing).
{
"scan": {
"riskThreshold": 70,
"failOnSeverity": "high",
"trustedPackages": ["axios"]
},
"tokenPolicy": {
"staleAfterDays": 30,
"mixedModeAllowed": false
},
"github": {
"owner": "your-org",
"repo": "your-repo",
"tokenEnvVar": "GITHUB_TOKEN"
}
}Key configuration fields:
| Field | Description |
|-------|-------------|
| baseline.directory | Directory for baseline data and signing keys |
| baseline.path | Signed baseline file path |
| baseline.privateKeyPath | Ed25519 private signing key path |
| baseline.publicKeyPath | Ed25519 public verification key path |
| scan.riskThreshold | Script risk score threshold for blocking |
| scan.failOnSeverity | Minimum severity that fails CI in --fail-fast mode |
| scan.trustedPackages | Packages whose mutations are treated as especially sensitive |
| scan.ignoreDirs | Directories to skip when building import graphs |
| scan.maxScriptFileBytes | Maximum script file size to analyze |
| tokenPolicy.staleAfterDays | Days after which a token is considered stale |
| tokenPolicy.mixedModeAllowed | Allow OIDC + static token coexistence |
| monitor.packages | Explicit package watch list |
| monitor.pollIntervalMs | npm change feed poll interval |
| monitor.slackWebhook | Default Slack webhook for alerts |
| monitor.webhook | Generic webhook for alerts |
| github.owner / github.repo / github.tokenEnvVar | Defaults for incident log scanning |
