├── .gitignore ├── Makefile ├── package.json ├── README.md ├── index.js └── AWSlack.template.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | code.zip 3 | .vscode/ 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: deploy-version 2 | 3 | install-dep: 4 | npm install 5 | 6 | zip-code: install-dep 7 | zip -r code.zip index.js package.json node_modules 8 | 9 | deploy-version: zip-code 10 | aws s3 cp AWSlack.template.json s3://awslack-v2/source/ --acl public-read 11 | aws s3 cp code.zip s3://awslack-v2/source/ --acl public-read 12 | 13 | clean: 14 | rm -f code.zip 15 | rm -rf node_modules -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gcp-alert-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/doitintl/gcp-alert-service.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/doitintl/gcp-alert-service/issues" 17 | }, 18 | "homepage": "https://github.com/doitintl/gcp-alert-service#readme", 19 | "dependencies": { 20 | "aws-sdk": "~2.80.0", 21 | "https": "^1.0.0", 22 | "slack-node": "~0.1.8", 23 | "url": "^0.11.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWSlack 2 | 3 | ## Prerequisites 4 | - A valid `AWS` Account. 5 | - Any `AWS` supported browser. ( Chrome / Firefox / Safari etc. ) 6 | 7 | ## Generate Slack API Token 8 | - Go to https://.slack.com/apps/new/A0F7YS25R-bots 9 | - Enter a name for the bot to post with. (i.e. @aws) 10 | - Click `Add bot integration`. 11 | - Wait until the UI displays the `API Token` and copy the string (i.e. xxxx-yyyyyyyyyyyy-zzzzzzzzzzzzzzzzzzzzzzzz). 12 | - Keep this token for using in the next step. 13 | - Don't forget to invite your new bot to a channel by `@` mentioning it. 14 | 15 | ## Deployment 16 | - Hold down the `Ctrl` button and Click the `Launch Stack` button to deploy the stack into your account: 17 | 18 | 19 | - Paste your `API Token` in the `SlackAPIToken` parameter. 20 | - The bot will publish messages to the channel in the `DefaultSlackChannel` parameter. The default is `aws`. 21 | - Click `Next` and Confirm all steps until the stack deploys. 22 | 23 | ## Configure 24 | - Open your browser at [AWS DynamoDB Console](https://console.aws.amazon.com/dynamodb/home) 25 | - Switch to the `Tables` tab. 26 | - Select the `awslack.tests` table and go to the `Items` tab. 27 | - You can add new tests or change/delete existing tests. 28 | - You can change the `slackChannel` attribute of each test to another Slack channel. 29 | - Select the `awslack.configs` table and go to the `Items` tab. 30 | - You can edit the slackAPIToken and set the `value` to another slack API token. 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | function runTest(test, data) { 2 | try { 3 | $ = data; 4 | return eval(`'use strict';(${test});`); 5 | } 6 | catch (err) { 7 | console.log(`Rule test error in '${test}': ${err}`); 8 | return false; 9 | } 10 | } 11 | 12 | function evalMessage(message, data) { 13 | try { 14 | $ = data; 15 | return eval(`'use strict';\`${message}\`;`); 16 | } 17 | catch (err) { 18 | console.log(`Rule message error in '${message}': ${err}`); 19 | return ""; 20 | } 21 | } 22 | 23 | const AWS = require('aws-sdk'); 24 | const Slack = require('slack-node'); 25 | const dynamodb = new AWS.DynamoDB(); 26 | 27 | exports.handleEvent = function (event, context, callback) { 28 | console.log(`event data: ${JSON.stringify(event)}`); 29 | Promise.all([ 30 | getConfig(), 31 | getTests() 32 | ]) 33 | .then(([config, tests]) => { 34 | return Promise.all(tests.map(test => { 35 | let clonedData = JSON.parse(JSON.stringify(event)); 36 | if (runTest(test.test, clonedData)) { 37 | let message = evalMessage(test.message, clonedData); 38 | return sendSlack(test.slackChannel, message, config.slackAPIToken); 39 | } 40 | else { 41 | return Promise.resolve(); 42 | } 43 | })); 44 | }) 45 | .then(() => { 46 | callback(null, {}); 47 | }) 48 | .catch(err => { 49 | callback(err); 50 | }); 51 | }; 52 | 53 | function getConfig() { 54 | return readDynamo("awslack.configs").then(data => { 55 | return data.reduce((configs, config) => { 56 | configs[config.name.S] = config.value.S; 57 | return configs; 58 | }, {}); 59 | }); 60 | } 61 | 62 | function getTests() { 63 | return readDynamo("awslack.tests").then(data => { 64 | return data.map(test => ({ 65 | test: test.test.S, 66 | message: test.message.S, 67 | slackChannel: test.slackChannel.S 68 | })); 69 | }); 70 | } 71 | 72 | function readDynamo(tableName) { 73 | return new Promise((resolve, reject) => { 74 | try { 75 | dynamodb.scan({ 76 | TableName: tableName 77 | }, function (err, data) { 78 | if (!!err) { 79 | reject(err); 80 | } 81 | else { 82 | resolve(data.Items); 83 | } 84 | }); 85 | } 86 | catch (e) { 87 | reject(e); 88 | } 89 | }); 90 | } 91 | 92 | function writeDynamo(tableName, item) { 93 | return new Promise((resolve, reject) => { 94 | try { 95 | dynamodb.putItem({ 96 | TableName: tableName, 97 | Item: item 98 | }, function (err, data) { 99 | if (!!err) { 100 | reject(err); 101 | } 102 | else { 103 | resolve(data); 104 | } 105 | }); 106 | } 107 | catch (e) { 108 | reject(e); 109 | } 110 | }); 111 | } 112 | 113 | function sendSlack(channel, message, apiToken) { 114 | return new Promise((resolve, reject) => { 115 | const slack = new Slack(apiToken); 116 | slack.api('chat.postMessage', { 117 | text: message, 118 | channel: channel, 119 | as_user : true 120 | }, function (err, response) { 121 | if (!!err) { 122 | reject(err); 123 | } 124 | else { 125 | resolve(response); 126 | } 127 | }); 128 | }); 129 | } 130 | 131 | exports.initDynamoDB = function (event, context, callback) { 132 | const slackAPIToken = event.ResourceProperties.SlackAPIToken; 133 | const defaultSlackChannel = event.ResourceProperties.DefaultSlackChannel; 134 | 135 | Promise.all([ 136 | writeDynamo("awslack.configs", { name: { S: "slackAPIToken" }, value: { S: slackAPIToken } }), 137 | writeDynamo("awslack.tests", { name: { S: "bucket_create" }, test: { S: "$.source==='aws.s3' && $.detail.eventName==='CreateBucket'" }, message: { S: "Bucket ${$.detail.requestParameters.bucketName} created in region ${$.region} by ${$.detail.userIdentity.arn}" }, slackChannel: { S: defaultSlackChannel } }), 138 | writeDynamo("awslack.tests", { name: { S: "bucket_delete" }, test: { S: "$.source==='aws.s3' && $.detail.eventName==='DeleteBucket'" }, message: { S: "Bucket ${$.detail.requestParameters.bucketName} deleted in region ${$.region} by ${$.detail.userIdentity.arn}" }, slackChannel: { S: defaultSlackChannel } }), 139 | writeDynamo("awslack.tests", { name: { S: "lambda_update" }, test: { S: "$.source==='aws.lambda' && $.detail.eventName.includes('UpdateFunctionCode')" }, message: { S: "The Lambda function ${$.detail.requestParameters.functionName} was updated by ${$.detail.userIdentity.arn}" }, slackChannel: { S: defaultSlackChannel } }), 140 | writeDynamo("awslack.tests", { name: { S: "ec2_start" }, test: { S: "$.source==='aws.ec2' && $['detail-type']==='EC2 Instance State-change Notification' && $.detail.state==='running'" }, message: { S: "EC2 instance ${$.detail['instance-id']} started in region ${$.region}" }, slackChannel: { S: defaultSlackChannel } }), 141 | writeDynamo("awslack.tests", { name: { S: "autoscale" }, test: { S:"$.source==='aws.autoscaling' && ( $['detail-type']==='EC2 Instance Terminate Successful' || $['detail-type']==='EC2 Instance Launch Successful')" }, message: { S: "Autoscaling event of type ${$['detail-type']} on group ${$.detail.AutoScalingGroupName} in region ${$.region} - Cause: ${$.detail.Cause}" }, slackChannel: { S: defaultSlackChannel } }), 142 | writeDynamo("awslack.tests", { name: { S: "health" }, test: { S:"$.source==='aws.health'" }, message: { S: "Health event ${$.detail.eventTypeCode} in region ${$.region}" }, slackChannel: { S: defaultSlackChannel } }), 143 | writeDynamo("awslack.tests", { name: { S: "signin" }, test: { S:"$.source==='aws.signin'" }, message: { S: "Sign-in event by ${$.detail.userIdentity.arn} from ${$.detail.sourceIPAddress} in region ${$.region} at ${$.detail.eventTime} with UserAgent: ${$.detail.userAgent}" }, slackChannel: { S: defaultSlackChannel } }) 144 | ]) 145 | .then(() => { 146 | sendResponse(event, context, "SUCCESS", {}); 147 | callback(null, {}); 148 | }) 149 | .catch(err => { 150 | sendResponse(event, context, "FAILED", err); 151 | callback(err); 152 | }); 153 | }; 154 | 155 | function sendResponse(event, context, responseStatus, responseData) { 156 | var responseBody = JSON.stringify({ 157 | Status: responseStatus, 158 | Reason: JSON.stringify(responseData), 159 | PhysicalResourceId: context.logStreamName, 160 | StackId: event.StackId, 161 | RequestId: event.RequestId, 162 | LogicalResourceId: event.LogicalResourceId, 163 | Data: responseData 164 | }); 165 | 166 | console.log("RESPONSE BODY:\n", responseBody); 167 | 168 | var https = require("https"); 169 | var url = require("url"); 170 | 171 | var parsedUrl = url.parse(event.ResponseURL); 172 | var options = { 173 | hostname: parsedUrl.hostname, 174 | port: 443, 175 | path: parsedUrl.path, 176 | method: "PUT", 177 | headers: { 178 | "content-type": "", 179 | "content-length": responseBody.length 180 | } 181 | }; 182 | 183 | console.log("SENDING RESPONSE...\n"); 184 | 185 | var request = https.request(options, function (response) { 186 | console.log("STATUS: " + response.statusCode); 187 | console.log("HEADERS: " + JSON.stringify(response.headers)); 188 | // Tell AWS Lambda that the function execution is done 189 | context.done(); 190 | }); 191 | 192 | request.on("error", function (error) { 193 | console.log("sendResponse Error:" + error); 194 | // Tell AWS Lambda that the function execution is done 195 | context.done(); 196 | }); 197 | 198 | // write data to request body 199 | request.write(responseBody); 200 | request.end(); 201 | } -------------------------------------------------------------------------------- /AWSlack.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "AWSlack AWS CloudFormation Template: This template deploys Slack integration into your account Please see https://github.com/doitintl/AWSlack more information. **WARNING** You will be billed for the AWS resources used if you create a stack from this template.", 4 | "Parameters": { 5 | "SlackAPIToken": { 6 | "Type": "String", 7 | "Description": "Slack Bot API Token", 8 | "MinLength": 42, 9 | "MaxLength": 55, 10 | "AllowedPattern": "^[a-z]{4}(?:-\\d{11,12})?-\\d{12}-[a-zA-Z0-9]{24}$", 11 | "NoEcho": true 12 | }, 13 | "DefaultSlackChannel": { 14 | "Type": "String", 15 | "Description": "The bot will publish messages to this channel. You may publish different events to different channels by editing the 'awslack.tests' DynamoDB table for each 'test' Item", 16 | "Default": "aws", 17 | "MinLength": 1, 18 | "MaxLength": 21 19 | } 20 | }, 21 | "Resources": { 22 | "EventRule": { 23 | "Type": "AWS::Events::Rule", 24 | "Properties": { 25 | "Description": "EventRule", 26 | "EventPattern": { 27 | "account": [ 28 | { 29 | "Ref": "AWS::AccountId" 30 | } 31 | ] 32 | }, 33 | "State": "ENABLED", 34 | "Targets": [ 35 | { 36 | "Arn": { 37 | "Fn::GetAtt": [ 38 | "EventHandler", 39 | "Arn" 40 | ] 41 | }, 42 | "Id": "TargetFunctionV1" 43 | } 44 | ] 45 | } 46 | }, 47 | "PermissionForEventsToInvokeLambda": { 48 | "Type": "AWS::Lambda::Permission", 49 | "Properties": { 50 | "FunctionName": { 51 | "Ref": "EventHandler" 52 | }, 53 | "Action": "lambda:InvokeFunction", 54 | "Principal": "events.amazonaws.com", 55 | "SourceArn": { 56 | "Fn::GetAtt": [ 57 | "EventRule", 58 | "Arn" 59 | ] 60 | } 61 | } 62 | }, 63 | "LambdaExecutionRole": { 64 | "Type": "AWS::IAM::Role", 65 | "Properties": { 66 | "AssumeRolePolicyDocument": { 67 | "Version": "2012-10-17", 68 | "Statement": [ 69 | { 70 | "Effect": "Allow", 71 | "Principal": { 72 | "Service": [ 73 | "lambda.amazonaws.com" 74 | ] 75 | }, 76 | "Action": [ 77 | "sts:AssumeRole" 78 | ] 79 | } 80 | ] 81 | }, 82 | "Path": "/", 83 | "Policies": [ 84 | { 85 | "PolicyName": "root", 86 | "PolicyDocument": { 87 | "Version": "2012-10-17", 88 | "Statement": [ 89 | { 90 | "Effect": "Allow", 91 | "Action": [ 92 | "logs:*" 93 | ], 94 | "Resource": "arn:aws:logs:*:*:*" 95 | }, 96 | { 97 | "Effect": "Allow", 98 | "Action": [ 99 | "dynamodb:*" 100 | ], 101 | "Resource": [ 102 | "arn:aws:dynamodb:*:*:table/awslack.configs", 103 | "arn:aws:dynamodb:*:*:table/awslack.tests" 104 | ] 105 | } 106 | ] 107 | } 108 | } 109 | ] 110 | } 111 | }, 112 | "EventHandler": { 113 | "Type": "AWS::Lambda::Function", 114 | "Properties": { 115 | "Code": { 116 | "S3Bucket": "awslack-v2", 117 | "S3Key": "source/code.zip" 118 | }, 119 | "Description": "Handles events by sending Slack notifications.", 120 | "FunctionName": "awslack", 121 | "Handler": "index.handleEvent", 122 | "Environment": { 123 | "Variables": { 124 | "key": "value" 125 | } 126 | }, 127 | "Role": { 128 | "Fn::GetAtt": [ 129 | "LambdaExecutionRole", 130 | "Arn" 131 | ] 132 | }, 133 | "Runtime": "nodejs6.10" 134 | } 135 | }, 136 | "ConfigTable": { 137 | "Type": "AWS::DynamoDB::Table", 138 | "Properties": { 139 | "AttributeDefinitions": [ 140 | { 141 | "AttributeName": "name", 142 | "AttributeType": "S" 143 | } 144 | ], 145 | "KeySchema": [ 146 | { 147 | "AttributeName": "name", 148 | "KeyType": "HASH" 149 | } 150 | ], 151 | "ProvisionedThroughput": { 152 | "ReadCapacityUnits": 10, 153 | "WriteCapacityUnits": 1 154 | }, 155 | "TableName": "awslack.configs" 156 | } 157 | }, 158 | "TestTable": { 159 | "Type": "AWS::DynamoDB::Table", 160 | "Properties": { 161 | "AttributeDefinitions": [ 162 | { 163 | "AttributeName": "name", 164 | "AttributeType": "S" 165 | } 166 | ], 167 | "KeySchema": [ 168 | { 169 | "AttributeName": "name", 170 | "KeyType": "HASH" 171 | } 172 | ], 173 | "ProvisionedThroughput": { 174 | "ReadCapacityUnits": 10, 175 | "WriteCapacityUnits": 1 176 | }, 177 | "TableName": "awslack.tests" 178 | } 179 | }, 180 | "InitializeDynamoDBFunction": { 181 | "Type": "AWS::Lambda::Function", 182 | "Properties": { 183 | "Code": { 184 | "S3Bucket": "awslack-v2", 185 | "S3Key": "source/code.zip" 186 | }, 187 | "Description": "Initializes DynamoDB", 188 | "FunctionName": "awslack-init-db", 189 | "Handler": "index.initDynamoDB", 190 | "Role": { 191 | "Fn::GetAtt": [ 192 | "LambdaExecutionRole", 193 | "Arn" 194 | ] 195 | }, 196 | "Runtime": "nodejs6.10" 197 | } 198 | }, 199 | "TriggerDynamoDBInitialize": { 200 | "Type": "Custom::TriggerDynamoDBInitialize", 201 | "DependsOn": [ 202 | "ConfigTable", 203 | "TestTable", 204 | "InitializeDynamoDBFunction" 205 | ], 206 | "Properties": { 207 | "ServiceToken": { 208 | "Fn::GetAtt": [ 209 | "InitializeDynamoDBFunction", 210 | "Arn" 211 | ] 212 | }, 213 | "SlackAPIToken": { 214 | "Ref": "SlackAPIToken" 215 | }, 216 | "DefaultSlackChannel": { 217 | "Ref": "DefaultSlackChannel" 218 | } 219 | } 220 | } 221 | } 222 | } --------------------------------------------------------------------------------