[AWS]CICDパイプラインでDocker脆弱性スキャン

## 導入

### 背景・目的

- Amazon Inspectorは継続的に AWS ワークロードをスキャンする自動脆弱性管理サービスです。
- サービスの1機能を用いることで、Dockerイメージに対する脆弱性検査が可能です。
- CodeシリーズとInspectorとを組み合わせて、脆弱性検査をPassした安全なDockerイメージのみをデプロイする、CICDパイプラインを構築してみます。

### 読者に想定する知識レベル

- CodeシリーズやCDKの概要を理解している前提で記載します。

## CICDパイプライン構築

### アーキテクチャ紹介

- CodeBuild内でDockerイメージに対する脆弱性検査を実施します。
- 重大な脆弱性を検知しなかった場合のみ、DockerイメージをECRへPushします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/7c97200b-cbea-0994-51df-2abdf709ee6a.png)
[^1]
[^1]: [AWS公式](https://aws.amazon.com/jp/blogs/devops/how-to-migrate-your-aws-codecommit-repository-to-another-git-provider/)のアナウンス通り、CodeCommitは新規顧客の利用不可になりました。本記事はCodeBuild及びInspectorの挙動確認に重きを置いているため、CodeCommitを利用しています。本番ワークロードで本構成を採用する場合には、他選択肢を候補に検討ください。

### buildspecの作成

- まずは、CodeBuildのビルド仕様ファイルである`buildspec.yml`を作成します。
    - inspectorのSbomgenを用いてDockerイメージに対する脆弱性検査を実施します。
    - critical若しくはhighに分類される脆弱性を検知した場合にはECRへのPushを中止します。

```yaml
version: 0.2
phases:
  install:
    commands:
      - echo "Setting timezone to Japan Standard Time (JST)..."
      - cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
      - echo "Installing Amazon Inspector SBOM Generator..."
      - wget https://amazon-inspector-sbomgen.s3.amazonaws.com/latest/linux/amd64/inspector-sbomgen.zip
      - unzip inspector-sbomgen.zip
      - mv inspector-sbomgen-* inspector-sbomgen
      - chmod +x ./inspector-sbomgen/linux/amd64/inspector-sbomgen
      - ./inspector-sbomgen/linux/amd64/inspector-sbomgen --version
      - echo "Logging in to Amazon ECR..."
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
      - echo "Installing jq for JSON parsing..."
      - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
      - apt-get update -y
      - apt-get install -y jq
  build:
    commands:
      - echo "Building Docker image..."
      - TIMESTAMP=$(date +%Y%m%d%H%M%S)
      - IMAGE_TAG="$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 7)-${TIMESTAMP}"
      - echo "Tagging the image as ${IMAGE_TAG}..."
      - docker build -t ${IMAGE_NAME}:${IMAGE_TAG} .
  post_build:
    commands:
      - echo "Generating SBOM and scanning for vulnerabilities..."
      - ./inspector-sbomgen/linux/amd64/inspector-sbomgen container --image ${IMAGE_NAME}:${IMAGE_TAG} --scan-sbom -o /tmp/sbom.json
      - echo "Checking for critical and high vulnerabilities..."
      - |
        critical_vulnerabilities=$(jq -r '.metadata.properties[] | select(.name=="amazon:inspector:sbom_scanner:critical_vulnerabilities") | .value' /tmp/sbom.json)
        high_vulnerabilities=$(jq -r '.metadata.properties[] | select(.name=="amazon:inspector:sbom_scanner:high_vulnerabilities") | .value' /tmp/sbom.json)
        if [ "$critical_vulnerabilities" -ne 0 ] || [ "$high_vulnerabilities" -ne 0 ]; then
          echo "Critical or high vulnerabilities detected. Failing the build."
          exit 1
        else
          echo "No critical or high vulnerabilities found."
          echo "Tagging and pushing the Docker image..."
          docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${IMAGE_NAME}:${IMAGE_TAG}
          docker push ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${IMAGE_NAME}:${IMAGE_TAG}
        fi
artifacts:
  files:
    - /tmp/sbom.json
```

### ECRの構築

- Dockerイメージ格納用にECRを構築します。

```tsx
    // ------------ Amazon ECR ---------------
    const ecrRepo = new ecr.Repository(this, "EcrRepo", {
      repositoryName: `poc-python-app`,
      imageTagMutability: ecr.TagMutability.IMMUTABLE,
      removalPolicy: cdk.RemovalPolicy.DESTROY // 検証用のため、スタック削除時にECR削除
    })
```

### Codeシリーズの構築

- 前述のbuildspec.ymlを用いて、Dockerイメージのビルド及び脆弱性検査を実施するCodeBuildを構築します。
- CodePipelineを構築し、CodeCommit及びCodeBuildを組み込みます。

```tsx
    // ------------ AWS CodeSeries ---------------
    // ---- AWS CodeCommit
    const gitRepo = codecommit.Repository.fromRepositoryArn(this, "GitRepo", props.codecommitArn)

    // ---- AWS CodeBuild
    // Create IAM Role
    const buildRole = new iam.Role(this, 'BuildRole', {
      roleName: `${props.prefix}-${props.envName}-role-build-docker`,
      assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
    });
    // 検証用のため、広めの権限を付与
    buildRole.addToPolicy(new iam.PolicyStatement({
      actions: [
        'inspector-scan:*',
        'ecr:*',
      ],
      resources: ['*'],
    }));

    // Create Docker Build Project
    const dockerBuildProject = new codebuild.Project(this, 'DockerBuildProject', {
      projectName: `${props.prefix}-${props.envName}-build-docker`,
      buildSpec: codebuild.BuildSpec.fromAsset(path.join(__dirname, "../../assets/buildspec.yml")),
      role: buildRole,
      environment: {
        privileged: true, // Dockerビルド用に特権付与
        buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
      },
      environmentVariables: {
        ACCOUNT_ID: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: props.env?.account
        },
        IMAGE_NAME: {
          type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          value: ecrRepo.repositoryName
        }
      },
    });

    // ---- AWS CodePipeline
    const pipeline = new codepipeline.Pipeline(this, 'Pipeline', {
      pipelineName: `${props.prefix}-${props.envName}-pipeline`,
    });
    // Add Source Stage
    const sourceOutput = new codepipeline.Artifact();
    pipeline.addStage({
      stageName: 'Source',
      actions: [
        new codepipeline_actions.CodeCommitSourceAction({
          actionName: 'CodeCommit_Source',
          repository: gitRepo,
          branch: "main",
          output: sourceOutput,
        }),
      ],
    });
    // Add Build Stage
    const scanOutput = new codepipeline.Artifact();
    pipeline.addStage({
      stageName: 'Scan_Build',
      actions: [
        new codepipeline_actions.CodeBuildAction({
          actionName: 'CodeBuild_Scan_Build_Docker',
          project: dockerBuildProject,
          input: sourceOutput,
          outputs: [scanOutput]
        }),
      ],
    });
```

### 動作確認

CodeCommitへのPushをトリガにCodePipelineを実行し、動作確認してみます。
なお、CodeCommitにはDockerfileを格納しておき、イメージ作成時にhighに分類される脆弱性を検出するようにしておきます。

- CodePipelineから呼び出されたCodeBuild内で、Dockerイメージに対する脆弱性検査を実施します。highに分類される脆弱性を検知したことから、パイプライン実行及びECRへのPushが失敗します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/5776a97c-34ce-ca37-8c1d-7d5e943729ef.png)

```log
Critical or high vulnerabilities detected. Failing the build.
```

- buildspecを修正して、Push中止対象とする脆弱性をcriticalに限定してみます。すると、パイプライン実行及びECRへのPushに成功します。

```yaml
      - echo "Generating SBOM and scanning for vulnerabilities..."
      - ./inspector-sbomgen/linux/amd64/inspector-sbomgen container --image ${IMAGE_NAME}:${IMAGE_TAG} --scan-sbom -o /tmp/sbom.json
      - echo "Checking for critical vulnerabilities..."
      - |
        critical_vulnerabilities=$(jq -r '.metadata.properties[] | select(.name=="amazon:inspector:sbom_scanner:critical_vulnerabilities") | .value' /tmp/sbom.json)
        if [ "$critical_vulnerabilities" -ne 0 ]; then
          echo "Critical vulnerabilities detected. Failing the build."
          exit 1
        else
          echo "No critical vulnerabilities found."
          echo "Tagging and pushing the Docker image..."
          docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${IMAGE_NAME}:${IMAGE_TAG}
          docker push ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${IMAGE_NAME}:${IMAGE_TAG}
        fi
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/02e19fcc-4991-4508-5ef4-fbe30dfbc6ca.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/d82b89b3-2557-afc8-0357-36c63f34fea5.png)
```log
No critical vulnerabilities found.
Tagging and pushing the Docker image...
```

- 脆弱性検査結果はCodeBuildのArtifactとしてS3にアップロードされます。CycloneDX形式のSBOMファイルになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3562653/d39180dc-fd68-afe2-e448-3bd73d8028ef.png)

```json
{
  "bomFormat": "CycloneDX",
  "components": [
    (一部略)
  ],
  "metadata": {
    "properties": [
      {
        "name": "amazon:inspector:sbom_scanner:critical_vulnerabilities",
        "value": "0"
      },
      {
        "name": "amazon:inspector:sbom_scanner:high_vulnerabilities",
        "value": "3"
      },
      {
        "name": "amazon:inspector:sbom_scanner:medium_vulnerabilities",
        "value": "2"
      },
      {
        "name": "amazon:inspector:sbom_scanner:low_vulnerabilities",
        "value": "0"
      },
      {
        "name": "amazon:inspector:sbom_scanner:other_vulnerabilities",
        "value": "0"
      }
    ],
    (一部略)
    "vulnerabilities": [
    ]
}
```

## 参考資料
- https://aws.amazon.com/jp/inspector/
- https://docs.aws.amazon.com/inspector/latest/user/cicd-custom.html
- https://docs.aws.amazon.com/inspector/latest/user/sbom-generator.html#install-sbomgen