Files
SubMiner/src/main/runtime/log-redaction.test.ts
T

138 lines
5.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { LOG_EXPORT_REDACTION_RULES, redactLogExportText } from './log-redaction';
test('log export redaction rules expose auditable fixtures', () => {
assert.deepEqual(
LOG_EXPORT_REDACTION_RULES.map((rule) => rule.name),
[
'signed-youtube-media-url-query',
'url-sensitive-components',
'sensitive-headers',
'yt-dlp-cookie-args',
'json-secret-values',
'generic-sensitive-key-values',
'home-path-usernames',
'email-addresses',
'ip-addresses',
],
);
const names = new Set<string>();
for (const rule of LOG_EXPORT_REDACTION_RULES) {
assert.equal(names.has(rule.name), false, `duplicate rule name: ${rule.name}`);
names.add(rule.name);
assert.ok(rule.pattern.length > 0, `${rule.name} pattern`);
assert.ok(rule.replacement.length > 0, `${rule.name} replacement`);
assert.ok(rule.risk.length > 0, `${rule.name} risk`);
assert.ok(rule.examples.length > 0, `${rule.name} examples`);
for (const example of rule.examples) {
assert.equal(rule.redact(example.input), example.expected, `${rule.name} direct`);
assert.equal(redactLogExportText(example.input), example.expected, rule.name);
}
}
});
test('redactLogExportText redacts parsed URL query values without swallowing punctuation', () => {
const masked = redactLogExportText(
[
'failed URL (https://example.test/watch?access_token=tok123).',
'callback subminer://anilist-setup?access_token=ani-token&state=ok',
'encoded https://example.test/watch?client%5Fsecret=secret-value&plain=ok!',
].join('\n'),
);
assert.match(masked, /\(https:\/\/example\.test\/watch\?access_token=<redacted>\)\./);
assert.match(masked, /subminer:\/\/anilist-setup\?access_token=<redacted>&state=ok/);
assert.match(masked, /client%5Fsecret=<redacted>&plain=ok!/);
assert.doesNotMatch(masked, /tok123/);
assert.doesNotMatch(masked, /ani-token/);
assert.doesNotMatch(masked, /secret-value/);
});
test('redactLogExportText redacts URL credentials containing at signs', () => {
const masked = redactLogExportText('GET https://alice:p@ss@example.test/watch?state=ok');
assert.equal(masked, 'GET https://<credentials>@example.test/watch?state=ok');
assert.doesNotMatch(masked, /alice/);
assert.doesNotMatch(masked, /p@ss/);
});
test('redactLogExportText redacts signed YouTube media URL query strings', () => {
const masked = redactLogExportText(
[
'ffmpeg failed for (https://rr1---sn-a5mekn6r.googlevideo.com/videoplayback?expire=1777777777&ei=EI_VALUE&id=o-SECRETID&itag=251&ip=203.0.113.42&source=youtube&requiressl=yes&n=NSECRET&sparams=expire,ei,ip,id,itag,source,requiressl&sig=SIGSECRET&lsig=LSIGSECRET).',
'manifest https://manifest.googlevideo.com/videoplayback?expire=1777777777&signature=SIG2#frag',
].join('\n'),
);
assert.match(
masked,
/\(https:\/\/rr1---sn-a5mekn6r\.googlevideo\.com\/videoplayback\?<redacted>\)\./,
);
assert.match(masked, /https:\/\/manifest\.googlevideo\.com\/videoplayback\?<redacted>#frag/);
assert.doesNotMatch(masked, /1777777777/);
assert.doesNotMatch(masked, /EI_VALUE/);
assert.doesNotMatch(masked, /o-SECRETID/);
assert.doesNotMatch(masked, /203\.0\.113\.42/);
assert.doesNotMatch(masked, /NSECRET/);
assert.doesNotMatch(masked, /SIGSECRET/);
assert.doesNotMatch(masked, /LSIGSECRET/);
});
test('redactLogExportText redacts nested JSON secret values', () => {
const masked = redactLogExportText(
'json {"nested":{"token":123,"safe":"ok"},"items":[{"password":true},{"client_secret":null}]}',
);
assert.match(masked, /"token":"<redacted>"/);
assert.match(masked, /"safe":"ok"/);
assert.match(masked, /"password":"<redacted>"/);
assert.match(masked, /"client_secret":"<redacted>"/);
assert.doesNotMatch(masked, /123/);
assert.doesNotMatch(masked, /true/);
assert.doesNotMatch(masked, /null/);
});
test('redactLogExportText redacts compound secret keys', () => {
const masked = redactLogExportText(
[
'json {"openaiApiKey":"sk-secret","google_api_key":"AIza-secret","userPassword":"hunter2","safe":"ok"}',
'openaiApiKey=sk-inline google_api_key=AIza-inline userPassword=hunter-inline',
].join('\n'),
);
assert.match(masked, /"openaiApiKey":"<redacted>"/);
assert.match(masked, /"google_api_key":"<redacted>"/);
assert.match(masked, /"userPassword":"<redacted>"/);
assert.match(masked, /"safe":"ok"/);
assert.match(masked, /openaiApiKey=<redacted>/);
assert.match(masked, /google_api_key=<redacted>/);
assert.match(masked, /userPassword=<redacted>/);
assert.doesNotMatch(masked, /sk-secret|AIza-secret|hunter2/);
assert.doesNotMatch(masked, /sk-inline|AIza-inline|hunter-inline/);
});
test('redactLogExportText redacts quoted secret values containing the opposite quote', () => {
const masked = redactLogExportText(`"password":"pa'ss" 'token':'to"ken' "safe":"pa'ss"`);
assert.equal(masked, `"password":"<redacted>" 'token':'<redacted>' "safe":"pa'ss"`);
});
test('redactLogExportText redacts IPv6 addresses with zone identifiers', () => {
const masked = redactLogExportText('connected [fe80::1%en0]:443 and fe80::2%eth0');
assert.equal(masked, 'connected [<ip>]:443 and <ip>');
});
test('redactLogExportText keeps malformed JSON scans bounded', () => {
const malformedLine = `${'{'.repeat(70 * 1024)} token=secret`;
const masked = redactLogExportText(`${malformedLine}\njson {"token":"secret","safe":"ok"}`);
assert.match(masked, /token=<redacted>/);
assert.match(masked, /json {"token":"<redacted>","safe":"ok"}/);
assert.doesNotMatch(masked, /token=secret/);
assert.doesNotMatch(masked, /"token":"secret"/);
});