AWS CodeBuildでSnyk IaCを使用してみた

# はじめに

はじめまして！たかくにと申します！

この記事は「Snykを使って開発者セキュリティに関する記事を投稿しよう！」というテーマに沿って投稿しています！

https://qiita.com/official-events/786b978e4a1a28fad312

普段もIaCやAWSを中心にブログを書いているので、ご覧いただけると嬉しいです。

https://dev.classmethod.jp/author/takakuni/

突然ですが、みなさん！IaC使っていますかー?　IaC便利ですよね！
Googleトレンドでも、年々IaCの人気が増加していることが伺えます。
![スクリーンショット 2022-07-16 13.51.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371570/53f9e7b6-83c2-e042-c3d6-3e92bdb2fefb.png)
では、IaCで使っているコードのセキュリティを気にしていますか？
気にしていなくても、これから気にすれば大丈夫です！

今回は、AWS CodeBuildでSnyk IaCを使用してみた感想、詰まった点をご紹介します！

まずはじめに、Snyk IaCは、IaCのコードに対してセキュリティスキャンを行うツールです。コードに潜在した脆弱性を是正することで、各コードの品質を整えることができます。
具体的な使い方は私のブログをはじめ、さまざまな方々が書いて頂いている（これから書いて頂ける）と思うので今回は省略します。

参考までに私のブログを載せておきます。

https://dev.classmethod.jp/articles/snyk-infrastructure-as-code-security-overview/

# 構成

今回は、TerraformをデプロイするCICDパイプラインがあるとして、CodeBuildプロジェクトにSnyk IaCを組み込む際に詰まった点や解決方法をご紹介できればと思います。

具体的な構成図は次のとおりです。赤枠の線を中心にご紹介できればと思います。
![snyk_iac_origin_framework_2 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/371570/a3b0ecad-da54-b4ec-b2e6-92eaaac213ae.png)

### 2022/7/19 追記

検証で使用したCloudFormationファイルをアップします。

<details><summary>クリックで表示</summary><div>

```pipeline.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: Terraform pipeline template.
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: "Project Name Prefix"
      Parameters:
        - PrjPrefix
    - Label:
        default: "Repository Configuration"
      Parameters:
        - BranchName
    - Label:
        default: "Docker Login Profile"
      Parameters:
      - DockerHubUserName
      - DockerHubUserPassword
    - Label:
        default: "Snyk Organization Profile"
      Parameters:
      - SnykOrganizationId
      - SnykOrganizationApiKey
    - Label:
        default: "Logs Configuration"
      Parameters:
        - BuildLogsRationDay
    - Label:
        default: "terraform build configuration"
      Parameters:
        - TerraformVersion
    - Label:
        default: "CodePipeline notification configuration"
      Parameters:
        - CodePipelineNotificationEmail

Parameters:
  PrjPrefix:
    Type: String
    Description: "Enter resource prefix. (ex. terraform-dev)"
    Default: "terraform-dev"
  BranchName:
    Type: String
    Description: "Enter the name of the branch where you want to store exclude.yml"
    Default: "main"
  DockerHubUserName:
    Type: String
    Description: "Enter Docker Hub loging user name. (ex. terraform)"
    NoEcho: true
  DockerHubUserPassword:
    Type: String
    Description: "Enter Docker Hub loging user password. (ex. P@ssw0rd) If you enable MFA in Docker Hub, generate AccessToken and enter instead of user password."
    NoEcho: true
  SnykOrganizationId:
    Type: String
    Description: "Enter Snyk Organization ID."
    NoEcho: true
  SnykOrganizationApiKey:
    Type: String
    Description: "Enter Snyk Organization API Key."
    NoEcho: true
  BuildLogsRationDay:
    Type: Number
    Description: "Enter build log retention period."
    Default: 90
    AllowedValues:
      - 1
      - 3
      - 5
      - 7
      - 14
      - 30
      - 60
      - 90
      - 120
      - 150
      - 180
      - 365
      - 400
      - 545
      - 731
      - 1827
      - 3653
  TerraformVersion:
    Type: String
    Description: "Enter the version of Terraform that use with CodeBuild."
    Default: "1.2.0"
  CodePipelineNotificationEmail:
    Type: String
    Description: "Enter the email of CodePipeline notification email when pipeline failed."
    Default: "examples@example.com"

Resources:
#################################
# KMS (CloudWatch Logs)
#################################
  KeyCWL:
    Type: AWS::KMS::Key
    Properties:
      Description: "Terraform pipeline Build Logs Key"
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
          - Sid: Allow use of the key from CWL
            Effect: "Allow"
            Principal:
              Service: !Sub "logs.${AWS::Region}.amazonaws.com"
            Action:
              - "kms:Encrypt"
              - "kms:Decrypt"
              - "kms:ReEncrypt"
              - "kms:GenerateDataKey"
              - "kms:Describe"
            Resource: "*"
            Condition:
              ArnLike:
                kms:EncryptionContext:aws:logs:arn: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*"
      KeySpec: "SYMMETRIC_DEFAULT"
      KeyUsage: "ENCRYPT_DECRYPT"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-logs-key"

  KeyAliasCWL:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${PrjPrefix}-tf-pipeline-logs-key"
      TargetKeyId: !Ref KeyCWL

#################################
# KMS (S3 arthifact)
#################################
  KeyS3Arthifact:
    Type: AWS::KMS::Key
    Properties:
      Description: "Terraform pipeline arthifact Key"
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
      KeySpec: "SYMMETRIC_DEFAULT"
      KeyUsage: "ENCRYPT_DECRYPT"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-artifact-key"

  KeyAliasS3Arthifact:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${PrjPrefix}-tf-pipeline-artifact-key"
      TargetKeyId: !Ref KeyS3Arthifact

#################################
# KMS (S3 tfstate)
#################################
  KeyS3Tfstate:
    Type: AWS::KMS::Key
    Properties:
      Description: "Terraform pipeline tfstate Key"
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
      KeySpec: "SYMMETRIC_DEFAULT"
      KeyUsage: "ENCRYPT_DECRYPT"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-tfstate-key"

  KeyAliasS3Tfstate:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${PrjPrefix}-tf-pipeline-tfstate-key"
      TargetKeyId: !Ref KeyS3Tfstate

#################################
# KMS (Secrets Manager)
#################################
  KeySecretsManager:
    Type: AWS::KMS::Key
    Properties:
      Description: "Terraform pipeline Secrets Manager Key"
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
      KeySpec: "SYMMETRIC_DEFAULT"
      KeyUsage: "ENCRYPT_DECRYPT"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-secretsmanager-key"

  KeyAliasSecretsManager:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${PrjPrefix}-tf-pipeline-secretsmanager-key"
      TargetKeyId: !Ref KeySecretsManager

#################################
# KMS (DynamoDB)
#################################
  KeyDynamoDB:
    Type: AWS::KMS::Key
    Properties:
      Description: "Terraform pipeline DynamoDB Key"
      Enabled: true
      EnableKeyRotation: true
      KeyPolicy:
        Version: "2012-10-17"
        Statement:
          - Sid: "Enable IAM User Permissions"
            Effect: "Allow"
            Principal:
              AWS: !Sub "arn:${AWS::Partition}:iam::${AWS::AccountId}:root"
            Action: "kms:*"
            Resource: "*"
      KeySpec: "SYMMETRIC_DEFAULT"
      KeyUsage: "ENCRYPT_DECRYPT"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-dynamoDB-key"

  KeyAliasDynamoDB:
    Type: AWS::KMS::Alias
    Properties:
      AliasName: !Sub "alias/${PrjPrefix}-tf-pipeline-dynamoDB-key"
      TargetKeyId: !Ref KeyDynamoDB

#################################
# CodeCommit
#################################
  CodeCommit:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Sub "${PrjPrefix}-tf-repo"
      RepositoryDescription: !Sub "${PrjPrefix}-tf-repo"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-repo"

#################################
# Custom Resource
#################################
  PutExcludeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        - "arn:aws:iam::aws:policy/AWSCodeCommitPowerUser"
      RoleName: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude-role"

  PutExcludeLog:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
      KmsKeyId: !GetAtt KeyCWL.Arn

  PutExcludeFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
      Code:
        ZipFile: |
          import json
          import boto3
          import cfnresponse

          def handler(event, context):
              try:
                  repository = event['ResourceProperties']['RepositoryName']
                  branch = event['ResourceProperties']['BranchName']
                  content = event['ResourceProperties']['FileContent'].encode()
                  path = event['ResourceProperties']['FilePath']

                  if event['RequestType'] == 'Create':
                      codecommit = boto3.client('codecommit')
                      response = codecommit.put_file(
                          repositoryName=repository,
                          branchName=branch,
                          fileContent=content,
                          filePath=path,
                          commitMessage='Initial Commit',
                          name='Your Lambda Helper'
                      )
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, response)
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
                  if event['RequestType'] == 'Update':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
              except Exception as e:
                  print(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt PutExcludeRole.Arn
      Runtime: "python3.9"
      Timeout: 60
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
    DependsOn: PutExcludeLog

  PutExclude:
    Type: Custom::CodeCommitPutExclude
    Properties:
      ServiceToken: !GetAtt PutExcludeFunction.Arn
      RepositoryName: !GetAtt CodeCommit.Name
      BranchName: !Ref BranchName
      FileContent: "ignore:"
      FilePath: ".snyk"

#################################
# Secrets Manager
#################################
  SnykOrganizationProfile:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: "Snyk Web UI Organization profile for terraform pipelines"
      Name: !Sub "${PrjPrefix}/snyk_org"
      SecretString: !Sub '{"OrgId":"${SnykOrganizationId}","OrgApi":"${SnykOrganizationApiKey}"}'
      KmsKeyId: !GetAtt KeySecretsManager.Arn
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}/snyk_org"

#################################
# S3 Bucket (tfstate)
#################################
  BucketTfstate:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub "${PrjPrefix}-tf-pipeline-tfstate"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "aws:kms"
              KMSMasterKeyID: !GetAtt KeyS3Tfstate.Arn
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: "Enabled"
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-tfstate"

  BucketPolicyTfstate:
      Type: AWS::S3::BucketPolicy
      Properties: 
        Bucket: !Ref BucketTfstate
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Sid: "AllowSSLRequestsOnly"
              Effect: "Deny"
              Principal: "*"
              Action: "s3:*"
              Resource:
                - !GetAtt BucketTfstate.Arn
                - !Sub
                  - "${BucketArn}/*"
                  - { BucketArn: !GetAtt BucketTfstate.Arn }
              Condition:
                Bool:
                  aws:SecureTransport: "false"

#################################
# DynamoDB table (state lock)
#################################
  DDBTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub "${PrjPrefix}-tf-pipeline-state-lock-table"
      KeySchema:
        - AttributeName: "LockID"
          KeyType: "HASH"
      AttributeDefinitions:
        - AttributeName: "LockID"
          AttributeType: "S"
      BillingMode: "PAY_PER_REQUEST"
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true
      SSESpecification:
        KMSMasterKeyId: !GetAtt KeyDynamoDB.Arn
        SSEEnabled: true
        SSEType: "KMS"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-state-lock-table"

#################################
# S3 Bucket (Artifact)
#################################
  BucketArtifacts:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub "${PrjPrefix}-tf-pipeline-artifacts"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "aws:kms"
              KMSMasterKeyID: !GetAtt KeyS3Arthifact.Arn
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerEnforced
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-artifacts"

  BucketPolicyArtifacts:
      Type: AWS::S3::BucketPolicy
      Properties: 
        Bucket: !Ref BucketArtifacts
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Sid: "AllowSSLRequestsOnly"
              Effect: "Deny"
              Principal: "*"
              Action: "s3:*"
              Resource:
                - !GetAtt BucketArtifacts.Arn
                - !Sub
                  - "${BucketArn}/*"
                  - { BucketArn: !GetAtt BucketArtifacts.Arn }
              Condition:
                Bool:
                  aws:SecureTransport: "false"

#################################
# CloudWatch Logs (CodeBuild terraform plan)
#################################
  CWLTfplan:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-tfplan-project"
      RetentionInDays: !Ref BuildLogsRationDay
      KmsKeyId: !GetAtt KeyCWL.Arn
      Tags: 
        - Key: "Name"
          Value: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-tfplan-project"

#################################
# CodeBuild (terraform plan)
#################################
  PolicyTfplan:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${PrjPrefix}-tf-build-tfplan-project-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "BuildLogs"
            Effect: "Allow"
            Action:
              - "logs:CreateLogGroup"
              - "logs:CreateLogStream"
              - "logs:PutLogEvents"
            Resource:
              - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CWLTfplan}"
              - !GetAtt CWLTfplan.Arn
          - Sid: "KmsKey"
            Effect: "Allow"
            Action:
              - "kms:Decrypt"
              - "kms:Encrypt"
              - "kms:ReEncrypt"
              - "kms:GenerateDataKey"
            Resource:
              - !GetAtt KeyS3Arthifact.Arn
              - !GetAtt KeyS3Tfstate.Arn
              - !GetAtt KeyDynamoDB.Arn
          - Sid: "S3Tfstate"
            Effect: "Allow"
            Action:
              - "s3:PutObject*"
              - "s3:DeleteObject*"
            Resource:
              - !Sub
                - "${BucketArn}/*"
                - { BucketArn: !GetAtt BucketTfstate.Arn }
          - Sid: "S3Artifact"
            Effect: "Allow"
            Action:
              - "s3:PutObject"
              - "s3:GetObject"
              - "s3:GetObjectVersion"
              - "s3:GetBucketAcl"
              - "s3:GetBucketLocation"
            Resource:
              - !GetAtt BucketArtifacts.Arn
              - !Sub
                - "${BucketArn}/*"
                - { BucketArn: !GetAtt BucketArtifacts.Arn }
          - Sid: "DynamoDB"
            Effect: "Allow"
            Action:
              - "dynamodb:PutItem"
              - "dynamodb:DeleteItem"
            Resource: !GetAtt DDBTable.Arn

  RoleTfplan:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-build-tfplan-project-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "codebuild.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref PolicyTfplan
        - "arn:aws:iam::aws:policy/ReadOnlyAccess"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfplan-project-role"

  ProjectTfplan:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub "${PrjPrefix}-tf-build-tfplan-project"
      Description: "Execute terraform plan command."
      Source:
        Type: "CODEPIPELINE"
        BuildSpec: |
          version: 0.2
          env:
            exported-variables:
              - BUILD_URL
          phases:
            pre_build:
              commands:
                - "terraform init -input=false -no-color"
            build:
              commands:
                - "terraform plan -input=false -no-color -out=tfplan.binary"
            post_build:
              commands:
                - "terraform show -no-color -json tfplan.binary > tfplan.json"
                - "export BUILD_URL=`echo $CODEBUILD_BUILD_URL`"
          artifacts:
            files:
              - "tfplan.binary"
              - "tfplan.json"
      Artifacts:
        Type: "CODEPIPELINE"
      Cache:
        Type: "LOCAL"
        Modes:
          - "LOCAL_DOCKER_LAYER_CACHE"
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: !Sub "public.ecr.aws/hashicorp/terraform:${TerraformVersion}"
        ImagePullCredentialsType: "CODEBUILD"
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: "ENABLED"
          GroupName: !Ref CWLTfplan
      EncryptionKey: !GetAtt KeyS3Arthifact.Arn
      ResourceAccessRole: !GetAtt RoleTfplan.Arn
      ServiceRole: !GetAtt RoleTfplan.Arn
      TimeoutInMinutes: 60
      Visibility: "PRIVATE"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfplan-project"

#################################
# CloudWatch Logs (CodeBuild Snyk IaC)
#################################
  CWLSnykIaC:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-snyk-iac-project"
      RetentionInDays: !Ref BuildLogsRationDay
      KmsKeyId: !GetAtt KeyCWL.Arn
      Tags: 
        - Key: "Name"
          Value: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-snyk-iac-project"

#################################
# CodeBuild (Snyk IaC)
#################################
  PolicySnykIaC:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${PrjPrefix}-tf-build-snyk-iac-project-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "BuildLogs"
            Effect: "Allow"
            Action:
              - "logs:CreateLogGroup"
              - "logs:CreateLogStream"
              - "logs:PutLogEvents"
            Resource:
              - !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CWLSnykIaC}"
              - !GetAtt CWLSnykIaC.Arn
          - Sid: "BuildReports"
            Effect: "Allow"
            Action:
              - "codebuild:CreateReportGroup"
              - "codebuild:CreateReport"
              - "codebuild:UpdateReport"
              - "codebuild:BatchPutTestCases"
              - "codebuild:BatchPutCodeCoverages"
            Resource:
              - !Sub "arn:${AWS::Partition}:codebuild:${AWS::Region}:${AWS::AccountId}:report-group/${PrjPrefix}-tf-build-snyk-iac-project-reports"
          - Sid: "GitPull"
            Effect: "Allow"
            Action: "codecommit:GitPull"
            Resource: !GetAtt CodeCommit.Arn
          - Sid: "GetSecretValue"
            Effect: "Allow"
            Action: "secretsmanager:GetSecretValue"
            Resource:
              - !Ref SnykOrganizationProfile
          - Sid: "KmsKey"
            Effect: "Allow"
            Action:
              - "kms:Decrypt"
              - "kms:DescribeKey"
              - "kms:Encrypt"
              - "kms:ReEncrypt"
              - "kms:GenerateDataKey"
            Resource:
              - !GetAtt KeyS3Arthifact.Arn
              - !GetAtt KeySecretsManager.Arn
          - Sid: "S3Artifact"
            Effect: "Allow"
            Action:
              - "s3:PutObject"
              - "s3:GetObject"
              - "s3:GetObjectVersion"
              - "s3:GetBucketAcl"
              - "s3:GetBucketLocation"
            Resource:
              - !GetAtt BucketArtifacts.Arn
              - !Sub
                - "${BucketArn}/*"
                - { BucketArn: !GetAtt BucketArtifacts.Arn }

  RoleSnykIaC:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-build-snyk-iac-project-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "codebuild.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref PolicySnykIaC
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-snyk-iac-project-role"

  ProjectSnykIaC:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub "${PrjPrefix}-tf-build-snyk-iac-project"
      Description: "Analyze the code for vulnerabilities using Snyk IaC."
      Source:
        Type: "CODEPIPELINE"
        BuildSpec: |
          version: 0.2
          env:
            exported-variables:
              - BUILD_URL
          phases:
            install:
              runtime-versions:
                golang: 1.14
              commands:
                - curl https://static.snyk.io/cli/latest/snyk-linux -o snyk
                - "chmod +x ./snyk"
                - "mv ./snyk /usr/local/bin/"
            pre_build:
              commands:
                - "printenv"
                - "mkdir -p ${SRC_DIR}"
                - "cp -p tfplan.json ${SRC_DIR}/"
                - "cd ${SRC_DIR}"
                - "echo Executing Snyk IaC"
            build:
              commands:
                - "snyk iac test tfplan.json --report || true"
            post_build:
              commands:
                - "export BUILD_URL=`echo $CODEBUILD_BUILD_URL`"
      Artifacts:
        Type: "CODEPIPELINE"
      Cache:
        Type: "LOCAL"
        Modes:
          - "LOCAL_DOCKER_LAYER_CACHE"
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
        ImagePullCredentialsType: "CODEBUILD"
        EnvironmentVariables:
          - Name: "SNYK_TOKEN"
            Type: "SECRETS_MANAGER"
            Value: !Sub "${SnykOrganizationProfile}:OrgApi"
          - Name: "SNYK_CFG_ORG"
            Type: "SECRETS_MANAGER"
            Value: !Sub "${SnykOrganizationProfile}:OrgId"
          - Name: "SRC_DIR"
            Type: "PLAINTEXT"
            Value: !Sub "${PrjPrefix}-tf-build-snyk-iac-project"
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: "ENABLED"
          GroupName: !Ref CWLSnykIaC
      EncryptionKey: !GetAtt KeyS3Arthifact.Arn
      ResourceAccessRole: !GetAtt RoleSnykIaC.Arn
      ServiceRole: !GetAtt RoleSnykIaC.Arn
      TimeoutInMinutes: 60
      Visibility: "PRIVATE"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfsec-project"

  ReportGroupSnykIaC:
    Type: AWS::CodeBuild::ReportGroup
    Properties:
      Name: !Sub "${ProjectSnykIaC}-reports"
      Type: "TEST"
      DeleteReports: true
      ExportConfig:
        ExportConfigType: "NO_EXPORT"
      Tags: 
        - Key: "Name"
          Value: !Sub "${ProjectSnykIaC}-reports"

#################################
# CloudWatch Logs (CodeBuild terraform apply)
#################################
  CWLTfapply:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-tfapply-project-role"
      RetentionInDays: !Ref BuildLogsRationDay
      KmsKeyId: !GetAtt KeyCWL.Arn
      Tags: 
        - Key: "Name"
          Value: !Sub "/aws/codebuild/${PrjPrefix}-tf-build-tfapply-project-role"

#################################
# CodeBuild (terraform apply)
#################################
  RoleTfapply:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-build-tfapply-project-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "codebuild.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/AdministratorAccess"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfapply-project-role"

  ProjectTfapply:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub "${PrjPrefix}-tf-build-tfapply-project"
      Description: "Execute terraform apply command."
      Source:
        Type: "CODEPIPELINE"
        BuildSpec: |
          version: 0.2
          env:
            exported-variables:
              - BUILD_URL
          phases:
            pre_build:
              commands:
                - "terraform init -input=false -no-color"
            build:
              commands:
                - "terraform apply tfplan.binary -input=false -no-color -auto-approve"
      Artifacts:
        Type: "CODEPIPELINE"
      Cache:
        Type: "LOCAL"
        Modes:
          - "LOCAL_DOCKER_LAYER_CACHE"
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: !Sub "public.ecr.aws/hashicorp/terraform:${TerraformVersion}"
        ImagePullCredentialsType: "CODEBUILD"
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: "ENABLED"
          GroupName: !Ref CWLTfapply
      EncryptionKey: !GetAtt KeyS3Arthifact.Arn
      ResourceAccessRole: !GetAtt RoleTfapply.Arn
      ServiceRole: !GetAtt RoleTfapply.Arn
      TimeoutInMinutes: 60
      Visibility: "PRIVATE"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfapply-project"

#################################
# CodePipeline
#################################
  PolicyTfPipeline:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${PrjPrefix}-tf-pipeline-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "S3Artifact"
            Effect: "Allow"
            Action:
              - "s3:GetObject*"
              - "s3:GetBucket*"
              - "s3:List*"
              - "s3:DeleteObject*"
              - "s3:PutObject"
              - "s3:Abort*"
            Resource:
              - !GetAtt BucketArtifacts.Arn
              - !Sub
                - "${BucketArn}/*"
                - { BucketArn: !GetAtt BucketArtifacts.Arn }
          - Sid: "KmsKey"
            Effect: "Allow"
            Action:
              - "kms:Decrypt"
              - "kms:DescribeKey"
              - "kms:Encrypt"
              - "kms:ReEncrypt*"
              - "kms:GenerateDataKey*"
            Resource:
              - !GetAtt KeyS3Arthifact.Arn
          - Sid: "CodeCommitRepo"
            Effect: "Allow"
            Action:
              - "codecommit:GetBranch"
              - "codecommit:GetCommit"
              - "codecommit:UploadArchive"
              - "codecommit:GetUploadArchiveStatus"
              - "codecommit:CancelUploadArchive"
            Resource:
              - !GetAtt CodeCommit.Arn
          - Sid: "CodeBuildProjects"
            Effect: "Allow"
            Action:
              - "codebuild:BatchGetBuilds"
              - "codebuild:StartBuild"
              - "codebuild:StopBuild"
            Resource:
              - !GetAtt ProjectSnykIaC.Arn
              - !GetAtt ProjectTfplan.Arn
              - !GetAtt ProjectTfapply.Arn

  RoleTfPipelne:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-pipeline-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "codepipeline.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref PolicyTfPipeline
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-role"

  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub "${PrjPrefix}-tf-pipeline"
      ArtifactStore:
        EncryptionKey:
          Id: !GetAtt KeyS3Arthifact.Arn
          Type: "KMS"
        Location: !Ref BucketArtifacts
        Type: "S3"
      RoleArn: !GetAtt RoleTfPipelne.Arn
      Stages:
        - Name: "Source"
          Actions:
            - Name: "CodeCommit_Source"
              ActionTypeId:
                Category: "Source"
                Owner: "AWS"
                Provider: "CodeCommit"
                Version: "1"
              Configuration:
                RepositoryName: !GetAtt CodeCommit.Name
                BranchName: !Ref BranchName
                PollForSourceChanges: false
              OutputArtifacts:
                - Name: "Artifact_Source_CodeCommit"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
        - Name: "terraform_plan_Stage"
          Actions:
            - Name: "terraform_plan_command"
              Namespace: TFPLAN
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Provider: "CodeBuild"
                Version: "1"
              Configuration:
                ProjectName: !Ref ProjectTfplan
              InputArtifacts:
                - Name: "Artifact_Source_CodeCommit"
              OutputArtifacts:
                - Name: "Artifact_Build_terraform_plan"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
            - Name: "terraform_plan_command_Manual_Review"
              ActionTypeId:
                Category: "Approval"
                Owner: "AWS"
                Provider: "Manual"
                Version: "1"
              Configuration:
                CustomData: "terraform plan review"
                ExternalEntityLink: "#{TFPLAN.BUILD_URL}"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 2
        - Name: "snyk_iac_Stage"
          Actions:
            - Name: "Terraform_Security_Analysis"
              Namespace: SNYKIAC
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Provider: "CodeBuild"
                Version: "1"
              Configuration:
                ProjectName: !Ref ProjectSnykIaC
              InputArtifacts:
                - Name: "Artifact_Build_terraform_plan"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
            - Name: "Terraform_Security_Analysis_Manual_Review"
              ActionTypeId:
                Category: "Approval"
                Owner: "AWS"
                Provider: "Manual"
                Version: "1"
              Configuration:
                CustomData: "snyk iac review"
                ExternalEntityLink: "#{SNYKIAC.BUILD_URL}"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 2
        - Name: "terraform_apply_Stage"
          Actions:
            - Name: "terraform_apply_command"
              Namespace: TFAPPLY
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Provider: "CodeBuild"
                Version: "1"
              Configuration:
                ProjectName: !Ref ProjectTfapply
              InputArtifacts:
                - Name: "Artifact_Build_terraform_plan"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1

#################################
# EventBridge (CodeCommit State Change)
#################################
  PolicyEventBridgeCodeCommit:
    Type: AWS::IAM::ManagedPolicy
    Properties:
      ManagedPolicyName: !Sub "${PrjPrefix}-tf-pipeline-event-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "CodePipelineExec"
            Effect: "Allow"
            Action: "codepipeline:StartPipelineExecution"
            Resource: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}"
  
  RoleEventBridgeCodeCommit:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${PrjPrefix}-tf-pipeline-event-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service: "events.amazonaws.com"
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref PolicyEventBridgeCodeCommit
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-event-role"
  
  EventsTfPipelineCodeCommit:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub "${PrjPrefix}-tf-pipeline-event"
      EventPattern:
        source:
          - "aws.codecommit"
        resources:
          - !GetAtt CodeCommit.Arn
        detail-type:
          - "CodeCommit Repository State Change"
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - "branch"
          referenceName:
            - !Ref BranchName
      State: "ENABLED"
      Targets:
        - Arn: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}"
          Id: "TerraformPipeline"
          RoleArn: !GetAtt RoleEventBridgeCodeCommit.Arn

#################################
# SNS Topic (Snyk IaC Build Phase Failed)
#################################
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected-topic"
      DisplayName: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected-topic"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected-topic"

  SNSSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: "email"
      TopicArn: !Ref SNSTopic
      Endpoint: !Ref CodePipelineNotificationEmail

#################################
# CodeStar Notification case
#################################
  SNSTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "AWSCodeStarNotifications_publish"
            Effect: "Allow"
            Principal:
              Service: "codestar-notifications.amazonaws.com"
            Action:
              - "SNS:Publish"
            Resource: !Ref SNSTopic
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
      Topics: 
        - !Ref SNSTopic

#################################
# CodeStar Notification case
#################################
  CodePipelineFailedNotification:
    Type: AWS::CodeStarNotifications::NotificationRule
    Properties: 
      DetailType: "FULL"
      EventTypeIds:
        - "codebuild-project-build-phase-failure"
      Name: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected"
      Resource: !GetAtt ProjectSnykIaC.Arn
      Status: "ENABLED"
      Targets: 
        - TargetType: "SNS"
          TargetAddress: !Ref SNSTopic

#################################
# CodeStar Notification case
#################################
  CodePipelineManualNotification:
    Type: AWS::CodeStarNotifications::NotificationRule
    Properties: 
      DetailType: "FULL"
      EventTypeIds:
        - "codepipeline-pipeline-manual-approval-needed"
      Name: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-manual-approve-needed"
      Resource: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${CodePipeline}"
      Status: "ENABLED"
      Targets: 
        - TargetType: "SNS"
          TargetAddress: !Ref SNSTopic

#################################
# Eventbridge Notification case
#################################

  # SNSTopicPolicy:
  #   Type: AWS::SNS::TopicPolicy
  #   Properties: 
  #     PolicyDocument:
  #       Version: "2012-10-17"
  #       Statement:
  #         - Sid: "AWSEventBridgeNotifications_publish"
  #           Effect: "Allow"
  #           Principal:
  #             Service: "events.amazonaws.com"
  #           Action:
  #             - "sns:Publish"
  #           Resource: !Ref SNSTopic
  #           Condition:
  #             StringEquals:
  #               aws:SourceAccount: !Ref AWS::AccountId
  #     Topics: 
  #       - !Ref SNSTopic

  # EventsTfPipeline:
  #   Type: AWS::Events::Rule
  #   Properties:
  #     Name: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected-event"
  #     EventPattern:
  #       source:
  #         - "aws.codebuild"
  #       resources:
  #         - !GetAtt ProjectSnykIaC.Arn
  #       detail-type:
  #         - "Snyk IaC Build Detected"
  #       detail:
  #         build-status:
  #           - "FAILED"
  #         project-name:
  #           - !Ref ProjectSnykIaC
  #     State: "ENABLED"
  #     Targets:
  #       - Arn: !Ref SNSTopic
  #         Id: "SnykIaCDetectedTopic"

Outputs:
  TfstateBucketName:
    Value: !Ref BucketTfstate
    Export:
      Name: !Sub "${AWS::StackName}-TfstateBucketName"
  DDBTableName:
    Value: !Ref DDBTable
    Export:
      Name: !Sub "${AWS::StackName}-DDBTableName"
```

</div></details>

# やってみた
## Dockerイメージの取得先

Snykでは公式からDockerイメージが配布されています。
`/app`ディレクトリに対して解析を行いイメージタグで、解析する言語等を分岐しているイメージになります。

https://hub.docker.com/r/snyk/snyk

私も当初、`snyk/snyk:linux`イメージからCodeBuildの起動を試みましたが以下のエラーに遭遇しました。

```bash
[Container] 2022/07/17 14:02:00 Phase context status code: Secrets Manager Error Message: RequestError: send request failed caused by: Post "https://secretsmanager.ap-northeast-1.amazonaws.com/": x509: certificate signed by unknown authority
```

調査の結果、DockerイメージにCA証明書が含まれていないためエラーが起きているようでした。
以下のブログの通り、`aws/codebuild/amazonlinux2-x86_64-standard:3.0`イメージを使用してSnykインストール、Snyk CLIを実行しました。

https://qiita.com/sasasin/items/6e5f7001cba350851d49#%E3%83%8F%E3%83%9E%E3%82%8A%E3%81%A9%E3%81%93%E3%82%8D

### `buildspec.yaml`のサンプル

```buildspec.yaml
version: 0.2
env:
phases:
  install:
    runtime-versions:
      golang: 1.14
    commands:
      - curl https://static.snyk.io/cli/latest/snyk-linux -o snyk
      - "chmod +x ./snyk"
      - "mv ./snyk /usr/local/bin/"
  pre_build:
    commands:
      - "mkdir -p ${SRC_DIR}"
      - "cp -p tfplan.json ${SRC_DIR}/"
      - "cd ${SRC_DIR}"
      - "echo Executing Snyk IaC"
  build:
    commands:
      - "snyk iac test tfplan.json --report"
```

## Web UIへの出力

今回コードのソースは、AWS CodeCommitを使用しています。
そのため、Snyk IaCの結果をWeb UIで確認するには「`--report`」コマンドを使用する必要があります。

https://dev.classmethod.jp/articles/snyk-iac-results-with-the-snyk-web-ui-report-option/

Snyk CLIをCIで利用するためには、Service accountsが必要で、Service accountsは、AWSでいう`アクセスキー`のようなAPIキーです。
外部に漏れないよう、Secrets Manager等の利用がオススメです。

https://docs.snyk.io/features/user-and-group-management/structure-account-for-high-application-performance/service-accounts

Service accountsで発行したAPIキーは、「環境変数`SNYK_TOKEN`に設定すること」で利用可能になります。

https://docs.snyk.io/snyk-cli/configure-the-snyk-cli

### CloudFormation
```yaml
  ProjectSnykIaC:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub "${PrjPrefix}-tf-build-snyk-iac-project"
      Description: "Analyze the code for vulnerabilities using Snyk IaC."
      Source:
        Type: "CODEPIPELINE"
        BuildSpec: |
          version: 0.2
          env:
          phases:
            install:
              runtime-versions:
                golang: 1.14
              commands:
                - curl https://static.snyk.io/cli/latest/snyk-linux -o snyk
                - "chmod +x ./snyk"
                - "mv ./snyk /usr/local/bin/"
            pre_build:
              commands:
                - "mkdir -p ${SRC_DIR}"
                - "cp -p tfplan.json ${SRC_DIR}/"
                - "cd ${SRC_DIR}"
                - "echo Executing Snyk IaC"
            build:
              commands:
                - "snyk iac test tfplan.json --report"
      Artifacts:
        Type: "CODEPIPELINE"
      Cache:
        Type: "LOCAL"
        Modes:
          - "LOCAL_DOCKER_LAYER_CACHE"
      Environment:
        Type: "LINUX_CONTAINER"
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
        ImagePullCredentialsType: "CODEBUILD"
        EnvironmentVariables:
          - Name: "SNYK_TOKEN"
            Type: "SECRETS_MANAGER"
            Value: !Sub "${SnykOrganizationProfile}:OrgApi"
          - Name: "SNYK_CFG_ORG"
            Type: "SECRETS_MANAGER"
            Value: !Sub "${SnykOrganizationProfile}:OrgId"
          - Name: "SRC_DIR"
            Type: "PLAINTEXT"
            Value: !Sub "${PrjPrefix}-tf-build-snyk-iac-project"
        PrivilegedMode: true
      LogsConfig:
        CloudWatchLogs:
          Status: "ENABLED"
          GroupName: !Ref CWLSnykIaC
      EncryptionKey: !GetAtt KeyS3Arthifact.Arn
      ResourceAccessRole: !GetAtt RoleSnykIaC.Arn
      ServiceRole: !GetAtt RoleSnykIaC.Arn
      TimeoutInMinutes: 60
      Visibility: "PRIVATE"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-build-tfsec-project"
```

## SNS通知はマストで設定したい

Snyk IaCは解析結果によって終了ステータスが変わります。終了ステータスが0以外の場合、CodeBuildプロジェクトは失敗します。
そのため、何が悪かったのか（文法なのか、脆弱性なのか、その他の原因）を開発者に通知する必要があります。

```bash
$ snyk iac test -h
IaC test
Usage
  snyk iac test [<OPTIONS>] [<PATH>]

Description
  The snyk iac test command tests for any known security issue.

  For a list of related commands see the snyk iac help; iac --help

  For more information see Snyk CLI for Infrastructure as Code 
  https://docs.snyk.io/products/snyk-infrastructure-as-code/snyk-cli-for-infrastructure-as-code

Exit codes
  Possible exit codes and their meaning:

  0: success, no vulnerabilities found
  1: action_needed, vulnerabilities found
  2: failure, try to re-run command
  3: failure, no supported projects detected
```

https://docs.snyk.io/snyk-cli/commands/iac-test#exit-codes

SNS通知は大きく分けて、「**CodeStar経由で通知**」、「**EventBridge経由で通知**」の2パターンあるため、好みに合わせて設定しましょう。

https://docs.aws.amazon.com/ja_jp/dtconsole/latest/userguide/set-up-sns.html

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

### CloudFormation

```yaml
#################################
# SNS Topic (Snyk IaC Build Phase Failed)
#################################
  SNSTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected-topic"
      DisplayName: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected-topic"
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected-topic"

  SNSTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "AWSEventBridgeNotifications_publish"
            Effect: "Allow"
            Principal:
              Service: "events.amazonaws.com"
            Action:
              - "sns:Publish"
            Resource: !Ref SNSTopic
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId
      Topics: 
        - !Ref SNSTopic

  SNSSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: "email"
      TopicArn: !Ref SNSTopic
      Endpoint: !Ref CodePipelineNotificationEmail

#################################
# EventBridge (CodeCommit State Change)
#################################
  EventsTfPipeline:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected-event"
      EventPattern:
        source:
          - "aws.codebuild"
        resources:
          - !GetAtt ProjectSnykIaC.Arn
        detail-type:
          - "Snyk IaC Build Detected"
        detail:
          build-status:
            - "FAILED"
          project-name:
            - !Ref ProjectSnykIaC
      State: "ENABLED"
      Targets:
        - Arn: !Ref SNSTopic
          Id: "SnykIaCDetectedTopic"

##############
# CodeStart経由の場合
##############

  # SNSTopicPolicy:
  #   Type: AWS::SNS::TopicPolicy
  #   Properties: 
  #     PolicyDocument:
  #       Version: "2012-10-17"
  #       Statement:
  #         - Sid: "AWSCodeStarNotifications_publish"
  #           Effect: "Allow"
  #           Principal:
  #             Service: "codestar-notifications.amazonaws.com"
  #           Action:
  #             - "SNS:Publish"
  #           Resource: !Ref SNSTopic
  #           Condition:
  #             StringEquals:
  #               aws:SourceAccount: !Ref AWS::AccountId
  #     Topics: 
  #       - !Ref SNSTopic

  # CodePipelineFailedNotification:
  #   Type: AWS::CodeStarNotifications::NotificationRule
  #   Properties: 
  #     DetailType: "FULL"
  #     EventTypeIds:
  #       - "codebuild-project-build-phase-failure"
  #     Name: !Sub "${PrjPrefix}-tf-pipeline-snyk-iac-detected"
  #     Resource: !GetAtt ProjectSnykIaC.Arn
  #     Status: "ENABLED"
  #     Targets: 
  #       - TargetType: "SNS"
  #         TargetAddress: !Ref SNSTopic
```

### 2022/7/19 追記

Good Practiceによると、終了コードは、「|| true」で書き換えることができるみたいでした。

https://docs.snyk.io/integrations/ci-cd-integrations#exit-codes

```buildspec.yaml
version: 0.2
env:
  exported-variables:
    - BUILD_URL
phases:
  install:
    runtime-versions:
      golang: 1.14
    commands:
      - curl https://static.snyk.io/cli/latest/snyk-linux -o snyk
      - "chmod +x ./snyk"
      - "mv ./snyk /usr/local/bin/"
  pre_build:
    commands:
      - "printenv"
      - "mkdir -p ${SRC_DIR}"
      - "cp -p tfplan.json ${SRC_DIR}/"
      - "cd ${SRC_DIR}"
      - "echo Executing Snyk IaC"
  build:
    commands:
      - "snyk iac test tfplan.json --report || true"
  post_build:
    commands:
      - "export BUILD_URL=`echo $CODEBUILD_BUILD_URL`"
```

終了コードを書き換えた場合は、後続のパイプラインで手動承認のアクションを追加することをオススメします！
手動承認アクションのレビュー用URLは、CodeBuildのビルド結果に遷移するページを指定していますが、Snyk Web UIに遷移する仕組みも面白そうです。

https://dev.classmethod.jp/articles/output-build-result-to-manual-actions-review-url/

```yaml
  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      Name: !Sub "${PrjPrefix}-tf-pipeline"
      ArtifactStore:
        EncryptionKey:
          Id: !GetAtt KeyS3Arthifact.Arn
          Type: "KMS"
        Location: !Ref BucketArtifacts
        Type: "S3"
      RoleArn: !GetAtt RoleTfPipelne.Arn
      Stages:
        - Name: "Source"
          Actions:
            - Name: "CodeCommit_Source"
              ActionTypeId:
                Category: "Source"
                Owner: "AWS"
                Provider: "CodeCommit"
                Version: "1"
              Configuration:
                RepositoryName: !GetAtt CodeCommit.Name
                BranchName: !Ref BranchName
                PollForSourceChanges: false
              OutputArtifacts:
                - Name: "Artifact_Source_CodeCommit"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
        - Name: "terraform_plan_Stage"
          Actions:
            - Name: "terraform_plan_command"
              Namespace: TFPLAN
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Provider: "CodeBuild"
                Version: "1"
              Configuration:
                ProjectName: !Ref ProjectTfplan
              InputArtifacts:
                - Name: "Artifact_Source_CodeCommit"
              OutputArtifacts:
                - Name: "Artifact_Build_terraform_plan"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
            - Name: "terraform_plan_command_Manual_Review"
              ActionTypeId:
                Category: "Approval"
                Owner: "AWS"
                Provider: "Manual"
                Version: "1"
              Configuration:
                CustomData: "terraform plan review"
                ExternalEntityLink: "#{TFPLAN.BUILD_URL}"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 2
        - Name: "snyk_iac_Stage"
          Actions:
            - Name: "Terraform_Security_Analysis"
              Namespace: SNYKIAC
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Provider: "CodeBuild"
                Version: "1"
              Configuration:
                ProjectName: !Ref ProjectSnykIaC
              InputArtifacts:
                - Name: "Artifact_Build_terraform_plan"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
            - Name: "Terraform_Security_Analysis_Manual_Review"
              ActionTypeId:
                Category: "Approval"
                Owner: "AWS"
                Provider: "Manual"
                Version: "1"
              Configuration:
                CustomData: "snyk iac review"
                ExternalEntityLink: "#{SNYKIAC.BUILD_URL}"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 2
        - Name: "terraform_apply_Stage"
          Actions:
            - Name: "terraform_apply_command"
              Namespace: TFAPPLY
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Provider: "CodeBuild"
                Version: "1"
              Configuration:
                ProjectName: !Ref ProjectTfapply
              InputArtifacts:
                - Name: "Artifact_Build_terraform_plan"
              RoleArn: !GetAtt RoleTfPipelne.Arn
              RunOrder: 1
```

## .snykファイルの自動作成

Snyk IaCでは、`.snyk`ファイルで検知を回避（Ignore）したいルールを定義できます。

https://docs.snyk.io/products/snyk-infrastructure-as-code/snyk-cli-for-infrastructure-as-code/iac-ignores-using-the-.snyk-policy-file

`.snyk`ファイルにルールを記載しなくても、Snyk CLIの実行には問題ないです。そのため、CodeCommitレポジトリ作成時にデフォルトで作成するようにLambdaを仕込みます。

https://dev.classmethod.jp/articles/cfn-custom-resource-codecommit-readme/

### CloudFormation

```yaml
#################################
# Custom Resource
#################################
  PutExcludeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        - "arn:aws:iam::aws:policy/AWSCodeCommitPowerUser"
      RoleName: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude-role"

  PutExcludeLog:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
      KmsKeyId: !GetAtt KeyCWL.Arn

  PutExcludeFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
      Code:
        ZipFile: |
          import json
          import boto3
          import cfnresponse

          def handler(event, context):
              try:
                  repository = event['ResourceProperties']['RepositoryName']
                  branch = event['ResourceProperties']['BranchName']
                  content = event['ResourceProperties']['FileContent'].encode()
                  path = event['ResourceProperties']['FilePath']

                  if event['RequestType'] == 'Create':
                      codecommit = boto3.client('codecommit')
                      response = codecommit.put_file(
                          repositoryName=repository,
                          branchName=branch,
                          fileContent=content,
                          filePath=path,
                          commitMessage='Initial Commit',
                          name='Your Lambda Helper'
                      )
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, response)
                  if event['RequestType'] == 'Delete':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
                  if event['RequestType'] == 'Update':
                      cfnresponse.send(event, context, cfnresponse.SUCCESS, {'Response': 'Success'})
              except Exception as e:
                  print(e)
                  cfnresponse.send(event, context, cfnresponse.FAILED, {})
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt PutExcludeRole.Arn
      Runtime: "python3.9"
      Timeout: 60
      Tags:
        - Key: "Name"
          Value: !Sub "${PrjPrefix}-tf-repo-${BranchName}-put-exclude"
    DependsOn: PutExcludeLog

  PutExclude:
    Type: Custom::CodeCommitPutExclude
    Properties:
      ServiceToken: !GetAtt PutExcludeFunction.Arn
      RepositoryName: !GetAtt CodeCommit.Name
      BranchName: !Ref BranchName
      FileContent: "ignore:"
      FilePath: ".snyk"
```

# 最後に

以上、「AWS CodeBuildでSnyk IaCを使用してみた」でした！

脆弱性が見つかった場合、CodeBuildでエラーを返してCodePipelineが止まるため、是正するか`.snyk`で回避するかの対応が必要だと感じました。

個人的には「スキップ」という選択肢がないため、セキュアなデプロイメントパイプラインが組めるのではないかと思いました。

この記事がどなたかの参考になればとっても嬉しいです。以上、たかくにでした！