Checkov、Terraform、および Azure DevOps を使用した安全なインフラストラクチャのプロビジョニング

クラウド環境にインフラを定義·構築するためのIaC(Infrastructure as Code)ツールに転換する組織が増え、2021年までサイバー攻撃件数が125%増加し、最初からセキュリティにさらに力を入れることが義務付けられた。 この記事では、Checkov on Azure DevOps を使用して Terraform 設定コードの自動検証を実装する方法について説明します。

# チェコフとは何ですか？

CheckovはPythonで書かれたオープンソースのソフトウェア合成分析（SCA）で、1000以上の定義済みポリシーに基づいてIaCファイルをスキャンします。 グラフベースのスキャンを使用して、セキュリティまたはコンプライアンスの問題につながる可能性がある誤設定をチェックします。

## Checkov と "Terraform validate" コマンドの違いは何ですか？

デフォルトでは、Terraform CLI にはコンフィギュレーション ファイルを検証するコマンドが用意されています。 ただし、このコマンドは、セキュリティ スキャンを実行せずに構文の正確性と設定の一貫性を確認するだけです。 属性名や値タイプの正確性など、再利用可能なモジュールの一般的な検証に使用されます。

# Azure DevOps

Azure DevOps の継続的な統合パイプラインを使用することで、マージを進める前に特定のブランチをターゲットとする Pull Request の内容を評価できます。 これにより、開発の初期段階で問題やセキュリティの脆弱性を発見でき、識別と解決がはるかに簡単かつ迅速になります。 このチュートリアルでは、ビルド検証機能を使用します。 ブランチ ポリシーについて詳しく知りたい場合は、このトピックに関する以前の記事を参照することをお勧めします。

[Azure DevOps のブランチ保護](https://qiita.com/gtrekter/items/9f06a212019e876d9de9)

ビルド検証を追加するには、まずブランチに接続するための継続的な統合パイプラインを作成する必要があります。

## Microsoft がホストするエージェントを使用して Build Validation パイプラインを作成します。

パイプラインを実行している仮想マシンは、各ジョブの後に破壊されるため、パイプラインがトリガーされるたびにCheckovをインストールする必要があります。 幸いなことに、デフォルトでは、Checkov が実行するために必要なすべてのパッケージはすでに Azure 仮想マシンにプリインストールされています。 Checkovの再インストールには15~30秒かかります。

```
- script : |
    pip install -U checkov
  displayName: Install Checkov
```

次に、Checkovを実行する必要があります。 これを行う前に、次のパラメータを設定する必要があります:

- **-directory:** スキャンする Terraform コードを含むパス。
- **-output:** レポート出力形式を指定します。
- **-output-file-path:** 出力を保存するパス。 この場合、エージェントの一時フォルダにデフォルト名 results_junitxml.xml のレポート ファイルを生成します。
- **-soft-fail:** デフォルトでは、Checkov スキャンでエラーが検出されると、その終了コードは 1 です。 つまり、コンフィギュレーション ファイルにセキュリティ上の問題が 1 つでもある場合、結果をパブリッシュする前にパイプライン全体が失敗します。 この引数を追加すると、Checkovは結果に関係なく0の終了コードを返します。

その結果、次の手順が実行されます:

```
- script : |
    checkov \
      --directory . \
      --output junitxml \
      --skip-download \
      --compact \
      --output-file-path $(Agent.TempDirectory) \
      --soft-fail
  displayName: Run Checkov
  workingDirectory: $(System.DefaultWorkingDirectory)
```

最後に、Checkov が脆弱性を特定した場合にパイプラインが失敗する原因となる Azure Pipelines にスキャン結果を公開する必要があります。

```
- task: PublishTestResults@2
  inputs:
    testResultsFormat: 'JUnit'
    testResultsFiles: '**/*results_junitxml.xml'
    searchFolder: $(Agent.TempDirectory)
    testRunTitle: 'Publish Checkov report'
    publishRunAttachments: true 
    failTaskOnFailedTests: true
```

最終的なパイプラインは次のとおりです:

```
trigger: none
pool:
  vmImage: 'ubuntu-latest'
stages:
- stage: SecurityCheck
  displayName: Security Check
  jobs:
    - job: SecurityCheck
      displayName: Install Checkov
      steps: 
      - script : |
          pip install -U checkov
        displayName: Install Checkov
      - script : |
          checkov \
            --directory . \
            --output junitxml \
            --skip-download \
            --compact \
            --output-file-path $(Agent.TempDirectory) \
            --soft-fail
        displayName: Run Checkov
        workingDirectory: $(System.DefaultWorkingDirectory)
      - task: PublishTestResults@2
        inputs:
          testResultsFormat: 'JUnit'
          testResultsFiles: '**/*results_junitxml.xml'
          searchFolder: $(Agent.TempDirectory)
          testRunTitle: 'Publish Checkov report'
          publishRunAttachments: true 
          failTaskOnFailedTests: true
```

## （任意）セルフホスト エージェントと Azure Container Instance を使用してビルド検証パイプラインを作成します。

最適化に制限はありません。 Azure Container Instances を使用すると、Checkov のインストールに必要な実行時間をコスト効率よく削除でき、同時に環境の制御も強化できます。 この記事では、コードに焦点を当てていますが、コードが何をしているのか、プロセスについてもう少し詳しく知りたい場合は、この記事を見ることをお勧めします。

[Azure Container Instancesで実行するように Azure DevOps エージェントを設定します](https://qiita.com/gtrekter/items/7d24540d27a5c21f6cd1)

それでは、次のことを完了する必要があります。
- デスクトップで、azureagent という名前の新しいフォルダを作成します
- この新しいフォルダで、エージェントをダウンロードして設定する新しいシェルスクリプトを作成します。

```
#!/bin/bash
echo -e "0. Validate parameters..."
if [ -z "$AZDO_URL" ]; then
  echo "error: missing AZDO_URL environment variable"
  exit 1
fi
if [ -z "$AZDO_TOKEN" ]; then
  echo "error: missing AZDO_TOKEN environment variable"
  exit 1
fi
if [ -z "$AZDO_POOL" ]; then
  echo "error: missing AZDO_POOL environment variable"
  exit 1
else
  AZDO_AGENTPOOLS_API_URL="$AZDO_URL/_apis/distributedtask/pools"
  AZDO_AGENTPOOLS=$(curl -LsS \
      -u user:$AZDO_TOKEN \
      -H 'Accept:application/json;' \
      $AZDO_AGENTPOOLS_API_URL)
  AZDO_AGENTPOOLS_LENGTH=$(echo "$AZDO_AGENTPOOLS" | jq -r '[.value[] | select(.name == "$AZDO_POOL")] | length')
  if [ -z "$AZDO_AGENTPOOLS_LENGTH" ]; then
    echo "error: AZDO_POOL not found"
    exit 1
  fi
fi
if [ "$AZDO_WORK" ]; then
  mkdir -p "$AZDO_WORK"
fi
echo -e "1. Identifying latest Azure DevOps agent..."
TARGET_ARC=$(dpkg --print-architecture | sed "s/amd/x/g")
echo -e "architecture: $TARGET_ARC"
AZDO_AGENT_API_URL="$AZDO_URL/_apis/distributedtask/packages/agent?platform=linux-$TARGET_ARC&top=1"
echo -e "sending request to $AZDO_AGENT_API_URL"
AZDO_AGENT_PACKAGES=$(curl -LsS \
    -u user:$AZDO_TOKEN \
    -H 'Accept:application/json;' \
    $AZDO_AGENT_API_URL)
AZDO_AGENT_PACKAGE_LATEST_URL=$(echo "$AZDO_AGENT_PACKAGES" | jq -r '.value[0].downloadUrl')
if [ -z "$AZDO_AGENT_PACKAGE_LATEST_URL" -o "$AZDO_AGENT_PACKAGE_LATEST_URL" == "null" ]; then
  echo "error: could not determine a matching Azure Pipelines agent"
  echo "check that account '$AZDO_URL' is correct and the token is valid for that account"
  exit 1
fi
echo -e "2. Downloading and extracting Azure DevOps agent..."
echo -e "downloading agent from $AZDO_AGENT_PACKAGE_LATEST_URL"
curl -LsS $AZDO_AGENT_PACKAGE_LATEST_URL | tar -xz & wait $!
echo -e "3. Configuring Azure Pipelines agent..."
export AGENT_ALLOW_RUNASROOT="1"
./config.sh --unattended \
  --agent "${AZDO_AGENT_NAME:-$(hostname)}" \
  --url "$AZDO_URL" \
  --auth PAT \
  --token $AZDO_TOKEN \
  --pool "$AZDO_POOL" \
  --work "${AZDO_WORK:-_work}" \
  --replace \
  --acceptTeeEula & wait $!
echo -e "4. Running Azure Pipelines agent..."
chmod +x ./run-docker.sh
./run-docker.sh "$@" & wait $!
```

最後に、次の内容で新しい Docker ファイルを作成します:

```
FROM ubuntu:20.04
RUN DEBIAN_FRONTEND=noninteractive apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends apt-transport-https apt-utils ca-certificates curl git iputils-ping jq lsb-release software-properties-common
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash
RUN apt update
RUN apt install -y python3-pip
RUN pip3 install --upgrade pip && pip3 install --upgrade setuptools
RUN pip3 install checkov
WORKDIR /agent
COPY ./start.sh .
RUN chmod +x start.sh
ENTRYPOINT ["./start.sh"]
```

次に進む前に、プロセスで何が起こっているかを理解する時間をとってみましょう。
Docker Engine は Ubuntu 20.03 ベース イメージのダウンロードを開始します（まだローカルで入手できない場合）。その後、Azure DevOps エージェントが必要とするすべてのパッケージを「非対話的」にインストールします。

```
FROM ubuntu:20.04
RUN DEBIAN_FRONTEND=noninteractive apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get upgrade -y
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends apt-transport-https apt-utils ca-certificates curl git iputils-ping jq lsb-release software-properties-common
RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash
RUN apt update
```

その後、pythonとパッケージインストーラpipをインストールします。 両方のパッケージはすべての Azure 仮想マシンにプリインストールされているため、Microsoft ホスト エージェントではこの 2 つの手順は必要ありません。

```
RUN apt install -y python3-pip
RUN pip3 install --upgrade pip && pip3 install --upgrade setuptools
```

次に、Checkovをインストールします。

```
RUN pip3 install checkov
```

最後に、以前作成した start.sh スクリプトをコピーし、その権限を設定し、コンテナの起動時に実行されるコマンドとして設定します。

```
WORKDIR /agent
COPY ./start.sh .
RUN chmod +x start.sh
ENTRYPOINT ["./start.sh"]
```

エージェントを正しく設定するには、次の環境変数を渡す必要があります:
- **Personal Access Token（PAT）:** Azure DevOps の認証および対話にエージェントが使用するユーザを識別するスティング。 この場合、エージェント プール（読み取り、管理）スコープが必要になります。
- **Azure DevOps 組織の URL:** Azure DevOps 組織の URL。
- **Azure DevOps プール名:** 新しいエージェントを追加する Azure DevOps プールの名前。

この情報を入手したら、Docker ファイルとスクリプトを含むディレクトリで次のコマンドを使用して Azure Container レジストリに直接イメージをプッシュおよびビルドできます:

```
az acr build --registry acrcheckov --image acrcheckov.azurecr.io/azdoagent:latest .
```

これで、新しいイメージを実行する新しい Azure Container インスタンスを作成できます。

```
az container create /
  --resource-group rg-training /
  --name aci-azdo-checkov-dv-01 /
  --image acrcheckov.azurecr.io/azdoagent:latest /
  --ports 80 443
  --location uksouth
  --environment-variables AZDO_URL=https://dev.azure.com/GTRekter AZDO_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxx AZDO_POOL=Azure
```

最後に、継続的な統合パイプラインを構築することができます。 これは、Microsoft がホストするエージェントの場合と非常に似ています。 ただし、プール名を指定して、Azure DevOps にセルフホスト エージェントを使用するように指示する必要があります。

```
pool: Azure
```

Checkov はすでにコンテナにインストールされているので、この手順を削除できます。 最終的なパイプラインは次のとおりです:

```
trigger: none
pool: Azure
stages:
- stage: SecurityCheck
  displayName: Security Check
  jobs:
    - job: SecurityCheck
      displayName: Install Checkov
      steps: 
      - script : |
          checkov \
            --directory . \
            --output junitxml \
            --skip-download \
            --compact \
            --output-file-path $(Agent.TempDirectory) \
            --soft-fail
        displayName: Run Checkov
        workingDirectory: $(System.DefaultWorkingDirectory)
      - task: PublishTestResults@2
        inputs:
          testResultsFormat: 'JUnit'
          testResultsFiles: '**/*results_junitxml.xml'
          searchFolder: $(Agent.TempDirectory)
          testRunTitle: 'Publish Checkov report'
          publishRunAttachments: true 
          failTaskOnFailedTests: true
```

## パイプラインをトリガーするようにブランチ保護ポリシーを設定します。

エージェントとパイプラインの両方が用意されているので、ブランチ保護ポリシーを作成し、開発ブランチをターゲットにしたプルリクエストがあるたびに自動的にパイプラインをトリガーします。 その失敗または成功によって、マージを完了することが可能かどうかが示されます。

- [Repos] セクションを展開し、[Branch] をクリックします。
- 開発ブランチの横に3つの垂直な点があるアイコンをクリックし、「Branch policies」オプションを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/954e453b-ecf7-59b4-40cf-cddf6a43a6b2.png)

- [Build Validation] の横の + アイコンをクリックします。
- ドロップダウンから以前に作成したパイプラインを選択し、必要に応じて自動トリガー マークを選択して、ポリシーに名前を付けます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/89ea9b01-f763-4e81-6cd6-649c2bdca331.png)

- [Save] をクリックします

# コードのテスト

これまで行ったことをすべて検証するために、feature-exampleという新しいブランチを作成し、Terraformファイルをたくさんアップロードしてリモートリポジトリにプッシュしました。 ご想像の通り、ブランチを開発に手動でマージしようとすると、ターゲットブランチが保護されていてプルリクエストを開かなければならないというエラーが発生します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/ae4fe216-a1b4-1e9e-d629-8664133427ca.png)

次に、次の方法で新しいプル要求を開きます:

- [Repos] の下の [Pull requests] オプションを選択します。
- ターゲットブランチとして機能拡張ブランチを選択し、プルリクエストのタイトルと説明を指定して、Createをクリックします。
- 数秒後、以前に作成したパイプラインがトリガーされ、検証が実行されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/7312e5f6-c067-3bd0-1dec-8bbdaad577b3.png)

この場合、検証に失敗し、プル要求を完了できません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/0c691e6f-edd5-679e-9c2e-f91ec74e2b26.png)

失敗したテストを検査するには、次のことができます:
- [Test Plan ] を展開し、[Runs] をクリックします
- Test runs から実行を選択し、[Test results] をクリックします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3477222/9c40f4ee-8ed5-ef5e-89b1-dfcb49244f70.png)

# 参考資料
- **Checkov official site:** https://www.checkov.io/
- **Azure DevOps branch protection policies:** https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies