# Build reproducibility verified

- **Pattern:** `ab-002152` (`code-quality-essentials.dependencies.build-reproducible`)
- **Severity:** info
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002152
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002152)

## Why it matters

Using `npm install` in CI instead of `npm ci` means the lockfile is advisory rather than enforced — if `package.json` and the lockfile diverge (a common accident after a manual install), CI installs different versions than the lockfile specifies, silently breaking reproducibility. SLSA L2 and SSDF SP 800-218 PW.4.1 both require that the build process is deterministic and traceable. A non-reproducible build makes security audits meaningless: you cannot verify what code shipped without knowing exactly which dependency versions were installed.

## Severity rationale

Info because non-frozen CI installs are a build hygiene defect that undermines reproducibility guarantees, but they rarely cause immediate functional failures unless a transitive dependency changes in a breaking way.

## Remediation

Replace `npm install` with `npm ci` in all CI configuration. `npm ci` errors if the lockfile and `package.json` are out of sync, enforcing that the lockfile is the authoritative source.

```yaml
# .github/workflows/ci.yml
- name: Install dependencies
  run: npm ci
```

For Docker builds, copy the lockfile before installing:
```dockerfile
COPY package.json package-lock.json ./
RUN npm ci --only=production
```

For Yarn: `yarn install --frozen-lockfile`. For pnpm: `pnpm install --frozen-lockfile`.

## Detection

- **ID:** `build-reproducible`
- **Severity:** `info`
- **What to look for:** Check whether the CI pipeline uses a frozen lockfile install command (`npm ci`, `yarn install --frozen-lockfile`, `pnpm install --frozen-lockfile`, `bun install --frozen-lockfile`) rather than `npm install`. The difference matters: `npm install` may resolve newer patch versions even with a lockfile present, while `npm ci` installs exactly what the lockfile specifies and errors if the lockfile is out of sync with `package.json`. Also check whether the lockfile is committed (covered by a separate check). Look for `npm ci` in CI workflow files. Additionally check for Docker build caching patterns that ensure the lockfile is the cache key.
- **Pass criteria:** Enumerate all relevant code locations. Lockfile committed to repo (pass from earlier check) AND CI uses `npm ci` (or equivalent frozen install) rather than `npm install` with at least 1 verified location.
- **Fail criteria:** CI uses `npm install` instead of frozen lockfile install, OR no CI configured.
- **Skip (N/A) when:** Not a Node.js project.
- **Detail on fail:** `"CI uses 'npm install' instead of 'npm ci'; builds may not be reproducible if package.json and lockfile diverge"`
- **Remediation:** Replace `npm install` with `npm ci` in all CI configuration:

  ```yaml
  # .github/workflows/ci.yml
  - name: Install dependencies
    run: npm ci   # uses lockfile exactly, errors if out of sync
  ```

  For Docker:
  ```dockerfile
  COPY package.json package-lock.json ./
  RUN npm ci --only=production
  ```

## External references

- slsa L2
- ssdf PW.4.1

Taxons: supply-chain, operational-readiness

HTML version: https://auditbuffet.com/patterns/ab-002152
