Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions .github/workflows/skills-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,31 @@ jobs:
with:
node-version: "20"

# setup-node@v4 with node-version 20 provisions npm 10.8.2, which predates
# min-release-age and silently ignores it — same trap as node:20-bullseye
# in the Dockerfile. Upgrade before installing anything. Pinned so the
# bootstrap itself is deterministic.
- name: Upgrade npm (Node 20 ships npm 10.x, too old for min-release-age)
run: npm install -g npm@11.10.0

# Fail the build if the quarantine window isn't in effect. Without this,
# losing .npmrc — or an npm downgrade like the one above — would silently
# degrade CI to unprotected installs.
- name: Verify npm supply-chain hardening
run: node scripts/check-npm-hardening.js

# `npm ci` installs strictly from package-lock.json. `npm install` would
# re-resolve semver ranges on every run, so a freshly-published malicious
# version could land in CI without any commit to this repo — the exact
# delivery path used by the Shai-Hulud npm worm.
# --ignore-scripts: CI only builds/validates skills, so no native deps are
# needed here, and no dependency gets to execute code next to CI secrets.
- name: Install project dependencies
run: npm install --legacy-peer-deps
run: npm ci --legacy-peer-deps --ignore-scripts

- name: Install skills-ref CLI
run: |
if npm install -g skills-ref 2>/dev/null; then
if npm install -g skills-ref --ignore-scripts 2>/dev/null; then
echo "SKILLS_REF_AVAILABLE=true" >> $GITHUB_ENV
else
echo "SKILLS_REF_AVAILABLE=false" >> $GITHUB_ENV
Expand Down
25 changes: 25 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Supply-chain hardening (Shai-Hulud / Mini Shai-Hulud npm worm defense).
#
# Refuse any package version published less than 7 days ago. The worm's model is
# publish-and-propagate within minutes; researchers flag compromised versions
# within ~24h. A quarantine window means we never resolve a malicious release.
# Requires npm >= 11.10. Value is in DAYS (npm maps it onto the `before` config;
# verify with `npm config get before` after changing it, or run
# `npm run check:npm-hardening`).
#
# This gates dependency RESOLUTION (npm install / npm ci), not `npm view`, which
# queries registry metadata directly and still reports newer versions.
# One-off override when a genuinely new package is needed:
# npm install <pkg> --min-release-age=0
min-release-age=7

# Deliberately NOT setting prefer-offline. It pins npm to whatever packument is
# already cached, so a months-old cache hides every version published since —
# security patches included — and resolution fails with a confusing ETARGET
# ("no matching version ... with a date before <today-7d>") even for versions
# that are well outside the quarantine window. It buys no safety either: the
# protection here is min-release-age, not staleness.

# NOTE: ignore-scripts is deliberately NOT set here. canvas/sharp/secp256k1 need
# their build scripts, and the Docker build (npm ci) would break. Script blocking
# is applied per-install by scripts/safe-npm-update.sh instead.
16 changes: 14 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,20 @@ RUN echo "deb http://deb.debian.org/debian bullseye contrib" >> /etc/apt/sources
# Create app directory
WORKDIR /usr/src/app

# Copy package files
COPY package*.json ./
# Copy package files. .npmrc must come along BEFORE npm ci — it carries the
# min-release-age quarantine window, and without it the image build would
# resolve dependencies with no protection against a freshly-published
# compromised version (the Shai-Hulud npm worm delivery path).
COPY package*.json .npmrc ./
COPY scripts/check-npm-hardening.js ./scripts/

# node:20-bullseye bundles npm 10.8.2, which does not support min-release-age
# and SILENTLY IGNORES it — no warning, no error, just unprotected resolution.
# Every Node 20 and 22 release is affected; only Node 24+ bundles npm >= 11.10.
# So upgrade npm before installing anything, then assert the window is actually
# in effect. The check turns a silent downgrade into a failed build.
RUN npm install -g npm@11.10.0 \
&& node scripts/check-npm-hardening.js

# Install dependencies
RUN npm ci --legacy-peer-deps
Expand Down
Loading
Loading