├── .gitignore ├── README.md ├── index.js ├── lib ├── auditlog.js ├── plugins │ ├── express-api.js │ ├── express.js │ ├── mongoose.js │ └── passport.js ├── socketmonitor.js ├── stash │ └── socketClient.html └── transport │ ├── console.js │ ├── mongoose.js │ └── socket.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AuditLog 2 | 3 | Audit logging toolkit for Node.js. Choose your storage or notification strategy by utilizing one or 4 | more extendable transport systems. Automate log creation by utilizing plugins for common libraries such as 5 | Mongoose (CRUD logging via model plugin) and Express (access logging via route middleware). 6 | 7 | 8 | # How To Use It 9 | **0. Install the module:** 10 | 11 | Use NPM to install the module, like so: 12 | 13 | npm install audit-log 14 | 15 | **1. Include the library:** 16 | 17 | ```javascript 18 | var auditLog = require('audit-log'); 19 | ``` 20 | 21 | **2. Add a Transport System:** 22 | 23 | Different transports may have varying options. You should read their documentation for details. 24 | The Mongoose and Console transports are described in this example. 25 | ```javascript 26 | auditLog.addTransport("mongoose", {connectionString: "mongodb://localhost:27017/myDatabase"}); 27 | // either or both -- up to you where your messages are sent! 28 | auditLog.addTransport("console"); 29 | ``` 30 | 31 | **3a. Log an Event** 32 | 33 | Events are the usual structure for log events. 34 | ```javascript 35 | // method is logEvent( actor, origin, action, label, object, description ) 36 | 37 | // descriptive parameters for your reading pleasure: 38 | auditLog.logEvent('user id or something', 'maybe script name or function', 'what just happened', 'the affected target name perhaps', 'target id', 'additional info, JSON, etc.'); 39 | 40 | // a more realistic example: 41 | auditLog.logEvent(95049, 'AppServer', 'Shutdown', 'Production-3 Instance', 'ec2-255-255-255-255', 'Terminated from web console.'); 42 | ``` 43 | 44 | **3b. Log another kind of message of your devising** 45 | 46 | You can make up another type of message and give it a descriptive label, if you so desire... 47 | ```javascript 48 | auditLog.log({logType:'Warning', text:'An error occurred and you should fix it.', datetime:'2013-01-31 13:15:02', traceData:'...'}); 49 | 50 | // note: 51 | auditLog.log({message:'Call your mother.'}); 52 | // will send this to your active transports: 53 | // { logType: 'Generic', message: 'Call your mother.' } 54 | // because logType is needed by AuditLog to identify handlers. 55 | ``` 56 | 57 | **4. Use a Plugin to Automatically Send Messages** 58 | 59 | There are some plugins already available which ship with AuditLog, including Mongoose CRUD logging and Express route logging. 60 | 61 | 62 | **Addendum** 63 | 64 | It's usually a good idea to check any documentation in the Transports and Plugins, because they can vary a fair amount, 65 | and might be specifically written to handle a specific *logType*. 66 | 67 | 68 | # Plugins 69 | 70 | ## Express 71 | Log requests, and automatically assign an actor to events by checking session variables. 72 | 73 | ### Usage Example 74 | ```javascript 75 | // setup the plugin 76 | var auditLogExpress = auditLog.getPlugin('express', { 77 | userIdPath:['user','_id'], 78 | whiteListPaths:[/^\/some\/particular\/path.*$/] 79 | }); 80 | 81 | // use it in your Express app 82 | app.use(auditLogExpress.middleware); 83 | ``` 84 | 85 | ## Mongoose 86 | Log MongoDB database activity by adding this plugin to your models. 87 | 88 | ### Usage Example 89 | ```javascript 90 | // Here's the mongoose plugin being used on a Model 91 | var mongoose = require('mongoose'), 92 | Schema = mongoose.Schema 93 | auditLog = require('audit-log'); 94 | 95 | var HumanSchema = new Schema({ 96 | name: { type: String }, 97 | height: { type: Number } 98 | }); 99 | 100 | var pluginFn = auditLog.getPlugin('mongoose', {modelName:'Human', namePath:'name'}); // setup occurs here 101 | HumanSchema.plugin(pluginFn.handler); // .handler is the pluggable function for mongoose in this case 102 | ``` 103 | 104 | # Roadmap 105 | 106 | + Adding a Redis Transport 107 | + Log archival and pruning 108 | + Socket-driven live log monitoring API 109 | 110 | 111 | # License 112 | 113 | (The MIT License) 114 | 115 | Copyright (c) 2013 Craig Coffman 116 | 117 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 118 | 119 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 120 | 121 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 122 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./lib/auditlog'); -------------------------------------------------------------------------------- /lib/auditlog.js: -------------------------------------------------------------------------------- 1 | // Module dependencies 2 | var async = require('async'), 3 | util = require('util'); 4 | 5 | 6 | /* AuditLog 7 | * 8 | * Interface: 9 | * - addTransport : add a storage or output methodology 10 | * - logEvent : provides the preferred fields for event-type data as arguments 11 | * - log : free-form logging mechanism 12 | * 13 | */ 14 | function AuditLog() { 15 | this._transports = []; 16 | this._plugins = []; 17 | this._debug = false; 18 | this._userId = null; 19 | 20 | // The Highlander 21 | if ( AuditLog.prototype._singletonInstance ) { 22 | return AuditLog.prototype._singletonInstance; 23 | } 24 | AuditLog.prototype._singletonInstance = this; 25 | 26 | 27 | /* addTransport( transport, options) 28 | * 29 | * Add a transport methodology by its name. 30 | * Note: you could add more than one of the same type of transport, 31 | * they will not overwrite each other, but work in parallel. 32 | * 33 | * - transport : string name of transport methodology 34 | * - options : object containing any setup properties 35 | * 36 | */ 37 | this.addTransport = function(transport, options) { 38 | options = options || {}; 39 | if(typeof options.debug == 'undefined') options.debug = this._debug; // debug inheritance 40 | 41 | this.debugMessage('added '+(typeof options != 'undefined' ? ' with options '+util.inspect(options) : ''), transport) 42 | 43 | var myTransport = require('./transport/'+transport); 44 | this._transports.push(new myTransport(options)); 45 | } 46 | 47 | 48 | /* log( logData ) 49 | * 50 | * If you are specifying your own structure, use this method. 51 | * If you do not specify a 'logType' property, it will be 'Generic' 52 | * 53 | */ 54 | this.log = function( logData ) { 55 | logData.logType || (logData.logType = 'Generic'); 56 | 57 | return this.emitData( logData ); 58 | } 59 | 60 | 61 | /* logEvent( actor, action, label, object, description ) 62 | * 63 | * For direct, manual logging of events, use this "public" function. 64 | * 65 | * - actor : represents the initiator of this event (e.g., a user id, process name, etc.) 66 | * - origin : the source plugin of the event 67 | * - action : what action the event represents 68 | * - label : what type of entity the object is (e.g., 'users', a route, etc.) 69 | * - object : the object that was affected or the target recipient of this event 70 | * - description : additional information (e.g., 'changed password, name' or JSON data) 71 | * 72 | * examples: 73 | * 74 | * actor - date - origin - action - label - object - description 75 | * - 10:30am - mongoose - update - users - - changed password, name 76 | * - 10:25am - route - GET - /manager/users/edit - - n/a 77 | * 78 | */ 79 | this.logEvent = function( actor, origin, action, label, object, description ) { 80 | 81 | if(!actor && this._userId) actor = this._userId; // set the actor if it wasn't provided. 82 | 83 | var eventPackage = this.buildEventPackage(actor, new Date(), origin, action, label, object, description); 84 | 85 | return this.emitData(eventPackage); 86 | } 87 | 88 | 89 | /* emitData( dataObject ) 90 | * 91 | * Emits the log data to all transports in use. 92 | * used by the manual logEvent call and any logging handled by plugins. 93 | * 94 | * dataObject 95 | */ 96 | this.emitData = function( dataObject ) { 97 | async.forEach(this._transports, function(transport, cb) { 98 | transport.emit( dataObject ); 99 | cb(null); 100 | }, function(err) { 101 | return true; 102 | }); 103 | } 104 | 105 | 106 | /* buildEventPackage( actor, date, origin, action, label, object, description ) 107 | * 108 | * returns an object containing event options for transmission to transports 109 | */ 110 | this.buildEventPackage = function(actor, date, origin, action, label, object, description) { 111 | var defaults = { actor: '', date: new Date(), origin: '', action: '', 112 | label: '', object: '', description: '' }; 113 | 114 | return { 115 | logType: 'Event', 116 | actor: actor || defaults.actor, 117 | date: date || defaults.date, 118 | origin: origin || defaults.origin, 119 | action: action || defaults.action, 120 | label: label || defaults.label, 121 | object: object || defaults.object, 122 | description: description || defaults.description 123 | }; 124 | } 125 | 126 | 127 | /* getPlugin( pluginName, options ) 128 | * 129 | * returns an instance of the plugin you 130 | * 131 | */ 132 | this.getPlugin = function(pluginName, options) { 133 | var plugin = require('./plugins/'+pluginName); 134 | if(plugin) { 135 | options = options || {}; 136 | if(typeof options.debug == 'undefined') options.debug = this._debug; // debug inheritance 137 | options.auditLog = this; 138 | var myInstance = new plugin(options); 139 | return myInstance; 140 | } else { 141 | return false; 142 | } 143 | } 144 | 145 | 146 | /* debugMessage( msg [, source] ) 147 | * 148 | * shows a console log message if debug is true. 149 | * 150 | */ 151 | this.debugMessage = function(msg, source) { 152 | if(this._debug) { 153 | var fullMessage = 'Audits-Log'; 154 | if(typeof source != 'undefined') { 155 | fullMessage += '('+source+')'; 156 | } 157 | fullMessage += ': '+msg; 158 | console.log(fullMessage); 159 | } 160 | } 161 | 162 | this.socketMonitor = require('./socketmonitor.js'); 163 | } 164 | 165 | // expose yourself. 166 | 167 | exports = module.exports = new AuditLog(); -------------------------------------------------------------------------------- /lib/plugins/express-api.js: -------------------------------------------------------------------------------- 1 | /* ExpressAPIPlugin 2 | * 3 | * A plugin middleware for AuditLog and Express that exposes endpoints containing JSON log data. 4 | * 5 | */ 6 | var ExpressAPIPlugin = function(options) { 7 | 8 | this._options = { 9 | auditLog:null // instance of AuditLog 10 | }; 11 | 12 | //override default options with the provided values 13 | if(typeof options !== 'undefined') { 14 | for(var attr in options) { 15 | this._options[attr] = options[attr]; 16 | } 17 | } 18 | 19 | var self = this; 20 | 21 | /* middleware 22 | * 23 | * This is standard Express middleware that will add routes for api methods 24 | * 25 | */ 26 | this.middleware = function(req, res, next) { 27 | var method = req.method, 28 | path = req.url; 29 | 30 | return next(); 31 | } 32 | }; 33 | 34 | exports = module.exports = ExpressAPIPlugin; -------------------------------------------------------------------------------- /lib/plugins/express.js: -------------------------------------------------------------------------------- 1 | /* ExpressPlugin 2 | * 3 | * A plugin middleware for AuditLog and Express that automates the propagation of event logging for Express requests 4 | * 5 | * whitelist and blacklist can contain: 6 | * - regular expressions describing paths, or 7 | * - objects with this structure: 8 | * { 9 | * regex: /^\/my\/interesting\/path\/.*$/, // a path description 10 | * methods: ['GET', 'POST', 'PUT', 'DELETE'] // which http methods are allowed 11 | * } 12 | */ 13 | var ExpressPlugin = function(options) { 14 | 15 | this._userId = null; 16 | this._options = { 17 | auditLog:null, // instance of AuditLog 18 | userIdPath:null, // tell the plugin how to find the current user's ID in the request parameter. string or array of property names. 19 | whiteListPaths:[], // array of regular expressions for allowed paths. if none supplied, all paths are allowed. 20 | blackListPaths:[] // array of regular expressions for excluded paths. if none supplied, all paths are allowed. 21 | }; 22 | 23 | //override default options with the provided values 24 | if(typeof options !== 'undefined') { 25 | for(var attr in options) { 26 | this._options[attr] = options[attr]; 27 | } 28 | } 29 | 30 | var self = this; 31 | 32 | /* middleware 33 | * 34 | * This is standard Express middleware that will intercept requests and log them. 35 | * 36 | */ 37 | this.middleware = function(req, res, next) { 38 | var method = req.method, 39 | path = req.url, 40 | desc; 41 | 42 | // verify the path being requested is to be logged. 43 | if(!self.pathAllowed(path, method)) return next(); 44 | 45 | if(typeof self._options.userIdPath == 'string' && self._options.userIdPath.length) { 46 | // if the id path was a string, convert it to an array 47 | self._options.userIdPath = [self._options.userIdPath]; 48 | } 49 | if(self._options.userIdPath.length) { 50 | // loop through the user id path as long as the path exists 51 | self._userId = req; 52 | for(var keyIndex in self._options.userIdPath) { 53 | var key = self._options.userIdPath[keyIndex]; 54 | self._userId = self._userId[key] || false; 55 | if(!self._userId) break; 56 | } 57 | // pass the user id back to AuditLog for usage across other internal resources. 58 | if(self._userId) self._options.auditLog._userId = self._userId; 59 | } 60 | 61 | if(res.statusCode) 62 | desc = "statusCode: " + res.statusCode; 63 | 64 | self._options.auditLog.logEvent(self._userId, 'express', method, path, null, desc); // no object currently 65 | 66 | return next(); 67 | } 68 | 69 | 70 | /* pathAllowed( path, method ) 71 | * 72 | * Check the requested path and method against the whitelist and blacklist options, 73 | * return boolean representing whether logging this request is allowed. 74 | * 75 | */ 76 | this.pathAllowed = function(path, method) { 77 | var matched, i, x; 78 | 79 | if(self._options.whiteListPaths.length) { 80 | 81 | // if any whiteListPaths are set, the path must match at least one 82 | matched = false; 83 | 84 | whiteListCheck: 85 | for(i=0; i= 0) { 48 | description = JSON.stringify(docObj); 49 | } else { 50 | if(Object.prototype.hasOwnProperty.call(docObj, self._options.versionPath) && docObj[self._options.versionPath] == 0) { 51 | description = 'Created "'+docObj[self._options.namePath]+'"'; 52 | } else { 53 | description = 'Updated "'+docObj[self._options.namePath]+'"'; 54 | } 55 | } 56 | 57 | self._options.auditLog.logEvent(null, 'mongoose', 'save', self._options.modelName, docObj[self._options.idPath], description); 58 | }); 59 | 60 | /* 61 | * remove callback 62 | * 63 | */ 64 | schema.post('remove', function(doc) { 65 | var docObj = doc.toObject(), 66 | description = ''; 67 | 68 | if(self._options.storeDoc.indexOf('remove') >= 0) { 69 | description = JSON.stringify(docObj); 70 | } else { 71 | description = 'Removed "'+docObj[self._options.namePath]+'"'; 72 | } 73 | self._options.auditLog.logEvent(null, 'mongoose', 'remove', self._options.modelName, docObj[self._options.idPath], description); 74 | }); 75 | } 76 | } 77 | 78 | exports = module.exports = MongoosePlugin; -------------------------------------------------------------------------------- /lib/plugins/passport.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ccraigc/node-audit-log/7c523593fd36edd7204a8cb524a68e4f22f1bf31/lib/plugins/passport.js -------------------------------------------------------------------------------- /lib/socketmonitor.js: -------------------------------------------------------------------------------- 1 | function SocketMonitor() { 2 | 3 | this.io = require('socket.io'); 4 | 5 | this.listen = function(_options) { 6 | 7 | var options = { 8 | clientPort: 8181, 9 | socketPort: 8182 10 | }; 11 | 12 | // override default options with the provided values 13 | if(typeof _options !== 'undefined') { 14 | for(var attr in _options) { 15 | options[attr] = _options[attr]; 16 | } 17 | } 18 | 19 | io.listen(options.socketPort); 20 | 21 | io.sockets.on('connection', function (socket) { 22 | socket.emit('news', { hello: 'world' }); 23 | socket.on('my other event', function (data) { 24 | console.log(data); 25 | }); 26 | }); 27 | } 28 | } 29 | 30 | // expose yourself 31 | 32 | exports = module.exports = new SocketMonitor(); -------------------------------------------------------------------------------- /lib/stash/socketClient.html: -------------------------------------------------------------------------------- 1 | AuditLog Monitor 2 | 3 | 4 | 7 | 8 |

Event Monitor

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
actordateoriginactionlabelobjectdescription
...
29 | 43 | -------------------------------------------------------------------------------- /lib/transport/console.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | module.exports = function consoleTransport(options) { 4 | var _options = {}; 5 | if(typeof options !== 'undefined') this._options = options; 6 | 7 | this.name = 'console'; 8 | 9 | this.emit = function(dataObject) { 10 | console.log(util.inspect(dataObject)); 11 | } 12 | 13 | return this; 14 | } -------------------------------------------------------------------------------- /lib/transport/mongoose.js: -------------------------------------------------------------------------------- 1 | // Module dependencies 2 | var mongoose = require('mongoose'), 3 | Schema = mongoose.Schema, 4 | util = require('util'); 5 | 6 | /* MongooseTransport 7 | * 8 | * A MongoDB storage handler for Audit-Log for Node.js 9 | * 10 | */ 11 | MongooseTransport = function(options) { 12 | this.name = 'mongoose'; 13 | 14 | this._options = { collectionName:'auditLog', connectionString:'', debug: false }; 15 | this._connection; 16 | var self = this; 17 | 18 | // override default options with the provided values 19 | if(typeof options !== 'undefined') { 20 | for(var attr in options) { 21 | this._options[attr] = options[attr]; 22 | } 23 | } 24 | 25 | // attempt to setup the db connection 26 | this._connection = mongoose.createConnection(this._options.connectionString, function (err) { 27 | if (err) { 28 | this.debugMessage("could not connect to DB: " + err); 29 | } 30 | }); 31 | 32 | this.modelSchema = new Schema({ 33 | actor: {type:String}, 34 | date: {type:Date}, 35 | origin: {type:String}, 36 | action: {type:String}, 37 | label: {type:String}, 38 | object: {type:String}, 39 | description: {type:String} 40 | }); 41 | 42 | this.model = this._connection.model(this._options.collectionName, this.modelSchema); 43 | 44 | this.emit = function( dataObject ) { 45 | this.debugMessage('emit: '+util.inspect(dataObject)); 46 | 47 | if(dataObject.logType && dataObject.logType == 'Event') { 48 | var newEvent = new this.model( dataObject ); 49 | newEvent.save(function(err) { 50 | if(err) self.debugMessage('error saving event to database: '+err); 51 | }); 52 | } 53 | } 54 | 55 | 56 | this.debugMessage = function(msg) { if(this._options.debug) console.log('Audit-Log(mongoose): '+msg); } 57 | 58 | return this; 59 | } 60 | 61 | exports = module.exports = MongooseTransport; 62 | -------------------------------------------------------------------------------- /lib/transport/socket.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | fs = require('fs'), 3 | util = require('util'); 4 | 5 | module.exports = function socketTransport(options) { 6 | var _options = { 7 | socketPort: 8182, 8 | port: 8181 9 | }; 10 | 11 | // override default options with the provided values 12 | if(typeof options !== 'undefined') { 13 | for(var attr in options) { 14 | _options[attr] = options[attr]; 15 | } 16 | } 17 | 18 | var app = http.createServer(function(req, res) { 19 | 20 | console.log(req.url); 21 | 22 | if(/^\/(index\.html?)?$/.test(req.url)) { 23 | fs.readFile(__dirname+'/../stash/socketClient.html', function(error, content) { 24 | if (error) { 25 | console.log(error); 26 | res.writeHead(500); 27 | res.end(); 28 | } 29 | else { 30 | res.writeHead(200, { 'Content-Type': 'text/html' }); 31 | res.end(content, 'utf-8'); 32 | } 33 | }); 34 | } 35 | else if(0) { 36 | fs.readFile(__dirname+'/../stash/', function(error, content) {}); 37 | } 38 | else { 39 | res.writeHead(404); 40 | res.end(); 41 | } 42 | 43 | }); 44 | app.listen(_options.port); 45 | 46 | var io = require('socket.io').listen(app); 47 | io.sockets.on('connection', function (socket) {}); 48 | 49 | this.emit = function(dataObject) { 50 | io.sockets.emit('log', dataObject); 51 | console.log(util.inspect(dataObject)); 52 | } 53 | 54 | return this; 55 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audit-log", 3 | "description": "Node Audit Logging Toolkit", 4 | "version": "0.9.1", 5 | "keywords": [ 6 | "node", 7 | "log", 8 | "audit" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ccraigc/node-audit-log" 13 | }, 14 | "author": "Craig Coffman ", 15 | "main": "index", 16 | "dependencies": { 17 | "async": "0.1.22", 18 | "socket.io": "2.4.0" 19 | }, 20 | "engines": { 21 | "node": "*" 22 | }, 23 | "readmeFilename": "README.md" 24 | } 25 | --------------------------------------------------------------------------------