mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-07-28 04:49:49 -07:00
138 lines
5.6 KiB
TypeScript
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"/);
|
|
});
|