GuardDutyの結果をLambda ( Go ) でSlackに通知する
はじめに
AWSのセキュリティ対策のひとつにGuardDutyというサービスがあります。
現時点では30日間の無料トライアルがあるので、一度有効にしてみて、1ヶ月でどれくらいコストがかかるのかを確認することができます。
今回はこの脅威検出サービスで検知した結果を少しカスタマイズしてSlackに通知させます。
Slack通知を設定
アーキテクチャ
通知結果
今回は、GuardDutyにサンプル生成した結果をSlack通知させました。
- 高 ( GetFindingsの結果 severity パラメータの値は 7.0〜8.9 の範囲 )
- 中 ( GetFindingsの結果 severity パラメータの値は 4.0〜6.9 の範囲 )
- 低 ( GetFindingsの結果 severity パラメータの値は 0.1〜3.9 )
- ※ ) 値 0 と 9.0 から 10.0 が将来使用するために現在予約されています。
このseverity
の値 ( 高・中・低 ) によってSlack通知の色分けをしました。
今回、以下のように脅威度によって配色しました。
color
- 高:
danger
- 中:
warning
- 低:
#0000ff
- 高:
slack通知結果
CloudWatchの設定
- イベントバターン
- サービス名:
GuardDuty
- イベントタイプ:
GuardDuty Finding
- ターゲット: Lambda関数を選択して関数名を指定
serverlessFrameworkでデプロイしていますが、コンソール画面での設定ものせておきます。
Lambda関数
serverlessFrameworkでデプロイします。ソースコードはのちほど記載します。 通知のjsonをパースしてSlackに通知するGoのコードは以下の通りです。
cloudwatchに送信されたGuardDutyのjsonをパースするために調べて必要なところだけ加工したjsonが以下です。
- json全体をログ出力。
err := json.Unmarshal([]byte(event.Detail), gd) fmt.Println(string([]byte(event.Detail))) if err != nil { fmt.Println(err) }
- CloudWatchLogsでログを確認 (必要なところだけ抽出)
{ "schemaVersion": "2.0", "accountId": "XXX", "region": "ap-northeast-1", "type": "UnauthorizedAccess:EC2/TorRelay", "resource": { "resourceType": "Instance", "instanceDetails": { "instanceId": "i-99999999", "instanceType": "m3.xlarge" } }, "title": "EC2 instance i-99999999 is communicating with Tor Exit node.", "description": "EC2 instance i-99999999 is communicating with IP address 198.51.100.0 on the Tor Anonymizing Proxy network." }
{ "schemaVersion": "2.0", "accountId": "XXX", "region": "ap-northeast-1", "type": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration", "resource": { "resourceType": "AccessKey", "accessKeyDetails": { "accessKeyId": "GeneratedFindingAccessKeyId", "principalId": "GeneratedFindingPrincipalId", "userType": "IAMUser", "userName": "GeneratedFindingUserName" } }, "severity": 8, "title": "Credentials for instance role GeneratedFindingUserName used from external IP address.", "description": "Credentials created exclusively for an EC2 instance using instance role GeneratedFindingUserName have been used from external IP address 198.51.100.0." }
これをもとにgoのコードを作成。
- main.go
package main import ( "encoding/json" "fmt" "os" "strconv" "strings" "github.com/ashwanthkumar/slack-go-webhook" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/iam" ) const ( version = "0.0.1" region = "ap-northeast-1" ) var ( // USERNAME username of slack USERNAME = "GuardDutyAlert" // SLACKURL webhookurl of slack SLACKURL = os.Getenv("SLACKURL") notPostThreshold = os.Getenv("THRESHOLD") // add excluded account name: []string{"dev"} excludeAccountList = []string{"dev"} config = aws.Config{Region: aws.String(region)} svcIAM = iam.New(session.New(&config)) ) // GuardDutyFindings set guardduty GuardDutyFindingsValue type GuardDutyFindings struct { AccountID string `json:"accountId"` Region string `json:"region"` Type string `json:"type"` Severity json.Number `json:"severity"` Title string `json:"title"` Description string `json:"description"` Resource Resource `json:"resource"` } // Resource set guardduty ResourceValue type Resource struct { ResourceType string `json:"resourceType,omitempty"` UserName string `json:"userName,omitempty"` InstanceDetails InstanceDetails `json:"instanceDetails,omitempty"` AccessKeyDetails AccessKeyDetails `json:"accessKeyDetails,omitempty"` } // InstanceDetails set guardduty value type InstanceDetails struct { InstanceID string `json:"instanceId,omitempty"` InstanceType string `json:"instanceType,"` } // AccessKeyDetails set guardduty AccessKeyDetailsValue type AccessKeyDetails struct { UserName string `json:"userName,omitempty"` } func main() { lambda.Start(Handler) } // Handler get value from cloudwatch event func Handler(event events.CloudWatchEvent) (events.CloudWatchEvent, error) { var resource string postFlag := true gd := &GuardDutyFindings{} err := json.Unmarshal([]byte(event.Detail), gd) if err != nil { fmt.Println(err) } // cast to float64 float64Severity, err := gd.Severity.Float64() slackColor := CheckSeverityLevel(float64Severity) float64NotPostThreshold, err := strconv.ParseFloat(notPostThreshold, 64) // get aws account name accountAliasName := FetchAccountAlias() // Check excluded List for i := range excludeAccountList { if strings.Contains(accountAliasName, excludeAccountList[i]) { postFlag = false } } // Set affected resource if gd.Resource.InstanceDetails.InstanceID != "" { resource = gd.Resource.InstanceDetails.InstanceID } else if gd.Resource.AccessKeyDetails.UserName != "" { resource = gd.Resource.AccessKeyDetails.UserName } else { resource = "unknown" } // Post slack if postFlag == false && float64Severity < float64NotPostThreshold { fmt.Println("Do not post slack") } else { PostSlack(slackColor, gd.Title, accountAliasName, string(gd.Severity), resource, gd.Type, gd.Description) } return event, err } // CheckSeverityLevel fix the color func CheckSeverityLevel(severity float64) string { var color string if severity == 0.0 { color = "good" } else if (0.1 <= severity) && (severity <= 3.9) { color = "#0000ff" } else if (4.0 <= severity) && (severity <= 6.9) { color = "warning" } else { color = "danger" } return color } // FetchAccountAlias get account alias name func FetchAccountAlias() string { var accountAlias string params := &iam.ListAccountAliasesInput{} res, err := svcIAM.ListAccountAliases(params) if err != nil { fmt.Println(err) } if res.AccountAliases == nil { accountAlias = "None" } else { accountAlias = *res.AccountAliases[0] } return accountAlias } // PostSlack post slack result func PostSlack(slackColor string, title string, accountAliasName string, severity string, resource string, reason string, description string) { field0 := slack.Field{Title: "Title", Value: "_" + title + "_"} field1 := slack.Field{Title: "Account", Value: accountAliasName} field2 := slack.Field{Title: "Severity", Value: severity} field3 := slack.Field{Title: "Affected Resource", Value: resource} field4 := slack.Field{Title: "Type", Value: reason} field5 := slack.Field{Title: "Description", Value: "```" + description + "```"} attachment := slack.Attachment{} attachment.AddField(field0).AddField(field1).AddField(field2).AddField(field3).AddField(field4).AddField(field5) color := slackColor attachment.Color = &color payload := slack.Payload{ Username: USERNAME, Attachments: []slack.Attachment{attachment}, } err := slack.Send(SLACKURL, "", payload) if err != nil { os.Exit(1) } }
ServerlessFrameworkを利用したデプロイ
Lambda (Go) + IAMロール + CloudwatchEventをseverlessFrameworkを利用してデプロイしました。
■ デプロイコマンド
- Goコンパイル - $ make build - ServerlessFrameworkでデプロイ - $ sls deploy --aws-profile <PROFILE> --slackurl <SLACKURL>
export GO111MODULE=on ## Install dependencies .PHONY: deps deps: go get -v -d ## Setup development .PHONY: deps devel-deps: deps GO111MODULE=off go get -u golang.org/x/lint/golint go get -u github.com/motemen/gobump/cmd/gobump go get -u github.com/Songmu/make2help/cmd/make2help ## Setup build .PHONY: pre-build build-deps: go get -u github.com/mitchellh/gox ## Build binaries .PHONY: build build: build-deps rm -rf ./main gox -os=linux -arch=amd64 -output=./main -ldflags "-s -w" gobump show ## Lint .PHONY: lint lint: devel-deps go vet ./... golint -set_exit_status ./... ## Show help .PHONY: help help: @make2help $(MAKEFILE_LIST)
$ make help
build: Build binaries build-deps: Setup build deps: Install dependencies devel-deps: Setup development help: Show help lint: Lint
- serverless.yml
service: guardduby frameworkVersion: ">=1.48.0" provider: name: aws stage: dev runtime: go1.x region: ap-northeast-1 functions: slack: handler: main role: GuardDutyLambda timeout: 30 description: GuardDutyLambda slack notice memorySize: 128 environment: SLACKURL: ${opt:slackurl} THRESHOLD: 2.0 events: - cloudwatchEvent: event: source: - 'aws.guardduty' detail-type: - 'GuardDuty Finding' resources: Resources: GuardDutyLambda: Type: AWS::IAM::Role Properties: RoleName: GuardDutyLambda AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/IAMReadOnlyAccess Policies: - PolicyName: GuardDutyLambda PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "*"
まとめ
AWS GuardDutyのアラート通知を CloudwatchEvent + Lambda (Go)を使ってSlackに通知する方法を記載しました。 デプロイについては、ServerlessFrameworkを利用しています。
GuardDutyの通知は、最近サービスリリースされた ChatBotが対応しているので利用してみてもいいかもしれません。 jp.techcrunch.com