Supply-chain PR gate built on Aikido Safe Chain. Blocks a pull request when its dependency install pulls a package Aikido Intel flags as malicious, or one published within the last 48 hours.
Two things live here:
| Path | What it is |
|---|---|
action.yml |
Composite action: installs Safe Chain from a checksum-pinned release, then verifies the shims are live. Drop it into any existing workflow. |
.github/workflows/verify-deps.yml |
Reusable workflow: the complete PR gate. This is what repositories normally call. |
Add .github/workflows/supply-chain.yml to the consuming repository:
name: Supply chain
on:
pull_request: # deliberately unfiltered — see "Why no paths filter"
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
permissions:
contents: read
pull-requests: write
jobs:
# Job id + reusable job name form the required-check string "supply-chain / safe-chain".
supply-chain:
uses: squadrun/safe-chain-action/.github/workflows/verify-deps.yml@<commit-sha>
with:
ecosystem: node
install-command: corepack enable && yarn install --immutable
node-version-file: .nvmrc
dep-paths: |
package.json
yarn.lock
.yarnrc.ymlPin uses: to a commit SHA, not a tag. Floating a mutable tag on a supply-chain control
defeats its own purpose. Let Dependabot bump it.
Safe Chain has no lockfile-scan mode. It installs PATH shims for package managers and runs a local proxy that inspects registry downloads. Four consequences shape everything here:
- The install must really run. No install, nothing scanned. There is no static mode to fall back on.
- Dependency caching must be off. A cache hit means no registry traffic, which means Safe
Chain inspects nothing and the job passes vacuously. The reusable workflow sets
enable-cache: falseand omitssetup-node'scache:key for exactly this reason. If you write your own workflow, do the same. - Safe Chain must be installed after all toolchain setup.
setup-ciappends the shims directory to$GITHUB_PATH, and the runner gives later steps' entries precedence — so asetup-nodeorsetup-uvstep placed after Safe Chain shadows the shims and its installs bypass scanning.action.ymldetects this and fails with an explicit message. $GITHUB_PATHonly applies to later steps, so install and verification are separate steps by necessity.
Points 2–4 are why this wrapper exists rather than a copied curl snippet. Upstream's installer
warns instead of failing when setup-ci breaks, so the natural failure mode is a green check
that protects nothing. action.yml turns each of those warnings into a hard failure, and
self-test.yml asserts on negatives — the canary must be blocked, each guard must fire — because
a passing install cannot tell working protection apart from no protection.
The gate is meant to be a required status check. A workflow filtered with paths: never
reports a status on PRs that don't match, and a required check that never reports blocks the
merge forever. So the trigger stays unfiltered and the job early-exits green in ~15s when
dep-paths didn't change. Same CI savings, no deadlock.
Glob matching for dep-paths uses shell case patterns, where * also crosses /. That
over-matches slightly — a spurious scan is much cheaper than a missed one.
Safe Chain inspects npm-registry and PyPI traffic only. It is a detection tool, not a sandbox. It does not see:
git+httpsdependencies. Anything installed straight from a git repository is invisible.- URL and S3 tarballs. A dependency pinned to an arbitrary HTTPS artifact is invisible.
- Vendored or committed artifacts.
- Packages not yet known to Aikido Intel. Where install scripts are enabled, such a
package's
postinstallstill executes. Blocking is based on known-bad plus a publication-age heuristic, not on behavioural analysis.
Worth being precise, since the whole point of this repository is not over-claiming:
- npm blocking is directly proven. Aikido publishes an always-flagged canary
(
safe-chain-test); the self-test asserts it is refused and absent from disk. - PyPI blocking is not directly proven. There is no malicious PyPI canary. The self-test proves the weaker, still-essential property: PyPI traffic is genuinely proxied and every package is individually adjudicated. Blocking itself rides on the same proxy and threat feed as the npm path.
- The
minimum-package-age-hoursrule keys off release timestamps from Aikido's feed, not the local clock. You therefore cannot force it to fire on a long-published package by raising the threshold — upstream tests that path with a mock feed server. Treat the 48-hour window as protection against freshly published versions, which is the compromised-maintainer case it is designed for.
Do not read a green check as full supply-chain coverage. Repositories that install from forked git repos or unhashed remote tarballs need hash pinning or vendoring, which is a separate problem from this one.
| Input | Default | Notes |
|---|---|---|
require-shims |
— (required) | Space-separated managers that must be shimmed, e.g. yarn, uv. Missing or shadowed ⇒ the action fails. Other managers on the runner are reported informationally. |
version |
1.5.14 |
Safe Chain release tag. |
installer-sha256 |
pinned | Must be updated together with version; a mismatch fails the build. |
minimum-package-age-hours |
48 |
Blocks very fresh releases. |
age-exclusions |
"" |
Comma-separated packages exempt from the age check. Prefer this over lowering the threshold. |
logging |
verbose |
silent | normal | verbose. |
log-file |
$RUNNER_TEMP/safe-chain.log |
Upload on failure. |
Standalone use, in a workflow of your own:
- uses: actions/setup-node@v7 # toolchain FIRST
with:
node-version-file: .nvmrc
- uses: squadrun/safe-chain-action@<commit-sha>
with:
require-shims: "npm yarn"
- run: yarn install --immutable # now scannedA blocked install fails the job, uploads the Safe Chain log as an artifact, and posts a single sticky PR comment naming the packages.
- Real detection — do not merge. Revert the dependency change and escalate.
- Legitimate brand-new release — add the package to
age-exclusionsin that repository'ssupply-chain.yml. Do not lowerminimum-package-age-hours; the 48-hour window is what catches a compromised-maintainer publish before the ecosystem notices.
- Update
versionandinstaller-sha256inaction.yml, and the matching defaults inverify-deps.yml. - Get the checksum from Aikido's release notes, or:
curl -fsSL <installer-url> | sha256sum. - Open a PR.
self-test.ymlmust stay green, and the canary job must still go red — that job is the regression test for upstream behaviour changes.
- Never rename the
safe-chainjob inverify-deps.yml, or the caller's job id. Together they form the branch-protection check name; renaming either silently detaches the required check and every PR goes green. - The reusable workflow checks this repository out at
job.workflow_sha, so the action can never drift from the reusable workflow version a caller pinned. - A green run whose log shows zero packages inspected should be treated as a failure. It almost always means dependency caching leaked back into the job.