ADR-061: Supply-Chain & Secrets Hardening
ADR-061: Supply-Chain & Secrets Hardening
Status: Implemented Date: 2026-05-30 Sprint: 76
Context
ADR-059 (dependency audit gate) and ADR-060 (code-scanning gate) close the known-vulnerability and static-analysis gaps. Neither addresses the supply-chain ingestion vector: the path by which a malicious or compromised dependency executes code on a developer machine or in CI. With public release imminent, the recurring real-world pattern is a worm (e.g. "Shai-Hulud"-style) that propagates through npm lifecycle scripts (postinstall/preinstall) and non-deterministic installs. This ADR folds in a set of quick, durable hardening measures against that vector, plus two long-disabled secret-scanning sub-toggles.
Decision
1. Block lifecycle scripts on install — ignore-scripts=true
.npmrc now sets ignore-scripts=true. Dependency pre/postinstall scripts no longer run on npm ci / npm install, removing the primary worm execution vector.
Trade-off — git hooks no longer auto-install. The root postinstall previously ran scripts/install-hooks.sh; with ignore-scripts=true it would not run, so we removed the postinstall entry entirely (leaving a dead script that silently never runs is worse than removing it). The hooks:install npm script is retained. After cloning, run:
npm run hooks:install
This is documented in CLAUDE.md, README, and docs/GITHUB_ACTIONS_SETUP.md. A clean npm ci && npm run build was verified to still succeed with scripts ignored (no dependency in the tree relies on a build-time install script).
2. Deterministic installs everywhere — npm ci
e2e-tests.yml was the last workflow using non-deterministic npm install (root install + the tests/e2e install). Both are now npm ci, which installs strictly from the committed lockfile and fails on lockfile/package.json drift. (CI's other jobs already used npm ci.)
3. Registry provenance check — npm audit signatures
The security: CI job now runs npm audit signatures after npm ci — verifying the registry signatures/attestations on the installed tree. Informational (non-blocking) first; this ADR records the decision to flip it blocking once the signed-package coverage of our tree is confirmed stable.
4. Broader advisory coverage — OSV-Scanner
The security: job adds an OSV-Scanner step (Google's open-source vulnerability scanner) against package-lock.json. OSV draws on a broader advisory database than npm audit alone. Advisory-based, non-blocking first (continue-on-error: true); this ADR is the record for when it flips blocking.
Limitation: OSV-Scanner is advisory-based — it catches packages with known CVEs/advisories, not a brand-new malicious package that has no advisory yet. The behavioral complement is the Socket GitHub App (detects install scripts, network/filesystem access, obfuscation, etc. at the PR level). Socket is a recommended console install (not a committed gate, since it's an App, not a workflow), and pairs naturally with the direct-push flow.
5. Review-gated dependency updates — .github/dependabot.yml
Added Dependabot for npm (grouped into production-deps / dev-deps) and github-actions, weekly. Deliberately NO auto-merge — an auto-merged bump is itself an ingestion path; every Dependabot PR is human-reviewed before it lands.
Amended 2026-09-30 (Sprint 131, maintainer): security updates only. Weekly version updates had
turned into a steady queue of upgrade PRs, and each one cost a full review-and-deploy cycle with no
security or product need behind it. Both entries now set open-pull-requests-limit: 0. GitHub
documents this as disabling version updates, and security update PRs "are not subject to this limit".
Security updates come from the repository setting (automated security fixes, enabled). The existing
ignore entries (Expo SDK-managed packages, version-update types only) and the groups are unchanged.
Upgrades without an advisory happen deliberately, when a feature needs them. The policy is gated by
tests/regression/sprint-131-dependabot-security-only.test.ts. SHA-pinned Actions now get bumped only
by security updates or by hand.
6. Pin third-party GitHub Actions to commit SHA
CodeQL's actions/unpinned-tag rule flagged the third-party docker/* actions pinned to floating tags. They are now pinned to full commit SHAs (GitHub-owned actions/* are not flagged and are left on tags):
docker/setup-buildx-action@8d2750c…(# v3) — inci.yml+test.ymldocker/build-push-action@ca052bb…(# v5) — inci.ymlgoogle/osv-scanner-action/osv-scanner-action@e69cc6c…(# v2.0.2) — the new scanner step
A floating tag can be force-pushed to point at malicious code; a commit SHA cannot.
7. Secret-scanning sub-toggles
Secret scanning + push protection were already enabled. Two sub-toggles were attempted via API:
- Validity checks — verify whether a leaked credential is still live.
- Non-provider patterns — detect generic secrets beyond known-provider formats.
Status note: On this personal (user-owned) public repo, the
PATCH /repos/{owner}/{repo}calls forsecret_scanning_validity_checksandsecret_scanning_non_provider_patternsreturned 200 but the toggles did not persist (the feature appears gated behind an account/org-level setting or the UI for non-org repos). These remain enabled via the Settings UI when available; the API path is recorded here for when the repo moves under an org. Core secret scanning + push protection remain on.
Alternatives Considered
npm config set ignore-scriptsper-CI-job instead of.npmrc— rejected;.npmrcmakes it the default for every install (CI, server, and developer machines), which is the point.- Allow-list specific packages' install scripts — deferred; nothing in the current tree needs an install script, so a blanket block is simpler and stricter.
- Dependabot auto-merge for patch updates — rejected (ingestion path; see §5).
- Making OSV-Scanner / audit-signatures blocking immediately — deferred to avoid a noisy gate before we understand baseline signal; flip recorded as a follow-up.
Consequences
Positive
- The lifecycle-script worm vector is closed by default for all installs.
- CI installs are fully deterministic; action supply chain is SHA-pinned.
- Dependency updates arrive as reviewable, grouped PRs without auto-merge.
- Two additional advisory/provenance signals (OSV, audit signatures) in CI.
Negative / trade-offs
- Developers must run
npm run hooks:installafter clone (one-time, documented). - OSV-Scanner / audit-signatures are advisory-only until flipped blocking.
- The secret-scanning sub-toggles could not be persisted via API on this repo (UI / org-move follow-up).
Backlog (remaining supply-chain items, not in this sprint)
- Token hygiene — scope/rotate CI tokens; least-privilege
GITHUB_TOKENpermissions per job. - Revisit auto-merge-as-ingestion-path policy if/when a provenance-verified auto-merge (signed + Socket-clean) becomes viable.
- Socket GitHub App install (behavioral malicious-package detection).
- Centrally sanitize CR/LF in the shared logger to clear the 11
js/log-injectionmediums (deferred in ADR-060).