Skip to content

Commit

Permalink
add an integration scrubbing event parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
ma2gedev committed Feb 20, 2020
1 parent 47771a1 commit 6a87cde
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/integrations/src/index.ts
Expand Up @@ -6,6 +6,7 @@ export { Ember } from './ember';
export { ExtraErrorData } from './extraerrordata';
export { ReportingObserver } from './reportingobserver';
export { RewriteFrames } from './rewriteframes';
export { Scrub } from './scrub';
export { SessionTiming } from './sessiontiming';
export { Transaction } from './transaction';
export { Vue } from './vue';
102 changes: 102 additions & 0 deletions packages/integrations/src/scrub.ts
@@ -0,0 +1,102 @@
import { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types';
import { isPlainObject, isRegExp } from '@sentry/utils';

/** JSDoc */
interface ScrubOptions {
sanitizeKeys: Array<string | RegExp>;
}

/** JSDoc */
export class Scrub implements Integration {
/**
* @inheritDoc
*/
public name: string = Scrub.id;

/**
* @inheritDoc
*/
public static id: string = 'Scrub';

/** JSDoc */
private readonly _options: ScrubOptions;
private readonly _sanitizeMask: string;
private _lazySanitizeRegExp?: RegExp;

/**
* @inheritDoc
*/
public constructor(options: ScrubOptions) {
this._options = {
sanitizeKeys: [],
...options,
};
this._sanitizeMask = '********';
}

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
addGlobalEventProcessor((event: Event, _hint?: EventHint) => {
const self = getCurrentHub().getIntegration(Scrub);
if (self) {
return self.process(event);
}
return event;
});
}

/** JSDoc */
public process(event: Event): Event {
if (this._options.sanitizeKeys.length === 0) {
// nothing to sanitize
return event;
}

return this._sanitize(event) as Event;
}

/**
* lazily generate regexp
*/
private _sanitizeRegExp(): RegExp {
if (this._lazySanitizeRegExp) {
return this._lazySanitizeRegExp;
}

const sources = this._options.sanitizeKeys.reduce(
(acc, key) => {
if (typeof key === 'string') {
// escape string value
// see also: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Escaping
acc.push(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
} else if (isRegExp(key)) {
acc.push(key.source);
}
return acc;
},
[] as string[],
);

return (this._lazySanitizeRegExp = RegExp(sources.join('|'), 'i'));
}

/**
* sanitize event data recursively
*/
private _sanitize(input: unknown): unknown {
if (Array.isArray(input)) {
return input.map(value => this._sanitize(value));
}

if (isPlainObject(input)) {
const inputVal = input as { [key: string]: unknown };
return Object.keys(inputVal).reduce<Record<string, unknown>>((acc, key) => {
acc[key] = this._sanitizeRegExp().test(key) ? this._sanitizeMask : this._sanitize(inputVal[key]);
return acc;
}, {});
}
return input;
}
}
141 changes: 141 additions & 0 deletions packages/integrations/test/scrub.test.ts
@@ -0,0 +1,141 @@
import { Scrub } from '../src/scrub';

/** JSDoc */
function clone<T>(data: T): T {
return JSON.parse(JSON.stringify(data));
}

let scrub: Scrub;
const sanitizeMask = '********';
const messageEvent = {
fingerprint: ['MrSnuffles'],
message: 'PickleRick',
stacktrace: {
frames: [
{
colno: 1,
filename: 'filename.js',
function: 'function',
lineno: 1,
},
{
colno: 2,
filename: 'filename.js',
function: 'function',
lineno: 2,
},
],
},
};

describe('Scrub', () => {
describe('sanitizeKeys option is empty', () => {
beforeEach(() => {
scrub = new Scrub({
sanitizeKeys: [],
});
});

it('should not affect any changes', () => {
const event = clone(messageEvent);
const processedEvent = scrub.process(event);
expect(processedEvent).toEqual(event);
});
});

describe('sanitizeKeys option has type of string', () => {
beforeEach(() => {
scrub = new Scrub({
sanitizeKeys: ['message', 'filename'],
});
});

it('should mask matched value in object', () => {
const event = scrub.process(clone(messageEvent));
expect(event.message).toEqual(sanitizeMask);
});

it('should not mask unmatched value in object', () => {
const event = scrub.process(clone(messageEvent));
expect(event.fingerprint).toEqual(messageEvent.fingerprint);
});

it('should mask matched value in Array', () => {
const event: any = scrub.process(clone(messageEvent));
expect(event.stacktrace.frames[0].filename).toEqual(sanitizeMask);
expect(event.stacktrace.frames[1].filename).toEqual(sanitizeMask);
});

it('should not mask unmatched value in Array', () => {
const event: any = scrub.process(clone(messageEvent));
expect(event.stacktrace.frames[0].function).toEqual(messageEvent.stacktrace.frames[0].function);
expect(event.stacktrace.frames[1].function).toEqual(messageEvent.stacktrace.frames[1].function);
});
});

describe('sanitizeKeys option has type of RegExp', () => {
beforeEach(() => {
scrub = new Scrub({
sanitizeKeys: [/^name$/],
});
});

it('should mask only matched value', () => {
const testEvent: any = {
filename: 'to be show',
name: 'do not show',
};
const event: any = scrub.process(testEvent);
expect(event.filename).toEqual(testEvent.filename);
expect(event.name).toEqual(sanitizeMask);
});
});

describe('sanitizeKeys option has mixed type of RegExp and string', () => {
beforeEach(() => {
scrub = new Scrub({
sanitizeKeys: [/^filename$/, 'function'],
});
});

it('should mask only matched value', () => {
const event: any = scrub.process(clone(messageEvent));
expect(event.stacktrace.frames[0].function).toEqual(sanitizeMask);
expect(event.stacktrace.frames[1].function).toEqual(sanitizeMask);
expect(event.stacktrace.frames[0].filename).toEqual(sanitizeMask);
expect(event.stacktrace.frames[1].filename).toEqual(sanitizeMask);
});

it('should not mask unmatched value', () => {
const event: any = scrub.process(clone(messageEvent));
expect(event.stacktrace.frames[0].colno).toEqual(messageEvent.stacktrace.frames[0].colno);
expect(event.stacktrace.frames[1].colno).toEqual(messageEvent.stacktrace.frames[1].colno);
expect(event.stacktrace.frames[0].lineno).toEqual(messageEvent.stacktrace.frames[0].lineno);
expect(event.stacktrace.frames[1].lineno).toEqual(messageEvent.stacktrace.frames[1].lineno);
});
});

describe('event has circular objects', () => {
beforeEach(() => {
scrub = new Scrub({
sanitizeKeys: [/message/],
});
});

it('should not show call stack size exceeded', () => {
const event: any = {
contexts: {},
extra: {
message: 'do not show',
},
};
event.contexts.circular = event.contexts;

const actual = scrub.process(event);
expect(actual).toEqual({
contexts: { circular: '[Circular ~]' },
extra: { message: sanitizeMask },
});
});
});
});

0 comments on commit 6a87cde

Please sign in to comment.