LoginSignup
1
2

More than 1 year has passed since last update.

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

Posted at

目次

はじめに

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

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

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

構成図

スクリーンショット 2021-07-20 16.01.09.png

 補足

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

リソース作成

1. InspectorとSNSの設定

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

Amazon Inspectorエージェント
Amazon Inspectorのチュートリアル

予め作成したSNSにInspectorの「実行完了」ステータスになったら通知するように設定を行います。その他の設定は今回のために作ったサンプルEC2を対象として設定しました。
スクリーンショット 2021-07-20 15.43.361.png
InspectorからSNSへメッセージを発行するにはSNSからアクセスポリシーの追加が必要です。

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関数を作成しました。

診断完了通知
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 チュートリアル
サインアップおよびサインインでの Amazon Cognito でホストされる UI の使用

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

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

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

  • /validate : cognitoのコールバック先で、ユーザー検証とブラウザー画面遷移を担当します。
  • /download-report : cognito authorizerを利用して、cognitoからの正しいユーザーだったら評価レポートのURLを返します。
/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..'),
  };
};

/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
リンクを押してブラウザーに表示されるCognitoにログインします。
スクリーンショット 2021-07-20 19.56.03.png
そうすると診断結果の画面に遷移されます。こちらのリンクはCognitoにログインが成功するたびに作成され、その有効期限は15分です。
スクリーンショット 2021-07-20 20.08.21.png

まとめ

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

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

1
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
1
2