金色の配線パターンが広がる基板の接写
現場の実践

セキュリティスキャンをbashで自動化した業務基盤が壊れる理由

目次を見る

社内のセキュリティ診断や資産棚卸しを、subfinder・nuclei・httpxといったOSSツールをbashでつないで自動化している運用チームに向けた内容です。運用が長期化するほど、この構成には共通の落とし穴が現れます。ここでは何が起きるか、なぜ起きるかを分解し、自分のプロジェクトが該当するかの確認方法と対策手順を整理します。

何が起きるか:bashパイプラインの限界が露呈する

複数の外部ツールをパイプ(|)でつないでbashスクリプト化する運用は、初期段階では手早く動きます。ところがスキャン対象が数百から数千のエンドポイントに増えると、途端に破綻が見え始めます。

典型的な症状は次の3つです。

  • あるツールが途中でハングしても、後続処理が動き続けて不正確な結果を出力する
  • 並列実行を&任せにした結果、リソース枯渇でChromeやHTTPクライアントのプロセスが残り続ける(いわゆるゾンビプロセス)
  • どのツールがどの入力を処理したか記録がなく、失敗時にどこからリトライすべきか分からない

これは個人の検証環境では気づきにくく、CI/CDパイプラインに組み込んで定期実行し始めた段階で表面化しやすい問題です。夜間バッチで実行したスキャンが翌朝失敗しているのに、ログを見てもどの工程で止まったのか特定できない、という状態に近いイメージです。

なぜ起きるか:bashが前提としていない責務を背負わされている

原因は一段ずつ分解すると見えてきます。

第一に、bashのパイプは「依存関係の管理」を想定した設計ではありません。subfinderでサブドメインを列挙し、その結果をhttpxで生存確認し、nucleiで脆弱性スキャンする、という3段の処理には本来DAG(有向非巡回グラフ、処理の依存関係を矢印でつないだ構造)としての実行順序管理が必要です。bashの&&|はこの依存関係を表現できず、あるステップが実は前段の一部結果だけに依存している、といった部分並列化ができません。

第二に、並行処理の制御が弱いことです。bashで&を使ったバックグラウンド実行は、Go言語のgoroutine(軽量スレッドの仕組み)とsync.WaitGroup(複数の並行処理の終了を待ち合わせる仕組み)のような、完了待ちと同時実行数の制御を両立させる機構を持ちません。結果として「並列にしたつもりが実は待ち合わせできていない」状態に陥りやすくなります。

第三に、プロセスのライフサイクル管理が抜け落ちがちです。ヘッドレスブラウザ(画面表示なしで動くブラウザ、Chromeのheadlessモードなど)を使ったDOM解析やJavaScript評価を伴うスキャンでは、タイムアウトやキャンセル時に子プロセスを確実に終了させる仕組みが要ります。bashのシグナルハンドリングだけでは、孤立したChromeプロセスが残留し続けるケースがあります。

第四に、TLS(通信を暗号化する仕組み)を挟んだ中間者的な検証、たとえば権限昇格やIDOR(本来アクセスできないはずのリソースIDを推測して不正取得できてしまう不備)の検出を自動化しようとすると、bashの外部ツール呼び出しだけでは、セッションごとの証明書発行やリクエスト差分比較を柔軟に組み立てにくくなります。

自分のプロジェクトが該当するか確認する方法

次の観点でチェックしてみてください。

  • スキャン用スクリプトの行数をwc -l scan.shで確認し、300行を超えているか。行数が多いほど暗黙の依存関係が埋め込まれている可能性が高くなります
  • ps aux | grep chromeまたはps aux | grep nucleiで、スキャン終了後にプロセスが残っていないか確認する
  • crontabやCIのジョブ定義(.gitlab-ci.yml.github/workflows/*.yml)で、スキャンステップにtimeoutコマンドや明示的なタイムアウト設定があるかを見る。無い場合はハング時に検知できません
  • スキャン結果の出力に、実行時刻・入力元・成功/失敗のステータスが構造化された形(JSONなど)で残っているか。テキストログだけの場合はトレーサビリティが低い状態です

これらのうち2つ以上に該当するなら、bashベースの自動化がスケールの限界に近づいているサインです。

対策の手順

段階的に移行する手順を示します。いきなり全面書き換えする必要はありません。

ステップ1:処理をYAMLで宣言的に定義し直す

各ツールの実行順序と依存関係を、シェルスクリプトの手続き的な記述からYAML定義に切り出します。たとえば「サブドメイン列挙 → 生存確認 → 脆弱性スキャン」という依存関係を、ステップ名とdepends_onで明示する形式です。

name: "Recon Pipeline"
steps:
  - name: subdomain_scan
    type: subfinder
    config:
      threads: 50
    output_as: subdomains
  - name: live_filter
    type: simple_probe
    depends_on: [subdomain_scan]
    config:
      targets_from: subdomains
    output_as: live_hosts

この形にしておくと、依存のないステップだけを並列化する判断がしやすくなります。

ステップ2:並行実行の言語をGoやPythonのasyncioに寄せる

bashの&任せの並列化を、Goのgoroutinesync.WaitGroup、あるいはPythonのasyncioのような、完了待ちと同時実行数の上限を制御できる仕組みに置き換えます。エンタープライズ環境では既存の運用基盤がPython中心のケースが多いため、Go導入のハードルが高い場合はasyncio.Semaphoreで同時実行数を絞る方法でも十分効果があります。

ステップ3:子プロセスの後始末を明示する

ヘッドレスブラウザや外部プロセスを起動する場合は、専用のプロセスグループで起動し、タイムアウトやキャンセル時にSIGKILLで確実に終了させる処理を入れます。Goであればsyscall.SIGKILLをプロセスグループ単位で送る実装が可能です。bashのままで運用を続ける場合も、最低限timeoutコマンドでラップし、pkill -Pで子プロセスごと終了させる仕組みを入れておくと被害が抑えられます。

ステップ4:結果を構造化して残す

スキャン結果はテキストログではなく、JSONなど構造化された形式で出力し、実行時刻・入力元・成否を記録します。これにより、失敗したステップだけをピンポイントで再実行できるようになり、夜間バッチの障害調査が格段に速くなります。

まとめ

bashによるツール連携は、スキャン対象が小規模なうちは十分機能します。ただし対象規模が増え、CI/CDに組み込んで定期運用する段階になると、依存関係管理・並行制御・プロセス後始末・結果の構造化という4つの弱点が表面化します。

まずはps auxでプロセス残留の有無を確認し、次にYAMLで処理の依存関係を宣言的に書き出すところから始めてみてください。全面的な書き換えをせずとも、依存関係の可視化とタイムアウト処理の追加だけでも、運用の安定度は大きく変わります。

参考

Architecting a Custom Purple Team Infrastructure Scanner in Go.

この記事について: 本記事は AI を活用して作成し、forva AI 編集部が内容を確認・監修しています。

AI 駆動開発のご相談は forva AI へ。まずはお気軽にどうぞ。