├── .gitignore ├── .travis.yml.disabled ├── LICENSE ├── README.md ├── config.js ├── env.example ├── images ├── create_access_key.png └── delete_access_key.png ├── index.js ├── package.json └── test ├── all.sh ├── context.json ├── ec2.lambda.json ├── elasticloadbalancing.register.json ├── event.json ├── iam.CreateAccessKey.json └── iam.DeleteAccessKey.json /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .env 3 | .sublime-project* 4 | npm-debug.log 5 | 6 | build/ 7 | node_modules/ 8 | -------------------------------------------------------------------------------- /.travis.yml.disabled: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | 4 | node_js: 5 | - 0.10 6 | - 0.12 7 | - 4 8 | - 5 9 | - 6 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 KangarooBox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambda-cloudwatch-event-msteams 2 | 3 | An [AWS Lambda](http://aws.amazon.com/lambda/) function for better MS Teams notifications generated from CloudWatch 4 | events. This work is a direct descendant of [lambda-cloudwatch-slack](https://github.com/assertible/lambda-cloudwatch-slack) 5 | and wouldn't be possible without it. 6 | 7 | [![BuildStatus](https://travis-ci.org/KangarooBox/lambda-cloudwatch-msteams.png?branch=master)](https://travis-ci.org/assertible/lambda-cloudwatch-msteams) 8 | [![NPM version](https://badge.fury.io/js/lambda-cloudwatch-msteams.png)](http://badge.fury.io/js/lambda-cloudwatch-msteams) 9 | 10 | 11 | ## Overview 12 | 13 | This function was originally derived from the 14 | [lambda-cloudwatch-slack](https://github.com/assertible/lambda-cloudwatch-slack) project which was originally derived 15 | from the [AWS blueprint named `cloudwatch-alarm-to-msteams`](https://aws.amazon.com/blogs/aws/new-msteams-integration-blueprints-for-aws-lambda/). 16 | The function in this repo allows CloudWatch Events to generate MS Teams notifications. 17 | 18 | **Show important activities** 19 | ![Deleting an ACCESS KEY](images/delete_access_key.png) 20 | 21 | **Show dangerous activities** 22 | ![Creating an ACCESS KEY](images/create_access_key.png) 23 | 24 | ## Configuration 25 | 26 | Clone this repository then follow the steps below: 27 | 28 | 1. Open your MS Teams client and choose a channel to receive your notifications 29 | 1. Go to the options screen for that channel and choose "Connectors" 30 | 1. Select the "Incoming Webhook" connector, fill in the options and hit the "Create" button 31 | 1. Copy the URL to your clipboard and save the connector 32 | 1. Copy the ``env.example`` file to ``.env`` 33 | 1. Open the ``.env`` file in your editor and update the values inside 34 | * The WEBHOOK_URL is the URL you copied to your clipboard in an earlier step 35 | * The COLOR values can be changed as you see fit 36 | 37 | ## Tests 38 | 39 | With the variables filled in, you can test the function: 40 | 41 | ``` 42 | npm install 43 | npm test 44 | ``` 45 | 46 | ## License 47 | 48 | MIT License 49 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // webhook url 4 | WEBHOOK_URL: process.env.WEBHOOK_URL, 5 | 6 | // theme colors 7 | COLOR_DANGER: process.env.COLOR_DANGER, 8 | COLOR_WARNING: process.env.COLOR_WARNING, 9 | COLOR_OK: process.env.COLOR_OK, 10 | 11 | } 12 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | AWS_ENVIRONMENT=development 2 | AWS_ACCESS_KEY_ID=your_key 3 | AWS_SECRET_ACCESS_KEY=your_secret 4 | AWS_PROFILE= 5 | AWS_SESSION_TOKEN= 6 | AWS_ROLE_ARN=your_amazon_role 7 | AWS_REGION=us-east-1 8 | AWS_FUNCTION_NAME= 9 | AWS_HANDLER=index.handler 10 | AWS_MEMORY_SIZE=128 11 | AWS_TIMEOUT=3 12 | AWS_DESCRIPTION= 13 | AWS_RUNTIME=nodejs4.3 14 | AWS_VPC_SUBNETS= 15 | AWS_VPC_SECURITY_GROUPS= 16 | EXCLUDE_GLOBS="event.json" 17 | PACKAGE_DIRECTORY=build 18 | 19 | WEBHOOK_URL=https://outlook.office.com/webhook/SOMEGUID@ANOTHERGUID/IncomingWebhook/YETANOTHERGUID/ONEMOREGUID 20 | COLOR_DANGER="#990000" 21 | COLOR_WARNING="#FFFF00" 22 | COLOR_OK="#1569C7" 23 | -------------------------------------------------------------------------------- /images/create_access_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KangarooBox/lambda-cloudwatch-event-msteams/bc4da8583614fbc6eb356e6aa454055bd0bb9b74/images/create_access_key.png -------------------------------------------------------------------------------- /images/delete_access_key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KangarooBox/lambda-cloudwatch-event-msteams/bc4da8583614fbc6eb356e6aa454055bd0bb9b74/images/delete_access_key.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var AWS = require('aws-sdk'); 2 | var url = require('url'); 3 | var https = require('https'); 4 | var config = require('./config'); 5 | 6 | const WEBHOOK_URL = config.WEBHOOK_URL; 7 | const COLOR_DANGER = config.COLOR_DANGER; 8 | const COLOR_WARNING = config.COLOR_WARNING; 9 | const COLOR_OK = config.COLOR_OK; 10 | 11 | // Post the message to the chat URL 12 | var postMessage = function(message, callback) { 13 | var body = JSON.stringify(message); 14 | var options = url.parse(WEBHOOK_URL); 15 | options.method = 'POST'; 16 | options.headers = { 17 | 'Content-Type': 'application/json', 18 | 'Content-Length': Buffer.byteLength(body), 19 | }; 20 | 21 | var postReq = https.request(options, function(res) { 22 | var chunks = []; 23 | res.setEncoding('utf8'); 24 | res.on('data', function(chunk) { 25 | return chunks.push(chunk); 26 | }); 27 | res.on('end', function() { 28 | var body = chunks.join(''); 29 | if (callback) { 30 | callback({ 31 | body: body, 32 | statusCode: res.statusCode, 33 | statusMessage: res.statusMessage 34 | }); 35 | } 36 | }); 37 | return res; 38 | }); 39 | 40 | postReq.write(body); 41 | postReq.end(); 42 | }; 43 | 44 | // Format the message for IAM events 45 | var handleIAM = function(event, context) { 46 | var subject = "AWS IAM Notification"; 47 | var detail = event.detail; 48 | var message = { 'summary': subject, 'sections': [] }; 49 | 50 | try { 51 | message['title'] = `${subject} - account ${event.account}`; 52 | 53 | // Decode the important details of the event... 54 | var event_details = { 'facts':[] }; 55 | switch(detail.eventName.split(/(?=[A-Z])/)[0]){ 56 | case "Create": 57 | message['themeColor'] = COLOR_DANGER; 58 | event_details['facts'].push( { 'name': 'Event Name', 'value': detail.eventName }); 59 | event_details['facts'].push( { 'name': 'Actor', 'value': `${detail.userIdentity.userName} (${detail.userIdentity.type})` }); 60 | event_details['facts'].push( { 'name': 'Affected User', 'value': detail.requestParameters.userName }); 61 | break; 62 | 63 | case "Delete": 64 | message['themeColor'] = COLOR_OK; 65 | event_details['facts'].push( { 'name': 'Event Name', 'value': detail.eventName }); 66 | event_details['facts'].push( { 'name': 'Actor', 'value': `${detail.userIdentity.userName} (${detail.userIdentity.type})` }); 67 | event_details['facts'].push( { 'name': 'Affected User', 'value': detail.requestParameters.userName }); 68 | break; 69 | 70 | case "Start": 71 | message['themeColor'] = COLOR_OK; 72 | event_details['facts'].push( { 'name': 'Event Name', 'value': detail.eventName } ); 73 | event_details['facts'].push( { 'name': 'Actor', 'value': `${detail.userIdentity.sessionContext.sessionIssuer.userName} (${detail.userIdentity.type})`} ); 74 | break; 75 | 76 | default: 77 | message['themeColor'] = COLOR_WARNING; 78 | break; 79 | } 80 | 81 | // Add in some common facts... 82 | event_details['facts'].push( { 'name': 'Event ID', 'value': `[${detail.eventID}](https://console.aws.amazon.com/cloudtrail/home?region=${event.region}#/events?EventId=${detail.eventID})` }); 83 | event_details['facts'].push( { 'name': 'Region', 'value': detail.awsRegion }); 84 | 85 | message['sections'].push(event_details); 86 | } catch(e) { 87 | message = processError(e, event); 88 | } 89 | 90 | return message; 91 | }; 92 | 93 | // Build a suitable error message 94 | var processError = function(e, event){ 95 | var message = { 'summary': 'Error processing event', 'sections': [] }; 96 | message['title'] = message['summary']; 97 | message['themeColor'] = COLOR_DANGER; 98 | 99 | var error_details = {}; 100 | error_details['title'] = "Error Details"; 101 | error_details['facts'] = [ 102 | { 'name': 'NodeJS', 'value': `> "${e}"` }, 103 | { 'name': 'Event', 'value': `> ${JSON.stringify(event)}` } 104 | ]; 105 | message['sections'].push(error_details); 106 | 107 | return message; 108 | }; 109 | 110 | // Main handler 111 | exports.handler = function(event, context, callback) { 112 | // console.log("sns received:" + JSON.stringify(event, null, 2)); 113 | var message = null; 114 | 115 | switch(event.source) { 116 | case "aws.iam": 117 | console.log("processing IAM notification..."); 118 | message = handleIAM(event,context); 119 | break; 120 | default: 121 | console.log("processing unknown notification..."); 122 | message = processError(null, event); 123 | } 124 | 125 | postMessage(message, function(response) { 126 | if (response.statusCode < 400) { 127 | callback(null, 'message posted successfully'); 128 | } else if (response.statusCode < 500) { 129 | // Don't retry because the error is due to a problem with the request 130 | callback(null, `error posting message to API: ${response.statusCode} - ${response.statusMessage}`); 131 | } else { 132 | // Let Lambda retry 133 | callback(`server error when processing message: ${response.statusCode} - ${response.statusMessage}`); 134 | } 135 | }); 136 | }; 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-cloudwatch-event-msteams", 3 | "version": "0.0.1", 4 | "description": "MS Teams notifications for AWS CloudWatch Events", 5 | "authors": [ 6 | "Richard Hurt " 7 | ], 8 | "config": { 9 | "progress": "true" 10 | }, 11 | "scripts":{ 12 | "test": "test/all.sh", 13 | "package": "node-lambda package -x '.* *.json *.example images test'" 14 | }, 15 | "dependencies": { 16 | "aws-sdk": "^2.4.0", 17 | "https": "^1.0.0", 18 | "url": "^0.11.0" 19 | }, 20 | "devDependencies": { 21 | "node-lambda": "^0.9" 22 | }, 23 | "keywords": [ 24 | "aws", 25 | "msteams", 26 | "lambda", 27 | "cloudwatch", 28 | "event", 29 | "notifications" 30 | ], 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /test/all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | for file in test/*.json; do 5 | if [[ $file == "test/context.json" || $file == "test/event.json" ]] ; then 6 | continue; 7 | fi 8 | node-lambda run -x test/context.json -j $file 9 | done 10 | -------------------------------------------------------------------------------- /test/context.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /test/ec2.lambda.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": "YOURACCOUNTNUMBER", 3 | "detail": { 4 | "awsRegion": "us-east-1", 5 | "eventID": "d7a862f9-af85-4990-9018-4444444444", 6 | "eventName": "StartInstances", 7 | "eventSource": "ec2.amazonaws.com", 8 | "eventTime": "2017-03-16T07:00:32Z", 9 | "eventType": "AwsApiCall", 10 | "eventVersion": "1.05", 11 | "recipientAccountId": "YOURACCOUNTNUMBER", 12 | "requestID": "dcc53440-7f2c-466f-9f75-555555555", 13 | "requestParameters": { 14 | "instancesSet": { 15 | "items": [ 16 | { 17 | "instanceId": "i-12345678901234567" 18 | } 19 | ] 20 | } 21 | }, 22 | "responseElements": { 23 | "instancesSet": { 24 | "items": [ 25 | { 26 | "currentState": { 27 | "code": 0, 28 | "name": "pending" 29 | }, 30 | "instanceId": "i-12345678901234567", 31 | "previousState": { 32 | "code": 80, 33 | "name": "stopped" 34 | } 35 | } 36 | ] 37 | } 38 | }, 39 | "sourceIPAddress": "34.207.160.238", 40 | "userAgent": "aws-sdk-nodejs/2.22.0 linux/v4.3.2 exec-env/AWS_Lambda_nodejs4.3 promise", 41 | "userIdentity": { 42 | "accessKeyId": "ASIAIM3PGF76A4SKMTVQ", 43 | "accountId": "YOURACCOUNTNUMBER", 44 | "arn": "arn:aws:sts::YOURACCOUNTNUMBER:assumed-role/Lambda-Account-MyRoleId-VUKODM8XI6HI/StartStopEC2", 45 | "principalId": "YOURPRINCIPALID:StartStopEC2", 46 | "sessionContext": { 47 | "attributes": { 48 | "creationDate": "2017-03-16T07:00:07Z", 49 | "mfaAuthenticated": "false" 50 | }, 51 | "sessionIssuer": { 52 | "accountId": "YOURACCOUNTNUMBER", 53 | "arn": "arn:aws:iam::YOURACCOUNTNUMBER:role/lambda/Lambda-Account-MyRoleId-VUKODM8XI6HI", 54 | "principalId": "YOURPRINCIPALID", 55 | "type": "Role", 56 | "userName": "Lambda-Account-MyRoleId-VUKODM8XI6HI" 57 | } 58 | }, 59 | "type": "AssumedRole" 60 | } 61 | }, 62 | "detail-type": "AWS API Call via CloudTrail", 63 | "id": "85a56db4-67c8-44d1-ad17-555555555555555", 64 | "region": "us-east-1", 65 | "resources": [], 66 | "source": "aws.iam", 67 | "time": "2017-03-16T12:48:04Z", 68 | "version": "0" 69 | } -------------------------------------------------------------------------------- /test/elasticloadbalancing.register.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventVersion": "1.04", 3 | "userIdentity": { 4 | "type": "AssumedRole", 5 | "principalId": "YOURPRINCIPALID:i-234234234324", 6 | "arn": "arn:aws:sts::YOURACCOUNTNUMBER:assumed-role/MyRole/i-234234234324", 7 | "accountId": "YOURACCOUNTNUMBER", 8 | "accessKeyId": "ASIAIX3MBIWB6PG3YSKA", 9 | "sessionContext": { 10 | "attributes": { 11 | "mfaAuthenticated": "false", 12 | "creationDate": "2017-05-04T10:09:37Z" 13 | }, 14 | "sessionIssuer": { 15 | "type": "Role", 16 | "principalId": "YOURPRINCIPALID", 17 | "arn": "arn:aws:iam::YOURACCOUNTNUMBER:role/MyRole", 18 | "accountId": "YOURACCOUNTNUMBER", 19 | "userName": "MyRole" 20 | } 21 | } 22 | }, 23 | "eventTime": "2017-05-04T10:38:56Z", 24 | "eventSource": "elasticloadbalancing.amazonaws.com", 25 | "eventName": "RegisterInstancesWithLoadBalancer", 26 | "awsRegion": "us-east-1", 27 | "sourceIPAddress": "52.70.173.226", 28 | "userAgent": "AWSPowerShell/3.1.60.0 .NET_Runtime/4.0 .NET_Framework/4.0 OS/Microsoft_Windows_NT_6.3.9600.0 WindowsPowerShell/4.-1 ClientSync", 29 | "requestParameters": { 30 | "loadBalancerName": "elb-2342323432-242-24-24423423", 31 | "instances": [ 32 | { 33 | "instanceId": "i-123234345" 34 | } 35 | ] 36 | }, 37 | "responseElements": { 38 | "instances": [ 39 | { 40 | "instanceId": "i-234234234324" 41 | }, 42 | { 43 | "instanceId": "i-234324345345" 44 | } 45 | ] 46 | }, 47 | "requestID": "dfddfaec-30b5-11e7-96f8-5666666666", 48 | "eventID": "cae723b5-fdeb-4a7c-9245-7777777777", 49 | "eventType": "AwsApiCall", 50 | "apiVersion": "2012-06-01", 51 | "recipientAccountId": "YOURACCOUNTNUMBER" 52 | } 53 | -------------------------------------------------------------------------------- /test/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value", 3 | "key2": "value2", 4 | "other_key": "other_value" 5 | } 6 | -------------------------------------------------------------------------------- /test/iam.CreateAccessKey.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": "YOURACCOUNTNUMBER", 3 | "detail": { 4 | "awsRegion": "us-east-1", 5 | "eventID": "d3f21181-ff1c-4021-a913-111111111111", 6 | "eventName": "CreateAccessKey", 7 | "eventSource": "iam.amazonaws.com", 8 | "eventTime": "2017-03-16T13:01:11Z", 9 | "eventType": "AwsApiCall", 10 | "eventVersion": "1.02", 11 | "requestID": "a10bafe7-0a48-11e7-96c9-22222222222", 12 | "requestParameters": { 13 | "userName": "DELETE_ME3" 14 | }, 15 | "responseElements": { 16 | "accessKey": { 17 | "accessKeyId": "AKIAIVCQ6BYBN5QXHD3A", 18 | "createDate": "Mar 16, 2017 1:01:11 PM", 19 | "status": "Active", 20 | "userName": "DELETE_ME3" 21 | } 22 | }, 23 | "sourceIPAddress": "10.10.10.10", 24 | "userAgent": "signin.amazonaws.com", 25 | "userIdentity": { 26 | "accessKeyId": "ASIAIAGCYHRLPVSRJQUA", 27 | "accountId": "YOURACCOUNTNUMBER", 28 | "arn": "arn:aws:iam::YOURACCOUNTNUMBER:user/user_name", 29 | "invokedBy": "signin.amazonaws.com", 30 | "principalId": "YOURPRINCIPALID", 31 | "sessionContext": { 32 | "attributes": { 33 | "creationDate": "2017-03-16T11:44:40Z", 34 | "mfaAuthenticated": "true" 35 | } 36 | }, 37 | "type": "IAMUser", 38 | "userName": "user_name" 39 | } 40 | }, 41 | "detail-type": "AWS API Call via CloudTrail", 42 | "id": "399534ef-5388-4598-beb2-272565a037cd", 43 | "region": "us-east-1", 44 | "resources": [], 45 | "source": "aws.iam", 46 | "time": "2017-03-16T13:01:11Z", 47 | "version": "0" 48 | } -------------------------------------------------------------------------------- /test/iam.DeleteAccessKey.json: -------------------------------------------------------------------------------- 1 | { 2 | "account": "YOURACCOUNTNUMBER", 3 | "detail": { 4 | "awsRegion": "us-east-1", 5 | "eventID": "047ea87e-c1de-434b-80d2-11111111111", 6 | "eventName": "DeleteAccessKey", 7 | "eventSource": "iam.amazonaws.com", 8 | "eventTime": "2017-03-16T12:48:04Z", 9 | "eventType": "AwsApiCall", 10 | "eventVersion": "1.02", 11 | "requestID": "cbee265c-0a46-11e7-8f25-222222222", 12 | "requestParameters": { 13 | "accessKeyId": "AKIAISTGBXV3QYVGTKMQ", 14 | "userName": "DELETE_ME3" 15 | }, 16 | "responseElements": null, 17 | "sourceIPAddress": "10.10.10.10", 18 | "userAgent": "signin.amazonaws.com", 19 | "userIdentity": { 20 | "accessKeyId": "ASIAIAGCYHRLPVSRJQUA", 21 | "accountId": "YOURACCOUNTNUMBER", 22 | "arn": "arn:aws:iam::YOURACCOUNTNUMBER:user/user_name", 23 | "invokedBy": "signin.amazonaws.com", 24 | "principalId": "YOURPRINCIPALID", 25 | "sessionContext": { 26 | "attributes": { 27 | "creationDate": "2017-03-16T11:44:40Z", 28 | "mfaAuthenticated": "true" 29 | } 30 | }, 31 | "type": "IAMUser", 32 | "userName": "user_name" 33 | } 34 | }, 35 | "detail-type": "AWS API Call via CloudTrail", 36 | "id": "85a56db4-67c8-44d1-ad17-1111111111111", 37 | "region": "us-east-1", 38 | "resources": [], 39 | "source": "aws.iam", 40 | "time": "2017-03-16T12:48:04Z", 41 | "version": "0" 42 | } --------------------------------------------------------------------------------