Skip to content

Commit 16d0b15

Browse files
committed
A DynamoDB + S3 solution to store ip block list.
This is a big change in structure and files in this commit. changes include: use index.js to route different events to a proper handler. modify monitor.js to handle guardduty finding events. modify monitor.js to save malicious ip addresses to DynamoDB. add generator.js to handle dynamodb stream events. use generator.js to create and store a static ip block list to S3. enforce code quality checking with eslint. add all necessary files to include on build. create a local.js to help developers/automation tools invoke lambda functions locally. remove unnecessary dependencies. Change-Id: Id4a4216ac7ce24855f7e46747ace47992d5e203a
1 parent 4302eff commit 16d0b15

12 files changed

+607
-182
lines changed

.eslintignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/dist
2+
/node_modules
3+
/local

.eslintrc.json

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{
2+
"extends": "eslint:recommended",
3+
"env": {
4+
"node": true,
5+
"commonjs": true,
6+
"es6": true
7+
},
8+
"parserOptions": {
9+
"ecmaVersion": 8
10+
},
11+
"rules": {
12+
"array-bracket-spacing": "error",
13+
"arrow-parens": ["error", "as-needed"],
14+
"arrow-spacing": "error",
15+
"block-spacing": "error",
16+
"brace-style": ["error", "1tbs", {
17+
"allowSingleLine": true
18+
}],
19+
"comma-dangle": "error",
20+
"comma-style": ["error", "last"],
21+
"computed-property-spacing": "error",
22+
"curly": "error",
23+
"dot-notation": "error",
24+
"eol-last": "error",
25+
"eqeqeq": ["error", "always", {
26+
"null": "ignore"
27+
}],
28+
"func-call-spacing": "error",
29+
"generator-star-spacing": "error",
30+
"indent": ["error", 4, {
31+
"CallExpression": {
32+
"arguments": "off"
33+
},
34+
"FunctionDeclaration": {
35+
"parameters": "off"
36+
},
37+
"FunctionExpression": {
38+
"parameters": "off"
39+
},
40+
"MemberExpression": "off",
41+
"SwitchCase": 1
42+
}],
43+
"keyword-spacing": "error",
44+
"key-spacing": ["error", {
45+
"beforeColon": false,
46+
"afterColon": true,
47+
"mode": "minimum"
48+
}],
49+
"linebreak-style": ["error", "unix"],
50+
"max-depth": ["error", 5 ],
51+
"max-len": ["error", 100, {
52+
"ignoreRegExpLiterals": true
53+
}],
54+
"max-params": ["error", 7],
55+
"new-parens": "error",
56+
"no-bitwise": "error",
57+
"no-cond-assign": ["error", "except-parens"],
58+
"no-confusing-arrow": "error",
59+
"no-console": "off",
60+
"no-constant-condition": ["error", {
61+
"checkLoops": false
62+
}],
63+
"no-empty": ["error", {
64+
"allowEmptyCatch": true
65+
}],
66+
"no-extend-native": "error",
67+
"no-extra-bind": "error",
68+
"no-lone-blocks": "error",
69+
"no-mixed-spaces-and-tabs": "error",
70+
"no-multi-spaces": ["error", {
71+
"ignoreEOLComments": true,
72+
"exceptions": {
73+
"VariableDeclarator": true
74+
}
75+
}],
76+
"no-multi-str": "error",
77+
"no-nested-ternary": "error",
78+
"no-new": "error",
79+
"no-new-func": "error",
80+
"no-shadow": "error",
81+
"no-tabs": "error",
82+
"no-template-curly-in-string": "error",
83+
"no-trailing-spaces": "error",
84+
"no-unneeded-ternary": "error",
85+
"no-unused-expressions": "error",
86+
"no-use-before-define": ["error", {
87+
"functions": false
88+
}],
89+
"no-whitespace-before-property": "error",
90+
"operator-linebreak": ["error", "after"],
91+
"prefer-template": "error",
92+
"quote-props": ["error", "as-needed"],
93+
"quotes": ["error", "single"],
94+
"require-await": "error",
95+
"semi": ["error", "always", {
96+
"omitLastInOneLineBlock": true
97+
}],
98+
"semi-spacing": ["error", {
99+
"before": false,
100+
"after": true
101+
}],
102+
"semi-style": "error",
103+
"spaced-comment": ["error", "always", {
104+
"block": {
105+
"balanced": true
106+
}
107+
}],
108+
"space-before-blocks": ["error", "always"],
109+
"space-before-function-paren": ["error", {
110+
"anonymous": "never",
111+
"named": "never",
112+
"asyncArrow": "always"
113+
}],
114+
"space-infix-ops": "error",
115+
"space-in-parens": "error",
116+
"strict": "error",
117+
"switch-colon-spacing": "error",
118+
"template-curly-spacing": "error",
119+
"valid-jsdoc": ["error", {
120+
"requireReturn": false
121+
}],
122+
"wrap-iife": ["error", "any"],
123+
"yield-star-spacing": "error",
124+
"yoda": "error"
125+
}
126+
}

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
/dist
22
/node_modules
3+
npm-debug.log
4+
/local

extension/object_extension.js

-16
This file was deleted.

generator.js

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
'use strict';
2+
3+
/*
4+
Author: Fortinet
5+
6+
This generator script handles the creation of the static ip block list resource in the S3 bucket.
7+
Information about the Lambda function and configuration is provided in the main script: index.js.
8+
9+
Required IAM permissions:
10+
S3: ListBucket, HeadBucket, GetObject, PutObject, PutObjectAcl
11+
DynamoDB: Scan
12+
13+
*/
14+
const respArr = [];
15+
16+
let S3 = null,
17+
docClient = null;
18+
19+
/*
20+
* set response for callback
21+
*/
22+
const setResp = (msg, detail) => {
23+
respArr.push({
24+
msg: msg,
25+
detail: detail
26+
});
27+
};
28+
29+
/*
30+
* clear response for callback
31+
*/
32+
const unsetResp = () => {
33+
respArr.length = 0;
34+
};
35+
36+
/*
37+
* check if bucket exists
38+
*/
39+
const bucketExists = () => {
40+
return new Promise((resolve, reject) => {
41+
let params = {
42+
Bucket: process.env.S3_BUCKET
43+
};
44+
S3.headBucket(params, function(err, data) { // eslint-disable-line no-unused-vars
45+
if (err) {
46+
console.log('called bucketExists and return error: ', err.stack);
47+
reject(err);
48+
} else {
49+
console.log('called bucketExists: no error.'); // successful response
50+
resolve(params.Bucket);
51+
}
52+
});
53+
});
54+
};
55+
56+
/*
57+
* scan the ip block list table and return a set of ip block list
58+
* return a promise to resolve a list of the table items.
59+
*/
60+
const scanDBTable = () => {
61+
return new Promise((resolve, reject) => {
62+
let params = {
63+
TableName: process.env.DDB_TABLE_NAME
64+
};
65+
docClient.scan(params, function(err, data) {
66+
if (err) {
67+
console.log('call scanDBTable: return error', err.stack);
68+
reject(err);
69+
} else {
70+
console.log('called scanDBTable: scan completed.');
71+
resolve(data.Items);
72+
}
73+
});
74+
});
75+
};
76+
77+
/*
78+
* get the block list file
79+
*/
80+
const getBlockListFile = () => {
81+
return new Promise((resolve, reject) => {
82+
S3.getObject({
83+
Bucket: process.env.S3_BUCKET,
84+
Key: process.env.S3_BLOCKLIST_KEY
85+
}, function(err, data) {
86+
if (err && err.statusCode.toString() !== '404') {
87+
console.log('called saveBlockListToBucket and return error: ', err.stack);
88+
reject('Get ip block list error.');
89+
} else {
90+
if (err && err.statusCode.toString() === '404') {
91+
resolve('');
92+
} else {
93+
resolve(data.Body.toString('ascii'));
94+
}
95+
}
96+
});
97+
});
98+
};
99+
100+
/*
101+
* save the block list file
102+
*/
103+
const saveBlockListFile = (items, blockList) => {
104+
return new Promise((resolve, reject) => {
105+
let found = new Set(),
106+
added = 0;
107+
108+
items.forEach(finding => {
109+
if (blockList.indexOf(finding.ip) < 0) {
110+
blockList += `${finding.ip}\r\n`;
111+
added++;
112+
}
113+
found.add(finding.ip);
114+
});
115+
116+
S3.putObject({
117+
Body: blockList,
118+
Bucket: process.env.S3_BUCKET,
119+
Key: process.env.S3_BLOCKLIST_KEY,
120+
ACL: 'public-read',
121+
ContentType: 'text/plain'
122+
}, function(err, data) { // eslint-disable-line no-unused-vars
123+
if (err) {
124+
console.log('called saveBlockListToBucket and return error: ',
125+
err.stack);
126+
reject('Put ip block list error');
127+
} else {
128+
console.log('called saveBlockListToBucket: no error.');
129+
let msg = `${found.size} IP addresses found,
130+
and ${added} new IP addresses have been added to ip block list.`;
131+
setResp(msg, {
132+
found: found.size,
133+
added: added
134+
});
135+
resolve();
136+
}
137+
});
138+
});
139+
};
140+
141+
exports.handler = async (event, context, callback) => {
142+
const AWS = require('aws-sdk');
143+
144+
// locking API versions
145+
AWS.config.apiVersions = {
146+
lambda: '2015-03-31',
147+
s3: '2006-03-01',
148+
dynamodb: '2012-08-10',
149+
dynamodbstreams: '2012-08-10'
150+
};
151+
152+
unsetResp();
153+
154+
// verify all required process env variables
155+
// check and set AWS region
156+
if (!process.env.REGION) {
157+
setResp('Must specify an AWS region.', null);
158+
callback(null, respArr);
159+
return;
160+
}
161+
162+
if (!process.env.S3_BUCKET || !process.env.S3_BLOCKLIST_KEY) {
163+
setResp('Must specify the S3 bucket and the IP block list file.', null);
164+
callback(null, respArr);
165+
return;
166+
}
167+
168+
if (!process.env.DDB_TABLE_NAME) {
169+
setResp('Must specify an AWS DB Table name.', null);
170+
callback(null, respArr);
171+
return;
172+
}
173+
174+
AWS.config.update({
175+
region: process.env.REGION
176+
});
177+
// AWS services
178+
S3 = new AWS.S3();
179+
docClient = new AWS.DynamoDB.DocumentClient();
180+
181+
try {
182+
unsetResp();
183+
// scan the DynamoDB table to get all ip records
184+
let ipRecords = await scanDBTable();
185+
// check if s3 bucket exists.
186+
await bucketExists();
187+
// get the current block list
188+
let blockList = await getBlockListFile();
189+
// update and save the ip block list file
190+
await saveBlockListFile(ipRecords, blockList);
191+
} catch (err) {
192+
setResp('There\'s a problem in generating ip block list. Pleasesee detailed' +
193+
' information in CloudWatch logs.', null);
194+
} finally {
195+
callback(null, respArr);
196+
}
197+
};

0 commit comments

Comments
 (0)