Regoによるソースコード位置の自動特定
2024年2月12日
0 分で読めますSnykはOpen Policy AgentのRegoを愛用しています。Snyk IaCはRegoで記述された豊富なルールを基盤としており、お客様も独自のカスタムルールを追加できます。
Snyk IaCに一連の機能改善をリリースしました。このブログでは、特に興味深い機能である、ルール違反のソースコード位置を自動で特定する仕組みについて技術的に掘り下げます。
既知の問題に照らしてIaCファイルをチェックすると、更新された `snyk iac test` コマンドは、各ルール違反について正確なファイル名、行番号、列番号を表示します。カスタムルールでも、ユーザー側で何も作業することなく機能します。

ただし、この手法の単独の概念実証を示す前に、いくつか簡略化する必要があります。完全な実装は、当社の統合ポリシーエンジンでご覧いただけます。
まずはCloudFormationの例を見てみましょう。当社のIaCエンジンは多くの形式に対応し、特にTerraformを重視していますが、依存関係があまり多くなくても解析できるため、CloudFormationはよい例です(結局のところ、単なるYAMLです)。
サブネットで `/24` より大きなCIDRブロックを使わないようにするため、次のようなRegoポリシーを記述します。
この方法では、`deny` によって拒否されたリソースのセットが返されます。Regoの仕組みの詳細には踏み込みませんが、詳しく学びたい方には、優れたOPA by Exampleコースをおすすめします。
この問題は、次の2つの部分に分けられます。
ポリシーが `CidrBlock` 属性を使用していることを推論する
ソースコードの位置を取得する
コードに慣れるためにも、まずは(2)から始めましょう。
ソース位置の取得
ソース位置は次のような形式です。
YAML内のパスを表す補助型も導入します。YAMLの入れ子になったドキュメントには、配列とオブジェクトの2種類があります。
任意のサブドキュメントを参照するには、JSON Pathに似たものを使えます。上の例では、[`"some_array", 1`] が `"word"` を指します。ただし、概念実証では配列に対応しないため、文字列の配列だけで十分です。
パスの例は次のようになります。
これで、YAMLを読み込み、指定した `Paths` の `Location` を取得する便利な型を用意できます。
`Path` のソース位置を見つけるには、YAMLノードのツリーをたどります。
パスのセットとツリー
これで、ポリシー内で使われるソース位置を自動的に推論する問題を、属性パスを自動的に推論する問題へと置き換えられました。
これは、ほかの理由からも重要です。たとえばSnykは、同じポリシーをIaCリソースとクラウドスキャンで検出したリソースの両方に適用できます。後者には意味のあるソース位置はありませんが、意味のある属性パスはあります。
属性パスのセットを定義したいところです。パスは配列に基づいているため、残念ながらGoでセットとして `map[Path]struct{}` のようなものは使えません。
代わりに、これらを再帰的なツリーに格納する必要があります。
この表現にはほかの利点もあります。一般に、ポリシーが使うパスのうち、より長いものだけを対象にします。より具体的だからです。例のポリシーは `Path{"Resources", "PrivateSubnet", "Properties"}` と `Path{"Resources", "PrivateSubnet", "Properties", "CidrBlock"}` の両方を使っていますが、対象にするのは後者だけです。
ツリーに `Path` を挿入する再帰メソッドを定義します。
また、Pathsの一覧を取り出す方法も用意します。少し余分なメモリ割り当てが発生しますが、許容できます。
これで、ポリシーが使った `Paths` を適切に格納し、それらをソース位置に変換する方法が用意できました。
静的解析と実行時解析
次に、与えられた入力のどの `Paths` がポリシーで使われているかを特定し、それらをツリーに `Insert` する必要があります。
これは簡単な問題ではありません。パスを使う前に、コードが入力をさまざまな方法で処理する可能性があるからです。障害となり得る例を示すため、ユーザー定義関数( `has_bad_subnet`)だけでなく、組み込み関数( `object.get`)も調べる必要があります。
幸い、この問題に取り組むのは私たちだけではありません。最初のプログラムが書かれて以来、人々はプログラムの動作に関心を持ってきました。コードについてこのような問いに答える方法は、一般に2つあります。
静的解析: 構文木、型、その他OPAインタープリターから取得(または追加)できる静的情報を調べて答えを導きます。ポリシーを実行せずに済むため、ポリシーの作成者を信頼できない場合にも有効です。一方、静的解析手法では、偽陰性や偽陽性が生じることも少なくありません。
実行時解析: 特定のポリシーの実行を追跡し、実行時情報を調べて、使われている `Paths` を推論します。欠点は、実際にポリシーを実行する必要があることと、この解析の追加によりポリシーの評価が遅くなる可能性があることです。
両方の方法を試しましたが、より確実に実装しやすく、パフォーマンスへのオーバーヘッドも無視できる程度だったため、後者を採用しました。また、どちらか一方を選ぶ必要はありません。両者を組み合わせたハイブリッド方式も可能です。
OPAには、インタープリターの動作に関するイベントを受け取るためのTracerインターフェースがあります。トレーサーは、メトリクスやデバッグ情報を一元管理されたログに送信する用途でよく使われます。しかし今回は、別の目的に利用します。
項の使用状況をトレースする
Regoは表現力の高い言語です。インタープリター向けに単純な形式へ変換する処理が一部行われますが、それでもかなりの数のイベントが発生します。
そのうち対象とするのは2つだけです。次の場合、値が使用されたとみなします。
1. 別の式に対して統合される(代入と考えることができます。詳しくは説明しません)。たとえば、次のような場合です。
これは `==` と `:=` も対象にします。失敗する可能性のあるテストなので、左辺と右辺の両方を使用したと判断できます。
2. 次のように、組み込み関数の引数として使用される。
Regoはいくつかの概念を遅延評価型言語から取り入れていますが、組み込み関数の引数は、関数の呼び出し前に必ず完全に具体化されます。そのため、組み込み関数に渡された引数はすべて使用されたと言えます。
3. 次のように、単独の式として使用される。
これは、真偽値の評価や属性の存在確認によく使われます。
では、実装に移りましょう。2つのイベントを照合し、コードを読みやすくするために、処理を個別の関数に委譲します。
`PathTree` への挿入処理は、後ほど補助関数 `used(*ast.Term)` で扱います。まずは、統合処理の左辺と右辺を使用済みとしてマークします。
`event.Plug` は、変数を実際の値で埋めるためのヘルパーです。
`EvalOp` イベントは、前述の(2)と(3)の両方を対象とします。組み込み関数の場合、項の配列があり、最初の要素が関数、残りの要素が引数です。`ast.BuiltinMap` を調べることで、組み込み関数かどうかを確認できます。
単独の式の場合は簡単です。
項に注釈を付ける
`used(*ast.Term)` を実装する際、次の疑問が生じます。項が与えられたとき、入力内の `Path` にどう対応付ければよいでしょうか。
入力ドキュメント内で一致する項を検索する方法もあります。しかし、`10.0.0.0/24` のような文字列は入力内に何度も現れる可能性があり、偽陽性が大量に発生します。
代わりに、すべての項にそのパスを注釈として付けます。OPAの項にはメタデータを含めることができ、その中にはRegoのソースファイル内の位置も含まれます。このフィールドに入力の `Path` を格納できます。少々裏技的ではありますが、このフィールドは位置を格納するためのものなので、十分に解釈を広げれば本来の用途から大きく外れてはいません。
次のスニペットは、CloudFormationテンプレートの最初の数行に注釈を付ける方法を示しています。
`annotate` は再帰的に走査し、値の各ノードにおける `Path` を特定します。簡潔にするため、オブジェクトのみをサポートし、セットと配列は対象外とします。
この注釈があれば、`used(*ast.Term)` を簡単に記述できます。覚えておくべきなのは、すべての値に注釈が付くわけではないことです。注釈を付けるのは入力ドキュメントに由来する値だけで、たとえばRegoソースコードに埋め込まれたリテラルには付けません。
まとめ
以上です。配列や、HCLのようなより複雑なIaC言語に適用する方法など、多くの詳細は省略しました。
さらに、ポリシー内で確認する `Type` 属性も、使用済みとしてマークしています。これは理想的ではないため、代替案としてリソース指向のRego APIを提供するよう取り組んでいます。ただし、この例の範囲を超えるため、ここでは詳しく説明しません。
これらの機能について詳しく知りたい方は、コア実装のsnyk/policy-engine、または更新されたSnyk IaCをご覧ください。Snyk IaCには、この機能に加えて、網羅的なルールバンドルなど、さまざまな機能が含まれています。
最後に、すべてをまとめてデバッグ情報を出力するメイン関数を示します。これまでに定義した基本要素をほぼそのまままとめ、例に対して実行するだけです。ただし、この記事を再現可能な単独の例として機能させるため、掲載しておきます。
この概念実証の全コードはこちらのgistでご覧いただけます。
開発者のために設計されたIaCセキュリティ
Snykは、統合されたポリシー・アズ・コードエンジンにより、SDLCからクラウドでの実行時までInfrastructure as Codeを保護します。すべてのチームが安全に開発、デプロイ、運用できるよう支援します。
