Skip to content

Commit

Permalink
Add support for trimming strategies
Browse files Browse the repository at this point in the history
  • Loading branch information
Dakrs committed Feb 6, 2023
1 parent 42ddd73 commit f0114cc
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 10 deletions.
70 changes: 67 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ Object redaction with whitelist and blacklist. Blacklist items have higher prior

`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).

`options.trim` _(Boolean)_: A flag that enables trimming all redacted values, saving their keys to a `__redacted__` list (default value is `false`).
`options.strategy` _(string)_: An option that specifies the trimming strategy. Should be one of the following:
* `redact` is the default behavior and obfuscates the values without removing the redacted ones.
* `trim` removes the redacted values from the output.
* `trim-and-list` removes and adds a list with all deleted paths.

### Example
### Examples
#### Example using `redact` as strategy

```js
const { anonymizer } = require('@uphold/anonymizer');
Expand All @@ -37,9 +41,69 @@ anonymize(data);
// },
// bar: { foo: 1, bar: 2 },
// toAnonymize: { baz: '--REDACTED--', bar: '--REDACTED--' },
// toAnonymizeSuperString: '--REDACTED--'
// toAnonymizeSuperString: 'foo'
// }
```
#### Example using `trim` as strategy

```js
const { anonymizer } = require('@uphold/anonymizer');
const whitelist = ['foo.key', 'foo.depth.*', 'bar.*', 'toAnonymize.baz', 'toAnonymizeSuperString'];
const blacklist = ['foo.depth.innerBlacklist', 'toAnonymize.*'];
const anonymize = anonymizer({ blacklist, whitelist }, { strategy: 'trim' });

const data = {
foo: { key: 'public', another: 'bar', depth: { bar: 10, innerBlacklist: 11 } },
bar: { foo: 1, bar: 2 },
toAnonymize: { baz: 11, bar: 12 },
toAnonymizeSuperString: 'foo'
};

anonymize(data);

// {
// foo: {
// key: 'public',
// depth: { bar: 10 }
// },
// bar: { foo: 1, bar: 2 },
// toAnonymizeSuperString: 'foo'
// }
```

#### Example using `trim-and-list` as strategy

```js
const { anonymizer } = require('@uphold/anonymizer');
const whitelist = ['foo.key', 'foo.depth.*', 'bar.*', 'toAnonymize.baz', 'toAnonymizeSuperString'];
const blacklist = ['foo.depth.innerBlacklist', 'toAnonymize.*'];
const anonymize = anonymizer({ blacklist, whitelist }, { strategy: 'trim-and-list' });

const data = {
foo: { key: 'public', another: 'bar', depth: { bar: 10, innerBlacklist: 11 } },
bar: { foo: 1, bar: 2 },
toAnonymize: { baz: 11, bar: 12 },
toAnonymizeSuperString: 'foo'
};

anonymize(data);

// {
// foo: {
// key: 'public',
// depth: { bar: 10 }
// },
// bar: { foo: 1, bar: 2 },
// toAnonymizeSuperString: 'foo',
// __redacted__: [
// 'foo.another',
// 'foo.depth.innerBlacklist',
// 'toAnonymize.baz',
// 'toAnonymize.bar'
// ]
// }
```


#### Example using serializers

Expand Down
9 changes: 9 additions & 0 deletions src/enums/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

/**
* Export `strategies` enum.
*/

module.exports = {
strategies: require('./strategy-enum')
};
11 changes: 11 additions & 0 deletions src/enums/strategy-enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';

/**
* Export `strategy` enum.
*/

module.exports = {
REDACT: 'redact',
TRIM: 'trim',
TRIM_AND_LIST: 'trim-and-list'
};
11 changes: 9 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

const { serializeError } = require('serialize-error');
const { strategies } = require('./enums');
const get = require('lodash.get');
const set = require('lodash.set');
const stringify = require('json-stringify-safe');
Expand All @@ -14,6 +15,7 @@ const traverse = require('traverse');
* Constants.
*/

const { REDACT, TRIM, TRIM_AND_LIST } = strategies;
const DEFAULT_REPLACEMENT = '--REDACTED--';

/**
Expand Down Expand Up @@ -69,12 +71,17 @@ function parseAndSerialize(values, serializers) {

module.exports.anonymizer = (
{ blacklist = [], whitelist = [] } = {},
{ replacement = () => DEFAULT_REPLACEMENT, serializers = [], trim = false } = {}
{ replacement = () => DEFAULT_REPLACEMENT, serializers = [], strategy = REDACT } = {}
) => {
if (!Object.values(strategies).includes(strategy)) {
throw new Error(`Strategy ${strategy} not supported. Choose one from [${Object.values(strategies).join(', ')}]`);
}

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 trim = [TRIM, TRIM_AND_LIST].includes(strategy);

validateSerializers(serializers);

Expand Down Expand Up @@ -121,7 +128,7 @@ module.exports.anonymizer = (
}
});

if (blacklistedKeys.size) {
if (strategy === TRIM_AND_LIST && blacklistedKeys.size) {
// eslint-disable-next-line no-underscore-dangle
obj.__redacted__ = Array.from(blacklistedKeys);
}
Expand Down
106 changes: 101 additions & 5 deletions test/src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
const { anonymizer } = require('../../src');
const { generateObjectSample, generateObjectSamplePaths } = require('./benchmark/samples');
const { serializeError } = require('serialize-error');
const { strategies } = require('../../src/enums');

/**
* Constants.
*/

const { TRIM, TRIM_AND_LIST } = strategies;

/**
* Test `Anonymizer`.
Expand Down Expand Up @@ -375,9 +382,37 @@ describe('Anonymizer', () => {
});
});

describe('trim', () => {
describe('strategy', () => {
it('should throw an error if strategy is not supported', () => {
try {
anonymizer({ whitelist: ['*'] }, { strategy: 'foobar' });

fail();
} catch (error) {
expect(error).toBeInstanceOf(Error);
expect(error.message).toEqual(
`Strategy foobar not supported. Choose one from [${Object.values(strategies).join(', ')}]`
);
}
});

it('should use `REDACT` as default strategy', () => {
const anonymize = anonymizer({ whitelist: ['foo*'] });
const result = anonymize({
buz: 'baz',
foo: { bar: 'bar', biz: 'biz' },
foz: { baz: 'baz' }
});

expect(result).toEqual({
buz: '--REDACTED--',
foo: { bar: 'bar', biz: 'biz' },
foz: { baz: '--REDACTED--' }
});
});

it('should group array keys', () => {
const anonymize = anonymizer({ whitelist: ['foo'] }, { trim: true });
const anonymize = anonymizer({ whitelist: ['foo'] }, { strategy: TRIM_AND_LIST });

expect(
anonymize({
Expand All @@ -391,7 +426,7 @@ describe('Anonymizer', () => {
});

it('should trim obfuscated fields and add their paths to a `__redacted__` list', () => {
const anonymize = anonymizer({ whitelist: ['foo'] }, { trim: true });
const anonymize = anonymizer({ whitelist: ['foo'] }, { strategy: TRIM_AND_LIST });

expect(
anonymize({
Expand All @@ -405,7 +440,21 @@ describe('Anonymizer', () => {
});
});

it('should not trim obfuscated values that have different obfuscation techniques', () => {
it('should trim obfuscated fields without adding their paths to a `__redacted__` list', () => {
const anonymize = anonymizer({ whitelist: ['foo'] }, { strategy: TRIM });

expect(
anonymize({
biz: 'baz',
buz: { bux: { qux: 'quux' } },
foo: 'bar'
})
).toEqual({
foo: 'bar'
});
});

it(`should not trim obfuscated values if 'replacement' evaluated value is different from the default and strategy is ${TRIM}`, () => {
const replacement = (key, value) => {
if (key === 'biz') {
return value;
Expand All @@ -417,7 +466,35 @@ describe('Anonymizer', () => {

return '--REDACTED--';
};
const anonymize = anonymizer({ whitelist: ['foo'] }, { replacement, trim: true });
const anonymize = anonymizer({ whitelist: ['foo'] }, { replacement, strategy: TRIM });

expect(
anonymize({
biz: 'baz',
buz: 'bux',
foo: 'bar',
qux: 'quux'
})
).toEqual({
biz: 'baz',
buz: '--HIDDEN--',
foo: 'bar'
});
});

it(`should not trim obfuscated values if 'replacement' evaluated value is different from the default and strategy is ${TRIM_AND_LIST}`, () => {
const replacement = (key, value) => {
if (key === 'biz') {
return value;
}

if (value === 'bux') {
return '--HIDDEN--';
}

return '--REDACTED--';
};
const anonymize = anonymizer({ whitelist: ['foo'] }, { replacement, strategy: TRIM_AND_LIST });

expect(
anonymize({
Expand Down Expand Up @@ -483,6 +560,25 @@ describe('Anonymizer', () => {
expect(msElapsed).toBeLessThan(175);
expect(serializer).toHaveBeenCalledTimes(32768);
});

[TRIM, TRIM_AND_LIST].forEach(strategy => {
it(`should run in '${strategy}' mode with an object with '32768' properties in less than '225' ms`, () => {
const depth = 10;
const data = generateObjectSample({ depth });
const serializer = jest.fn(() => 'bii');
const serializers = generateObjectSamplePaths({ depth }).map(path => ({ path, serializer }));
const anonymize = anonymizer({ blacklist: ['*'] }, { serializers, strategy });
const startTime = process.hrtime();

anonymize(data);

const endTime = process.hrtime(startTime);
const msElapsed = endTime[1] / 1000000;

expect(msElapsed).toBeLessThan(225);
expect(serializer).toHaveBeenCalledTimes(32768);
});
});
});
});
});

0 comments on commit f0114cc

Please sign in to comment.