├── .gitignore ├── LICENSE ├── README.md ├── env.yml.example ├── handler.js ├── hits.js ├── loggly.js ├── package.json ├── s3.js └── serverless.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .serverless 3 | env.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 MyBuilder Limited 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 | AWS WAF Logger 2 | -------------- 3 | 4 | The AWS WAF is an amazing feature however actually getting meaningful logs out of it can be a pain. 5 | Since putting it in-place we have been wanting to analyse the traffic patterns and which rules are getting hit. 6 | However, at this time AWS does not provide such a log stream. 7 | 8 | To remedy this we have created this small scheduled Lambda which queries the AWS SDK [`GetSampledRequests`](http://docs.aws.amazon.com/waf/latest/APIReference/API_GetSampledRequests.html) action to fetch any matches and store them in S3 and/or [Loggly](https://www.loggly.com/). 9 | This allows us to look at current and historical data about the WAF's actions. 10 | 11 | ### Configuration 12 | 13 | You must first specify your desired configuration within `env.yml`, using `env.yml.example` as a template. 14 | This service uses [Serverless](https://serverless.com/) to manage provisioning the Lambda, so with this present on your machine you can simply execute: 15 | 16 | ```bash 17 | $ serverless deploy -v 18 | ``` 19 | 20 | Depending on if you have configured to output the logs to S3 and/or Loggly you will now begin to see any resulting output based on your check frequency. 21 | 22 | **Note**: `GetSampledRequests` only returns a 'sample' (max 500) among the first 5,000 request that your resource receives during the specified time range. 23 | As such the check frequency may need to be adjusted according to your throughput. 24 | -------------------------------------------------------------------------------- /env.yml.example: -------------------------------------------------------------------------------- 1 | CHECK_EVERY_MINUTES: 10 2 | MAX_WAF_ITEMS: 500 3 | WEB_ACLS: ',' 4 | LOG_BUCKET: '' 5 | LOGGLY_TOKEN: '' 6 | LOGGLY_TAG: 'aws-waf' 7 | -------------------------------------------------------------------------------- /handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { getWebAclHits } = require('./hits'); 4 | 5 | const { CHECK_EVERY_MINUTES, LOG_BUCKET, LOGGLY_TOKEN } = process.env; 6 | const WEB_ACLS = process.env.WEB_ACLS.split(','); 7 | 8 | const createTimeRange = (minutesAgo) => { 9 | const end = new Date(); 10 | const start = new Date(end); 11 | start.setMinutes(end.getMinutes() - minutesAgo); 12 | return { start, end }; 13 | }; 14 | 15 | const flatten = (arrs) => Array.prototype.concat.apply([], arrs); 16 | 17 | module.exports.log = (event, context, callback) => { 18 | const timeRange = createTimeRange(CHECK_EVERY_MINUTES); 19 | 20 | Promise.all(WEB_ACLS.map(aclId => getWebAclHits(aclId, timeRange))) 21 | .then(flatten) 22 | .then(hits => LOG_BUCKET ? require('./s3').writeHitsToBucket(timeRange, hits) : hits) 23 | .then(hits => LOGGLY_TOKEN ? require('./loggly').sendHitsToLoggly(hits) : hits) 24 | .then(hits => hits.length > 0 && callback(undefined, `Successfully logged ${hits.length} requests.`)) 25 | .catch(err => callback(err)); 26 | }; 27 | -------------------------------------------------------------------------------- /hits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AWS = require('aws-sdk'); 4 | const waf = new AWS.WAF(); 5 | 6 | const { MAX_WAF_ITEMS } = process.env; 7 | 8 | const getRuleIds = (aclId) => new Promise((res, rej) => { 9 | waf.getWebACL({ WebACLId: aclId }, (err, data) => { 10 | if (err) rej(err); 11 | else res(data.WebACL.Rules.map(r => r.RuleId)); 12 | }); 13 | }); 14 | 15 | const getRuleHits = (aclId, ruleId, { start, end }) => new Promise((res, rej) => { 16 | const params = { 17 | MaxItems: MAX_WAF_ITEMS, 18 | RuleId: ruleId, 19 | TimeWindow: { StartTime: start, EndTime: end }, 20 | WebAclId: aclId, 21 | }; 22 | 23 | waf.getSampledRequests(params, (err, data) => { 24 | if (err) rej(err); 25 | else { 26 | res(data.SampledRequests.reduce((acc, req) => { 27 | return [ ...acc, Object.assign(req, { WebAclId: aclId, RuleId: ruleId }) ]; 28 | }, [])); 29 | } 30 | }); 31 | }); 32 | 33 | const normaliseHeaders = (hit) => { 34 | hit.Request.Headers = hit.Request.Headers.reduce((headers, { Name, Value }) => { 35 | headers[Name.toLowerCase()] = Value; 36 | return headers; 37 | }, {}); 38 | 39 | return hit; 40 | }; 41 | 42 | const flatten = (arrs) => Array.prototype.concat.apply([], arrs); 43 | 44 | module.exports.getWebAclHits = (aclId, timeRange) => 45 | getRuleIds(aclId) 46 | .then(ruleIds => Promise.all(ruleIds.map(ruleId => getRuleHits(aclId, ruleId, timeRange)))) 47 | .then(flatten) 48 | .then(hits => hits.map(normaliseHeaders)); 49 | -------------------------------------------------------------------------------- /loggly.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const https = require('https'); 4 | 5 | const { LOGGLY_TOKEN, LOGGLY_TAG } = process.env; 6 | 7 | const sendHitToLoggly = (hit) => new Promise((res, rej) => { 8 | const payload = JSON.stringify(hit); 9 | const params = { 10 | host: 'logs-01.loggly.com', 11 | path: `/inputs/${LOGGLY_TOKEN}/tag/${LOGGLY_TAG}`, 12 | method: 'POST', 13 | headers: { 14 | 'Content-Type': 'application/json', 15 | 'Content-Length': payload.length, 16 | }, 17 | }; 18 | 19 | const req = https.request(params, (r) => { 20 | if (r.statusCode < 200 || r.statusCode > 299) rej(new Error(`Request failed: ${r.statusCode}`)); 21 | }); 22 | req.on('error', rej); 23 | req.write(payload); 24 | req.end(); 25 | res(); 26 | }); 27 | 28 | module.exports.sendHitsToLoggly = (hits) => 29 | Promise.all(hits.map(sendHitToLoggly)).then(() => hits); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-waf-logger", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mybuilder/aws-waf-logger.git" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/mybuilder/aws-waf-logger/issues" 18 | }, 19 | "homepage": "https://github.com/mybuilder/aws-waf-logger#readme" 20 | } 21 | -------------------------------------------------------------------------------- /s3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const AWS = require('aws-sdk'); 4 | const s3 = new AWS.S3(); 5 | 6 | const { LOG_BUCKET } = process.env; 7 | 8 | module.exports.writeHitsToBucket = (timeRange, hits) => new Promise((res, rej) => { 9 | if (hits.length === 0) { 10 | res(hits); 11 | return; 12 | } 13 | 14 | const params = { 15 | Bucket: LOG_BUCKET, 16 | Key: `${timeRange.start}-${timeRange.end}`, 17 | Body: JSON.stringify(hits), 18 | ContentType: 'application/json', 19 | }; 20 | 21 | s3.putObject(params, (err, data) => { 22 | if (err) rej(err); 23 | else res(hits); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: aws-waf-logger 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs6.10 6 | stage: prod 7 | region: eu-west-1 8 | memorySize: 3008 9 | timeout: 300 10 | iamRoleStatements: 11 | - Effect: Allow 12 | Action: 13 | - s3:* 14 | Resource: 'arn:aws:s3:::${self:provider.environment.LOG_BUCKET}/*' 15 | - Effect: Allow 16 | Action: 17 | - waf:* 18 | Resource: '*' 19 | environment: ${file(./env.yml)} 20 | 21 | package: 22 | exclude: 23 | - env.yml 24 | - env.yml.example 25 | - README.md 26 | - package.json 27 | 28 | functions: 29 | log: 30 | handler: handler.log 31 | events: 32 | - schedule: 33 | rate: rate(${self:provider.environment.CHECK_EVERY_MINUTES} minutes) 34 | --------------------------------------------------------------------------------