UNPKG

autotel

Version:
377 lines (375 loc) 12.7 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); //#region src/attribute-redacting-processor.ts /** * Built-in patterns for detecting sensitive data */ const REDACTOR_PATTERNS = { email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/gi, phone: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, ssn: /\b\d{3}[-]?\d{2}[-]?\d{4}\b/g, creditCard: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, bearerToken: /Bearer\s+[A-Za-z0-9._~+/=-]+/gi, apiKeyInValue: /(?:api[_-]?key|apikey|api_secret)[=:][\s"']*[A-Za-z0-9_-]+/gi, jwt: /eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g, sensitiveKey: /^(password|passwd|pwd|secret|token|api[_-]?key|auth|credential|private[_-]?key|authorization)$/i }; /** * Built-in PII detection patterns with smart masking. * Each builtin preserves just enough signal for debugging while scrubbing PII. */ const builtinPatterns = { /** Credit card numbers → ****1111 (PCI DSS: last 4 allowed) */ creditCard: { pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, mask: (m) => `****${m.replace(/[\s-]/g, "").slice(-4)}` }, /** Email addresses → a***@***.com */ email: { pattern: /[\w.+-]+@[\w-]+\.[\w.]+/g, mask: (m) => { if (m.indexOf("@") < 1) return "***@***"; const tld = m.slice(m.lastIndexOf(".")); return `${m[0]}***@***${tld}`; } }, /** IPv4 addresses → ***.***.***.100 (last octet only) */ ipv4: { pattern: /\b(?!0\.0\.0\.0\b)(?!127\.0\.0\.1\b)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, mask: (m) => `***.***.***.${m.split(".").pop()}` }, /** * International / formatted phone numbers. * * Matches: * - `+33 1 23 45 67 89` -> `+33******89` * - `(415) 555-1234` -> `********34` * - `555-123-4567` / `555.123.4567` / `5551234567` -> `********67` * * Bare short digit runs like `12345678` are intentionally not matched. */ phone: { pattern: /(?:\+\d{1,3}[\s.-]?\(?\d{1,4}\)?(?:[\s.-]?\d{2,4}){2,4}|\(\d{1,4}\)(?:[\s.-]?\d{2,4}){2,4}|\b\d{3}[-.]?\d{3}[-.]?\d{4}\b)/g, mask: (m) => { const digits = m.replace(/[^\d]/g, ""); if (m.startsWith("+") && digits.length > 4) { const ccMatch = m.match(/^\+\d{1,3}/); return `${ccMatch ? ccMatch[0] : "+"}******${digits.slice(-2)}`; } if (digits.length > 2) return `${"*".repeat(digits.length - 2)}${digits.slice(-2)}`; return "***"; } }, /** JWT tokens → eyJ***.*** */ jwt: { pattern: /\beyJ[\w-]*\.[\w-]*\.[\w-]*\b/g, mask: () => "eyJ***.***" }, /** Bearer tokens → Bearer *** */ bearer: { pattern: /\bBearer\s+[\w\-.~+/]{8,}=*/gi, mask: () => "Bearer ***" }, /** IBAN → FR76****189 (country + check digits + last 3) */ iban: { pattern: /\b[A-Z]{2}\d{2}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{4}[\s-]?[\dA-Z]{0,4}[\s-]?[\dA-Z]{0,4}[\s-]?[\dA-Z]{0,4}\b/g, mask: (m) => { const clean = m.replace(/[\s-]/g, ""); return `${clean.slice(0, 4)}****${clean.slice(-3)}`; } } }; function cloneRegex(re) { return new RegExp(re.source, re.flags); } function isPlainObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function toRegExp(value) { if (value instanceof RegExp) return value; if (typeof value === "string") return new RegExp(value, "g"); if (isPlainObject(value) && typeof value.source === "string") { const flags = typeof value.flags === "string" ? value.flags : "g"; return new RegExp(value.source, flags); } } function toRegExpArray(value) { if (!Array.isArray(value)) return void 0; const out = []; for (const item of value) { const re = toRegExp(item); if (re) out.push(re); } return out.length > 0 ? out : []; } function builtinToValuePattern(name) { const b = builtinPatterns[name]; return { name, pattern: cloneRegex(b.pattern), mask: b.mask }; } /** * Default value patterns for the 'default' preset */ const DEFAULT_VALUE_PATTERNS = [ builtinToValuePattern("email"), builtinToValuePattern("phone"), { name: "ssn", pattern: REDACTOR_PATTERNS.ssn }, builtinToValuePattern("creditCard") ]; /** * Built-in redactor presets */ const REDACTOR_PRESETS = { /** * Default preset - covers common PII patterns with smart masking * Detects: emails (a***@***.com), phone numbers, SSNs, credit cards (****1111) * Redacts keys: password, secret, token, apiKey, auth, credential */ default: { keyPatterns: [REDACTOR_PATTERNS.sensitiveKey], valuePatterns: DEFAULT_VALUE_PATTERNS, builtins: true, replacement: "[REDACTED]" }, /** * Strict preset - more aggressive redaction for high-security environments * Includes everything in default plus: Bearer tokens, JWTs, IBAN, API keys in values */ strict: { keyPatterns: [ REDACTOR_PATTERNS.sensitiveKey, /bearer/i, /jwt/i ], valuePatterns: [ ...DEFAULT_VALUE_PATTERNS, builtinToValuePattern("jwt"), builtinToValuePattern("bearer"), builtinToValuePattern("iban"), { name: "apiKeyInValue", pattern: REDACTOR_PATTERNS.apiKeyInValue } ], builtins: true, replacement: "[REDACTED]" }, /** * PCI-DSS preset - focused on payment card industry compliance * Redacts: credit card numbers (****1111), CVV-like patterns, card-related keys */ "pci-dss": { keyPatterns: [ /card/i, /cvv/i, /cvc/i, /pan/i, /expir/i, /ccn/i ], valuePatterns: [builtinToValuePattern("creditCard")], builtins: ["creditCard"], replacement: "[REDACTED]" } }; /** * Normalize redactor config that may have been deserialized from JSON/YAML. * Converts regex-like values back to RegExp instances. */ function normalizeAttributeRedactorConfig(raw) { if (raw === void 0 || raw === null) return void 0; if (typeof raw === "string") return raw; if (!isPlainObject(raw)) return void 0; const config = {}; if (Array.isArray(raw.paths)) config.paths = raw.paths.filter((value) => typeof value === "string"); if (typeof raw.replacement === "string") config.replacement = raw.replacement; if (typeof raw.builtins === "boolean") config.builtins = raw.builtins; else if (Array.isArray(raw.builtins)) config.builtins = raw.builtins.filter((name) => typeof name === "string"); if (typeof raw.redactor === "function") config.redactor = raw.redactor; const keyPatterns = toRegExpArray(raw.keyPatterns); if (keyPatterns) config.keyPatterns = keyPatterns; const patterns = toRegExpArray(raw.patterns); if (patterns) config.patterns = patterns; if (Array.isArray(raw.valuePatterns)) { const valuePatterns = []; for (const item of raw.valuePatterns) { if (!isPlainObject(item) || typeof item.name !== "string") continue; const pattern = toRegExp(item.pattern); if (!pattern) continue; valuePatterns.push({ name: item.name, pattern, replacement: typeof item.replacement === "string" ? item.replacement : void 0, mask: typeof item.mask === "function" ? item.mask : void 0 }); } config.valuePatterns = valuePatterns; } return config; } /** * Resolve config to a normalized form */ function resolveConfig(config) { const normalized = normalizeAttributeRedactorConfig(config); if (!normalized) throw new Error("Invalid attribute redactor config"); if (typeof normalized === "string") { const preset = REDACTOR_PRESETS[normalized]; if (!preset) throw new Error(`Unknown attribute redactor preset: "${normalized}". Available presets: ${Object.keys(REDACTOR_PRESETS).join(", ")}`); return preset; } const resolvedConfig = { ...normalized, keyPatterns: normalized.keyPatterns ? [...normalized.keyPatterns] : void 0, valuePatterns: normalized.valuePatterns ? [...normalized.valuePatterns] : void 0, paths: normalized.paths ? [...normalized.paths] : void 0, patterns: normalized.patterns ? [...normalized.patterns] : void 0 }; if (resolvedConfig.builtins !== false) { const builtinValuePatterns = (Array.isArray(resolvedConfig.builtins) ? resolvedConfig.builtins : Object.keys(builtinPatterns)).filter((name) => name in builtinPatterns).map(builtinToValuePattern); resolvedConfig.valuePatterns = [...resolvedConfig.valuePatterns ?? [], ...builtinValuePatterns]; } return resolvedConfig; } /** * Create a redactor function from config */ function createRedactorFromConfig(config) { if (config.redactor) return config.redactor; const keyPatterns = config.keyPatterns ?? []; const valuePatterns = config.valuePatterns ?? []; const paths = config.paths ?? []; const pathSet = new Set(paths); const customPatterns = config.patterns ?? []; const defaultReplacement = config.replacement ?? "[REDACTED]"; const maskers = valuePatterns.filter((vp) => vp.mask).map((vp) => [cloneRegex(vp.pattern), vp.mask]); return (key, value) => { if (typeof value === "string") { for (const pattern of keyPatterns) { pattern.lastIndex = 0; if (pattern.test(key)) return defaultReplacement; } if (pathSet.has(key)) return defaultReplacement; } if (typeof value !== "string") { if (Array.isArray(value)) return value.map((item) => { if (typeof item === "string") return redactStringValue(item, valuePatterns, maskers, customPatterns, defaultReplacement); return item; }); return value; } return redactStringValue(value, valuePatterns, maskers, customPatterns, defaultReplacement); }; } /** * Apply three-tier redaction strategy to a string * 1. Masker-based: built-in patterns with smart partial masking * 2. Pattern-based: custom RegExp patterns replaced with replacement */ function redactStringValue(value, patterns, maskers, customPatterns, defaultReplacement) { let result = value; for (const [pattern, mask] of maskers) { pattern.lastIndex = 0; result = result.replace(pattern, mask); } for (const { pattern, replacement, mask } of patterns) { if (mask) continue; pattern.lastIndex = 0; result = result.replaceAll(pattern, replacement ?? defaultReplacement); } for (const pattern of customPatterns) { pattern.lastIndex = 0; result = result.replaceAll(pattern, defaultReplacement); } return result; } /** * Create a proxy wrapper around ReadableSpan with redacted attributes * * Since ReadableSpan.attributes is readonly, we use a Proxy to intercept * attribute access and return the redacted version. */ function createRedactedSpan(span, redactor) { const redactedAttributes = {}; for (const [key, value] of Object.entries(span.attributes)) if (value !== void 0) redactedAttributes[key] = redactor(key, value); return new Proxy(span, { get(target, prop) { if (prop === "attributes") return redactedAttributes; const value = Reflect.get(target, prop); if (typeof value === "function") return value.bind(target); return value; } }); } /** * Create an attribute redactor function from a config or preset. * * This is useful when you need to apply the same redaction logic * outside of the span processor pipeline (e.g., for canonical log lines). * * @example * ```typescript * const redactor = createAttributeRedactor('default'); * const redactedValue = redactor('user.password', 'secret123'); * // redactedValue === '[REDACTED]' * ``` */ function createAttributeRedactor(config) { return createRedactorFromConfig(resolveConfig(config)); } /** * Span processor that redacts sensitive data from span attributes. * * Redaction happens in onEnd() when all attributes are finalized. * Uses a Proxy wrapper to intercept attribute access since ReadableSpan * attributes are readonly. * * Common use cases: * - PII compliance (GDPR, CCPA) * - PCI-DSS compliance for payment data * - Preventing secrets from leaking to observability backends */ var AttributeRedactingProcessor = class { wrappedProcessor; redactor; constructor(wrappedProcessor, options) { this.wrappedProcessor = wrappedProcessor; const config = resolveConfig(options.redactor); this.redactor = createRedactorFromConfig(config); } /** * Pass through onStart unchanged - attributes aren't finalized yet */ onStart(span, parentContext) { this.wrappedProcessor.onStart(span, parentContext); } /** * Redact attributes and forward to wrapped processor */ onEnd(span) { try { const redactedSpan = createRedactedSpan(span, this.redactor); this.wrappedProcessor.onEnd(redactedSpan); } catch { this.wrappedProcessor.onEnd(span); } } forceFlush() { return this.wrappedProcessor.forceFlush(); } shutdown() { return this.wrappedProcessor.shutdown(); } }; //#endregion exports.AttributeRedactingProcessor = AttributeRedactingProcessor; exports.REDACTOR_PATTERNS = REDACTOR_PATTERNS; exports.REDACTOR_PRESETS = REDACTOR_PRESETS; exports.builtinPatterns = builtinPatterns; exports.createAttributeRedactor = createAttributeRedactor; exports.createRedactedSpan = createRedactedSpan; exports.normalizeAttributeRedactorConfig = normalizeAttributeRedactorConfig; //# sourceMappingURL=attribute-redacting-processor.cjs.map