攻撃対象面の見え方を相互検証し、互いに裏付けの取れないエンドポイントを見つけ出す。
エンドポイントは自己を説明できるべきである。コードに存在するなら、契約がそれを記述しているはずだ。契約に存在するなら、何かがそれを実装しているはずだ。実際のトラフィックを受けているなら、どこかに存在しているはずだ。ある見え方がエンドポイントを認識しているのに、他の見え方が認識していないとき、そのギャップが発見事項である。
alibi は OWASP noir を実行し、その JSON を読み、各見え方を相互に比較する。
Noir はすでに同じ対象面の5つの独立した見え方を読み取る:
| 見え方 | 読み取り元 |
|---|---|
| code | 33言語にわたる200以上のアナライザー |
| doc | OpenAPI、RAML、WSDL、GraphQL SDL、AsyncAPI、gRPC、Smithy、TypeSpec、OData、OpenRPC |
| traffic | HAR、mitmproxy、Burp、Caido、ZAP、Postman、Insomnia、Bruno、.http |
| gateway | nginx、Apache、Envoy、Kong、Traefik、APISIX、Caddy、Istio、Kubernetes Ingress および Gateway API |
| infra | Terraform、CloudFormation、CDK、Serverless、Vercel、Netlify、Wrangler、Azure Functions、Kamal |
それがやらないのは、それらを比較することだ。ここでの仕事はまさにそれであり、noir への変更は一切不要である — alibi は見え方ごとに一度実行し、結果を結合する。
見え方ごとの部分が重要である。Noir はすべてのアナライザーにわたって (method, url) で重複排除するため、同一に綴られた Flask ルートと OpenAPI パスは、1つの技術を持つ1つのエンドポイントに collapse する。これは発見ツールとしては正しい — それは1つのエンドポイントである — が、このツールが測定するために作られた裏付けを消し去り、しかも最悪の方向に消し去る: 2つの見え方が一致するほど、より多くが消えるのだ。Casdoor は372のコードエンドポイントと9の文書化されたエンドポイントとしてスキャンされる; その swagger/ ディレクトリだけをスキャンすると、仕様には235ある。
--only-techs は検出器プールを制限するので、見え方ごとに1回のスキャンでそれぞれを完全に保つことができる。どの技術がどの見え方を代表するかは views.yml であり、どの技術が存在するかは noir list techs が報告するものである。
alibi は独自の API フォーマットを一切パースしない。その唯一の入力は noir の JSON である。
PATH 上に noir 1.0.0 以降が必要である — これは noir list techs がサブコマンドになったリリースであり、そのカタログがすべての技術をある見え方に割り当てるものである。開発は現在の noir リリースを追跡している。古いバイナリは、最初のカタログ読み取りで失敗させるのではなく、名前を挙げて拒否される。
$ uv tool install noir-alibi # or: pipx install noir-alibi
$ alibi scan ./my-service
$ alibi scan # the working directory
$ alibi scan ./service ./contracts ./prod.har # or wherever the views live
すべてのパスはソースであり、見え方ごとに一度スキャンされる。手持ちのもの — ソースツリー、仕様ディレクトリ、単一のキャプチャファイル — を指定すれば、欠けている見え方はレポートを溢れさせるのではなく、そのルールをオフにする。
alibi · 1 source · 377 endpoints
code 372 doc 235
230 corroborated -- vouched for by more than one view
19 endpoints nearly matched another view -- these may be matching failures, not real gaps
SHADOW Shadow API -- Implemented, but no contract describes it
134 findings · 4 critical, 57 high, 62 medium, 11 low
critical POST /api/upload-groups router.go:87
upload paths carry more consequence than reads
critical POST /api/upload-permissions router.go:208
...
... and 122 more (SHADOW in full: -f json)
TWO SURFACES?
The doc view is 97% under /api, and 37 of these findings are outside it.
If that is a separate surface the contract never covered, narrow the scan:
alibi scan <paths> --ignore '^/(?!api(/|$))'
If it is the same surface left undocumented, they are the findings that matter most.
グループは12で止まる — 順序は最悪が先なので、末尾は最も情報量の少ない部分であり、-f json にはすべてが含まれる。
- run: alibi scan . ./contracts -f sarif > alibi.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: alibi.sarif }
レポートは見え方が不一致だと述べる; --endpoints はそれぞれが何を含んでいたかを述べる。
$ alibi scan ./repo -f json --endpoints
すべての見え方がリストを得る: キー、それを裏付けた見え方、その背後の技術、ファイル、そして正規化前の綴り — 2つの行が一致すべきなのに一致しなかったとき、違いは常にそこにある。これはペイロードの残りの3〜4倍の大きさなので、デフォルトではなくフラグである。
あるいは直接ゲートする: alibi scan . ./contracts --fail-on high は、発見事項がその重大度に達したときに非ゼロで終了する。noir が完全に読み取れなかったスキャンは executionSuccessful: false を報告するので、劣化した実行がクリーンなものとして通ることはない。
Noir は共通のものを発明するのではなく、各フレームワーク自身のルート構文を保持するため、同じエンドポイントがいくつかの綴りで到着する:
python_flask /api/users/<int:user_id>
aiohttp /users/{id}
java_spring /api/catalog/{id}
oas3 /v1/pets/{petId}
rails /posts/:id
nginx /admin/.*
これらを比較可能にするルール: パスパラメータの名前はその同一性の一部ではない。 {petId} と <int:user_id> は同じスロットを記述する; 重要なのはその位置と、/ にまたがるかどうかだけである。名前は証拠として保持され報告されるが、キーには決して到達しない。
発見事項はマッチがどのように行われたかを述べる:
| グレード | 意味 |
|---|---|
G1 | 綴りがすでに一致していた |
G2 | パラメータ構文が正規化されると一致する |
G0 | 1つの見え方だけが持っている — 何もマッチしなかった |
このようなツールは、初回実行で何百もの発見事項を報告するか、誰も達成していない進捗を報告することで死ぬ。6つのことが反論する:
両方の見え方がなければルールは発火しない。 どこにも契約がないコードベースをスキャンすると、すべてのエンドポイントが技術的には文書化されていないシャドウ API に該当する。それらの発見事項は、あなたが文書を何も提供しなかったこと以外何も述べていないので、ルールはそれが推論するすべての見え方が実際にスキャンに含まれていたときにのみ実行される。レポートは参加しなかったルールを名指しする。
ニアミスは発見事項ではなく疑いとして報告される。 「コードにあるが、ドキュメントにない」は「両方にあるが、alibi がそれらを並べるのに失敗した」と区別がつかない。そこで、ある見え方に落ちたエンドポイントは、他の見え方に対してニアミスがないかチェックされる — 異なる動詞を持つ同じパス、または一方がパラメータで他方がリテラルである1セグメント違い。ニアミスを伴う発見事項は降格され、レビューのためにフラグが立てられる。その数は合計の隣に位置する、なぜならすべての発見事項はそれが小さいほど信頼できるからである。
決して出会わなかった見え方は、何百もの発見事項ではなく1つの診断である。 Argo CD は Go で /api を登録し、その下に198のパスを文書化しているので、そのコードとその仕様は1つのエンドポイントも共有していない。文字通りに読めば、それは58のシャドウ API と198のファントム契約であり、どれも本物ではない。2つの populated な見え方の間のゼロの裏付けは、比較が機能しなかったことを意味する — その下のルートの代わりに立つマウントポイント、または noir が読み取れなかったスタック — なので、ルールは保留され、代わりに理由が出力される。他の見え方からの多くのエンドポイントがその下にあることが判明したパスは、推定マウントとしてラベル付けされる。
2つの見え方が、一方から定数プレフィックスを取り除くと整列するとき、診断はそう述べ、そのプレフィックスを名指しする。Gitea の生成された仕様は basePath: /GITEA-API-APP-SUBURL/api/v1 を宣言する一方、その Go ルーターは /api/v1 をマウントする; 見え方は何も共有しないが、535の文書化されたパスのうち154が、それら3つのセグメントを取り除くとコードパスに一致する。それは仕様の basePath、servers[].url、またはコードリーダーが落としたマウントである — そしてそれは報告され、決して適用されない、なぜならパスを再整列させるとそれが見つけたバグを隠してしまうからである。
本当に1つの欠落したサブツリーである flood は、1つとして名指しされる: NodeBB の354のファントム契約のうち207が /api/v3 の下にあり、そこではコードの見え方は何も保持していない。
欠落した見え方と空の見え方は反対のことを意味する。 Noir は読み取れなかったものを報告し、alibi はそれを発見事項の上に出力する。NetBox は308のパスを持つ12.35MBの OpenAPI ドキュメントを同梱している; noir はファイルサイズ上限を超えたためそれをスキップし、その報告がなければ alibi はプロジェクトが何も文書化していないと述べる — 単に不完全なだけでなく、自信を持って述べられた誤った答えである。
実行を止めたルールは何も解決していない。 スキャンを記録して比較することは、距離を置いて同じ過ちを再導入する: ある実行で contracts ディレクトリを忘れると SHADOW は何も評価しない、それは素朴な差分にはすべてのシャドウ API が閉じられたのと全く同じに見える。5つの見え方のフィクスチャで、1つの引数を落とすと7つの standing な発見事項が「解決済み」になった。スナップショットはどのルールが評価されたかを記録し、差分は両方のスキャンで実行されたルールのみを考慮し、残りは NOT COMPARED の下に名指しされる。
不在は、シグナルが存在するときにのみ証拠となる。 Noir の auth タガーは、それが知っているフレームワークをカバーする。それがカバーしないスタックでは、何も auth タグを持たず、それを「未認証」として扱うとすべての発見事項を昇格させ、重大度の列から意味を排出してしまう。欠落したタグで発火する調整は、そのタグがまずスキャンのどこかに現れることを要求する。
重大度はその後、noir のタガーが見つけたものに応じて変動する: 個人データ、ファイルアップロード、認証の兆候なし、または状態を変更するメソッド。
見え方マップ (views.yml) とルール (rules.yml) はどちらもコードではなくデータである。
1つの location /api/ はその下のすべてを代表するので、ゲートウェイとインフラのルールは これがそのエンドポイントに到達するか に答え、これがそれを含むか には答えない。集合として比較すると、すべてのプレフィックスルールは誰も実装していないルートに見え、すべての実装されたルートは到達不能に見える。
カバレッジは意図的に寛容である。Noir はルールがマッチするパスを報告するが、それがプレフィックスとしてマッチするのか正確にマッチするのか (location = /x、Ingress の pathType: Exact) は報告しないので、正確性は回復できない — そしてすべてのルールをプレフィックスとして扱うことは、発見事項を捏造するのではなく抑制する。
レポートは各ルーティング見え方がコードのどれだけに到達するかを述べる、なぜなら「どのゲートウェイも到達しない34のエンドポイント」が本物かどうかは、その設定がサービスを front しているものかどうかに依存するからである。それを誠実に分ける閾値は存在しない: Argo CD の e2e テストフィクスチャはそのコードの39%に到達し、NetBox の実際の設定は100%に到達する。
catch-all — location /、/ の Ingress、RewriteRule ^(.*)$ — はどちらの証拠でもない。それはすべてをルーティングするか何もルーティングしないかで、すべてのエンドポイントに対して同じなので、それらのどれにも到達しないものとして数えられる。他に何も保持しないゲートウェイの見え方は提供できるシグナルを持たず、UNEXPOSED は参加を控えてそう述べる、すべてのエンドポイントを到達不能として報告するのではなく。Casdoor の Helm チャートはまさにそれである: / の1つの Ingress ルールで、証拠として読むと365の発見事項を生み出した。
HAR キャプチャは発生したリクエストを記録する。Postman コレクションは誰かが行おうとしたリクエストを記録する。ORPHAN、LIVE_UNDOC、COLD はすべて何が実行されたかを推論するので、誰かが実際に監視した見え方を要求し、参加を控えるときはそう述べる。
Noir は CLI 引数、Kafka トピック、モバイルディープリンクを同じリストで報告する。cli://gitops-engine/agent を HTTP に平坦化すると /agent になり — その名前の任意の Web ルートと衝突し、ゲートウェイがそこにルーティングするかどうかを問われる。プロトコルはエンドポイントの同一性に属する; http と https は1つの空間であり、それ以外はすべて独自のものを保つ。
一部のギャップは意図された状態である。ソースの隣に .alibi.yml を置く:
ignore:
- path: "^/internal/"
why: internal-only admin surface
- rule: UNEXPOSED
path: "^/debug/"
why: not fronted by the gateway in this repo
または一度限りなら --ignore REGEX を渡す。抑制された発見事項はカウントされ、その数が出力される — 発見事項を静かに落とすツールは、多すぎるものを出力するツールよりも悪い、なぜなら何を保留したかを知る方法がもはやないからである。
初期段階だが、5つの見え方すべてが比較されている。
5つのリポジトリに対して測定:
Casdoor は最もクリーンなケースである: その235の文書化されたエンドポイントのうち230がコードに一致し、パス正規化の失敗は一切なかった。 19のニアミスはすべて異なる動詞の下の同じパスだった — noir が Go の catch-all ハンドラーですべてのメソッドを登録しているのであり、マッチングの問題ではない。
NetBox は教訓的なものである。それは1つのリポジトリに2つの対象面を保持する: サーバーレンダリングの Web UI と、2番目のものだけが文書化されている DRF ルーター REST API。全体をスキャンすると746の発見事項を報告し、そのほとんどは Web UI が API 仕様にないという真実だが役に立たない観察である。契約が記述する対象面にスコープを絞ると、実際に言う価値のあるものに collapse する:
$ alibi scan ./netbox --ignore '^/(?!api(/|$))'
→ 3つのシャドウ API: /api/plugins、/api/schema/redoc、/api/schema/swagger-ui、3つとも本当に提供され、本当にスキーマに存在しない。残る397のファントムは、NetBox 自身のルーターサブクラスがすべてのリストエンドポイントに追加する一括操作であり、どの urlconf ウォークも見ることができない。
他の3つは保留されている、それぞれ知る価値のある理由で:
/api を登録し、その下に198のパスを文書化している — 2つの粒度での同じ対象面。urls モジュールをインポートすることで実行時に URLconf を組み立てる、それはどの静的リーダーも追跡できない。.proto アノテーションで実装されており、それが grpc がコードの見え方を代表する理由である: そこにファイルされると、その36の文書化されたパスのうち36が裏付けられる。これがこのツールの天井であり、率直に述べると: それは noir が読み取れるものを比較し、間違った粒度で読まれた見え方は全く読まれないものよりも悪い。上記の機構のほとんどは、それらを欠陥として報告するのではなく、それらを見分けるために存在する。
MIT
| ルール | 条件 | 重大度 |
|---|
ORPHAN | 実際のリクエストを受けているが、コードに存在しない | high |
LIVE_UNDOC | 実際のリクエストを受けているが、どの契約にも記述されていない | high |
SHADOW | コードにあるが、どの契約にもない | medium |
DANGLING | 実装された何にも到達しないゲートウェイルール | medium |
DRIFT | デプロイのために宣言されているが、コードに欠けている | medium |
PHANTOM | 契約にあるが、コードにない | low |
UNEXPOSED | 実装されているが、どのゲートウェイルールも到達しない | low |
COLD | 実装されているが、リクエストを受けているのを一度も見られていない | info |
| リポジトリ | code | doc | corroborated | findings | code↔doc |
|---|
| casdoor | 372 | 235 | 230 (98%) | 139 | compared |
| netbox | 1146 | 1193 | 796 (67%) | 746 | compared |
| argo-cd | 59 | 198 | 1 | 31 | held back |
| authentik | 231 | 1193 | 1 | 192 | held back |
| flipt | 2 | 42 | 0 | 0 | held back |