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.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -1
- package/package.json +2 -1
- package/src/lib/cgroup-memory.js +35 -11
- package/src/lib/failure-handler.js +12 -6
- package/src/lib/log-sanitizer.js +303 -220
- package/src/lib/log-uploader.js +6 -3
- package/src/lib/screen-isolation.js +72 -42
- package/test/isolation.js +32 -0
- package/test/log-sanitizer.js +302 -135
- package/test/regression-247.js +140 -0
package/test/log-sanitizer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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('
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
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('
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
.
|
|
113
|
-
|
|
114
|
-
assert.
|
|
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('
|
|
129
|
-
const
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
146
|
-
'
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
}
|
|
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('
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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('
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
() =>
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
|
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);
|