Skip to main content

Open Policy Agent(OPA)のRego言語を使う5つのヒント

header cloud security

2020年9月18日

0 分で読めます

FugueではOpen Policy Agent(OPA)をとても気に入っており、クラウドリソースを安全に保つために、これまで多くのRegoコードを書いてきました。そこで、取り組みを通じて得た特に役立つ知見をまとめました。OPAとRegoを使ってポリシーをコードとして実装すれば、コード化されたポリシーを自動的に適用することもできます。

1. ド・モルガンの法則は頼れる味方

このリストを1つだけに絞るなら、これを選びます。ド・モルガンの法則は、2つの変換規則です。Regoに適用すると、動作をまったく変えずにルールを変換できます。

一般に、次のように表されます。

¬(A ∧ B) ⇔ ¬A ∨ ¬B
¬(A ∨ B) ⇔ ¬A ∧ ¬B

コードで表すと、次のとおりです。

!(a && b) <=> !a || !b
!(a || b) <=> !a && !b

例を挙げると、さらに分かりやすいでしょう。次の2つの文は同じ意味で、1つ目の法則を適用した例です。

  • これは、ハムとマッシュルームがのっていないピザです。

  • このピザには、ハムがのっていない、またはマッシュルームがのっていません。

この法則を使えば、コードの動作をまったく変えずに、AND節をOR節に、またはその逆に変換できます。「コードの動作が変わらないのなら、なぜこの変換が役立つの?」と思うかもしれません。

重要なのは、クエリ言語であるRegoは、非常に選言(OR文)を中心に構成されているという点です。たとえば、グループの中にピザを切る資格がある人がいるかどうかは、次のように確認できます。

default allow = false

allow {
  input.people[_].profession == "mathematician"
}

では、グループの全員にピザを切る資格があるかどうかは、どう確認すればよいでしょうか?まず、資格のある人たちのグループを作って、その人数を調べる……という方法もあります。

qualified_pizza_cutters[name] = person {
  person = input.people[name]
  person.profession == "mathematician"
}

default allow = false

allow {
  count(qualified_pizza_cutters) == count(input.people)
}

これはあまりよい方法ではありません。読みにくいだけでなく、こうした集合を数える必要があるため、クエリプランナーによる最適化も難しくなります。グループに未成年者が1人でもいれば、その時点で処理を打ち切れる場合でも同じです。

ここでド・モルガンの法則が役立ちます。いくつかの部分を否定すれば、クエリを常に選言として記述できるのです。元の文を否定する補助文を追加します。

cannot_cut_pizza {
  input.people[_].profession != "mathematician"
}

あとはポリシーを簡単に記述できます。

default allow = false

allow {
  not cannot_cut_pizza
}

これで完成です!効率的で読みやすくなりました。

まとめ:Regoポリシーの記述が難しいと感じたら、まず否定したポリシーのほうが簡単に書けないか考え、それをもう一度否定してみましょう。論理ってすばらしいですね!

2. or、それともany?

先ほど見たように、OR文を先頭に置き、ルール本文の中でAND文を順に並べると、Regoは読み書きしやすくなります。しかしプログラマーなら、慣用的な書き方がうまく機能しないケースに出くわすまで、それほど時間はかかりません。

Regoでは、ルール本文にすでに多くのクエリが含まれている場合によく起こります。たとえば、次のような場合です。

probably_a_pizza {
  input.shape == 'circle'
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
}

ピザのスライスも許可したい場合はどうすればよいでしょうか?ルール本文全体を複製する方法があります。

probably_a_pizza {
  input.shape == 'circle'
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
} {
  input.shape == 'circular_sector'
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
}

これはあまりよくありません。多くの場合、補助ルールを追加できます。

has_a_pizza_shape {
  input.shape == 'circle'
} {
  input.shape == 'circular_sector'
}

probably_a_pizza {
  has_a_pizza_shape
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
}

このケースではうまくいきます。しかし、ときには補助ルールが増えすぎて、適切な名前を付けることさえ難しくなることがあります。そんなまれなケースでは、RegoにanyというOR文があるかのように書けます。

probably_a_pizza {
  any([input.shape == 'circle', input.shape == 'circular_sector'])
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
}

この書き方なら、十分読みやすくなります。anyを使いすぎないようにすれば、作業がずっと楽になります。

3. anyとallの内包表記

同じ考え方をさらに進めると、anyとallは、特にリスト内包表記と組み合わせると、さまざまな場面で非常に役立ちます。この2つの機能を組み合わせると、単独で使う以上の効果が得られます。

最初の例に戻ってみましょう。allをリスト内包表記と組み合わせると、同じポリシーを次のように表せます。

allow {
  all([person.profession == "mathematician" | person = input.people[_]])
}

ド・モルガンの法則に従って作ったものと同じくらい、読みやすいと思います。どちらがより慣用的かは、通常、文脈によって変わります。

  1. 否定したポリシーのほうが考えやすいでしょうか?その場合は、ド・モルガンの変換を使います。

  2. それとも、取り締まりの対象をリストや集合として考えるほうが簡単でしょうか?その場合はリスト内包表記が適しています。

4. walkでドキュメントツリー全体をたどる

Regoのクエリは必ず終了します。これは優れた特性ですが、その代わり、一般に再帰ルールを記述することはできません。

再帰ルールが必要なのはなぜでしょうか?深くネストされたツリーのような、再帰的な入力ドキュメントを扱うのに非常に便利だからです。幸い、OPAにはこれを助ける組み込み関数があります。

たとえば、ディスカッションスレッドを監視するポリシーを作成するとします。

[
  {
    "author": "Alice",
    "message": "I think New York is at least as good as Italy for pizza",
    "replies": [
      {
        "author": "Bob",
        "message": "It's because of the water!",
        "replies": [
          {
            "Author": "Alice",
            "message": "I've heard that before but I'm not convinced.",
            "replies": []
          }
        ]
      },
    ]
  },
  {
    "author": "Charlie",
    "message": "You are a horrible person.",
    "replies": []
  }
]

Regoでメッセージを取得するにはどうすればよいでしょうか?次のように書く方法があります。

messages[message] {
  message = input[_].message
} {
  message = input[_].replies[_].message
}
  message = input[_].replies[_].replies[_].message
}

ネストの深さが有限ならこれで問題ありませんが、常にそうだとは限りませんし、あまり読みやすい書き方でもありません。代わりにwalkを使えば、ツリーを再帰的にたどり、すべてのmessageフィールドを取得できます。

messages[message] {
  [_, value] := walk(walk_input)
  message = value.message
}

メッセージを通常のRegoルールで扱えるようになれば、禁止用語を含むディスカッションスレッドを拒否する、慣用的なポリシーを記述できます。

deny {
  swear_words := {"hawaii", "pineapple"}
  contains(lower(messages[_]), swear_words[_])
}

実際の例としては、Terraformの子モジュールがあります。Regulaではここでwalkを使い、ディスカッションスレッドのメッセージをフラット化するのとよく似た方法で、子モジュールをフラット化しています。

5. パッケージとルールを動的に読み込む

Regoの興味深い設計の1つは、ルールとデータからなる「世界」全体が、同じドキュメントの下にネストされていることです。ユーザー入力、JSONファイルやYAMLファイルのデータ、パッケージ内のルールのいずれにアクセスする場合も、すべて単なる参照として扱います。

input.people[0].order
data.recipes.pizza.pepperoni
data.policies.strict.allow

Regoでは変数やワイルドカードを使って参照を列挙できるため、読み込まれたパッケージの集合を動的に調べられます。これにより、新しいポリシーファイルを追加するだけで、それらすべてをレポートにまとめられるアーキテクチャを構築できます。

ポリシーを別々のパッケージに置くと、個別にデバッグでき、ルール名が競合する心配もありません。すべてをdata.policies名前空間に配置します。最初のポリシーでは、ド・モルガンの法則を使ってピザにチーズがのっているかどうかを確認します。

package policies.cheese

contains_cheese {
  input.toppings[_] == "cheese"
}

deny["must contain cheese"] {
  not contains_cheese
}

2つ目のポリシーでは、最後の一切れをめぐって2人が争わないことを確認します。

package policies.slices

deny["must be able to share with two people"] {
  input.slices % 2 != 0
}

ポリシーを配置する名前空間が決まれば、それらをまとめるのは比較的簡単です。data.policies内のすべてのdenyを取得し、人が読めるレポートとして整形します。

package summary

report = msg {
  denies := {m | m := data.policies[_].deny[_]}
  msg := sprintf("%d policies failed\n%s", [
    count(denies),
    concat("\n", [sprintf("- %s", [m]) | denies[m]]),
  ])
}

実際の値を抽出するjqと組み合わせれば、ターミナル上に見やすいレポートを出力できます。ここでは-Iを使ってstdinから入力を読み込みます。

$ opa eval -I 'data.summary.report' -d . --format json | \
    jq -r '.result | .[] | .expressions | .[] | .value'
{"toppings": ["crab", "tomato sauce"], "slices": 6}
1 policies failed
- must contain cheese

パッケージをデータとして扱うことで、さらに高度なこともできます。たとえば、パッケージがinput_version = 1を宣言しているかinput_version = 2を宣言しているかに応じて、inputを別の形式に変換して渡すこともできます。すべてRegoの中で実現できます!

続きを読む

feature customer snowflake
Article

開発初期からセキュリティを確保:Snowflake Cortex Code向けSnyk Studioインテグレーションを発表

Snyk StudioがSnowflake Cortex Codeと連携し、開発中にAI生成コード、依存関係、コンテナの脆弱性をスキャンします。

Blog

Stadium Summer:Snyk Connect Fan Zone Tour

SnykのFan Zone Tourでは、8都市と3回のオンラインセッションで、AIセキュリティのワークショップやネットワーキング、楽しい競技を開催しました。参加者はスキルを磨き、アイデアを共有し、共に成長しました。

Blog

Snyk VulnBench JS 1.0:LLMは同じバグを2回見つけられるか?

Snyk VulnBench JS 1.0:300回の反復スキャンで、LLMのセキュリティ検出結果は実行ごとに異なる一方、SASTとモデルはそれぞれ異なる脆弱性の見落としを検出することが明らかに。