Skip to content

Commit

Permalink
fix(core): improve substitute and safeStringify stability
Browse files Browse the repository at this point in the history
  • Loading branch information
jdharvey-ibm committed Nov 17, 2023
1 parent 5ef91ad commit a859944
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 31 deletions.
16 changes: 5 additions & 11 deletions src/main/core/anonymize/substitute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { safeStringify } from '../log/safe-stringify.js'
import { TypedKeyMap } from './typed-key-map.js'

const subs = new Map()
const subs = new TypedKeyMap()
let curSub = 1

/**
Expand All @@ -23,12 +23,8 @@ export function substitute<T extends Record<string, unknown>>(
allowedKeys: Array<keyof T>,
allowedValues: unknown[]
): T {
// for each raw entry, map it to something
const substitutedEntries = Object.entries(raw).map(([key, value]) => {
// Avoid object equality for complex values
value = safeStringify(value)

// Key is not safe. substitute key and value
// Key is not safe. Substitute key and value
if (!allowedKeys.includes(key)) {
if (!subs.has(key)) {
subs.set(key, nextSub())
Expand All @@ -40,9 +36,7 @@ export function substitute<T extends Record<string, unknown>>(
return { key: subs.get(key), value: subs.get(value) }
}

// Past this point, key is safe

// Value is a string that's not safe
// Key is safe. Value is a string that's not safe
if (typeof value === 'string' && !allowedValues.includes(value)) {
if (!subs.has(value)) {
subs.set(value, nextSub())
Expand All @@ -51,7 +45,7 @@ export function substitute<T extends Record<string, unknown>>(
return { key, value: subs.get(value) }
}

// Value is an object that's not safe
// Key is safe. Value is an object that's not safe
if (typeof value === 'object' && !allowedValues.includes(value)) {
if (!subs.has(value)) {
subs.set(value, nextSub())
Expand Down
42 changes: 42 additions & 0 deletions src/main/core/anonymize/typed-key-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright IBM Corp. 2023, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { safeStringify } from '../log/safe-stringify.js'

/**
* Extension of a Map that uses a custom key generator. The generator uses the MapKey class to wrap
* incoming keys, including their type and value in the MapKey object. A stringified representation
* of the MapKey is used as the key of the entry in the TypedKeyMap object.
*/
export class TypedKeyMap extends Map {
override has(key: unknown): boolean {
const mapKey = new MapKey(typeof key, key)

return super.has(safeStringify(mapKey))
}

override get(key: unknown) {
const mapKey = new MapKey(typeof key, key)

return super.get(safeStringify(mapKey))
}

override set(key: unknown, value: unknown): this {
const mapKey = new MapKey(typeof key, key)

return super.set(safeStringify(mapKey), value)
}
}

/**
* Keys of the TypedKeyMap class.
*/
class MapKey {
constructor(
public readonly type: string,
public readonly val: unknown
) {}
}
28 changes: 10 additions & 18 deletions src/main/core/log/safe-stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,25 @@
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import util from 'util'

/**
* Converts any argument to it's string representation.
* Converts any argument to it's string representation using node's util.inspect function.
*
* @param arg - The element to stringify.
* @returns - The string representation of the supplied argument.
*/
export function safeStringify(arg: unknown): string {
let result

if (typeof arg === 'string') {
return arg
}

if (arg instanceof Function) {
return (arg.name === '' ? 'unknownFn' : arg.name) + '(...)'
}

if (result === undefined) {
try {
result = JSON.stringify(arg)
} catch {}
}

if (result === undefined) {
result = String(arg)
}

return result
return util.inspect(arg, {
compact: true,
breakLength: Infinity,
depth: 20,
showHidden: false,
maxArrayLength: 10000,
maxStringLength: 10000
})
}
7 changes: 5 additions & 2 deletions src/main/scopes/jsx/get-tracked-source-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import path from 'node:path'
import * as ts from 'typescript'

import { type Logger } from '../../core/log/logger.js'
import { safeStringify } from '../../core/log/safe-stringify.js'
import { TrackedFileEnumerator } from '../../core/tracked-file-enumerator.js'

/**
Expand Down Expand Up @@ -48,6 +47,10 @@ export async function getTrackedSourceFiles(root: string, logger: Logger) {

const results = await Promise.all(promises)

logger.traceExit('', 'getTrackedSourceFiles', safeStringify(results))
logger.traceExit(
'',
'getTrackedSourceFiles',
results.map((result) => result.fileName)
)
return results
}
94 changes: 94 additions & 0 deletions src/test/core/anonymize/substitute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright IBM Corp. 2023, 2023
*
* This source code is licensed under the Apache-2.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
import { describe, expect, it } from 'vitest'

import { substitute } from '../../../main/core/anonymize/substitute.js'

describe('substitute', () => {
it('correctly anonymizes sensitive data', () => {
const obj = {
sensitiveKey: 'sensitive value'
}
const anonymized = substitute(obj, [], [])

expect(anonymized.sensitiveKey).toBeUndefined()
expect(Object.values(anonymized)).not.toContain('sensitive value')
})

it('does not anonymize an allowed key, but anonymizes a sensitive value', () => {
const obj = {
knownKey: 'cool sensitive value'
}
const anonymized = substitute(obj, ['knownKey'], [])

expect(anonymized.knownKey).not.toBe('cool sensitive value')
})

it('does not anonymize an allowed key/value combo', () => {
const obj = {
knownKey: 'known value'
}
const anonymized = substitute(obj, ['knownKey'], ['known value'])

expect(anonymized).toMatchObject({
knownKey: 'known value'
})
})

it('reuses a substitution key/value that appears more than once', () => {
const obj1 = {
sensitiveKey: 'sensitive value'
}
const obj2 = {
sensitiveKey: 'sensitive value'
}

const anon1 = substitute(obj1, [], [])
const anon2 = substitute(obj2, [], [])

expect(Object.keys(anon1)).toStrictEqual(Object.keys(anon2))
expect(Object.values(anon1)).toStrictEqual(Object.values(anon2))
})

it('anonymizes a sensitive object', () => {
const obj = {
knownKey: { some: 'object' }
}

substitute(obj, [], [])
const anonymized = substitute(obj, ['knownKey'], [])

expect(anonymized).toMatchObject({
knownKey: '[redacted5]'
})
})

it('anonymizes two identical objects to the same value', () => {
const obj1 = {
knownKey: { some: 'object' }
}
const obj2 = {
knownKey: { some: 'object' }
}

const anon1 = substitute(obj1, ['knownKey'], [])
const anon2 = substitute(obj2, ['knownKey'], [])

expect(anon1.knownKey).toStrictEqual(anon2.knownKey)
})

it('manifests an object and a string that looks like the object differently', () => {
const obj = {
knownKey1: { some: 'object' },
knownKey2: "{ some: 'object' }"
}

const anon = substitute(obj, ['knownKey1', 'knownKey2'], [])

expect(anon.knownKey1).not.toBe(anon.knownKey2)
})
})

0 comments on commit a859944

Please sign in to comment.