start-command 0.39.0 → 0.39.2

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.
@@ -6,156 +6,238 @@ const os = require('os');
6
6
  const path = require('path');
7
7
  const { execFileSync } = require('child_process');
8
8
  const {
9
+ AuthorizationHeaderMask,
10
+ SanitizationError,
11
+ resolveSanitizerCommand,
9
12
  sanitizeLogToTemp,
10
- StreamingSanitizer,
13
+ sanitizeText,
11
14
  } = require('../src/lib/log-sanitizer');
12
15
 
16
+ const GH_CONFIG_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'sanitize-gh-'));
17
+
18
+ // Only what the runtime needs, so the host's own credential-named variables
19
+ // and gh login cannot change the expected output.
20
+ function testEnv(extra = {}) {
21
+ const env = { GH_CONFIG_DIR };
22
+ for (const name of [
23
+ 'PATH',
24
+ 'Path',
25
+ 'SystemRoot',
26
+ 'SYSTEMROOT',
27
+ 'TEMP',
28
+ 'TMP',
29
+ 'TMPDIR',
30
+ 'HOME',
31
+ 'USERPROFILE',
32
+ ]) {
33
+ if (process.env[name] !== undefined) {
34
+ env[name] = process.env[name];
35
+ }
36
+ }
37
+ return { ...env, ...extra };
38
+ }
39
+
40
+ function withLog(content, callback) {
41
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sanitize-test-'));
42
+ const source = path.join(dir, 'source.log');
43
+ fs.writeFileSync(source, content);
44
+ try {
45
+ return callback(source, dir);
46
+ } finally {
47
+ fs.rmSync(dir, { recursive: true, force: true });
48
+ }
49
+ }
50
+
51
+ function sanitizeFile(content, env = testEnv()) {
52
+ return withLog(content, (source) => {
53
+ const copy = sanitizeLogToTemp(source, { env });
54
+ try {
55
+ return fs.readFileSync(copy.path);
56
+ } finally {
57
+ copy.cleanup();
58
+ }
59
+ });
60
+ }
61
+
62
+ function mask(input, blockSize) {
63
+ const authorization = new AuthorizationHeaderMask();
64
+ const blocks = [];
65
+ for (let offset = 0; offset < input.length; offset += blockSize) {
66
+ blocks.push(authorization.push(input.subarray(offset, offset + blockSize)));
67
+ assert.ok(authorization.pending.length <= blockSize + 160);
68
+ }
69
+ blocks.push(authorization.push(Buffer.alloc(0), true));
70
+ return Buffer.concat(blocks).toString();
71
+ }
72
+
73
+ // The three lines from issue #249, which the old rules truncated.
74
+ const ISSUE_249_LINES = [
75
+ "Command: gate='/tmp/hive-mind-disk-baseline-0ce9e170-e6d3-494d-91e1-0d820245d34b'",
76
+ '🧠 Formal AI: endpoint http://172.18.0.2:8080/api/openai/v1',
77
+ '{"bodyPreview":"{\\"messages\\":[{\\"content\\":\\"use: curl -L -H \\\\\\"Authorization: token $(gh auth token)\\\\\\" -o <filename> <url>\\\\n...the rest of a 100 KB prompt...\\"}]}"}',
78
+ 'proxy http://myproxy:3128 and http://127.0.0.1:34271/session',
79
+ 'Authorization: [REDACTED] stays as it is',
80
+ 'account konard@example.com https://github.com/konard',
81
+ ];
82
+
13
83
  function credentials() {
14
84
  return [
15
85
  ...'pousr'.split('').map((kind) => `gh${kind}_${'a'.repeat(36)}`),
16
86
  `github_` + `pat_${'b'.repeat(82)}`,
17
87
  `sk-${'c'.repeat(48)}`,
18
88
  `sk-` + `proj-${'d'.repeat(90)}`,
19
- `sk-` + `ant-api03-${'e'.repeat(90)}`,
20
89
  `AK` + `IA${'F'.repeat(16)}`,
21
90
  `AS` + `IA${'G'.repeat(16)}`,
22
91
  'ey' + 'JhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.signature',
23
- 'Authorization: Bearer arbitrary-credential',
24
- 'Proxy-Authorization: Basic encoded-credential',
25
- 'https://user:password@example.test/repo.git',
26
- 'AWS_SECRET_ACCESS_KEY=arbitrary-aws-secret',
92
+ 'arbitrary-credential',
93
+ 'encoded-credential',
94
+ 'password',
95
+ 'arbitrary-aws-secret',
27
96
  'custom-secret-value',
28
- `${Buffer.from(' \n{"alg":"HS256"}').toString('base64url')}.${Buffer.from('{"sub":"x"}').toString('base64url')}.signature`,
29
97
  ];
30
98
  }
31
99
 
32
- function sanitize(input, blockSize) {
33
- const sanitizer = new StreamingSanitizer({
34
- START_TEST_SECRET: 'custom-secret-value',
100
+ describe('log sanitization', () => {
101
+ it('keeps every line from issue #249 and public account details', () => {
102
+ const input = `${ISSUE_249_LINES.join('\n')}\n`;
103
+ assert.strictEqual(sanitizeFile(input).toString(), input);
35
104
  });
36
- const blocks = [];
37
- for (let offset = 0; offset < input.length; offset += blockSize) {
38
- blocks.push(sanitizer.push(input.subarray(offset, offset + blockSize)));
39
- }
40
- blocks.push(sanitizer.push(Buffer.alloc(0), true));
41
- return Buffer.concat(blocks);
42
- }
43
105
 
44
- describe('log sanitization', () => {
45
- it('redacts required formats and environment values at every chunk boundary', () => {
46
- const secrets = credentials();
47
- for (const size of [1, 7, 31, 256, 1024, 65536]) {
48
- const input = Buffer.from(
49
- `ordinary utf8: café 😀\n${' '.repeat(Math.max(0, size - 43))}${secrets.join('\n')}\n${'padding\n'.repeat(400)}finished\n`
50
- );
51
- const output = sanitize(input, size).toString();
52
- for (const secret of secrets) {
53
- assert.ok(!output.includes(secret));
54
- }
55
- for (const value of [
56
- 'arbitrary-credential',
57
- 'encoded-credential',
58
- 'password',
59
- 'arbitrary-aws-secret',
60
- ]) {
61
- assert.ok(!output.includes(value));
62
- }
63
- assert.ok(output.startsWith('ordinary utf8: café 😀\n'));
64
- assert.ok(output.endsWith('\nfinished\n'));
106
+ it('redacts credentials and credential-named environment values', () => {
107
+ const input = [
108
+ ...credentials().slice(0, 9),
109
+ 'Authorization: Bearer arbitrary-credential',
110
+ 'Proxy-Authorization: Basic encoded-credential',
111
+ 'clone https://user:password@example.test/repo.git',
112
+ 'AWS_SECRET_ACCESS_KEY=arbitrary-aws-secret',
113
+ 'value custom-secret-value end',
114
+ 'ordinary utf8: café 😀',
115
+ '',
116
+ ].join('\n');
117
+ const output = sanitizeFile(
118
+ input,
119
+ testEnv({ START_TEST_SECRET: 'custom-secret-value' })
120
+ ).toString();
121
+ for (const [index, secret] of credentials().entries()) {
122
+ assert.ok(!output.includes(secret), `leaked secret #${index}`);
123
+ }
124
+ for (const kept of [
125
+ 'Authorization: [REDACTED]\n',
126
+ 'Proxy-Authorization: [REDACTED]\n',
127
+ 'clone https://[REDACTED]@example.test/repo.git\n',
128
+ 'value [REDACTED] end\n',
129
+ 'ordinary utf8: café 😀\n',
130
+ ]) {
131
+ assert.ok(output.includes(kept), `missing ${kept}`);
65
132
  }
66
133
  });
67
134
 
68
- it('bounds memory for long tokens and authorization values without releasing suffixes', () => {
69
- for (const prefix of ['gh' + 'p_', 'Authorization: Bearer ']) {
70
- const sanitizer = new StreamingSanitizer({});
71
- let output = sanitizer.push(Buffer.from(prefix));
72
- for (let i = 0; i < 128; i++) {
73
- output = Buffer.concat([
74
- output,
75
- sanitizer.push(Buffer.alloc(16384, 97)),
76
- ]);
77
- assert.ok(sanitizer.pendingBytes <= 65536);
78
- }
79
- output = Buffer.concat([
80
- output,
81
- sanitizer.push(Buffer.from('\nordinary\n'), true),
82
- ]);
83
- assert.strictEqual(output.toString(), '[REDACTED]\nordinary\n');
135
+ it('masks only the Authorization scheme and token, keeping the rest of the line (#249)', () => {
136
+ const cases = [
137
+ [
138
+ `curl -H 'Authorization: Basic dXNlcjpwYXNz' -o out.txt url`,
139
+ `curl -H 'Authorization: [REDACTED]' -o out.txt url`,
140
+ ],
141
+ [
142
+ 'Authorization: Bearer abc rest of line',
143
+ 'Authorization: [REDACTED] rest of line',
144
+ ],
145
+ [
146
+ 'Authorization: token abc, other: keep',
147
+ 'Authorization: [REDACTED], other: keep',
148
+ ],
149
+ [
150
+ '{"h":"Authorization: Bearer abc\\" tail","next":"keep"}',
151
+ '{"h":"Authorization: [REDACTED]\\" tail","next":"keep"}',
152
+ ],
153
+ [
154
+ 'Authorization: token $(gh auth token) and ${GITHUB_TOKEN}',
155
+ 'Authorization: token $(gh auth token) and ${GITHUB_TOKEN}',
156
+ ],
157
+ ['Authorization: Bearer <token>', 'Authorization: Bearer <token>'],
158
+ ];
159
+ const input = cases.map(([line]) => line).join('\n');
160
+ const expected = cases.map(([, line]) => line).join('\n');
161
+ for (const size of [1, 7, 31, 256, 65536]) {
162
+ assert.strictEqual(mask(Buffer.from(input), size), expected);
84
163
  }
164
+ // The library itself masks the scheme before a placeholder; nothing is lost.
165
+ assert.strictEqual(
166
+ sanitizeFile(input).toString(),
167
+ expected.replace('Bearer <token>', '[REDACTED] <token>')
168
+ );
85
169
  });
86
170
 
87
- it('retains full header continuation when an inner token reaches the same block boundary', () => {
88
- const sanitizer = new StreamingSanitizer({});
171
+ it('masks long Authorization tokens across blocks with bounded memory', () => {
89
172
  const input = Buffer.from(
90
- `Authorization: Bearer gh${'p_'}${'a'.repeat(131072)} trailing-private-value\nordinary\n`
173
+ `Authorization: Bearer ${'a'.repeat(200000)} tail\nordinary\n`
91
174
  );
92
- const output = [];
93
- for (let offset = 0; offset < input.length; offset += 65536) {
94
- output.push(sanitizer.push(input.subarray(offset, offset + 65536)));
95
- }
96
- output.push(sanitizer.push(Buffer.alloc(0), true));
97
175
  assert.strictEqual(
98
- Buffer.concat(output).toString(),
99
- '[REDACTED]\nordinary\n'
176
+ mask(input, 65536),
177
+ 'Authorization: [REDACTED] tail\nordinary\n'
178
+ );
179
+ assert.strictEqual(
180
+ sanitizeFile(input).toString(),
181
+ 'Authorization: [REDACTED] tail\nordinary\n'
100
182
  );
101
183
  });
102
184
 
103
- it('redacts AWS assignment values enclosed in either quote style', () => {
104
- const sanitizer = new StreamingSanitizer({});
105
- const output = sanitizer
106
- .push(
107
- Buffer.from(
108
- `AWS_SECRET_ACCESS_KEY="quoted-aws-secret"\nAWS_SESSION_TOKEN='quoted-session-value'\n`
109
- ),
110
- true
111
- )
112
- .toString();
113
- assert.ok(!output.includes('quoted-aws-secret'));
114
- assert.ok(!output.includes('quoted-session-value'));
115
- });
116
-
117
- it('redacts multiline and short known secrets, preserving unrelated bytes', () => {
118
- const sanitizer = new StreamingSanitizer({
119
- GH_TOKEN: 'abc\nxyz',
120
- TEST_PASSWORD: '!',
121
- });
122
- const output = sanitizer
123
- .push(Buffer.from('safe abc\nxyz value !'), true)
124
- .toString();
125
- assert.strictEqual(output, 'safe [REDACTED] value [REDACTED]');
185
+ it('preserves invalid UTF-8 bytes and line endings', () => {
186
+ const input = Buffer.concat([
187
+ Buffer.from('bad '),
188
+ Buffer.from([0xff, 0xfe, 0xc3]),
189
+ Buffer.from(` gh${'p_'}${'x'.repeat(36)}\r\nnext\rend`),
190
+ ]);
191
+ const expected = Buffer.concat([
192
+ Buffer.from('bad '),
193
+ Buffer.from([0xff, 0xfe, 0xc3]),
194
+ Buffer.from(' [REDACTED]\r\nnext\rend'),
195
+ ]);
196
+ assert.deepStrictEqual(sanitizeFile(input), expected);
197
+ assert.deepStrictEqual(sanitizeFile(Buffer.alloc(0)), Buffer.alloc(0));
126
198
  });
127
199
 
128
- it('merges overlapping environment secrets and preserves ordinary domain names', () => {
129
- const sanitizer = new StreamingSanitizer({
130
- GH_TOKEN: 'abcde',
131
- TEST_PASSWORD: 'cdefg',
132
- GITHUB_PAT: 'opaque-known-pat',
133
- });
200
+ it('sanitizes issue text with the same rules', () => {
201
+ const token = `gh${'p_'}${'a'.repeat(36)}`;
134
202
  assert.strictEqual(
135
- sanitizer
136
- .push(
137
- Buffer.from('abcdefg opaque-known-pat example.test.invalid'),
138
- true
139
- )
140
- .toString(),
141
- '[REDACTED] [REDACTED] example.test.invalid'
203
+ sanitizeText(`echo ${token} 😀 'Authorization: token abc' -o x`, {
204
+ env: testEnv(),
205
+ }),
206
+ `echo [REDACTED] 😀 'Authorization: [REDACTED]' -o x`
142
207
  );
143
- const short = new StreamingSanitizer({ GH_TOKEN: 'a' });
144
208
  assert.strictEqual(
145
- short.push(Buffer.alloc(65536, 97), true).toString(),
146
- '[REDACTED]'
209
+ sanitizeText('echo "quoted" $(id) `hostname`', { env: testEnv() }),
210
+ 'echo "quoted" $(id) `hostname`'
211
+ );
212
+ });
213
+
214
+ it('resolves the override first and the bundled library otherwise', () => {
215
+ assert.deepStrictEqual(
216
+ resolveSanitizerCommand({ START_SENSITIVE_DATA_SANITIZER: '/x/sds' }),
217
+ { command: '/x/sds', args: [] }
218
+ );
219
+ const bundled = resolveSanitizerCommand({});
220
+ assert.strictEqual(bundled.command, process.execPath);
221
+ assert.ok(
222
+ bundled.args[0].endsWith(
223
+ path.join(
224
+ 'sensitive-data-sanitizer',
225
+ 'bin',
226
+ 'sensitive-data-sanitizer.js'
227
+ )
228
+ )
147
229
  );
148
230
  });
149
231
 
150
232
  it('creates a private sanitized copy and removes it without changing the source', () => {
151
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sanitize-test-'));
152
- const source = path.join(dir, 'source.log');
153
233
  const input = `gh` + `p_${'x'.repeat(36)}`;
154
- fs.writeFileSync(source, input);
155
- try {
156
- const copy = sanitizeLogToTemp(source, { env: {} });
234
+ withLog(input, (source) => {
235
+ const copy = sanitizeLogToTemp(source, { env: testEnv() });
157
236
  assert.notStrictEqual(copy.path, source);
158
237
  assert.strictEqual(fs.readFileSync(copy.path, 'utf8'), '[REDACTED]');
238
+ assert.deepStrictEqual(fs.readdirSync(path.dirname(copy.path)), [
239
+ 'execution.log',
240
+ ]);
159
241
  if (process.platform !== 'win32') {
160
242
  assert.strictEqual(fs.statSync(copy.path).mode & 0o777, 0o600);
161
243
  assert.strictEqual(
@@ -165,23 +247,17 @@ describe('log sanitization', () => {
165
247
  }
166
248
  assert.strictEqual(fs.readFileSync(source, 'utf8'), input);
167
249
  copy.cleanup();
168
- assert.ok(!fs.existsSync(copy.path));
169
- } finally {
170
- fs.rmSync(dir, { recursive: true, force: true });
171
- }
250
+ assert.ok(!fs.existsSync(path.dirname(copy.path)));
251
+ });
172
252
  });
173
253
 
174
- it('enforces private permissions at creation even with a permissive umask', () => {
254
+ it('keeps private permissions even with a permissive umask', () => {
175
255
  if (process.platform === 'win32') {
176
256
  return;
177
257
  }
178
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sanitize-creation-'));
179
- const source = path.join(dir, 'source.log');
180
- fs.writeFileSync(source, 'ordinary output');
181
258
  const originalOpen = fs.openSync;
182
259
  const originalUmask = process.umask(0);
183
260
  let creationChecked = false;
184
- let copy;
185
261
  fs.openSync = (...args) => {
186
262
  if (args[1] !== 'wx') {
187
263
  return originalOpen(...args);
@@ -196,26 +272,104 @@ describe('log sanitization', () => {
196
272
  return descriptor;
197
273
  };
198
274
  try {
199
- copy = sanitizeLogToTemp(source, { env: {} });
200
- assert.ok(creationChecked);
201
- assert.strictEqual(fs.readFileSync(copy.path, 'utf8'), 'ordinary output');
275
+ withLog('ordinary output', (source) => {
276
+ const copy = sanitizeLogToTemp(source, { env: testEnv() });
277
+ try {
278
+ assert.ok(creationChecked);
279
+ assert.strictEqual(fs.statSync(copy.path).mode & 0o777, 0o600);
280
+ assert.strictEqual(
281
+ fs.readFileSync(copy.path, 'utf8'),
282
+ 'ordinary output'
283
+ );
284
+ } finally {
285
+ copy.cleanup();
286
+ }
287
+ });
202
288
  } finally {
203
289
  fs.openSync = originalOpen;
204
290
  process.umask(originalUmask);
205
- copy?.cleanup();
206
- fs.rmSync(dir, { recursive: true, force: true });
207
291
  }
208
292
  });
209
293
 
210
- it('fails closed on invalid source and excessive environment secret sizes', () => {
211
- assert.throws(
212
- () => sanitizeLogToTemp('/does-not-exist', { env: {} }),
213
- /sanitization failed/i
214
- );
294
+ it('reports scanned bytes and added redactions in verbose mode only', () => {
295
+ const messages = [];
296
+ const originalLog = console.log;
297
+ console.log = (message) => messages.push(message);
298
+ try {
299
+ withLog(`[REDACTED] gh${'p_'}${'x'.repeat(36)}\n`, (source) => {
300
+ sanitizeLogToTemp(source, { env: testEnv() }).cleanup();
301
+ sanitizeLogToTemp(source, { env: testEnv(), verbose: true }).cleanup();
302
+ });
303
+ } finally {
304
+ console.log = originalLog;
305
+ }
306
+ assert.deepStrictEqual(messages, [
307
+ 'Prepared private sanitized log: 52 bytes scanned, 1 redactions.',
308
+ ]);
309
+ });
310
+
311
+ it('fails closed with value-free codes and leaves no copy behind', () => {
312
+ const before = fs
313
+ .readdirSync(os.tmpdir())
314
+ .filter((name) => name.startsWith('start-sanitized-')).length;
315
+ const attempt = (source, env = testEnv()) => {
316
+ try {
317
+ sanitizeLogToTemp(source, { env }).cleanup();
318
+ } catch (error) {
319
+ assert.ok(error instanceof SanitizationError);
320
+ return error;
321
+ }
322
+ assert.fail('sanitization must fail');
323
+ };
324
+ assert.match(attempt('/does-not-exist').message, /\(ERR_SOURCE\)/);
325
+ assert.match(attempt(os.tmpdir()).message, /\(ERR_SOURCE\)/);
326
+ withLog('secret\0binary', (source, dir) => {
327
+ // The library rejects NUL bytes rather than guessing at binary content.
328
+ assert.strictEqual(attempt(source).code, 'ERR_BINARY');
329
+ assert.strictEqual(
330
+ attempt(
331
+ source,
332
+ testEnv({
333
+ START_SENSITIVE_DATA_SANITIZER: path.join(dir, 'missing'),
334
+ })
335
+ ).code,
336
+ 'ERR_UNAVAILABLE'
337
+ );
338
+ if (process.platform !== 'win32') {
339
+ const failing = path.join(dir, 'failing-sanitizer');
340
+ fs.writeFileSync(
341
+ failing,
342
+ '#!/bin/sh\necho "secret-from-stderr (ERR_WORKER)" >&2\nexit 2\n',
343
+ { mode: 0o755 }
344
+ );
345
+ const error = attempt(
346
+ source,
347
+ testEnv({ START_SENSITIVE_DATA_SANITIZER: failing })
348
+ );
349
+ assert.strictEqual(
350
+ error.message,
351
+ 'Log sanitization failed (ERR_WORKER); upload blocked.'
352
+ );
353
+ // A sanitizer that succeeds without writing the output blocks too.
354
+ fs.writeFileSync(failing, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
355
+ assert.strictEqual(
356
+ attempt(source, testEnv({ START_SENSITIVE_DATA_SANITIZER: failing }))
357
+ .code,
358
+ 'ERR_OUTPUT'
359
+ );
360
+ }
361
+ });
215
362
  assert.throws(
216
- () => new StreamingSanitizer({ GH_TOKEN: 'a'.repeat(65537) }),
217
- /too large/i
363
+ () =>
364
+ sanitizeText('x', {
365
+ env: testEnv({ START_SENSITIVE_DATA_SANITIZER: '/does-not-exist' }),
366
+ }),
367
+ /^Error: Text sanitization failed \(ERR_UNAVAILABLE\); publication blocked\.$/
218
368
  );
369
+ const after = fs
370
+ .readdirSync(os.tmpdir())
371
+ .filter((name) => name.startsWith('start-sanitized-')).length;
372
+ assert.ok(after <= before);
219
373
  });
220
374
 
221
375
  it('blocks both upload paths on read or write failure and does not expose exception text', () => {
@@ -234,7 +388,12 @@ describe('log sanitization', () => {
234
388
  throw new Error('sensitive error details');
235
389
  };
236
390
  try {
237
- assert.strictEqual(uploadLogPath(source).success, false);
391
+ const result = uploadLogPath(source);
392
+ assert.strictEqual(result.success, false);
393
+ assert.strictEqual(
394
+ result.error,
395
+ 'Log sanitization failed (ERR_SOURCE); upload blocked.'
396
+ );
238
397
  assert.strictEqual(uploadLog(source), null);
239
398
  } finally {
240
399
  fs[method] = original;
@@ -245,6 +404,11 @@ describe('log sanitization', () => {
245
404
  (message) => !message.includes('sensitive error details')
246
405
  )
247
406
  );
407
+ assert.ok(
408
+ messages.includes(
409
+ 'Warning: Log sanitization failed (ERR_SOURCE); upload blocked.'
410
+ )
411
+ );
248
412
  } finally {
249
413
  console.log = originalLog;
250
414
  fs.rmSync(dir, { recursive: true, force: true });
@@ -257,7 +421,9 @@ describe('log sanitization', () => {
257
421
  }
258
422
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'upload-sanitize-test-'));
259
423
  const log = path.join(dir, 'source.log');
260
- fs.writeFileSync(log, `gh` + `p_${'x'.repeat(36)}`);
424
+ const line =
425
+ "Command: gate='/tmp/hive-mind-disk-baseline-0ce9e170' http://172.18.0.2:8080/v1";
426
+ fs.writeFileSync(log, `${line} gh` + `p_${'x'.repeat(36)}`);
261
427
  fs.writeFileSync(
262
428
  path.join(dir, 'gh-upload-log'),
263
429
  '#!/bin/sh\nprintf "%s\\n" "$@" > "$CAPTURE_ARGS"\ncat "$1" > "$CAPTURE_LOG"\nls -ld "$1" | cut -c1-10 > "$CAPTURE_MODE"\necho https://gist.github.com/test/id\n',
@@ -270,15 +436,14 @@ describe('log sanitization', () => {
270
436
  ? `require(${JSON.stringify(require.resolve('../src/lib/failure-handler'))}).uploadLog(process.env.SOURCE_LOG)`
271
437
  : `require(${JSON.stringify(require.resolve('../src/lib/log-uploader'))}).uploadLogPath(process.env.SOURCE_LOG, { noSanitize: ${kind === 'optout'} })`;
272
438
  execFileSync(process.execPath, ['-e', script], {
273
- env: {
274
- ...process.env,
439
+ env: testEnv({
275
440
  PATH: `${dir}${path.delimiter}${process.env.PATH}`,
276
441
  SOURCE_LOG: log,
277
442
  CAPTURE_ARGS: path.join(dir, 'args'),
278
443
  CAPTURE_LOG: path.join(dir, 'uploaded'),
279
444
  CAPTURE_MODE: path.join(dir, 'mode'),
280
445
  GH_UPLOAD_PUBLIC: 'true',
281
- },
446
+ }),
282
447
  stdio: 'pipe',
283
448
  });
284
449
  const args = fs
@@ -288,7 +453,9 @@ describe('log sanitization', () => {
288
453
  assert.strictEqual(args[1], '--private');
289
454
  assert.strictEqual(
290
455
  fs.readFileSync(path.join(dir, 'uploaded'), 'utf8'),
291
- kind === 'optout' ? fs.readFileSync(log, 'utf8') : '[REDACTED]'
456
+ kind === 'optout'
457
+ ? fs.readFileSync(log, 'utf8')
458
+ : `${line} [REDACTED]`
292
459
  );
293
460
  if (kind !== 'optout') {
294
461
  assert.notStrictEqual(args[0], log);