Software supply chain attacks — compromised npm packages, tampered Docker images, malicious dependencies — have become a primary attack vector. The SolarWinds and XZ Utils incidents demonstrated that production software with no obvious code bugs can still deliver attacker code through the supply chain. Claude Code implements the technical controls that make your build pipeline trustworthy: Software Bill of Materials (SBOM), artifact signing, dependency pinning, and automated update workflows.
Software Bill of Materials (SBOM)
Generate an SBOM for our project and add it to every release.
We need visibility into our full dependency tree including transitive deps.
# .github/workflows/release.yml — SBOM generation in CI
- name: Generate SBOM (Node.js)
uses: anchore/sbom-action@v0
with:
format: spdx-json # SPDX 2.3 — industry standard
output-file: sbom.spdx.json
# Include dev dependencies? No for production builds
config: ./.sbom-config.yaml
# .sbom-config.yaml
catalogers:
- node-modules-cataloger # Reads node_modules
- npm-cataloger # Reads package-lock.json
exclude:
- '**/node_modules/**' # Already covered by node-modules-cataloger
- '**/*.test.ts' # Exclude test files
For Docker images (full dependency tree):
# Generate SBOM from Docker image — scans OS packages + language packages
syft ubuntu-22.04 -o spdx-json > ubuntu-sbom.json
# More useful: scan your production image
syft myapp:latest -o spdx-json > myapp-sbom.spdx.json
# Scan for known vulnerabilities in the SBOM
grype sbom:myapp-sbom.spdx.json --fail-on high
Attach SBOM to releases:
- name: Attach SBOM to GitHub release
uses: softprops/action-gh-release@v2
with:
files: |
sbom.spdx.json
dist/app.tar.gz
Signing Artifacts with Sigstore/cosign
After generating the SBOM and build artifacts,
sign them so users can verify they came from our CI pipeline.
# .github/workflows/release.yml — keyless signing with Sigstore
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign container image (keyless — uses OIDC identity)
run: |
cosign sign --yes \
--rekor-url=https://rekor.sigstore.dev \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
env:
COSIGN_EXPERIMENTAL: 1 # Keyless mode — no private key management
- name: Sign SBOM and attach to image
run: |
cosign attest --yes \
--type spdxjson \
--predicate sbom.spdx.json \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
- name: Sign release binary
run: |
cosign sign-blob --yes \
--output-signature dist/app.tar.gz.sig \
--output-certificate dist/app.tar.gz.cert \
dist/app.tar.gz
Verify in your deployment pipeline:
# Verify the image was signed by our CI pipeline (checks OIDC identity)
cosign verify \
--certificate-identity-regexp="https://github.com/mycompany/myapp/.github/workflows/release.yml@refs/heads/main" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
myregistry.io/myapp:1.2.3
# Verify and extract the SBOM
cosign verify-attestation \
--type spdxjson \
--certificate-identity-regexp="https://github.com/mycompany/myapp/.github/workflows/release.yml" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
myregistry.io/myapp:1.2.3 | jq .payload | base64 -d | jq .predicate
Dependency Pinning and Automated Updates
Our package.json uses ^ ranges — a compromised patch release could get in.
Pin everything but automate updates so we don't fall behind.
Generate pinned lockfile and enforce it in CI:
# Pin exact versions in package.json
npm shrinkwrap # Creates npm-shrinkwrap.json — exact versions
# Or use --exact flag when adding packages
npm install --save-exact express # "express": "4.18.2" not "^4.18.2"
Enforce no lockfile drift in CI:
# .github/workflows/ci.yml
- name: Verify lockfile is current
run: |
npm ci # Fails if package-lock.json doesn't match package.json
# npm ci is more deterministic than npm install — always use in CI
Renovate for automated dependency updates:
// renovate.json — automated, security-gated updates
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
// Auto-merge patch updates that pass CI (low risk)
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"matchCurrentVersion": "!/^0/", // Exclude pre-1.0 (unstable)
"automerge": true,
"automergeType": "pr"
},
{
// Security updates: merge immediately
"matchCategories": ["security"],
"automerge": true,
"schedule": ["at any time"]
},
{
// Major updates: group together, require human review
"matchUpdateTypes": ["major"],
"groupName": "major dependencies",
"schedule": ["on the first day of the month"]
}
],
// Vulnerability alerts: always create PRs regardless of schedule
"vulnerabilityAlerts": {
"enabled": true,
"schedule": ["at any time"]
},
"lockFileMaintenance": {
"enabled": true,
"schedule": ["on Sunday"]
}
}
Container Image Hardening
Our Docker images run as root and include many tools attackers could use.
Harden them.
# Multi-stage: builder has full toolchain, runtime has nothing extra
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev # Only production deps in final image
COPY src/ ./src/
COPY tsconfig.json ./
RUN npm run build
# Final image: distroless — no shell, no package manager, no attack surface
FROM gcr.io/distroless/nodejs20-debian12 AS runtime
# Security: run as non-root
USER nonroot:nonroot
WORKDIR /app
COPY --from=builder --chown=nonroot:nonroot /app/node_modules ./node_modules
COPY --from=builder --chown=nonroot:nonroot /app/dist ./dist
# Read-only filesystem — attacker can't modify files
VOLUME ["/tmp"] # Allow writes only to /tmp
EXPOSE 3000
CMD ["/app/dist/server.js"]
Scan image in CI before pushing:
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy-results.sarif
# Fail on high/critical
exit-code: '1'
severity: 'HIGH,CRITICAL'
ignore-unfixed: true # Only flag if a fix is available
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
Dependency Integrity Checks
We want to verify npm packages haven't been tampered with
before installing them in production.
# package.json: enforce integrity hashes
# npm already writes these to package-lock.json:
# "integrity": "sha512-abc123..."
# Audit against known vulnerabilities
npm audit --audit-level=high
# Check for suspicious packages (typosquatting, metadata anomalies)
npx socket --strict # Socket.dev: detects supply chain risks
# In CI: never install from registry — use lockfile only
npm ci # Reads package-lock.json, verifies integrity hashes
For applying broader security controls including container security policies, see the security testing guide and the Kubernetes operators guide for policy enforcement. For CI/CD pipeline security that validates artifact signing as part of deployment, the CI/CD advanced guide covers pipeline hardening. The Claude Skills 360 bundle includes DevSecOps skill sets for SBOM generation, artifact signing, and dependency hardening. Start with the free tier to try supply chain security configurations.