@ogcio/o11y-sdk-node
Version:
Opentelemetry standard instrumentation SDK for NodeJS based project
117 lines (116 loc) • 3.75 kB
JavaScript
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;
}