{ "AWSTemplateFormatVersion": "2010-09-09", "Description": "Demonstrates how to connect GuardDuty to your Slack channel. The template Installs a Lambda function that writes CW Events to a Slack incoming web hook. This relies on you creating an *incoming web hook* in your slack account and simply passing the URL as a parameter to this template", "Metadata": { "AWS::CloudFormation::Interface": { "ParameterGroups": [ { "Label": { "default": "Slack Configuration" }, "Parameters": [ "IncomingWebHookURL", "SlackChannel", "MinSeverityLevel" ] } ], "ParameterLabels": { "IncomingWebHookURL": { "default": "Slack Incoming Web Hook URL" }, "SlackChannel" : { "default" : "Slack channel to send findings to" }, "MinSeverityLevel" : { "default" : "Minimum severity level (LOW, MEDIUM, HIGH)" } } } }, "Parameters": { "IncomingWebHookURL": { "Default": "https://hooks.slack.com/services/XXXXXX/YYYYY/REPLACE_WITH_YOURS", "Description": "Your unique Incoming Web Hook URL from slack service", "Type": "String" }, "SlackChannel": { "Default": "#general", "Description": "The slack channel to send findings to", "Type": "String" }, "MinSeverityLevel": { "Default": "LOW", "Description": "The minimum findings severity to send to your slack channel (LOW, MEDIUM or HIGH)", "Type": "String", "AllowedValues": [ "LOW", "MEDIUM", "HIGH" ] } }, "Resources": { "GuardDutyToSlackRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Statement": [ { "Effect": "Allow", "Principal": { "Service": [ "lambda.amazonaws.com" ] }, "Action": [ "sts:AssumeRole" ] } ] }, "Path": "/service-role/", "ManagedPolicyArns": [ "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess", "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ], "Policies": [] } }, "ScheduledRule": { "DependsOn": "findingsToSlack", "Type": "AWS::Events::Rule", "Properties": { "Description": "GuardDutyRule", "State": "ENABLED", "EventPattern" : { "source": ["aws.guardduty"], "detail-type": ["GuardDuty Finding"] }, "Targets": [{ "Arn": { "Fn::GetAtt" : ["findingsToSlack", "Arn"] }, "Id": "GuardDutyFunction" }] } }, "LambdaInvokePermission": { "DependsOn": ["findingsToSlack","ScheduledRule"], "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", "Principal": "events.amazonaws.com", "FunctionName": { "Fn::GetAtt" : ["findingsToSlack", "Arn"] }, "SourceArn": { "Fn::GetAtt": ["ScheduledRule", "Arn"]} } }, "findingsToSlack": { "Type": "AWS::Lambda::Function", "Properties": { "Handler": "index.handler", "Role": {"Fn::GetAtt" : ["GuardDutyToSlackRole", "Arn"] }, "Code": { "ZipFile" : { "Fn::Join": [ "", [ "'use strict';\n", "\n", "/**\n", " * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.\n", " * SPDX-License-Identifier: MIT-0\n", " */\n", "const AWS = require('aws-sdk');\n", "const url = require('url');\n", "const https = require('https');\n", "\n", "const webHookUrl = process.env['webHookUrl'];\n", "const slackChannel = process.env.slackChannel;\n", "const minSeverityLevel = process.env['minSeverityLevel'];\n", "\n", "function postMessage(message, callback) {\n", " const body = JSON.stringify(message);\n", " const options = url.parse(webHookUrl);\n", " options.method = 'POST';\n", " options.headers = {\n", " 'Content-Type': 'application/json',\n", " 'Content-Length': Buffer.byteLength(body),\n", " };\n", "\n", " const postReq = https.request(options, (res) => {\n", " const chunks = [];\n", " res.setEncoding('utf8');\n", " res.on('data', (chunk) => chunks.push(chunk));\n", " res.on('end', () => {\n", " if (callback) {\n", " callback({\n", " body: chunks.join(''),\n", " statusCode: res.statusCode,\n", " statusMessage: res.statusMessage,\n", " });\n", " }\n", " });\n", " return res;\n", " });\n", "\n", " postReq.write(body);\n", " postReq.end();\n", "}\n", "\n", "function processEvent(event, callback) {\n", " const message = event;\n", " const consoleUrl = `https://console.aws.amazon.com/guardduty`;\n", " const finding = message.detail.type;\n", " const findingDescription = message.detail.description;\n", " const findingTime = message.detail.updatedAt;\n", " const findingTimeEpoch = Math.floor(new Date(findingTime) / 1000);\n", " const account = message.detail.accountId;\n", " const region = message.region;\n", " const messageId = message.detail.id;\n", " const lastSeen = `<!date^${findingTimeEpoch}^{date} at {time} | ${findingTime}>`;\n", " var color = '#7CD197';\n", " var severity = '';\n", " var skip = false;\n", "\n", " if (message.detail.severity < 4.0) {\n", " if (minSeverityLevel !== 'LOW') {\n", " skip = true;\n", " }\n", " severity = 'Low';\n", " color = '#e2d43b';\n", " } else if (message.detail.severity < 7.0) {\n", " if (minSeverityLevel !== 'LOW' && minSeverityLevel !== 'MEDIUM') {\n", " skip = true;\n", " }\n", " severity = 'Medium';\n", " color = '#ff8c00';\n", " } else if (message.detail.severity >= 7.0) {\n", " severity = 'High';\n", " color = '#ad0614';\n", " } else {\n", " skip = true;\n", " }\n", "\n", " const attachment = [{\n", " \"fallback\": finding + ` - ${consoleUrl}/home?region=` +\n", " `${region}#/findings?search=id%3D${messageId}`,\n", " \"pretext\": `*Finding in ${region} for Acct: ${account}*`,\n", " \"title\": `${finding}`,\n", " \"title_link\": `${consoleUrl}/home?region=${region}#/findings?search=id%3D${messageId}`,\n", " \"text\": `${findingDescription}`,\n", " \"fields\": [\n", " {\"title\": \"Severity\",\"value\": `${severity}`, \"short\": true},\n", " {\"title\": \"Region\",\"value\": `${region}`,\"short\": true},\n", " {\"title\": \"Last Seen\",\"value\": `${lastSeen}`, \"short\": true}\n", " ],\n", " \"mrkdwn_in\": [\"pretext\"],\n", " \"color\": color\n", " }];\n", "\n", " const slackMessage = {\n", " channel: slackChannel,\n", " text : '',\n", " attachments : attachment,\n", " username: 'GuardDuty',\n", " 'mrkdwn': true,\n", " icon_url: 'https://raw.githubusercontent.com/aws-samples/amazon-guardduty-to-slack/master/images/gd_logo.png'\n", " };\n", "\n", " if (!skip) {\n", " postMessage(slackMessage, (response) => {\n", " if (response.statusCode < 400) {\n", " console.info('Message posted successfully');\n", " callback(null);\n", " } else if (response.statusCode < 500) {\n", " console.error(`Error posting message to Slack API: ${response.statusCode} - ${response.statusMessage}`);\n", " callback(null);\n", " } else {\n", " callback(`Server error when processing message: ${response.statusCode} - ${response.statusMessage}`);\n", " }\n", " });\n", " }\n", "}\n", "\n", "exports.handler = (event, context, callback) => {\n", " processEvent(event, callback);\n", "};\n"]] }}, "Environment" : { "Variables" : { "slackChannel" : { "Ref" : "SlackChannel" }, "webHookUrl" : { "Ref": "IncomingWebHookURL" }, "minSeverityLevel" : {"Ref" : "MinSeverityLevel"} } }, "Runtime": "nodejs16.x", "MemorySize" : "128", "Timeout": "10", "Description" : "Lambda to push GuardDuty findings to slack", "TracingConfig": { "Mode": "Active" } } } } }