Expose safe actionable upload diagnostics
This commit is contained in:
@@ -9,13 +9,28 @@ function cleanText(value, limit = 320) {
|
|||||||
return '[URL]';
|
return '[URL]';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
text = text.replace(/\b(authorization|proxy-authorization|cookie|set-cookie)\s*[:=]\s*[^,]+/gi, '$1=[redacted]');
|
||||||
text = text.replace(/(["']?(?:api[_-]?key|token|password|cookie|authorization|session)["']?\s*[:=]\s*)[^,;\s}"']+/gi, '$1[redacted]');
|
text = text.replace(/(["']?(?:api[_-]?key|token|password|cookie|authorization|session)["']?\s*[:=]\s*)[^,;\s}"']+/gi, '$1[redacted]');
|
||||||
if (/^(?:bearer|basic)\s+/i.test(text)) {
|
if (/^(?:bearer|basic)\s+/i.test(text)) {
|
||||||
text = '[redacted authorization]';
|
text = '[redacted authorization]';
|
||||||
}
|
}
|
||||||
|
text = text.replace(/\b[A-Za-z0-9_-]{20,}\b/g, '[redacted]');
|
||||||
return text.slice(0, limit);
|
return text.slice(0, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeEndpointHost(value) {
|
||||||
|
const host = String(value || '').trim().toLowerCase();
|
||||||
|
if (!host || host.length > 253 || !/^[a-z0-9.-]+$/.test(host)) return '';
|
||||||
|
if (host.startsWith('.') || host.endsWith('.') || host.includes('..')) return '';
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePhase(value) {
|
||||||
|
const phase = String(value || '').trim();
|
||||||
|
if (/^[a-z0-9._:-]{1,80}$/i.test(phase)) return phase;
|
||||||
|
return cleanText(phase, 80);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeFailureDetails(diagnostic) {
|
function normalizeFailureDetails(diagnostic) {
|
||||||
if (!diagnostic || typeof diagnostic !== 'object') return null;
|
if (!diagnostic || typeof diagnostic !== 'object') return null;
|
||||||
const httpStatus = Number.isInteger(Number(diagnostic.http)) && Number(diagnostic.http) >= 100 && Number(diagnostic.http) <= 599
|
const httpStatus = Number.isInteger(Number(diagnostic.http)) && Number(diagnostic.http) >= 100 && Number(diagnostic.http) <= 599
|
||||||
@@ -23,9 +38,18 @@ function normalizeFailureDetails(diagnostic) {
|
|||||||
: null;
|
: null;
|
||||||
const contentType = cleanText(diagnostic.contentType, 120);
|
const contentType = cleanText(diagnostic.contentType, 120);
|
||||||
const responseSnippet = cleanText(diagnostic.payloadSnippet, 320);
|
const responseSnippet = cleanText(diagnostic.payloadSnippet, 320);
|
||||||
|
const phase = normalizePhase(diagnostic.phase);
|
||||||
|
const endpointHost = normalizeEndpointHost(diagnostic.safeEndpointHost);
|
||||||
|
const responseKind = ['empty', 'json', 'html', 'text'].includes(diagnostic.responseKind)
|
||||||
|
? diagnostic.responseKind
|
||||||
|
: '';
|
||||||
const details = {};
|
const details = {};
|
||||||
|
if (phase) details.phase = phase;
|
||||||
if (httpStatus !== null) details.httpStatus = httpStatus;
|
if (httpStatus !== null) details.httpStatus = httpStatus;
|
||||||
if (contentType) details.contentType = contentType;
|
if (contentType) details.contentType = contentType;
|
||||||
|
if (endpointHost) details.endpointHost = endpointHost;
|
||||||
|
if (responseKind) details.responseKind = responseKind;
|
||||||
|
if (typeof diagnostic.retryable === 'boolean') details.retryable = diagnostic.retryable;
|
||||||
if (responseSnippet) details.responseSnippet = responseSnippet;
|
if (responseSnippet) details.responseSnippet = responseSnippet;
|
||||||
return Object.keys(details).length > 0 ? details : null;
|
return Object.keys(details).length > 0 ? details : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,3 +25,40 @@ test('bereinigt Zugangsdaten und mehrere URLs aus Hosterantworten', () => {
|
|||||||
|
|
||||||
assert.equal(result.responseSnippet, 'token=[redacted] one.example/a password: [redacted] two.example/b');
|
assert.equal(result.responseSnippet, 'token=[redacted] one.example/a password: [redacted] two.example/b');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('behält sichere Transportdetails für eine konkrete Fehleranalyse', () => {
|
||||||
|
const result = normalizeFailureDetails({
|
||||||
|
phase: 'web-upload-confirmation',
|
||||||
|
http: 502,
|
||||||
|
contentType: 'application/json',
|
||||||
|
safeEndpointHost: 'upload.doodstream.com',
|
||||||
|
responseKind: 'json',
|
||||||
|
retryable: true,
|
||||||
|
payloadSnippet: 'json response (148 bytes)'
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
phase: 'web-upload-confirmation',
|
||||||
|
httpStatus: 502,
|
||||||
|
contentType: 'application/json',
|
||||||
|
endpointHost: 'upload.doodstream.com',
|
||||||
|
responseKind: 'json',
|
||||||
|
retryable: true,
|
||||||
|
responseSnippet: 'json response (148 bytes)'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('verwirft unsichere Transportfelder und unbenannte lange Geheimnisse', () => {
|
||||||
|
const secret = 'SYNTHETIC_UNNAMED_SECRET_1234567890';
|
||||||
|
const result = normalizeFailureDetails({
|
||||||
|
phase: `upload ${secret}`,
|
||||||
|
safeEndpointHost: `evil.example/${secret}`,
|
||||||
|
responseKind: `json-${secret}`,
|
||||||
|
payloadSnippet: `Authorization: Bearer ${secret}`
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.phase, 'upload [redacted]');
|
||||||
|
assert.equal(result.endpointHost, undefined);
|
||||||
|
assert.equal(result.responseKind, undefined);
|
||||||
|
assert.doesNotMatch(JSON.stringify(result), /SYNTHETIC_UNNAMED_SECRET/);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user