目次
はじめに
今回はAmazon Inspectorで評価したレポートをCognitoで認証したユーザーに表示する方法を考えてみました。レポートは元々はコンソールで見れますが、共有機能がないのでその代わりに今回の仕組みを利用できます。
管理コンソールでレポートを発行してそのURLを共有する方法もありますが、URLの有効期限が15分しかないので必要なたびに管理コンソールにアクセスして発行する手間がかかります。
Cognitoを利用すると必要なタイミングでログインしてレポートが見れるのでログインID/Passwordさえ共有すればいつでもレポートを見ることができます。
構成図
#### 補足
- CloudWatch Eventを利用してタイムベースでInspectorを週1で回します。(Inspectorは実行後に通知ができるので、今回はメールで結果のURLを受信してみます)
- メールで受信したCognitoリンクにアクセスして、事前に共有があったID/Passwordでログインします。
- Cognitoで認証が成功するとSTSでトークンが発行され、API GatewayのCognito認証を利用してレポートのダウンロードを実行します。
- レポートの作成が終わったら、Lambda関数はレポートのURLにユーザーを遷移させます。
リソース作成
#####1. InspectorとSNSの設定
この記事ではInspector自体の説明やエージェントの設置などはスキップさせていただきます。詳しくはAWSのドキュメントをご参考ください。
Amazon Inspectorエージェント
Amazon Inspectorのチュートリアル
予め作成したSNSにInspectorの「実行完了」ステータスになったら通知するように設定を行います。その他の設定は今回のために作ったサンプルEC2を対象として設定しました。
Inspectorから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を返します。
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..'),
};
};
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の実行が完了したら次のようなメッセージがきます。
リンクを押してブラウザーに表示されるCognitoにログインします。
そうすると診断結果の画面に遷移されます。こちらのリンクはCognitoにログインが成功するたびに作成され、その有効期限は15分です。
まとめ
今回はCognitoを利用してAmazon Inspectorの評価レポートを共有する機能を実装してみました。評価レポートはシステムの脆弱性と関係があるので、正しい権限を持っているユーザーにだけ閲覧権限を与える必要があるます。
評価レポートをPDF化して管理するのもありですが、脆弱性が書かれているファイルの管理もしないといけないのでCognitoでサーバレス環境で認証されたユーザーにだけアクセスできるようにしたら管理も楽になるでしょう。