@ogcio/o11y-sdk-node
Version:
Opentelemetry standard instrumentation SDK for NodeJS based project
146 lines (124 loc) • 3.88 kB
text/typescript
import type { AnyValue, AnyValueMap } from "@opentelemetry/api-logs";
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();
export type PIISource = "trace" | "log" | "metric";
/**
* 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: string): {
redacted: string;
count: number;
domains: Record<string, number>;
} {
let count = 0;
const domains: Record<string, number> = {};
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: string) {
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: AnyValue, source: PIISource): AnyValue {
if (Array.isArray(value)) {
return value.map((v) => _cleanStringPII(v, source));
}
if (typeof value !== "string") {
return value;
}
let kind: "string" | "url" = "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: object, source: PIISource) {
if (!entry) {
return entry;
}
return Object.fromEntries(
Object.entries(entry).map(([k, v]) => [k, _cleanStringPII(v, source)]),
);
}
export function _cleanLogBodyPII(value: AnyValue): AnyValue {
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") as string;
return encoder.encode(sanitized);
} catch {
return value;
}
}
if (Array.isArray(value)) {
return value.map(_cleanLogBodyPII);
}
if (typeof value === "object") {
const sanitized: AnyValueMap = {};
for (const [key, val] of Object.entries(value)) {
sanitized[key] = _cleanLogBodyPII(val);
}
return sanitized;
}
return value;
}