102 lines
4.8 KiB
JavaScript
102 lines
4.8 KiB
JavaScript
'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, '<HOME>'],
|
|
[cwd, '<APP_ROOT>']
|
|
].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, '<HOME>')
|
|
.replace(/\/(?:home|Users)\/[^/\s]+/g, '<HOME>');
|
|
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, '<PRIVATE_ADDRESS>')
|
|
.replace(/\b(?:https?|ssh):\/\/[^\s"'<>]+/gi, '<PRIVATE_URL>')
|
|
.replace(/\/(?:mnt|srv|opt|var\/lib)\/[^\s"'<>]*/g, '<SERVER_PATH>');
|
|
}
|
|
|
|
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 };
|