diff --git a/README.md b/README.md index 9184386..00174b4 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,32 @@ you can skip this (not recommanded) by setting the environment variable In addition, you can update the pattern on which to make the match with the environment variable `LOGGER_SENSITIVE_DATA_PATTERN`. Its value must represent a valid [capturing regular expression](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/RegExp#group_back). + +You also can add custom filter : +``` +const sensitiveDataFragment = '(pass|password)'; // Will obfuscate 'pass' and 'password' data + +const newLogger = init({ + logger: { + sensitiveDataFragment, + }, +}); +``` + +Moreover, you can add customize the way it replaces data : +``` +const sensitiveDataPattern = [ + { + regex: YOUR_NEW_REGEX, + substitute: SUBSTITUTION_CONTENT, + } +]; // Will replace data matching with new regex by substitute content + +const newLogger = init({ + logger: { + sensitiveDataPattern, + }, +}); +``` + +🚨 Process is synchronous, it means that `sensitiveDataPattern` can produce performance issues on large sizes. diff --git a/index.js b/index.js index e2c6a01..b5ba61a 100644 --- a/index.js +++ b/index.js @@ -49,7 +49,10 @@ function init(config) { } else if (finalConfig.logger.hideSensitiveData) { loggerConfig.streams.push({ level: loggerLevel, - stream: new SensitiveDataStream(finalConfig.logger.sensitiveDataPattern), + stream: new SensitiveDataStream( + finalConfig.logger.sensitiveDataFragment, + finalConfig.logger.sensitiveDataPattern + ), }); } else { loggerConfig.streams.push({ level: loggerLevel, stream: process.stdout }); diff --git a/package.json b/package.json index bca0c7a..f679e35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chpr-logger", - "version": "3.2.0", + "version": "4.2.0", "description": "Logger for NodeJS application stack", "main": "index.js", "directories": { diff --git a/streams/sensitive-data.js b/streams/sensitive-data.js index db8906b..d4f5a0c 100644 --- a/streams/sensitive-data.js +++ b/streams/sensitive-data.js @@ -4,12 +4,27 @@ const DEFAULT_SENSITIVE_DATA_FRAGMENTS = '(mdp|password|authorization|token|pwd|auth)'; module.exports = class SensitiveDataStream { - constructor(fragments) { + constructor(fragments, patterns = []) { this.fragments = fragments || DEFAULT_SENSITIVE_DATA_FRAGMENTS; - this.pattern = new RegExp(`"${this.fragments}":"([^"]*)"`, 'ig'); + this.replacer = '__SENSITIVE_DATA__'; + + this.patterns = [ + ...patterns, + { + // Default pattern + regex: new RegExp(`"${this.fragments}":"([^"]*)"`, 'ig'), // @Match "mdp":"My super password" + substitute: `"$1":"${this.replacer}"`, + }, + ]; } + write(input) { - const sanitized = input.replace(this.pattern, '"$1":"__SENSITIVE_DATA__"'); + let sanitized = input; + + // Apply replace on input looping through patterns array + for (let pattern of this.patterns) { + sanitized = sanitized.replace(pattern.regex, pattern.substitute); + } return process.stdout.write(sanitized); } diff --git a/test/index.test.js b/test/index.test.js index baf6546..a8a639f 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -154,15 +154,64 @@ describe('index.js', () => { req: { token: 'My personal token', }, + cmd: 'command docker build password=test', }, 'Logging amazingly sensitive data!' ); + + const message = logs.shift(); + + expect(message.password).to.equal('__SENSITIVE_DATA__'); + expect(message.headers['accept-language']).to.equal('fr-FR'); + expect(message.headers.authorization).to.equal('__SENSITIVE_DATA__'); + expect(message.req.token).to.equal('__SENSITIVE_DATA__'); + }); + + it('should replace good parts of string using custom replacement & fragment patterns', () => { + const sensitiveDataPattern = [ + { + regex: new RegExp(`(password)=([\\w-]*)`, 'ig'), + substitute: `$1=__SENSITIVE_DATA__`, + }, + ]; + + const newLogger = init({ + logger: { sensitiveDataPattern }, + }); + + newLogger.info( + { + password: 'My personal password', + pass: 'My other personal password', + headers: { + 'accept-language': 'fr-FR', // unchanged + authorization: 'Bearer token', + }, + req: { + token: 'My personal token', + }, + cmd: + 'command docker build --build-arg password=test-1234-abcd --build-arg password=test1234-abcd --build-arg password=TEST-1234-abcd ', + cmd2: 'command docker build --build-arg password=test', + }, + 'Logging amazingly sensitive data!' + ); + const message = logs.shift(); + // Default cases expect(message.password).to.equal('__SENSITIVE_DATA__'); expect(message.headers['accept-language']).to.equal('fr-FR'); expect(message.headers.authorization).to.equal('__SENSITIVE_DATA__'); expect(message.req.token).to.equal('__SENSITIVE_DATA__'); + + // Custom cases + expect(message.cmd).to.equal( + 'command docker build --build-arg password=__SENSITIVE_DATA__ --build-arg password=__SENSITIVE_DATA__ --build-arg password=__SENSITIVE_DATA__ ' + ); + expect(message.cmd2).to.equal( + 'command docker build --build-arg password=__SENSITIVE_DATA__' + ); }); }); }); @@ -182,7 +231,7 @@ describe('index.js', () => { it('should use the sensitive data stream with specific pattern fragments if set', () => { const newLogger = init({ - logger: { sensitiveDataPattern: '(password)' }, + logger: { sensitiveDataFragment: '(password)' }, }); expect(newLogger.streams).to.have.lengthOf(1); @@ -192,6 +241,32 @@ describe('index.js', () => { expect(newLogger.streams[0].stream.fragments).to.equal('(password)'); }); + it('should use the sensitive data stream with specific replacement patterns if set', () => { + const sensitiveDataPattern = [ + { + regex: new RegExp(`(testing_string)=([\\w-]*)`, 'ig'), + substitute: `"$1":"__TESTING_REPLACEMENT__"`, + }, + ]; + + const newLogger = init({ + logger: { sensitiveDataPattern }, + }); + + expect(newLogger.streams).to.have.lengthOf(1); + expect(newLogger.streams[0]).to.have.property('stream'); + + expect(newLogger.streams[0].stream).to.be.instanceOf(SensitiveDataStream); + expect(newLogger.streams[0].stream.patterns).to.have.lengthOf(2); // New pattern & default one + expect(newLogger.streams[0].stream.patterns[0]).to.deep.equal( + // Test the 1st pattern (the other one is the default one) + { + regex: /(testing_string)=([\w-]*)/gi, + substitute: `"$1":"__TESTING_REPLACEMENT__"`, + } + ); + }); + it('should use only the default stdout stream if LOGGER_USE_SENSITIVE_DATA_STREAM is false', () => { const newLogger = init({ logger: { hideSensitiveData: false },