|
| 1 | +AWSTemplateFormatVersion: 2010-09-09 |
| 2 | +Description: > |
| 3 | + This function is invoked by AWS CloudWatch events in response to state change |
| 4 | + in your AWS resources which matches a event target definition. The event |
| 5 | + payload received is then forwarded to Sumo Logic HTTP source endpoint. |
| 6 | +Parameters: |
| 7 | + SumoEndpointUrl: |
| 8 | + Type: String |
| 9 | +Outputs: |
| 10 | + CloudWatchEventFunction: |
| 11 | + Description: CloudWatchEvent Processor Function ARN |
| 12 | + Value: !GetAtt |
| 13 | + - CloudWatchEventFunction |
| 14 | + - Arn |
| 15 | +Resources: |
| 16 | + CloudWatchEventFunction: |
| 17 | + Type: 'AWS::Lambda::Function' |
| 18 | + Metadata: |
| 19 | + SamResourceId: CloudWatchEventFunction |
| 20 | + Properties: |
| 21 | + Code: |
| 22 | + ZipFile: | |
| 23 | + // SumoLogic Endpoint to post logs |
| 24 | + var SumoURL = process.env.SUMO_ENDPOINT; |
| 25 | +
|
| 26 | + // For some beta AWS services, the default is to remove the outer fields of the received object since they are not useful. |
| 27 | + // change this if necessary. |
| 28 | + var removeOuterFields = false; |
| 29 | +
|
| 30 | + // The following parameters override the sourceCategoryOverride, sourceHostOverride and sourceNameOverride metadata fields within SumoLogic. |
| 31 | + // Not these can also be overridden via json within the message payload. See the README for more information. |
| 32 | + var sourceCategoryOverride = process.env.SOURCE_CATEGORY_OVERRIDE || ''; // If empty sourceCategoryOverride will not be overridden |
| 33 | + var sourceHostOverride = process.env.SOURCE_HOST_OVERRIDE || ''; // If empty sourceHostOverride will not be set to the name of the logGroup |
| 34 | + var sourceNameOverride = process.env.SOURCE_NAME_OVERRIDE || ''; // If empty sourceNameOverride will not be set to the name of the logStream |
| 35 | +
|
| 36 | + var retryInterval = process.env.RETRY_INTERVAL || 5000; // the interval in millisecs between retries |
| 37 | + var numOfRetries = process.env.NUMBER_OF_RETRIES || 3; // the number of retries |
| 38 | +
|
| 39 | + var https = require('https'); |
| 40 | + var zlib = require('zlib'); |
| 41 | + var url = require('url'); |
| 42 | +
|
| 43 | + Promise.retryMax = function(fn,retry,interval,fnParams) { |
| 44 | + return fn.apply(this,fnParams).catch( err => { |
| 45 | + var waitTime = typeof interval === 'function' ? interval() : interval; |
| 46 | + console.log("Retries left " + (retry-1) + " delay(in ms) " + waitTime); |
| 47 | + return (retry>1? Promise.wait(waitTime).then(()=> Promise.retryMax(fn,retry-1,interval, fnParams)):Promise.reject(err)); |
| 48 | + }); |
| 49 | + } |
| 50 | +
|
| 51 | + Promise.wait = function(delay) { |
| 52 | + return new Promise((fulfill,reject)=> { |
| 53 | + //console.log(Date.now()); |
| 54 | + setTimeout(fulfill,delay||0); |
| 55 | + }); |
| 56 | + }; |
| 57 | +
|
| 58 | + function exponentialBackoff(seed) { |
| 59 | + var count = 0; |
| 60 | + return function() { |
| 61 | + count++; |
| 62 | + return count*seed; |
| 63 | + } |
| 64 | + } |
| 65 | +
|
| 66 | + function postToSumo(callback, messages) { |
| 67 | + var messagesTotal = Object.keys(messages).length; |
| 68 | + var messagesSent = 0; |
| 69 | + var messageErrors = []; |
| 70 | +
|
| 71 | + var urlObject = url.parse(SumoURL); |
| 72 | + var options = { |
| 73 | + 'hostname': urlObject.hostname, |
| 74 | + 'path': urlObject.pathname, |
| 75 | + 'method': 'POST' |
| 76 | + }; |
| 77 | +
|
| 78 | + var finalizeContext = function () { |
| 79 | + var total = messagesSent + messageErrors.length; |
| 80 | + if (total == messagesTotal) { |
| 81 | + console.log('messagesSent: ' + messagesSent + ' messagesErrors: ' + messageErrors.length); |
| 82 | + if (messageErrors.length > 0) { |
| 83 | + callback('errors: ' + messageErrors); |
| 84 | + } else { |
| 85 | + callback(null, "Success"); |
| 86 | + } |
| 87 | + } |
| 88 | + }; |
| 89 | +
|
| 90 | + function httpSend(options, headers, data) { |
| 91 | + return new Promise( (resolve,reject) => { |
| 92 | + var curOptions = options; |
| 93 | + curOptions.headers = headers; |
| 94 | + var req = https.request(curOptions, function (res) { |
| 95 | + var body = ''; |
| 96 | + res.setEncoding('utf8'); |
| 97 | + res.on('data', function (chunk) { |
| 98 | + body += chunk; // don't really do anything with body |
| 99 | + }); |
| 100 | + res.on('end', function () { |
| 101 | + if (res.statusCode == 200) { |
| 102 | + resolve(body); |
| 103 | + } else { |
| 104 | + reject({'error':'HTTP Return code ' + res.statusCode,'res':res}); |
| 105 | + } |
| 106 | + }); |
| 107 | + }); |
| 108 | + req.on('error', function (e) { |
| 109 | + reject({'error':e,'res':null}); |
| 110 | + }); |
| 111 | + for (var i = 0; i < data.length; i++) { |
| 112 | + req.write(JSON.stringify(data[i]) + '\n'); |
| 113 | + } |
| 114 | + console.log("sending to Sumo...") |
| 115 | + req.end(); |
| 116 | + }); |
| 117 | + } |
| 118 | + Object.keys(messages).forEach(function (key, index) { |
| 119 | + var headerArray = key.split(':'); |
| 120 | + var headers = { |
| 121 | + 'X-Sumo-Name': headerArray[0], |
| 122 | + 'X-Sumo-Category': headerArray[1], |
| 123 | + 'X-Sumo-Host': headerArray[2], |
| 124 | + 'X-Sumo-Client': 'cloudwatchevents-aws-lambda' |
| 125 | + }; |
| 126 | + Promise.retryMax(httpSend, numOfRetries, retryInterval, [options, headers, messages[key]]).then((body)=> { |
| 127 | + messagesSent++; |
| 128 | + finalizeContext() |
| 129 | + }).catch((e) => { |
| 130 | + messageErrors.push(e.error); |
| 131 | + finalizeContext(); |
| 132 | + }); |
| 133 | + }); |
| 134 | + } |
| 135 | +
|
| 136 | + exports.handler = function (event, context, callback) { |
| 137 | +
|
| 138 | + // Used to hold chunks of messages to post to SumoLogic |
| 139 | + var messageList = {}; |
| 140 | + var final_event; |
| 141 | + // Validate URL has been set |
| 142 | + var urlObject = url.parse(SumoURL); |
| 143 | + if (urlObject.protocol != 'https:' || urlObject.host === null || urlObject.path === null) { |
| 144 | + callback('Invalid SUMO_ENDPOINT environment variable: ' + SumoURL); |
| 145 | + } |
| 146 | +
|
| 147 | + //console.log(event); |
| 148 | + if ((event.source==="aws.guardduty") || (removeOuterFields)) { |
| 149 | + final_event =event.detail; |
| 150 | + } else { |
| 151 | + final_event = event; |
| 152 | + } |
| 153 | + messageList[sourceNameOverride+':'+sourceCategoryOverride+':'+sourceHostOverride]=[final_event]; |
| 154 | + postToSumo(callback, messageList); |
| 155 | + }; |
| 156 | + Handler: index.handler |
| 157 | + Role: !GetAtt |
| 158 | + - CloudWatchEventFunctionRole |
| 159 | + - Arn |
| 160 | + Runtime: nodejs22.x |
| 161 | + Timeout: 300 |
| 162 | + Environment: |
| 163 | + Variables: |
| 164 | + SUMO_ENDPOINT: !Ref SumoEndpointUrl |
| 165 | + Tags: |
| 166 | + - Key: 'lambda:createdBy' |
| 167 | + Value: SAM |
| 168 | + CloudWatchEventFunctionRole: |
| 169 | + Type: 'AWS::IAM::Role' |
| 170 | + Properties: |
| 171 | + AssumeRolePolicyDocument: |
| 172 | + Version: 2012-10-17 |
| 173 | + Statement: |
| 174 | + - Action: |
| 175 | + - 'sts:AssumeRole' |
| 176 | + Effect: Allow |
| 177 | + Principal: |
| 178 | + Service: |
| 179 | + - lambda.amazonaws.com |
| 180 | + ManagedPolicyArns: |
| 181 | + - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' |
| 182 | + Tags: |
| 183 | + - Key: 'lambda:createdBy' |
| 184 | + Value: SAM |
| 185 | + CloudWatchEventFunctionCloudWatchEventTrigger: |
| 186 | + Type: 'AWS::Events::Rule' |
| 187 | + Properties: |
| 188 | + EventPattern: |
| 189 | + source: |
| 190 | + - aws.guardduty |
| 191 | + Targets: |
| 192 | + - Arn: !GetAtt |
| 193 | + - CloudWatchEventFunction |
| 194 | + - Arn |
| 195 | + Id: CloudWatchEventFunctionCloudWatchEventTriggerLambdaTarget |
| 196 | + CloudWatchEventFunctionCloudWatchEventTriggerPermission: |
| 197 | + Type: 'AWS::Lambda::Permission' |
| 198 | + Properties: |
| 199 | + Action: 'lambda:InvokeFunction' |
| 200 | + FunctionName: !Ref CloudWatchEventFunction |
| 201 | + Principal: events.amazonaws.com |
| 202 | + SourceArn: !GetAtt |
| 203 | + - CloudWatchEventFunctionCloudWatchEventTrigger |
| 204 | + - Arn |
0 commit comments