UNPKG

purgo

Version:

Zero-config PHI-scrubber for browser and Node.js

229 lines (225 loc) 6.39 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/node.ts var node_exports = {}; __export(node_exports, { initNodePatches: () => initNodePatches, pinoRedactor: () => pinoRedactor, redact: () => redact }); module.exports = __toCommonJS(node_exports); // src/redact.ts var BUILT_IN_PATTERNS = { // Email addresses email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Phone numbers (various formats) phone: /(\+\d{1,2}\s?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}/g, // Social Security Numbers ssn: /\b\d{3}[-]?\d{2}[-]?\d{4}\b/g, // Medical Record Numbers (generic pattern, can be customized) mrn: /\b\d{6,10}\b/g, // ICD-10 codes icd10: /\b[A-Z]\d{2}(\.[A-Z0-9]{1,4})?\b/g }; var DEFAULT_CENSOR = () => "***"; var globalConfig = { patterns: [], censor: DEFAULT_CENSOR, hashMode: false }; function initRedactionEngine(options = {}) { const patterns = options.patterns || ["email", "phone", "ssn", "mrn", "icd10"]; const regexPatterns = patterns.map((pattern) => { if (typeof pattern === "string") { return BUILT_IN_PATTERNS[pattern] || new RegExp(pattern, "g"); } return pattern; }); globalConfig = { patterns: regexPatterns, censor: options.censor || DEFAULT_CENSOR, hashMode: options.hashMode || false }; } function redactString(value) { if (typeof value !== "string") return value; let result = value; try { for (const pattern of globalConfig.patterns) { const patternCopy = new RegExp(pattern.source, pattern.flags); result = result.replace(patternCopy, (match) => { if (globalConfig.hashMode) { const hash = match.split("").reduce((a, b) => { const hashCode = a.charCodeAt(0) ^ b.charCodeAt(0); return String.fromCharCode(hashCode); }, "\0"); return `[HASH:${hash}]`; } return globalConfig.censor(match); }); } } catch (e) { console.error("Purgo: Error during string redaction, returning original value", e); return value; } return result; } function isDOMNode(obj) { return typeof Node === "object" ? obj instanceof Node : obj && typeof obj === "object" && typeof obj.nodeType === "number"; } function redact(value, seen = /* @__PURE__ */ new WeakSet()) { if (value === null || value === void 0) return value; if (typeof value !== "object") { return typeof value === "string" ? redactString(value) : value; } if (isDOMNode(value)) return value; if (seen.has(value)) return value; seen.add(value); if (Array.isArray(value)) { let hasCircularRefs2 = false; const length = value.length; const result2 = new Array(length); for (let i = 0; i < length; i++) { const item = value[i]; if (item === value) { result2[i] = null; } else { result2[i] = redact(item, seen); } } if (hasCircularRefs2) { for (let i = 0; i < length; i++) { if (value[i] === value) { result2[i] = result2; } } } return result2; } let hasPrimitiveOnly = true; let hasCircularRefs = false; let hasPHI = false; for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { const propValue = value[key]; if (propValue === value) { hasCircularRefs = true; hasPrimitiveOnly = false; break; } if (propValue !== null && typeof propValue === "object") { hasPrimitiveOnly = false; break; } if (typeof propValue === "string" && containsPHI(propValue)) { hasPHI = true; break; } } } if (hasPrimitiveOnly && !hasPHI) { return value; } const result = {}; const circularKeys = []; for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { const propValue = value[key]; if (propValue === value) { hasCircularRefs = true; circularKeys.push(key); result[key] = null; } else { result[key] = redact(propValue, seen); } } } if (hasCircularRefs) { for (const key of circularKeys) { result[key] = result; } } return result; } function containsPHI(value) { try { for (const pattern of globalConfig.patterns) { const patternCopy = new RegExp(pattern.source, pattern.flags); if (patternCopy.test(value)) { return true; } } return false; } catch (e) { console.error("Purgo: Error checking for PHI, assuming it might contain PHI", e); return true; } } initRedactionEngine(); // src/node.ts var originalStdoutWrite = null; function patchStdout() { if (typeof process === "undefined" || !process.stdout) return; originalStdoutWrite = process.stdout.write; process.stdout.write = function(buffer, encoding, callback) { if (typeof encoding === "function") { callback = encoding; encoding = void 0; } let redactedBuffer = buffer; if (typeof buffer === "string") { redactedBuffer = redact(buffer); } return originalStdoutWrite.call( process.stdout, redactedBuffer, encoding, callback ); }; } function pinoRedactor(options) { const { paths, additionalPatterns = [], censor } = options; const allPatterns = [ "email", "phone", "ssn", "mrn", "icd10", ...additionalPatterns ]; initRedactionEngine({ patterns: allPatterns, censor }); return { paths, censor: (value) => redact(value) }; } function initNodePatches(options = {}) { initRedactionEngine(options); patchStdout(); } initNodePatches();