【CDK】ECRイメージスキャンの結果をchatbot経由でslackに通知させる

## 背景
CDKはCloudFormationやTerraformと違って○○を作るとしたらという情報が少なく、最初の学習ハードルが気持ち高いので、積極的にCDK関連を書くことで誰かの役に立てればいいなあと。

今回はECRにContainerimageがpushされたらイメージスキャンが走り、その結果をChatbotを経由してSlackに通知させる動きを作っていきます。

## イメージ図
<img width="627" alt="スクリーンショット 2022-03-12 15.15.54.png (41.0 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/e1f44ebd-65ff-46f3-9ca8-6fc8645b0f25.png">

## 2種類のスキャン
AWS re:Invent 2021でInspectorが[リニューアル](https://aws.amazon.com/jp/blogs/news/improved-automated-vulnerability-management-for-cloud-workloads-with-a-new-amazon-inspector/)されて、これまでのEC2に対する脆弱性管理だけでなく、ECRにpushされたコンテナイメージも対象となりました。これにより、コンテナイメージに対するスキャニングは、ECRの機能としての基本スキャンとInspectorの機能としての拡張スキャンの2種類のオプションを選択することが出来るようになりました。

## 基本スキャン
### 脆弱性データベース
> ClairプロジェクトのCommon Vulnerabilities and Exposures（CVE）データベースを使用

https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-scanning-basic.html

### スキャン検知対象
- OS パッケージ

## 拡張スキャン
### 脆弱性データベース

>mazon Inspector は、Snyk と提携して、脆弱性データベースに追加の脆弱性インテリジェンスを受け取りました。

https://aws.amazon.com/jp/about-aws/whats-new/2021/11/amazon-inspector-continual-vulnerability-management/

### スキャン検知対象
- OS パッケージ(各OSのversionは下記一覧参照)
    - Alpine Linux
    - Amazon Linux
    - CentOS Linux
    - Debian Server
    - Oracle Linux
    - Red Hat Enterprise Linux
    - OpenSUSE Leap
    - SUSE Linux Enterprise Server
    - Ubuntu Server
- プログラミング言語パッケージ
    - C#
    - Golang
    - Java
    - Javascript
    - PHP
    - Python
    - Ruby
    - Rust

[サポート対象OS&プログラミング言語一覧](https://docs.aws.amazon.com/inspector/latest/user/supported.html)

## 事前準備
### ChatbotとSlack連携
事前にchatbotがslackworkspaceにアクセスする権限を付与しておきましょう。

- AWS Chatbotコンソールより「チャットクライアント」を「Slack」にして「クライアントを設定」を押下する
 <img width="713" alt="スクリーンショット_2022-03-12_16_05_21.png (64.0 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/3380bf47-3651-4dfc-b391-03dd2152d3e5.png">

- アクセス権限をリクエスト画面が表示されたら対象のSlackワークスペースを確認して「許可する」を押下する
<img width="531" alt="スクリーンショット_2022-03-12_16_13_16.png (69.1 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/33ba529b-7d34-4018-a150-8cbd8a23b6ae.png">

## CDK
- Version:2.8.0

- 作成されるリソース
    - SNSTopic
    - SNSTopicPolicy
    - ECRRepository
    - EventBridgeRule
    - Chatbot

※CDKではまだ拡張スキャンを有効化出来なさそうなので、今回は基本スキャンで実装していきます。
自分が見つけられていないだけで、こういうやり方で出来るよというのがありましたらコメント下さい！

### cdk.json
今回はContextに、prefixと対象となるSlackワークスペースID、SlackチャンネルIDを置いています。

```cdk.json
{
  "app": "npx ts-node --prefer-ts-exts bin/src.ts",
  "watch": {
    "include": [
      "**"
    ],
    "exclude": [
      "README.md",
      "cdk*.json",
      "**/*.d.ts",
      "**/*.js",
      "tsconfig.json",
      "package*.json",
      "yarn.lock",
      "node_modules",
      "test"
    ]
  },
  "context": {
    "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
    "@aws-cdk/core:stackRelativeExports": true,
    "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
    "@aws-cdk/aws-lambda:recognizeVersionProps": true,
    "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
    "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
    "@aws-cdk/core:target-partitions": [
      "aws",
      "aws-cn"
    ],
    "prefix": "sampletest",
    "slackWorkspaceId":"xxxxxxxxx",
    "slackChannelId":"xxxxxxxxx"
  }
}

```

### app
```bin/src.ts
#!/usr/bin/env node
import * as cdk from "aws-cdk-lib";
import { SampleStack } from "../lib/sample-stack";

const app = new cdk.App();

const sampleStack = new SampleStack(app, `sample`);
```

### stack
```lib/sample-stack.ts
import * as cdk from "aws-cdk-lib";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as sns from "aws-cdk-lib/aws-sns";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import * as chatbot from "aws-cdk-lib/aws-chatbot";

export interface SampleStackProps { 
  readonly repo: ecr.Repository
  readonly snsTopic: sns.Topic
}

export class SampleStack extends cdk.Stack {
  public readonly repo: ecr.Repository
  public readonly snsTopic: sns.Topic

  // ECR Repository
  private createRepository(name: string): ecr.Repository {

    const repo = new ecr.Repository(this, `${name}`, {
      repositoryName: name,
      imageScanOnPush: true, //　push時にスキャン
      imageTagMutability: ecr.TagMutability.MUTABLE,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      lifecycleRules:[
        {
          maxImageAge: cdk.Duration.days(30),
        }
      ],
    });
    return repo;
  }

  // SNS Topic
  private createSndTopic(name: string): sns.Topic {

    const snsTopic = new sns.Topic(this, `${name}`, {
      displayName: "ECR Scan Nottification",
      topicName: name,
    });
    return snsTopic;
  }

  // ECR Scan Event
  private createScanEventRule(name: string, repo: ecr.Repository, sns: sns.Topic): events.Rule {
    
    const eventRule = new events.Rule(this, `${name}`, {
      description: 'ecr scan completed',
      eventPattern: {
        source: ['aws.ecr'],
        detailType: ['ECR Image Scan'],
        detail: {
          'repository-name': [`${repo.repositoryName}`], // 作成したecrリポジトリが対象
          'scan-status': ['COMPLETE'], 
          'image-tags': [{"prefix": "v-" }] // "v-"を含むタグで実行
        },
      },
      ruleName: `${name}-scan-notification`,
    });
    eventRule.addTarget(new targets.SnsTopic(sns)) // 作成したSNSをターゲットに指定
    return eventRule;
  }

  // Chatbot
  private createChatbot(name: string, sns: sns.Topic): chatbot.SlackChannelConfiguration {
    const slackWorkspaceId = this.node.tryGetContext("slackWorkspaceId"); // Contextで指定したslackworkspaceidを取得
    const slackChannelId = this.node.tryGetContext("slackChannelId"); // Contextで指定したslackchannelidを取得

    const slackchatbot = new chatbot.SlackChannelConfiguration(this, `${name}`, {
      slackChannelConfigurationName: name,
      slackWorkspaceId: slackWorkspaceId, // 事前にコンソール上でchatbotがslackworkspaceにアクセスする権限を与えていること
      slackChannelId: slackChannelId,
      loggingLevel: chatbot.LoggingLevel.ERROR,
      notificationTopics: [sns],
    });
    return slackchatbot;
  }

  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const prefix = this.node.tryGetContext("prefix"); // Contextで指定したprefixを取得

    this.snsTopic = this.createSndTopic(`${prefix}-sns-topic`);
    this.repo = this.createRepository(`${prefix}-repo`);
    this.createScanEventRule(`${prefix}-event-rule`, this.repo, this.snsTopic);
    this.createChatbot(`${prefix}-chatbot`, this.snsTopic);
  }
}
```

## デプロイ
```sh
cdk deploy -c slackWorkspaceId=<対象のSlackワークスペースID> -c slackChannelId=<対象のSlackチャンネルID>
```

## 作成リソース確認

### ECR
push時にスキャンするECRリポジトリの作成確認

<img width="1231" alt="スクリーンショット_2022-03-12_16_41_08.png (51.6 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/290591cd-7436-4b88-8fcf-4709b2d5835f.png">

### Chatbot
SNSがマップングされたChatbotの作成確認
<img width="1236" alt="スクリーンショット_2022-03-12_16_46_25.png (66.7 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/5b47c20e-a9a3-4b5e-a4d1-5c27d82dd831.png">


### SNS
ChatbotがEndpointに設定されいるSNSの作成確認
<img width="1215" alt="スクリーンショット_2022-03-12_16_47_11.png (87.7 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/6b4a0ba8-e7c2-474e-a77d-1d595fc64104.png">

### EventBridge
下記イベントパターンのEventRuleの作成確認
- 対象リポジトリ、スキャンステータス、image-tagが合致
<img width="971" alt="スクリーンショット_2022-03-12_16_48_56.png (77.9 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/4b633c57-c8a4-44cf-a790-4a9ed466752c.png">

イベントターゲットがSNS
<img width="449" alt="スクリーンショット_2022-03-12_16_49_21.png (26.9 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/4f528f40-e15a-44d1-9fe7-544e8dec6238.png">

## 動作確認
- ECRにログイン
```sh
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
```

- tagを付けてECRにpush
 ```sh
docker tag amazon/aws-cli:latest xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/sampletest-repo:v-1.0.0

docker push xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/sampletest-repo:v-1.0.0
 ```

- Slack通知確認
<img width="641" alt="スクリーンショット_2022-03-12_17_11_07.png (50.8 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/bc62f7b1-c3e3-479b-9d47-31bb729931a6.png">

- EventBridgeでも一応確認
同じ時間にトリガーされている事が確認出来ますね
<img width="1232" alt="スクリーンショット 2022-03-12 17.13.23.png (58.5 kB)" src="https://img.esa.io/uploads/production/attachments/10655/2022/03/12/108350/e1080560-ebef-469e-8d76-3e9b10d34b7f.png">

## まとめ
CDK(v2)でECRスキャン結果をSlackへ通知してみました
慣れてくるまでが大変でしたが扱えるようになってくると楽しい

今回はローカルからECRに直接pushしましたが、CodebuildやGitHub Actions経由であってもECRリポジトリにコンテナイメージが置かれたタイミングで自動的に基本スキャンが走るのでCI部分はお好みで

拡張スキャンも対応されたらそちらでもまた書いていきたいと思います。