Amazon InspectorのレポートをCognitoで認証したユーザーに表示する仕組みを検証

## 目次
- [はじめに](#はじめに)
- [構成図](#構成図)
- [リソース作成](#リソース作成)
    - [InspectorとSNSの設定](#1-inspectorとsnsの設定)
    - [Cognito User PoolとHosted UIを利用](#2-cognito-user-poolとhosted-uiを利用)
    - [API Gatewayの作成とCognito Authorizerの設定](#3-api-gatewayの作成とcognito-authorizerの設定)
- [作成リソースの動作確認](#作成リソースの動作確認)
- [まとめ](#まとめ)

## はじめに

今回はAmazon Inspectorで評価したレポートをCognitoで認証したユーザーに表示する方法を考えてみました。レポートは元々はコンソールで見れますが、共有機能がないのでその代わりに今回の仕組みを利用できます。

管理コンソールでレポートを発行してそのURLを共有する方法もありますが、URLの有効期限が15分しかないので必要なたびに管理コンソールにアクセスして発行する手間がかかります。

Cognitoを利用すると必要なタイミングでログインしてレポートが見れるのでログインID/Passwordさえ共有すればいつでもレポートを見ることができます。

## 構成図

![スクリーンショット 2021-07-20 16.01.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472833/2e9f8827-9ef7-065a-619f-b52d856b5b17.png)

####　補足

1. CloudWatch Eventを利用してタイムベースでInspectorを週１で回します。（Inspectorは実行後に通知ができるので、今回はメールで結果のURLを受信してみます）
2. メールで受信したCognitoリンクにアクセスして、事前に共有があったID/Passwordでログインします。
3. Cognitoで認証が成功するとSTSでトークンが発行され、API GatewayのCognito認証を利用してレポートのダウンロードを実行します。
4. レポートの作成が終わったら、Lambda関数はレポートのURLにユーザーを遷移させます。

## リソース作成

#####1. InspectorとSNSの設定

この記事ではInspector自体の説明やエージェントの設置などはスキップさせていただきます。詳しくはAWSのドキュメントをご参考ください。

[Amazon Inspectorエージェント](https://docs.aws.amazon.com/ja_jp/inspector/latest/userguide/inspector_agents.html)
[Amazon Inspectorのチュートリアル](https://docs.aws.amazon.com/ja_jp/inspector/latest/userguide/inspector_tutorials.html)

予め作成したSNSにInspectorの「実行完了」ステータスになったら通知するように設定を行います。その他の設定は今回のために作ったサンプルEC2を対象として設定しました。
![スクリーンショット 2021-07-20 15.43.361.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472833/ffe875c0-0d00-2097-ac12-d58ab8f8a789.png)
InspectorからSNSへメッセージを発行するにはSNSからアクセスポリシーの追加が必要です。

```json:SNSアクセスポリシー
{
  "Version": "2008-10-17",
  "Id": "__default_policy_ID",
  "Statement": [
    {
      "Sid": "__default_statement_ID",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": [
        "SNS:GetTopicAttributes",
        "SNS:SetTopicAttributes",
        "SNS:AddPermission",
        "SNS:RemovePermission",
        "SNS:DeleteTopic",
        "SNS:Subscribe",
        "SNS:ListSubscriptionsByTopic",
        "SNS:Publish",
        "SNS:Receive"
      ],
      "Resource": "arn:aws:sns:${AWS_REGION}:${AWS_ACCOUNT_ID}:inspector-sns-sample",
      "Condition": {
        "StringEquals": {
          "AWS:SourceOwner": "${AWS_ACCOUNT_ID}"
        }
      }
    },
    {
      "Sid": "InspectorSNSPolicy",
      "Effect": "Allow",
      "Principal": {
        "Service": "inspector.amazonaws.com"
      },
      "Action": "SNS:Publish",
      "Resource": "arn:aws:sns:${AWS_ACCOUNT_ID}:${AWS_ACCOUNT_ID}:inspector-sns-sample"
    }
  ]
}
```
上記のInspectorSNSPolicyを参考にしましょう。InspectorにSNSの発行権限を与えています。

次は通知用のLambda関数を作成しました。

```javascript:診断完了通知
const axios = require('axios');
const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb');
const dynamoDbClient = new DynamoDBClient();

exports.handler = async (event, context) => {
  const { run, time, event: inspectorEvent } = await JSON.parse(event.Records[0].Sns.Message);

  // Inspectorの実行完了時点で通知
  if (inspectorEvent == 'ASSESSMENT_RUN_COMPLETED') {
    const { awsRequestId } = context;

    // DynamoDBにLambda関数の実行IDをKeyにしてInspectorのRunArnを保存
    await dynamoDbClient.send(
      new PutItemCommand({
        TableName: process.env.DYNAMO_DB_TABLE,
        Item: {
          request_id: { S: `${awsRequestId}` },
          run_arn: { S: `${run}` },
        },
      }),
    );

    // こちらに通知用のwebhookを記載してください。
    // SlackならIncoming Webhookでメッセージを送ります。
    // ${run} : InspectorのRunARN（後ほどのレポート作成時に使われます）
    // cognito login URL : ${cognito-domain}?client_id=${cognito-client-id}&response_type=code&scope=openid&redirect_uri=${認証用lambdaのapi-gatewayURL}
  }

  const response = {
    statusCode: 200,
    body: null,
  };
  return response;
};

```

#####2. Cognito User PoolとHosted UIを利用

Cognito User Poolの作成方法やHostedUIを予め用意する必要があります。こちらもAWSのドキュメントで説明がありますのでリンクを紹介します。

[Amazon Cognito チュートリアル](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/tutorials.html)
[サインアップおよびサインインでの Amazon Cognito でホストされる UI の使用](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-app-integration.html)

作成したCognito User PoolはログインしてSTSトークンを発行し、API GatewayでCognito Authorizerで認証するために使われます。

#####3. API Gatewayの作成とCognito Authorizerの設定

今回の仕組みではAPI Gatewayのリソースを２つ利用しました。

- /validate : cognitoのコールバック先で、ユーザー検証とブラウザー画面遷移を担当します。
- /download-report : cognito authorizerを利用して、cognitoからの正しいユーザーだったら評価レポートのURLを返します。

```javascript:/validate
const axios = require('axios');
const qs = require('qs');
const { DynamoDBClient, GetItemCommand } = require('@aws-sdk/client-dynamodb');
const dynamoDbClient = new DynamoDBClient();

exports.handler = async (event) => {
  let code, state;

  try {
    ({
      queryStringParameters: { code, state },
    } = event);
  } catch (err) {}

  if (code == null || state == null) {
    return {
      statusCode: 400,
      body: JSON.stringify('Bad request: Mandatory parameter is not defined'),
    };
  }

  // CognitoのClient-IDとAPP-Secretを利用して認証コードを作成します。(Base64エンコード)
  const authCode = `${process.env.APP_CLIENT_ID}:${process.env.APP_CLIENT_SECRET}`;
  const requestData = {
    grant_type: 'authorization_code',
    code,
    redirect_uri: `${process.env.REDIRECT_URI}`,
  };

  let id_token;

  try {
    ({
      data: { id_token },
    } = await axios({
      method: 'post',
      url: process.env.TOKEN_URL,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Authorization: `Basic ${Buffer.from(authCode).toString('base64')}`,
      },
      data: qs.stringify(requestData),
    }));
  } catch (err) {}

  //console.log(`id_token: ${id_token}`);

  if (id_token == null) {
    return {
      statusCode: 400,
      body: JSON.stringify('Bad request: No result from cognito'),
    };
  }

  let run_arn;

  try {
    ({
      Item: {
        run_arn: { S: run_arn },
      },
    } = await dynamoDbClient.send(
      new GetItemCommand({
        TableName: process.env.DYNAMO_DB_TABLE,
        Key: {
          request_id: { S: `${state}` },
        },
      }),
    ));
  } catch (err) {}

  //console.log(run_arn);

  if (run_arn == null) {
    return {
      statusCode: 400,
      body: JSON.stringify('Bad request: No result from DB'),
    };
  }

  let status, url;

  try {
    ({
      data: { status, url },
    } = await axios({
      method: 'get',
      // /download-reportでInspectorの評価レポートのURLを持ってきます。
      url: process.env.DOWNLOAD_REPORT_URL, 
      headers: {
        Authorization: `${id_token}`,
      },
      data: {
        runArn: run_arn,
      },
    }));
  } catch (err) {}

  //console.log(status);
  //console.log(url);

  if (status == 'COMPLETED') {
    return {
      statusCode: 302,
      headers: {
        Location: url,
      },
    };
  }

  return {
    statusCode: 400,
    body: JSON.stringify('Something goes wrong..'),
  };
};

```

```javascript:/download-report
const { InspectorClient, GetAssessmentReportCommand } = require('@aws-sdk/client-inspector');

const REGION = 'ap-northeast-1';
const inspectorClient = new InspectorClient({
  region: REGION,
});

// nodejsでsleepを実装
const sleep = async (t) => {
  return await new Promise((r) => {
    setTimeout(() => {
      r();
    }, t);
  });
};

exports.handler = async (event) => {
  const { body } = event;
  const { runArn } = await JSON.parse(body);
  let data;

  while (true) {
    data = await inspectorClient.send(
      new GetAssessmentReportCommand({
        assessmentRunArn: `${runArn}`,
        reportFileFormat: 'HTML',
        reportType: 'FINDING',
      }),
    );

    const { status } = data;
    if (status == 'WORK_IN_PROGRESS') {
      // Inspectorの評価レポートはrequestを入れてからすぐ作成されるものではないのでその間は待ちます。
      await sleep(5000);
    } else {
      break;
    }
  }

  const response = {
    statusCode: 200,
    body: JSON.stringify(data),
  };
  return response;
};

```

これでリソースの作成は完了です。そして実際Inspectorを回してみました。

## 作成リソースの動作確認

Inspectorの実行が完了したら次のようなメッセージがきます。
![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472833/365b164a-cb07-e08d-5f13-ef221f66f707.png)
リンクを押してブラウザーに表示されるCognitoにログインします。
![スクリーンショット 2021-07-20 19.56.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472833/913ec610-6f2a-c6ac-28c7-177f9d8f384d.png)
そうすると診断結果の画面に遷移されます。こちらのリンクはCognitoにログインが成功するたびに作成され、その有効期限は15分です。
![スクリーンショット 2021-07-20 20.08.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/472833/93008f99-a791-43db-86ce-780b1ae852eb.png)

## まとめ

今回はCognitoを利用してAmazon Inspectorの評価レポートを共有する機能を実装してみました。評価レポートはシステムの脆弱性と関係があるので、正しい権限を持っているユーザーにだけ閲覧権限を与える必要があるます。

評価レポートをPDF化して管理するのもありですが、脆弱性が書かれているファイルの管理もしないといけないのでCognitoでサーバレス環境で認証されたユーザーにだけアクセスできるようにしたら管理も楽になるでしょう。