syntropylog
Version:
An instance manager with observability for Node.js applications
367 lines • 12.7 kB
JavaScript
/**
* FILE: src/masking/MaskingEngine.ts
* DESCRIPTION: Ultra-fast data masking engine using JSON flattening strategy.
*
* This engine flattens complex nested objects into linear key-value pairs,
* applies masking rules, and then reconstructs the original structure.
* This approach provides extreme processing speed for any object depth.
*/
// Using type assertion for regex-test module since it lacks proper TypeScript declarations
import RegexTest from 'regex-test';
/**
* @enum MaskingStrategy
* @description Different masking strategies for various data types.
*/
export var MaskingStrategy;
(function (MaskingStrategy) {
MaskingStrategy["CREDIT_CARD"] = "credit_card";
MaskingStrategy["SSN"] = "ssn";
MaskingStrategy["EMAIL"] = "email";
MaskingStrategy["PHONE"] = "phone";
MaskingStrategy["PASSWORD"] = "password";
MaskingStrategy["TOKEN"] = "token";
MaskingStrategy["CUSTOM"] = "custom";
})(MaskingStrategy || (MaskingStrategy = {}));
/**
* @class MaskingEngine
* Ultra-fast data masking engine using JSON flattening strategy.
*
* Instead of processing nested objects recursively, we flatten them to a linear
* structure for extreme processing speed. This approach provides O(n) performance
* regardless of object depth or complexity.
*/
export class MaskingEngine {
/** @private Array of masking rules */
rules = [];
/** @private Default mask character */
maskChar;
/** @private Whether to preserve original length by default */
preserveLength;
/** @private Whether the engine is initialized */
initialized = false;
/** @private Secure regex tester with timeout */
regexTest;
constructor(options) {
this.maskChar = options?.maskChar || '*';
this.preserveLength = options?.preserveLength ?? true; // Default to true for security
this.regexTest = new RegexTest({ timeout: 100 });
// Add default rules if enabled
if (options?.enableDefaultRules !== false) {
this.addDefaultRules();
}
// Add custom rules from options
if (options?.rules) {
for (const rule of options.rules) {
this.addRule(rule);
}
}
}
/**
* Adds default masking rules for common data types.
* @private
*/
addDefaultRules() {
const defaultRules = [
{
pattern: /credit_card|card_number|payment_number/i,
strategy: MaskingStrategy.CREDIT_CARD,
preserveLength: true,
maskChar: this.maskChar
},
{
pattern: /ssn|social_security|security_number/i,
strategy: MaskingStrategy.SSN,
preserveLength: true,
maskChar: this.maskChar
},
{
pattern: /email/i,
strategy: MaskingStrategy.EMAIL,
preserveLength: true,
maskChar: this.maskChar
},
{
pattern: /phone|phone_number|mobile_number/i,
strategy: MaskingStrategy.PHONE,
preserveLength: true,
maskChar: this.maskChar
},
{
pattern: /password|pass|pwd|secret/i,
strategy: MaskingStrategy.PASSWORD,
preserveLength: true,
maskChar: this.maskChar
},
{
pattern: /token|api_key|auth_token|jwt|bearer/i,
strategy: MaskingStrategy.TOKEN,
preserveLength: true,
maskChar: this.maskChar
}
];
for (const rule of defaultRules) {
this.addRule(rule);
}
}
/**
* Adds a custom masking rule.
* @param rule - The masking rule to add
*/
addRule(rule) {
// Compile regex pattern for performance
if (typeof rule.pattern === 'string') {
rule._compiledPattern = new RegExp(rule.pattern, 'i');
}
else {
rule._compiledPattern = rule.pattern;
}
// Set defaults
rule.preserveLength = rule.preserveLength ?? this.preserveLength;
rule.maskChar = rule.maskChar ?? this.maskChar;
this.rules.push(rule);
}
/**
* Processes a metadata object and applies the configured masking rules.
* Uses JSON flattening strategy for extreme performance.
* @param meta - The metadata object to process
* @returns A new object with the masked data
*/
process(meta) {
// Set initialized flag on first use
if (!this.initialized) {
this.initialized = true;
}
try {
// Apply masking rules directly to the data structure
const masked = this.applyMaskingRules(meta);
// Return the masked data
return masked;
}
catch (error) {
// Silent observer - return original data if masking fails
return meta;
}
}
/**
* Applies masking rules to data recursively.
* @param data - Data to mask
* @returns Masked data
* @private
*/
applyMaskingRules(data) {
if (data === null || typeof data !== 'object') {
return data;
}
if (Array.isArray(data)) {
return data.map(item => this.applyMaskingRules(item));
}
const masked = { ...data };
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
const value = data[key];
if (typeof value === 'string') {
// Check each rule
for (const rule of this.rules) {
if (rule._compiledPattern && rule._compiledPattern.test(key)) {
masked[key] = this.applyStrategy(value, rule);
break; // First matching rule wins
}
}
}
else if (typeof value === 'object' && value !== null) {
// Recursively mask nested objects
masked[key] = this.applyMaskingRules(value);
}
}
}
return masked;
}
/**
* Applies specific masking strategy to a value.
* @param value - Value to mask
* @param rule - Masking rule to apply
* @returns Masked value
* @private
*/
applyStrategy(value, rule) {
if (rule.strategy === MaskingStrategy.CUSTOM && rule.customMask) {
return rule.customMask(value);
}
switch (rule.strategy) {
case MaskingStrategy.CREDIT_CARD:
return this.maskCreditCard(value, rule);
case MaskingStrategy.SSN:
return this.maskSSN(value, rule);
case MaskingStrategy.EMAIL:
return this.maskEmail(value, rule);
case MaskingStrategy.PHONE:
return this.maskPhone(value, rule);
case MaskingStrategy.PASSWORD:
return this.maskPassword(value, rule);
case MaskingStrategy.TOKEN:
return this.maskToken(value, rule);
default:
return this.maskDefault(value, rule);
}
}
/**
* Masks credit card number.
* @param value - Credit card number
* @param rule - Masking rule
* @returns Masked credit card
* @private
*/
maskCreditCard(value, rule) {
const clean = value.replace(/\D/g, '');
if (rule.preserveLength) {
// Preserve original format, mask all but last 4 digits
return value.replace(/\d/g, (match, offset) => {
const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
return digitIndex < clean.length - 4 ? rule.maskChar : match;
});
}
else {
// Fixed format: ****-****-****-1111
return `${rule.maskChar.repeat(4)}-${rule.maskChar.repeat(4)}-${rule.maskChar.repeat(4)}-${clean.slice(-4)}`;
}
}
/**
* Masks SSN.
* @param value - SSN
* @param rule - Masking rule
* @returns Masked SSN
* @private
*/
maskSSN(value, rule) {
const clean = value.replace(/\D/g, '');
if (rule.preserveLength) {
// Preserve original format, mask all but last 4 digits
return value.replace(/\d/g, (match, offset) => {
const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
return digitIndex < clean.length - 4 ? rule.maskChar : match;
});
}
else {
// Fixed format: ***-**-6789
return `***-**-${clean.slice(-4)}`;
}
}
/**
* Masks email address.
* @param value - Email address
* @param rule - Masking rule
* @returns Masked email
* @private
*/
maskEmail(value, rule) {
const atIndex = value.indexOf('@');
if (atIndex > 0) {
const username = value.substring(0, atIndex);
const domain = value.substring(atIndex);
if (rule.preserveLength) {
// Preserve original length: first char + asterisks + @domain
const maskedUsername = username.length > 1
? username.charAt(0) + rule.maskChar.repeat(username.length - 1)
: rule.maskChar.repeat(username.length);
return maskedUsername + domain;
}
else {
return `${username.charAt(0)}***${domain}`;
}
}
return this.maskDefault(value, rule);
}
/**
* Masks phone number.
* @param value - Phone number
* @param rule - Masking rule
* @returns Masked phone number
* @private
*/
maskPhone(value, rule) {
const clean = value.replace(/\D/g, '');
if (rule.preserveLength) {
// Preserve original format, mask all but last 4 digits
return value.replace(/\d/g, (match, offset) => {
const digitIndex = value.substring(0, offset).replace(/\D/g, '').length;
return digitIndex < clean.length - 4 ? rule.maskChar : match;
});
}
else {
// Fixed format: ***-***-4567
return `${rule.maskChar.repeat(3)}-${rule.maskChar.repeat(3)}-${clean.slice(-4)}`;
}
}
/**
* Masks password.
* @param value - Password
* @param rule - Masking rule
* @returns Masked password
* @private
*/
maskPassword(value, rule) {
return rule.maskChar.repeat(value.length);
}
/**
* Masks token.
* @param value - Token
* @param rule - Masking rule
* @returns Masked token
* @private
*/
maskToken(value, rule) {
if (rule.preserveLength) {
return value.substring(0, 4) + rule.maskChar.repeat(value.length - 9) + value.substring(value.length - 5);
}
else {
if (value.length > 8) {
return value.substring(0, 4) + '...' + value.substring(value.length - 5);
}
return rule.maskChar.repeat(value.length);
}
}
/**
* Default masking strategy.
* @param value - Value to mask
* @param rule - Masking rule
* @returns Masked value
* @private
*/
maskDefault(value, rule) {
if (rule.preserveLength) {
return rule.maskChar.repeat(value.length);
}
else {
return rule.maskChar.repeat(Math.min(value.length, 8));
}
}
/**
* Gets masking engine statistics.
* @returns Dictionary with masking statistics
*/
getStats() {
return {
initialized: this.initialized,
totalRules: this.rules.length,
defaultRules: this.rules.filter(r => [MaskingStrategy.CREDIT_CARD, MaskingStrategy.SSN, MaskingStrategy.EMAIL,
MaskingStrategy.PHONE, MaskingStrategy.PASSWORD, MaskingStrategy.TOKEN].includes(r.strategy)).length,
customRules: this.rules.filter(r => r.strategy === MaskingStrategy.CUSTOM).length,
strategies: this.rules.map(r => r.strategy)
};
}
/**
* Checks if the masking engine is initialized.
* @returns True if initialized
*/
isInitialized() {
return this.initialized;
}
/**
* Shutdown the masking engine.
*/
shutdown() {
this.rules = [];
this.initialized = false;
}
}
//# sourceMappingURL=MaskingEngine.js.map