攻撃対象領域の見解を相互検証し、互いに裏付けが取れないエンドポイントを見つけ出します。
攻撃対象領域の複数のビューを相互照合し、互いに裏付けが取れないエンドポイントを見つけ出します。
エンドポイントはそれ自体を説明できるべきです。コードの中にあるなら、契約がそれを記述しているはずです。契約の中にあるなら、何かがそれを実装しているはずです。実際のトラフィックを受けているなら、どこかに存在しているはずです。あるビューがエンドポイントを認識しているのに、他のビューが認識していないとき、そのギャップこそが発見です。
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 が行わないのは、それらを比較することです。ここでの仕事はまさにそれであり、noir への変更は一切不要です — alibi はビューごとに一度ずつ noir を実行し、結果を結合します。
ビューごとに分けることが重要です。Noir はすべてのアナライザーにわたって (method, url) で重複排除するため、同一に綴られた Flask のルートと OpenAPI のパスは、1 つの技術を持つ 1 つのエンドポイントに統合されます。これは発見ツールとしては正しい挙動です — それは 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 にはそのすべてが含まれる。
noir 向けのフラグは、裸の -- の後ろに置くか、--noir-arg を通して指定する。--exclude-path のようなフィルタは問題ないが、JSON の契約を置き換えたり、alibi のビュー単位のスキャンを崩壊させたりするフラグ(--format、--diff-*、--only-techs など)は、終了ステータス 2 で拒否される。
- 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 | 片方のビューにしか存在しない — 何もマッチしなかった |
この種のツールは、初回実行で何百もの検出結果を報告したり、誰も進めていない進捗を報告したりすることで破綻します。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をマウントしています。ビューは何も共有していませんが、それら3つのセグメントを取り除くと、535の文書化されたパスのうち154がコードパスと一致します。それは仕様のbasePath、servers[].url、あるいはコードリーダーが取りこぼしたマウントであり、報告はされますが適用はされません。なぜなら、パスを再調整すると、それが見つけたバグが隠れてしまうからです。
本当は1つの欠落したサブツリーにすぎない大量の検出は、1つとして名付けられます: NodeBBの354のファントムコントラクトのうち207は/api/v3配下にあり、そこではコードビューが何も保持していません。
ビューの欠落と空のビューは正反対の意味を持ちます。 noirは読み取れなかったものを報告し、alibiはそれを検出結果の上に出力します。NetBoxは308のパスを持つ12.35MBのOpenAPIドキュメントを同梱していますが、noirはファイルサイズ上限を超えるためそれをスキップします。その報告がなければ、alibiはプロジェクトが何も文書化していないと述べます — 単に不完全なだけでなく、自信を持って間違った答えを述べることになります。
実行を停止したルールは何も解決していません。 スキャンを記録して比較することは、距離を置いて同じ過ちを再導入します: ある実行でコントラクトディレクトリを忘れるとSHADOWは何も評価せず、素朴な差分にはそれがすべてのシャドウAPIが閉じられたのと全く同じに見えます。5ビューのフィクスチャでは、1つの引数を落とすと7つの継続的な検出結果が「解決済み」に変わりました。スナップショットはどのルールが評価されたかを記録し、差分は両方のスキャンで実行されたルールのみを考慮し、残りはNOT COMPAREDの下に明記されます。
不在は、シグナルが存在する場合にのみ証拠となる。 Noirの認証タガーは それが知っているフレームワークをカバーする。それがカバーしないスタックでは、何も認証タグを持たず、 それを「未認証」として扱うと、すべての検出結果が昇格され、 重大度の列が意味を失う。欠落したタグに基づいて発火する調整は、 そのタグがまずスキャンのどこかに現れることを要求する。
| ルール | 条件 | 重大度 |
|---|---|---|
ORPHAN | 実際のリクエストを受けているが、コードに存在しない | high |
LIVE_UNDOC | 実際のリクエストを受けているが、どのコントラクトにも記述されていない | high |
SHADOW | コードにあるが、どのコントラクトにもない | medium |
DANGLING | 実装された何にも到達しないゲートウェイルール | medium |
DRIFT | デプロイ用に宣言されているが、コードに欠落している | medium |
PHANTOM | コントラクトにあるが、コードにない | low |
UNEXPOSED | 実装されているが、どのゲートウェイルールも到達しない | low |
COLD | 実装されているが、リクエストを受けているのを一度も見たことがない | info |
重大度はその後、noirのタガーが見つけたもの——個人データ、ファイルアップロード、 認証の兆候なし、または状態を変更するメソッド——に応じて変化する。
ビューマップ(views.yml)とルール(rules.yml)はどちらもコードではなくデータである。
1つの location /api/ はその下にあるすべてを表すため、ゲートウェイと
インフラストラクチャのルールはこれがそのエンドポイントに到達するかに答えるのであって、これがそれを含むかではない。
集合として比較すると、すべてのプレフィックスルールは誰も実装していないルートのように見え、
すべての実装されたルートは到達不能に見える。
カバレッジは意図的に寛容である。Noirはルールが一致するパスを報告するが、
それがプレフィックスとして一致するのか正確に一致するのか(location = /x、Ingressの
pathType: Exact)は報告しないため、正確性を復元することはできない——そしてすべてのルールを
プレフィックスとして扱うことは、検出結果を捏造するのではなく抑制する。
レポートは、各ルーティングビューがコードのどれだけに到達するかを示す。なぜなら 「ゲートウェイが到達しない34のエンドポイント」が現実かどうかは、その設定が サービスをフロントしているものかどうかに依存するからである。それを正直に分けるしきい値は存在しない。 Argo CDのe2eテストフィクスチャはコードの39%に到達し、NetBoxの実際の設定は100%に到達する。