今日から始めるセキュリティシフトレフト on AWS ②AWS CodeBuildのバッチビルドで効率よく静的解析する

# はじめに
セキュリティシフトレフト記事第二弾。

前回は、主にGitリポジトリにCommitする前のローカルPC上でのセキュリティチェックについて実施方法をまとめた。

https://qiita.com/neruneruo/items/8e6799864d68bee9b52a

今回は、Commit後に行うべきセキュリティチェックの構築方法についてまとめていく。
なお、今回以降、ブランチ戦略はGitLab Flowを想定しているが、GitHub FlowでもGit Flowでも考え方は同じなので、ブランチ名を適宜変更しながら対応していただければ良い。

# どのタイミングでチェックをするのが適切か？
## 考察
さて、リポジトリCommit後に行うセキュリティチェックはどのタイミングで行うのが良いだろうか？
前回は、Commitに1度でも含めてしまったら危険なものを排除する最低限のチェックを行った。
Commitの都度チェックをかけるのでは過剰すぎるが、当然ながらチェックの結果コードが変わる可能性もあり得るため、レビュー後、プルリクエストの承認をするタイミングでエラーになると手戻りのコストが高くなる。
レビュアーがピカピカの状態でレビューすることを考慮すると、ここは、***プルリクエストを発行したタイミング***でチェックを行い、静的解析が終わったタイミングでレビュー実施するのが良いだろう。

また、静的解析の時間が長いとレビューまでのリードタイムが延びてしまうため、できるだけ効率良く終わらせたい。
静的解析は複数のチェックを並行して行っても差し支えが無いので、今回は並列実行のソリューションとして、AWS
AWS CodeBuildのバッチビルドを選択する。
※上記の通り、プルリクエスト発行のタイミングであるため、ビルドアーティファクトのレジストリへの格納はマージ完了後に行うため、今回の記事では静的解析完了でAWS CodeBuildが完了する形になっている。

## システム構成図
上記を踏まえたAWSの構成図は以下のようになる。

![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/bf91bc50-0aa3-618b-70ea-5f14f82aa51b.png)

ポイントは以下の通り。

- AWS CodeBuildの標準機能ではAWS CodeCommitのプルリクエスト契機でトリガーすることができないため、Amazon EventBridgeを介して起動する
- Amazon EventBridgeはAWS CodeBuildのバッチ起動に対応していないため、AWS Lambdaを介して起動する(詳細は以下のユーザーガイド参照)

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-triggers.html

# AWS CodeBuildのバッチビルドの構成
AWS CodeBuildのバッチビルドは以下のように構成する。
なお、バッチビルドの基本については以下のユーザーガイド参照。

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/batch-build.html

今回は、並行で動作させたいことと、BuildSpecの共通化は特にしなくて良いため、ビルドグラフで対応を行う。
以下のような依存関係のビルドグラフを構築する。

![ビルドグラフ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/1b23af12-8807-ba47-20a0-e9c675fe7f1e.png)

シンプル過ぎてあまりバッチビルドにするが無いように見えるかもしれないが、アプリケーション静的解析の段数や並列度が増える可能性があるため、これで良い。(アプリケーション静的解析については次回以降で説明するため、今回は対象外で、ダミーで正常終了するBuildSpecを用意する)

## BuildSpecの構成
リポジトリのディレクトリ構成は以下のようになる。
上記の通り、```11_golangci_lint.yml```の中身はダミーである。

```
リポジトリのルートディレクトリ
├── Dockerfile
├── buildspec
│   ├── 10_hadolint.yml
│   ├── 11_golangci_lint.yml
│   ├── 20_dockle.yml
│   └── 21_trivy.yml
└── buildspec.yml
```

ルートディレクトリ直下にあるbuildspec.ymlが全体を統制する。
上記設計に対して、ビルドグラフの記法に従い、以下のようにYAMLを書いていく。

```buildspec.yml
version: 0.2

batch:
  fast-fail: false
  build-graph:
    - identifier: hadolint
      buildspec: buildspec/10_hadolint.yml
      ignore-failure: false
    - identifier: golangci_lint
      buildspec: buildspec/11_golangci_lint.yml
      ignore-failure: false
    - identifier: dockle
      buildspec: buildspec/20_dockle.yml
      ignore-failure: false
      depend-on:
        - hadolint
        - golangci_lint
    - identifier: trivy
      buildspec: buildspec/21_trivy.yml
      ignore-failure: false
      depend-on:
        - hadolint
        - golangci_lint
```

# それぞれのビルドのセキュリティに関する静的解析について
いずれもシングルバイナリで提供しているものであるため、ローカルで動作させたり、Git Hooksに組み込んでも問題ない。ただし、Git Hooksに組み込んだからといって、サーバ側で実施をしなくて良いというわけではない。
セキュリティの考え方は基本的にゼロトラストであるべきなので、ローカルでチェックをしない輩がCommitしてきてもチェックアウトできるようにするのが肝要だ。

なお、後述するが、今回のAWS CodeBuildの実行イメージは```aws/codebuild/amazonlinux2-x86_64-standard:5.0```を用いるため、別のアーキテクチャ、OSを使用する場合はそれに合わせてBuildSpecの内容を変更していただきたい。

## hadolint
### 概要
hadolintはシングルバイナリで動作する軽量のDockerfileのLinterだ。

https://github.com/hadolint/hadolint

Dockerfileにおけるベストプラクティスに従っているかのチェックをしてくれるが、コンテナビルド前に実行する静的解析であるため、この後出てくる他のツールと比べるとカバレッジの範囲は狭い。

ただし、上記の通り軽量であるため、重いビルドを時間をかけて実行してからエラーがでるよりも早く検知ができるということが重要だ。

アプリケーションについてもビルド前にチェックをかけた方が、エラー時のダメージが小さいため、このチェックを並行でやっておくことで効率的に検査を行うことができるだろう。

### BuildSpec
hadolintは引数でDockerfileを渡せば良い。

```YAML:10_hadolint.yml
version: 0.2

env:
  variables:
    HADOLINT_PATH: "/tmp/hadolint"

phases:
  install:
    commands:
      # install hadolint
      - VERSION=$(curl --silent "https://api.github.com/repos/hadolint/hadolint/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
      - wget -q https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64 -O ${HADOLINT_PATH} && chmod 755 ${HADOLINT_PATH}
  build:
    commands:
      - ${HADOLINT_PATH} ./Dockerfile

```

## dockle
### 概要
dockleも、hadolint同様にシングルバイナリで動作するDockerfileのLinterかつコンテナイメージそのものの静的解析を行う。イメージビルド後の実際のイメージ内の検査を行うため、hadolintよりも広い範囲のチェックを行える。チェック範囲の差分については、以下のReadmeの「Checkpoints Comparison」に詳細に記載されている。

https://github.com/goodwithtech/dockle?tab=readme-ov-file

### BuildSpec
基本的にコンテナイメージを引数で渡せば良いが、今回、Nginxのイメージを渡したら、NGINX_GPGKEYS, NGINX_GPGKEY_PATHが入っているとしてエラーになった。Dockleは、シークレット情報の疑いがあるものをチェックするため、Nginxの公式イメージにもともと含まれていて安全であると分かった上記については、```-ak(--accept-key)```オプションで除外している。

:::note alert
当然ながら何でもかんでも```-ak```すれば良いものではない。AWSのシークレットも同様のエラーとなるため、誤って```-ak```オプションで除外しすぎないように気を付けよう。
:::

:::note warn
dockleはデフォルトではチェックで引っかかったものがあった際に、EXIT CODEが0で終了してしまい、AWS CodeBuildが正常終了してしまう。パイプライン中では必ず、```--exit-code 1```オプションを付与して実行しよう。
:::

```YAML:20_dockle.yml
version: 0.2

env:
  variables:
    IMAGE_NAME : "container_image"
    IMAGE_TAG  : "1.0"
    DOCKLE_PATH: "/usr/bin/dockle"

phases:
  install:
    commands:
      # install dockle
      - VERSION=$(curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
      - rpm -ivh https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.rpm
  build:
    commands:
      - docker build . -t ${IMAGE_NAME}:${IMAGE_TAG}
      - ${DOCKLE_PATH} --exit-code 1 -ak NGINX_GPGKEYS -ak NGINX_GPGKEY_PATH ${IMAGE_NAME}:${IMAGE_TAG} 
```

## Trivy
### 概要
Trivyは第1弾記事でも使用した、Aquasecurity社が提供する統合的なセキュリティチェッカーだ。前回はファイルにハードコーディングされているシークレット情報の検査に使用したが、今回はコンテナイメージのチェックに利用する。dockleとはチェック範囲が異なることと、こちらは脆弱性データベース(CVE)をもとに、コンテナイメージ内にある依存関係のあるベースイメージまで含めてチェックを行う。dockleと組み合わせることで、より強固なセキュリティチェックを行うことが可能だ。

https://trivy.dev/

### BuildSpec
Trivyも、引数にビルド済みのコンテナイメージを渡せば良い。

:::note warn
Trivyもdockle同様、デフォルトではチェックで引っかかったものがあった際に、EXIT CODEが0で終了してしまい、AWS CodeBuildが正常終了してしまう。パイプライン中では必ず、```--exit-code 1```オプションを付与して実行しよう。
:::

```YAML:21_trivy.yml
version: 0.2

env:
  variables:
    IMAGE_NAME: "container_image"
    IMAGE_TAG : "1.0"
    TRIVY_PATH: "/usr/bin/trivy"

phases:
  install:
    commands:
      # install Trivy
      - VERSION=$(curl --silent https://api.github.com/repos/aquasecurity/trivy/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
      - rpm -ivh https://github.com/aquasecurity/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.rpm
  build:
    commands:
      - docker build . -t ${IMAGE_NAME}:${IMAGE_TAG}
      - ${TRIVY_PATH} image --quiet --exit-code 1 ${IMAGE_NAME}:${IMAGE_TAG} 
```

:::note info
お気付きだろうが、dockleとTrivyと両方でコンテナのビルドを行っているため、多少リソースの無駄遣いをしている。AWS CodeBuildのバッチビルドは、それぞれのビルドを別インスタンスで起動するため、イメージを引き継ぐことができない。Amazon ECRに登録したイメージを検査することも可能だが、無駄にECRにイメージ登録はしたくないため、今回はそれぞれのビルドの中でコンテナイメージをビルドした。
:::

# TerraformによるAWSリソースの構築
さて、AWS CodeBuildのバッチビルドの構成要素について作り終わったところで、構築をしていく。

## AWS CodeBuild
ここはそんなに難しくない。AWS CodeBuildを構築したことがある人であれば苦労しないだろう。
```build_batch_config```は設定しないと差分検知してしまう(おそらくTerraformの不具合？)ので設定している。特に意図があっての設計値ではない。

```HCL
resource "aws_codebuild_project" "example" {
  name         = local.codebuild_project_name
  service_role = aws_iam_role.codebuild.arn

  source {
    type      = "CODECOMMIT"
    location  = aws_codecommit_repository.example.clone_url_http
    buildspec = "buildspec.yml"
  }

  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    type            = "LINUX_CONTAINER"
    compute_type    = "BUILD_GENERAL1_SMALL"
    image           = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
    privileged_mode = "true"
  }

  build_batch_config {
    service_role    = aws_iam_role.codebuild.arn
    timeout_in_mins = 480

    restrictions {
      compute_types_allowed  = []
      maximum_builds_allowed = 100
    }
  }

  logs_config {
    cloudwatch_logs {
      group_name = aws_cloudwatch_log_group.codebuild.name
    }
  }

  cache {
    type = "LOCAL"
    modes = [
      "LOCAL_CUSTOM_CACHE",
    ]
  }
}
```

上記を作成するにあたって必要なIAMとAmazon CloudWatch Logsは以下のように記述している。

```HCL
resource "aws_cloudwatch_log_group" "codebuild" {
  name              = local.clouddwatch_loggroup_name
  retention_in_days = 3
}
```

```HCL
resource "aws_iam_role" "codebuild" {
  name               = local.iam_codebuild_role_name
  assume_role_policy = data.aws_iam_policy_document.codebuild_assume.json
}

data "aws_iam_policy_document" "codebuild_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "codebuild.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "codebuild" {
  name   = local.iam_codebuild_policy_name
  role   = aws_iam_role.codebuild.id
  policy = data.aws_iam_policy_document.codebuild_custom.json
}

data "aws_iam_policy_document" "codebuild_custom" {
  statement {
    effect = "Allow"

    actions = [
      "codeBuild:StartBuild",
      "codeBuild:StartBuildBatch",
    ]

    resources = [
      aws_codebuild_project.example.arn,
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "codecommit:GitPull",
    ]

    resources = [
      aws_codecommit_repository.example.arn,
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = [
      aws_cloudwatch_log_group.codebuild.arn,
      "${aws_cloudwatch_log_group.codebuild.arn}:log-stream:*",
    ]
  }
}
```

## Amazon EventBridge
Amazon EventBridgeについてはLambda起動のイベントターゲット設定を行う。
必要に応じて、```aws_cloudwatch_event_target```には```dead_letter_config```の設定を入れよう。
※プロダクションのワークロードではないため、そこまで神経質にならず失敗したら自分でリトライすれば良いので、今回は省いている。

今回、AWS CodeCommitのプルリクエストを処理できるイベントのルールを作っている。
イベントに```pullRequestSourceBranchUpdated```を入れておくことで、エラー後に再度featureブランチにCommitをしたら自動でAWS CodeBuildが再実行されるようにしてある。

```sourceReference```で``` { "prefix" = "refs/heads/feature/" }```と記載することで、feature/hogehogeといった具合に前方一致でブランチを引っ掛けられるようになるため、複数イベントを作らなくて良いようにしている。

```HCL
resource "aws_cloudwatch_event_rule" "codecommit_pull_request_example" {
  name        = local.event_rule_codecommit_pr_name
  description = "AWS CodeCommit Pull Request Trigger"

  event_pattern = jsonencode({
    source = [
      "aws.codecommit",
    ]
    resources = [
      aws_codecommit_repository.example.arn,
    ]
    detail-type = [
      "CodeCommit Pull Request State Change",
    ]
    detail = {
      event = [
        "pullRequestCreated",
        "pullRequestSourceBranchUpdated",
      ],
      pullRequestStatus = [
        "Open",
      ],
      sourceReference = [
        { "prefix" = "refs/heads/feature/" },
      ]
      destinationReference = [
        "refs/heads/master",
      ]
    },
  })
}

resource "aws_cloudwatch_event_target" "codecommit_pull_request_example" {
  target_id = local.event_target_codecommit_pr_name

  rule = aws_cloudwatch_event_rule.codecommit_pull_request_example.name
  arn  = aws_lambda_function.codecommit_pull_request_example.arn
}
```

Amazon EventBridgeに対するIAMの権限付与はここでは不要だ。
実際は```lambda:InvokeFunction```のAPIを実行するが、Lambdaについてはリソースベースポリシで権限付与するため、ここで設定をする必要がない。

## AWS Lambda
Lambdaについては以下のように設定する。
面倒くさくてPythonのスクリプトをインラインで書いてしまったが、見づらいので外出ししておく。
実際に設定する際は、```archive_file.codecommit_pull_request_example```の中に転記をしていただきたい。

AWS CodeCommitの```post_comment_for_pull_request```のAPIを実行しているのは、プルリクエスト作成後にAWS CodeBuildのログと紐づかないと扱いづらいためだ。実行結果の通知が必要な場合は、AWS CodeBuildの通知で更にコメントを追加するとより便利になる(ビルド結果の通知については今回の記事の本筋ではないので割愛する)。

```Python
import boto3
import pprint

def lambda_handler(event, context):
  codebuild_client = boto3.client('codebuild')
  codecommit_client = boto3.client('codecommit')

  try:
    codebuild_response = codebuild_client.start_build_batch(
      projectName   = '${aws_codebuild_project.example.name}',
      sourceVersion = event['detail']['sourceCommit']
    )
    codecommit_client.post_comment_for_pull_request(
      repositoryName = event['detail']['repositoryNames'][0],
      pullRequestId = event['detail']['pullRequestId'],
      beforeCommitId = event['detail']['destinationCommit'],
      afterCommitId = event['detail']['sourceCommit'],
      content = 'Build Start.\n\n[See Detail](https://${data.aws_region.current.name}.console.aws.amazon.com/codesuite/codebuild/${data.aws_caller_identity.current.account_id}/projects/${aws_codebuild_project.example.name}/batch/' + codebuild_response['buildBatch']['id'] + '?region=${data.aws_region.current.name})'
    )
  except Exception as e:
    print(e)
```

```HCL
data "archive_file" "codecommit_pull_request_example" {
  type        = "zip"
  output_path = "../output/codecommitpr.zip"

  source {
    filename = "codecommitpr.py"
    content  = <<EOF
    // ★★★上記Pythonの中身をここに転記する★★★
EOF
  }
}

resource "aws_lambda_function" "codecommit_pull_request_example" {
  depends_on = [
    aws_cloudwatch_log_group.codecommit_pull_request_example_lambda,
  ]

  function_name    = local.lambda_function_eventbridge_codecommit_pr_name
  filename         = data.archive_file.codecommit_pull_request_example.output_path
  role             = aws_iam_role.eventbridge_lambda.arn
  handler          = "codecommitpr.lambda_handler"
  source_code_hash = data.archive_file.codecommit_pull_request_example.output_base64sha256
  runtime          = "python3.10"

  memory_size = 128
  timeout     = 60
}

resource "aws_lambda_permission" "codecommit_pull_request_example_allow_evnentbridge" {
  statement_id  = "AllowExecutionFromEventBridge"
  function_name = aws_lambda_function.codecommit_pull_request_example.function_name

  principal  = "events.amazonaws.com"
  source_arn = aws_cloudwatch_event_rule.codecommit_pull_request_example.arn
  action     = "lambda:InvokeFunction"
}
```

Lambdaを実行するためのIAMとAmazon CloudWatch Logsの設定は以下の通り。

```HCL
resource "aws_cloudwatch_log_group" "codecommit_pull_request_example_lambda" {
  name              = "/aws/lambda/${local.lambda_function_eventbridge_codecommit_pr_name}"
  retention_in_days = 3
}
```

```HCL
resource "aws_iam_role" "eventbridge_lambda" {
  name               = local.iam_role_eventbridge_lambda_name
  assume_role_policy = data.aws_iam_policy_document.eventbridge_lambda_assume.json
}

data "aws_iam_policy_document" "eventbridge_lambda_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "lambda.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "eventbridge_lambda" {
  name   = local.iam_policy_eventbridge_lambda_name
  role   = aws_iam_role.eventbridge_lambda.id
  policy = data.aws_iam_policy_document.eventbridge_lambda_custom.json
}

data "aws_iam_policy_document" "eventbridge_lambda_custom" {
  statement {
    effect = "Allow"

    actions = [
      "codebuild:StartBuildBatch",
    ]

    resources = [
      aws_codebuild_project.example.arn,
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "codecommit:PostCommentForPullRequest",
    ]

    resources = [
      aws_codecommit_repository.example.arn,
    ]
  }
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = [
      aws_cloudwatch_log_group.codecommit_pull_request_example_lambda.arn,
      "${aws_cloudwatch_log_group.codecommit_pull_request_example_lambda.arn}:log-stream:*",
    ]
  }
}
```

# いざ、動かす！
上記を```terraform apply```したら、実際にAWS CodeCommitでプルリクエストを発行してみよう。

![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/9a21a5ba-ae46-794c-7372-a6430946fe86.png)


アクティビティのタブに、AWS Lambdaから送信した情報が追記される。
このリンクを開くと、以下のように、どの検査でエラーになったか一目瞭然になっている。

![キャプチャ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/f10304b0-95a1-0997-5d28-23a12869a18b.png)

これで、プルリクエストのレビュー前に必要なセキュリティが確保できるようになった！