Skip to content

Commit

Permalink
Added ability to filter globally. Closes #315.
Browse files Browse the repository at this point in the history
  • Loading branch information
arb committed Apr 10, 2015
1 parent 0ff67e6 commit 0fcfad8
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 8 deletions.
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -38,6 +38,14 @@ set `options` to an object with the following optional settings:
- `key` - one of the supported good events or any of the `extensions` events that this reporter should listen for
- `value` - a single string or an array of strings to filter incoming events. "\*" indicates no filtering. `null` and `undefined` are assumed to be "\*"
- `config` - an implementation specific configuration value used to instantiate the reporter
- `[filter]` - an object with the following keys:
- `key` - the key of the data property to change
- `value` - a string that can be one of the following:
- "censor" - replace the text with "X"s
- "remove" - `delete`s the value
- a valid regular express string. Only supports a single group. Ex: `"(\\d{4})$"` will replace the last four digits with "X"s. Take extra care when creating this string. You will need to make sure that the resultant RegExp object is what you need.

`filter` can be used to remove potentially sensitive information (credit card numbers, social security numbers, etc.) from the log payloads before they are sent out to reporters. This setting only impacts `response` events and only if payloads are included via `requestPayload` and `responsePayload`. `filter` is intended to impact the reporting of ALL downstream reporters. If you want filtering in only one, you will need to create a customized reporter.

## Reporter Interface

Expand Down
10 changes: 5 additions & 5 deletions lib/monitor.js
Expand Up @@ -31,7 +31,8 @@ internals.defaults = {
requestHeaders: false,
requestPayload: false,
responsePayload: false,
extensions: []
extensions: [],
filter: {}
};


Expand All @@ -45,9 +46,8 @@ module.exports = internals.Monitor = function (server, options) {
options = Hoek.applyToDefaultsWithShallow(internals.defaults, options, ['reporters', 'httpAgents', 'httpsAgents']);

// Force them to be arrays
var args = [];
options.httpAgents = args.concat(options.httpAgents || Http.globalAgent);
options.httpsAgents = args.concat(options.httpsAgents || Https.globalAgent);
options.httpAgents = [].concat(options.httpAgents || Http.globalAgent);
options.httpsAgents = [].concat(options.httpsAgents || Https.globalAgent);


this.settings = options;
Expand Down Expand Up @@ -226,7 +226,7 @@ internals.Monitor.prototype._errorHandler = function (request, error) {

internals.Monitor.prototype._responseHandler = function (request) {

this._dataStream.push(Utils.GreatResponse(request, this._responseOptions));
this._dataStream.push(Utils.GreatResponse(request, this._responseOptions, this.settings.filter));
};


Expand Down
3 changes: 2 additions & 1 deletion lib/schema.js
Expand Up @@ -20,5 +20,6 @@ internals.monitorOptions = Joi.object().keys({
opsInterval: Joi.number().integer().min(100),
reporters: Joi.array().includes(Joi.object(), Joi.string()).required().min(1),
responseEvent: Joi.string().valid('response', 'tail'),
extensions: Joi.array().includes(Joi.string().invalid('log', 'request-error', 'ops', 'request', 'response', 'tail'))
extensions: Joi.array().includes(Joi.string().invalid('log', 'request-error', 'ops', 'request', 'response', 'tail')),
filter: Joi.object().pattern(/./, Joi.string())
}).unknown(false);
41 changes: 39 additions & 2 deletions lib/utils.js
Expand Up @@ -67,15 +67,46 @@ exports.GreatError.prototype.toJSON = function () {


// Payload for "response" events
exports.GreatResponse = function (request, options) {
exports.GreatResponse = function (request, options, filterRules) {

if (this.constructor !== exports.GreatResponse) {
return new exports.GreatResponse(request, options);
return new exports.GreatResponse(request, options, filterRules);
}

var req = request.raw.req;
var res = request.raw.res;

var replacer = function (match, group1) {

return (new Array(group1.length + 1).join('X'));
};

var applyFilter = function (data) {

for (var key in data) {
if (typeof data[key] === 'object') {
return applyFilter(data[key]);
}

// there is a filer for this key, so we are going to update the data
if (filterRules[key]) {
var filter = filterRules[key].toLowerCase();

if (filter === 'censor') {
data[key] = ('' + data[key]).replace(/./gi, 'X');
}
else if (filter === 'remove') {
delete data[key];
}
// Means this is a string that needs to be turned into a RegEx
else {
var regex = new RegExp(filter);
data[key] = ('' + data[key]).replace(regex, replacer);
}
}
}
};

this.event = 'response';
this.timestamp = request.info.received;
this.id = request.id;
Expand Down Expand Up @@ -107,6 +138,12 @@ exports.GreatResponse = function (request, options) {
this.responsePayload = request.response.source;
}

if (Object.keys(filterRules).length) {

applyFilter(this.requestPayload);
applyFilter(this.responsePayload);
}

Object.freeze(this);
};

Expand Down
3 changes: 3 additions & 0 deletions test/index.js
Expand Up @@ -8,6 +8,9 @@ var Https = require('https');
var Lab = require('lab');
var Wreck = require('wreck');

// Done for testing because Wreck is a singleton and every test run ads one event to it
Wreck.setMaxListeners(0);

var GoodReporter = require('./helper');


Expand Down
89 changes: 89 additions & 0 deletions test/monitor.js
Expand Up @@ -476,6 +476,95 @@ describe('good', function () {
});
});

it('filters payloads per the filter rules', function (done) {

var server = new Hapi.Server();
server.connection({ host: 'localhost' });
server.route({
method: 'POST',
path: '/',
handler: function (request, reply) {

reply({
first: 'John',
last: 'Smith',
ssn: 'ABCDEFG',
ccn: '9999999999',
userId: 555645465,
address: {
street: '123 Main Street',
city: 'Pittsburgh',
last: 'Jones'
}
});
}
});

var one = new GoodReporter({ response: '*' });
var plugin = {
register: require('../lib/index').register,
options: {
reporters: [one],
requestPayload: true,
responsePayload: true,
filter: {
ssn: 'remove',
last: 'censor',
password: 'censor',
email: 'remove',
ccn: '(\\d{4})$',
userId: '(645)',
street: 'censor',
city: '(\\w?)'
}
}
};

server.register(plugin, function () {

server.start(function () {

var req = Http.request({
hostname: '127.0.0.1',
port: server.info.port,
method: 'POST',
path: '/',
headers: {
'Content-Type': 'application/json'
}
}, function (res) {

var messages = one.messages;
var response = messages[0];

expect(res.statusCode).to.equal(200);
expect(messages).to.have.length(1);
expect(response.requestPayload).to.deep.equal({
password: 'XXXXX'
});
expect(response.responsePayload).to.deep.equal({
first: 'John',
last: 'XXXXX',
ccn: '999999XXXX',
userId: '555XXX465',
address: {
street: 'XXXXXXXXXXXXXXX',
city: 'Xittsburgh',
last: 'XXXXX'
}
});
done();
});

req.write(JSON.stringify({
password: 12345,
email: 'adam@hapijs.com'
}));
req.end();
});
});
});

it('does not send an "ops" event if an error occurs during information gathering', function (done) {

var options = {
Expand Down

0 comments on commit 0fcfad8

Please sign in to comment.