Sitadel は今も出荷している最も古いプロジェクトです。放置された Web アプリケーションスキャナーの Python 3 書き直しとして始まり、あるとき — 誰かに頼まれたわけでもなく — Kali LinuxBlackArch に 収録されました。プロジェクトがそれまでの意味で「私のもの」でなくなった瞬間です。

コードは私が選び、到達範囲は他の人が選びました。「作ること」と「使われること」の距離について、これ以上 教わったものはありません。そして「壊してよいもの」の基準が変わりました。

プロジェクトの年表。2018 年のフォークと書き直し、プラグインシステム、Kali と BlackArch への収録、そして 1.5 の指摘品質、1.6 の TUI ダッシュボード、1.6.1 の構造的な片付け。
真ん中のマーカーが働き方を変えたものです。その右側はすべて、自分で集めたのではなく引き継いだ読者に向けて書かれています。

イメージに入ると何が変わるか

pip install したユーザーは選択をしています。何かを読み、他と比べ、自分で選びました。フラグを壊しても、 その人は変更履歴を探し当てます。

渡されたマシンに入っていたから sitadel と打ったユーザーは、そんな選択をしていません。プロジェクトとの 関係も、リリースノートを読む理由もありません。そしてここが重要なのですが、その人はしばしば他人の authorised な案件で、時間に追われながら作業しています。

自分に課している実務上の帰結が 3 つあります。コマンドラインのフラグは契約であり、追加は安いが改名は 安くない。出力フォーマットは私が一生見ないスクリプトに消費される。そしてクラッシュは機能の欠落より悪い。 案件の途中でのクラッシュは、取り戻せない請求可能な時間を誰かから奪うからです。

実際に解いている問題

スキャナーの仕事は見つけることではありません。人が行動できるものを作ることです。

この区別を本当に飲み込むのに何年もかかりましたし、今出荷しているバージョンはそれを反映しています。 Sitadel はまずフィンガープリントを取り — サーバー、Web/フロントエンドフレームワーク、WAF、CDN、CMS、 言語 — そのあと、選ばれたリスクレベルが許す攻撃モジュールを、ブルートフォース・インジェクション・既知の 脆弱性・情報漏えいにわたって実行します。

ローカルのテストサーバーに対する Sitadel スキャンの端末出力。ASCII バナー、セキュリティヘッダー欠如とサーバー検出のフィンガープリント結果、実行された攻撃モジュールが表示されている。
使い捨てのローカルサーバーに対する実際の実行結果。リスクレベルは、どの攻撃モジュールがそもそも走ってよいかを制御します。

リスクレベルは便宜ではなく設計上の立場です。チェックの中にはノイズの大きいものがあり、稼働中のシステムに 対して本当に危険なものもあります。その区別を表現できないツールは、本番に向けられないツールです。

どのスキャナーでも最初にやる変更

検知の追加ではありません。指摘の質です。

左: エンドポイントごとに繰り返される「ヒット」。深刻度も証拠も修正手順もない。右: クラスとプラグイン、深刻度と確信度、証拠、CWE と標準へのマッピング、修正手順を伴う「指摘」。
検知カバレッジは人がベンチマークする部分。指摘の品質は、何かが実際に直るかを決める部分です。

以前のバージョンが出していたのは「ヒット」でした。同じインジェクションをエンドポイントごとに 1 回ずつ、 深刻度なし、証拠なし、次に何をすべきかの示唆なし。同じことを言う 47 行は 47 倍の価値ではありません。 1 件の指摘と、読者に押し付けた 46 単位のトリアージ作業です。

だから今は、指摘はグループへ重複排除され、深刻度と並んで確信度を持ち、それを証明した証拠 — ペイロードと レスポンス中のマーカー — を含み、CWE にマップされ、修正手順が付きます。レポートは JSON・HTML・SARIF で 出力でき、SARIF が特に重要なのは、CI パイプラインが既に読み方を知っている形式だからです。パイプラインが 理解する形式で届いた指摘はチケットになります。端末の散文で届いた指摘は、チャットのスクリーンショットに なります。

これはスキャナーを書いて学んだことではありません。10 年にわたって OWASP ASVS チェックリスト を チームに渡し、どの成果物が実際に作業を発生させるかを見て学びました。そのあとツールに書き込んだのです。

実際の端末でしか起きなかったバグ

バージョン 1.6 で、スキャンの進行を眺められるオプションの TUI を追加しました。クロールツリーが埋まり、 指摘がクラスごとにまとまり、攻撃フェーズでプログレスバーが動きます。

そして q を押したら、シェルがハングしました。

Before: q を押すと UI は閉じ、shutdown はスキャンワーカーを待ち、ワーカーは自分のスレッドプールを持ち、止める手段がなく、シェルがハングする。After: quit が共有キャンセルイベントを立て、クローラーはフロンティアを高速に空にし、ランナーはプローブ間とモジュール間で停止し、約 1 秒でほどけて部分レポートも書かれる。
TUI がこれを作ったのではありません。ずっとあった穴を表に出しただけです。それまで誰もスキャンを途中で止めようとしていなかったからです。

スキャンはワーカースレッドで走り、攻撃フェーズでは自分のスレッドプールを生成します。TUI フレームワークの shutdown は礼儀正しくそのワーカーを待ち — そしてコードベースのどこにも、スキャンに止まってくれと頼む 仕組みがありませんでした。終了コードはゼロ。テストスイートは緑。シェルはスキャンが自力で終わるまで 使えません。

修正は協調的キャンセルです。quit アクションが立てる共有イベントを、クローラーはフロンティアを消化しながら 確認し、攻撃ランナーはプローブ間とモジュール間で確認します。スキャンは約 1 秒でほどけ、レポートの finally は部分的な指摘を書き出します。80% でキャンセルした人は、その 80% を持ち帰るべきだからです。

ここから 2 つ持ち帰りました。1 つ、キャンセルはコードベースのあらゆる長時間ループが持つか持たないかの 性質であり、後付けよりはるかに安く設計に入れられます。2 つ、モックではなく実際の疑似端末で検証しました。 バグ全体が、端末とフレームワークの shutdown シーケンスとスレッドプールの相互作用の中に住んでいたからです。 モックなら通っていました。

誰も頼んでこない作業

最新リリースは新機能ゼロ・挙動変更ゼロの構造的な片付けです。永遠に後回しにしやすい種類の作業なので、 何が入っていて、なぜバージョン番号に値したのかを書きます。

ファイルハンドルのリーク。ワードリストがコンテキストマネージャなしで開かれ、閉じられていませんでした。 ブルートフォースの 2 モジュールにわたって。すべてのフィンガープリントプラグインがスキャンごとに 2 回 インスタンス化されていました。ランナーが登録用に 1 回、呼び出し用にもう 1 回オブジェクトを作っていたから です。何も読まないリクエストオブジェクトの死んだ属性。そしてインジェクション検知器が、レスポンスごと・ ペイロードごと・ターゲットごとに正規表現を再コンパイルしていました。SQL モジュールは 9 パターン、LDAP モジュールは 20 回の逐次検索を何度も。どちらも import 時に 1 回コンパイルできます。

どれもユーザーには見えません。そしてすべてが、8 年目でもまだ変更できるコードベースと、できないコード ベースの差です。

判断が要るのはどこか

8 年経って一番よく出てくる判断は、検知ロジックの話ではありません。

モジュールに No と言う。 プラグインシステムがあるのは、フォークせずにチェックを追加できるようにする ためです。つまりほとんどの機能要望に対する正しい答えは、コア変更ではなくプラグインです。コアにあるすべての モジュールは、私が動かし続けなければならないものです。

退屈な依存を選ぶ。 このツールは、私が一生見ないマシンに、私が制御しないパッケージマネージャで、私が 選んでいない Python バージョンでインストールされます。依存はすべて、再現できない環境で失敗する経路です。

次の修正を安くするものを直す。 これが 1.6.1 の正当化のすべてであり、成果を機能数で測っている相手に 最も説明しづらい主張です。

Python 3.11 以降、GPL-3.0、179 コミット、そして必要になればまだリリースします。ASVS チェックリスト も引き続きメンテナンスしており — 10 年経って、私が書いたもののうち最も使われている のは表計算シートです。プラットフォームを作りたくなったとき、思い出す価値のある事実です。