From bb2e0ed76958f8930a1193b17e45d239ac23c544 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Thu, 18 Apr 2024 19:59:21 -0700 Subject: [PATCH 1/4] Force redact all & Redact by Default --- src/constants.ts | 18 +++++- src/index.ts | 4 +- src/types.ts | 17 +++--- src/utils.test.ts | 151 +++++++++++++++++++++++++++++++++++++++++----- src/utils.ts | 106 +++++++++++++++++++++++--------- 5 files changed, 241 insertions(+), 55 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 831f69f..1055d35 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,6 +15,10 @@ const defaultConfig = { logResponseHeaders: true, logResponseBody: true, ignoredDomains: [], + forceRedactAll: false, + redactByDefault: false, + allowedDomains: [], + cacheTtl: 0, // After the close command is sent, wait for this many milliseconds before // exiting. This gives any hanging responses a chance to return. @@ -38,6 +42,16 @@ const errors = { 'No Client Secret Provided, set SUPERGOOD_CLIENT_SECRET or pass it as an argument' }; +const SensitiveKeyActions = { + REDACT: 'REDACT', + ALLOW: 'ALLOW' +}; + +const EndpointActions = { + ALLOW: 'Allow', + IGNORE: 'Ignore' +} + const TestErrorPath = '/api/supergood-test-error'; const LocalClientId = 'local-client-id'; const LocalClientSecret = 'local-client-secret'; @@ -47,5 +61,7 @@ export { errors, TestErrorPath, LocalClientId, - LocalClientSecret + LocalClientSecret, + SensitiveKeyActions, + EndpointActions }; diff --git a/src/index.ts b/src/index.ts index 150db97..a5e4fd7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -301,7 +301,7 @@ const Supergood = () => { const responseArray = prepareData( responseCacheValues as EventRequestType[], - supergoodConfig.remoteConfig, + supergoodConfig, ) as Array; let data = [...responseArray]; @@ -310,7 +310,7 @@ const Supergood = () => { if (force) { const requestArray = prepareData( requestCacheValues as EventRequestType[], - supergoodConfig.remoteConfig + supergoodConfig ) as Array; data = [...requestArray, ...responseArray]; } diff --git a/src/types.ts b/src/types.ts index c6e327b..bc33a0e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -43,8 +43,6 @@ interface ConfigType { allowedDomains: string[]; allowLocalUrls: boolean; allowIpAddresses: boolean; - cacheTtl: number; - keysToHash: string[]; remoteConfigFetchEndpoint: string; // Defaults to {baseUrl}/config if not provided eventSinkEndpoint: string; // Defaults to {baseUrl}/events if not provided errorSinkEndpoint: string; // Defaults to {baseUrl}/errors if not provided @@ -57,6 +55,8 @@ interface ConfigType { logRequestBody: boolean; logResponseHeaders: boolean; logResponseBody: boolean; + forceRedactAll: boolean; + redactByDefault: boolean; } interface TelemetryType { @@ -69,7 +69,7 @@ interface EndpointConfigType { location: string; regex: string; ignored: boolean; - sensitiveKeys: Array; + sensitiveKeys: Array<{ keyPath: string, action: string }>; } interface RemoteConfigType { @@ -85,19 +85,20 @@ interface MetadataType { serviceName?: string; } +type TagType = Record; interface EventRequestType { request: RequestType; response: ResponseType; - tags?: Record; + tags?: TagType; metadata?: { sensitiveKeys: Array; - tags?: Record; + tags?: TagType; }; } type SupergoodContext = { - tags: Record; + tags: TagType; }; // interface EventResponseType {} @@ -147,6 +148,7 @@ type RemoteConfigPayloadType = Array<{ sensitiveKeys: Array< { keyPath: string; + action: string; }>; } }>; @@ -174,5 +176,6 @@ export type { RemoteConfigPayloadType, MetadataType, TelemetryType, - SupergoodContext + SupergoodContext, + TagType }; diff --git a/src/utils.test.ts b/src/utils.test.ts index 44a224e..3058f53 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,9 +1,10 @@ -import { RequestType, ResponseType } from './types'; +import { RequestType, ResponseType, ConfigType } from './types'; import { prepareData, expandSensitiveKeySetForArrays, redactValuesFromKeys } from './utils'; +import { defaultConfig, SensitiveKeyActions, EndpointActions } from './constants'; import { get as _get } from 'lodash'; it('generates multiple sensitive key paths for an array', () => { @@ -34,13 +35,13 @@ it('generates multiple sensitive key paths for an array', () => { ] } }; - const sensitiveKeys = ['blog.posts[].title']; + const sensitiveKeys = [{ keyPath: 'blog.posts[].title', action: SensitiveKeyActions.REDACT}]; expect(expandSensitiveKeySetForArrays(obj, sensitiveKeys)).toEqual([ 'blog.posts[0].title', 'blog.posts[1].title', 'blog.posts[2].title', 'blog.posts[3].title' - ]); + ].map((key) => ({ keyPath: key, action: SensitiveKeyActions.REDACT }))); }); it('generates multiple sensitive key paths for an object with nested arrays', () => { @@ -129,7 +130,7 @@ it('generates multiple sensitive key paths for an object with nested arrays', () ] } }; - const sensitiveKeys = ['blog.posts[].comments[].body']; + const sensitiveKeys = [{ keyPath: 'blog.posts[].comments[].body', action: SensitiveKeyActions.REDACT }]; expect(expandSensitiveKeySetForArrays(obj, sensitiveKeys)).toEqual([ 'blog.posts[0].comments[0].body', 'blog.posts[0].comments[1].body', @@ -141,7 +142,7 @@ it('generates multiple sensitive key paths for an object with nested arrays', () 'blog.posts[2].comments[0].body', 'blog.posts[2].comments[1].body', 'blog.posts[3].comments[0].body' - ]); + ].map((key) => ({ keyPath: key, action: SensitiveKeyActions.REDACT }))); }); it('redacts values from keys with proper marshalling', () => { @@ -189,12 +190,13 @@ it('redacts values from keys with proper marshalling', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: ['requestBody.posts[].title'] + sensitiveKeys: [{ keyPath: 'requestBody.posts[].title', action: SensitiveKeyActions.REDACT }] } } }; - const redactedObj = redactValuesFromKeys(obj, remoteConfig); + const config = { remoteConfig, ...defaultConfig } as ConfigType; + const redactedObj = redactValuesFromKeys(obj, config); expect(_get(redactedObj, 'event.request.body.posts[0].title')).toBeNull(); expect(redactedObj.sensitiveKeyMetadata[0]).toEqual({ keyPath: 'requestBody.posts[0].title', @@ -306,12 +308,12 @@ it('redacts values from keys of nested array', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: ['requestBody.posts[].comments[].body'] + sensitiveKeys: [{ keyPath: 'requestBody.posts[].comments[].body', action: SensitiveKeyActions.REDACT }] } } }; - - const redactedObj = redactValuesFromKeys(obj, remoteConfig); + const config = { remoteConfig, ...defaultConfig } as ConfigType; + const redactedObj = redactValuesFromKeys(obj, config); expect( _get(redactedObj, 'event.request.body.posts[0].comments[0].body') ).toBeNull(); @@ -346,12 +348,12 @@ it('will not blow up or redact anything if the sensitive key is bad', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: ['request_body.posts[].title[]'] + sensitiveKeys: [{ keyPath: 'request_body.posts[].title[]', action: SensitiveKeyActions.REDACT }] } } }; - - const redactedObj = redactValuesFromKeys(obj, remoteConfig); + const config = { remoteConfig, ...defaultConfig } as ConfigType; + const redactedObj = redactValuesFromKeys(obj, config); expect(_get(redactedObj, 'event.request.body.name')).toBeTruthy(); expect(redactedObj.sensitiveKeyMetadata.length).toEqual(0); }); @@ -395,13 +397,130 @@ it('will prepare the data appropriately for posting to the server', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: ['responseBody.user.email', 'requestBody.blogType.name'] + sensitiveKeys: [ + { keyPath: 'responseBody.user.email', action: SensitiveKeyActions.REDACT}, + { keyPath: 'requestBody.blogType.name', action: SensitiveKeyActions.REDACT} + ] } } }; - - const events = prepareData([obj], remoteConfig); + const config = { remoteConfig, ...defaultConfig } as ConfigType; + const events = prepareData([obj], config); expect(_get(events[0], 'response.body.user.email')).toBeFalsy(); expect(_get(events[0], 'request.body.blogType.name')).toBeFalsy(); expect(events[0].metadata.sensitiveKeys.length).toEqual(2); }); + +it('will force redact all keys if the config is set to do so', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + blogType: { + name: 'My Blog' + } + } + }, + response: { + headers: {}, + status: 200, + statusText: 'OK', + respondedAt: new Date(), + body: { + name: 'My Blog', + user: { + name: 'John Doe', + email: 'john@doe.com' + }, + comments: [{ id: 7, comment: 'good blog'}, { id: 8, comment: 'bad blog'}] + } + } + }; + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: [] + } + } + }; + const config = { remoteConfig, ...defaultConfig, forceRedactAll: true } as ConfigType; + const events = prepareData([obj], config); + expect(_get(events[0], 'request.body.blogType.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.user.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.user.email')).toBeFalsy(); + expect(_get(events[0], 'response.body.comments[0].id')).toBeFalsy(); + expect(_get(events[0], 'response.body.comments[0].comment')).toBeFalsy(); + expect(_get(events[0], 'response.body.comments[1].id')).toBeFalsy(); + expect(_get(events[0], 'response.body.comments[1].comment')).toBeFalsy(); + expect(events[0].metadata.sensitiveKeys.length).toEqual(8); +}); + +it('will redact by default if the config is set to do so', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + blogType: { + name: 'My Blog' + } + } + }, + response: { + headers: {}, + status: 200, + statusText: 'OK', + respondedAt: new Date(), + body: { + name: 'My Blog', + user: { + name: 'John Doe', + email: 'john@doe.com' + }, + comments: [{ id: 7, comment: 'good blog'}, { id: 8, comment: 'bad blog'}] + } + } + }; + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: [ + { keyPath: 'responseBody.user.email', action: SensitiveKeyActions.ALLOW }, + { keyPath: 'requestBody.blogType.name', action: SensitiveKeyActions.REDACT }, + { keyPath: 'responseBody.comments[].id', action: SensitiveKeyActions.ALLOW } + ] + } + } + }; + const config = { remoteConfig, ...defaultConfig, redactByDefault: true } as ConfigType; + const events = prepareData([obj], config); + expect(_get(events[0], 'request.body.blogType.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.user.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.user.email')).toBeTruthy(); + expect(_get(events[0], 'response.body.comments[0].id')).toBeTruthy(); + expect(_get(events[0], 'response.body.comments[0].comment')).toBeFalsy(); + expect(_get(events[0], 'response.body.comments[1].id')).toBeTruthy(); + expect(_get(events[0], 'response.body.comments[1].comment')).toBeFalsy(); + expect(events[0].metadata.sensitiveKeys.length).toEqual(5); +}); diff --git a/src/utils.ts b/src/utils.ts index d9c8d89..263a121 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -9,7 +9,9 @@ import { RemoteConfigType, EndpointConfigType, SensitiveKeyMetadata, - TelemetryType + TelemetryType, + ConfigType, + TagType } from './types'; import { postError } from './api'; import { name, version } from '../package.json'; @@ -17,6 +19,7 @@ import https from 'https'; import http from 'http'; import { errors } from './constants'; import { get as _get, set as _set } from 'lodash'; +import { SensitiveKeyActions, EndpointActions } from './constants'; const logger = ({ errorSinkUrl, @@ -115,25 +118,27 @@ const unmarshalKeyPath = (keypath: string) => { const expandSensitiveKeySetForArrays = ( obj: any, - sensitiveKeys: Array -): Array => { - const expandKey = (key: string, obj: any): Array => { - // Split the key by dots, considering the array brackets as part of the key - const parts = key.match(/[^.\[\]]+|\[\d*\]|\[\*\]/g) || []; + sensitiveKeys: Array<{ keyPath: string; action: string }> +): Array<{ keyPath: string, action: string }> => { + const expandKey = (key: { keyPath: string; action: string }, obj: any): Array<{ keyPath: string; action: string }> => { + // Split the key by dots, considering the array brackets as part of the key + const parts = key?.keyPath.match(/[^.\[\]]+|\[\d*\]|\[\*\]/g) || []; // Recursively expand the key - return expand(parts, obj, ''); + return expand(parts, obj, { action: key.action, keyPath: '' }); }; const expand = ( parts: string[], obj: any, - keyPath: string - ): Array => { - const path = keyPath; + key: { keyPath: string; action: string } + ): Array<{ keyPath: string; action: string }> => { + + const path = key.keyPath; if (parts.length === 0) { - return [path]; // Remove trailing dot + return [{ keyPath: path, action: key.action }]; // Remove trailing dot } + const part = parts[0]; const isProperty = !part.startsWith('['); const separator = path && isProperty ? '.' : ''; @@ -145,20 +150,22 @@ const expandSensitiveKeySetForArrays = ( } // Expand for each element in the array return obj.flatMap((_, index) => - expand(parts.slice(1), obj[index], `${path}${separator}[${index}]`) + expand(parts.slice(1), obj[index], { keyPath: `${path}${separator}[${index}]`, action: key.action }) ); + } else if (part.startsWith('[') && part.endsWith(']')) { // Specific index in the array const index = parseInt(part.slice(1, -1), 10); if (!isNaN(index) && index < obj.length) { - return expand(parts.slice(1), obj[index], `${path}${separator}${part}`); + return expand(parts.slice(1), obj[index], { keyPath: `${path}${separator}${part}`, action: key.action }); } else { return []; } + } else { // Regular object property if (obj && typeof obj === 'object' && part in obj) { - return expand(parts.slice(1), obj[part], `${path}${separator}${part}`); + return expand(parts.slice(1), obj[part], { keyPath: `${path}${separator}${part}`, action: key.action }); } else { return []; } @@ -168,14 +175,37 @@ const expandSensitiveKeySetForArrays = ( return sensitiveKeys.flatMap((key) => expandKey(key, obj)); }; +function getKeyPaths(obj: any, path: string = ''): string[] { + let paths: string[] = []; + + if (typeof obj === 'object' && obj !== null) { + // Object.keys returns indeces for arrays + Object.keys(obj).forEach(key => { + const value = obj[key]; + const newPath = Array.isArray(obj) ? `${path}[${key}]` : `${path}${path ? '.' : ''}${key}`; + if (typeof value === 'object' && value !== null) { + paths = paths.concat(getKeyPaths(value, newPath)); + } else { + paths.push(newPath); + } + }); + } else { + paths.push(path); + } + + return paths; +} + const redactValuesFromKeys = ( - event: { request?: RequestType; response?: ResponseType, tags?: Record }, - remoteConfig: RemoteConfigType + event: { request?: RequestType; response?: ResponseType, tags?: TagType }, + config: ConfigType ): { event: { request?: RequestType; response?: ResponseType }; sensitiveKeyMetadata: Array; - tags: Record; + tags: TagType; } => { + const { redactByDefault, forceRedactAll } = config; + const remoteConfig = config?.remoteConfig || {} as RemoteConfigType; // Move the tags off the event object and into the metadata object let tags = {}; if(event.tags) { @@ -188,22 +218,40 @@ const redactValuesFromKeys = ( event.request as RequestType, remoteConfig ); - if (!endpointConfig || !endpointConfig?.sensitiveKeys?.length) + + if ((!endpointConfig || !endpointConfig?.sensitiveKeys?.length) && (!redactByDefault && !forceRedactAll)) { return { event, sensitiveKeyMetadata, tags }; - else { - const sensitiveKeys = expandSensitiveKeySetForArrays( + } else { + + const keyPathsForLeavesOnEvent = [ + ...getKeyPaths(event.request?.headers, 'request.headers'), + ...getKeyPaths(event.request?.body, 'request.body'), + ...getKeyPaths(event.response?.headers, 'response.headers'), + ...getKeyPaths(event.response?.body, 'response.body') + ].map((key) => ({ keyPath: key, action: SensitiveKeyActions.REDACT })) + + let sensitiveKeys = expandSensitiveKeySetForArrays( event, - endpointConfig.sensitiveKeys.map((key) => marshalKeyPath(key)) + (endpointConfig?.sensitiveKeys || []).map((key) => ({ keyPath: marshalKeyPath(key.keyPath), action: key.action })) ); + + if (forceRedactAll) { + sensitiveKeys = keyPathsForLeavesOnEvent + } else if (redactByDefault) { + sensitiveKeys = keyPathsForLeavesOnEvent.filter( + (key) => !sensitiveKeys.some(sk => sk.keyPath === key.keyPath && sk.action === SensitiveKeyActions.ALLOW) + ); + } + for (let i = 0; i < sensitiveKeys.length; i++) { - const keyPath = sensitiveKeys[i]; + const key = sensitiveKeys[i]; // Add sensitive key for array expansion - const value = _get(event, keyPath); + const value = _get(event, key.keyPath); if (value) { - _set(event, keyPath, null); + _set(event, key.keyPath, null); // Don't return : for null values sensitiveKeyMetadata.push({ - keyPath: unmarshalKeyPath(keyPath), + keyPath: unmarshalKeyPath(key.keyPath), ...redactValue(value) }); } @@ -250,12 +298,12 @@ const redactValue = ( const prepareData = ( events: Array, - remoteConfig: RemoteConfigType, + supergoodConfig: ConfigType, ) => { return events.map((e) => { const { event, sensitiveKeyMetadata, tags } = redactValuesFromKeys( e, - remoteConfig + supergoodConfig ); return { ...event, @@ -380,8 +428,8 @@ const processRemoteConfig = (remoteConfigPayload: RemoteConfigPayloadType) => { endpointConfig[regex] = { location, regex, - ignored: action === 'Ignore', - sensitiveKeys: (sensitiveKeys || []).map((key) => key.keyPath) + ignored: action === EndpointActions.IGNORE, + sensitiveKeys: (sensitiveKeys || []).map((key) => ({ keyPath: key.keyPath, action: key.action })) }; return endpointConfig; }, {} as { [endpointName: string]: EndpointConfigType }); From 9bb2c45928e3c2931e48b52817f536fe57d29b25 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Thu, 18 Apr 2024 20:28:25 -0700 Subject: [PATCH 2/4] Added test for arrays --- src/utils.test.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/utils.test.ts b/src/utils.test.ts index 3058f53..2a4180f 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -524,3 +524,57 @@ it('will redact by default if the config is set to do so', () => { expect(_get(events[0], 'response.body.comments[1].comment')).toBeFalsy(); expect(events[0].metadata.sensitiveKeys.length).toEqual(5); }); + +it('will redact by default for an array of strings', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + blogType: { + name: 'My Blog' + } + } + }, + response: { + headers: {}, + status: 200, + statusText: 'OK', + respondedAt: new Date(), + body: { + name: 'My Blog', + user: { + name: 'John Doe', + email: 'john@doe.com' + }, + tags: ['good blog', 'bad blog'] + } + } + }; + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: [] + } + } + }; + const config = { remoteConfig, ...defaultConfig, redactByDefault: true } as ConfigType; + const events = prepareData([obj], config); + expect(_get(events[0], 'request.body.blogType.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.user.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.user.email')).toBeFalsy(); + expect(_get(events[0], 'response.body.tags')).toBeTruthy(); + expect(_get(events[0], 'response.body.tags[0]')).toBeFalsy(); + expect(_get(events[0], 'response.body.tags[1]')).toBeFalsy(); + expect(events[0].metadata.sensitiveKeys.length).toEqual(6); +}); From 806f90c3c4869e667b7e326d06a02185c6a2e847 Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Fri, 19 Apr 2024 10:30:40 -0700 Subject: [PATCH 3/4] Fixed typo and fixed no feature --- src/utils.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++++- src/utils.ts | 6 ++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index 2a4180f..fb29e1d 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -563,7 +563,11 @@ it('will redact by default for an array of strings', () => { location: 'path', regex: '/posts', ignored: false, - sensitiveKeys: [] + sensitiveKeys: [ + { keyPath: 'responseBody.user.email', action: SensitiveKeyActions.ALLOW }, + { keyPath: 'requestBody.blogType.name', action: SensitiveKeyActions.REDACT }, + { keyPath: 'responseBody.comments[].id', action: SensitiveKeyActions.ALLOW } + ] } } }; @@ -578,3 +582,60 @@ it('will redact by default for an array of strings', () => { expect(_get(events[0], 'response.body.tags[1]')).toBeFalsy(); expect(events[0].metadata.sensitiveKeys.length).toEqual(6); }); + +it('will redact ONLY sensitive keys marked as redact, without either option enabled', () => { + const MOCK_DATA_SERVER = 'http://localhost:3001'; + const obj = { + request: { + id: '', + headers: {}, + method: 'GET', + url: `${MOCK_DATA_SERVER}/posts`, + path: '/posts', + search: '', + requestedAt: new Date(), + body: { + blogType: { + name: 'My Blog' + } + } + }, + response: { + headers: {}, + status: 200, + statusText: 'OK', + respondedAt: new Date(), + body: { + name: 'My Blog', + user: { + name: 'John Doe', + email: 'john@doe.com' + }, + comments: [{ id: 7, comment: 'good blog'}, { id: 8, comment: 'bad blog'}] + } + } + }; + const remoteConfig = { + [new URL(MOCK_DATA_SERVER).hostname]: { + '/posts': { + location: 'path', + regex: '/posts', + ignored: false, + sensitiveKeys: [ + { keyPath: 'responseBody.user.email', action: SensitiveKeyActions.ALLOW }, + { keyPath: 'requestBody.blogType.name', action: SensitiveKeyActions.REDACT }, + { keyPath: 'responseBody.comments[].id', action: SensitiveKeyActions.ALLOW } + ] + } + } + }; + const config = { remoteConfig, ...defaultConfig } as ConfigType; + const events = prepareData([obj], config); + expect(_get(events[0], 'request.body.blogType.name')).toBeFalsy(); + expect(_get(events[0], 'response.body.name')).toBeTruthy(); + expect(_get(events[0], 'response.body.user.name')).toBeTruthy(); + expect(_get(events[0], 'response.body.user.email')).toBeTruthy(); + expect(_get(events[0], 'response.body.comments')).toBeTruthy(); + expect(_get(events[0], 'response.body.comments[0].id')).toBeTruthy(); + expect(events[0].metadata.sensitiveKeys.length).toEqual(1); +}); diff --git a/src/utils.ts b/src/utils.ts index 263a121..bc6dc94 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -179,7 +179,7 @@ function getKeyPaths(obj: any, path: string = ''): string[] { let paths: string[] = []; if (typeof obj === 'object' && obj !== null) { - // Object.keys returns indeces for arrays + // Object.keys returns indices for arrays Object.keys(obj).forEach(key => { const value = obj[key]; const newPath = Array.isArray(obj) ? `${path}[${key}]` : `${path}${path ? '.' : ''}${key}`; @@ -236,11 +236,15 @@ const redactValuesFromKeys = ( ); if (forceRedactAll) { + // Sensitive keys = every leaf on the event sensitiveKeys = keyPathsForLeavesOnEvent } else if (redactByDefault) { + // Sensitive keys = All of the leaves on the event EXCEPT the ones marked allwoed from the remote config sensitiveKeys = keyPathsForLeavesOnEvent.filter( (key) => !sensitiveKeys.some(sk => sk.keyPath === key.keyPath && sk.action === SensitiveKeyActions.ALLOW) ); + } else { + sensitiveKeys = sensitiveKeys.filter((sk) => sk.action !== SensitiveKeyActions.ALLOW); } for (let i = 0; i < sensitiveKeys.length; i++) { From 62c7c2b7504400962c80735851625a86d4bd7cdd Mon Sep 17 00:00:00 2001 From: Alex Klarfeld Date: Fri, 19 Apr 2024 10:37:02 -0700 Subject: [PATCH 4/4] Fixed test --- src/utils.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils.test.ts b/src/utils.test.ts index fb29e1d..88e299a 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -566,7 +566,7 @@ it('will redact by default for an array of strings', () => { sensitiveKeys: [ { keyPath: 'responseBody.user.email', action: SensitiveKeyActions.ALLOW }, { keyPath: 'requestBody.blogType.name', action: SensitiveKeyActions.REDACT }, - { keyPath: 'responseBody.comments[].id', action: SensitiveKeyActions.ALLOW } + { keyPath: 'responseBody.tags[]', action: SensitiveKeyActions.ALLOW } ] } } @@ -576,11 +576,11 @@ it('will redact by default for an array of strings', () => { expect(_get(events[0], 'request.body.blogType.name')).toBeFalsy(); expect(_get(events[0], 'response.body.name')).toBeFalsy(); expect(_get(events[0], 'response.body.user.name')).toBeFalsy(); - expect(_get(events[0], 'response.body.user.email')).toBeFalsy(); + expect(_get(events[0], 'response.body.user.email')).toBeTruthy(); expect(_get(events[0], 'response.body.tags')).toBeTruthy(); - expect(_get(events[0], 'response.body.tags[0]')).toBeFalsy(); - expect(_get(events[0], 'response.body.tags[1]')).toBeFalsy(); - expect(events[0].metadata.sensitiveKeys.length).toEqual(6); + expect(_get(events[0], 'response.body.tags[0]')).toBeTruthy(); + expect(_get(events[0], 'response.body.tags[1]')).toBeTruthy(); + expect(events[0].metadata.sensitiveKeys.length).toEqual(3); }); it('will redact ONLY sensitive keys marked as redact, without either option enabled', () => {