攻撃対象領域の見解を相互検証し、互いに裏付けが取れないエンドポイントを見つけ出します。
攻撃サーフェスのビューを相互照合し、互いを裏付けられないエンドポイントを見つけ出します。
エンドポイントはそれ自体を説明できるべきです。コードにあるなら、契約がそれを記述しているはずです。契約にあるなら、何かがそれを実装しているはずです。実際のトラフィックを受けているなら、どこかに存在しているはずです。あるビューがエンドポイントを認識しているのに、他のビューが認識していないとき、そのギャップが発見です。
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
すべてのパスはソースであり、ビューごとに 1 回スキャンされます。手持ちのもの — ソースツリー、仕様ディレクトリ、単一のキャプチャファイル — を指定すれば、欠けているビューはレポートを溢れさせるのではなく、そのルールをオフにします。
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 のビューごとのスキャンを collapse させたりするフラグ (--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 | 1 つのビューだけが持っている — 何もマッチしなかった |
このようなツールは、初回実行で何百もの発見を報告することで死ぬか、誰も成し遂げなかった進歩を報告することで死にます。6 つのことが反論します:
両方のビューがなければルールは発火しません。 どこにも契約がないコードベースをスキャンすると、すべてのエンドポイントが技術的には未文書化のシャドウ API に該当します。それらの発見は、あなたが文書を提供しなかったということ以外何も言わないので、ルールはそれが推論するすべてのビューが実際にスキャンに含まれていたときにのみ実行されます。レポートは参加しなかったルールを名指しします。
ニアミスは発見ではなく疑いとして報告されます。 「コードにあり、ドキュメントにない」は「両方にあるが、alibi がそれらを並べるのに失敗した」と区別がつきません。そこで、1 つのビューに落ちたエンドポイントは、ニアミスがないか他のビューと照合されます — 同じパスで異なる動詞、または一方がパラメータで他方がリテラルである 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 つの欠落したサブツリーである洪水は、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 タグを持たず、それを「未認証」として扱うと、すべての発見を昇格させ、重大度の列を意味から排水してしまいます。欠落したタグで発火する調整は、そのタグがスキャン内のどこかに最初に現れることを要求します。
| ルール | 条件 | 重大度 |
|---|---|---|
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 のエンドポイントにどのゲートウェイも到達しない」が本物かどうかは、その設定がサービスを front しているものかどうかに依存するからです。それを正直に分ける閾値はありません: Argo CD の e2e テストフィクスチャはそのコードの 39% に到達し、NetBox の実際の設定は 100% に到達します。
catch-all — location /、/ の Ingress、RewriteRule ^(.*)$ — はどちらかの証拠ではありません。それはすべてをルーティングするか何もルーティングしないかで、すべてのエンドポイントに対して同じなので、どれにも到達しないとして数えられます。他に何も保持しないゲートウェイビューには提供できるシグナルがなく、UNEXPOSED は参加を控えてそう言い、すべてのエンドポイントを到達不能として報告することはありません。Casdoor の Helm チャートはまさにそれです: / の 1 つの Ingress ルールで、証拠として読むと 365 の発見を生み出しました。