Amazon Inspector と Lambda の連携を IaC 化
やりたいこと
この環境を見かけたので Lambda コード部分以外を IaC (AWS CloudFormation) で実装
謝辞
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 レイヤーは以下の有志の方のをお借りしました。
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 を実行するときはリソースベースポリシーで許可が必要!(コンソール上からの設定を除く)
これを忘れてずっと待ってたけど一向に実行ログが出なかったため 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