6
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-03-12

背景

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

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

イメージ図

スクリーンショット 2022-03-12 15.15.54.png (41.0 kB)

2種類のスキャン

AWS re:Invent 2021でInspectorがリニューアルされて、これまでのEC2に対する脆弱性管理だけでなく、ECRにpushされたコンテナイメージも対象となりました。これにより、コンテナイメージに対するスキャニングは、ECRの機能としての基本スキャンとInspectorの機能としての拡張スキャンの2種類のオプションを選択することが出来るようになりました。

基本スキャン

脆弱性データベース

ClairプロジェクトのCommon Vulnerabilities and Exposures(CVE)データベースを使用

スキャン検知対象

  • OS パッケージ

拡張スキャン

脆弱性データベース

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

スキャン検知対象

  • 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&プログラミング言語一覧

事前準備

ChatbotとSlack連携

事前にchatbotがslackworkspaceにアクセスする権限を付与しておきましょう。

  • AWS Chatbotコンソールより「チャットクライアント」を「Slack」にして「クライアントを設定」を押下する
スクリーンショット_2022-03-12_16_05_21.png (64.0 kB)
  • アクセス権限をリクエスト画面が表示されたら対象のSlackワークスペースを確認して「許可する」を押下する
スクリーンショット_2022-03-12_16_13_16.png (69.1 kB)

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);
  }
}

デプロイ

cdk deploy -c slackWorkspaceId=<対象のSlackワークスペースID> -c slackChannelId=<対象のSlackチャンネルID>

作成リソース確認

ECR

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

スクリーンショット_2022-03-12_16_41_08.png (51.6 kB)

Chatbot

SNSがマップングされたChatbotの作成確認
スクリーンショット_2022-03-12_16_46_25.png (66.7 kB)

SNS

ChatbotがEndpointに設定されいるSNSの作成確認
スクリーンショット_2022-03-12_16_47_11.png (87.7 kB)

EventBridge

下記イベントパターンのEventRuleの作成確認

  • 対象リポジトリ、スキャンステータス、image-tagが合致
スクリーンショット_2022-03-12_16_48_56.png (77.9 kB)

イベントターゲットがSNS
スクリーンショット_2022-03-12_16_49_21.png (26.9 kB)

動作確認

  • ECRにログイン
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
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通知確認
スクリーンショット_2022-03-12_17_11_07.png (50.8 kB)
  • EventBridgeでも一応確認
    同じ時間にトリガーされている事が確認出来ますね
スクリーンショット 2022-03-12 17.13.23.png (58.5 kB)

まとめ

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

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

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

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2