Files
ForgeFlow/src/main/log-redaction.cjs
T
NuklearRabbit 8cca1bfc01
Managed validation / full (pull_request) Successful in 44s
ChatGPT validation / quality (push) Failing after 2m28s
Prepare ForgeFlow for public release
2026-08-31 20:10:07 +02:00

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 };