# pnpm で依存パッケージのサプライチェーン攻撃に備える設定

## この記事のポイント

- 公開から 7 日たっていないバージョンは入れない(`minimumReleaseAge`)
- `package.json` のバージョンは `^` を付けずに固定し、CI は lockfile のとおりに入れる
- 更新は Dependabot の PR で受け取り、こちらも公開から 7 日待つ(`cooldown`)
- pnpm のバージョンも `packageManager` で固定する

## 何に備えるのか

npm のパッケージは、作者のアカウントが乗っ取られると、悪いコードを入れたバージョンが公開されることがあります。よく使われるパッケージでも起きていて、インストールしただけで、手元や CI の認証情報(npm や GitHub のトークン、クラウドの鍵など)を盗まれます。

こうしたバージョンは、多くの場合、公開から数時間〜数日で見つかって消されます。危ないのは、**公開されてすぐのバージョンを、知らないうちに入れてしまう**ことです。

次のようなときに起きます。

- `^1.2.0` のような範囲指定で、`pnpm install` や `pnpm update` が新しいバージョンを選ぶ
- 依存パッケージの依存パッケージ(間接的な依存)が、新しいバージョンに変わる
- CI が lockfile と違うバージョンを入れる

ここで紹介する設定は、どれも「新しいバージョンは、少し待ってから、目に見える形で入れる」ためのものです。

## 公開から 7 日たっていないバージョンを入れない

pnpm 10.16 から、`minimumReleaseAge` で、公開から指定した時間(分)がたっていないバージョンを選ばないようにできます。間接的な依存にも効きます。

```yaml
# pnpm-workspace.yaml
minimumReleaseAge: 10080 # 7 日
```

ワークスペースを使っていなくても、pnpm の設定は `pnpm-workspace.yaml` に書きます。

セキュリティの修正などで、新しいバージョンをすぐに入れたいときは、パッケージ名を一時的に除外します。

```yaml
minimumReleaseAgeExclude:
  - some-package
```

この確認は、バージョンを選ぶとき(lockfile を作る・更新するとき)にだけ行われます。lockfile にあるバージョンをそのまま入れるときは確かめないので、設定を入れる前の lockfile に新しいバージョンがあっても、そのまま残ります。

## バージョンを固定する

`saveExact` を有効にすると、`pnpm add` や `pnpm update` で、`package.json` に `^` を付けずにバージョンを書きます。

```yaml
# pnpm-workspace.yaml
saveExact: true
```

```json
{
  "dependencies": {
    "astro": "7.4.2"
  }
}
```

実際に入るバージョンは lockfile で決まりますが、`package.json` も固定しておくと、入っているバージョンがひと目でわかり、更新は必ず `package.json` の差分として残ります。

CI では、lockfile と違うものを入れないように、`--frozen-lockfile` を付けます。lockfile が `package.json` と合っていなければ、インストールが失敗します。

```yaml
- run: pnpm install --frozen-lockfile
```

## 更新は Dependabot の PR で受け取る

バージョンを固定すると、自分で更新しない限り古いままになります。Dependabot に、週 1 回、更新の PR を作ってもらいます。

```yaml
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
    cooldown:
      default-days: 7
    groups:
      minor-and-patch:
        update-types:
          - minor
          - patch
```

- `cooldown` で、公開から 7 日たったバージョンだけを PR にします。`minimumReleaseAge` と日数を合わせておかないと、PR の CI でインストールが失敗します
- 脆弱性の修正(セキュリティアップデート)には、`cooldown` は効きません。すぐに PR が作られます
- マイナーとパッチは 1 つの PR にまとめ、メジャーはパッケージごとの PR にします

PR になるので、どのパッケージがどう変わったかを確認でき、CI が通ってから取り込めます。

## pnpm と GitHub Actions も固定する

pnpm 自体のバージョンは、`package.json` の `packageManager` で固定します。

```json
{
  "packageManager": "pnpm@10.33.4"
}
```

`pnpm/action-setup` は、`version` を指定しなければ、この値を読みます。手元と CI で同じ pnpm を使えます。

GitHub Actions のアクションも、タグではなくコミットの SHA で指定します。タグは付け替えられるので、アクションが乗っ取られると、同じタグのまま中身が変わることがあります。

```yaml
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
```

Dependabot に `package-ecosystem: github-actions` を追加すると、SHA の更新も PR にしてくれます。

## これで防げないこと

- 7 日たっても見つからない、悪いバージョン
- 以前から入っているパッケージに、もともと含まれている問題

7 日待つのは、多くの人が使って問題が見つかるのを待つ、という考え方です。更新の PR を取り込むときは、変わったパッケージの名前と、大きな変更がないかを見ておきます。

なお pnpm 10 では、依存パッケージの `postinstall` などのスクリプトは、許可したものしか実行されません。インストールしただけで悪いコードが動く経路を、既定で減らしています。