Harden audit durability and diagnostic redaction

Persist fallback audit targets before use and fail upload starts or active-batch additions closed when the plan cannot be recorded. Keep lifecycle audits out of session and debug logs, expose diagnostics through opaque metadata, localize audit failures, and redact paths plus complete credential values without corrupting benign text.
This commit is contained in:
Sucukdeluxe
2026-08-13 20:34:26 +02:00
parent a98b63618d
commit e63214cae8
7 changed files with 372 additions and 77 deletions
+59 -23
View File
@@ -26,47 +26,83 @@ function collectSecretValues(config) {
if (typeof o !== 'object') return;
for (const k of Object.keys(o)) {
const v = o[k];
if (CRED_KEYS.has(k) && typeof v === 'string' && v.length >= 6) out.add(v);
if (CRED_KEYS.has(k) && typeof v === 'string' && v.length > 0) out.add(v);
else walk(v);
}
})(config);
return Array.from(out);
}
function redactLogText(text, secrets) {
if (typeof text !== 'string' || !text) return text;
function redactConfiguredSecrets(text, secrets) {
if (!Array.isArray(secrets)) return text;
const values = Array.from(new Set(secrets.filter(value => typeof value === 'string' && value.length > 0)))
.sort((a, b) => b.length - a.length);
let out = text;
if (Array.isArray(secrets)) {
for (const s of secrets) {
if (typeof s === 'string' && s.length >= 6) out = out.split(s).join(REDACTED);
for (const value of values) {
if (value.length >= 6) {
out = out.split(value).join(REDACTED);
continue;
}
let offset = 0;
while (offset < out.length) {
const index = out.indexOf(value, offset);
if (index < 0) break;
const first = value[0];
const last = value[value.length - 1];
const before = index > 0 ? out[index - 1] : '';
const after = index + value.length < out.length ? out[index + value.length] : '';
const identifier = character => /[A-Za-z0-9_]/.test(character);
if ((!identifier(first) || !identifier(before)) && (!identifier(last) || !identifier(after))) {
out = `${out.slice(0, index)}${REDACTED}${out.slice(index + value.length)}`;
offset = index + REDACTED.length;
} else {
offset = index + value.length;
}
}
}
out = out
return out;
}
function redactHtmlCredentialFields(text) {
return text.replace(/<input\b[^>]*>/gi, input => {
const sensitive = /\btype\s*=\s*["']?password\b/i.test(input)
|| /\b(?:name|id)\s*=\s*["']?(?:password|passwd|api[_-]?(?:key|token)|token|secret|authorization|cookie|session(?:[_-]?id)?)\b/i.test(input);
if (!sensitive) return input;
return input
.replace(/(\bvalue\s*=\s*)(["'])(.*?)\2/gi, `$1$2${REDACTED}$2`)
.replace(/(\bvalue\s*=\s*)(?!["'])([^\s>]+)/gi, `$1${REDACTED}`);
});
}
function redactLogText(text, secrets) {
if (typeof text !== 'string' || !text) return text;
let out = redactConfiguredSecrets(text, secrets);
out = redactHtmlCredentialFields(out)
.replace(/("(?:file|fileName|stagedFile|sourceFile|targetFile|path|[A-Za-z0-9_]*Path)"\s*:\s*")[^"]*(")/gi, '$1<redacted-path>$2')
.replace(/\b[A-Za-z]:(?:\\+|\/+)[^\r\n"'<>|]*?(?=\s+(?:trigger|error|outcome|hoster|attempt|status|code)=|\r?\n|$|["'])/gi, '<redacted-path>')
.replace(/\\{2,}[A-Za-z0-9._$-]+\\+[^\r\n"'<>|]*?(?=\s+(?:trigger|error|outcome|hoster|attempt|status|code)=|\r?\n|$|["'])/g, '<redacted-path>')
.replace(/https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w-]+/gi, 'https://discord.com/api/webhooks/' + REDACTED)
.replace(/(\/\/[^\s/:@]+:)[^\s/@]+(@)/g, '$1' + REDACTED + '$2')
.replace(/(authorization:\s*(?:bearer|basic)\s+)\S+/gi, '$1' + REDACTED)
.replace(/\bbearer\s+[A-Za-z0-9._\-/+]{16,}/gi, 'bearer ' + REDACTED)
.replace(/(\b(?:proxy-)?authorization\s*:\s*)[^\r\n]*/gi, '$1' + REDACTED)
.replace(/(\b(?:set-cookie|cookie)\s*:\s*)[^\r\n]*/gi, '$1' + REDACTED)
.replace(/(\b(?:bearer|basic)\s+)[A-Za-z0-9._~+\-/=]+/gi, '$1' + REDACTED)
.replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}/g, REDACTED)
.replace(/([?&](?:api_?key|key|token|access_token|password|pass)=)[^\s&"'`]+/gi, '$1' + REDACTED)
.replace(/("?\b(?:api[_-]?key|apikey|password|passwd|secret|(?:access|refresh|auth|session)[_-]?token|token|sessionid|session)"?\s*[:=]\s*"?)[A-Za-z0-9._\-/+]{8,}/gi, '$1' + REDACTED)
.replace(/(\bset-cookie:|\bcookie:)\s*\S[^\n]*/gi, '$1 ' + REDACTED)
.replace(/(\bsess(?:_?id)?\b["'=:\s]+)[A-Za-z0-9._\-]{8,}/gi, '$1' + REDACTED);
.replace(/([?&](?:api[_-]?key|key|token|access[_-]?token|refresh[_-]?token|auth|authorization|password|pass|cookie|session(?:[_-]?id)?)=)[^\s&#"'`]+/gi, '$1' + REDACTED)
.replace(/("?\b(?:api[_-]?key|apikey|password|passwd|secret|authorization|cookie|(?:access|refresh|auth|session)[_-]?token|token|session[_-]?id|sessionid|session|sess[_-]?id|sessid|sess)"?\s*[:=]\s*)(["'])(.*?)\2/gi, `$1$2${REDACTED}$2`)
.replace(/("?\b(?:api[_-]?key|apikey|password|passwd|secret|authorization|cookie|(?:access|refresh|auth|session)[_-]?token|token|session[_-]?id|sessionid|session|sess[_-]?id|sessid|sess)"?\s*[:=]\s*)(?!["'])([^\s,;}\]\r\n]+)/gi, '$1' + REDACTED);
return out;
}
function valueScrub(value, secrets) {
if (value === null || value === undefined) return value;
const json = JSON.stringify(value);
let scrubbed = json;
if (Array.isArray(secrets)) {
for (const s of secrets) {
if (typeof s === 'string' && s.length >= 6) scrubbed = scrubbed.split(s).join(REDACTED);
}
if (typeof value === 'string') return redactLogText(value, secrets);
if (Array.isArray(value)) return value.map(entry => valueScrub(entry, secrets));
if (typeof value === 'object') {
const out = {};
for (const [key, entry] of Object.entries(value)) out[key] = valueScrub(entry, secrets);
return out;
}
return JSON.parse(scrubbed);
return value;
}
function collectFile(filePath, label, maxBytes, options) {
@@ -107,12 +143,12 @@ function buildSupportBundleText({ header, sanitizedConfig, files, secrets }) {
}
parts.push('\n');
parts.push('=== Config (sanitized — password/apiKey/token/cookie/sessionId redacted) ===\n');
parts.push(redactLogText(JSON.stringify(sanitizedConfig, null, 2), secrets));
parts.push(JSON.stringify(sanitizedConfig, null, 2));
parts.push('\n\n');
for (const f of (files || [])) {
parts.push(redactLogText(collectFile(f.path, f.label || f.path, f.maxBytes, { includePath: false }), secrets));
parts.push(collectFile(f.path, f.label || 'log', f.maxBytes, { includePath: false }));
}
return parts.join('');
return redactLogText(parts.join(''), secrets);
}
module.exports = { sanitizeConfig, collectSecretValues, redactLogText, valueScrub, collectFile, buildSupportBundleText, CRED_KEYS, REDACTED };