Skip to content

Commit

Permalink
Merge e07cd96 into dc96169
Browse files Browse the repository at this point in the history
  • Loading branch information
nikksan committed Mar 30, 2021
2 parents dc96169 + e07cd96 commit 24e31d2
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 172 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@luckbox/logger-factory",
"version": "3.2.0",
"version": "4.0.0",
"description": "Easy to use logger with several levels of logging as well as different adapters that can be used separately or in combinations",
"author": "Luckbox",
"license": "ISC",
Expand Down
111 changes: 111 additions & 0 deletions src/Masker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import lodash from 'lodash';

enum Tag {
PII = 'PII',
SECRET = 'SECRET'
}

type PlainObject = Record<string, unknown>;

class Masker {
public maskString(value: string, tag: Tag): string {
return `[${tag}]${value}[/${tag}]`;
}

public maskNumber(value: number, tag: Tag): string {
return `[${tag}]${value}[/${tag}]`;
}

public maskObject<T extends PlainObject | Error>(object: T, maskSettings: Array<[string, Tag]>): T {
if (this.isError(object)) {
return this.maskError(object, maskSettings);
} else if (this.isPlainObject(object)) {
return this.maskPlainObject(object, maskSettings);
}

throw new Error('Input must be plain object or a class inheriting from Error');
}

private maskPlainObject<T extends PlainObject>(plainObject: T, maskSettings: Array<[string, Tag]>): T {
const clonedObj = this.clone(plainObject);
const pathToTagMap = this.constructPathToTagMap(maskSettings);

const allPaths = this.collectPaths(plainObject);
for (const path of allPaths) {
const tag = pathToTagMap.get(path);
if (!tag) {
continue;
}

const rawValue = lodash.get(clonedObj, path);
if (typeof rawValue === 'string') {
lodash.set(clonedObj, path, this.maskString(rawValue, tag));
}

if (typeof rawValue === 'number') {
lodash.set(clonedObj, path, this.maskNumber(rawValue, tag));
}
}

return clonedObj;
}

private maskError<T extends Error>(err: T, maskSettings: Array<[string, Tag]>): T {
const clonedErr = new Error(err.message);
Object.setPrototypeOf(clonedErr, Object.getPrototypeOf(err));

const dataToAssign = {};
for (const prop of Object.getOwnPropertyNames(err)) {
dataToAssign[prop] = this.clone(err[prop]);
}

Object.assign(clonedErr, this.maskPlainObject(dataToAssign, maskSettings));

return clonedErr as T;
}

private collectPaths(input: any, currentPath?: string) {
const paths: Array<string> = [];

if (lodash.isPlainObject(input)) {
for (const key in input) {
const fullPath = this.buildPath(key, currentPath);
const value = input[key];

paths.push(fullPath, ...this.collectPaths(value).map((nestedPath) => this.buildPath(nestedPath, fullPath)));
}
}

return paths;
}

private buildPath(propPath: string, basePath?: string) {
return basePath === undefined ? String(propPath) : `${basePath}.${propPath}`;
}

private isPlainObject(value: unknown): value is PlainObject {
return lodash.isPlainObject(value);
}

private isError(value: unknown): value is Error {
return value instanceof Error;
}

private constructPathToTagMap(maskSettings: Array<[string, Tag]>) {
const pathToTagMap = new Map();
for (const [path, tag] of maskSettings) {
pathToTagMap.set(path, tag);
}

return pathToTagMap;
}

private clone<T extends any>(value: T): T {
return lodash.cloneDeep(value);
}
}

export {
Masker,
Tag,
};
104 changes: 0 additions & 104 deletions src/Obfuscator.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LoggerFactory, Adapters, LogLevels, ConsoleAdapterSettings, SentryAdapterSettings } from './LoggerFactory';
import { Logger } from './Logger';
import { Obfuscator, Tag } from './Obfuscator';
import { Masker, Tag } from './Masker';

export {
LoggerFactory,
Expand All @@ -9,6 +9,6 @@ export {
Logger,
ConsoleAdapterSettings,
SentryAdapterSettings,
Obfuscator,
Masker,
Tag as ObfuscatorTag,
};
78 changes: 78 additions & 0 deletions src/tests/unit-tests/Masker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Masker, Tag } from '../../Masker';

describe('Masker', () => {
const masker = new Masker();

describe('maskString', () => {
it('should wrap the provided tag around the string that needs to be maskd', () => {
expect(masker.maskString('string', Tag.PII)).toEqual('[PII]string[/PII]');
});
});

describe('maskNumber', () => {
it('should wrap the provided tag around the number that needs to be maskd', () => {
expect(masker.maskNumber(42, Tag.PII)).toEqual('[PII]42[/PII]');
});
});

describe('maskObject', () => {
it('should wrap the provided tag around root-level elements in object', () => {
const originalObject = { name: 'Pencho' };
const maskedObject = { name: '[PII]Pencho[/PII]' };
expect(masker.maskObject(originalObject, [['name', Tag.PII]])).toEqual(maskedObject);
});

it('should wrap the provided tag around nested elements in object', () => {
const originalObject = { id: 1, data: { name: 'Gosho', email: 'email@example.com' } };
const maskedObject = { id: 1, data: { name: '[PII]Gosho[/PII]', email: '[PII]email@example.com[/PII]' } };
expect(masker.maskObject(originalObject, [['data.name', Tag.PII], ['data.email', Tag.PII]])).toEqual(maskedObject);
});

it('should work with numbers', () => {
const originalObject = { id: 1 };

expect(masker.maskObject(originalObject, [['id', Tag.PII]])).toEqual({
id: '[PII]1[/PII]',
});
});

it('should NOT wrap the provided tag around elements that are not specified for obfuscating in object', () => {
const originalObject = { favouriteColor: 'red', nested: { field: 'value' } };
expect(masker.maskObject(originalObject, [['name', Tag.PII]])).toEqual(originalObject);
});

it('should return a copy of the error and not modify the original', () => {
const originalError = new Error();
const maskedError = masker.maskObject(originalError, [['bar', Tag.PII]]);
expect(maskedError).not.toBe(originalError);
});

it('should preserve the prototype, name, message and stack of the error', () => {
class CustomError extends Error {}
const originalError = new CustomError();
const maskedError = masker.maskObject(originalError, [['bar', Tag.PII]]);

expect(maskedError).toBeInstanceOf(CustomError);
expect(maskedError.name).toEqual(originalError.name);
expect(maskedError.message).toEqual(originalError.message);
expect(maskedError.stack).toEqual(originalError.stack);
});

it('should mask error specific props', () => {
class CustomError extends Error {
bar = 'foo'
foo = {
test: 'test',
}
}
const originalError = new CustomError();
const maskedError = masker.maskObject(originalError, [
['bar', Tag.PII],
['foo.test', Tag.PII],
]);

expect(maskedError.bar).toEqual('[PII]foo[/PII]');
expect(maskedError.foo.test).toEqual('[PII]test[/PII]');
});
});
});
64 changes: 0 additions & 64 deletions src/tests/unit-tests/Obfuscator.test.ts

This file was deleted.

0 comments on commit 24e31d2

Please sign in to comment.