ConftestでCFnをテストする

# はじめに

先日、AWS Dev Day 2023 Tokyoのセッション動画が公開されました。

https://aws.amazon.com/jp/events/devday/japan/

8月末までの公開のため、少しずつ見ていっています。
その中の以下のセッションのスライドで、AWS SAMがテストできるとあり、興味がでたため少し触ってみました。

https://speakerdeck.com/nemops/yahariporisi-dot-dot-dot-porisihaquan-tewojie-jue-suru-dot-dot-dot-opa-deshi-meru-policy-as-code?slide=39



# 概要

- `Conftest`というツールを試用
    - `Open Policy Agent`の`Rego`というポリシー専用の言語で記述
    - Yamlをチェックできる
        - それを用いて、CFnをチェックする

# 参考

https://www.conftest.dev/

https://www.openpolicyagent.org/docs/latest/

https://zenn.dev/mizutani/books/d2f1440cfbba94/viewer/rego-cheat-sheet

# やってみた
作業用に、Cloud9(t2.micro)をつかっています。

## インストール
以下のページのコマンドをそのまま行います。

https://www.conftest.dev/install/

```bash
LATEST_VERSION=$(wget -O - "https://api.github.com/repos/open-policy-agent/conftest/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-)
wget "https://github.com/open-policy-agent/conftest/releases/download/v${LATEST_VERSION}/conftest_${LATEST_VERSION}_Linux_x86_64.tar.gz"
tar xzf conftest_${LATEST_VERSION}_Linux_x86_64.tar.gz
sudo mv conftest /usr/local/bin

conftest -v
```

## サンプル実行
公式のリポジトリを丸ごと持ってきて、AWS SAMの例をやってみます。
```
git clone https://github.com/open-policy-agent/conftest.git
cd conftest/examples/awssam
```

ディレクトリ構成は以下になっています。policyディレクトリ以下の`.rego`ファイルが定義ファイルの模様です。
```
$ pwd;find . | sort | sed '1d;s/^\.//;s/\/\([^/]*\)$/|--\1/;s/\/[^/|]*/|  /g'
/home/ec2-user/environment/conftest/examples/awssam
|--lambda.yaml
|--policy
|  |--policy.rego
```
それぞれの中身は以下のようになっています。

https://github.com/open-policy-agent/conftest/blob/master/examples/awssam/lambda.yaml

https://github.com/open-policy-agent/conftest/blob/master/examples/awssam/policy/policy.rego


サンプルをそのまま実行すると、4件失敗しました。
```
$ conftest test lambda.yaml 
FAIL - lambda.yaml - main - Sensitive data not allowed in environment variables
FAIL - lambda.yaml - main - excessive Action permissions not allowed
FAIL - lambda.yaml - main - excessive Resource permissions not allowed
FAIL - lambda.yaml - main - python2.7 runtime not allowed

7 tests, 3 passed, 0 warnings, 4 failures, 0 exceptions
```

ソースを修正して再実行してみます。
```diff_yaml:lambda.yaml
-      Runtime: python2.7
+      Runtime: python3.7
```
失敗が3件に減りました。
```
$ conftest test lambda.yaml 
FAIL - lambda.yaml - main - Sensitive data not allowed in environment variables
FAIL - lambda.yaml - main - excessive Action permissions not allowed
FAIL - lambda.yaml - main - excessive Resource permissions not allowed

7 tests, 4 passed, 0 warnings, 3 failures, 0 exceptions
```

## セッションの例
最初に紹介したセッションの記述もやってみます。
新しくディレクトリとファイルを作成します。
```bash
# 一旦上まで戻る
cd ~/environment

mkdir testConftest && cd testConftest

mkdir policy
touch sampleCreateLambda.yaml
touch policy/lambdaPolicy.rego
```

`sampleCreateLambda.yaml`に以下のサンプル(Yaml)を転記します。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#aws-resource-lambda-function--examples

`.rego`ファイルは以下をそのまま転記します。

https://speakerdeck.com/nemops/yahariporisi-dot-dot-dot-porisihaquan-tewojie-jue-suru-dot-dot-dot-opa-deshi-meru-policy-as-code?slide=39

```rego:policy/lambdaPolicy.rego
package main
import future.keywords

legacy_runtime = [
  "python2.7",
  "nodejs12.x",
  "dotnetcore3.1"
]

deny[msg] {
  some id, res in input.Resources
  res.Type == "AWS::Lambda::Function"
  runtime = res.Properties.Runtime
  contains(runtime, legacy_runtime[_])
  msg = sprintf("%s: %s runtime not allowed",[id , runtime])
}
```

実行して、失敗が出ませんでした。
```
$ conftest test sampleCreateLambda.yaml 

1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions
```

失敗になるように、Yamlを修正して実行します。
```diff_yaml:sampleCreateLambda.yaml
-     Runtime: nodejs18.x
+     Runtime: nodejs12.x
```
```
$ conftest test sampleCreateLambda.yaml 
FAIL - sampleCreateLambda.yaml - main - primer: nodejs12.x runtime not allowed

1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
```

## IAMロールをテスト

IAMロールのテストも作ってみます。
```
touch sampleCreateRole.yaml
touch policy/rolePolicy.rego
```
`sampleCreateRole.yaml`は以下を利用します。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#aws-resource-iam-role--examples

アクションに*が使われているかのチェックを作ってみました。

```rego:policy/rolePolicy.rego
package main
import future.keywords

# Actionに * が使われているか
deny[msg] {
  # idに論理IDが格納される
  some id, res in input.Resources
  res.Type == "AWS::IAM::Role"
  
  action = res.Properties.Policies[_].PolicyDocument.Statement[_].Action
  
  is_string(action)
  endswith(action, "*")
  
  msg = sprintf("%s 内でアクションに * が使われています。",[id])
}
```

実行してちゃんとチェックできていることが確認できました。
ここでテストが2件実行されているのは、先に作成した`policy/lambdaPolicy.rego`も使われているためです。
```
$ conftest test sampleCreateRole.yaml 
FAIL - sampleCreateRole.yaml - main - RootRole 内でアクションに * が使われています。

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
```

リソースに * が使われているかのチェックも追加してみます。
```rego:policy/rolePolicy.rego
package main
import future.keywords

# Actionに * が使われているか
deny[msg] {
  # idに論理IDが格納される
  some id, res in input.Resources
  res.Type == "AWS::IAM::Role"
  
  action = res.Properties.Policies[_].PolicyDocument.Statement[_].Action
  
  is_string(action)
  endswith(action, "*")
  
  msg = sprintf("%s 内でアクションに * が使われています。",[id])
}

# Resourceに * が使われているか
deny[msg] {
  some id, res in input.Resources
  res.Type == "AWS::IAM::Role"
  
  resource = res.Properties.Policies[_].PolicyDocument.Statement[_].Resource
  
  is_string(resource)
  endswith(resource, "*")
  
  msg = sprintf("%s 内でリソースに * が使われています。",[id])
}
```

ちゃんとチェックできるようになりました。
```
$ conftest test sampleCreateRole.yaml 
FAIL - sampleCreateRole.yaml - main - RootRole 内でアクションに * が使われています。
FAIL - sampleCreateRole.yaml - main - RootRole 内でリソースに * が使われています。

3 tests, 1 passed, 0 warnings, 2 failures, 0 exceptions
```


## 決められたタグの有無をチェック

`domain`というタグがあるかのチェックを書いてみました。

```rego:policy/tagPolicy.rego
package main
import future.keywords

# タグセクションが存在するか
deny[msg] {
  some id, res in input.Resources
  not res.Properties.Tags 
  msg = sprintf("%s 内には、Tagsが定義されていません。",[id])
}

# 特定のタグが存在するか
deny[msg] {
  targetKey = "domain"
  
  some id, res in input.Resources

  hasTarget = [ target | target = res.Properties.Tags[_].Key ]
  1 != count( [x | hasTarget[x] == targetKey ])
  
  msg = sprintf("%s 内で、'%s'をキーとしたタグが定義されていません。",[id,targetKey])
}
```
先のLambdaのCFnテンプレートに実行すると、以下のようにエラーになりました。
```
$ conftest test sampleCreateLambda.yaml 
FAIL - sampleCreateLambda.yaml - main - primer 内で、'domain'をキーとしたタグが定義されていません。
FAIL - sampleCreateLambda.yaml - main - primer 内には、Tagsが定義されていません。

5 tests, 3 passed, 0 warnings, 2 failures, 0 exceptions
```
CFnテンプレートに以下を加えてみます。
```yaml
      Tags:
        - Key: domain
          Value: train
        - Key: customer
          Value: jr
```

再度conftestを実行すると、うまくいきました。

```
$ conftest test sampleCreateLambda.yaml 

5 tests, 5 passed, 0 warnings, 0 failures, 0 exceptions
```

別のリソースでタグが定義されていない場合も、チェックされます。
```
$ conftest test sampleCreateLambda.yaml 
FAIL - sampleCreateLambda.yaml - main - primer2 内で、'domain'をキーとしたタグが定義されていません。

5 tests, 4 passed, 0 warnings, 1 failure, 0 exceptions
```


# おわりに

今回は`Conftest`を使って、CFnテンプレートのテストを作ってみました。
うまく使うことで、過剰な権限の付与を防止したりできそうでした。
この記事がどなたかのお役に立てれば幸いです。