🚀

Amazon Inspector と Lambda の連携を IaC 化

2023/02/28に公開

やりたいこと

この環境を見かけたので Lambda コード部分以外を IaC (AWS CloudFormation) で実装

https://cloudone.trendmicro.com/docs/workload-security/aws-integrate-sdk/

謝辞

Lambda のコードについては言及してないです。
具体的な Cloud One の仕様の話になってしまうので今回このブログでは記載していないです。

成果物

今回は Amazon Inspector のパターンでの実装になっています。
その他にする場合、EventBridge ルールの箇所を GuardDuty や Security Hub にすることでほかのパターンもできると思います。

以下のテンプレートをデプロイすることで一発完成です。
詳細は後述します。

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  EventBridgeRuleName:
    Type: String
    Default: inspector-finding
  NotifySeverities:
    Type: CommaDelimitedList
    Default: HIGH, CRITICAL

Resources:
  C1WSSetupLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: 2012-10-17
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Path: /

  C1WSSetupLambda:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      MemorySize: 128
      Runtime: python3.9
      Timeout: 60
      Role: !GetAtt C1WSSetupLambdaRole.Arn
      Layers:
        - Fn::Join:
          -  ""
          - - "arn:aws:lambda:"
            - !Ref AWS::Region
            - ":770693421928:layer:Klayers-p39-requests:10"
      Code:
        ZipFile: |
          import json, os, requests

          def lambda_handler(event, context):

              instance_ids = event['resources']
              cve_number = event['detail']['title']
	      
	      # 実際に行う処理

  EventBridgeRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref EventBridgeRuleName
      EventPattern:
        source: ["aws.inspector2"]
        detail-type: ["Inspector2 Finding"]
        detail:
          status: ["ACTIVE"]
          severity: !Ref NotifySeverities
      Targets:
        - Arn: !GetAtt C1WSSetupLambda.Arn
          Id: lambda

  InspectorEvent:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt C1WSSetupLambda.Arn
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventBridgeRule.Arn

内容について

テンプレートの中身について軽くコメントします。

Parameters:
  EventBridgeRuleName:
    Type: String
    Default: inspector-finding
  NotifySeverities:
    Type: CommaDelimitedList
    Default: HIGH, CRITICAL

Lambda をキックする脆弱性の重要度を選択します

Amazon Inspector のスコアと重要度ラベルの関係

  • INFORMATIONAL: 0
  • LOW : 0.1 - 3.9
  • MEDIUM: 4.0 - 6.9
  • HIGH: 7.0 - 8.9
  • CRITICAL: 9.0 - 10.0
  C1WSSetupLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: 2012-10-17
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      Path: /

  C1WSSetupLambda:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      MemorySize: 128
      Runtime: python3.9
      Timeout: 60
      Role: !GetAtt C1WSSetupLambdaRole.Arn
      Layers:
        - Fn::Join:
          -  ""
          - - "arn:aws:lambda:"
            - !Ref AWS::Region
            - ":770693421928:layer:Klayers-p39-requests:10"

IAM ロールの作成、および Lambda の作成です。
Lambda レイヤーは以下の有志の方のをお借りしました。
https://github.com/keithrozario/Klayers

  EventBridgeRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref EventBridgeRuleName
      EventPattern:
        source: ["aws.inspector2"]
        detail-type: ["Inspector2 Finding"]
        detail:
          status: ["ACTIVE"]
          severity: !Ref NotifySeverities
      Targets:
        - Arn: !GetAtt C1WSSetupLambda.Arn
          Id: lambda

EventBridge の設定
ターゲットは作成した Lambda
起動ルールは Amazon Inspector の Finding

  InspectorEvent:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt C1WSSetupLambda.Arn
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventBridgeRule.Arn

Lambda に対して EventBridge が実行することを許可する設定

はまった個所

EventBridge から Lambda を実行するときはリソースベースポリシーで許可が必要!(コンソール上からの設定を除く)

https://aws.amazon.com/jp/premiumsupport/knowledge-center/eventbridge-lambda-not-triggered/

これを忘れてずっと待ってたけど一向に実行ログが出なかったため EventBridge 側のメトリクスを見たら大量のFailedInvocationsが出力されていたため検索。。。
EventBridgeではLambdaを実行させるときに別途権限の付与が必要とのこと

※Permissionがないと以下のようにLambdaコンソールに存在していない

Amazon Inspector (EventBridge) からもらう内容

eventの中に含まれています

{
    "version": "0",
    "id": "22ac7f76-c252-fa8f-98e3-f81d657c5574",
    "detail-type": "Inspector2 Finding",
    "source": "aws.inspector2",
    "account": "xxx",
    "time": "2023-02-25T04:12:24Z",
    "region": "ap-northeast-1",
    "resources": [
        "i-xxx"
    ],
    "detail": {
        "awsAccountId": "xxx",
        "description": "Windows Common Log File System Driver Elevation of Privilege Vulnerability",
        "findingArn": "arn:aws:inspector2:ap-northeast-1:xxx:finding/70bdf77b670c4de97568f3c9db380e3f",
        "firstObservedAt": "Feb 25, 2023, 4:12:24 AM",
        "fixAvailable": "YES",
        "inspectorScore": 7.8,
        "inspectorScoreDetails": {
            "adjustedCvss": {
                "adjustments": [],
                "cvssSource": "WINDOWS_SERVER_2016",
                "score": 7.8,
                "scoreSource": "WINDOWS_SERVER_2016",
                "scoringVector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H/E:U/RL:O/RC:C",
                "version": "3.1"
            }
        },
        "lastObservedAt": "Feb 25, 2023, 4:12:24 AM",
        "packageVulnerabilityDetails": {
            "cvss": [
                {
                    "baseScore": 7.8,
                    "scoringVector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H/E:U/RL:O/RC:C",
                    "source": "WINDOWS_SERVER_2016",
                    "version": "3.1"
                }
            ],
            "referenceUrls": [
                "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-23376",
                "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5022838"
            ],
            "relatedVulnerabilities": [],
            "source": "WINDOWS_SERVER_2016",
            "sourceUrl": "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2023-23376",
            "vendorCreatedAt": "Feb 14, 2023, 8:00:00 AM",
            "vendorSeverity": "N/A",
            "vulnerabilityId": "CVE-2023-23376"
        },
        "remediation": {
            "recommendation": {
                "text": "See References"
            }
        },
        "resources": [
            {
                "details": {
                    "awsEc2Instance": {
                        "iamInstanceProfileArn": "arn:aws:iam::xxx:instance-profile/xxx",
                        "imageId": "ami-xxx",
                        "ipV4Addresses": [
                            "x.x.x.x",
                            "x.x.x.x"  # パブリックIPアドレスもここに表示される
                        ],
                        "ipV6Addresses": [],
                        "keyName": "xxx_pem",
                        "launchedAt": "Feb 25, 2023, 4:02:56 AM",
                        "platform": "WINDOWS_SERVER_2016",
                        "subnetId": "subnet-xxx",
                        "type": "t2.medium",
                        "vpcId": "vpc-xxx"
                    }
                },
                "id": "i-xxx",
                "partition": "aws",
                "region": "ap-northeast-1",
                "tags": {
                    "Name": "xxx"
                },
                "type": "AWS_EC2_INSTANCE"
            }
        ],
        "severity": "HIGH",
        "status": "ACTIVE",
        "title": "CVE-2023-23376",
        "type": "PACKAGE_VULNERABILITY",
        "updatedAt": "Feb 25, 2023, 4:12:24 AM"
    }
}

最後に

簡単にですが久しぶりに CloudFormation テンプレートを書くのも楽しいですね。

追記

汚いですが自分用に全部のコードも載せておきます
*Cloud One の部分が多分に含まれているためそこはご容赦ください

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  EventBridgeRuleName:
    Type: String
    Default: inspector-finding
  NotifySeverities:
    Type: CommaDelimitedList
    Default: HIGH, CRITICAL
  CloudOneRegion:
    Type: String
  CloudOneAPIKey:
    Type: String

Resources:
  C1WSSetupLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: 2012-10-17
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: /
      Policies:
        - PolicyName: SendCommandPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:SendCommand
                Resource: "*"

  C1WSSetupLambda:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      MemorySize: 128
      Runtime: python3.9
      Timeout: 60
      Environment:
        Variables:
          C1_REGION: !Ref CloudOneRegion
          C1_APIKEY: !Ref CloudOneAPIKey
      Role: !GetAtt C1WSSetupLambdaRole.Arn
      Layers:
        - Fn::Join:
          -  ""
          - - "arn:aws:lambda:"
            - !Ref AWS::Region
            - ":770693421928:layer:Klayers-p39-requests:10"
      Code:
        ZipFile: |
          import json, os, requests, boto3, time

          c1_region = os.getenv('C1_REGION')
          api_key = os.getenv('C1_APIKEY')
          headers = {
              "Authorization": f"ApiKey {api_key}",
              "api-version": "v1",
              'Content-Type': 'application/json'
          }

          def get_specific_dsa_info_from_instance_id(instance_id):
              # 特定インスタンスIDのDSA情報を取得する
              req_url = f"https://workload.{c1_region}.cloudone.trendmicro.com/api/computers"
              res = requests.get(req_url, headers=headers).json()
              computers = res["computers"]
              target_dsa = ""
              for computer in computers:
                  aws_info = computer.get("ec2VirtualMachineSummary", computer.get("noConnectorVirtualMachineSummary"))
                  #print("HostName: {}".format(computer.get("hostName")))
                  #print("ID: {}".format(computer.get("ID")))
                  if aws_info:
                      #print("InstanceID: {}".format(aws_info.get("instanceID")))
                      if aws_info.get("instanceID") == instance_id:
                          target_dsa = computer
              return target_dsa

          def search_ips_rule_from_cve_number(cve_number):
              # 該当CVEに対応するIPSルールを取得
              req_url = f"https://workload.{c1_region}.cloudone.trendmicro.com/api/intrusionpreventionrules/search"
              data = {
                  "searchCriteria": [
                      {
                          "fieldName": "CVE",
                          "stringTest": "equal",
                          "stringValue": cve_number
                      }
                  ]
              }
              res = requests.post(req_url, data=json.dumps(data), headers=headers).json()
              rules = res["intrusionPreventionRules"]
              #for rule in rules:
              #    print(rule["name"])
              #    print(rule["ID"])
              return rules

          def get_ips_rule_id_attached_specific_policy(policy_id=1):
              # 特定ポリシーにアサインされているIPSルールIDを取得
              req_url = f"https://workload.{c1_region}.cloudone.trendmicro.com/api/policies/{policy_id}/intrusionprevention/assignments"
              res = requests.get(req_url, headers=headers).json()
              return res['assignedRuleIDs']

          def add_ips_rule_to_specific_policy(rule_ids, policy_id=1):
              # 特定ポリシーに特定IPSルールを追加
              req_url = f"https://workload.{c1_region}.cloudone.trendmicro.com/api/policies/{policy_id}/intrusionprevention/assignments"
              data = {
                  "ruleIDs": rule_ids
              }
              res = requests.post(req_url, data=json.dumps(data), headers=headers).json()
              return res

          def activate_ips_feature_on_dsa(computer_id, state="prevent"):
              # DSAのIPSを有効化(ポリシー単位ではない点に注意、設定をIDSにする場合"detect"を指定する)
              req_url = f"https://workload.{c1_region}.cloudone.trendmicro.com/api/computers/{computer_id}"
              data = {
                  "intrusionPrevention": {
                      "state": state
                  }
              }
              res = requests.post(req_url, data=json.dumps(data), headers=headers).json()
              return res

          def create_policy(parent_id=1, state="prevent"):
              # ポリシーを作成(継承元"parentID":1はBasePolicy、設定をIDSにする場合"detect"を指定する)
              req_url = f"https://workload.{c1_region}.cloudone.trendmicro.com/api/policies"
              data = {
                  "parentID": parent_id,
                  "name": "AutomatedCreatePolicy",
                  "intrusionPrevention": {
                      "state": state
                  }
              }
              res = requests.post(req_url, data=json.dumps(data), headers=headers).json()
              return res

          def attach_policy(computer_id, policy_id):
              # DSAにポリシーをアタッチする
              req_url = f"https://workload.{c1_region}.cloudone.trendmicro.com/api/computers/{computer_id}"
              data = {
                  "policyID": policy_id
              }
              res = requests.post(req_url, data=json.dumps(data), headers=headers).json()
              return res

          def distribute_agent(instance_ids):
              client = boto3.client('ssm')
              response = client.send_command(
                  InstanceIds=instance_ids,
                  DocumentName='AWS-ConfigureAWSPackage',
                  TimeoutSeconds=600,
                  Parameters={
                      'action': [
                          'Install'
                      ],
                      'name': [
                          'TrendMicro-CloudOne-WorkloadSecurity'
                      ]
                  },
              )
              time.sleep(5.0)

              command_id = response['Command']['CommandId']
              output = ssm_client.get_command_invocation(
                  CommandId=command_id,
                  InstanceId=instance_id,
              )
              if output.get('StandardOutputContent'):
                  success = output['StandardOutputContent']
                  print("Output = \n{}\n".format(success))
                  return True
              else:
                  error = output['StandardErrorContent']
                  print("Error  = \n{}\n".format(error))
                  return False

          def lambda_handler(event, context):

              instance_ids = event['resources']
              cve_number = event['detail']['title']
              rules = search_ips_rule_from_cve_number(cve_number)  # CVEに対応したIPSフィルタ有無のチェック
              if not len(rules) > 0:  # 対応フィルタがない場合終了
                print("Not found Corresponding Filter. CVE: {}".format(cve_number))
                return

              computers = []
              non_agents = []
              for instance_id in instance_ids:
                computer = get_specific_dsa_info_from_instance_id(instance_id)
                if computer:
                  computers.append(computer)
                else:
                  non_agents.append(instance_id)

              need_setup = []
              if computers:
                for computer in computers:

                  computer_id = computer["ID"]
                  computer_policy_id = computer["policyID"]
                  computer_ips_state = computer["intrusionPrevention"]["moduleStatus"]["agentStatus"]
                  computer_assigned_ips_rules = computer["intrusionPrevention"]["ruleIDs"]

                  # IPS 機能のステータスチェック
                  if computer_ips_state == "inactive":  # 有効化されていない場合は有効化
                      activate_ips_feature_on_dsa(computer_id, state="prevent")
                  elif computer_policy_id != "active":  # 有効化以外の場合何らかのエラーが発生しているので終了
                      print("Output, Unexpected errors")
                      continue

                  non_assign_rule = rules
                  for rule in rules:  # 該当ルールがアサインされているかのチェック
                      if rule in computer_assigned_ips_rules:
                          non_assign_rule.remove(rule)

                  if not len(non_assign_rule) > 0:  # すでに該当のルールがアサインされている場合終了
                      print("Already assigned target rules")
                      continue

                  if  computer_policy_id != 0:  # ポリシーがすでに割り当てられている場合、ポリシーにルールを追加して終了 #TODO: ポリシーIDの0はたぶん未割当
                      add_ips_rule_to_specific_policy(rules, computer_policy_id)
                      print("Added target rules to assigned policy")
                      continue

                  need_setup.append(computer)

              else:
                result = distribute_agent(non_agents)
                if not result:
                  print("Error occurre, DSA install Failed")
                for instance_id in non_agents:
                  computer = get_specific_dsa_info_from_instance_id(instance_id)
                  if computer:
                    need_setup.append(computer)

              if not len(need_setup) > 0:
                print("Non Target")
                return

              res = create_policy()  # ポリシーを新規作成
              created_policy_id = res["ID"]
              add_ips_rule_to_specific_policy(rules, created_policy_id)  # 作成したポリシーに該当ルールを追加

              for computer in need_setup:
                computer_id = computer["ID"]
                attach_policy(computer_id, created_policy_id)  # 作成したポリシーをアタッチ
                print("Created new policy that assigned target rules, and attach to agent")

  EventBridgeRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Ref EventBridgeRuleName
      EventPattern:
        source: ["aws.inspector2"]
        detail-type: ["Inspector2 Finding"]
        detail:
          status: ["ACTIVE"]
          severity: !Ref NotifySeverities
          resources:
            type: ["AWS_EC2_INSTANCE"]
      Targets:
        - Arn: !GetAtt C1WSSetupLambda.Arn
          Id: lambda

  InspectorEvent:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt C1WSSetupLambda.Arn
      Principal: events.amazonaws.com
      SourceArn: !GetAtt EventBridgeRule.Arn

  GetTenantParamLambda:
    Type: AWS::Lambda::Function
    Description: >
      This Function get token and tenantID.
    Properties:
      Handler: index.lambda_handler
      MemorySize: 128
      Runtime: python3.9
      Timeout: 60
      Environment:
        Variables:
          C1_REGION: !Ref CloudOneRegion
          C1_APIKEY: !Ref CloudOneAPIKey
      Role: !GetAtt C1WSSetupLambdaRole.Arn
      Layers:
        - Fn::Join:
          -  ""
          - - "arn:aws:lambda:"
            - !Ref AWS::Region
            - ":770693421928:layer:Klayers-p39-requests:10"
      Code:
        ZipFile: |
          import json, os, requests
          import cfnresponse

          c1_region = os.getenv('C1_REGION')
          api_key = os.getenv('C1_APIKEY')
          headers = {
              "Authorization": f"ApiKey {api_key}",
              "api-version": "v1",
              'Content-Type': 'application/json'
          }

          def deploy_script_generate(platform, policy_id=1):
              # デプロイスクリプトの取得 (継承元"parentID":1はBasePolicyを想定)
              req_url = f"https://workload.{c1_region}.cloudone.trendmicro.com/api/agentdeploymentscripts"
              data = {
                  "policyID": policy_id,
                  "platform": platform,
                  "validateCertificateRequired": "true",
                  "validateDigitalSignatureRequired": "false",
                  "activationRequired": "true"
              }
              res = requests.post(req_url, data=json.dumps(data), headers=headers).json()
              return res

          def lambda_handler(event, context):
              try:
                script = deploy_script_generate("windows")
                persed_script = script['scriptBody'].split(" ")
                for i in persed_script:
                    if "token:" in i:
                        token = i.split(":")[1]
                    if "tenantID:" in i:
                        tenant_id = i.split(":")[1]
                sendResponseCfn(event, context, cfnresponse.SUCCESS, token, tenant_id)
              except Exception as e:
                print(e)
                sendResponseCfn(event, context, cfnresponse.FAILED)

          def sendResponseCfn(event, context, responseStatus, token="", tenant_id=""):
              responseData = {}
              responseData['Token'] = token
              responseData['TenantID'] = tenant_id
              cfnresponse.send(event, context, responseStatus, responseData, "CustomResourcePhysicalID")

  GetTenantParam:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !GetAtt GetTenantParamLambda.Arn

  dsTokenSsm:
    Type: AWS::SSM::Parameter
    Properties:
      Name: dsToken
      Value: !GetAtt GetTenantParam.Token
      Type: String

  dsTenantIdSsm:
    Type: AWS::SSM::Parameter
    Properties:
      Name: dsTenantId
      Value: !GetAtt GetTenantParam.TenantID
      Type: String

  dsManagerUrlSsm:
    Type: AWS::SSM::Parameter
    Properties:
      Name: dsManagerUrl
      Value: !Sub https://workload.{CloudOneRegion}.cloudone.trendmicro.com:443
      Type: String

  dsActivationUrlSsm:
    Type: AWS::SSM::Parameter
    Properties:
      Name: dsActivationUrl
      Value: !Sub dsm://agents.workload.{CloudOneRegion}.cloudone.trendmicro.com:443
      Type: String

Discussion