EC2の脆弱性診断をInspectorで自動化してみた


本記事はサムザップ #1 Advent Calendar 2020の 20 日目の記事です。

Amazon Inspectorとは

InspectorについてはAWSの公式ドキュメントでご確認ください。
https://aws.amazon.com/jp/inspector/

実現したいこと

定期的に自動でEC2インスタンスを起動してInspectorで脆弱性をチェックしてSlackへ通知する。起動中のインスタンスを接直チェックはしたくない。
上記要件を踏まえた上で以下構成で定期的に脆弱性チェックを行うことにしました。

  1. Amazon EventBridgeでLambdaを実行
  2. Lambdaから脆弱性チェックしたいEC2インスタンスのAMIからインスタンスを起動
  3. 起動したインスタンスをAmazon Inspectorで脆弱性チェック
  4. Inspectorの診断が完了したらSNS経由で診断結果を取得してSlackに通知するLambdaを実行

AWSリソースの作成は全てTerraformでコード管理できるようにしました。

Inspectorの設定

inspector.tf

######################################
# 評価ターゲット作成
######################################
resource "aws_inspector_resource_group" "resource" {
  tags = {
    Inspector = "true"
  }
}

resource "aws_inspector_assessment_target" "target" {
  name               = "test-target"
  resource_group_arn = aws_inspector_resource_group.resource.arn
}

######################################
# Rule Package 取得
######################################
data "aws_inspector_rules_packages" "rules" {}

######################################
# 評価テンプレート作成
######################################
resource "aws_inspector_assessment_template" "template" {
  name               = "test-template"
  target_arn         = aws_inspector_assessment_target.target.arn
  duration           = 3600
  rules_package_arns = data.aws_inspector_rules_packages.rules.arns
}

・評価ターゲットの作成
Tag:Inspector:trueが設定されているEC2インスタンスをターゲットにします。

・評価Rule Packageの取得
以下全てのRule Packageで評価テンプレートを取得します。
Common Vulnerabilities and Exposures: 共通脆弱性識別子
Security Best Practices: Amazon Inspector のセキュリティのベストプラクティス
Center for Internet Security (CIS) Benchmarks: Center for Internet Security (CIS) ベンチマーク
Network Reachability: ネットワーク到達可能性

・評価テンプレート作成
作成した評価ターゲットを設定
実行時間を1時間に設定
取得した評価Rule Packageを設定

Lambdaの設定

Lambda関数は以下の2つの関数を作成します。
・AMIを起動してInspector評価の実行を行うLambda
・Inspector評価の結果をSlackに通知するLambda

AMIを起動してInspector評価の実行を行うLambda
function_inspector_run.tf

######################################
# Lambda IAM Role作成
######################################
resource "aws_iam_role" "lambda_role" {
  name = "lambda-inspector-role"
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

######################################
# Lambda IAM Policy作成
######################################
resource "aws_iam_policy" "lambda_policy" {
  name        = "lamda-inspector-policy"
  description = "LambdaFunction function_inspector_run/function_inspector_report Use Policy"
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "inspector:GetAssessmentReport",
                "inspector:ListAssessmentTemplates",
                "inspector:ListAssessmentTargets",
                "inspector:ListAssessmentRuns",
                "inspector:StartAssessmentRun",
                "inspector:PreviewAgents"
            ],
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

######################################
# RoleにPolicyをアタッチ
######################################
resource "aws_iam_role_policy_attachment" "lambda_role_policy_attach" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = aws_iam_policy.lambda_policy.arn
}

######################################
# Lambda関数の作成
######################################
resource "aws_lambda_function" "inspector_run" {
  filename         = "./function_inspector_run.zip"
  function_name    = "function_inspector_run"
  role             = aws_iam_role.lambda_role.arn
  handler          = "lambda_function.lambda_handler"
  source_code_hash = filebase64sha256("./function_inspector_run.zip")
  runtime          = "python3.8"
  timeout          = 300
  environment {
    variables = {
      TZ               = "Asia/Tokyo"
      SECURITYGROUP_ID = "xxxxxx"
      SUBNET_ID        = "xxxxxx"
    }
  }
}

function_inspector_run.py

import boto3
import os
from operator import itemgetter
import logging
import datetime
import time

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    create_ec2_instance()
    start_inspector_assessment_run()

def create_ec2_instance():
    """
    Inspectorで分析するため最新のAMIを取得しAMIからEC2Instanceを起動します
    起動設定でawsagentをinstallします
    """
    # web AMI一覧取得
    ec2 = boto3.client('ec2')
    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_images
    response = ec2.describe_images(
        Filters=[
            {
                "Name": "tag:Name",
                "Values": [
                    "test-web*",
                ]
            }
        ],
        Owners=[
            "self"
        ]
    )
    # 最新のAMIを取得
    image_details = sorted(response['Images'], key=itemgetter('CreationDate'), reverse=True)
    image_id = image_details[0]['ImageId']
    user_data = """#!/bin/sh
    sudo su -
    wget https://inspector-agent.amazonaws.com/linux/latest/install
    bash install -u false
    /etc/init.d/awsagent restart
    """
    # Instance作成/起動
    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.run_instances
    response = ec2.run_instances(
        ImageId=image_id,
        InstanceType="t2.micro",
        MinCount=1,
        MaxCount=1,
        KeyName="test_key_pair",
        SecurityGroupIds=[
            os.environ['SECURITYGROUP_ID']
        ],
        SubnetId=os.environ['SUBNET_ID'],
        UserData=user_data,
        TagSpecifications=[
            {
                'ResourceType': 'instance',
                'Tags': [
                    {
                        'Key': 'Inspector',
                        'Value': 'true'
                    },
                    {
                        'Key': 'Name',
                        'Value': 'inspector-target'
                    }
                ]
            }
        ]
    )
    # EC2Instanceがstatus:runningになるまで待ち
    instance_id = response.get('Instances')[0]['InstanceId']
    while 1:
        time.sleep(30)
        instances = ec2.describe_instances(
            InstanceIds=[
                instance_id
            ]
        )
        logger.info(instances.get('Reservations')[0].get('Instances')[0].get('State').get('Name'))
        if instances.get('Reservations')[0].get('Instances')[0].get('State').get('Name') == 'running':
            break

def start_inspector_assessment_run():
    """
    Inspector評価実行を行います
    """
    # 評価テンプレート一覧取得
    inspector = boto3.client('inspector')
    # 評価ターゲットのEC2Instance awsagentのステータスがHEALTHYになるまで待機
    assessment_target = inspector.list_assessment_targets(
        filter={
            'assessmentTargetNamePattern': os.environ['ENV'] + '-target'
        }
    )
    while 1:
        time.sleep(30)
        awsagents_status = inspector.preview_agents(previewAgentsArn=assessment_target.get('assessmentTargetArns')[0])
        logger.info(awsagents_status.get('agentPreviews')[0])
        if awsagents_status.get('agentPreviews')[0].get('agentHealth') == 'HEALTHY':
            break
    """
    list_assessment_templates Response Syntax
    {
        'assessmentTemplateArns': [
            'string',
        ],
        'nextToken': 'string'
    }
    https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/inspector.html#Inspector.Client.list_assessment_templates
    """
    assessment_templates = inspector.list_assessment_templates(
        filter={
            'namePattern': 'test-template'
        }
    )
    # 評価テンプレートARN一覧取得
    if not assessment_templates.get('assessmentTemplateArns'):
        return
    logger.info(assessment_templates.get('assessmentTemplateArns'))
    # 評価の実行
    for template_arn in assessment_templates.get('assessmentTemplateArns'):
        inspector.start_assessment_run(
            assessmentTemplateArn=template_arn,
            assessmentRunName='RunAssessment_' + 'test-template' + datetime.date.today().strftime("%Y-%m-%d")
        )

Inspector評価の結果をSlackに通知するLambda
function_inspector_report.tf

resource "aws_lambda_function" "inspector_report" {
  filename         = "./function_inspector_report.zip"
  function_name    = "function_inspector_report"
  role             = aws_iam_role.lambda_role.arn
  handler          = "lambda_function.lambda_handler"
  source_code_hash = filebase64sha256("./function_inspector_report.zip")
  runtime          = "python3.8"
  timeout          = 300
  environment {
    variables = {
      TZ       = "Asia/Tokyo"
      SLACK_HOOK_URL = "https://xxxxxxxxxxxxx"
    }
  }
}

resource "aws_lambda_permission" "inspector_report_permission" {
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.inspector_report.function_name
  principal     = "sns.amazonaws.com"
}

function_inspector_report.py

import json
import boto3
import os
import logging
import datetime
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
import time

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):

    assessment_run_arn = get_assessment_run_arn(event)
    if not assessment_run_arn:
        logger.info('AssessmentTarget Not Found')
        logger.info(event)
        return

    send_inspector_assessment_report(assessment_run_arn)
    terminate_ec2_instance()

def get_assessment_run_arn(event):
    """
      評価実行のARN取得処理
      SNS, Lambdaテスト実行の判定を行いInspector評価実行のARNを返却します
      Args:
          event : Lambda実行Parameter
      Return:
          assessment_run_arn: Inspector 評価実行ARN
    """
    inspector = boto3.client('inspector')
    # SNSからのLambda実行
    if event.get('Records'):
        logger.info('SNS execute')
        message = event['Records'][0]['Sns']['Message']
        message = json.loads(message)
        logger.info(message)
        return message.get('run')
    # Lambda テスト実行パラメータ有
    elif event.get('target_date'):
        logger.info('Lambda execute Parameter ON target_date =' + event.get('target_date'))
        """
        list_assessment_runs Response Syntax
        {
            'assessmentRunArns': [
                'string',
            ],
            'nextToken': 'string'
        }
        https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/inspector.html#Inspector.Client.list_assessment_runs
        """
        assessment_runs = inspector.list_assessment_runs(
            filter={
                'namePattern':'RunAssessment_' + 'test-template' + event.get('target_date')
            }
        )
        return assessment_runs.get('assessmentRunArns')[0]
    # Lambda テスト実行パラメータ無
    else:
        logger.info('Lambda execute Parameter OFF')
        assessment_runs = inspector.list_assessment_runs(
            filter={
                'namePattern':'RunAssessment_' + 'test-template' + datetime.date.today().strftime("%Y-%m-%d")
            }
        )
        return assessment_runs.get('assessmentRunArns')[0]

def send_inspector_assessment_report(assessment_run_arn):
    """
    Inspector評価実行結果をSlackに送信します
    """
    inspector = boto3.client('inspector')
    report = {}
    """
    get_assessment_report Response Syntax
    {
        'status': 'WORK_IN_PROGRESS'|'FAILED'|'COMPLETED',
        'url': 'string'
    }
    https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/inspector.html#Inspector.Client.get_assessment_report
    """
    # すぐ結果を取得しても取得できない時があるのでStatusがCOMPLETEDになるまで待機
    while 1:
        time.sleep(30);
        # HTML形式でReport出力
        assessment_report_html = inspector.get_assessment_report(
            assessmentRunArn=assessment_run_arn,
            reportFileFormat='HTML',
            reportType='FULL'
        )
        if assessment_report_html.get('status') == 'COMPLETED':
            break

    # すぐ結果を取得しても取得できない時があるのでStatusがCOMPLETEDになるまで待機
    while 1:
        time.sleep(30);
        # PDF形式でReport出力
        assessment_report_pdf = inspector.get_assessment_report(
            assessmentRunArn=assessment_run_arn,
            reportFileFormat='PDF',
            reportType='FULL'
        )
        logger.info(assessment_report_pdf.get('status'))
        if assessment_report_pdf.get('status') == 'COMPLETED':
            break

    report['arn'] = assessment_run_arn
    report['html'] = assessment_report_html.get('url')
    report['pdf'] = assessment_report_pdf.get('url')

    channel = '#' + 'inspector-report'
    message = "*Inspector 評価実行結果*\n*Arn*:```{}```\n\n*HTML結果*:```{}```\n\n*PDF結果*:```{}```\n\n※ページの有効期限が900sで切れます。有効期限が切れた時はLambda関数:function_inspector_reportでテスト実行またはawscliから実行を行なってください\n```テストイベントのパラメータ:{}\naws inspector get-assessment-report --assessment-run-arn {} --report-file-format PDF --report-type FULL```"
    slack_message = {
        'channel': channel,
        'text': message.format(
            report.get('arn'),
            report.get('html'),
            report.get('pdf'),
            '{"target_date": YYYYMMDD}',
            report.get('arn')
        )
    }
    HOOK_URL = os.environ['SLACK_HOOK_URL']

    req = Request(HOOK_URL, json.dumps(slack_message).encode('UTF-8'))
    try:
        response = urlopen(req)
        response.read()
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

def terminate_ec2_instance():
    """
    Inspectorで分析したEC2InstanceをTerminateします
    """
    ec2 = boto3.client('ec2')
    # tag:Inspector:trueのEC2Instance一覧取得
    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.describe_instances
    instances = ec2.describe_instances(Filters=[{
        'Name': 'tag:Inspector',
        'Values': ['true']
    }])

    if not instances:
        return

    # InstanceIDの配列生成
    instance_ids = []
    for reservation in instances.get('Reservations'):
        for instance in reservation.get('Instances'):
            logger.info(instance.get('InstanceId'))
            instance_ids.append(instance.get('InstanceId'))

    if not instance_ids:
        return

    # 対象EC2InstanceのTerminate
    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Client.terminate_instances
    ec2.terminate_instances(
        InstanceIds=instance_ids
    )

CloudWatchEventsの設定

最後にAmazon EventBridgeでInspector実行Lambdaをスケジュール設定して完了です。

実行してみた結果

Slackに以下メッセージが表示されたことが確認できました!!

Inspector 評価実行結果
Arn:
arn:aws:inspector:xxxx:xxxx:target/xxxx/template/xxxx/run/xxxx
HTML結果:
https://inspector-html-report
PDF結果:
https://inspector-pdf-report

※ページの有効期限が900sで切れます。有効期限が切れた時はLambda関数:function_inspector_reportでテスト実行またはawscliから実行を行なってください
テストイベントのパラメータ:{"target_date": YYYYMMDD}
aws inspector get-assessment-report --assessment-run-arn xxx --report-file-format PDF or HTML --report-type FULL