From 16d0b1594c71d6413c343dc6e4b5ad1ed15187c0 Mon Sep 17 00:00:00 2001
From: Jayden Liang <jliang01@fortinet.com>
Date: Fri, 27 Jul 2018 14:28:36 -0700
Subject: [PATCH] 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
---
 .eslintignore                 |   3 +
 .eslintrc.json                | 126 +++++++++++++++
 .gitignore                    |   2 +
 extension/object_extension.js |  16 --
 generator.js                  | 197 +++++++++++++++++++++++
 index.js                      |  54 +++++++
 local.js                      |  56 +++++++
 monitor.js                    | 283 +++++++++++++++-------------------
 package.json                  |   9 +-
 scripts/install_notes.js      |   4 +-
 scripts/make_dist.js          |  15 +-
 utils/ObjectUtils.js          |  24 +++
 12 files changed, 607 insertions(+), 182 deletions(-)
 create mode 100644 .eslintignore
 create mode 100644 .eslintrc.json
 delete mode 100644 extension/object_extension.js
 create mode 100644 generator.js
 create mode 100644 index.js
 create mode 100644 local.js
 create mode 100644 utils/ObjectUtils.js

diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..0538620
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,3 @@
+/dist
+/node_modules
+/local
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..f0a823d
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,126 @@
+{
+    "extends": "eslint:recommended",
+    "env": {
+        "node": true,
+        "commonjs": true,
+        "es6": true
+    },
+    "parserOptions": {
+        "ecmaVersion": 8
+    },
+    "rules": {
+        "array-bracket-spacing": "error",
+        "arrow-parens": ["error", "as-needed"],
+        "arrow-spacing": "error",
+        "block-spacing": "error",
+        "brace-style": ["error", "1tbs", {
+            "allowSingleLine": true
+        }],
+        "comma-dangle": "error",
+        "comma-style": ["error", "last"],
+        "computed-property-spacing": "error",
+        "curly": "error",
+        "dot-notation": "error",
+        "eol-last": "error",
+        "eqeqeq": ["error", "always", {
+            "null": "ignore"
+        }],
+        "func-call-spacing": "error",
+        "generator-star-spacing": "error",
+        "indent": ["error", 4, {
+            "CallExpression": {
+                "arguments": "off"
+            },
+            "FunctionDeclaration": {
+                "parameters": "off"
+            },
+            "FunctionExpression": {
+                "parameters": "off"
+            },
+            "MemberExpression": "off",
+            "SwitchCase": 1
+        }],
+        "keyword-spacing": "error",
+        "key-spacing": ["error", {
+            "beforeColon": false,
+            "afterColon": true,
+            "mode": "minimum"
+        }],
+        "linebreak-style": ["error", "unix"],
+        "max-depth": ["error", 5 ],
+        "max-len": ["error", 100, {
+            "ignoreRegExpLiterals": true
+        }],
+        "max-params": ["error", 7],
+        "new-parens": "error",
+        "no-bitwise": "error",
+        "no-cond-assign": ["error", "except-parens"],
+        "no-confusing-arrow": "error",
+        "no-console": "off",
+        "no-constant-condition": ["error", {
+            "checkLoops": false
+        }],
+        "no-empty": ["error", {
+            "allowEmptyCatch": true
+        }],
+        "no-extend-native": "error",
+        "no-extra-bind": "error",
+        "no-lone-blocks": "error",
+        "no-mixed-spaces-and-tabs": "error",
+        "no-multi-spaces": ["error", {
+            "ignoreEOLComments": true,
+            "exceptions": {
+                "VariableDeclarator": true
+            }
+        }],
+        "no-multi-str": "error",
+        "no-nested-ternary": "error",
+        "no-new": "error",
+        "no-new-func": "error",
+        "no-shadow": "error",
+        "no-tabs": "error",
+        "no-template-curly-in-string": "error",
+        "no-trailing-spaces": "error",
+        "no-unneeded-ternary": "error",
+        "no-unused-expressions": "error",
+        "no-use-before-define": ["error", {
+            "functions": false
+        }],
+        "no-whitespace-before-property": "error",
+        "operator-linebreak": ["error", "after"],
+        "prefer-template": "error",
+        "quote-props": ["error", "as-needed"],
+        "quotes": ["error", "single"],
+        "require-await": "error",
+        "semi": ["error", "always", {
+            "omitLastInOneLineBlock": true
+        }],
+        "semi-spacing": ["error", {
+            "before": false,
+            "after": true
+        }],
+        "semi-style": "error",
+        "spaced-comment": ["error", "always", {
+            "block": {
+                "balanced": true
+            }
+        }],
+        "space-before-blocks": ["error", "always"],
+        "space-before-function-paren": ["error", {
+            "anonymous": "never",
+            "named": "never",
+            "asyncArrow": "always"
+        }],
+        "space-infix-ops": "error",
+        "space-in-parens": "error",
+        "strict": "error",
+        "switch-colon-spacing": "error",
+        "template-curly-spacing": "error",
+        "valid-jsdoc": ["error", {
+            "requireReturn": false
+        }],
+        "wrap-iife": ["error", "any"],
+        "yield-star-spacing": "error",
+        "yoda": "error"
+    }
+}
diff --git a/.gitignore b/.gitignore
index c925c21..27a91e2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,4 @@
 /dist
 /node_modules
+npm-debug.log
+/local
\ No newline at end of file
diff --git a/extension/object_extension.js b/extension/object_extension.js
deleted file mode 100644
index e8a9d8f..0000000
--- a/extension/object_extension.js
+++ /dev/null
@@ -1,16 +0,0 @@
-Object.prototype.fetch = function(path) {
-    var keys = path.split('/');
-    var tmp = this;
-
-    for (var i = 0,size = keys.length; i < size; i++) {
-        var k = keys[i];
-        if (tmp.hasOwnProperty(k)) {
-            tmp = tmp[k]
-        } else if (typeof tmp[k] === 'undefined') {
-            return null;
-        }
-    }
-
-    return tmp;
-};
-
diff --git a/generator.js b/generator.js
new file mode 100644
index 0000000..09b77be
--- /dev/null
+++ b/generator.js
@@ -0,0 +1,197 @@
+'use strict';
+
+/*
+Author: Fortinet
+
+This generator script handles the creation of the static ip block list resource in the S3 bucket.
+Information about the Lambda function and configuration is provided in the main script: index.js.
+
+Required IAM permissions:
+S3: ListBucket, HeadBucket, GetObject, PutObject, PutObjectAcl
+DynamoDB: Scan
+
+*/
+const respArr = [];
+
+let S3 = null,
+    docClient = null;
+
+/*
+ * set response for callback
+ */
+const setResp = (msg, detail) => {
+    respArr.push({
+        msg: msg,
+        detail: detail
+    });
+};
+
+/*
+ * clear response for callback
+ */
+const unsetResp = () => {
+    respArr.length = 0;
+};
+
+/*
+ * check if bucket exists
+ */
+const bucketExists = () => {
+    return new Promise((resolve, reject) => {
+        let params = {
+            Bucket: process.env.S3_BUCKET
+        };
+        S3.headBucket(params, function(err, data) { // eslint-disable-line no-unused-vars
+            if (err) {
+                console.log('called bucketExists and return error: ', err.stack);
+                reject(err);
+            } else {
+                console.log('called bucketExists: no error.'); // successful response
+                resolve(params.Bucket);
+            }
+        });
+    });
+};
+
+/*
+ * scan the ip block list table and return a set of ip block list
+ * return a promise to resolve a list of the table items.
+ */
+const scanDBTable = () => {
+    return new Promise((resolve, reject) => {
+        let params = {
+            TableName: process.env.DDB_TABLE_NAME
+        };
+        docClient.scan(params, function(err, data) {
+            if (err) {
+                console.log('call scanDBTable: return error', err.stack);
+                reject(err);
+            } else {
+                console.log('called scanDBTable: scan completed.');
+                resolve(data.Items);
+            }
+        });
+    });
+};
+
+/*
+ * get the block list file
+ */
+const getBlockListFile = () => {
+    return new Promise((resolve, reject) => {
+        S3.getObject({
+            Bucket: process.env.S3_BUCKET,
+            Key: process.env.S3_BLOCKLIST_KEY
+        }, function(err, data) {
+            if (err && err.statusCode.toString() !== '404') {
+                console.log('called saveBlockListToBucket and return error: ', err.stack);
+                reject('Get ip block list error.');
+            } else {
+                if (err && err.statusCode.toString() === '404') {
+                    resolve('');
+                } else {
+                    resolve(data.Body.toString('ascii'));
+                }
+            }
+        });
+    });
+};
+
+/*
+ * save the block list file
+ */
+const saveBlockListFile = (items, blockList) => {
+    return new Promise((resolve, reject) => {
+        let found = new Set(),
+            added = 0;
+
+        items.forEach(finding => {
+            if (blockList.indexOf(finding.ip) < 0) {
+                blockList += `${finding.ip}\r\n`;
+                added++;
+            }
+            found.add(finding.ip);
+        });
+
+        S3.putObject({
+            Body: blockList,
+            Bucket: process.env.S3_BUCKET,
+            Key: process.env.S3_BLOCKLIST_KEY,
+            ACL: 'public-read',
+            ContentType: 'text/plain'
+        }, function(err, data) { // eslint-disable-line no-unused-vars
+            if (err) {
+                console.log('called saveBlockListToBucket and return error: ',
+                    err.stack);
+                reject('Put ip block list error');
+            } else {
+                console.log('called saveBlockListToBucket: no error.');
+                let msg = `${found.size} IP addresses found,
+                        and ${added} new IP addresses have been added to ip block list.`;
+                setResp(msg, {
+                    found: found.size,
+                    added: added
+                });
+                resolve();
+            }
+        });
+    });
+};
+
+exports.handler = async (event, context, callback) => {
+    const AWS = require('aws-sdk');
+
+    // locking API versions
+    AWS.config.apiVersions = {
+        lambda: '2015-03-31',
+        s3: '2006-03-01',
+        dynamodb: '2012-08-10',
+        dynamodbstreams: '2012-08-10'
+    };
+
+    unsetResp();
+
+    // verify all required process env variables
+    // check and set AWS region
+    if (!process.env.REGION) {
+        setResp('Must specify an AWS region.', null);
+        callback(null, respArr);
+        return;
+    }
+
+    if (!process.env.S3_BUCKET || !process.env.S3_BLOCKLIST_KEY) {
+        setResp('Must specify the S3 bucket and the IP block list file.', null);
+        callback(null, respArr);
+        return;
+    }
+
+    if (!process.env.DDB_TABLE_NAME) {
+        setResp('Must specify an AWS DB Table name.', null);
+        callback(null, respArr);
+        return;
+    }
+
+    AWS.config.update({
+        region: process.env.REGION
+    });
+    // AWS services
+    S3 = new AWS.S3();
+    docClient = new AWS.DynamoDB.DocumentClient();
+
+    try {
+        unsetResp();
+        // scan the DynamoDB table to get all ip records
+        let ipRecords = await scanDBTable();
+        // check if s3 bucket exists.
+        await bucketExists();
+        // get the current block list
+        let blockList = await getBlockListFile();
+        // update and save the ip block list file
+        await saveBlockListFile(ipRecords, blockList);
+    } catch (err) {
+        setResp('There\'s a problem in generating ip block list. Pleasesee detailed' +
+            ' information in CloudWatch logs.', null);
+    } finally {
+        callback(null, respArr);
+    }
+};
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..50c89f0
--- /dev/null
+++ b/index.js
@@ -0,0 +1,54 @@
+'use strict';
+
+/*
+Author: Fortinet
+
+The following Lambda function will be called in CloudWatch when GuardDuty sends logs to CloudWatch.
+This script will write the malicious IP to a dedicate file in S3 bucket.
+Firewall service (i.e. FortiOS) can pull this list, and add those malicious IPs to the blacklist.
+
+Currently the script has the following configurations (By environment variable):
+
+MIN_SEVERITY: (integer only)
+S3_BLOCKLIST_KEY: (path to the file)
+S3_BUCKET: (S3 bucket name)
+REGION: (AWS region)
+DDB_TABLE_NAME: (DynamoDB table to store ip)
+
+Required IAM permissions:
+S3: ListBucket, HeadBucket, GetObject, PutObject, PutObjectAcl
+DynamoDB: DescribeStream, ListStreams, Scan, GetShardIterator, GetRecords, UpdateItem
+
+The script will report the IP for the following conditions:
+
+1. For inbound connection direction, if severity is greater than or equal to MIN_SEVERITY
+2. For unknown connection direction, if the IP was flagged in the threat list name
+
+** This script will only focus on the external attack, internal attack won't get reported.
+Therefore, only remote IP will be stored to black list.
+
+*/
+
+const ObjectUtils = require('./utils/ObjectUtils.js');
+var script = null;
+// process event and route to a proper handler script such as monitor.js or generator.js
+const index = (event, context, callback) => {
+    const serviceName = ObjectUtils.fetch(event, 'detail/service/serviceName') || null,
+        records = ObjectUtils.fetch(event, 'Records') || null;
+    if (serviceName === 'guardduty') {
+        // route to monitor.js
+        console.log('calling monitor script.');
+        script = require('./monitor');
+        script.handler.call(null, event, context, callback);
+    } else if (records !== null) {
+        // route to generator.js
+        console.log('calling generator script.');
+        script = require('./generator');
+        script.handler.call(null, event, context, callback);
+    } else {
+        console.log('No matched script to run. Function exits.');
+        callback('Invalid event.');
+    }
+};
+
+exports.handler = index;
diff --git a/local.js b/local.js
new file mode 100644
index 0000000..74e320f
--- /dev/null
+++ b/local.js
@@ -0,0 +1,56 @@
+'use strict';
+/*
+Author: Fortinet
+
+This script intends to run the project in local node dev environment instead of AWS Lambda over the
+cloud, for local development purpose.
+Please install aws-cli and configure it with a proper AWS account you use for development.
+
+requirements:
+The lambda function entry is index.js by default. Or change it on line 54.
+The lambda function entry and local.js must be situated in the same directory.
+
+This script accept a few command line arguments. See the argument list below:
+
+Argument list:
+first argument: the file path to an external resource as an input event.
+second argument: the file path to a script to load environment variables into process.
+
+To load a set of variables into process.env, you can also create a separate script in the 'local'
+directory. Then specify the file path as the second argument when you execute the local.js script
+via 'npm run local.js' command.
+
+*/
+console.log('Start to run in local environment...');
+
+// the argument index for the source file path of event json
+const ARGV_PROCESS_EVENT_JSON = 2;
+// the argument index for the source file path where this script loads environment variable from
+const ARGV_PROCESS_ENV_SCRIPT = 3;
+var fs = require('fs');
+
+var event = null,
+    context = {},
+    callback = function(context, response) { // eslint-disable-line no-shadow
+        console.log('handle callback is called with:', response, context);
+    };
+
+// run the script to load process.env if the command line argument is specified.
+if (process.argv[ARGV_PROCESS_ENV_SCRIPT] !== undefined) {
+    require(require.resolve(`${process.cwd()}/${process.argv[ARGV_PROCESS_ENV_SCRIPT]}`));
+}
+
+// if provided an event json file, use is. otherwise, use an empty event.
+if (process.argv[ARGV_PROCESS_EVENT_JSON] !== undefined &&
+    fs.existsSync(process.argv[ARGV_PROCESS_EVENT_JSON])) {
+    const data = fs.readFileSync(process.argv[ARGV_PROCESS_EVENT_JSON]);
+    try {
+        event = JSON.parse(data);
+    } catch (e) {
+        throw e;
+    }
+}
+// load entry script with an event
+var entryScript = require(require.resolve(`${__dirname}/index`));
+console.log('Loading entry script...');
+entryScript.handler.call(null, event, context, callback);
diff --git a/monitor.js b/monitor.js
index d946181..d514899 100644
--- a/monitor.js
+++ b/monitor.js
@@ -3,174 +3,139 @@
 /*
 Author: Fortinet
 
-The following Lambda function will be called in CloudWatch when GuardDuty sends logs to CloudWatch.
-This script will write the malicious IP to a dedicate file in S3 bucket. Firewall service (i.e. FortiOS) can
-pull this list, and add those malicious IPs to the blacklist.
+This monitor script handles the reporting and logging part of the Lambda function.
+Information about the Lambda function and configuration is provided in the main script: index.js.
 
-Currently the script has the following configurations (By environment variable):
-
-MIN_SEVERITY: (integer only)
-S3_BLACKLIST_KEY: (path to the file)
-S3_BUCKET: (S3 bucket name)
-
-The script will report the IP for the following conditions:
-
-1. For inbound connection direction, if severity is greater than or equal to MIN_SEVERITY
-2. For unknown connection direction, if the IP was flagged in the threat list name
-
-** This script will only focus on the external attack, internal attack won't get reported.
-Therefore, only remote IP will be stored to black list.
+Required IAM permissions:
+DynamoDB: UpdateItem
 
 */
+const
+    objectUtils = require('./utils/ObjectUtils.js'),
+    respArr = [];
 
+let docClient = null;
 
-require('extension/object_extension.js');
-
-var monitor = function() {
-
-  const
-    _q = require('q'),
-    _aws = require('aws-sdk'),
-    _s3 = new _aws.S3(),
-    _s3_param = {
-        Bucket : process.env.S3_BUCKET,
-        Key : process.env.S3_BLACKLIST_KEY
-    };
-
-  var
-    _found = 0,
-    _added = 0,
-    _resp = [],
-    _s3_get = function() {
-        var deferred = _q.defer();
-        _s3.getObject(_s3_param, function(error, data) {
-            if (error) {
-                deferred.reject(error);
-            } else {
-                deferred.resolve(data);
-            }
-        });
-
-        return deferred.promise;
-    },
-    _s3_put = function(data) {
-        var tmp_param = Object.assign({}, _s3_param);
-        var deferred = _q.defer();
-
-        tmp_param.Body = data;
-        tmp_param.ACL = 'public-read';
-        tmp_param.ContentType = 'text/plain';
+/*
+ * set response for callback
+ */
+const setResp = (msg, detail) => {
+    respArr.push({
+        msg: msg,
+        detail: detail
+    });
+};
 
-        _s3.putObject(tmp_param, function(error, data) {
-            if (error) {
-                deferred.reject(error);
+/*
+ * clear response for callback
+ */
+const unsetResp = () => {
+    respArr.length = 0;
+};
+
+// updating ip address information into DynamoDB
+const updateDBTable = (findingId, ip, lastSeen) => {
+    return new Promise((resolve, reject) => {
+        let params = {
+            TableName: process.env.DDB_TABLE_NAME,
+            Key: {
+                finding_id: findingId,
+                ip: ip
+            },
+            ExpressionAttributeNames: {
+                '#last_seen': 'last_seen'
+            },
+            ExpressionAttributeValues: {
+                ':last_seen': lastSeen,
+                ':n_one': 1
+            },
+            UpdateExpression: 'SET #last_seen = :last_seen ADD detection_count :n_one'
+        };
+
+        docClient.update(params, function(err, data) {
+            if (err) {
+                console.log('called updateDBTable and returned with error:', err.stack);
+                reject('Unable to Update ip into DynamoDB Table.');
             } else {
-                deferred.resolve(data);
+                console.log('called updateDBTable: ' +
+                    `finding entry (${findingId}) updated into DB.`);
+                resolve(data);
             }
         });
-
-        return deferred.promise;
-    },
-    _set_resp = function(msg, detail) {
-        _resp.push({
-            msg: msg,
-            detail: detail
-        });
-    },
-    _unset_resp = function() {
-        _resp = [];
-    },
-
-    _build_ip_list = function(blacklist, ip) {
-        _found = [];
-        _added = [];
-        var out = '';
-        if (ip) {
-            if (ip && blacklist.indexOf(ip) < 0) {
-                blacklist += ip + "\r\n";
-                _added.push(ip);
-                _found.push(ip);
-            } else if (ip) {
-                _found.push(ip);
-            }
-        }
-        return blacklist;
-    },
-
-    handler = function(event, context, handler_cb) {
-        var min_severity = process.env.MIN_SEVERITY || 3,
-            detail = event.fetch('detail') || {},
-            ip = detail.fetch('service/action/networkConnectionAction/remoteIpDetails/ipAddressV4'),
-            direction = detail.fetch('service/action/networkConnectionAction/connectionDirection'),
-            threat_list_name = detail.fetch('service/additionalInfo/threatListName'),
-            blacklist = null;
-
-        _unset_resp();
-
-        if (!ip) {
-
-            _set_resp('IP not found', null);
-            handler_cb(null, _resp);
-
-        } else if(direction == 'OUTBOUND') {
-
-            _set_resp('Ignore OUTBOUND connection', null);
-            handler_cb(null, _resp);
-
-        } else if(direction == 'UNKNOWN' && !threat_list_name) {
-
-            _set_resp('Ignore UNKNOWN connection due to undefined threat list name', null);
-            handler_cb(null, _resp);
-
-        } else if (detail.severity >= min_severity) {
-
-            _s3_get()
-            .then(
-                function(data) {
-                    blacklist = _build_ip_list(data.Body.toString('ascii'), ip);
-                },
-                function(error) {
-                    if (error.fetch('statusCode') === 404) {
-                        blacklist = _build_ip_list('', ip);
-                        _set_resp('Create new blacklist.', null);
-                    } else {
-                        _set_resp('Get blacklist error.', error);
-                    }
-                }
-            ).then(
-                function() {
-                    return _s3_put(blacklist);
-                }
-            ).then(
-                function(data) {
-
-                    var msg = _found.length + ' IP addresses found, and '
-                            + _added.length + ' new IP addresses have been added to blacklist.';
-
-                    _set_resp(msg, {
-                        found: _found,
-                        added: _added,
-                        event: event
-                    });
-                },
-                function(error) {
-                    _set_resp('Put blacklist error', error);
-                }
-            ).done(function() {
-                console.log(JSON.stringify(_resp));
-                handler_cb(null, _resp);
-            });
-        } else {
-
-            _set_resp('Ignore due to severity less than ' + min_severity, null);
-            handler_cb(null, _resp);
-        }
+    });
+};
+
+exports.handler = async (event, context, callback) => {
+    const AWS = require('aws-sdk');
+    // locking API versions
+    AWS.config.apiVersions = {
+        lambda: '2015-03-31',
+        s3: '2006-03-01',
+        dynamodb: '2012-08-10',
+        dynamodbstreams: '2012-08-10'
     };
 
-    return {
-        handler: handler
-    };
-}();
-
-exports.handler = monitor.handler;
+    unsetResp();
+
+    // verify all required process env variables
+    // check and set AWS region
+    if (!process.env.REGION) {
+        setResp('Must specify an AWS region.', null);
+        callback(null, respArr);
+        return;
+    }
+
+    if (!process.env.DDB_TABLE_NAME) {
+        setResp('Must specify an AWS DB Table name.', null);
+        callback(null, respArr);
+        return;
+    }
+
+    AWS.config.update({
+        region: process.env.REGION
+    });
+
+    docClient = new AWS.DynamoDB.DocumentClient();
+
+    const minSeverity = process.env.minSeverity || 3,
+        detail = objectUtils.fetch(event, 'detail') || {},
+        ip = objectUtils.fetch(detail,
+            'service/action/networkConnectionAction/remoteIpDetails/ipAddressV4'),
+        direction = objectUtils.fetch(detail,
+            'service/action/networkConnectionAction/connectionDirection'),
+        threatListName = objectUtils.fetch(detail,
+            'service/additionalInfo/threatListName'),
+        findingId = objectUtils.fetch(event, 'id'),
+        lastSeen = objectUtils.fetch(detail, 'service/eventLastSeen');
+
+    if (!ip) {
+
+        setResp('IP not found', null);
+        callback(null, respArr);
+
+    } else if (direction === 'OUTBOUND') {
+
+        setResp('Ignore OUTBOUND connection', null);
+        callback(null, respArr);
+
+    } else if (direction === 'UNKNOWN' && !threatListName) {
+
+        setResp('Ignore UNKNOWN connection due to undefined threat list name', null);
+        callback(null, respArr);
+
+    } else if (detail.severity >= minSeverity) {
+        try {
+            await updateDBTable(findingId, ip, lastSeen);
+            setResp(`finding entry (${findingId}) updated into DB.`, null);
+        } catch (err) {
+            setResp('There\'s a problem in updating ip to the DB. Please' +
+                ' see detailed information in CloudWatch logs.', null);
+        } finally {
+            callback(null, respArr);
+        }
+    } else {
 
+        setResp(`Ignore due to severity less than ${minSeverity}`, null);
+        callback(null, respArr);
+    }
+};
diff --git a/package.json b/package.json
index 19daae2..80ed6ed 100644
--- a/package.json
+++ b/package.json
@@ -4,18 +4,25 @@
   "description": "Lambda function to be called in CloudWatch when GuardDuty sends logs to CloudWatch. This script will write the malicious IP to a dedicated file in an S3 bucket. Firewall service (i.e. FortiOS) can pull this list, and add those malicious IPs to the blacklist",
   "main": "monitor.js",
   "files_to_deploy": [
+    "utils/",
     "monitor.js",
+    "generator.js",
+    "index.js",
     "package.json",
     "package-lock.json"
   ],
   "dependencies": {
-    "q": "^1.5.1"
   },
   "devDependencies": {
     "dpl": "^3.8.0",
+    "eslint": "^5.2.0",
+    "jasmine": "^3.1.0",
+    "jasmine-node": "^1.15.0",
+    "request": "^2.87.0",
     "rimraf": "^2.6.2"
   },
   "scripts": {
+    "pretest": "./node_modules/.bin/eslint --fix *.js;",
     "deploy": "dpl",
     "build": "scripts/make_dist.js",
     "install": "node -e \"try { require('./scripts/install_notes.js') } catch (e) {}\""
diff --git a/scripts/install_notes.js b/scripts/install_notes.js
index f2397a2..3ffafe5 100644
--- a/scripts/install_notes.js
+++ b/scripts/install_notes.js
@@ -1,6 +1,8 @@
+'use strict';
+
 require('dpl');
 var notes = [
     'run "npm run build" to create the distribution zip file',
     'run "npm run deploy" to deploy to AWS directly'
 ];
-console.log(notes.join('\n') + '\n');
+console.log(`${notes.join('\n')}\n`);
diff --git a/scripts/make_dist.js b/scripts/make_dist.js
index 2c02117..1418695 100755
--- a/scripts/make_dist.js
+++ b/scripts/make_dist.js
@@ -1,4 +1,7 @@
 #!/usr/bin/env node
+
+'use strict';
+
 console.log('Making distribution zip package');
 var pkg = require('../package.json'),
     os = require('os'),
@@ -7,20 +10,22 @@ var pkg = require('../package.json'),
     rimraf = require('rimraf');
 
 process.env.TMPDIR = fs
-    .mkdtempSync(path.join(process.env.TMPDIR || os.tmpdir(), pkg.name + '-')) + path.sep;
+    .mkdtempSync(path.join(process.env.TMPDIR || os.tmpdir(), `${pkg.name}-`)) + path.sep;
 
 // Shorter version of node_modules/dpl/dpl.js which avoids the 'upload' phase
 
 var dpl = require('dpl/lib/index.js');
 // 'upload' into the ./dist folder instead.
 dpl.upload = function() {
-    var fileName = pkg.name + '.zip';
+    var fileName = `${pkg.name}.zip`;
     var zipFile = path.normalize(process.env.TMPDIR + fileName);
     var distDir = path.normalize(path.join(__dirname, '..', 'dist'));
-    try { fs.mkdirSync(distDir); } catch (ex) {}
+    try {
+        fs.mkdirSync(distDir);
+    } catch (ex) {}
     copyFile(zipFile, path.join(distDir, fileName), function() {
         rimraf.sync(path.dirname(zipFile));
-        console.log('zipped to ' + path.relative(process.cwd(), path.join(distDir, fileName)));
+        console.log(`zipped to ${path.relative(process.cwd(), path.join(distDir, fileName))}`);
     });
 };
 require('dpl/dpl.js');
@@ -29,4 +34,4 @@ function copyFile(src, dest, cb) {
     fs.createReadStream(src).pipe(fs.createWriteStream(dest))
         .on('error', console.error)
         .on('close', cb);
-}
\ No newline at end of file
+}
diff --git a/utils/ObjectUtils.js b/utils/ObjectUtils.js
new file mode 100644
index 0000000..135c5e2
--- /dev/null
+++ b/utils/ObjectUtils.js
@@ -0,0 +1,24 @@
+'use strict';
+
+/*
+Object operation utility class.
+*/
+let ObjectUtils = {};
+
+ObjectUtils.fetch = function(obj, path) {
+    var keys = path.split('/');
+    var tmp = obj;
+
+    for (var i = 0, size = keys.length; i < size; i++) {
+        var k = keys[i];
+        if (tmp.hasOwnProperty(k)) {
+            tmp = tmp[k];
+        } else if (typeof tmp[k] === 'undefined') {
+            return null;
+        }
+    }
+
+    return tmp;
+};
+
+module.exports = ObjectUtils;