'use strict'; const path = require('node:path'); const os = require('node:os'); const crypto = require('node:crypto'); const SENSITIVE_KEY = /(^|_)(token|password|passwd|authorization|secret|credential|clientsecret|client_secret|apikey|api_key|privatekey|private_key|encryptedtoken|encrypted_token)($|_)/i; const MAX_DIAGNOSTIC_STRING = 200_000; function redactSecrets(value, secrets = []) { let text = String(value ?? ''); const candidates = [...new Set((secrets || []).map((item) => String(item || '').trim()).filter((item) => item.length >= 4))] .sort((a, b) => b.length - a.length); for (const secret of candidates) text = text.split(secret).join('[REDACTED]'); text = text .replace(/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/gi, '[REDACTED PRIVATE KEY]') .replace(/(authorization\s*[:=]\s*(?:token|bearer|basic)\s+)[^\s,;]+/gi, '$1[REDACTED]') .replace(/([?&](?:access_token|token|api_key|apikey|key|secret|password)=)[^&#\s]+/gi, '$1[REDACTED]') .replace(/((?:access_token|token|api_key|apikey|client_secret|password|passwd|secret)\s*[=:]\s*)[^\s,;]+/gi, '$1[REDACTED]') .replace(/("(?:access_token|token|api_key|apikey|client_secret|password|passwd|secret)"\s*:\s*")[^"]+("?)/gi, '$1[REDACTED]$2') .replace(/(https?:\/\/[^\s:@/]+:)[^@\s/]+@/gi, '$1[REDACTED]@') .replace(/\b(?:ghp|github_pat|glpat|gitea)_[A-Za-z0-9_-]{16,}\b/g, '[REDACTED TOKEN]'); return text.length > MAX_DIAGNOSTIC_STRING ? `${text.slice(0, MAX_DIAGNOSTIC_STRING)}\n[TRUNCATED]` : text; } function pathAlias(value, { homeDir = os.homedir(), cwd = process.cwd() } = {}) { let text = String(value ?? ''); const replacements = [ [homeDir, ''], [cwd, ''] ].filter(([candidate]) => candidate && candidate.length > 3) .sort((a, b) => b[0].length - a[0].length); for (const [candidate, replacement] of replacements) { const normalized = path.resolve(candidate); text = text.split(normalized).join(replacement); text = text.split(normalized.replace(/\\/g, '/')).join(replacement); text = text.split(normalized.replace(/\//g, '\\')).join(replacement); } text = text .replace(/[A-Za-z]:\\Users\\[^\\\s]+/g, '') .replace(/\/(?:home|Users)\/[^/\s]+/g, ''); return text; } function stableAlias(value, prefix = 'item') { const hash = crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 12); return `${prefix}-${hash}`; } function redactPrivateInfrastructure(value) { return String(value ?? '') .replace(/\b(?:10(?:\.\d{1,3}){3}|127(?:\.\d{1,3}){3}|169\.254(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}|192\.168(?:\.\d{1,3}){2})\b/g, '') .replace(/\b(?:https?|ssh):\/\/[^\s"'<>]+/gi, '') .replace(/\/(?:mnt|srv|opt|var\/lib)\/[^\s"'<>]*/g, ''); } function sanitizeForDiagnostics(value, options = {}, seen = new WeakSet()) { const { secrets = [], pathMode = 'alias', homeDir = os.homedir(), cwd = process.cwd(), strictIdentifiers = false } = options; if (value === null || value === undefined || typeof value === 'boolean' || typeof value === 'number') return value; if (typeof value === 'bigint') return value.toString(); if (typeof value === 'string') { let output = redactSecrets(value, secrets); if (pathMode === 'alias') output = pathAlias(output, { homeDir, cwd }); if (strictIdentifiers) output = redactPrivateInfrastructure(output); return output; } if (value instanceof Error) { return sanitizeForDiagnostics({ name: value.name, message: value.message, code: value.code, stack: value.stack }, options, seen); } if (Array.isArray(value)) return value.slice(0, 1000).map((item) => sanitizeForDiagnostics(item, options, seen)); if (typeof value !== 'object') return redactSecrets(String(value), secrets); if (seen.has(value)) return '[CIRCULAR]'; seen.add(value); const output = {}; for (const [key, item] of Object.entries(value)) { const normalizedKey = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/[-.]/g, '_'); if (SENSITIVE_KEY.test(normalizedKey)) { output[key] = '[REDACTED]'; continue; } if (strictIdentifiers && ['full_name', 'repository', 'owner', 'user', 'login', 'email', 'host', 'hostname', 'username', 'base_path', 'private_key_path', 'local_path', 'remote_folder', 'remote_url', 'clone_url', 'status_url', 'healthcheck_url', 'web_ui_url', 'workspace_roots', 'scan_roots'].includes(normalizedKey.toLowerCase())) { output[key] = stableAlias(typeof item === 'object' ? JSON.stringify(item) : item, key.toLowerCase()); continue; } output[key] = sanitizeForDiagnostics(item, options, seen); } seen.delete(value); return output; } module.exports = { redactSecrets, sanitizeForDiagnostics, pathAlias, stableAlias, redactPrivateInfrastructure, SENSITIVE_KEY };