AWS IoT Device Defenderの監査レポートをLambdaとCDKでSlackに通知してみた

2022.01.26

AWS IoT Device Defenderの監査レポートはChatbotに対応していないので、Slackへ通知するためにはスクリプトの実装が必要です。Simple Notification Service (SNS)とLambdaとCDKでSlack通知を実装します。

AWS IoT Device Defenderの設定は以下の記事で紹介しています。

環境情報

項目 内容
OS macOS Big Sur 11.6(20G165)
Node.js 14.18.1
TypeScript 3.9.7
AWS CDK 1.131.0
@slack/webhook 6.0.0

Slack通知のLambdaを実装

Node Slack SDKを利用します。以下のコマンドでインストールします。

npm install @slack/webhook

Slack通知のLambdaをTypeScriptで実装します。非準拠の項目がある場合のみ、Slackにメッセージを送信します。

functions/notify-slack-of-device-defender.ts

import { IncomingWebhook } from '@slack/webhook';

const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;
const AWS_REGION = process.env.AWS_REGION!;

const AWS_DEVICE_DEFENDER_AUDIT_RESULT_URI = `https://${AWS_REGION}.console.aws.amazon.com/iot/home?region=${AWS_REGION}#/dd/audit/results`;

interface AuditDetail {
  checkName: string;
  checkRunStatus: string;
  nonCompliantResourcesCount: number;
  totalResourcesCount: number;
  suppressedNonCompliantResourceCount: number;
}

interface Message {
  accountId: string;
  taskId: string;
  taskStatus: string;
  taskType: string;
  failedChecksCount: number;
  canceledChecksCount: number;
  nonCompliantChecksCount: number;
  compliantChecksCount: number;
  totalChecksCount: number;
  taskStartTime: number;
  auditDetails: AuditDetail[];
}

interface Sns {
  Type: 'Notification';
  MessageId: string;
  TopicArn: string;
  Subject: string | null;
  Message: string;
  Timestamp: string;
  SignatureVersion: string;
  Signature: string;
  SigningCertUrl: string;
  UnsubscribeUrl: string;
  MessageAttributes: object;
}

interface Record {
  EventSource: 'aws:sns';
  EventVersion: string;
  EventSubscriptionArn: string;
  Sns: Sns;
}

interface Event {
  Records: Record[];
}

export const handler = async (event: Event): Promise<void> => {
  const webhook = new IncomingWebhook(SLACK_WEBHOOK_URL);

  await Promise.all(
    event.Records.map(async (record) => {
      const message = JSON.parse(record.Sns.Message) as Message;

      const nonCompliantAuditDetailList = message.auditDetails
        .filter((auditDetail) => auditDetail.nonCompliantResourcesCount > 0)
        .map((auditDetail) => ({
          type: 'section',
          text: {
            type: 'mrkdwn',
            text:
              `*${auditDetail.checkName}*\n` +
              `CheckRunStatus: ${auditDetail.checkRunStatus}\n` +
              `NonCompliantResourcesCount: ${auditDetail.nonCompliantResourcesCount}\n` +
              `TotalResourcesCount: ${auditDetail.totalResourcesCount}\n` +
              `SuppressedNonCompliantResourceCount: ${auditDetail.suppressedNonCompliantResourceCount}`,
          },
        }));

      if (nonCompliantAuditDetailList.length > 0) {
        await webhook.send({
          attachments: [
            {
              color: '#e01e5a',
              blocks: [
                {
                  type: 'section',
                  text: {
                    type: 'mrkdwn',
                    text: `<${AWS_DEVICE_DEFENDER_AUDIT_RESULT_URI}/${message.taskId}|:rotating_light: *AWS IoT Device Defender | Audit Report | ${message.taskId}*>`,
                  },
                },
                {
                  type: 'section',
                  text: {
                    type: 'plain_text',
                    text: `Non-compliant audit item has been detected. (NonCompliantChecksCount: ${message.nonCompliantChecksCount})`,
                  },
                },
                ...nonCompliantAuditDetailList,
              ],
            },
          ],
        });
      }
    }),
  );
};

SlackのWebhook URLを取得

Slackの管理画面でアプリを作成します。 https://api.slack.com/apps

Incoming Webhooksを有効化して、メッセージ送信先のSlackチャンネルを登録します。SlackチャンネルごとにWebhook URLが発行されるので取得しておきます。

Slack Incoming Webhooks

Slack通知のCDKを実装

CDKでAWS IoT Device Defenderの監査結果を通知するSNSトピックと、SNS通知を処理してSlackにメッセージを送信するLambdaを実装します。

lib/cdk-iot-device-defender.ts

import * as iam from '@aws-cdk/aws-iam';
import * as iot from '@aws-cdk/aws-iot';
import * as lambda from '@aws-cdk/aws-lambda';
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';
import * as sns from '@aws-cdk/aws-sns';
import * as snsSubscriptions from '@aws-cdk/aws-sns-subscriptions';
import * as cdk from '@aws-cdk/core';

const SLACK_WEBHOOK_URL = 'ここにSlack Webhook URL';

export class IotDeviceDefenderStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const accountId = cdk.Stack.of(this).account;

    const topic = new sns.Topic(this, 'IotDeviceDefenderTopic', {
      topicName: 'iot-device-defender-topic',
    });

    const iotDeviceDefenderAuditRole = new iam.Role(
      this,
      'IotDeviceDefenderAuditRole',
      {
        roleName: 'iot-device-defender-audit-role',
        assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'),
        managedPolicies: [
          iam.ManagedPolicy.fromAwsManagedPolicyName(
            'service-role/AWSIoTDeviceDefenderAudit',
          ),
        ],
      },
    );

    const iotDeviceDefenderAuditNotificationRole = new iam.Role(
      this,
      'IotDeviceDefenderAuditNotificationRole',
      {
        roleName: 'iot-device-defender-audit-notification-role',
        assumedBy: new iam.ServicePrincipal('iot.amazonaws.com'),
        inlinePolicies: {
          'iot-device-defender-audit-notification-policy': new iam.PolicyDocument(
            {
              statements: [
                new iam.PolicyStatement({
                  effect: iam.Effect.ALLOW,
                  actions: ['sns:Publish'],
                  resources: [topic.topicArn],
                }),
              ],
            },
          ),
        },
      },
    );

    const accountAuditConfiguration = new iot.CfnAccountAuditConfiguration(
      this,
      'AccountAuditConfiguration',
      {
        accountId: accountId,
        auditCheckConfigurations: {
          authenticatedCognitoRoleOverlyPermissiveCheck: {
            enabled: true,
          },
          caCertificateExpiringCheck: {
            enabled: true,
          },
          caCertificateKeyQualityCheck: {
            enabled: true,
          },
          conflictingClientIdsCheck: {
            enabled: true,
          },
          deviceCertificateExpiringCheck: {
            enabled: true,
          },
          deviceCertificateKeyQualityCheck: {
            enabled: true,
          },
          deviceCertificateSharedCheck: {
            enabled: true,
          },
          iotPolicyOverlyPermissiveCheck: {
            enabled: true,
          },
          iotRoleAliasAllowsAccessToUnusedServicesCheck: {
            enabled: true,
          },
          iotRoleAliasOverlyPermissiveCheck: {
            enabled: true,
          },
          loggingDisabledCheck: {
            enabled: true,
          },
          revokedCaCertificateStillActiveCheck: {
            enabled: true,
          },
          revokedDeviceCertificateStillActiveCheck: {
            enabled: true,
          },
          unauthenticatedCognitoRoleOverlyPermissiveCheck: {
            enabled: true,
          },
        },
        auditNotificationTargetConfigurations: {
          sns: {
            enabled: true,
            roleArn: iotDeviceDefenderAuditNotificationRole.roleArn,
            targetArn: topic.topicArn,
          },
        },
        roleArn: iotDeviceDefenderAuditRole.roleArn,
      },
    );

    const scheduledAudit = new iot.CfnScheduledAudit(
      this,
      'DailyScheduledAudit',
      {
        scheduledAuditName: 'DailyScheduledAudit',
        frequency: 'DAILY',
        targetCheckNames: [
          'AUTHENTICATED_COGNITO_ROLE_OVERLY_PERMISSIVE_CHECK',
          'CA_CERTIFICATE_EXPIRING_CHECK',
          'CA_CERTIFICATE_KEY_QUALITY_CHECK',
          'CONFLICTING_CLIENT_IDS_CHECK',
          'DEVICE_CERTIFICATE_EXPIRING_CHECK',
          'DEVICE_CERTIFICATE_KEY_QUALITY_CHECK',
          'DEVICE_CERTIFICATE_SHARED_CHECK',
          'IOT_POLICY_OVERLY_PERMISSIVE_CHECK',
          'IOT_ROLE_ALIAS_ALLOWS_ACCESS_TO_UNUSED_SERVICES_CHECK',
          'IOT_ROLE_ALIAS_OVERLY_PERMISSIVE_CHECK',
          'LOGGING_DISABLED_CHECK',
          'REVOKED_CA_CERTIFICATE_STILL_ACTIVE_CHECK',
          'REVOKED_DEVICE_CERTIFICATE_STILL_ACTIVE_CHECK',
          'UNAUTHENTICATED_COGNITO_ROLE_OVERLY_PERMISSIVE_CHECK',
        ],
      },
    );

    scheduledAudit.addDependsOn(accountAuditConfiguration);

    const notifySlackOfDeviceDefenderFunction = new NodejsFunction(
      this,
      'NotifySlackOfDeviceDefenderFunction',
      {
        functionName: 'notify-slack-of-device-defender',
        entry: '../functions/notify-slack-of-device-defender.ts',
        runtime: lambda.Runtime.NODEJS_14_X,
        tracing: lambda.Tracing.ACTIVE,
        environment: {
          SLACK_WEBHOOK_URL: SLACK_WEBHOOK_URL,
        },
      },
    );

    topic.addSubscription(
      new snsSubscriptions.LambdaSubscription(
        notifySlackOfDeviceDefenderFunction,
      ),
    );
  }
}

動作確認

即時実行の監査スケジュールを作成して監査結果を確認します。

AWS IoT Device Defenderスケジュール作成

非準拠の項目が検出された場合、Incoming Webhooksで指定したSlackチャンネルへメッセージが投稿されます。

AWS IoT Device Defenderアラート

タイトルのリンクをクリックすると監査結果の詳細が表示されます。

AWS IoT Device Defender監査結果

まとめ

監査で非準拠な項目がある場合だけSlackに通知させることで、設定のミスや不審な動作に気がつくことが出来るようになりました。GuardDutyなどの監視ツールではChatbotが対応しているので、AWS IoT Device Defenderもそのうち対応してもらえると嬉しいですね。

参考資料