data-guardian
Version:
Tiny, zero-dependencies, package which tries to mask sensitive data in arbitrary collections, errors, objects and strings.
293 lines • 11.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.maskArguments = exports.maskData = exports.maskString = void 0;
const helpers_1 = require("../utils/helpers");
const defaultSensitiveKeyFragments = new Set([
'password',
'pwd',
'pass',
'token',
'secret',
'key',
'passphrase',
'privateKey',
'mail',
'shared',
'credential',
'auth',
'signature',
'certificate',
'crypt',
'apikey',
'security',
'phone',
'mobile'
]);
const sensitiveContentRegExp = {
uuid: /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi,
creditCard: /\b(?:\d[ -]*?){13,16}\b/g,
ssn: /\b[0-9]{3}-[0-9]{2}-[0-9]{4}\b/g,
url: /\b(?:https?|ftp):\/\/[a-z0-9-+&@#/%?=~_|!:,.;]*[a-z0-9-+&@#/%=~_|]\b/gi,
ipv4: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g,
email: /(?<=^|[\s'"-#+.><])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
passwordSubstring: /\b(?!password\b)\w*password\w*\b/gi,
password: /\b(?!\w*\/)(?=\S*\d)(?=\S*[A-Za-z])[\w!@#$%^&*()_+=\-,.]{6,32}\b/gm,
passwordInUri: /(?<=:\/\/[^:]+:)[^@]+?(?=@)/,
passwordMention: /(?<=.*(password|passwd|pwd)(?:\s*:\s*|\s+))\S+/gi
};
const SKIP_MASKING_PATTERN = /##([^#]*)##/g;
function maskSensitiveValue(value, options) {
const skipMaskingPattern = SKIP_MASKING_PATTERN; // Pattern to match content that should not be masked
const maskLength = (options === null || options === void 0 ? void 0 : options.maskLength) || value.length - 4;
const maskingChar = (options === null || options === void 0 ? void 0 : options.maskingChar) || '*';
// Skip masking if the entire value is wrapped in '##'
if (skipMaskingPattern.test(value)) {
return value;
}
if (options === null || options === void 0 ? void 0 : options.fixedMaskLength) {
return maskingChar.repeat(16);
}
if (value.length <= 4 || maskLength >= value.length) {
return maskingChar.repeat(value.length);
}
const visibleLength = (value.length - maskLength) / 2;
return (value.substring(0, visibleLength) +
maskingChar.repeat(maskLength) +
value.substring(value.length - visibleLength));
}
function shouldMaskKey(key) {
const lowerCaseKey = key.toLowerCase();
for (const sensitiveKey of defaultSensitiveKeyFragments) {
if (lowerCaseKey.includes(sensitiveKey)) {
return true;
}
}
return false;
}
function maskSensitiveContent(originalValue, defaultPatterns, customPatterns = {}, options) {
const allPatterns = Object.assign(Object.assign({}, defaultPatterns), customPatterns);
const skipMaskingPattern = SKIP_MASKING_PATTERN; // Pattern to match content that should not be masked
const applyPatternMasking = (currentValue, pattern) => {
let result = '';
let lastIndex = 0;
// Process parts of the string outside '##' blocks
currentValue.replace(skipMaskingPattern, (match, p1, offset) => {
// Apply masking to the content before the '##' block
const substring = currentValue.substring(lastIndex, offset);
result += substring.replace(pattern, match => maskSensitiveValue(match, options));
// Don't mask inside the '##' block
result += match;
lastIndex = offset + match.length;
return match; // Necessary for the replace function, though it's not used here
});
// Apply masking to the content after the last '##' block
const substring = currentValue.substring(lastIndex);
result += substring.replace(pattern, match => maskSensitiveValue(match, options));
pattern.lastIndex = 0; // Reset regex state for global patterns
return result;
};
return Object.values(allPatterns).reduce((maskedValue, pattern) => applyPatternMasking(maskedValue, pattern), originalValue);
}
function unmaskContent(value) {
const unmaskPattern = /##(.*?)##/g;
let isUnmasked = false;
const content = value.replace(unmaskPattern, (match, capture) => {
isUnmasked = true;
return capture;
});
return { isUnmasked, content };
}
function maskString(value, typesOrOptions, // This should be optional
customSensitiveContentRegExp = {}, options) {
if (!value || value.length === 0)
return value;
let types;
if (typesOrOptions && !Array.isArray(typesOrOptions)) {
// If typesOrOptions is not an array, it means it's the options parameter
options = typesOrOptions;
}
else {
types = typesOrOptions;
}
if (!value || value.length === 0)
return value;
// try to mask a stringified object correctly
if (value.startsWith('{') && value.endsWith('}')) {
let parsed;
try {
// if the provided string is nothing else but a stringified object,
// parse it back to object, mask and stringify it again
// ref issue: https://github.com/slippyex/data-guardian/issues/8
parsed = JSON.parse(value);
const resulting = maskData(parsed);
return JSON.stringify(resulting);
}
catch (err) {
// ignore error
}
}
// Check if the content is marked to be unmasked
const { isUnmasked, content } = unmaskContent(value);
if (isUnmasked) {
return content; // return content without '##' and without masking
}
if (!types)
types = Object.keys(sensitiveContentRegExp);
if (!customSensitiveContentRegExp)
customSensitiveContentRegExp = {};
if (options === null || options === void 0 ? void 0 : options.excludeMatchers) {
const excludeMatchersSet = new Set(options.excludeMatchers);
types = types.filter(t => !excludeMatchersSet.has(t));
}
const applicablePatterns = types.reduce((acc, type) => {
if (type in sensitiveContentRegExp) {
// check if 'type' is a valid key to satisfy TypeScript's safety checks
acc[type] = sensitiveContentRegExp[type]; // TypeScript now knows 'type' is a valid key
}
return acc;
}, {}); // Also define the accumulator's type
return maskSensitiveContent(value, applicablePatterns, customSensitiveContentRegExp, options);
}
exports.maskString = maskString;
function maskMap(item, options) {
// Process Map items
const processedMap = options.immutable ? new Map() : item;
item.forEach((value, key) => {
if (typeof key === 'string') {
processedMap.set(key, performMasking(key, value, options));
}
else {
// If the key is not a string, we can't check it against sensitive content, but we still mask the value.
processedMap.set(key, maskData(value, options));
}
});
return processedMap;
}
function maskSet(item, options) {
const processedSet = options.immutable ? new Set() : item;
const toAdd = [];
const toRemove = [];
item.forEach(value => {
let maskedValue;
if (typeof value === 'string') {
// If the value is a string, perform masking
maskedValue = maskString(value, undefined, options.customPatterns, options);
}
else if ((0, helpers_1.isObject)(value) || Array.isArray(value) || value instanceof Map) {
// If the value is an object, array, or map, recursively call maskData
maskedValue = maskData(value, options);
}
else {
// If the value is of any other type, just keep the original value
maskedValue = value;
}
if (options.immutable) {
// If immutability is required, add masked value to the new set
processedSet.add(maskedValue);
}
else {
// If immutability is not required, track the original and masked values for later update
toAdd.push(maskedValue);
toRemove.push(value);
}
});
if (!options.immutable) {
// Update the original set
toRemove.forEach(value => item.delete(value));
toAdd.forEach(value => item.add(value));
}
return processedSet;
}
function maskError(item, options) {
const maskedError = new Error(maskString(item.message, undefined, options.customPatterns, options));
maskedError.stack = maskString(item.stack, undefined, options.customPatterns, options);
return maskedError;
}
function maskObject(item, options) {
if ((0, helpers_1.hasCircularReference)(item)) {
return item;
}
// Clone the item if immutability is required
const processedItem = options.immutable ? (0, helpers_1.deepClone)(item) : item;
const assignMaskedValue = (obj, key, value) => {
obj[key] = performMasking(key, value, options);
};
return Object.entries(processedItem).reduce((acc, [key, value]) => {
assignMaskedValue(acc, key, value);
return acc;
}, options.immutable ? {} : processedItem);
}
function maskNumber(item, options) {
return maskSensitiveValue(String(item), options);
}
function maskArray(item, options) {
return item.map(i => maskData(i, options));
}
function performMasking(key, value, options) {
// Check if the content is marked to be unmasked and it's a string
if ((0, helpers_1.isString)(value)) {
const { isUnmasked, content } = unmaskContent(value);
if (isUnmasked) {
return content; // return content without '##' and without masking
}
}
if (options.keyCheck(key)) {
if (options.convertNumbersToStringWhenFieldMatch && (0, helpers_1.isNumber)(value)) {
return maskNumber(value, options);
}
if ((0, helpers_1.isString)(value)) {
return maskSensitiveValue(value, options); // Pass maskLength here
}
}
return maskData(value, options);
}
/**
* Masks data based on the type and the provided masking options.
* This function is recursively called for nested objects.
*
* @template T
* @param {T} item - The original data to mask.
* @param {Partial<IMaskDataOptions>} [options={}] - Optional masking options.
* @returns {T} The masked data.
*/
function maskData(item, options = {}) {
const { keyCheck = shouldMaskKey, immutable = true } = options;
if ((0, helpers_1.isString)(item)) {
// Pass custom regex patterns to maskString
return maskString(item, undefined, options.customPatterns, options);
}
options.keyCheck = keyCheck;
options.immutable = immutable;
if (item instanceof Map) {
return maskMap(item, options);
}
if (item instanceof Set) {
return maskSet(item, options);
}
if (item instanceof Error) {
return maskError(item, options);
}
if ((0, helpers_1.isObject)(item)) {
return maskObject(item, options);
}
if (Array.isArray(item)) {
return maskArray(item, options);
}
return item;
}
exports.maskData = maskData;
/**
* Masks arguments of a function call, based on the provided masking options.
*
* @template T
* @param {unknown[]} args - The original arguments to mask.
* @param {IMaskDataOptions} [options] - Optional masking options.
* @returns {T[]} The masked arguments.
*/
function maskArguments(args, options) {
if ((0, helpers_1.isNullish)(args) || args.length === 0)
return args;
return args.map(arg => typeof arg !== 'object' && typeof arg !== 'string' ? arg : maskData(arg, options));
}
exports.maskArguments = maskArguments;
//# sourceMappingURL=dataGuard.js.map