UNPKG

@ogcio/o11y-sdk-node

Version:

Opentelemetry standard instrumentation SDK for NodeJS based project

117 lines (116 loc) 3.75 kB
import { _getPIICounterRedactionMetric } from "./shared-metrics.js"; const EMAIL_REGEX = /[a-zA-Z0-9._%+-]+@([a-zA-Z0-9.-]+\.[a-z]{2,})/gi; const decoder = new TextDecoder(); const encoder = new TextEncoder(); /** * Redacts all email addresses in the input string and collects metadata. * * @param {string} value The input string potentially containing email addresses. * @returns {{ * redacted: string, * count: number, * domains: Record<string, number> * }} * * An object containing: * - `redacted`: the string with email addresses replaced by `[REDACTED EMAIL]` * - `count`: total number of email addresses redacted * - `domains`: a map of domain names to the number of times they were redacted */ function _redactEmails(value) { let count = 0; const domains = {}; const redacted = value.replace(EMAIL_REGEX, (_, domain) => { count++; domains[domain] = (domains[domain] || 0) + 1; return "[REDACTED EMAIL]"; }); return { redacted, count, domains }; } /** * Checks whether a string contains URI-encoded components. * * @param {string} value - The string to inspect. * @returns {boolean} `true` if the string is encoded, `false` otherwise. */ function _containsEncodedComponents(value) { try { return decodeURI(value) !== decodeURIComponent(value); } catch { return false; } } /** * Cleans a string by redacting email addresses and emitting metrics for PII. * * If the string is URL-encoded, it will be decoded before redaction. * Metrics are emitted for each domain found in redacted email addresses. * * @param {string} value - The input string to sanitize. * @param {"trace" | "log"} source - The source context of the input, used in metrics. * @returns {string} The cleaned string with any email addresses replaced by `[REDACTED EMAIL]`. */ export function _cleanStringPII(value, source) { if (Array.isArray(value)) { return value.map((v) => _cleanStringPII(v, source)); } if (typeof value !== "string") { return value; } let kind = "string"; let decodedValue = value; if (_containsEncodedComponents(value)) { decodedValue = decodeURIComponent(value); kind = "url"; } const { redacted, count, domains } = _redactEmails(decodedValue); if (count > 0) { for (const [domain, domainCount] of Object.entries(domains)) { _getPIICounterRedactionMetric().add(domainCount, { pii_type: "email", redaction_source: source, pii_email_domain: domain, pii_format: kind, }); } } return redacted; } export function _cleanObjectPII(entry, source) { if (!entry) { return entry; } return Object.fromEntries(Object.entries(entry).map(([k, v]) => [k, _cleanStringPII(v, source)])); } export function _cleanLogBodyPII(value) { if (typeof value === "string") { return _cleanStringPII(value, "log"); } if (typeof value === "number" || typeof value === "boolean" || value == null) { return value; } if (value instanceof Uint8Array) { try { const decoded = decoder.decode(value); const sanitized = _cleanStringPII(decoded, "log"); return encoder.encode(sanitized); } catch { return value; } } if (Array.isArray(value)) { return value.map(_cleanLogBodyPII); } if (typeof value === "object") { const sanitized = {}; for (const [key, val] of Object.entries(value)) { sanitized[key] = _cleanLogBodyPII(val); } return sanitized; } return value; }