autotel
Version:
Write Once, Observe Anywhere
349 lines (347 loc) • 11.7 kB
JavaScript
;
// src/attribute-redacting-processor.ts
var REDACTOR_PATTERNS = {
// Value patterns (match content in attribute values)
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,
// Key patterns (match attribute names - redacts entire value)
sensitiveKey: /^(password|passwd|pwd|secret|token|api[_-]?key|auth|credential|private[_-]?key|authorization)$/i
};
var 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) => {
const at = m.indexOf("@");
if (at < 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 phone numbers → +33******78 (country code + last 2 digits) */
phone: {
pattern: /(?:\+\d{1,3}[\s.-]?)?\(?\d{1,4}\)?[\s.-]?\d{2,4}[\s.-]?\d{2,4}[\s.-]?\d{2,4}\b/g,
mask: (m) => {
const digits = m.replace(/[^\d]/g, "");
const hasPlus = m.startsWith("+");
if (hasPlus && digits.length > 4) {
const ccMatch = m.match(/^\+\d{1,3}/);
const cc = ccMatch ? ccMatch[0] : "+";
return `${cc}******${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);
}
return void 0;
}
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 };
}
var DEFAULT_VALUE_PATTERNS = [
builtinToValuePattern("email"),
builtinToValuePattern("phone"),
{ name: "ssn", pattern: REDACTOR_PATTERNS.ssn },
builtinToValuePattern("creditCard")
];
var 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]"
}
};
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;
}
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 builtinNames = Array.isArray(resolvedConfig.builtins) ? resolvedConfig.builtins : Object.keys(builtinPatterns);
const builtinValuePatterns = builtinNames.filter((name) => name in builtinPatterns).map(builtinToValuePattern);
resolvedConfig.valuePatterns = [
...resolvedConfig.valuePatterns ?? [],
...builtinValuePatterns
];
}
return resolvedConfig;
}
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) => {
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
);
};
}
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;
}
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;
}
});
}
function createAttributeRedactor(config) {
return createRedactorFromConfig(resolveConfig(config));
}
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();
}
};
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=chunk-CMNGGTQL.cjs.map
//# sourceMappingURL=chunk-CMNGGTQL.cjs.map