こんにちは。テックリードの私だ。
PHPでの開発において、私たちは驚異的なスピードで外部のライブラリ(ベンダーパッケージ)をプロジェクトに組み込んでいる。`composer require` を1回叩くだけで、世界中の優秀なエンジニアが書いた複雑なロジックが手に入り、開発スピードは劇的に向上した。
しかし、その裏で何が起きているか意識したことはあるだろうか?
あなたが何気なくインポートしたパッケージの、さらにその孫依存(Transitive Dependencies)のパッケージが、過去に致命的な脆弱性を抱えていたとしたら——?
今回は、Composerの標準機能でありながら、いまだにCI/CDパイプラインや日々のローカル開発に組み込んでいない現場が後を絶たない `composer audit` に焦点を当てる。単なるコマンドの使い方ではなく、チーム全体のセキュリティ担保と開発効率を最大化するプロの実践テクニックを伝授しよう。
—
なぜ `composer audit` なのか?(アーキテクトの視点)
これまでのPHPエコシステムでは、サードパーティの脆弱性スキャンといえば、CIで別途セキュリティツールを走らせたり、GitHubのDependabot頼みだったりした。しかし、Dependabotは「PRが乱立してノイズになる」「マージするまでローカルの安全性が担保されない」というジレンマがあった。
Composer v2.4.0以降、Packagist APIおよびGitHub Advisory Databaseと連携したネイティブの脆弱性スキャン機能(`composer audit`)が標準実装された。これにより、追加の重いミドルウェアや外部SaaSを導入せずとも、パッケージマネージャー自体のコンテキストで、瞬時に依存関係の毒素を検知できるようになったのだ。
内部の動きとしては、Composerはローカルの `composer.lock` に記述されたパッケージ名とバージョンを抽出し、公式の脆弱性データベース(National Vulnerability Database や GitHub Security Advisories等)のハッシュ/バージョン制約と突合している。つまり、ロックファイルが存在していれば、わずかコンマ数秒でプロジェクト全体のセキュリティ診断が完了する。
—
1. 実践:`composer audit` の基本と出力フォーマットの制御
まずは手元のプロジェクトで以下のコマンドを叩いてみてほしい。
プロジェクトの依存関係に既知の脆弱性がないかスキャンを実行
composer audit
もし脆弱性が検知された場合、Composerは非ゼロの終了コード(Exit Code `1`)を返してプロセスを終了する。これが何を意味するか? CI/CDパイプラインのゲートキーパーとしてそのまま利用できるということだ。
マシンリーダブルな出力でCIをハックする
CI/CDやログ監視ツール(DatadogやSentryなど)と連携させる場合、人間向けのきれいなテーブル出力ではなく、JSON形式での出力が必須となる。以下のオプションを使い分けろ。
JSON形式で出力し、ファイルに保存(ログ監査用)
composer audit –format=json –output=audit-report.json
フォーマットを明示的に指定して標準出力に出す
composer audit –format=table
—
2. チーム開発の生産性を落とさない「脆弱性除外(Ignore)」の神テクニック
セキュリティスキャンの最大の敵は「誤検知(False Positive)」や「今すぐ修正できないが、ビジネス上の理由で一時的に許容せざるを得ない脆弱性」によるCIのブロックだ。これに直面した開発者が `–no-audit` をつけて逃げ出すようになったら、その組織のセキュリティガバナンスは崩壊している。
Composerには、特定の脆弱性を一時的、あるいは恒久的に「無視(Ignore)」するための仕組みが備わっている。これを `composer.json` に適切に定義するのがテックリードの腕の見せ所だ。
`composer.json` での高度な監査設定(ベストプラクティス構成)
以下に、実務で即座に採用できる `composer.json` の `config` セクションの設定例を示す。
{
“name”: “example/secure-enterprise-app”,
“description”: “Composer auditを完全統制したエンタープライズPHPアプリケーション”,
“type”: “project”,
“require”: {
“php”: “^8.2”,
“laravel/framework”: “^10.0”,
“guzzlehttp/guzzle”: “^7.8”
},
“config”: {
“optimize-autoloader”: true,
“preferred-install”: “dist”,
“sort-packages”: true,
“audit”: {
“abandoned”: “report”,
“ignore”: {
“CVE-2023-12345”: {
“reason”: “影響を受けるコンポーネントの該当機能を使用しておらず、次回のメジャーアップデート(来月)でライブラリごとリプレイス予定のため一時除外”
}
}
}
}
}
この設定の恐るべきメリット
1. `abandoned: “report”` の指定:
単に脆弱性(Vulnerability)だけでなく、「メンテナーによって放棄された(Abandoned)パッケージ」に対しても警告を出させる設定だ。放置されたパッケージは将来のゼロデイ攻撃の温床になるため、これを検知できる意味は非常に大きい。`fail` にすると厳しすぎるため、まずは `report` でチームに意識改革を促すのが定石だ。
2. `ignore` による理由(Reason)の明文化:
CVE単位で除外理由を JSON 内にコメントとして残せる。これにより、セキュリティ監査が入った際も「なぜこの脆弱性を放置しているのか」をコードベースで完全に証明できる。
—
3. 依存ライブラリのセキュリティパッチ適用フロー(安全なアップデート戦略)
`composer audit` で脆弱性が検知された場合、慌てて `composer update` を全パッチに対して打つのは素人のやることだ。本番環境の破壊(Breaking Changes)を引き起こすリスクがある。プロのエンジニアは以下のフローで機械的に、かつ安全にパッチを適用する。
ステップ1: 該当パッケージの特定と影響範囲の確認
監査で脆弱性がヒットしたら、まずはそのパッケージピンポイントで情報を取得する。
特定のパッケージの更新可能バージョンとセキュリティ情報を確認
composer outdated –direct –minor-only
ステップ2: 最小限の安全なアップデート
対象のパッケージのみをアップデートし、他の依存関係への波及を最小限に抑える。
脆弱性が修正されたバージョンへ安全にアップデート(vendor/composer.lock も同時に更新)
composer update vendor-name/vulnerable-package –with-dependencies
ステップ3: 変更の検証とCIでの自動化
アップデート後は、必ず自動テスト(PHPUnit / Pest 等)を走らせ、リグレッション(デグレ)が発生していないことを確認する。
—
4. チーム開発で役立つ:GitHooks & CI/CDによる強制力の実装
ルールの共有化はドキュメントではなく、「仕組み(Automation)」によって強制しなければ意味がない。開発者のローカル環境とCIパイプラインの両方で `composer audit` が走るようにセットアップしよう。
A. Git Hooks(Husky またはネイティブGithooks)によるローカルブロック
開発者が `git push` または `git commit` する前に、ローカルで監査を走らせる。
`.git/hooks/pre-push` (またはプロジェクト管理下のシェルスクリプト)に以下を仕込む。
!/bin/sh
—————————————————————–
Git Pre-Push Hook: プッシュ前にComposerの脆弱性スキャンを強制実行
—————————————————————–
echo “🔍 Running Composer Security Audit…”
composer audit を実行。終了コードが非ゼロ(脆弱性あり)ならプッシュを中断
composer audit –format=table
if [ $? -ne 0 ]; then
echo “❌ [ERROR] 依存パッケージに脆弱性が検知されました。”
echo “修正してから再度プッシュしてください。”
exit 1
fi
echo “✨ Security check passed successfully.”
exit 0
B. GitHub Actions によるCIパイプラインの要塞化
リポジトリのルートに `.github/workflows/security-audit.yml` を配置し、プルリクエスト作成時および定期実行(cron)で脆弱性を監視する。
name: “Composer Security Audit”
on:
pull_request:
branches: [ “main”, “develop” ]
paths:
- ‘composer.json’
- ‘composer.lock’
schedule:
# 毎日深夜3時に依存関係のゼロデイ脆弱性をチェック
- cron: ‘0 3 ‘
jobs:
audit:
name: “Run composer audit”
runs-on: ubuntu-latest
steps:
- name: “Checkout Code”
uses: actions/checkout@v4
- name: “Setup PHP Environment”
uses: shivammathur/setup-php@v2
with:
php-version: ‘8.2’
tools: composer:v2
- name: “Get Composer Cache Directory”
id: composer-cache
run: |
echo “dir=$(composer config cache-files-dir)” >> $github_output
- name: “Cache Composer Dependencies”
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${