Web制作会社をやっていると、納品したWordPressサイトがどんどん増えていきます。公開した瞬間がゴールではなく、そこから先の保守が仕事です。そして保守でいちばん怖いのは、気づかないうちに更新が止まっていることだと思っています。
「あのサイト、プラグインの更新いつ最後に見たっけ」「PHPのバージョン、そろそろ危ないんじゃなかったっけ」。サイトが増えるほど、こういう不安を人の記憶と手作業で抱えるのが難しくなります。そこで、複数のWordPressサイトの状態を毎朝まとめて見張る社内システムを、AIと一緒に作りました。今回はその開発の裏側です。
何を見張るのか
見張るのは、次の3つです。
- プラグインとWordPressコアのバージョンと、有効・無効
- 公表されている既知の脆弱性に、使っているバージョンが当たっていないか
- PHPとWordPressのサポート終了(EOL)が近づいていないか
変化があればメール(とSlack)に通知し、状態は1画面のダッシュボードで一覧できます。
逆に、やらないことも最初に決めました。改ざん検知は、すでに別に作っているAI Guardianの仕事。死活監視は、UptimeRobotのような外部サービスの仕事。このシステムは「状態の見える化」だけに絞ります。役割を絞ったおかげで、作るものがぶれませんでした。
実は、AI Guardianを作った時点で「プラグインのバージョン管理と脆弱性の突き合わせ」も構想にはありました。ただ、脆弱性データの入手方法(商用ライセンスやAPIの上限)の検討事項が多くて、いったん保留にしていた部分でした。今回はそこを、無料で使える脆弱性フィードを日次で取り込む形にして、別システムとして切り出しています。
仕組み:各サイトに「読み取り専用の窓口」を置く
構成はこんな流れです。
- 各WordPressサイトに、小さなプラグイン(`mu-plugins`に置くエージェント)を1つ置く
- エージェントは、合言葉(サイトごとに違うキー)を持った相手にだけ、プラグイン一覧・バージョン・更新の有無・WPとPHPのバージョンをJSONで返す
- 中央のサーバーが毎朝、全サイトを巡回して状態を保存し、前回との差分・脆弱性・EOLを判定して、まとめて通知する
- 脆弱性のデータは、中央が毎朝、公開されているフィードを取り込んでおく
設計でいちばん大事にしたのは、エージェントは読み取り専用ということです。中央のサーバーから、サイトを書き換えたり操作したりする機能は、あえて持たせていません。万が一この仕組みのどこかが破られても、起きるのは「情報が見られる」まで。「サイトが乗っ取られる」にはつながらない。便利さより、被害が連鎖しないことを優先した判断です。
ログインを楽にするために、管理画面のログインURLだけは登録できるようにしました。ただし保存するのはURLだけで、IDやパスワードは一切持ちません。
AIとの進め方
ここは、これまでのシステムと同じです。仕様の壁打ちと設計はClaude、実装はClaude CodeとCursor。セッションをまたいでも文脈が切れないよう、引き継ぎ用のドキュメントを常に最新に保つルールで進めました。「何を、なぜ、どうなれば完了か」を書いた指示書を渡して実装してもらう、という流れです。
今回は、もう1つ工程を足しました。作ったものを、別のAI(コーディング支援AI)にレビューしてもらうことです。これが、一番勉強になりました。
ハマったこと
サーバーの「素のphp」は古かった
エックスサーバーで`php`と打つと、動くのはずっと古いバージョンでした。Webでは新しいPHPで動いているのに、SSHやcronで呼ぶ`php`は別物。以前のタスク管理ツールでも同じ罠にハマったのに、また踏みかけました。今回は最初から、cronもすべて`php8.2`とバージョンを明示。手順書にも太字で書いています。
脆弱性フィードの仕様が変わっていた
参考にした古い情報どおりのURLを叩くと、「HTTP 410(もう無い)」。脆弱性フィードの旧バージョンは廃止されていて、APIキーをつけて取りにいく新しい形式に変わっていました。無料のキーには取得頻度の制限もあって、連続で試すと弾かれます。さらにデータが大きく、取り込みにはメモリを増やして実行する必要がありました。
一番多いミスは「キーの不一致」
各サイトに置いた合言葉と、中央に登録した合言葉がズレている。これが、動かないときの原因の堂々の1位でした。とても地味ですが、手順書の「つまづきやすい点」の先頭に書いています。
監視できないサイトもある
エージェントはWordPressのREST APIに乗って動くので、セキュリティ対策でREST APIを止めているサイトからは、そもそも応答が返ってきません。そういうサイトは監視対象から外して、手動で管理することにしました。「全部を自動で」にこだわらず、できないものは線を引く、も大事でした。
AIが書いたコードを、別のAIにレビューさせたら
実装が一通り動いたところで、その別のAIにコードレビューを頼みました。出てきた指摘は、どれも「動いているように見えて、実は危ない」ものばかりでした。
- 通知が、送れていないのに「送った」ことになる。メールやSlackの送信が失敗しても、最後に「通知済み」とマークしてしまう。失敗した通知は二度と送られない
- 空のデータを取り込んで、脆弱性データベースが空っぽになる。取得したフィードが空や壊れていても、既存のデータを消してしまう。そうなると、画面は静かに「異常なし」を表示し続ける
- 想定外の形のデータが来ると、画面が落ちる。
- そのほか、通信先の安全確認の抜け穴など
「緑=安全」ではない、というのを痛感しました。監視ツールが壊れたとき、一番困るのは「壊れたことに気づけず、異常なしと表示し続ける」ことです。
直したあと、もう一度レビューをお願いすると、直したコードの穴を指摘されました。
- 長い脆弱性の説明文が保存できず、そのサイトの更新ごと失敗する
- フィードが「一部だけ」壊れていると見逃して、一部の脆弱性を静かに失う
- 情報の足りない応答を「正常」と判断してしまう
守るためのコードが、新しい壊れ方を作ることがあるんですね。そこで、テストの書き方も変えました。「長い値が入ったら失敗する」ことをまずテストで再現してから直す。さらに、検証を厳しくして本物のエージェントまで弾いていないかを確かめるために、エージェントの実際のコードを動かしてその出力を検証するテストも加えました。最終的にテストは150件を超えました。
とはいえ、テストが全部通ることと、本番で問題なく動くことは別の話です。本番のPHPのバージョンやデータベース、サーバーのライブラリでの動作確認を済ませたうえで反映し、いまは実際に、毎朝すべてのサイトの状態を見張る運用が始まっています。
振り返って
- 役割を絞る。改ざん検知・死活監視・見える化を混ぜずに、それぞれ別の道具に任せたら、作るものがシンプルになった
- 強くしすぎず、被害が連鎖しない設計にする。読み取り専用、保存するのはURLだけ、という割り切りが安心につながった
- AIが書いたコードは、別のAIにも見てもらう。自分で気づけない穴が見つかる。ただし、指摘をそのまま信じず、必ず再現して確かめてから直す
- 「止まったとき」の振る舞いを先に考える。失敗したときに、静かに成功を装わないこと
そして皮肉なことに、このシステムが最近いちばん熱心に教えてくれるのは、自社のサーバーで使っているPHPのバージョンが、もうすぐサポート終了だということです。自分たちで作った見張り番に、自分たちが叱られています。年末までに、PHPの引き上げを進める予定です。
