cabloy 5.1.214 → 5.1.215

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,736 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import {
4
+ chmod,
5
+ mkdir,
6
+ mkdtemp,
7
+ readFile,
8
+ realpath,
9
+ rm,
10
+ stat,
11
+ symlink,
12
+ writeFile,
13
+ } from 'node:fs/promises';
14
+ import { homedir, tmpdir } from 'node:os';
15
+ import { dirname, join, resolve } from 'node:path';
16
+ import { Readable, Writable } from 'node:stream';
17
+ import { test } from 'node:test';
18
+ import { fileURLToPath, pathToFileURL } from 'node:url';
19
+
20
+ import {
21
+ checkPaths,
22
+ createMatcher,
23
+ DEFAULT_REPO_ROOT,
24
+ FILE_LIMIT,
25
+ LINE_LIMIT,
26
+ runCli,
27
+ sanitizeStream,
28
+ } from '../tools/evidence-paths/sanitize.mjs';
29
+
30
+ const CLI = fileURLToPath(new URL('../tools/evidence-paths/sanitize.mjs', import.meta.url));
31
+ const matcher = createMatcher({
32
+ repoRoot: '/synthetic/checkout',
33
+ maps: [{ root: '/synthetic/other', alias: 'OTHER' }],
34
+ });
35
+
36
+ async function fixture(t, files = {}) {
37
+ const root = await mkdtemp(join(await realpath(tmpdir()), 'evidence-paths-'));
38
+ t.after(() => rm(root, { recursive: true, force: true }));
39
+ for (const [name, content] of Object.entries(files)) {
40
+ await mkdir(dirname(join(root, name)), { recursive: true });
41
+ await writeFile(join(root, name), content);
42
+ }
43
+ return root;
44
+ }
45
+
46
+ function cli(args, input = '', options = {}) {
47
+ return spawnSync(process.execPath, [CLI, ...args], {
48
+ encoding: 'utf8',
49
+ input,
50
+ maxBuffer: 4 * LINE_LIMIT,
51
+ ...options,
52
+ });
53
+ }
54
+
55
+ async function symlinkOrSkip(t, target, path, type) {
56
+ try {
57
+ await symlink(target, path, type);
58
+ return true;
59
+ } catch (error) {
60
+ if (process.platform === 'win32' && ['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) {
61
+ t.skip(`Windows ${type} link creation unavailable (${error.code})`);
62
+ return false;
63
+ }
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ function sink() {
69
+ let text = '';
70
+ const stream = new Writable({
71
+ write(chunk, _encoding, done) {
72
+ text += chunk.toString('utf8');
73
+ done();
74
+ },
75
+ });
76
+ return { stream, text: () => text };
77
+ }
78
+
79
+ async function streamed(chunks, options = {}) {
80
+ const output = sink();
81
+ await sanitizeStream(Readable.from(chunks), output.stream, { matcher, ...options });
82
+ return output.text();
83
+ }
84
+
85
+ test('module import is inert and repo default is anchored to the tool rather than cwd', async t => {
86
+ assert.equal(DEFAULT_REPO_ROOT, resolve(CLI, '../../../..'));
87
+ const cwd = await fixture(t);
88
+ const imported = spawnSync(
89
+ process.execPath,
90
+ ['--input-type=module', '-e', `await import(${JSON.stringify(pathToFileURL(CLI).href)})`],
91
+ {
92
+ cwd,
93
+ encoding: 'utf8',
94
+ input: 'unconsumed input',
95
+ },
96
+ );
97
+ assert.equal(imported.status, 0);
98
+ assert.equal(imported.stdout, '');
99
+ assert.equal(imported.stderr, '');
100
+ const result = cli(['sanitize'], `${join(DEFAULT_REPO_ROOT, 'sample.mjs')}\n`, { cwd });
101
+ assert.equal(result.status, 0);
102
+ assert.equal(result.stdout, 'sample.mjs\n');
103
+ });
104
+
105
+ test('native paths with spaces and Unicode work in matchers, CLI arguments and checker scopes', async t => {
106
+ const root = await fixture(t, {
107
+ 'repo space-é/source 文件.txt': 'clean\n',
108
+ 'mapped space-🙂/evidence.txt': '/home/Secret/a\n',
109
+ });
110
+ const repoRoot = join(root, 'repo space-é');
111
+ const mappedRoot = join(root, 'mapped space-🙂');
112
+ const source = join(repoRoot, 'source 文件.txt');
113
+ const evidence = join(mappedRoot, 'evidence.txt');
114
+ const nativeMatcher = createMatcher({ repoRoot, maps: [{ root: mappedRoot, alias: 'OTHER' }] });
115
+ const input = `"${source}":3:4\n"${evidence}"\n${pathToFileURL(source).href}\n`;
116
+ const expected = '"source 文件.txt":3:4\n"<OTHER>/evidence.txt"\nsource 文件.txt\n';
117
+ assert.equal(nativeMatcher.find(input).length, 3);
118
+ assert.equal(nativeMatcher.sanitize(input), expected);
119
+ assert.equal(nativeMatcher.find(expected).length, 0);
120
+ const sanitized = cli(
121
+ ['sanitize', '--repo-root', repoRoot, '--map', `${mappedRoot}=OTHER`],
122
+ input,
123
+ );
124
+ assert.equal(sanitized.status, 0);
125
+ assert.equal(sanitized.stdout, expected);
126
+ assert.equal(sanitized.stderr, '');
127
+
128
+ const checked = await checkPaths([root], { matcher: nativeMatcher });
129
+ assert.deepEqual(checked, {
130
+ status: 1,
131
+ diagnostics: ['scope-1/mapped space-🙂/evidence.txt:1 PATH_FOUND'],
132
+ });
133
+ const command = cli(['check', '--repo-root', repoRoot, '--', root]);
134
+ assert.equal(command.status, checked.status);
135
+ assert.equal(command.stdout, `${checked.diagnostics.join('\n')}\n`);
136
+ assert.equal(command.stderr, '');
137
+ assert.deepEqual(await checkPaths([source, evidence], { matcher: nativeMatcher }), {
138
+ status: 1,
139
+ diagnostics: ['scope-2/evidence.txt:1 PATH_FOUND'],
140
+ });
141
+ const relative = cli(['check', '--', 'source 文件.txt', '../mapped space-🙂/evidence.txt'], '', {
142
+ cwd: repoRoot,
143
+ });
144
+ assert.equal(relative.status, 1);
145
+ assert.equal(relative.stdout, 'scope-2/evidence.txt:1 PATH_FOUND\n');
146
+ assert.equal(relative.stderr, '');
147
+ if (process.platform === 'win32') {
148
+ assert.deepEqual(
149
+ await checkPaths([source.replaceAll('\\', '/'), evidence.replaceAll('\\', '/')], {
150
+ matcher: nativeMatcher,
151
+ }),
152
+ { status: 1, diagnostics: ['scope-2/evidence.txt:1 PATH_FOUND'] },
153
+ );
154
+ }
155
+ });
156
+
157
+ test('checkout, explicit aliases, overlap priority and root boundary semantics', () => {
158
+ const overlap = createMatcher({
159
+ repoRoot: '/synthetic/repo',
160
+ maps: [
161
+ { root: '/synthetic', alias: 'WORK' },
162
+ { root: '/synthetic/repo/vendor', alias: '<VENDOR>' },
163
+ ],
164
+ });
165
+ const input =
166
+ '/synthetic/repo /synthetic/repo/a.ts:10:2 /synthetic/repo/vendor/b.js /synthetic/repository/c';
167
+ assert.equal(overlap.sanitize(input), '. a.ts:10:2 <VENDOR>/b.js <WORK>/repository/c');
168
+ assert.equal(
169
+ matcher.sanitize('/synthetic/checkout-other/a /synthetic/otherness/a x/synthetic/checkout/a'),
170
+ '/synthetic/checkout-other/a /synthetic/otherness/a x/synthetic/checkout/a',
171
+ );
172
+ assert.equal(matcher.sanitize('/synthetic/other /synthetic/other/a'), '<OTHER> <OTHER>/a');
173
+ assert.equal(
174
+ matcher.sanitize('/synthetic/other/../private /synthetic/checkout/../private'),
175
+ '<PRIVATE_PATH> <PRIVATE_PATH>',
176
+ );
177
+ assert.equal(
178
+ matcher.sanitize('"/synthetic/checkout/folder /home/Secret/a"'),
179
+ '"folder <PRIVATE_PATH>"',
180
+ );
181
+ const homeRepo = createMatcher({ repoRoot: '/Users/Example/project' });
182
+ assert.equal(
183
+ homeRepo.sanitize('/Users/Example/project/src/a /Users/Example/elsewhere/a'),
184
+ 'src/a <PRIVATE_PATH>',
185
+ );
186
+ });
187
+
188
+ test('invalid aliases, roots and conflicting replacements fail with safe constant errors', () => {
189
+ for (const alias of ['secret-name', 'Name', '<BAD', 'A/B', '', '<NAME>\n']) {
190
+ assert.throws(
191
+ () => createMatcher({ maps: [{ root: '/synthetic/map', alias }] }),
192
+ /^Error: INVALID_ALIAS$/u,
193
+ );
194
+ }
195
+ for (const root of ['relative/private', '/', 'C:\\', '/synthetic/../private', '/secret\npath']) {
196
+ assert.throws(() => createMatcher({ repoRoot: root }), /^Error: INVALID_ROOT$/u);
197
+ }
198
+ assert.throws(
199
+ () =>
200
+ createMatcher({
201
+ repoRoot: '/synthetic/root',
202
+ maps: [{ root: '/synthetic/root', alias: 'ROOT' }],
203
+ }),
204
+ /^Error: CONFLICTING_ROOT$/u,
205
+ );
206
+ assert.throws(
207
+ () => createMatcher({ repoRoot: 'C:\\Repo', maps: [{ root: 'c:/repo/', alias: 'ROOT' }] }),
208
+ /^Error: CONFLICTING_ROOT$/u,
209
+ );
210
+ const duplicate = createMatcher({
211
+ maps: [
212
+ { root: '/synthetic/root/', alias: 'ROOT' },
213
+ { root: '/synthetic/root', alias: '<ROOT>' },
214
+ ],
215
+ });
216
+ assert.equal(duplicate.sanitize('/synthetic/root/file'), '<ROOT>/file');
217
+ });
218
+
219
+ test('bounded POSIX private coverage removes identity and full tokens, preserving stack suffixes', () => {
220
+ const roots = [
221
+ '/Users/Identity',
222
+ '/home/Identity',
223
+ '/tmp',
224
+ '/private/tmp',
225
+ '/var/folders/ab/Identity',
226
+ '/private/var/folders/ab/Identity',
227
+ ];
228
+ for (const root of roots) {
229
+ assert.equal(
230
+ matcher.sanitize(`at (${root}/private/project/file.ts:123:45)`),
231
+ 'at (<PRIVATE_PATH>:123:45)',
232
+ );
233
+ assert.equal(matcher.sanitize(`"${root}/folder with spaces/file.txt"`), '"<PRIVATE_PATH>"');
234
+ }
235
+ for (const root of [homedir(), tmpdir()]) {
236
+ assert.equal(matcher.sanitize(`"${root}/identity-sensitive/file"`), '"<PRIVATE_PATH>"');
237
+ }
238
+ assert.equal(matcher.sanitize('"/Users/Full Name/private folder/file"'), '"<PRIVATE_PATH>"');
239
+ });
240
+
241
+ test('Windows native, forward, escaped and UNC conventions', () => {
242
+ const input = [
243
+ String.raw`C:\Users\Secret\project\a.ts:3:4`,
244
+ 'D:/Users/Secret/project/a.ts:3:4',
245
+ String.raw`C:\\Users\\Secret\\project\\a.ts:3:4`,
246
+ String.raw`C:\Temp\private\file`,
247
+ String.raw`C:\Windows\Temp\private\file`,
248
+ String.raw`\\private-server\private-share\account\file:2:8`,
249
+ String.raw`\\\\private-server\\private-share\\account\\file:2:8`,
250
+ String.raw`"C:\Users\Full Name\private folder\file"`,
251
+ ];
252
+ for (const value of input) {
253
+ const output = matcher.sanitize(value);
254
+ assert.ok(output.includes('<PRIVATE_PATH>'), JSON.stringify(value));
255
+ assert.doesNotMatch(
256
+ output,
257
+ /Secret|private-server|private-share|account|Full Name|private folder/u,
258
+ );
259
+ if (value.endsWith(':3:4')) assert.ok(output.endsWith(':3:4'));
260
+ }
261
+ const windows = createMatcher({
262
+ repoRoot: String.raw`C:\Workspace\Repo`,
263
+ maps: [
264
+ { root: String.raw`\\host\share\root`, alias: 'SHARED' },
265
+ { root: String.raw`D:\Work Area`, alias: 'WORK' },
266
+ ],
267
+ });
268
+ assert.equal(windows.sanitize(String.raw`c:\workspace\repo\src\file.ts:1:2`), 'src/file.ts:1:2');
269
+ assert.equal(windows.sanitize(String.raw`C:\\Workspace\\Repo\\src\\file.ts`), 'src/file.ts');
270
+ assert.equal(windows.sanitize(String.raw`\\host\share\root\file`), '<SHARED>/file');
271
+ assert.equal(
272
+ windows.sanitize(String.raw`"D:\Work Area\folder name\file"`),
273
+ '"<WORK>/folder name/file"',
274
+ );
275
+ });
276
+
277
+ test('file URLs decode local paths while non-file URLs, API and container paths are untouched', () => {
278
+ const cases = [
279
+ ['file:///synthetic/checkout/src/a.ts:3:2', 'src/a.ts:3:2'],
280
+ ['file:///synthetic/other/folder%20name/a', '<OTHER>/folder name/a'],
281
+ ['file:///Users/Secret/folder%20name/a.ts:3:2', '<PRIVATE_PATH>:3:2'],
282
+ ['file:///C:/Users/Secret/a', '<PRIVATE_PATH>'],
283
+ ['file://private-host/share/Secret/a', '<PRIVATE_PATH>'],
284
+ ['file://localhost/home/Secret/a', '<PRIVATE_PATH>'],
285
+ ['file:///Users/%XX/private', '<PRIVATE_PATH>'],
286
+ ];
287
+ for (const [input, output] of cases) assert.equal(matcher.sanitize(input), output);
288
+ const negatives = [
289
+ 'https://example.test/Users/Secret/a?root=/synthetic/checkout/a',
290
+ 'http://host/tmp/one',
291
+ 's3://bucket/home/name',
292
+ 'ssh://host/Users/name',
293
+ '/api/Users/Secret/a',
294
+ '/api/tmp/a',
295
+ '/app/src/main.ts',
296
+ '/workspace/src/main.ts',
297
+ '/usr/bin/node',
298
+ '/temporary/a',
299
+ '/homebrew/a',
300
+ '/UsersExtra/name/a',
301
+ 'prefix/tmp/file',
302
+ './tmp/file',
303
+ '../home/name/file',
304
+ '<OTHER>/suffix',
305
+ '<PRIVATE_PATH>',
306
+ '//host/Users/name/a',
307
+ ];
308
+ for (const input of negatives) assert.equal(matcher.sanitize(input), input);
309
+ });
310
+
311
+ test('recognition and sanitization agree and remain idempotent across mixed evidence', () => {
312
+ const input = [
313
+ '/synthetic/checkout/a:2:4',
314
+ '/synthetic/other/b',
315
+ '/Users/Secret/a',
316
+ String.raw`C:\\Users\\Secret\\a`,
317
+ 'file:///home/Secret/a',
318
+ 'https://host/Users/Secret/a',
319
+ '/api/a',
320
+ 'plain',
321
+ ].join('\n');
322
+ assert.equal(matcher.find(input).length, 5);
323
+ const sanitized = matcher.sanitize(input);
324
+ assert.equal(matcher.find(sanitized).length, 0);
325
+ assert.equal(matcher.sanitize(sanitized), sanitized);
326
+ });
327
+
328
+ test('stream handles split UTF-8, split paths, CRLF, empty lines and final unterminated line', async () => {
329
+ const bytes = Buffer.from('é🙂 /synthetic/checkout/a\r\n\r\n/home/Secret/file:9:2');
330
+ const chunks = Array.from(bytes, byte => Buffer.from([byte]));
331
+ assert.equal(await streamed(chunks), 'é🙂 a\r\n\r\n<PRIVATE_PATH>:9:2');
332
+ assert.equal(await streamed([]), '');
333
+ assert.equal(await streamed([Buffer.from('\n')]), '\n');
334
+ });
335
+
336
+ test('stream fatal UTF-8 and line limit never emit raw fragments of the failing line', async () => {
337
+ for (const chunks of [
338
+ [Buffer.from('/Users/Secret/a '), Buffer.from([0xc3])],
339
+ [Buffer.from('/Users/Secret/a '), Buffer.from([0xff, 10])],
340
+ ]) {
341
+ const output = sink();
342
+ await assert.rejects(
343
+ sanitizeStream(Readable.from(chunks), output.stream, { matcher }),
344
+ /^Error: INVALID_UTF8$/u,
345
+ );
346
+ assert.equal(output.text(), '');
347
+ }
348
+ const output = sink();
349
+ await assert.rejects(
350
+ sanitizeStream(
351
+ Readable.from([Buffer.from('good\n/Users/Secret/'), Buffer.alloc(LINE_LIMIT, 97)]),
352
+ output.stream,
353
+ { matcher },
354
+ ),
355
+ /^Error: LINE_LIMIT$/u,
356
+ );
357
+ assert.equal(output.text(), 'good\n');
358
+ assert.equal(
359
+ (await streamed([Buffer.alloc(LINE_LIMIT, 97), Buffer.from('\n')])).length,
360
+ LINE_LIMIT + 1,
361
+ );
362
+ await assert.rejects(
363
+ streamed([Buffer.from(`${'é'.repeat(LINE_LIMIT / 2)}x`)]),
364
+ /^Error: LINE_LIMIT$/u,
365
+ );
366
+ await assert.rejects(streamed(['/Users/Secret/raw']), /^Error: INVALID_INPUT$/u);
367
+ });
368
+
369
+ test('stream rejects binary control bytes on complete and final lines without raw fragments', async () => {
370
+ for (const byte of [0, 1, 27, 127]) {
371
+ for (const ending of ['', '\n']) {
372
+ const bytes = Buffer.concat([
373
+ Buffer.from('/Users/Secret/file '),
374
+ Buffer.from([byte]),
375
+ Buffer.from(ending),
376
+ ]);
377
+ const output = sink();
378
+ await assert.rejects(
379
+ sanitizeStream(Readable.from([bytes]), output.stream, { matcher }),
380
+ /^Error: BINARY$/u,
381
+ );
382
+ assert.equal(output.text(), '');
383
+ const result = cli(['sanitize'], bytes);
384
+ assert.equal(result.status, 2);
385
+ assert.equal(result.stdout, '');
386
+ assert.equal(result.stderr, 'evidence-paths: BINARY\n');
387
+ }
388
+ }
389
+ assert.equal(await streamed([Buffer.from('plain\ttext\f\r\n')]), 'plain\ttext\f\r\n');
390
+ });
391
+
392
+ test('stream input/output failures and CLI diagnostics never expose raw exception text or stacks', async () => {
393
+ const input = Readable.from(
394
+ (async function* () {
395
+ throw new Error('/Users/Secret/raw error');
396
+ })(),
397
+ );
398
+ await assert.rejects(sanitizeStream(input, sink().stream, { matcher }), /^Error: INPUT_ERROR$/u);
399
+ const broken = new Writable({
400
+ write(_chunk, _encoding, done) {
401
+ done(new Error('/Users/Secret/output'));
402
+ },
403
+ });
404
+ const errors = sink();
405
+ assert.equal(
406
+ await runCli(['sanitize'], {
407
+ input: Readable.from([Buffer.from('/home/Secret/a\n')]),
408
+ output: broken,
409
+ error: errors.stream,
410
+ }),
411
+ 2,
412
+ );
413
+ assert.equal(errors.text(), 'evidence-paths: OUTPUT_ERROR\n');
414
+ const helpBroken = new Writable({
415
+ write(_chunk, _encoding, done) {
416
+ done(new Error('private'));
417
+ },
418
+ });
419
+ assert.equal(await runCli(['--help'], { output: helpBroken, error: sink().stream }), 2);
420
+ });
421
+
422
+ test('checker scans sorted recursive text of any extension, only reports safe locations, and never writes', async t => {
423
+ const root = await fixture(t, {
424
+ 'z.data': '/home/Secret/z\n',
425
+ 'a/note.weird': 'ordinary\n/synthetic/checkout/source.ts\n/home/Secret/private\n',
426
+ 'clean': 'https://host/Users/Secret/a\n/api/resource\n',
427
+ });
428
+ const paths = ['z.data', 'a/note.weird', 'clean'];
429
+ const before = await Promise.all(
430
+ paths.map(async name => ({
431
+ bytes: await readFile(join(root, name)),
432
+ mtime: (await stat(join(root, name))).mtimeMs,
433
+ })),
434
+ );
435
+ const result = await checkPaths([root], { matcher });
436
+ assert.deepEqual(result, {
437
+ status: 1,
438
+ diagnostics: [
439
+ 'scope-1/a/note.weird:2 PATH_FOUND',
440
+ 'scope-1/a/note.weird:3 PATH_FOUND',
441
+ 'scope-1/z.data:1 PATH_FOUND',
442
+ ],
443
+ });
444
+ const command = cli(['check', '--repo-root', '/synthetic/checkout', '--', root]);
445
+ assert.equal(command.status, 1);
446
+ assert.equal(command.stdout, `${result.diagnostics.join('\n')}\n`);
447
+ assert.equal(command.stderr, '');
448
+ for (let index = 0; index < paths.length; index++) {
449
+ assert.deepEqual(await readFile(join(root, paths[index])), before[index].bytes);
450
+ assert.equal((await stat(join(root, paths[index]))).mtimeMs, before[index].mtime);
451
+ }
452
+ assert.equal((await checkPaths([join(root, 'clean')], { matcher })).status, 0);
453
+ assert.equal(cli(['check', '--', join(root, 'clean')]).status, 0);
454
+ });
455
+
456
+ test('checker excludes env and non-text inputs with incomplete precedence', async t => {
457
+ const root = await fixture(t, {
458
+ '.env.secret': '/home/Secret/env\n',
459
+ '.env-dir/nested': '/home/Secret/nested\n',
460
+ 'binary.any': Buffer.from([0, 1, 2]),
461
+ 'invalid.any': Buffer.from([0xff]),
462
+ 'private.txt': '/home/Secret/private\n',
463
+ 'oversized': Buffer.alloc(FILE_LIMIT + 1, 97),
464
+ 'long-line': Buffer.alloc(LINE_LIMIT + 1, 97),
465
+ });
466
+ const result = await checkPaths([root], { matcher });
467
+ assert.equal(result.status, 2);
468
+ assert.deepEqual(result.diagnostics, [
469
+ 'scope-1/.env-dir:0 ENV_EXCLUDED',
470
+ 'scope-1/.env.secret:0 ENV_EXCLUDED',
471
+ 'scope-1/binary.any:0 BINARY',
472
+ 'scope-1/invalid.any:0 INVALID_UTF8',
473
+ 'scope-1/long-line:0 LINE_LIMIT',
474
+ 'scope-1/oversized:0 SIZE_LIMIT',
475
+ 'scope-1/private.txt:1 PATH_FOUND',
476
+ ]);
477
+ const resultCli = cli(['check', '--', root]);
478
+ assert.equal(resultCli.status, 2);
479
+ assert.equal(resultCli.stdout, `${result.diagnostics.join('\n')}\n`);
480
+ assert.equal(resultCli.stderr, '');
481
+ assert.doesNotMatch(resultCli.stdout + resultCli.stderr, /Secret|\/home\/|evidence-paths-[^/ ]/u);
482
+ assert.deepEqual(await checkPaths([join(root, '.env-dir/nested')], { matcher }), {
483
+ status: 2,
484
+ diagnostics: ['scope-1:0 ENV_EXCLUDED'],
485
+ });
486
+ });
487
+
488
+ test(
489
+ 'checker treats POSIX mode-zero files as unreadable even for privileged runners',
490
+ {
491
+ skip:
492
+ process.platform === 'win32'
493
+ ? 'Windows readability is determined by ACL/open errors, not POSIX mode bits'
494
+ : false,
495
+ },
496
+ async t => {
497
+ const root = await fixture(t, { unreadable: '/home/Secret/a\n' });
498
+ const path = join(root, 'unreadable');
499
+ await chmod(path, 0);
500
+ try {
501
+ assert.deepEqual(await checkPaths([path], { matcher }), {
502
+ status: 2,
503
+ diagnostics: ['scope-1/unreadable:0 UNREADABLE'],
504
+ });
505
+ assert.deepEqual(await checkPaths([root], { matcher }), {
506
+ status: 2,
507
+ diagnostics: ['scope-1/unreadable:0 UNREADABLE'],
508
+ });
509
+ const result = cli(['check', '--', path]);
510
+ assert.equal(result.status, 2);
511
+ assert.equal(result.stdout, 'scope-1/unreadable:0 UNREADABLE\n');
512
+ assert.equal(result.stderr, '');
513
+ } finally {
514
+ await chmod(path, 0o600);
515
+ }
516
+ },
517
+ );
518
+
519
+ test('checker excludes file symlinks as explicit scopes and directory entries', async t => {
520
+ const root = await fixture(t, { 'private.txt': '/home/Secret/private\n' });
521
+ const link = join(root, 'link');
522
+ if (!(await symlinkOrSkip(t, join(root, 'private.txt'), link, 'file'))) return;
523
+ assert.deepEqual(await checkPaths([link], { matcher }), {
524
+ status: 2,
525
+ diagnostics: ['scope-1:0 SYMLINK_EXCLUDED'],
526
+ });
527
+ const result = await checkPaths([root], { matcher });
528
+ assert.deepEqual(result, {
529
+ status: 2,
530
+ diagnostics: ['scope-1/link:0 SYMLINK_EXCLUDED', 'scope-1/private.txt:1 PATH_FOUND'],
531
+ });
532
+ const command = cli(['check', '--', root]);
533
+ assert.equal(command.status, result.status);
534
+ assert.equal(command.stdout, `${result.diagnostics.join('\n')}\n`);
535
+ assert.equal(command.stderr, '');
536
+ });
537
+
538
+ async function checkDirectoryLink(t, type) {
539
+ const root = await fixture(t, { 'target/private.txt': '/home/Secret/private\n' });
540
+ const link = join(root, 'directory-link');
541
+ if (!(await symlinkOrSkip(t, join(root, 'target'), link, type))) return;
542
+ assert.deepEqual(await checkPaths([link, join(link, 'private.txt')], { matcher }), {
543
+ status: 2,
544
+ diagnostics: ['scope-1:0 SYMLINK_EXCLUDED', 'scope-2:0 SYMLINK_EXCLUDED'],
545
+ });
546
+ assert.deepEqual(await checkPaths([root], { matcher }), {
547
+ status: 2,
548
+ diagnostics: [
549
+ 'scope-1/directory-link:0 SYMLINK_EXCLUDED',
550
+ 'scope-1/target/private.txt:1 PATH_FOUND',
551
+ ],
552
+ });
553
+ const command = cli(['check', '--', join(link, 'private.txt')]);
554
+ assert.equal(command.status, 2);
555
+ assert.equal(command.stdout, 'scope-1:0 SYMLINK_EXCLUDED\n');
556
+ assert.equal(command.stderr, '');
557
+ }
558
+
559
+ test('checker excludes directory symlinks and symlink scope ancestors', async t => {
560
+ await checkDirectoryLink(t, 'dir');
561
+ });
562
+
563
+ test(
564
+ 'checker excludes Windows junctions and junction scope ancestors',
565
+ { skip: process.platform !== 'win32' ? 'Windows junction capability test' : false },
566
+ async t => {
567
+ await checkDirectoryLink(t, 'junction');
568
+ },
569
+ );
570
+
571
+ test(
572
+ 'special files are excluded before open',
573
+ { skip: process.platform === 'win32' ? 'mkfifo requires POSIX' : false },
574
+ async t => {
575
+ const root = await fixture(t);
576
+ const fifo = join(root, 'fifo');
577
+ const made = spawnSync('mkfifo', [fifo]);
578
+ assert.equal(made.status, 0);
579
+ assert.deepEqual(await checkPaths([fifo], { matcher }), {
580
+ status: 2,
581
+ diagnostics: ['scope-1/fifo:0 SPECIAL_EXCLUDED'],
582
+ });
583
+ },
584
+ );
585
+
586
+ test('missing scopes reveal no root', async t => {
587
+ const root = await fixture(t);
588
+ const missing = cli(['check', '--', join(root, 'private-identity/missing')]);
589
+ assert.equal(missing.status, 2);
590
+ assert.equal(missing.stdout, 'scope-1:0 UNREADABLE\n');
591
+ assert.equal(missing.stderr, '');
592
+ });
593
+
594
+ test('portable diagnostic filenames escape Unicode formatting characters', async t => {
595
+ const name = 'evil\u202E name-é.txt';
596
+ const root = await fixture(t, { [name]: '/home/Secret/a\n' });
597
+ const result = await checkPaths([root, join(root, name)], { matcher });
598
+ assert.deepEqual(result, {
599
+ status: 1,
600
+ diagnostics: [
601
+ 'scope-1/evil\\u202e name-é.txt:1 PATH_FOUND',
602
+ 'scope-2/evil\\u202e name-é.txt:1 PATH_FOUND',
603
+ ],
604
+ });
605
+ const command = cli(['check', '--', root, join(root, name)]);
606
+ assert.equal(command.status, result.status);
607
+ assert.equal(command.stdout, `${result.diagnostics.join('\n')}\n`);
608
+ assert.equal(command.stderr, '');
609
+ });
610
+
611
+ test(
612
+ 'POSIX diagnostic filenames sanitize Windows-looking paths and escape line/control/colon characters',
613
+ {
614
+ skip:
615
+ process.platform === 'win32'
616
+ ? 'These literal filename characters are illegal on Windows'
617
+ : false,
618
+ },
619
+ async t => {
620
+ const name = 'evil\n\u001B\u202E:name';
621
+ const privateName = String.raw`C:\Users\Secret\file`;
622
+ const root = await fixture(t, {
623
+ [name]: '/home/Secret/a\n',
624
+ [privateName]: '/home/Secret/a\n',
625
+ });
626
+ assert.deepEqual(await checkPaths([root], { matcher }), {
627
+ status: 1,
628
+ diagnostics: [
629
+ 'scope-1/<PRIVATE_PATH>:1 PATH_FOUND',
630
+ 'scope-1/evil\\u000a\\u001b\\u202e\\u003aname:1 PATH_FOUND',
631
+ ],
632
+ });
633
+ const direct = await checkPaths([join(root, privateName), join(root, name)], { matcher });
634
+ assert.deepEqual(direct, {
635
+ status: 1,
636
+ diagnostics: [
637
+ 'scope-1/<PRIVATE_PATH>:1 PATH_FOUND',
638
+ 'scope-2/evil\\u000a\\u001b\\u202e\\u003aname:1 PATH_FOUND',
639
+ ],
640
+ });
641
+ const command = cli(['check', '--', join(root, privateName), join(root, name)]);
642
+ assert.equal(command.status, direct.status);
643
+ assert.equal(command.stdout, `${direct.diagnostics.join('\n')}\n`);
644
+ assert.equal(command.stderr, '');
645
+ assert.doesNotMatch(command.stdout, /Secret/u);
646
+ },
647
+ );
648
+
649
+ test(
650
+ 'POSIX literal backslashes remain filename characters in explicit scopes and directory scans',
651
+ { skip: process.platform === 'win32' ? 'Backslash is a path separator on Windows' : false },
652
+ async t => {
653
+ const name = String.raw`literal\file.txt`;
654
+ const directory = String.raw`literal\directory`;
655
+ const root = await fixture(t, {
656
+ [name]: '/home/Secret/a\n',
657
+ [join(directory, 'evidence.txt')]: '/home/Secret/b\n',
658
+ });
659
+ assert.deepEqual(await checkPaths([join(root, name)], { matcher }), {
660
+ status: 1,
661
+ diagnostics: ['scope-1/literal\\u005cfile.txt:1 PATH_FOUND'],
662
+ });
663
+ assert.deepEqual(await checkPaths([join(root, directory)], { matcher }), {
664
+ status: 1,
665
+ diagnostics: ['scope-1/evidence.txt:1 PATH_FOUND'],
666
+ });
667
+ assert.deepEqual(await checkPaths([join(root, directory, 'evidence.txt')], { matcher }), {
668
+ status: 1,
669
+ diagnostics: ['scope-1/evidence.txt:1 PATH_FOUND'],
670
+ });
671
+ const result = await checkPaths([root], { matcher });
672
+ assert.deepEqual(result, {
673
+ status: 1,
674
+ diagnostics: [
675
+ 'scope-1/literal\\u005cdirectory/evidence.txt:1 PATH_FOUND',
676
+ 'scope-1/literal\\u005cfile.txt:1 PATH_FOUND',
677
+ ],
678
+ });
679
+ const command = cli(['check', '--', join(root, name), root]);
680
+ assert.equal(command.status, 1);
681
+ assert.equal(
682
+ command.stdout,
683
+ [
684
+ 'scope-1/literal\\u005cfile.txt:1 PATH_FOUND',
685
+ ...result.diagnostics.map(label => label.replace('scope-1/', 'scope-2/')),
686
+ '',
687
+ ].join('\n'),
688
+ );
689
+ assert.equal(command.stderr, '');
690
+ },
691
+ );
692
+
693
+ test('sanitized files are clean under the same shared checker', async t => {
694
+ const raw = '/synthetic/checkout/file:1:2\n/synthetic/other/file\nfile:///Users/Secret/file\n';
695
+ const root = await fixture(t, { evidence: matcher.sanitize(raw) });
696
+ assert.deepEqual(await checkPaths([root], { matcher }), { status: 0, diagnostics: [] });
697
+ });
698
+
699
+ test('CLI exact conventions, safe usage errors, explicit scope requirement and statuses', () => {
700
+ const good = cli(
701
+ ['sanitize', '--repo-root', '/synthetic/checkout', '--map', '/synthetic/other=<OTHER>'],
702
+ '/synthetic/checkout/a\r\n/synthetic/other/b\n/home/Secret/c',
703
+ );
704
+ assert.equal(good.status, 0);
705
+ assert.equal(good.stdout, 'a\r\n<OTHER>/b\n<PRIVATE_PATH>');
706
+ assert.equal(good.stderr, '');
707
+ for (const args of [
708
+ [],
709
+ ['unknown-private-argument'],
710
+ ['check'],
711
+ ['check', '--'],
712
+ ['check', '/private/scope'],
713
+ ['sanitize', '--', 'private-command'],
714
+ ['sanitize', '--output', '/private/path'],
715
+ ['sanitize', '--repo-root', '/private/path', '--repo-root', '/other/path'],
716
+ ['sanitize', '--map', '/private/path=bad-private-alias'],
717
+ ['sanitize', '--map', 'private-root=ROOT'],
718
+ ]) {
719
+ const result = cli(args);
720
+ assert.equal(result.status, 2, JSON.stringify(args));
721
+ assert.equal(result.stdout, '');
722
+ assert.match(result.stderr, /^evidence-paths: [A-Z_]+\n$/u);
723
+ }
724
+ const utf8 = cli(['sanitize'], Buffer.from([0xff]));
725
+ assert.equal(utf8.status, 2);
726
+ assert.equal(utf8.stdout, '');
727
+ assert.equal(utf8.stderr, 'evidence-paths: INVALID_UTF8\n');
728
+ const limited = cli(['sanitize'], Buffer.alloc(LINE_LIMIT + 1, 97));
729
+ assert.equal(limited.status, 2);
730
+ assert.equal(limited.stdout, '');
731
+ assert.equal(limited.stderr, 'evidence-paths: LINE_LIMIT\n');
732
+ const help = cli(['--help']);
733
+ assert.equal(help.status, 0);
734
+ assert.match(help.stdout, /NOT a universal secret/u);
735
+ assert.match(help.stdout, /Quote path tokens containing spaces/u);
736
+ });