From fc3679063d8889e2f9aa8e70a41936f71799f0cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Cruz?= Date: Sun, 1 Dec 2024 21:22:50 +0000 Subject: [PATCH] Add support for glob alike pattern matching Also revamped README. --- README.md | 301 ++++++++++++++++++++----- package.json | 2 +- src/index.js | 37 +++- test/src/index.test.js | 490 ++++++++++++++++++++++++++++++----------- 4 files changed, 639 insertions(+), 191 deletions(-) diff --git a/README.md b/README.md index 0eee226..44af866 100644 --- a/README.md +++ b/README.md @@ -1,105 +1,296 @@ # anonymizer -Object redaction with whitelist and blacklist. Blacklist items have higher priority and will always supercede the whitelist. -## Arguments -1. `whitelist` _(Array)_: The whitelist array. -2. `blacklist` _(Array)_: The blacklist array. -3. `options` _(Object)_: An object with optional options. +Object redaction library that supports whitelisting, blacklisting, and wildcard matching. - `options.replacement` _(Function)_: A function that allows customizing the replacement value (default implementation is `--REDACTED--`). +## Installation - `options.serializers` _(List[Object])_: A list with serializers to apply. Each serializers must contain two properties: `path` (path for the value to be serialized, must be a `string`) and `serializer` (function to be called on the path's value). +```bash +npm install @uphold/anonymizer +``` - `options.trim` _(Boolean)_: A flag that enables trimming all redacted values, saving their keys to a `__redacted__` list (default value is `false`). +## Usage -### Example +### Basic example ```js -const { anonymizer } = require('@uphold/anonymizer'); -const whitelist = ['foo.key', 'foo.depth.*', 'bar.*', 'toAnonymize.baz', 'toAnonymizeSuperString']; -const blacklist = ['foo.depth.innerBlacklist', 'toAnonymize.*']; +import { anonymizer } from '@uphold/anonymizer'; + +const whitelist = ['key1', 'key2.foo']; +const anonymize = anonymizer({ whitelist }); + +const data = { + key1: 'bar', + key2: { + foo: 'bar', + bar: 'baz', + baz: { + foo: 'bar', + bar: 'baz' + } + } +}; + +anonymize(data); + +// { +// key1: 'bar', +// key2: { +// foo: 'bar', +// bar: '--REDACTED--', +// baz: { +// foo: '--REDACTED--' +// bar: '--REDACTED--' +// } +// } +// } +``` + +### Wildcard matching example + +Using `*` allows you to match any character in a key, except for `.`. +This is similar to how `glob` allows you to use `*` to match any character, except for `/`. + +```js +import { anonymizer } from '@uphold/anonymizer'; + +const whitelist = ['key2.*']; +const anonymize = anonymizer({ whitelist }); + +const data = { + key1: 'bar', + key2: { + foo: 'bar', + bar: 'baz', + baz: { foo: 'bar' } + } +}; + +anonymize(data); + +// { +// key1: '--REDACTED--', +// key2: { +// foo: 'bar', +// bar: 'baz', +// baz: { +// foo: '--REDACTED--' +// bar: '--REDACTED--' +// } +// } +// } +``` + +### Double wildcard matching example + +Using `**` allows you to match any nested key. +This is similar to how `glob` allows you to use `**` to match any nested directory. + +```js +import { anonymizer } from '@uphold/anonymizer'; + +const whitelist = ['key2.**', '**.baz']; +const blacklist = ['key2.bar'] const anonymize = anonymizer({ blacklist, whitelist }); const data = { - foo: { key: 'public', another: 'bar', depth: { bar: 10, innerBlacklist: 11 } }, - bar: { foo: 1, bar: 2 }, - toAnonymize: { baz: 11, bar: 12 }, - toAnonymizeSuperString: 'foo' + key1: 'bar', + key2: { + foo: 'bar', + bar: 'baz', + baz: { foo: 'bar' } + }, + key3: { + foo: { + baz: 'biz' + } + } }; anonymize(data); // { -// foo: { -// key: 'public', -// another: '--REDACTED--', -// depth: { bar: 10, innerBlacklist: '--REDACTED--' } +// key1: '--REDACTED--', +// key2: { +// foo: 'bar', +// bar: '--REDACTED--', +// baz: { +// foo: 'bar' +// bar: 'baz' +// } // }, -// bar: { foo: 1, bar: 2 }, -// toAnonymize: { baz: '--REDACTED--', bar: '--REDACTED--' }, -// toAnonymizeSuperString: '--REDACTED--' +// key3: { +// foo: { +// baz: 'biz' +// } +// } // } ``` -#### Example using serializers +#### Custom replacement example + +By default, the replacement value is `--REDACTED--`. You can customize it by passing a `replacement` function in the options. + +Here's an example that keeps strings partially redacted: ```js -const { anonymizer } = require('@uphold/anonymizer'); -const whitelist = ['foo.key', 'foo.depth.*', 'bar.*', 'toAnonymize.baz']; -const blacklist = ['foo.depth.innerBlacklist']; -const serializers = [ - { path: 'foo.key', serializer: () => 'biz' }, - { path: 'toAnonymize', serializer: () => ({ baz: 'baz' }) } -] -const anonymize = anonymizer({ blacklist, whitelist }, { serializers }); +import { anonymizer } from '@uphold/anonymizer'; + +const replacement = (key, value, path) => { + if (typeof value !== 'string') { + return '--REDACTED--'; + } + + // Keep the first half of the string and redact the rest. + const charsToKeep = Math.floor(value.length / 2); + + return value.substring(0, charsToKeep) + '*'.repeat(Math.min(value.length - charsToKeep, 100)); +}; + +const anonymize = anonymizer({}, { replacement }); const data = { - foo: { key: 'public', another: 'bar', depth: { bar: 10, innerBlacklist: 11 } }, - bar: { foo: 1, bar: 2 }, - toAnonymize: {} + key1: 'bar', + key2: { + foo: 'bar', + bar: 'baz', + baz: { + bar: 'baz', + foo: 'bar' + } + } }; anonymize(data); // { -// foo: { -// key: 'biz', -// another: '--REDACTED--', -// depth: { bar: 10, innerBlacklist: '--REDACTED--' } -// }, -// bar: { foo: 1, bar: 2 }, -// toAnonymize: { baz: 'baz' } +// key1: 'b**', +// key2: { +// foo: 'b**' +// bar: 'b**', +// baz: { +// bar: 'b**', +// foo: 'b**' +// }, +// } // } ``` -### Default serializers +#### Trim redacted values to keep output shorter -The introduction of serializers also added the possibility of using serializer functions exported by our module. The list of default serializers is presented below: -- error +In certain scenarios, you may want to trim redacted values to keep the output shorter. Such example is f you are redacting logs and sending them to a provider. -#### Example +This can be achieved by setting the `trim` option to `true`, like so: ```js -const { anonymizer, defaultSerializers } = require('@uphold/anonymizer'); -const serializers = [ - { path: 'foo', serializer: defaultSerializers.error } -]; +const whitelist = ['key1', 'key2.foo']; +const anonymize = anonymizer({ whitelist }, { trim: true }); -const anonymize = anonymizer({}, { serializers }); +const data = { + key1: 'bar', + key2: { + foo: 'bar', + bar: 'baz', + baz: { + foo: 'bar', + bar: 'baz' + } + } +}; -const data = { foo: new Error('Foobar') }; +anonymize(data); + +// { +// __redacted__: [ 'key2.bar', 'key2.baz.foo', 'key2.baz.bar'] +// key1: 'bar', +// key2: { +// foo: 'bar' +// } +// } +``` + +#### Serializers example + +Serializers allow you to apply custom transformations to specific values before being redacted. + +Here's an example: + +```js +const { anonymizer } = require('@uphold/anonymizer'); +const whitelist = ['foo.key']; +const serializers = [ + { path: 'foo.key', serializer: () => 'biz' }, +] +const anonymize = anonymizer({ whitelist }, { serializers }); + +const data = { + foo: { key: 'public' }, +}; anonymize(data); // { // foo: { -// name: '--REDACTED--', -// message: '--REDACTED--', -// stack: '--REDACTED--' +// key: 'biz' // } // } ``` +Take a look at the [built-in serializers](#serializers) for common use cases. + +## API + +### anonymizer({ whitelist, blacklist }, options) + +Returns a function that redacts a given object based on the provided whitelist and blacklist. + +#### whitelist + +Type: `Array` + +An array of whitelisted patterns to use when matching against object paths that should not be redacted. + +#### blacklist + +Type: `Array` + +An array of blacklisted patterns to use when matching against object paths that should be redacted. + +By default, every value is redacted. However, the blacklist can be used in conjunction with a whitelist. The values that match the blacklist will be redacted, even if they match the whitelist. + +#### options + +##### options.replacement + +Type: `Function` + +A function that allows customizing the replacement value (default implementation is `--REDACTED--`). + +It receives the following arguments: `key` _(String)_, `value` _(Any)_, and `path` _(String)_. + +##### options.serializers + +Type: `Array` + +A list with serializers to apply. Each serializers must contain two properties: `path` (path for the value to be serialized, must be a `string`) and `serializer` (function to be called on the path's value). + +##### options.trim + +Type: `Boolean` + +A flag that enables trimming all redacted values, saving their keys to a `__redacted__` list (default value is `false`). Please note that trimming is only applied when the replacement value is `--REDACTED--`. + +### serializers + +Built-in serializer functions you may use in the `serializers` option. + +### error + +Serializes an `Error` object. + +### datadogSerializer + +Serializes an `Error` object for the purpose of sending it to Datadog, adding a `kind` property based on the error class name. + ## Release process The release of a version is automated via the [release](https://github.com/uphold/anonymizer/.github/workflows/release.yml) GitHub workflow. Run it by clicking the "Run workflow" button. diff --git a/package.json b/package.json index cc45e7c..22dda33 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@uphold/anonymizer", "version": "5.1.0", - "description": "Object redaction with whitelist as main feature.", + "description": "Object redaction library that supports whitelisting, blacklisting, and wildcard matching", "homepage": "https://github.com/uphold/anonymizer#readme", "bugs": { "url": "https://github.com/uphold/anonymizer/issues" diff --git a/src/index.js b/src/index.js index a9aafba..522f20d 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ * Module dependencies. */ -const { cloneDeep, cloneDeepWith, get, set } = require('lodash'); +const { cloneDeep, cloneDeepWith, escapeRegExp, get, set } = require('lodash'); const { serializeError } = require('serialize-error'); const stringify = require('json-stringify-safe'); const traverse = require('traverse'); @@ -74,8 +74,8 @@ function validateSerializers(serializers) { * During `parseAndSerialize` execution, we perform additional copies to avoid having a serializer updating the * original object by reference. These copies are only done in the values passed to serializers to avoid two full * copies of the original values. For this, we used `cloneDeepWith` with a custom clone only for errors. When an - * error is found, we compute a list with all properties (properties from the class itself and from extended cla- - * sses). Then we use these properties to get the original values and copying them into a new object. + * error is found, we compute a list with all properties (properties from the class itself and from extended classes). + * Then we use these properties to get the original values and copying them into a new object. */ function parseAndSerialize(values, serializers) { @@ -106,6 +106,25 @@ function parseAndSerialize(values, serializers) { return target; } +/** + * Parses patterns into a single RegExp. + */ + +function parsePatternsIntoRegExp(patterns) { + const patternRegExps = patterns.map(pattern => + // Escape regex special characters. + escapeRegExp(pattern) + // Handle `**` feature. + .replaceAll('\\.\\*\\*\\.', '\\..*') + .replaceAll('\\*\\*\\.', '(.*\\.)?') + .replaceAll('\\*\\*', '.*') + // Handle `*` feature. + .replaceAll('\\*', '[^\\.]*') + ); + + return new RegExp(`^(${patternRegExps.join('|')})$`, 'i'); +} + /** * Module exports `anonymizer` function. */ @@ -114,10 +133,8 @@ module.exports.anonymizer = ( { blacklist = [], whitelist = [] } = {}, { replacement = () => DEFAULT_REPLACEMENT, serializers = [], trim = false } = {} ) => { - const whitelistTerms = whitelist.join('|'); - const whitelistPaths = new RegExp(`^(${whitelistTerms.replace(/\./g, '\\.').replace(/\*/g, '.*')})$`, 'i'); - const blacklistTerms = blacklist.join('|'); - const blacklistPaths = new RegExp(`^(${blacklistTerms.replace(/\./g, '\\.').replace(/\*/g, '.*')})$`, 'i'); + const whitelistRegExp = parsePatternsIntoRegExp(whitelist); + const blacklistRegExp = parsePatternsIntoRegExp(blacklist); validateSerializers(serializers); @@ -145,13 +162,13 @@ module.exports.anonymizer = ( return; } - if (isBuffer && !blacklistPaths.test(path) && whitelistPaths.test(path)) { + if (isBuffer && !blacklistRegExp.test(path) && whitelistRegExp.test(path)) { return this.update(Buffer.from(this.node), true); } - const replacedValue = replacement(this.key, this.node, this.path); + if (blacklistRegExp.test(path) || !whitelistRegExp.test(path)) { + const replacedValue = replacement(this.key, this.node, this.path); - if (blacklistPaths.test(path) || !whitelistPaths.test(path)) { if (trim && replacedValue === DEFAULT_REPLACEMENT) { const path = this.path.map(value => (isNaN(value) ? value : '[]')); diff --git a/test/src/index.test.js b/test/src/index.test.js index 3ccd003..6041ee2 100644 --- a/test/src/index.test.js +++ b/test/src/index.test.js @@ -53,180 +53,420 @@ describe('Anonymizer', () => { expect(anonymize({ foo })).toEqual({ foo: { bar: 'biz' } }); }); + it('should obfuscate values whose type is Buffer', () => { + const anonymize = anonymizer(); + + expect(anonymize({ foo: Buffer.from('foobarfoobar') })).toEqual({ foo: '--REDACTED--' }); + }); + describe('whitelist', () => { - const whitelist = ['key1', 'key2', 'key3']; + it('should default to an empty whitelist', () => { + const anonymize = anonymizer(); + + expect(anonymize({ foo: 'foo' })).toEqual({ foo: '--REDACTED--' }); + }); + + it('should not obfuscate keys that are whitelisted', () => { + const anonymize = anonymizer({ whitelist: ['key1', 'key2'] }); + + expect(anonymize({ key1: 'foo', key2: 'bar', key3: 'baz' })).toEqual({ + key1: 'foo', + key2: 'bar', + key3: '--REDACTED--' + }); + }); + + it('should match in a case insensitive way', () => { + const anonymize = anonymizer({ whitelist: ['foo'] }); + + expect(anonymize({ FOO: 'foo' })).toEqual({ FOO: 'foo' }); + }); + + it('should not obfuscate values of type Buffer that are whitelisted', () => { + const anonymize = anonymizer({ whitelist: ['foo'] }); + + expect(anonymize({ foo: Buffer.from('foobarfoobar') })).toEqual({ foo: Buffer.from('foobarfoobar') }); + }); + + it('should escape special characters when building the regular expression', () => { + const anonymize = anonymizer({ whitelist: ['f+o^o$b?a[r'] }); + + expect(anonymize({ 'f+o^o$b?a[r': 'foo' })).toEqual({ 'f+o^o$b?a[r': 'foo' }); + }); + + it('should support wildcard matching', () => { + const data = { + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } + }; - whitelist.forEach(key => { - it(`should not obfuscate \`${key}\``, () => { - const anonymize = anonymizer({ whitelist }); + expect(anonymizer({ whitelist: ['parent1.*'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: 'bar' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); - expect(anonymize({ [key]: 'foo' })).toEqual({ [key]: 'foo' }); + expect(anonymizer({ whitelist: ['parent2.f*'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: 'bar' } }); - it(`should not obfuscate \`${key}\` with different casing`, () => { - const anonymize = anonymizer({ whitelist }); + expect(anonymizer({ whitelist: ['parent2.*o'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: 'bar' } + }); - expect(anonymize({ [key.toUpperCase()]: 'foo' })).toEqual({ [key.toUpperCase()]: 'foo' }); - expect(anonymize({ [key.toLowerCase()]: 'foo' })).toEqual({ [key.toLowerCase()]: 'foo' }); + // `parent2.*o*` should match all parent2 direct children that contain an `o`. + expect(anonymizer({ whitelist: ['parent2.*o*'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: 'bar' } }); - it(`should obfuscate keys that contain \`${key}\``, () => { - const anonymize = anonymizer({ whitelist }); + expect(anonymizer({ whitelist: ['*e*.*'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } + }); - expect(anonymize({ [`${key}foo`]: 'bar' })).toEqual({ [`${key}foo`]: '--REDACTED--' }); - expect(anonymize({ [`foo${key}`]: 'bar' })).toEqual({ [`foo${key}`]: '--REDACTED--' }); - expect(anonymize({ [`foo${key}foo`]: 'bar' })).toEqual({ [`foo${key}foo`]: '--REDACTED--' }); + expect(anonymizer({ whitelist: ['parent1.*.foo'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: 'bar' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); + + expect(anonymizer({ whitelist: ['*.foo'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: 'bar' + }, + parent2: { bar: '--REDACTED--', foo: 'bar' } + }); + + expect(anonymizer({ whitelist: ['*t1.foo'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: 'bar' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } }); }); - it(`should obfuscate keys whose type is Buffer`, () => { - const anonymize = anonymizer(); + it('should support double wildcard matching', () => { + const data = { + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } + }; + + expect(anonymizer({ whitelist: ['parent1.**'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); + + expect(anonymizer({ whitelist: ['parent**'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } + }); + + expect(anonymizer({ whitelist: ['parent1.c**'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); + + expect(anonymizer({ whitelist: ['parent1.**.foo'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); + + expect(anonymizer({ whitelist: ['**.foo'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: '--REDACTED--', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: '--REDACTED--', foo: 'bar' } + }); + + expect(anonymizer({ whitelist: ['**oo'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: '--REDACTED--', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: '--REDACTED--', foo: 'bar' } + }); + + expect(anonymizer({ whitelist: ['**.oo'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); - expect(anonymize({ foo: Buffer.from('foobarfoobar') })).toEqual({ foo: '--REDACTED--' }); + expect(anonymizer({ whitelist: ['**.fo'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); }); + }); - it(`should not obfuscate Buffer-type keys that are whitelisted`, () => { + describe('blacklist', () => { + it('should default to an empty blacklist', () => { const anonymize = anonymizer({ whitelist: ['foo'] }); - expect(anonymize({ foo: Buffer.from('foobarfoobar') })).toEqual({ foo: Buffer.from('foobarfoobar') }); + expect(anonymize({ foo: 'foo' })).toEqual({ foo: 'foo' }); }); - it(`should default to an empty whitelist`, () => { - const anonymize = anonymizer(); + it('should obfuscate keys that are blacklisted', () => { + const anonymize = anonymizer({ blacklist: ['key1', 'key2'], whitelist: ['**'] }); - expect(anonymize({ foo: 'foo' })).toEqual({ foo: '--REDACTED--' }); + expect(anonymize({ key1: 'foo', key2: 'bar', key3: 'baz' })).toEqual({ + key1: '--REDACTED--', + key2: '--REDACTED--', + key3: 'baz' + }); }); - it('should not obfuscate recursively the keys of an object that are part of the whitelist', () => { - const anonymize = anonymizer({ whitelist }); + it('should prioritize blacklist over whitelist', () => { + const anonymize = anonymizer({ blacklist: ['key1.innerKey1'], whitelist: ['key1.innerKey1'] }); expect( anonymize({ - foo: { - bar: { - baz: { bax: [2, 3, { bax: 4, [whitelist[1]]: '5' }] }, - [whitelist[0]]: 'foobar', - [whitelist[2]]: 'foobiz' - } - } + key1: { innerKey1: 'bar', innerKey2: 'foo' } }) ).toEqual({ - foo: { - bar: { - baz: { bax: ['--REDACTED--', '--REDACTED--', { bax: '--REDACTED--', [whitelist[1]]: '--REDACTED--' }] }, - [whitelist[0]]: '--REDACTED--', - [whitelist[2]]: '--REDACTED--' - } - } + key1: { innerKey1: '--REDACTED--', innerKey2: '--REDACTED--' } }); }); - it('should not obfuscate a key that is part of the whitelist', () => { - const anonymize = anonymizer({ whitelist: ['foo'] }); + it('should match in a case insensitive way', () => { + const anonymize = anonymizer({ blacklist: ['key1'], whitelist: ['KEy1'] }); - expect(anonymize({ foo: 'bar' })).toEqual({ foo: 'bar' }); + expect(anonymize({ KEy1: 'foo' })).toEqual({ KEy1: '--REDACTED--' }); }); - it('should not treat a `.` in the whitelist as a special character in the regexp', () => { - const anonymize = anonymizer({ whitelist: ['foo.bar'] }); + it('should escape special characters when building the regular expression', () => { + const anonymize = anonymizer({ blacklist: ['f+o^o$b?a[r'], whitelist: ['**'] }); - expect(anonymize({ foo: { bar: 'biz' }, fooabar: 'foobiz' })).toEqual({ - foo: { bar: 'biz' }, - fooabar: '--REDACTED--' - }); + expect(anonymize({ 'f+o^o$b?a[r': 'FOO' })).toEqual({ 'f+o^o$b?a[r': '--REDACTED--' }); }); - it('should allow using `*` in the whitelist path', () => { - const anonymize = anonymizer({ whitelist: ['*.foo', '*.foobar', 'parent.*.biz'] }); + it('should support wildcard matching', () => { + const data = { + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } + }; - expect(anonymize({ parent: { foo: 'bar', foobar: 'foobiz', quux: { biz: 'baz', foobiz: 'bar' } } })).toEqual({ - parent: { foo: 'bar', foobar: 'foobiz', quux: { biz: 'baz', foobiz: '--REDACTED--' } } + expect(anonymizer({ blacklist: ['parent1.*'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: '--REDACTED--' + }, + parent2: { bar: 'biz', foo: 'bar' } }); - }); - }); - describe('blacklist', () => { - it(`should default to an empty blacklist`, () => { - const anonymize = anonymizer({ whitelist: ['foo'] }); + expect(anonymizer({ blacklist: ['parent2.f*'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: '--REDACTED--' } + }); - expect(anonymize({ foo: 'foo' })).toEqual({ foo: 'foo' }); - }); + expect(anonymizer({ blacklist: ['parent2.*o'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: '--REDACTED--' } + }); + + // `parent2.*o*` should match all parent2 direct children that contain an `o`. + expect(anonymizer({ blacklist: ['parent2.*o*'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: '--REDACTED--' } + }); - it('should not treat a `.` in the blacklist as a special character in the regexp', () => { - const anonymize = anonymizer({ blacklist: ['foo.bar'], whitelist: ['fooabar'] }); + expect(anonymizer({ blacklist: ['*e*.*'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); - expect(anonymize({ foo: { bar: 'biz' }, fooabar: 'foobiz' })).toEqual({ - foo: { bar: '--REDACTED--' }, - fooabar: 'foobiz' + expect(anonymizer({ blacklist: ['parent1.*.foo'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: '--REDACTED--' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } }); - }); - describe('in case of collision', () => { - it('should prioritize blacklist over whitelist', () => { - const anonymize = anonymizer({ blacklist: ['key1.innerKey1'], whitelist: ['key1.innerKey1'] }); + expect(anonymizer({ blacklist: ['*.foo'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: '--REDACTED--' + }, + parent2: { bar: 'biz', foo: '--REDACTED--' } + }); - expect( - anonymize({ - key1: { innerKey1: 'bar', innerKey2: 'foo' } - }) - ).toEqual({ - key1: { innerKey1: '--REDACTED--', innerKey2: '--REDACTED--' } - }); + expect(anonymizer({ blacklist: ['*t1.foo'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: '--REDACTED--' + }, + parent2: { bar: 'biz', foo: 'bar' } }); + }); - it(`should obfuscate key with different casing`, () => { - const anonymize = anonymizer({ blacklist: ['key1'], whitelist: ['KEy1'] }); + it('should support double wildcard matching', () => { + const data = { + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } + }; - expect(anonymize({ KEy1: 'foo' })).toEqual({ KEy1: '--REDACTED--' }); + expect(anonymizer({ blacklist: ['parent1.**'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: 'biz', foo: 'bar' } }); - it('should allow using `*` in blacklist path', () => { - const whitelist = ['key1.*', 'key2.innerKey2']; - const blacklist = ['*innerKey2']; - const anonymize = anonymizer({ blacklist, whitelist }); + expect(anonymizer({ blacklist: ['parent**'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: '--REDACTED--', foo: '--REDACTED--' } + }); - expect( - anonymize({ - key1: { innerKey1: 'bar', innerKey2: 'bam' }, - key2: { innerKey1: 'bar', innerKey2: 'bam' } - }) - ).toEqual({ - key1: { innerKey1: 'bar', innerKey2: '--REDACTED--' }, - key2: { innerKey1: '--REDACTED--', innerKey2: '--REDACTED--' } - }); + expect(anonymizer({ blacklist: ['parent1.c**'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: '--REDACTED--', foo: '--REDACTED--' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } }); - it(`should obfuscate Buffer-type keys that are blacklisted`, () => { - const anonymize = anonymizer({ blacklist: ['foo'], whitelist: ['foo'] }); + expect(anonymizer({ blacklist: ['parent1.**.foo'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: 'biz', foo: 'bar' } + }); - expect(anonymize({ foo: Buffer.from('foobarfoobar') })).toEqual({ foo: '--REDACTED--' }); + expect(anonymizer({ blacklist: ['**.foo'], whitelist: ['**'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: 'biz', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: 'biz', foo: '--REDACTED--' } }); - it('should obfuscate recursively the keys of an object that are part of the blacklist', () => { - const anonymize = anonymizer({ - blacklist: ['foo.bar.*'], - whitelist: ['foo.bar.key1', 'foo.bar.key2', 'foo.bar.baz.bax.*', '*key1'] - }); + expect(anonymizer({ blacklist: ['**oo'], whitelist: ['**'] })(data)).toEqual({ + foo: '--REDACTED--', + parent1: { + child: { bar: 'biz', foo: '--REDACTED--' }, + foo: '--REDACTED--' + }, + parent2: { bar: 'biz', foo: '--REDACTED--' } + }); - expect( - anonymize({ - foo: { - bar: { - baz: { bax: [2, 3, { bax: 4, key1: '5' }] }, - key1: 'foobar', - key2: 'foobiz' - } - } - }) - ).toEqual({ - foo: { - bar: { - baz: { bax: ['--REDACTED--', '--REDACTED--', { bax: '--REDACTED--', key1: '--REDACTED--' }] }, - key1: '--REDACTED--', - key2: '--REDACTED--' - } - } - }); + expect(anonymizer({ blacklist: ['**.oo'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } + }); + + expect(anonymizer({ blacklist: ['**.fo'], whitelist: ['**'] })(data)).toEqual({ + foo: 'bar', + parent1: { + child: { bar: 'biz', foo: 'bar' }, + foo: 'bar' + }, + parent2: { bar: 'biz', foo: 'bar' } }); }); }); @@ -259,7 +499,7 @@ describe('Anonymizer', () => { describe('serializers', () => { it('should throw an error when serializer is not a function', () => { const serializers = [{ path: 'foo', serializer: 123 }]; - const whitelist = ['*']; + const whitelist = ['**']; try { anonymizer({ whitelist }, { serializers }); @@ -275,7 +515,7 @@ describe('Anonymizer', () => { const foobar = jest.fn(() => 'bii'); const foobiz = jest.fn(() => 'bzz'); const foobzz = jest.fn(() => ({ bar: 'biz' })); - const whitelist = ['*']; + const whitelist = ['**']; const serializers = [ { path: 'bar', serializer: foobiz }, { path: 'foo', serializer: foobar }, @@ -297,7 +537,7 @@ describe('Anonymizer', () => { const foobar = jest.fn(() => 'bii'); const foobiz = jest.fn(() => 'bzz'); const fooerror = jest.fn(serializeError); - const whitelist = ['*']; + const whitelist = ['**']; const serializers = [ { path: 'bar.foo', serializer: foobiz }, { path: 'bar.error', serializer: fooerror }, @@ -330,7 +570,7 @@ describe('Anonymizer', () => { return 'biz'; }); - const whitelist = ['*']; + const whitelist = ['**']; const serializers = [ { path: 'foo', serializer: foobar }, { path: 'foz', serializer: fozbar } @@ -426,7 +666,7 @@ describe('Anonymizer', () => { return serialized; }); const serializers = [{ path: 'error', serializer }]; - const whitelist = ['*']; + const whitelist = ['**']; const anonymize = anonymizer({ whitelist }, { serializers }); const result = anonymize({ error }); @@ -460,7 +700,7 @@ describe('Anonymizer', () => { return serialized; }); const serializers = [{ path: 'error', serializer }]; - const whitelist = ['*']; + const whitelist = ['**']; const anonymize = anonymizer({ whitelist }, { serializers }); const result = anonymize({ error }); @@ -485,7 +725,7 @@ describe('Anonymizer', () => { { path: 'foo', serializer }, { path: 'foz', serializer } ]; - const whitelist = ['*']; + const whitelist = ['**']; const anonymize = anonymizer({ whitelist }, { serializers }); const result = anonymize(data); @@ -506,7 +746,7 @@ describe('Anonymizer', () => { { path: 'err', serializer }, { path: 'error', serializer } ]; - const whitelist = ['*']; + const whitelist = ['**']; const anonymize = anonymizer({ whitelist }, { serializers }); const result = anonymize({