UNPKG

spartan-shield

Version:

nodejs project to package and configure common security middleware.

458 lines (457 loc) 16.2 kB
'use strict' let fs = require('fs'), secJson = require('./security.json'), logLocation = secJson.loggingPolicy.logCollection.storage, label = 'SECURITY EVENT', timestamp = require('time-stamp'), id = require('shortid'), createCsvWriter = require('csv-writer').createObjectCsvWriter, csvWriter = createCsvWriter({ path: `${logLocation}/${timestamp.utc('YYYY-MM-DD')}-logs.csv`, header: [{ id: 'timestamp', title: 'Timestamp' }, { id: 'id', title: 'ID' }, { id: 'label', title: 'Label' }, { id: 'class', title: 'Class' }, { id: 'subclass', title: 'Subclass' }, { id: 'type', title: 'Type' }, { id: 'context', title: 'Description' }, { id: 'message', title: 'Message' } ], append: true }), targz = require('targz'), rSched, mSize, compressionSetting, encryptionSetting, nodemailer = require('nodemailer'), colors = require('colors'), counter = 0 const getCount = () => { return counter } const setCount = (value) => { counter = counter + value } const auditor = (auditFilePath, message, id, tstamp, classType, subclass, type, action) => { let auditWriter = createCsvWriter({ path : auditFilePath, header: [ {id : 'id', title: 'id'}, {id : 'timestamp', title: 'timestamp'}, {id : 'action', title: 'action'}, {id: 'class', title: 'class'}, {id: 'subclass', title: 'subclass'}, {id: 'type', title: 'type'}, {id: 'message', title: 'message'} ], append: true }), audits = [], auditRecord = { id : id, timestamp : tstamp, action : action, class : classType, subclass, subclass, type : type, message : message } audits.push(auditRecord) if(!fs.existsSync(auditFilePath)) { let stream = fs.createWriteStream(auditFilePath) stream.write('ID, TIMESTAMP, ACTION, CLASS, SUBCLASS, TYPE, MESSAGE') stream.close() } auditWriter.writeRecords(audits).then(() => { // console.log(`Executed ${action} on ${auditFilePath} at ${timestamp.utc('YYYY-MM-DD.HH:mm:ms')}`) }).catch(e => { let m = `Action ${action} was not recorded to ${auditFilePath} due to an error, ${e.message}` console.log(m) }) } const checkPath = (pathToCheck) => { if(!fs.existsSync(pathToCheck)) { let stream = fs.createWriteStream(pathToCheck) stream.write(" \n") stream.close() } } const filterData = (details, filters) => { for (let d in details) { for (let f in filters) { if (typeof details[d] === 'string' && details[d].match(filters[f][0])) { details[d] = details[d].replace(filters[f][0], filters[f][1]) } } } return details } const decompress = (src, dest) => { targz.decompress({ src: src, dest: dest }, (err => { if (err) console.log(err) })) } const compress = (src, dest) => { targz.compress({ src: src, dest: dest }, (err => { if (err) console.log(err) })) } /** * @name changeLogs * @description checks the rotation schedule set on the logger and returns the timestamp prefix matching the schedule specs * @returns {TimeRanges} timestamp string value */ const changeLogs = (sched) => { let append switch(sched) { case 'yearly' : append = timestamp.utc('[YYYY]') break case 'monthly' : append = timestamp.utc('[YYYY-MM]') break case 'weekly' : let today = new Date(), diff = today.getDate() - today.getDay() + (today.getDay() === 0 ? -6 : 0), startWeek = new Date(today.setDate(diff)) append = timestamp.utc(`[${startWeek.getUTCFullYear()}-${startWeek.getUTCMonth()}-${startWeek.getUTCDate()}]`) break case 'hourly' : append = timestamp.utc('[YYYY-MM-DD.HH]') break default : append = timestamp.utc('[YYYY-MM-DD]') } return append } class LogWriter { constructor(options) { this.console = options.console this.file = options.file this.filters = options.filters } /** * @name logSettings * @description fetches existing settings on the logger */ get logSettings() { return { console : this.console, file : this.file, filters : this.filters } } /** * @name logSettings * @description overloads existing settings for the logger and applies new settings * @param {Object} settings object with console, file and filter options set */ set logSettings (settings) { this.console = settings.console, this.file = settings.file this.filters = settings.filters } get compression () { return this._compression } set compression (c) { this._compression = c } get rotateLogs () { return this._rotationSchedule } set rotateLogs (sched) { this._rotationSchedule = sched } get maxFileSize () { return this._maxSize } set maxFileSize (m) { this._maxSize = m } /** * @description class method to enable or disable log encryption prior to storage, requires a private/public key pair * @param {Boolean} enc * @param {String} pathToKey path to encryption key (if using public/private key pair, the PUBLIC key goes here) * @param {String} alg encryption algorithm to be used * @param {Function} callback collects errors and messages * @returns {Function} */ setEncryption (enc, pathToKey, alg, callback) { encryptionSetting = enc this._encryption = enc } /** * @name setCompression * @description class method to enable or disable compression * @param {Boolean} comp */ setCompression (comp) { compressionSetting = comp this._compression = comp } setLogRotation(sched) { rSched = sched this._rotationSchedule = sched } /** * @name setMaxFileSize * @description class method to set the max file size of logs * @param {Number} maxSize * @returns void */ setMaxFileSize (maxSize) { mSize = maxSize this._maxSize = maxSize } /** * @name eventBuilder * @description allows users to specify their own security event criteria * @param {Object} eventCriteria object containing : event name, levels to watch for, event label, and structure. Structure is an open-ended object, but can contain criteria name, requirement, expected type, and associated components to capture * @returns {Function} returns a function with the properties specified */ eventBuilder(eventCriteria) { try { let csvMetaData = {} if (!eventCriteria) { let err = new Error('logging/missing-criteria') err.code = 'logging/missing-criteria' err.message = 'Criteria for event structure is required but was not found.' throw err } // name & label are required if (!eventCriteria.name) { let err = new Error('logging/missing-name') err.code = 'logging/missing-name' err.message = 'A name is required for the event you want to define' throw err } else if (!eventCriteria.label) { let err = new Error('logging/missing-label') err.code = 'logging/missing-label' err.message = 'A label is required for the event you want to define' throw err } else if (!eventCriteria.structure) { let err = new Error('logging/missing-event-structure') err.code = 'logging/missing-event-structure' err.message = 'The compnents of the event to be logged must be defined' throw err } else { // nothing } if (eventCriteria.recordPath) { checkPath(eventCriteria.recordPath) csvMetaData.path = eventCriteria.recordPath } if (eventCriteria.append === true) { csvMetaData.append = true } // build headers for the csv file let header = [] header.push({id : 'timestamp', name: 'timestamp'}) header.push({ id: 'id', name: 'id' }) header.push({ id: 'label', name: 'label' }) for (let k in eventCriteria.structure) { let newObj = { id: (eventCriteria.structure[k].name).toString(), title: (eventCriteria.structure[k].name).toString() } header.push(newObj) } csvMetaData.header = header return { [eventCriteria.name] : (d, f) => { try { let w = createCsvWriter(csvMetaData), details = [] d = filterData(d,f) d['timestamp'] = timestamp.utc('YYYY-MM-DD.HHmm.ms') d['id'] = id.generate() d['label'] = eventCriteria.label for (let i in eventCriteria.structure) { if (eventCriteria.structure[i].required === true && !Object.keys(d).includes(eventCriteria.structure[i].name)) { let err = new Error('logging/missing-required-fields') err.message = `Field ${eventCriteria.structure[i].name} is required and cannot be undefined` err.code = 'logging/missing-required-fields' throw err } if (typeof d[eventCriteria.structure[i].name] === 'function') { let err = new Error('logging/dangerous-practice') err.message = `Type of ${eventCriteria.structure[i].name} appears to be a function. This exposes the application to arbitrary code execution vulnerabilities upon log parsing.` err.code = 'logging/dangerous-practice' throw err } if (typeof d[eventCriteria.structure[i].name] === 'object') { d[eventCriteria.structure[i].name] = JSON.stringify(d[eventCriteria.structure[i].name]) let c = d[eventCriteria.structure[i].name] for (let h in c) { if (typeof c[h] === 'function') { let err = new Error('logging/dangerous-practice') err.message = `Type of ${eventCriteria.structure[i].name} appears to be a function. This exposes the application to arbitrary code execution vulnerabilities upon log parsing.` err.code = 'logging/dangerous-practice' throw err } } } if (d[eventCriteria.structure[i].name] instanceof Array) { for (let l = 0; l < d[eventCriteria.structure[i].name].length; l++) { if (typeof d[eventCriteria.structure[i].name][l] === 'function') { let err = new Error('logging/dangerous-practice') err.message = `Type of ${eventCriteria.structure[i].name} appears to be a function. This exposes the application to arbitrary code execution vulnerabilities upon log parsing.` err.code = 'logging/dangerous-practice' throw err } } } details.push(d) // if (eventCriteria.compress) { // decompress(`${path.dirname(csvMetaData.path)}/${path.basename(csvMetaData.path)}.tar.gz`, // path.dirname(csvMetaData.path)) // } w.writeRecords(details).then(() => { // if (eventCriteria.compress) { // compress(path.dirname(csvMetaData.path), // `${path.dirname(csvMetaData.path)}${path.basename(csvMetaData.path)}-log.tar.gz`) // } console.log(`Whew! Wrote to Log File`) }).catch(e => {throw e}) } } catch (e) { throw e } } } } catch (e) { throw e } } writer(details) { details = filterData(details, this.filters) let arr = [], time = this._rotationSchedule ? changeLogs(this._rotationSchedule) : timestamp.utc('YYYY-MM-DD') details['label'] = label details['timestamp'] = `[${timestamp.utc('YYYY-MM-DD')}T${timestamp.utc('HH:mm:ss.ms')}]` details['id'] = id.generate() arr.push(details) if (this.console === true) { console.log(details) } if (this.file === true) { checkPath(`${logLocation}/${time}-logs.csv`) // if(this._compression === true) { // decompress(`${logLocation}/${time}-log.tar.gz`, // `${logLocation}/`) // } csvWriter.writeRecords(arr).then(() => { // compress(`${logLocation}/`, // `${logLocation}/${time}-log.tar.gz`) auditor(`${logLocation}/${timestamp.utc('YYYY-MM-DD')}-audit.csv`, `Accessed file ${logLocation}/${time}-logs.csv`, details.id, details.timestamp, details.class, details.subclass, details.type, 'WRITE') }).catch(e => { let m = `Could not write to file ${logLocation}/${time}-logs.csv due to an error, ${e.message}` console.log(m) }) } } } /** * @name Bullhorn * @description watches and notifies specified recipient of identified event occurences * @param {Object} options - name & notification methods (email, console) */ class Bullhorn { constructor(options) { this.name = options.name this.method = options.method } consoler (subject, content) { if(content instanceof Object) { content = JSON.stringify(content) } let message = ` ATTENTION: \n -----------\n You asked to be notified of the following condition ${subject}\n which occurred at ${new Date()}\n Here are the details : ${content}\n Please consult the logs at ${logLocation} for more information` console.log(message.yellow) return message } messageContent(subject, content) { if (content instanceof Object) { content = JSON.stringify(content) } this.method.email.template.subject = subject this.method.email.template.text = content return this.method.email } mail(callback) { if(this.method.email) { let t = { service : this.method.email.transporter[0], auth : this.method.email.transporter[1] } let transporter = nodemailer.createTransport(t) transporter.sendMail(this.method.email.template, (err, info) => { if (err) { return callback(err, false, null)} else { let message = `here is the message id of the sent email: ${info.messageId}` return callback(null, info, message) } }) } } setConditions(conditions) { this._conditions = conditions /** * @description function to handle notification events * @param {Object} whatToWatch object of which details to include in the notification * @param {Number} [howMuchToCount=1] value of how much this notification should count. Set this to the max value in the threshold if you want to be notified instantly */ return (whatToWatch, howMuchToCount) => { if (howMuchToCount) { setCount(howMuchToCount) } else { setCount(1) } for (let w in whatToWatch) { conditions.details[w] = whatToWatch[w] } if (conditions.notifyOn.console && conditions.notifyOn.console <= counter) { this.consoler(conditions.name, conditions.details) if (conditions.resetOnNotify) { counter = 0 } } else { console.log(`There are ${conditions.notifyOn.console - counter} events left`) } if(conditions.notifyOn.email && conditions.notifyOn.email <= counter) { this.messageContent(conditions.name, conditions.details) this.mail((err, info, msg) => { if (err) console.log(err.message) else { console.log(msg) } }) if (conditions.resetOnNotify) { counter = 0 } } else { console.log(`There are ${conditions.notifyOn.email - counter} events left before the next escalation`) } } } } module.exports = { LogWriter: LogWriter, Bullhorn : Bullhorn }