UNPKG

@whook/whook

Version:

Build strong and efficient REST web services.

223 lines (208 loc) 6.22 kB
import { autoService, location } from 'knifecycle'; import { type LogService } from 'common-services'; import { type JsonValue } from 'type-fest'; import { type WhookHeaders } from '../index.js'; export type WhookSensibleValueDescriptor = { name: string; pattern: RegExp; clearIndices: number[]; }; export type WhookObfuscatorConfig = { SHIELD_CHAR?: string; MAX_CLEAR_CHARS?: number; MAX_CLEAR_RATIO?: number; SENSIBLE_PROPS?: WhookSensibleValueDescriptor[]; SENSIBLE_HEADERS?: WhookSensibleValueDescriptor[]; }; export type WhookObfuscatorDependencies = { log?: LogService; } & WhookObfuscatorConfig; export type WhookObfuscatorService = { obfuscate: (secret: string) => string; obfuscateSensibleProps: ( propValue: JsonValue, propName?: string, ) => JsonValue; obfuscateSensibleHeaders: (headers: WhookHeaders) => WhookHeaders; }; export default location(autoService(initObfuscator), import.meta.url); const noop = () => undefined; const DEFAULT_MAX_CLEAR_CHARS = 6; const DEFAULT_MAX_CLEAR_RATIO = 4; const DEFAULT_SHIELD_CHAR = '🛡'; const DEFAULT_SENSIBLE_HEADERS: WhookSensibleValueDescriptor[] = [ { name: 'authorization', pattern: /^(Bearer |Basic )(.*)$/i, clearIndices: [0], }, { name: 'cookie', pattern: /^(.*)$/i, clearIndices: [] }, { name: 'set-cookie', pattern: /^([^=]+=)([^;]*)(.*)$/i, clearIndices: [0, 2], }, ]; const DEFAULT_SENSIBLE_PROPS: WhookSensibleValueDescriptor[] = [ { name: 'access_token', pattern: /^(.*)$/i, clearIndices: [0], }, { name: 'refresh_token', pattern: /^(.*)$/i, clearIndices: [0], }, { name: 'password', pattern: /^(.*)$/i, clearIndices: [], }, { name: 'token', pattern: /^(.*)$/i, clearIndices: [], }, ...DEFAULT_SENSIBLE_HEADERS, ]; /** * Obfuscate sensible information. * @param {Object} services * The service depend on * @param {Object} [services.SHIELD_CHAR] * The char for replacing sensible information * @param {Object} [services.MAX_CLEAR_CHARS] * The maximum clear chars to display * @param {Object} [services.MAX_CLEAR_RATIO] * The maximum clear chars ratio to display * @param {Object} [services.SENSIBLE_PROPS] * Sensible properties names * @param {Object} [services.SENSIBLE_HEADERS] * Sensible headers names * @return {Promise<Object>} * A promise of an object containing the gathered constants. * @example * import { initObfuscator } from '@whook/whook'; * import { alsoInject } from 'knifecycle'; * import { log } from 'node:console'; * * const obfuscator = await initObfuscator(); * * log(obfuscator('my very secret information!)); * // my ...on! */ async function initObfuscator({ SHIELD_CHAR = DEFAULT_SHIELD_CHAR, MAX_CLEAR_CHARS = DEFAULT_MAX_CLEAR_CHARS, MAX_CLEAR_RATIO = DEFAULT_MAX_CLEAR_RATIO, SENSIBLE_PROPS = DEFAULT_SENSIBLE_PROPS, SENSIBLE_HEADERS = DEFAULT_SENSIBLE_HEADERS, log = noop, }: WhookObfuscatorDependencies): Promise<WhookObfuscatorService> { log('debug', '🕶️ - Initializing the obfuscator service.'); return { obfuscate, obfuscateSensibleProps, obfuscateSensibleHeaders, }; function obfuscate(secret) { const numClearChars = Math.min( MAX_CLEAR_CHARS, Math.floor(secret.length / MAX_CLEAR_RATIO), ); if (numClearChars <= 1) { return SHIELD_CHAR; } return ( secret.slice(0, Math.ceil(numClearChars / 2)) + '...' + secret.slice(secret.length - Math.floor(numClearChars / 2)) ); } function selectivelyObfuscateAll( pattern: RegExp, clearIndices: number[], values: string | string[], ) { return values instanceof Array ? values.map((value) => selectivelyObfuscate(pattern, clearIndices, value), ) : selectivelyObfuscate(pattern, clearIndices, values); } function selectivelyObfuscate( pattern: RegExp, clearIndices: number[], value: string, ) { // SECURITY: Here, we first test the pattern to ensure // the selective obfuscation will work, if not, we obfuscate // the whole value to default to security return 'string' !== typeof value ? obfuscate('') : pattern.test(value) ? value.replace(pattern, (...args) => { return args .slice(1, -2) .map((value, index) => clearIndices.includes(index) ? value : obfuscate(value), ) .join(''); }) : obfuscate(value); } function obfuscateSensibleHeaders(headers: WhookHeaders): WhookHeaders { return Object.keys(headers).reduce((finalHeaders, headerName) => { const sensibleHeader = SENSIBLE_HEADERS.find( (sensibleHeader) => sensibleHeader.name.toLowerCase() === headerName.toLowerCase(), ); return { ...finalHeaders, [headerName]: sensibleHeader ? selectivelyObfuscateAll( sensibleHeader.pattern, sensibleHeader.clearIndices, headers[headerName], ) : headers[headerName], }; }, {}); } function obfuscateSensibleProps( propValue: JsonValue, propName = '_', ): JsonValue { if (propValue instanceof Array) { return propValue.map((value) => obfuscateSensibleProps(value, propName)); } else if (typeof propValue === 'object' && propValue !== null) { return Object.keys(propValue).reduce( (newObject, key) => ({ ...newObject, [key]: obfuscateSensibleProps(propValue[key] as JsonValue, key), }), {}, ); } else if ( typeof propValue === 'boolean' || typeof propValue === 'string' || typeof propValue === 'number' ) { const sensibleProp = SENSIBLE_PROPS.find( (sensibleProp) => sensibleProp.name.toLowerCase() === propName.toLowerCase(), ); return sensibleProp ? selectivelyObfuscate( sensibleProp.pattern, sensibleProp.clearIndices, propValue.toString(), ) : propValue; } else if (null === propValue) { return null; } return null; } }