Inspector のコンテナイメージスキャンを CodeBuild で実行してみた #AWSreInvent

Inspector v2 イメージスキャンの CI/CD 対応を記念して、 CodeBuild 上で動かしてみました!
2023.12.10

こんにちは! AWS 事業本部コンサルティング部のたかくに(@takakuni_)です。

re:Invent 2023 にて、 Amazon Inspector が CI/CD 内で実行可能になりました。

そこで今回は、 CodeBuild のビルド処理中に、 Inspector のコンテナイメージスキャンを試してみようと思います。

やってみる前に

まず、今回発表された Inspector の脆弱性スキャンは、従来提供されているようなコンテナイメージに対して直接スキャンするのではなく、 SBOM(ソフトウェア部品表) ファイルに対して、脆弱性スキャンをかけるような仕組みになります。そのため、脆弱性スキャンを実行する前に SBOM Generator で SBOM ファイルの生成が必要になります。

今回の機能に対応するために、 AWS から Amazon Inspector SBOM Generator (inspector-sbomgen) が提供開始されました。

Amazon Inspector SBOM Generator

Amazon Inspector SBOM Generator

Amazon Inspector SBOM Generator は先ほども記載とおり、 Inspector-Scan の ScanSbom API で必要な SBOM ファイルの生成ツールになります。

今回の ScanSbom API では、 CycloneDX v1.5 形式の SBOM ファイルが必要となり、 Amazon Inspector SBOM Generator は CycloneDX v1.5 形式での SBOM 生成をサポートしています。

何が言いたいかというと、 re:Inforce 2023 で発表された SBOM のエクスポート(CreateSbomExport) API は 執筆時点で、 CycloneDX v1.5 に対応していないため、注意が必要です。(SBOM 生成ツールに、こだわりがなければ、 Amazon Inspector SBOM Generator を使っていただくのが無難かと思います。)

いくつか気になる要件があるため、ピックアップしてみます。

性能

パフォーマンスを最大限に活かすため、推奨されている CPU, メモリの記載があります。ちょっと大きいなぁと思った方は、ステージ分けるのも、アリだと思います。

For best performance, we recommend running the binary from a system with these minimum hardware specs: - 4x core CPU - 8 GB RAM

OS

現時点で、サポートされている OS は、 現状 Linux のみとなります。また、コンテナイメージを扱うため、ビルドされているマシンに Docker, Podman, containerd などがインストールされている必要があります。

特権モードが必要かどうか

Amazon Inspector SBOM Generator は、コンテナイメージのビルドや、アクセス権限の昇格を利用しないため、特権モードは必要ないです。

ただし、シフトレフトな考え方だとコンテナリポジトリへのプッシュ前、つまり、ローカルでコンテナイメージをスキャンするのがより好ましいため、同一ビルド処理内で組むのもありかと思います。

以下はビルド処理の一例です。

  • コンテナイメージのビルド
  • ローカルでビルドしたイメージをスキャン
    • 緊急度に応じた脆弱性があればビルドの停止
    • なければ、リポジトリへプッシュ

と言ったものの、緊急度に応じた脆弱性の中でも、すぐに対応が難しいケースもあると思うので、スキャンとプッシュの前後関係は必要に応じて入れ替えていただくといいと思います。

大切なのは、デプロイ前に脆弱性の有無を把握し、検出された脆弱性をどう扱うのかだと私は思います。

コンテナイメージ

コンテナイメージに対してもリミットがあります。

  • イメージサイズが 5 GB を超える場合
  • レイヤー数が 60 を超える場合
  • インストールされているパッケージ数が 2,000 を超える場合

Sbomgen can’t scan container images if they are greater than 5 GB in size, have more than 60 layers, or over 2,000 installed packages.

やってみる

長くなりましたが、それでは CodeBuild 内で、 Inspector のコンテナイメージスキャンを組み込んでみようと思います。

作成する構成は以下の通りです。今回は ECR へプッシュした後に、プッシュしたイメージをスキャンしてみようと思います。

最後に承認ステージを挟んでスキャン結果を確認してみます。

今回のデプロイは、ほぼほぼ Terraform で担います。 terraform apply中に、イメージスキャンに関して、設定した部分をお読みいただけると幸いです。

最終的なコードは以下になります。

IAM

今回は、 ECR にプッシュされたイメージに対して、 SBOM の生成を行うため、 ECR の権限が必要になります。また、 Inspector-Scan API は従来の、Inspector v2 API とは別のスキーマ(inspector-scan)で API 提供されいてるため注意です。What's new で紹介されていた、「Inspector を有効化することなく利用可能」の意味は、この部分を指しているのだと思います。

私が今回、イメージスキャン用の CodeBuild に設定した IAM Policy は以下になります。ワイルドカード部分は、もっと絞ることが可能ですが、本筋ではないため省略しました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Artfact",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:GetObjectVersion",
        "s3:GetBucketAcl",
        "s3:GetBucketLocation"
      ],
      "Resource": "*"
    },
    {
      "Sid": "BuildLog",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    },
    {
      "Sid": "PackageVulnerabilityScanning",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchGetImage",
        "ecr:BatchGetRepositoryScanningConfiguration",
        "ecr:GetAuthorizationToken",
        "ecr:GetDownloadUrlForLayer",
        "ecr:GetRegistryScanningConfiguration",
        "inspector-scan:ScanSbom"
      ],
      "Resource": "*"
    }
  ]
}

イメージタグの受け渡し

イメージスキャンを行うステージに、「どのイメージが直近のビルドしたイメージなのか?」、情報を受け渡す必要があります。

今回は、 CodePipeline で exported-variables を利用して、受け渡しを行いました。

そのためには、イメージビルド側の buildspec で、イメージ URI のエクスポートを行います。

codebuild/image_build.yaml

version: 0.2
env:
  variables:
    DOCKER_BUILDKIT: '1'
    AWS_PAGER: ''
  exported-variables:
    - IMAGE_URL

phases:
  pre_build:
    commands:
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-10)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG codebuild
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - IMAGE_URL=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

エクスポートした、IMAGE_URLの受け渡しは以下の部分で行いました。

codepipeline.tf

#################################################
# CodePipeline
#################################################
resource "aws_codepipeline" "main" {
  name = "${local.prefix}-pipeline"

  role_arn = aws_iam_role.codepipeline.arn

  artifact_store {
    location = aws_s3_bucket.artifact.bucket
    type     = "S3"
  }

  stage {
    name = "Source"
    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeCommit"
      version          = 1
      output_artifacts = ["source_output"]
      configuration = {
        RepositoryName       = aws_codecommit_repository.main.repository_name
        BranchName           = "main"
        OutputArtifactFormat = "CODE_ZIP"
        PollForSourceChanges = "false"
      }
    }
  }

  stage {
    name = "Build"
    action {
      name             = "Build"
      category         = "Build"
      namespace        = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = 1
      input_artifacts  = ["source_output"]
      output_artifacts = ["build_output"]

      configuration = {
        ProjectName = aws_codebuild_project.image_build.name
      }
    }
  }

  stage {
    name = "Scan"
    action {
      name             = "Scan"
      category         = "Build"
      namespace        = "Scan"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = 1
      input_artifacts  = ["source_output"]
      output_artifacts = ["scan_output"]
      

      configuration = {
        ProjectName = aws_codebuild_project.image_scan.name
        EnvironmentVariables = jsonencode([
          {
            name  = "IMAGE_URL"
            value = "#{Build.IMAGE_URL}"
            type  = "PLAINTEXT"
          }
        ])
      }
    }
  }

  stage {
    name = "Approval"
    action {
      name     = "Approval"
      category = "Approval"
      owner    = "AWS"
      provider = "Manual"
      version  = 1

      configuration = {
        CustomData         = "Container image scan result."
        ExternalEntityLink = "#{Scan.BUILD_URL}"
      }
    }
  }
}

AWS CLI

AWS CodeBuild で利用されていた、 マネージド型イメージ (public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:5.0-23.07.28) の AWS CLI バージョンは、 2.13.35 でした。

[Container] 2023/12/08 15:25:54.940259 Waiting for agent ping
[Container] 2023/12/08 15:25:55.941284 Waiting for DOWNLOAD_SOURCE
[Container] 2023/12/08 15:25:58.048304 Phase is DOWNLOAD_SOURCE
[Container] 2023/12/08 15:25:58.051089 CODEBUILD_SRC_DIR=/codebuild/output/src1876042495/src
[Container] 2023/12/08 15:25:58.051520 YAML location is /codebuild/output/src1876042495/src/image_scan.yaml
[Container] 2023/12/08 15:25:58.052224 Selecting shell bash as specified in buildspec.
[Container] 2023/12/08 15:25:58.053622 Setting HTTP client timeout to higher timeout for S3 source
[Container] 2023/12/08 15:25:58.053708 Processing environment variables
[Container] 2023/12/08 15:25:58.264585 No runtime version selected in buildspec.
[Container] 2023/12/08 15:25:58.305904 Moving to directory /codebuild/output/src1876042495/src
[Container] 2023/12/08 15:25:58.308790 Unable to initialize cache download: no paths specified to be cached
[Container] 2023/12/08 15:25:58.490902 Configuring ssm agent with target id: codebuild:f2ea7dbb-99c6-4369-81ab-444d04f4a64d
[Container] 2023/12/08 15:25:58.534162 Successfully updated ssm agent configuration
[Container] 2023/12/08 15:25:58.534637 Registering with agent
[Container] 2023/12/08 15:25:58.534656 Phases found in YAML: 4
[Container] 2023/12/08 15:25:58.534677  INSTALL: 12 commands
[Container] 2023/12/08 15:25:58.534682  PRE_BUILD: 2 commands
[Container] 2023/12/08 15:25:58.534686  BUILD: 2 commands
[Container] 2023/12/08 15:25:58.534696  POST_BUILD: 1 commands
[Container] 2023/12/08 15:25:58.535047 Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
[Container] 2023/12/08 15:25:58.535058 Phase context status code:  Message: 
[Container] 2023/12/08 15:25:58.637017 Entering phase INSTALL
[Container] 2023/12/08 15:25:58.637526 Running command aws --version
aws-cli/2.13.35 Python/3.11.6 Linux/4.14.291-218.527.amzn2.x86_64 exec-env/AWS_ECS_EC2 exe/x86_64.amzn.2023 prompt/off

inspector-scan API は、バージョン 2.13.38 から利用可能なため、現時点ではアップデートしてあげる必要があります。

codebuild/image_scan.yaml

version: 0.2
env:
  shell: bash
  variables:
    DOCKER_BUILDKIT: '1'
    AWS_PAGER: ''
  exported-variables:
    - BUILD_URL

phases:
  install:
    commands:
      - aws --version
      - echo AWS CLI update...
      - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
      - unzip awscliv2.zip
      - ./aws/install --bin-dir /root/.pyenv/shims --install-dir /usr/local/aws-cli --update
      - aws --version
      - echo Install Amazon Inspector SBOM Generator...
      - curl -O https://amazon-inspector-sbomgen.s3.amazonaws.com/latest/linux/amd64/inspector-sbomgen.zip
      - unzip inspector-sbomgen.zip
      - mv inspector-sbomgen-* inspector-sbomgen-latest
      - chmod +x inspector-sbomgen-latest/linux/amd64/inspector-sbomgen
      - ./inspector-sbomgen-latest/linux/amd64/inspector-sbomgen --version
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - ./inspector-sbomgen-latest/linux/amd64/inspector-sbomgen container --image $IMAGE_URL --outfile /tmp/sbom.json --quiet
      - aws inspector-scan scan-sbom --sbom file:///tmp/sbom.json --output-format INSPECTOR --query 'sbom.vulnerabilities'
  post_build:
    commands:
      - export BUILD_URL=$CODEBUILD_BUILD_URL

コードのプッシュ

そろそろ、 Terraform での AWS インフラが出来上がったのではないでしょうか。 ここからは、 Dockerfile や buildspec の配置を行なっていきます。

CodeCommit に送信したいアセットは、 codebuild フォルダ一式になります。

takakuni@inspector % tree . -L 1 
.
├── README.md
├── codebuild
├── codecommit.tf
├── codepipeline.tf
├── ecr.tf
├── event_pattern
├── image_build.tf
├── image_scan.tf
├── local.tf
├── policy_document
├── providers.tf
├── terraform.tfstate
└── terraform.tfstate.backup

CodeCommit に一式コピーする形で、 git push を行います。

cd ..
git clone ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/inspector-scan-repo
cp -R inspector-cicd-codebuild/codebuild inspector-scan-repo
cd inspector-scan-repo
git add . && git commit -m '[add] code asset added' && git push

挙動を見てみる

API output formats に記載の通り、inspector-scan API では、 SBOM をもとに脆弱性のある OSS ライブラリの検出を行ってくれます。

{
  "status": "SBOM parsed successfully, 1 vulnerability found",
  "inspector": {
    "messages": [
      {
        "name": "foo",
        "purl": "pkg:maven/foo@1.0.0", // Will not exist in output if missing in sbom
        "info": "Component skipped: no rules found."
      }
    ],
    "vulnerability_count": {
      "critical": 1,
      "high": 0,
      "medium": 0,
      "low": 0
    },
    "vulnerabilities": [
      {
        "id": "CVE-2021-44228",
        "severity": "critical",
        "source": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228",
        "related": [
          "SNYK-JAVA-ORGAPACHELOGGINGLOG4J-2314720",
          "GHSA-jfh8-c2jp-5v3q"
        ],
        "description": "Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled. From log4j 2.15.0, this behavior has been disabled by default. From version 2.16.0 (along with 2.12.2, 2.12.3, and 2.3.1), this functionality has been completely removed. Note that this vulnerability is specific to log4j-core and does not affect log4net, log4cxx, or other Apache Logging Services projects.",
        "references": [
          "https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-00646.html",
          "https://support.apple.com/kb/HT213189",
          "https://msrc-blog.microsoft.com/2021/12/11/microsofts-response-to-cve-2021-44228-apache-log4j2/",
          "https://logging.apache.org/log4j/2.x/security.html",
          "https://www.debian.org/security/2021/dsa-5020",
          "https://cert-portal.siemens.com/productcert/pdf/ssa-479842.pdf",
          "https://www.oracle.com/security-alerts/alert-cve-2021-44228.html",
          "https://www.oracle.com/security-alerts/cpujan2022.html",
          "https://cert-portal.siemens.com/productcert/pdf/ssa-714170.pdf",
          "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/M5CSVUNV4HWZZXGOKNSK6L7RPM7BOKIB/",
          "https://cert-portal.siemens.com/productcert/pdf/ssa-397453.pdf",
          "https://cert-portal.siemens.com/productcert/pdf/ssa-661247.pdf",
          "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/VU57UJDCFIASIO35GC55JMKSRXJMCDFM/",
          "https://www.oracle.com/security-alerts/cpuapr2022.html",
          "https://twitter.com/kurtseifried/status/1469345530182455296",
          "https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-apache-log4j-qRuKNEbd",
          "https://lists.debian.org/debian-lts-announce/2021/12/msg00007.html",
          "https://www.kb.cert.org/vuls/id/930724"
        ],
        "created": "2021-12-10T10:15:00Z",
        "updated": "2023-04-03T20:15:00Z",
        "properties": {
          "cisa_kev_date_added": "2021-12-10T00:00:00Z",
          "cisa_kev_date_due": "2021-12-24T00:00:00Z",
          "cwes": [
            400,
            20,
            502
          ],
          "cvss": [
            {
              "source": "NVD",
              "severity": "critical",
              "cvss3_base_score": 10.0,
              "cvss3_base_vector": "AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
              "cvss2_base_score": 9.3,
              "cvss2_base_vector": "AC:M/Au:N/C:C/I:C/A:C"
            },
            {
              "source": "SNYK",
              "severity": "critical",
              "cvss3_base_score": 10.0,
              "cvss3_base_vector": "AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/E:H"
            },
            {
              "source": "GITHUB",
              "severity": "critical",
              "cvss3_base_score": 10.0,
              "cvss3_base_vector": "AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"
            }
          ],
          "epss": 0.97565,
          "exploit_available": true,
          "exploit_last_seen_in_public": "2023-03-06T00:00:00Z"
        },
        "affects": [
          {
            "installed_version": "pkg:maven/org.apache.logging.log4j/log4j-core@2.12.1",
            "fixed_version": "2.15.0",
            "path": "/home/dev/foo.jar"
          }
        ]
      }
    ]
  }
}

今回、 Scan で利用した buildspec は、 vulnerabilities だけ抽出するような形で出力しています。

image_scan.yaml

version: 0.2
env:
  shell: bash
  variables:
    DOCKER_BUILDKIT: '1'
    AWS_PAGER: ''
  exported-variables:
    - BUILD_URL

phases:
  install:
    commands:
      - aws --version
      - echo AWS CLI update...
      - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
      - unzip awscliv2.zip
      - ./aws/install --bin-dir /root/.pyenv/shims --install-dir /usr/local/aws-cli --update
      - aws --version
      - echo Install Amazon Inspector SBOM Generator...
      - curl -O https://amazon-inspector-sbomgen.s3.amazonaws.com/latest/linux/amd64/inspector-sbomgen.zip
      - unzip inspector-sbomgen.zip
      - mv inspector-sbomgen-* inspector-sbomgen-latest
      - chmod +x inspector-sbomgen-latest/linux/amd64/inspector-sbomgen
      - ./inspector-sbomgen-latest/linux/amd64/inspector-sbomgen --version
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - ./inspector-sbomgen-latest/linux/amd64/inspector-sbomgen container --image $IMAGE_URL --outfile /tmp/sbom.json --quiet
      - aws inspector-scan scan-sbom --sbom file:///tmp/sbom.json --output-format INSPECTOR --query 'sbom.vulnerabilities'
  post_build:
    commands:
      - export BUILD_URL=$CODEBUILD_BUILD_URL

CodeBuild 側でも、検出できていることがわかります。

書き方によっては、以下のように 重要度が Critical, High の時のみ出力するようなこともできます。

version: 0.2
# 省略
  build:
    commands:
      - ./inspector-sbomgen-latest/linux/amd64/inspector-sbomgen container --image $IMAGE_URL --outfile /tmp/sbom.json --quiet
      # - aws inspector-scan scan-sbom --sbom file:///tmp/sbom.json --output-format INSPECTOR --query 'sbom.vulnerabilities'
      # 重要度が critical または high の場合、出力する
      - aws inspector-scan scan-sbom --sbom file:///tmp/sbom.json --output-format INSPECTOR --query 'sbom.vulnerabilities[?severity==`critical` || severity==`high`]'

まとめ

以上、「Inspector のコンテナイメージスキャンを CodeBuild で実行してみた」でした。

inspector-sbomgen の正体と、実際に カスタム CI に組み込むにはどうすればいいかが、よくわかりました。実際に触ってみるのが、わかりやすいですね。

この記事がどなたかの参考になれば幸いです。AWS 事業本部コンサルティング部のたかくに(@takakuni_)でした!