@mossbear/eval-core 0.1.0-alpha.100
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/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/adapter.d.ts +31 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +10 -0
- package/dist/adapter.js.map +1 -0
- package/dist/credential-patterns.d.ts +9 -0
- package/dist/credential-patterns.d.ts.map +1 -0
- package/dist/credential-patterns.js +9 -0
- package/dist/credential-patterns.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/redact-prose.d.ts +77 -0
- package/dist/redact-prose.d.ts.map +1 -0
- package/dist/redact-prose.js +339 -0
- package/dist/redact-prose.js.map +1 -0
- package/dist/redact-prose.test.d.ts +2 -0
- package/dist/redact-prose.test.d.ts.map +1 -0
- package/dist/redact-prose.test.js +429 -0
- package/dist/redact-prose.test.js.map +1 -0
- package/dist/redaction.d.ts +23 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +23 -0
- package/dist/redaction.js.map +1 -0
- package/dist/sanitize-command.d.ts +21 -0
- package/dist/sanitize-command.d.ts.map +1 -0
- package/dist/sanitize-command.js +908 -0
- package/dist/sanitize-command.js.map +1 -0
- package/dist/sanitize-command.test.d.ts +2 -0
- package/dist/sanitize-command.test.d.ts.map +1 -0
- package/dist/sanitize-command.test.js +867 -0
- package/dist/sanitize-command.test.js.map +1 -0
- package/dist/steering-classification.d.ts +19 -0
- package/dist/steering-classification.d.ts.map +1 -0
- package/dist/steering-classification.js +55 -0
- package/dist/steering-classification.js.map +1 -0
- package/dist/strip-code-blocks.d.ts +68 -0
- package/dist/strip-code-blocks.d.ts.map +1 -0
- package/dist/strip-code-blocks.js +280 -0
- package/dist/strip-code-blocks.js.map +1 -0
- package/dist/strip-code-blocks.test.d.ts +2 -0
- package/dist/strip-code-blocks.test.d.ts.map +1 -0
- package/dist/strip-code-blocks.test.js +250 -0
- package/dist/strip-code-blocks.test.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { sanitizeCommand } from './sanitize-command.js';
|
|
3
|
+
describe('sanitizeCommand', () => {
|
|
4
|
+
it('redacts ordinary credential values passed as separate flag arguments', () => {
|
|
5
|
+
for (const flag of ['--password', '--token', '--api-key', '--client-secret']) {
|
|
6
|
+
for (const value of [
|
|
7
|
+
'fixture-credential',
|
|
8
|
+
'"fixture credential"',
|
|
9
|
+
"'fixture credential'",
|
|
10
|
+
]) {
|
|
11
|
+
const redacted = sanitizeCommand(`tool ${flag} ${value} --verbose`);
|
|
12
|
+
expect(redacted).not.toContain('fixture');
|
|
13
|
+
expect(redacted).toContain(`${flag} [redacted] --verbose`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
expect(sanitizeCommand('tool --output result.json')).toBe('tool --output result.json');
|
|
17
|
+
});
|
|
18
|
+
it('redacts Basic authorization payloads rather than just the scheme', () => {
|
|
19
|
+
const payload = Buffer.from('fixture-user:fixture-password').toString('base64');
|
|
20
|
+
expect(sanitizeCommand(`curl -H "Authorization: Basic ${payload}"`)).not.toContain(payload);
|
|
21
|
+
expect(sanitizeCommand(`curl -H 'Authorization: basic ${payload}'`)).not.toContain(payload);
|
|
22
|
+
});
|
|
23
|
+
it('redacts JSON credentials embedded in shell request bodies', () => {
|
|
24
|
+
const body = {
|
|
25
|
+
password: 'fixture-password',
|
|
26
|
+
apiKey: 'fixture-key',
|
|
27
|
+
client_secret: 'fixture-secret',
|
|
28
|
+
message: 'hello',
|
|
29
|
+
};
|
|
30
|
+
const command = `curl -d '${JSON.stringify(body)}'`;
|
|
31
|
+
const redacted = sanitizeCommand(command);
|
|
32
|
+
expect(redacted).not.toContain('fixture');
|
|
33
|
+
expect(redacted).toContain('"message":"hello"');
|
|
34
|
+
expect(sanitizeCommand(String.raw `curl -d '{"password":"fixture\\\"password"}'`)).not.toContain('fixture');
|
|
35
|
+
});
|
|
36
|
+
it('redacts shell-escaped JSON keys and every sensitive value shape', () => {
|
|
37
|
+
const body = {
|
|
38
|
+
password: 'fixture-password',
|
|
39
|
+
authorization: 'Basic fixture-credential',
|
|
40
|
+
tokens: ['fixture-token'],
|
|
41
|
+
nested: { api_keys: { first: 'fixture-key' } },
|
|
42
|
+
message: 'hello',
|
|
43
|
+
};
|
|
44
|
+
const json = JSON.stringify(body);
|
|
45
|
+
for (const argument of [`'${json}'`, JSON.stringify(json)]) {
|
|
46
|
+
const redacted = sanitizeCommand(`curl -d ${argument}`);
|
|
47
|
+
expect(redacted).not.toContain('fixture');
|
|
48
|
+
expect(redacted).toContain('hello');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
it('redacts JSON bodies with shell-only escapes and malformed JSON', () => {
|
|
52
|
+
for (const command of [
|
|
53
|
+
String.raw `curl -d "{\"password\":\"fixture\$password\"}"`,
|
|
54
|
+
String.raw `curl -d "{\"password\":\"fixture\!password\"}"`,
|
|
55
|
+
String.raw `curl -d "{\"password\":\"fixture\\!password\"}"`,
|
|
56
|
+
'curl -d "{\\"password\\":\\"fixture\\`password\\"}"',
|
|
57
|
+
String.raw `curl -d "{\"authorization\":\"Basic fixture\$credential\"}"`,
|
|
58
|
+
String.raw `curl -d '{"password":"fixture-password",}'`,
|
|
59
|
+
]) {
|
|
60
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
it('redacts token authorization schemes', () => {
|
|
64
|
+
const payload = ['fixture', 'credential'].join('-');
|
|
65
|
+
expect(sanitizeCommand(`curl -H "Authorization: token ${payload}"`)).not.toContain(payload);
|
|
66
|
+
});
|
|
67
|
+
it('redacts credentials in recursively JSON-encoded string values', () => {
|
|
68
|
+
let body = JSON.stringify({ password: 'fixture-password' });
|
|
69
|
+
for (let depth = 0; depth < 4; depth++) {
|
|
70
|
+
body = JSON.stringify({ config: body });
|
|
71
|
+
for (const argument of [`'${body}'`, JSON.stringify(body)]) {
|
|
72
|
+
expect(sanitizeCommand(`curl -d ${argument}`)).not.toContain('fixture');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
it('preserves quoted programs and brace expansion without credentials', () => {
|
|
77
|
+
for (const command of [
|
|
78
|
+
"awk '{print $1}' /var/log/syslog",
|
|
79
|
+
"jq '[.items[] | .name]' resp.json",
|
|
80
|
+
"echo '{a,b}'",
|
|
81
|
+
]) {
|
|
82
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
it('redacts complete shell words with concatenated quote sections', () => {
|
|
86
|
+
const body = JSON.stringify({
|
|
87
|
+
name: "O'Brien",
|
|
88
|
+
config: JSON.stringify({ password: 'fixture-password' }),
|
|
89
|
+
});
|
|
90
|
+
for (const escaped of ["'\\''", "'\"'\"'"]) {
|
|
91
|
+
const argument = `'${body.replaceAll("'", escaped)}'`;
|
|
92
|
+
expect(sanitizeCommand(`curl -d ${argument}`)).not.toContain('fixture');
|
|
93
|
+
const value = `'fixture${escaped}password'`;
|
|
94
|
+
expect(sanitizeCommand(`tool --password ${value}`)).toBe('tool --password [redacted]');
|
|
95
|
+
expect(sanitizeCommand(`PASSWORD=${value} tool`)).toBe('PASSWORD=[redacted] tool');
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
it('bounds scanning time for long malformed JSON-like arguments', () => {
|
|
99
|
+
const command = `jq '{${'x'.repeat(128_000)}: .y}' file.json`;
|
|
100
|
+
const start = performance.now();
|
|
101
|
+
expect(sanitizeCommand(command)).toContain('[... truncated,');
|
|
102
|
+
expect(performance.now() - start).toBeLessThan(2000);
|
|
103
|
+
});
|
|
104
|
+
it('preserves author and oauth metadata', () => {
|
|
105
|
+
for (const command of [
|
|
106
|
+
'git commit --author="Jane Doe <jane@example.com>"',
|
|
107
|
+
`curl -d '${JSON.stringify({ author: 'Jane Doe', oauth_provider: 'example' })}'`,
|
|
108
|
+
'oauth_provider=example tool',
|
|
109
|
+
]) {
|
|
110
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
it('consumes shell-escaped whitespace in flag and assignment credentials', () => {
|
|
114
|
+
for (const command of [
|
|
115
|
+
String.raw `tool --password fixture\ secret --verbose`,
|
|
116
|
+
String.raw `PASSWORD=fixture\ secret tool`,
|
|
117
|
+
]) {
|
|
118
|
+
const redacted = sanitizeCommand(command);
|
|
119
|
+
expect(redacted).not.toContain('fixture');
|
|
120
|
+
expect(redacted).not.toContain(' secret');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
it('leaves short benign commands untouched', () => {
|
|
124
|
+
expect(sanitizeCommand('bun test')).toBe('bun test');
|
|
125
|
+
expect(sanitizeCommand('git commit -m "fix: thing"')).toBe('git commit -m "fix: thing"');
|
|
126
|
+
});
|
|
127
|
+
it('redacts our own CLI token, including the space-separated flag form', () => {
|
|
128
|
+
// The documented setup command puts a live dashboard credential on the
|
|
129
|
+
// command line, where PostToolUse logs it and the next sync would upload
|
|
130
|
+
// it in an action summary. The `key=value` rule never sees a
|
|
131
|
+
// space-separated flag, so the token prefix has to be matched directly.
|
|
132
|
+
expect(sanitizeCommand('mossbear sync --token mossbear_tPtcsSvXyRxygMSkBjjDpBKz')).toBe('mossbear sync --token [redacted]');
|
|
133
|
+
expect(sanitizeCommand('mossbear init --token=mossbear_abcdefgh12345678')).toBe('mossbear init --token=[redacted]');
|
|
134
|
+
});
|
|
135
|
+
it('still redacts a token issued under the pre-rename prefix', () => {
|
|
136
|
+
// Those keys are deleted by migration 0056, but a CLI can be older or newer
|
|
137
|
+
// than the dashboard it syncs to; while one is still live, dropping the rule
|
|
138
|
+
// would upload it in plaintext.
|
|
139
|
+
expect(sanitizeCommand('mossbear sync --token auden_tPtcsSvXyRxygMSkBjjDpBKz')).toBe('mossbear sync --token [redacted]');
|
|
140
|
+
});
|
|
141
|
+
it('does not redact ordinary words that merely start with the prefix', () => {
|
|
142
|
+
expect(sanitizeCommand('cd mossbear_repo && ls')).toBe('cd mossbear_repo && ls');
|
|
143
|
+
expect(sanitizeCommand('cd auden_repo && ls')).toBe('cd auden_repo && ls');
|
|
144
|
+
});
|
|
145
|
+
it('redacts bearer tokens in Authorization headers', () => {
|
|
146
|
+
const out = sanitizeCommand('curl -H "Authorization: Bearer sk-abc123def456ghi789" https://api.example.com');
|
|
147
|
+
expect(out).not.toContain('sk-abc123def456ghi789');
|
|
148
|
+
expect(out).toContain('[redacted]');
|
|
149
|
+
expect(out).toContain('https://api.example.com');
|
|
150
|
+
});
|
|
151
|
+
it('redacts credentials in connection-string URLs', () => {
|
|
152
|
+
const out = sanitizeCommand('psql postgres://mossbear:s3cretpw@db.host:5432/mossbear');
|
|
153
|
+
expect(out).not.toContain('s3cretpw');
|
|
154
|
+
expect(out).toContain('postgres://mossbear:[redacted]@db.host:5432/mossbear');
|
|
155
|
+
});
|
|
156
|
+
it('redacts sensitive key=value assignments', () => {
|
|
157
|
+
const out = sanitizeCommand('OPENAI_API_KEY=sk-live-abcdef123456 bun run eval');
|
|
158
|
+
expect(out).not.toContain('abcdef123456');
|
|
159
|
+
expect(out).toContain('bun run eval');
|
|
160
|
+
});
|
|
161
|
+
it('redacts quoted secret values that contain spaces', () => {
|
|
162
|
+
const out = sanitizeCommand('PASSWORD="correct horse battery staple" deploy');
|
|
163
|
+
expect(out).not.toContain('correct horse battery staple');
|
|
164
|
+
expect(out).toContain('PASSWORD=[redacted]');
|
|
165
|
+
expect(out).toContain('deploy');
|
|
166
|
+
const single = sanitizeCommand("DB_PASSWORD='a b c d' run");
|
|
167
|
+
expect(single).not.toContain('a b c d');
|
|
168
|
+
expect(single).toContain('run');
|
|
169
|
+
});
|
|
170
|
+
it('strips heredoc bodies, keeping only the delimiters', () => {
|
|
171
|
+
const out = sanitizeCommand("cat > src/config.ts <<EOF\nexport const KEY = 'sk-verysecret12345'\nmore lines\nEOF");
|
|
172
|
+
expect(out).not.toContain('sk-verysecret12345');
|
|
173
|
+
expect(out).not.toContain('more lines');
|
|
174
|
+
expect(out).toContain('[redacted]');
|
|
175
|
+
expect(out).toContain('EOF');
|
|
176
|
+
});
|
|
177
|
+
it('redacts well-known token prefixes standing alone', () => {
|
|
178
|
+
for (const secret of [
|
|
179
|
+
'ghp_16C7e42F292c6912E7710c838347Ae178B4a', // oss-boundary-ignore — fake token shape
|
|
180
|
+
'github_pat_11ABCDEFG_abcdefghij', // oss-boundary-ignore — fake token shape
|
|
181
|
+
'xoxb-1234567890-abcdefghijk', // oss-boundary-ignore — fake token shape
|
|
182
|
+
'AKIAIOSFODNN7EXAMPLE', // oss-boundary-ignore — fake token shape
|
|
183
|
+
]) {
|
|
184
|
+
const out = sanitizeCommand(`deploy --with ${secret}`);
|
|
185
|
+
expect(out).not.toContain(secret);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
it('redacts private key blocks even without an END marker', () => {
|
|
189
|
+
const out = sanitizeCommand('cat > key.pem <<EOF\n-----BEGIN RSA PRIVATE KEY-----\nMIIEow\nEOF' // oss-boundary-ignore — synthetic fixture, not a real key
|
|
190
|
+
);
|
|
191
|
+
expect(out).not.toContain('MIIEow');
|
|
192
|
+
});
|
|
193
|
+
it('truncates long commands and notes omitted length', () => {
|
|
194
|
+
const long = `echo ${'x'.repeat(500)}`;
|
|
195
|
+
const out = sanitizeCommand(long);
|
|
196
|
+
expect(out.length).toBeLessThan(300);
|
|
197
|
+
expect(out).toContain('[... truncated,');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('command disclosure regressions', () => {
|
|
201
|
+
it('redacts cookie request headers', () => {
|
|
202
|
+
expect(sanitizeCommand(`curl -H 'Cookie: session=fixture-cookie; csrf=fixture-csrf' https://example.test`)).not.toContain('fixture-cookie');
|
|
203
|
+
expect(sanitizeCommand(`curl -b 'session=fixture-cookie' https://example.test`)).not.toContain('fixture-cookie');
|
|
204
|
+
});
|
|
205
|
+
it('redacts HTTP username/password arguments in short and long forms', () => {
|
|
206
|
+
for (const flag of ['-u ', '-u', '--user ', '--user=', '--proxy-user ']) {
|
|
207
|
+
expect(sanitizeCommand(`curl ${flag}'fixture-user:fixture-password' https://example.test`)).not.toContain('fixture-password');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
it('redacts credentials after shell line continuations', () => {
|
|
211
|
+
for (const input of [
|
|
212
|
+
'tool --password \\\n fixture-continued',
|
|
213
|
+
'tool --password \\\r\n fixture-continued',
|
|
214
|
+
])
|
|
215
|
+
expect(sanitizeCommand(input)).not.toContain('fixture-continued');
|
|
216
|
+
});
|
|
217
|
+
it('redacts token-only URL userinfo and URL query credentials', () => {
|
|
218
|
+
for (const input of [
|
|
219
|
+
'curl https://fixture-userinfo@example.test/',
|
|
220
|
+
'curl "https://example.test/?api_key=fixture-query&view=summary"',
|
|
221
|
+
]) {
|
|
222
|
+
const output = sanitizeCommand(input);
|
|
223
|
+
expect(output).not.toContain('fixture-userinfo');
|
|
224
|
+
expect(output).not.toContain('fixture-query');
|
|
225
|
+
expect(output).toContain('example.test');
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
it('strips here-string file contents while retaining command evidence', () => {
|
|
229
|
+
const output = sanitizeCommand(`cat <<< 'fixturePrivateSource second word' > output.txt`);
|
|
230
|
+
expect(output).not.toContain('fixturePrivateSource');
|
|
231
|
+
expect(output).toContain('cat <<<');
|
|
232
|
+
expect(output).toContain('output.txt');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
describe('review command-evidence regressions', () => {
|
|
236
|
+
it('preserves the host after a cookie header and still redacts query credentials', () => {
|
|
237
|
+
const text = sanitizeCommand("curl -H 'Cookie: session=fixture-cookie' https://example.test/?api_key=fixture-query");
|
|
238
|
+
expect(text).not.toContain('fixture-cookie');
|
|
239
|
+
expect(text).not.toContain('fixture-query');
|
|
240
|
+
expect(text).toContain('example.test');
|
|
241
|
+
});
|
|
242
|
+
it.each([
|
|
243
|
+
'sort -u fixtures/input.txt',
|
|
244
|
+
'git add -u .',
|
|
245
|
+
'grep -b pattern src/app.ts',
|
|
246
|
+
'ps -u root',
|
|
247
|
+
'docker run --user 1000:1000',
|
|
248
|
+
])('preserves non-HTTP flags: %s', (command) => {
|
|
249
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
250
|
+
});
|
|
251
|
+
it('redacts HTTP credentials after environment assignments and command separators', () => {
|
|
252
|
+
const credential = ['fixture-user', 'fixture-password'].join(':');
|
|
253
|
+
const text = sanitizeCommand(`SORT=1 sort -u input.txt && env MODE=test /usr/bin/curl -u '${credential}' https://example.test`);
|
|
254
|
+
expect(text).toContain('sort -u input.txt');
|
|
255
|
+
expect(text).not.toContain('fixture-password');
|
|
256
|
+
expect(text).toContain('example.test');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
describe('HTTPie authentication evidence', () => {
|
|
260
|
+
it.each(['-a ', '-a', '-a=', '--auth ', '--auth='])('redacts auth in the %s form', (flag) => {
|
|
261
|
+
const output = sanitizeCommand(`http ${flag}'fixture-user:fixture-password' example.test`);
|
|
262
|
+
expect(output).not.toContain('fixture-password');
|
|
263
|
+
expect(output).toContain('example.test');
|
|
264
|
+
});
|
|
265
|
+
it('preserves the auth type while redacting a bearer token on https', () => {
|
|
266
|
+
const output = sanitizeCommand('https -A bearer -a fixture-token example.test');
|
|
267
|
+
expect(output).not.toContain('fixture-token');
|
|
268
|
+
expect(output).toContain('-A bearer');
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
describe('credential syntax at the command upload boundary', () => {
|
|
272
|
+
it.each([
|
|
273
|
+
"bash -c 'API_TOKEN=x curl -x reader:fixture-value@proxy.test:3128 https://example.test'",
|
|
274
|
+
"bash -c 'password=x; https_proxy=reader:fixture-value@proxy.test:3128 curl https://example.test'",
|
|
275
|
+
"su -c 'API_TOKEN=x curl -x reader:fixture-value@proxy.test:3128 https://example.test'",
|
|
276
|
+
"env 'PASSWORD=fixture value' curl https://example.test",
|
|
277
|
+
"bash -c '{ curl -x reader:fixture-value@proxy.test:3128 https://example.test; }'",
|
|
278
|
+
"echo 'proxy = reader:fixture-value@proxy.test:3128' | curl -K - https://example.test",
|
|
279
|
+
"curl -K <(echo 'proxy = reader:fixture-value@proxy.test:3128') https://example.test",
|
|
280
|
+
"printf '%s\\n' 'proxy=reader:fixture-value@proxy.test:3128' | curl --config - https://example.test",
|
|
281
|
+
])('redacts assignment-led scripts and inline proxy configuration: %s', (command) => {
|
|
282
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
283
|
+
});
|
|
284
|
+
it.each([
|
|
285
|
+
'echo \'proxy-user = "reader:fixture-password"\' | curl -K - https://example.test',
|
|
286
|
+
'echo \'user = "reader:fixture-password"\' | curl -K - https://example.test',
|
|
287
|
+
'echo \'proxy-user = "reader:fixture-password"\' > cfg && curl -K cfg https://example.test',
|
|
288
|
+
'curl -K <(echo \'user = "reader:fixture-password"\') https://example.test',
|
|
289
|
+
"printf '%s\\n' 'user: reader:fixture-password' | curl -K - https://example.test",
|
|
290
|
+
'echo \'cookie = "SID=fixture-password"\' | curl -K - https://example.test',
|
|
291
|
+
'echo \'cert = "client.pem:fixture-password"\' | curl -K - https://example.test',
|
|
292
|
+
"echo 'proxy-cert = client.pem:fixture-password' | curl -K - https://example.test",
|
|
293
|
+
])('redacts credential fields in inline curl configuration: %s', (command) => {
|
|
294
|
+
const output = sanitizeCommand(command);
|
|
295
|
+
expect(output).not.toContain('fixture');
|
|
296
|
+
expect(output).toContain('[redacted]');
|
|
297
|
+
});
|
|
298
|
+
it.each([
|
|
299
|
+
'echo \'user = "reader"\' | curl -K - https://example.test',
|
|
300
|
+
'echo \'cert = "client.pem"\' | curl -K - https://example.test',
|
|
301
|
+
])('keeps curl configuration without a password: %s', (command) => {
|
|
302
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
303
|
+
});
|
|
304
|
+
it.each([
|
|
305
|
+
'sudo -E curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
306
|
+
'sudo -u deploy curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
307
|
+
'timeout --preserve-status 10 curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
308
|
+
'nice -n 10 curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
309
|
+
'stdbuf -oL curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
310
|
+
'xargs curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
311
|
+
'env -S "curl -x reader:fixture-password@proxy.test:3128 https://example.test"',
|
|
312
|
+
"su -c 'curl -x reader:fixture-password@proxy.test:3128 https://example.test'",
|
|
313
|
+
'sudo -s "curl -x reader:fixture-password@proxy.test:3128 https://example.test"',
|
|
314
|
+
String.raw `env -S "sh -c 'curl -x reader:fixture-password@proxy.test:3128 https://example.test'"`,
|
|
315
|
+
'bash -c "$(echo curl -x reader:fixture-password@proxy.test:3128 https://example.test)"',
|
|
316
|
+
"bash -c $'curl -x reader:fixture-password@proxy.test:3128 https://example.test'",
|
|
317
|
+
String.raw `bash -c $'true\ncurl -x reader:fixture-password@proxy.test:3128 https://example.test'`,
|
|
318
|
+
String.raw `bash -c $'\x63url -x reader:fixture-password@proxy.test:3128 https://example.test'`,
|
|
319
|
+
String.raw `bash -c $'echo \'ready\'; \x63url -x reader:fixture-password@proxy.test:3128 https://example.test'`,
|
|
320
|
+
])('redacts wrapper and encoded-script credentials: %s', (command) => {
|
|
321
|
+
const output = sanitizeCommand(command);
|
|
322
|
+
expect(output).not.toContain('fixture-password');
|
|
323
|
+
expect(output).toContain('proxy.test:3128');
|
|
324
|
+
expect(output).toContain('example.test');
|
|
325
|
+
});
|
|
326
|
+
it.each([
|
|
327
|
+
"sh -c 'export https_proxy=reader:fixture-password@proxy.test:3128; curl https://example.test'",
|
|
328
|
+
"sh -c 'cd /tmp; https_proxy=reader:fixture-password@proxy.test:3128 curl https://example.test'",
|
|
329
|
+
"eval 'export https_proxy=reader:fixture-password@proxy.test:3128' ; curl https://example.test",
|
|
330
|
+
"bash -c 'curl -x reader:fixture-password@proxy.test:3128 https://example.test'",
|
|
331
|
+
'bash -lc "curl -sSx reader:fixture-password@proxy.test:3128 https://example.test"',
|
|
332
|
+
'sudo bash -c "curl -x reader:fixture-password@proxy.test:3128 https://example.test"',
|
|
333
|
+
String.raw `bash -c 'sh -c "curl -x reader:fixture-password@proxy.test:3128 https://example.test"'`,
|
|
334
|
+
"bash -c 'https_proxy=reader:fixture-password@proxy.test:3128 curl -U reader:fixture-other https://example.test'",
|
|
335
|
+
'eval curl -x reader:fixture-password@proxy.test:3128 https://example.test',
|
|
336
|
+
])('redacts credentials inside quoted scripts: %s', (command) => {
|
|
337
|
+
const output = sanitizeCommand(command);
|
|
338
|
+
expect(output).not.toContain('fixture-password');
|
|
339
|
+
expect(output).not.toContain('fixture-other');
|
|
340
|
+
expect(output).toContain('proxy.test:3128');
|
|
341
|
+
expect(output).toContain('example.test');
|
|
342
|
+
});
|
|
343
|
+
it('bounds deeply nested interpreter scripts without retaining credentials', () => {
|
|
344
|
+
let command = 'curl -x reader:fixture-password@proxy.test:3128 https://example.test';
|
|
345
|
+
for (let depth = 0; depth < 12; depth += 1)
|
|
346
|
+
command = `bash -c ${JSON.stringify(command)}`;
|
|
347
|
+
const start = performance.now();
|
|
348
|
+
expect(sanitizeCommand(command)).not.toContain('fixture-password');
|
|
349
|
+
expect(performance.now() - start).toBeLessThan(2000);
|
|
350
|
+
});
|
|
351
|
+
it.each(['--pass ', '--pass=', '--proxy-pass ', '--proxy-pass='])('redacts separate private-key passphrases via %s', (flag) => {
|
|
352
|
+
expect(sanitizeCommand(`curl ${flag}'fixture-secret' https://example.test`)).not.toContain('fixture-secret');
|
|
353
|
+
});
|
|
354
|
+
it.each([
|
|
355
|
+
'curl reader:fixture-password@internal.test/api',
|
|
356
|
+
'curl -osx reader:fixture-password@internal.test/api',
|
|
357
|
+
'curl -- reader:fixture-password@internal.test/api',
|
|
358
|
+
"curl 'reader:fixture-password@internal.test/api'",
|
|
359
|
+
"bash -c 'curl reader:fixture-password@internal.test/api'",
|
|
360
|
+
])('redacts HTTP-client scheme-less URL credentials: %s', (command) => {
|
|
361
|
+
const output = sanitizeCommand(command);
|
|
362
|
+
expect(output).not.toContain('fixture-password');
|
|
363
|
+
expect(output).toContain('internal.test/api');
|
|
364
|
+
});
|
|
365
|
+
it('preserves benign quoted scripts and unrelated userinfo-shaped arguments', () => {
|
|
366
|
+
for (const command of [
|
|
367
|
+
"bash -c 'sort -u input.txt; curl -sS https://example.test'",
|
|
368
|
+
"docker pull 'image:tag@sha256:abc123'",
|
|
369
|
+
"git clone 'git@github.test:org/repo'",
|
|
370
|
+
])
|
|
371
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
372
|
+
});
|
|
373
|
+
it.each([
|
|
374
|
+
['x', 'reader:fixture-password@proxy.test:3128'],
|
|
375
|
+
['u', 'reader:fixture-password'],
|
|
376
|
+
['U', 'reader:fixture-password'],
|
|
377
|
+
['b', 'session=fixture-cookie'],
|
|
378
|
+
['E', 'client.pem:fixture-passphrase'],
|
|
379
|
+
['H', 'X-Api-Key: fixture-header'],
|
|
380
|
+
['d', '%70assword=fixture-form'],
|
|
381
|
+
['F', '%70assword=fixture-form'],
|
|
382
|
+
])('redacts curl clustered argument flag %s', (flag, value) => {
|
|
383
|
+
for (const prefix of ['s', 'sS', 'qkLf']) {
|
|
384
|
+
for (const space of ['', ' ']) {
|
|
385
|
+
const output = sanitizeCommand(`curl -${prefix}${flag}${space}'${value}' https://example.test`);
|
|
386
|
+
expect(output).not.toContain('fixture');
|
|
387
|
+
expect(output).toContain(`-${prefix}${flag}`);
|
|
388
|
+
expect(output).toContain('https://example.test');
|
|
389
|
+
if (flag === 'x')
|
|
390
|
+
expect(output).toContain('proxy.test:3128');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
it('does not interpret an argument-taking flag value as more curl flags', () => {
|
|
395
|
+
const command = "curl -osxreport.txt -sH'Accept: application/json' https://example.test";
|
|
396
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
397
|
+
const proxy = 'curl -sSxproxy.test:3128 https://example.test';
|
|
398
|
+
expect(sanitizeCommand(proxy)).toBe(proxy);
|
|
399
|
+
});
|
|
400
|
+
it.each(['-U ', '-U', '--proxy-user='])('redacts curl proxy credentials via %s', (flag) => {
|
|
401
|
+
expect(sanitizeCommand(`curl ${flag}'user:fixture proxy password' https://example.test`)).not.toContain('fixture');
|
|
402
|
+
});
|
|
403
|
+
it.each(['--oauth2-bearer ', '--oauth2-bearer='])('redacts curl OAuth bearer credentials via %s', (flag) => {
|
|
404
|
+
expect(sanitizeCommand(`curl ${flag}fixture-bearer https://example.test`)).not.toContain('fixture');
|
|
405
|
+
});
|
|
406
|
+
it.each(['-E ', '-E', '--cert ', '--cert=', '--proxy-cert='])('redacts certificate passphrases via %s', (flag) => {
|
|
407
|
+
expect(sanitizeCommand(`curl ${flag}'client.pem:fixture passphrase' https://example.test`)).not.toContain('fixture');
|
|
408
|
+
});
|
|
409
|
+
it.each(['-H ', '-H', '--header ', '--header=', '--proxy-header='])('redacts custom credential headers via %s', (flag) => {
|
|
410
|
+
expect(sanitizeCommand(`curl ${flag}'X-Api-Key: fixture-header-secret' https://example.test`)).not.toContain('fixture');
|
|
411
|
+
expect(sanitizeCommand(`curl ${flag}'Accept: application/json' https://example.test`)).toContain('Accept: application/json');
|
|
412
|
+
});
|
|
413
|
+
it.each(['-d ', '-d', '--data=', '--data-urlencode ', '-F ', '--form-string='])('redacts credential form bodies via %s', (flag) => {
|
|
414
|
+
expect(sanitizeCommand(`curl ${flag}'password=fixture%20password&name=reader' https://example.test`)).not.toContain('fixture');
|
|
415
|
+
expect(sanitizeCommand(`curl ${flag}'%70assword=fixture-password' https://example.test`)).not.toContain('fixture');
|
|
416
|
+
expect(sanitizeCommand(`curl ${flag}'view=summary' https://example.test`)).toContain('view=summary');
|
|
417
|
+
});
|
|
418
|
+
it.each([
|
|
419
|
+
"'--password'",
|
|
420
|
+
'"--password"',
|
|
421
|
+
String.raw `--pass\word`,
|
|
422
|
+
"'--password=fixture password'",
|
|
423
|
+
])('redacts shell-encoded credential flags %s', (flag) => {
|
|
424
|
+
const command = flag.includes('=')
|
|
425
|
+
? `tool ${flag}`
|
|
426
|
+
: `tool ${flag} 'fixture password' --verbose`;
|
|
427
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
428
|
+
});
|
|
429
|
+
it('redacts OAuth fragment credentials while retaining the address', () => {
|
|
430
|
+
const output = sanitizeCommand('curl "https://example.test/#access_token=fixture-fragment&view=summary"');
|
|
431
|
+
expect(output).not.toContain('fixture');
|
|
432
|
+
expect(output).toContain('https://example.test/');
|
|
433
|
+
expect(output).toContain('view=summary');
|
|
434
|
+
});
|
|
435
|
+
it.each(['-x ', '-x', '--proxy ', '--proxy=', '--proxy1.0 '])('redacts scheme-less proxy userinfo via %s', (flag) => {
|
|
436
|
+
const output = sanitizeCommand(`curl ${flag}'reader:fixture-password@proxy.test:3128' https://example.test`);
|
|
437
|
+
expect(output).not.toContain('fixture-password');
|
|
438
|
+
expect(output).toContain('proxy.test:3128');
|
|
439
|
+
expect(sanitizeCommand(`curl ${flag}proxy.test:3128 https://example.test`)).toContain('proxy.test:3128');
|
|
440
|
+
expect(sanitizeCommand(`curl ${flag}'http://reader:fixture-password@proxy.test:3128' https://example.test`)).not.toContain('fixture-password');
|
|
441
|
+
});
|
|
442
|
+
describe('proxy environment assignments', () => {
|
|
443
|
+
it.each([
|
|
444
|
+
"env 'https_proxy=reader:fixture-password@proxy.test:3128' curl https://example.test",
|
|
445
|
+
'env "ALL_PROXY=reader:fixture-password@proxy.test:3128" curl https://example.test',
|
|
446
|
+
String.raw `env https_pr\oxy=reader:fixture-password@proxy.test:3128 curl https://example.test`,
|
|
447
|
+
])('redacts shell-encoded complete proxy assignments: %s', (command) => {
|
|
448
|
+
const output = sanitizeCommand(command);
|
|
449
|
+
expect(output).not.toContain('fixture-password');
|
|
450
|
+
expect(output).toContain('proxy.test:3128');
|
|
451
|
+
});
|
|
452
|
+
const names = ['https_proxy', 'http_proxy', 'HTTPS_PROXY', 'ALL_PROXY', 'all_proxy'];
|
|
453
|
+
const values = [
|
|
454
|
+
'reader:fixture-password@proxy.test:3128',
|
|
455
|
+
"'reader:fixture-password@proxy.test:3128'",
|
|
456
|
+
'"reader:fixture-password@proxy.test:3128"',
|
|
457
|
+
"'reader:fixture password@proxy.test:3128'",
|
|
458
|
+
];
|
|
459
|
+
for (const name of names) {
|
|
460
|
+
it.each(values)(`redacts scheme-less userinfo in ${name}=%s`, (value) => {
|
|
461
|
+
for (const prefix of ['', 'export ', 'sudo -E ', 'env ']) {
|
|
462
|
+
const output = sanitizeCommand(`${prefix}${name}=${value} curl https://example.test`);
|
|
463
|
+
expect(output).not.toContain('fixture');
|
|
464
|
+
expect(output).toContain('reader:');
|
|
465
|
+
expect(output).toContain('proxy.test:3128');
|
|
466
|
+
expect(output).toContain('curl https://example.test');
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
it('keeps a proxy value without credentials intact', () => {
|
|
471
|
+
for (const command of [
|
|
472
|
+
'https_proxy=proxy.test:3128 curl https://example.test',
|
|
473
|
+
"ALL_PROXY='socks5://proxy.test:1080' curl https://example.test",
|
|
474
|
+
'no_proxy=localhost,127.0.0.1 curl https://example.test',
|
|
475
|
+
])
|
|
476
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
477
|
+
});
|
|
478
|
+
it('leaves unrelated scheme-less @ text alone', () => {
|
|
479
|
+
for (const command of [
|
|
480
|
+
'docker pull nginx:1.25@sha256:0123456789abcdef',
|
|
481
|
+
'git clone git@github.com:org/repo.git',
|
|
482
|
+
])
|
|
483
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
describe('OpenSSL and sshpass credential arguments', () => {
|
|
487
|
+
it.each([
|
|
488
|
+
'openssl enc -d -aes-256-cbc -pass pass:fixture-password -in f.enc',
|
|
489
|
+
"openssl rsa -passin 'pass:fixture password' -in key.pem",
|
|
490
|
+
'openssl pkcs12 -passout pass:fixture-password -out key.p12',
|
|
491
|
+
'openssl enc -aes-256-cbc -k fixture-password',
|
|
492
|
+
'openssl enc -aes-256-cbc -pass=pass:fixture-password',
|
|
493
|
+
'openssl enc -aes-256-cbc --pass pass:fixture-password',
|
|
494
|
+
'openssl enc -aes-256-cbc --pass=pass:fixture-password',
|
|
495
|
+
'openssl rsa --passin pass:fixture-password -in key.pem',
|
|
496
|
+
'openssl pkcs12 --passout pass:fixture-password -out key.p12',
|
|
497
|
+
'openssl enc -aes-256-cbc --k fixture-password',
|
|
498
|
+
'openssl pkcs12 -password pass:fixture-password',
|
|
499
|
+
"env 'SSHPASS=fixture password' sshpass -e ssh deploy@host",
|
|
500
|
+
"sudo -u root /usr/bin/openssl enc '-pass' pass:fixture-password",
|
|
501
|
+
"bash -c 'openssl enc -pass pass:fixture-password'",
|
|
502
|
+
'sshpass -p fixture-password ssh deploy@host git pull',
|
|
503
|
+
'sshpass -pfixture-password ssh deploy@host git pull',
|
|
504
|
+
"sshpass -vp'fixture password' ssh deploy@host",
|
|
505
|
+
'timeout 10 /usr/bin/sshpass -4vpfixture-password ssh deploy@host',
|
|
506
|
+
String.raw `sshpass -\p $'fixture\x20password' ssh deploy@host`,
|
|
507
|
+
"openssl passwd -1 'fixture-password'",
|
|
508
|
+
'openssl passwd -6 fixture-password',
|
|
509
|
+
'openssl passwd -salt pepper fixture-password',
|
|
510
|
+
'sudo openssl passwd -apr1 -salt x fixture-password fixture-second',
|
|
511
|
+
'openssl enc -aes-256-cbc -K 00fixture1122 -iv 00 -in f -out g',
|
|
512
|
+
'openssl enc -aes-256-cbc -K=00fixture1122 -in f -out g',
|
|
513
|
+
])('redacts direct passwords: %s', (command) => {
|
|
514
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
515
|
+
expect(sanitizeCommand(command)).toContain('[redacted]');
|
|
516
|
+
});
|
|
517
|
+
it.each([
|
|
518
|
+
'openssl enc -pass env:KEY_PASSWORD -in f.enc',
|
|
519
|
+
'openssl enc --pass env:KEY_PASSWORD -in f.enc',
|
|
520
|
+
'openssl rsa -passin file:password-source -in key.pem',
|
|
521
|
+
'openssl pkcs12 -passout fd:3',
|
|
522
|
+
'openssl enc -pass stdin',
|
|
523
|
+
'openssl enc -kfile password-source -in f.enc',
|
|
524
|
+
'sshpass -e ssh deploy@host',
|
|
525
|
+
'sshpass -f password-source ssh deploy@host',
|
|
526
|
+
'sshpass -d 3 ssh deploy@host',
|
|
527
|
+
'printf -p fixture-public',
|
|
528
|
+
'tool -pass pass:fixture-public',
|
|
529
|
+
'openssl passwd -stdin',
|
|
530
|
+
'openssl passwd -in password-source',
|
|
531
|
+
'openssl x509 -in cert.pem -noout -text',
|
|
532
|
+
])('preserves nonliteral sources and unrelated arguments: %s', (command) => {
|
|
533
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
describe('redis-cli and mysql password arguments', () => {
|
|
537
|
+
it.each([
|
|
538
|
+
'redis-cli -a fixture-password keys "*"',
|
|
539
|
+
'redis-cli -h cache -a fixture-password get k',
|
|
540
|
+
'redis-cli --pass fixture-password get k',
|
|
541
|
+
'redis-cli --pass=fixture-password get k',
|
|
542
|
+
'redis-cli -a "fixture password" get k',
|
|
543
|
+
'mysql -u root -pfixture-password -h db',
|
|
544
|
+
"mysql -u root -p'fixture password' db",
|
|
545
|
+
'mysql -pfixture-password db',
|
|
546
|
+
'mysql --password=fixture-password db',
|
|
547
|
+
'redis-cli --cluster import 127.0.0.1:7000 --cluster-from 127.0.0.1:6379 --cluster-from-pass fixture-password',
|
|
548
|
+
'redis-cli --cluster-from-pass=fixture-password --cluster info 127.0.0.1:6379',
|
|
549
|
+
'REDISCLI_AUTH=fixture-password redis-cli get k',
|
|
550
|
+
'MYSQL_PWD=fixture-password mysql -u root db',
|
|
551
|
+
'env MYSQL_PWD=fixture-password mysql db',
|
|
552
|
+
'redis-cli -u redis://:fixture-password@cache:6379 get k',
|
|
553
|
+
])('redacts a password argument: %s', (command) => {
|
|
554
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
555
|
+
expect(sanitizeCommand(command)).toContain('[redacted]');
|
|
556
|
+
});
|
|
557
|
+
it.each([
|
|
558
|
+
'redis-cli -h cache -p 6379 get k',
|
|
559
|
+
'redis-cli --user reader keys "*"',
|
|
560
|
+
'redis-cli SET artifact image:tag@sha256:digest',
|
|
561
|
+
'redis-cli SET contact user@example.com',
|
|
562
|
+
'mysql -u root -P 3306 db',
|
|
563
|
+
'mysql -u root -p -h db',
|
|
564
|
+
'mysql -p mydb',
|
|
565
|
+
'mysql -h db -p mydb',
|
|
566
|
+
'mysql --protocol tcp db',
|
|
567
|
+
])('preserves unrelated arguments: %s', (command) => {
|
|
568
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
describe('valkey-cli and MySQL-family password arguments', () => {
|
|
572
|
+
it.each([
|
|
573
|
+
'valkey-cli -a fixture-password get k',
|
|
574
|
+
'valkey-cli -h cache -a fixture-password get k',
|
|
575
|
+
'valkey-cli -a "fixture password" get k',
|
|
576
|
+
'valkey-cli --pass fixture-password get k',
|
|
577
|
+
'valkey-cli --pass=fixture-password get k',
|
|
578
|
+
'valkey-cli --cluster-from-pass=fixture-password --cluster info 127.0.0.1:6379',
|
|
579
|
+
'valkey-cli -u valkey://:fixture-password@cache:6379 get k',
|
|
580
|
+
'valkey-cli -u valkeys://:fixture-password@cache:6379 get k',
|
|
581
|
+
'redis-cli -u rediss://:fixture-password@cache:6379 get k',
|
|
582
|
+
'VALKEYCLI_AUTH=fixture-password valkey-cli get k',
|
|
583
|
+
'env VALKEYCLI_AUTH=fixture-password valkey-cli get k',
|
|
584
|
+
'mariadb -u root -pfixture-password -h db',
|
|
585
|
+
"mariadb -u root -p'fixture password' db",
|
|
586
|
+
'mariadb -pfixture-password db',
|
|
587
|
+
'mariadb --password=fixture-password db',
|
|
588
|
+
'mysqldump -u root -pfixture-password db',
|
|
589
|
+
"mysqldump -u root -p'fixture password' db",
|
|
590
|
+
'mysqldump --password=fixture-password db',
|
|
591
|
+
'mysqladmin -u root -pfixture-password ping',
|
|
592
|
+
'mysqladmin --password=fixture-password ping',
|
|
593
|
+
'mariadb-dump -u root -pfixture-password db',
|
|
594
|
+
'mariadb-dump --password=fixture-password db',
|
|
595
|
+
'mariadb-admin -u root -pfixture-password ping',
|
|
596
|
+
'mariadb-admin --password=fixture-password ping',
|
|
597
|
+
'valkey-cli.exe -a fixture-password get k',
|
|
598
|
+
'mariadb.exe -pfixture-password db',
|
|
599
|
+
'mysqldump.EXE -pfixture-password db',
|
|
600
|
+
'MYSQL.EXE -pfixture-password db',
|
|
601
|
+
'VALKEY-CLI.EXE -a fixture-password get k',
|
|
602
|
+
'MYSQL_PWD=fixture-password mariadb -u root db',
|
|
603
|
+
'env MYSQL_PWD=fixture-password mysqldump db',
|
|
604
|
+
])('redacts a password argument: %s', (command) => {
|
|
605
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
606
|
+
expect(sanitizeCommand(command)).toContain('[redacted]');
|
|
607
|
+
});
|
|
608
|
+
// A bare --password prompts in MySQL-family clients, so the next word is an
|
|
609
|
+
// operand. The tool-agnostic `--credential value` rule cannot tell that apart,
|
|
610
|
+
// so it redacts the operand: over-redaction, never a leak.
|
|
611
|
+
it.each([
|
|
612
|
+
'mysql --password appdb',
|
|
613
|
+
'mariadb --password appdb db',
|
|
614
|
+
'mysqldump --password appdb',
|
|
615
|
+
'mysqladmin --password ping',
|
|
616
|
+
'mariadb-dump --password appdb',
|
|
617
|
+
])('redacts, rather than leaks, the operand after a bare --password: %s', (command) => {
|
|
618
|
+
expect(sanitizeCommand(command)).not.toContain('appdb');
|
|
619
|
+
expect(sanitizeCommand(command)).not.toContain('ping');
|
|
620
|
+
});
|
|
621
|
+
it.each([
|
|
622
|
+
'valkey-cli -h cache -p 6379 get k',
|
|
623
|
+
'valkey-cli --user reader keys "*"',
|
|
624
|
+
'valkey-cli GET valkey://cache:6379',
|
|
625
|
+
'valkey-cli SET artifact image:tag@sha256:digest',
|
|
626
|
+
'valkey-cli SET contact user@example.com',
|
|
627
|
+
'mariadb -u root -P 3306 db',
|
|
628
|
+
'mariadb -u root -p -h db',
|
|
629
|
+
'mariadb -p mydb',
|
|
630
|
+
'mysqldump -p mydb',
|
|
631
|
+
'mysqldump --databases mydb',
|
|
632
|
+
'mysqladmin -u root -p ping',
|
|
633
|
+
// Unrelated tools keep their -p flags, including names that only start
|
|
634
|
+
// with a family member's name.
|
|
635
|
+
'tool -pfixture-public db',
|
|
636
|
+
'tool.exe -pfixture-public db',
|
|
637
|
+
'mysql_check -pfixture-public db',
|
|
638
|
+
'mariadb-dump-helper -pfixture-public db',
|
|
639
|
+
])('preserves unrelated arguments: %s', (command) => {
|
|
640
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
describe('inline curl configuration credentials', () => {
|
|
644
|
+
it.each([
|
|
645
|
+
`echo 'us''er = "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
646
|
+
String.raw `echo us\er=reader:fixture-password | curl -K - file:///dev/null`,
|
|
647
|
+
String.raw `echo $'u\x73er = "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
648
|
+
])('decodes shell-encoded config names: %s', (command) => {
|
|
649
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
650
|
+
});
|
|
651
|
+
it.each([
|
|
652
|
+
`printf 'user = "reader:%s"\\n' fixture-password | curl -K - file:///dev/null`,
|
|
653
|
+
`printf 'cookie = "%s"\\n' SID=fixture-password | curl -K - file:///dev/null`,
|
|
654
|
+
`printf '%s = %s\\n' user reader:fixture-password | curl -K - file:///dev/null`,
|
|
655
|
+
])('redacts printf credential substitutions: %s', (command) => {
|
|
656
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
657
|
+
});
|
|
658
|
+
it.each([
|
|
659
|
+
`bash -c 'echo "user = reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
660
|
+
`env -S 'printf "user = reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
661
|
+
])('redacts wrapped config producers: %s', (command) => {
|
|
662
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
663
|
+
});
|
|
664
|
+
it.each([
|
|
665
|
+
`echo 'user "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
666
|
+
`echo 'user: "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
667
|
+
`echo '--user = "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
668
|
+
`echo '-u "reader:fixture-password"' | curl -K - file:///dev/null`,
|
|
669
|
+
])('redacts alternate curl config separators: %s', (command) => {
|
|
670
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
671
|
+
});
|
|
672
|
+
it.each([
|
|
673
|
+
`echo 'proxy-user = "reader:fixture-password"' | curl -K - https://example.test`,
|
|
674
|
+
`echo 'user = "reader:fixture password"' | curl -K - https://example.test`,
|
|
675
|
+
`echo 'proxy-user="reader:fixture-password"' > cfg && curl -K cfg https://example.test`,
|
|
676
|
+
`curl -K <(echo 'user = "reader:fixture-password"') https://example.test`,
|
|
677
|
+
`echo 'cookie = "SID=fixture-password"' | curl -K - https://example.test`,
|
|
678
|
+
`echo 'cert = "client.pem:fixture-password"' | curl -K - https://example.test`,
|
|
679
|
+
`echo 'proxy-cert = "client.pem:fixture-password"' | curl -K - https://example.test`,
|
|
680
|
+
`printf '%s\\n' 'oauth2-bearer = "fixture-password"' | curl -K - https://example.test`,
|
|
681
|
+
`printf '%s\\n' 'pass = "fixture-password"' | curl -K - https://example.test`,
|
|
682
|
+
`echo 'proxy-pass = "fixture-password"' | curl -K - https://example.test`,
|
|
683
|
+
])('redacts credential field literals: %s', (command) => {
|
|
684
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
685
|
+
});
|
|
686
|
+
it('keeps unrelated flags before a config producer', () => {
|
|
687
|
+
expect(sanitizeCommand(`sort -u public-file && echo 'user = reader:fixture-password' | curl -K - file:///dev/null`)).toContain('sort -u public-file');
|
|
688
|
+
});
|
|
689
|
+
it('retains noncredential curl configuration', () => {
|
|
690
|
+
const command = `echo 'proxy = proxy.test:3128' | curl -K - https://example.test`;
|
|
691
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
describe('OpenSSL positional passwords and raw keys', () => {
|
|
695
|
+
it('redacts a valid raw AES key while retaining the IV', () => {
|
|
696
|
+
const key = '00112233445566778899aabbccddeeff';
|
|
697
|
+
const iv = 'ffeeddccbbaa99887766554433221100';
|
|
698
|
+
const output = sanitizeCommand(`openssl enc -aes-128-cbc -K ${key} -iv ${iv} -in public-file`);
|
|
699
|
+
expect(output).not.toContain(key);
|
|
700
|
+
expect(output).toContain(iv);
|
|
701
|
+
});
|
|
702
|
+
it.each(['curl', 'openssl', 'sshpass'])('redacts a password named %s', (password) => {
|
|
703
|
+
expect(sanitizeCommand(`openssl passwd -1 '${password}'`)).toBe('openssl passwd -1 [redacted]');
|
|
704
|
+
});
|
|
705
|
+
it.each([
|
|
706
|
+
`openssl passwd -1 'fixture-password'`,
|
|
707
|
+
`openssl passwd -6 'fixture password'`,
|
|
708
|
+
`openssl passwd -salt public-salt fixture-password`,
|
|
709
|
+
`openssl passwd -5 fixture-password fixture-other-password`,
|
|
710
|
+
`openssl passwd -- fixture-password`,
|
|
711
|
+
`openssl enc -aes-256-cbc -K fixture-key -in public-file`,
|
|
712
|
+
`openssl enc -aes-256-cbc --K=fixture-key -in public-file`,
|
|
713
|
+
])('redacts key material: %s', (command) => {
|
|
714
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
715
|
+
});
|
|
716
|
+
it.each([
|
|
717
|
+
'openssl passwd -1 -salt public-salt -in password-source',
|
|
718
|
+
'openssl passwd -6 -stdin',
|
|
719
|
+
'openssl passwd -rand entropy-source -writerand entropy-out -stdin',
|
|
720
|
+
])('preserves password sources and salt: %s', (command) => {
|
|
721
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
describe('Codex command-redaction findings', () => {
|
|
725
|
+
it.each([
|
|
726
|
+
`printf 'variable = cred=reader:fixture-password\nexpand-user = {{cred}}\nurl = file:///dev/null\n' | curl -K -`,
|
|
727
|
+
`echo 'variable = cred=fixture-password\nexpand-oauth2-bearer = {{cred}}' | curl -K -`,
|
|
728
|
+
`printf '%s\\n' 'variable = cred=fixture-password' 'expand-header = "X-Api-Key: {{cred}}"' | curl -K -`,
|
|
729
|
+
`export CRED=fixture-password; printf 'variable = %%CRED\nexpand-user = {{CRED}}' | curl -K -`,
|
|
730
|
+
])('redacts expanded credentials in curl config inputs: %s', (command) => {
|
|
731
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
732
|
+
});
|
|
733
|
+
it.each([
|
|
734
|
+
"env 'P.W=fixture-password' openssl enc -pass env:P.W",
|
|
735
|
+
"env 'P-W=fixture-password' openssl enc -pass env:P-W",
|
|
736
|
+
"env 'P W=fixture-password' openssl enc -pass 'env:P W'",
|
|
737
|
+
])('redacts non-shell environment names: %s', (command) => {
|
|
738
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
739
|
+
});
|
|
740
|
+
it.each([
|
|
741
|
+
'openssl s_client -srppass fixture-password -connect example.test:443',
|
|
742
|
+
'openssl s_client -srppass=fixture-password -connect example.test:443',
|
|
743
|
+
'openssl cms -pwri_password fixture-password -encrypt -in /dev/null',
|
|
744
|
+
'openssl dgst -hmac fixture-password /dev/null',
|
|
745
|
+
])('redacts other OpenSSL password/key flags: %s', (command) => {
|
|
746
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
747
|
+
});
|
|
748
|
+
it('redacts CMS key bytes while keeping their public identifier', () => {
|
|
749
|
+
const key = '0'.repeat(32);
|
|
750
|
+
expect(sanitizeCommand(`openssl cms -secretkey ${key} -secretkeyid public-id`)).toBe('openssl cms -secretkey [redacted] -secretkeyid public-id');
|
|
751
|
+
});
|
|
752
|
+
it.each([
|
|
753
|
+
'curl -u reader:$(printf fixture-password) https://example.test',
|
|
754
|
+
'sshpass -p$(printf fixture-password) ssh deploy@host',
|
|
755
|
+
'openssl enc -pass pass:$(printf fixture-password)',
|
|
756
|
+
'curl -u reader:`printf fixture-password` https://example.test',
|
|
757
|
+
'sshpass -p`printf fixture-password` ssh deploy@host',
|
|
758
|
+
'curl -u reader:$(printf "%s" "$(printf fixture-password)") https://example.test',
|
|
759
|
+
"curl -u reader:$(printf ')%s' fixture-password) https://example.test",
|
|
760
|
+
'curl -u "reader:$(printf fixture-password)" https://example.test',
|
|
761
|
+
"bash -c 'curl -u reader:$(printf fixture-password) https://example.test'",
|
|
762
|
+
'curl -u reader:$(printf fixture-password',
|
|
763
|
+
])('redacts command-substitution bodies in credential words: %s', (command) => {
|
|
764
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
765
|
+
});
|
|
766
|
+
it.each([
|
|
767
|
+
'openssl rand -out passwd 16',
|
|
768
|
+
'openssl x509 -in passwd -noout -text',
|
|
769
|
+
'openssl cms -secretkeyid public-key-id -help',
|
|
770
|
+
`echo 'variable = host=example.test\nexpand-url = https://{{host}}' | curl -K -`,
|
|
771
|
+
])('preserves subcommand arguments and public config expansions: %s', (command) => {
|
|
772
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
773
|
+
});
|
|
774
|
+
it.each([
|
|
775
|
+
'curl --variable cred=reader:fixture-password --expand-user {{cred}} file:///dev/null',
|
|
776
|
+
'curl --variable=cred=reader:fixture-password --expand-proxy-user={{cred}} https://example.test',
|
|
777
|
+
"curl --variable 'cred=fixture password' --expand-oauth2-bearer '{{cred:trim}}' https://example.test",
|
|
778
|
+
'curl --variable cred=fixture-password --expand-header "X-Api-Key: {{cred}}" https://example.test',
|
|
779
|
+
'curl --variable cred=fixture-password --expand-data "password={{cred}}" https://example.test',
|
|
780
|
+
'curl --variable cred=fixture-password --expand-url "https://reader:{{cred}}@example.test"',
|
|
781
|
+
'curl --variable cred=fixture-password --expand-url "https://example.test/#access_token={{cred}}"',
|
|
782
|
+
'curl --variable cred=fixture-password --expand-variable login=reader:{{cred}} --expand-user {{login}} file:///dev/null',
|
|
783
|
+
'curl --variable raw=reader:fixture-password@example.test --expand-variable alias={{raw}} --expand-url {{alias}}',
|
|
784
|
+
'curl --variable raw=reader:fixture-password@example.test --expand-url {{raw}} --variable raw=example.test',
|
|
785
|
+
'CRED=fixture-password curl --variable %CRED --expand-oauth2-bearer {{CRED}} https://example.test',
|
|
786
|
+
'curl --variable %CRED=fixture-password --expand-oauth2-bearer {{CRED}} https://example.test',
|
|
787
|
+
'curl --variable cred[0-99]=fixture-password --expand-oauth2-bearer {{cred}} https://example.test',
|
|
788
|
+
])('redacts expanded curl credentials at their source: %s', (command) => {
|
|
789
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
790
|
+
});
|
|
791
|
+
it.each([
|
|
792
|
+
'P=fixture-password openssl enc -aes-256-cbc -pass env:P -in /dev/null -out /dev/null',
|
|
793
|
+
"env 'P=fixture password' openssl enc -pass env:P",
|
|
794
|
+
'P+=fixture-password openssl enc -pass env:P',
|
|
795
|
+
'printf fixture-password | openssl enc -pass stdin',
|
|
796
|
+
'printf "%s\\n" fixture-password | openssl enc -pass stdin',
|
|
797
|
+
"printf fixture-password | sh -c 'openssl enc -pass stdin'",
|
|
798
|
+
'printf fixture-password > pass-source; openssl enc -pass file:pass-source',
|
|
799
|
+
'openssl enc -pass fd:3 3< <(printf fixture-password)',
|
|
800
|
+
'openssl enc -pass file:<(printf fixture-password)',
|
|
801
|
+
'openssl enc -pass stdin <<< fixture-password',
|
|
802
|
+
'printf fixture-password > pass-source; openssl enc -kfile pass-source',
|
|
803
|
+
'printf fixture-password | openssl passwd -stdin',
|
|
804
|
+
'printf fixture-password > pass-source; openssl passwd -in pass-source',
|
|
805
|
+
'P=$(printf fixture-password) openssl enc -pass env:P',
|
|
806
|
+
])('redacts literal OpenSSL passphrase producers: %s', (command) => {
|
|
807
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
808
|
+
});
|
|
809
|
+
it.each([
|
|
810
|
+
'HTTPS_PROXY+=reader:fixture-password@proxy.test:3128 curl https://example.test',
|
|
811
|
+
"ALL_PROXY+='reader:fixture password@proxy.test:3128' curl https://example.test",
|
|
812
|
+
])('redacts appended proxy assignments: %s', (command) => {
|
|
813
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
814
|
+
expect(sanitizeCommand(command)).toContain('proxy.test:3128');
|
|
815
|
+
});
|
|
816
|
+
it.each([
|
|
817
|
+
'openssl passwd -provider default -stdin',
|
|
818
|
+
'openssl passwd -provider-path /public/providers -provider default -stdin',
|
|
819
|
+
'openssl passwd -propquery provider=default -stdin',
|
|
820
|
+
'openssl passwd -provparam public=value -stdin',
|
|
821
|
+
'echo "user = reader:public-value"; ssh -K host; curl https://example.test',
|
|
822
|
+
'curl https://example.test; ssh -K host; echo "user = reader:public-value"',
|
|
823
|
+
'echo "user = reader:public-value"; curl -o -K https://example.test',
|
|
824
|
+
'curl --variable host=example.test --expand-url "https://{{host}}/public"',
|
|
825
|
+
'curl --variable name=public --expand-output {{name}} https://example.test',
|
|
826
|
+
'echo public-value; openssl enc -pass stdin',
|
|
827
|
+
'echo public-value; openssl enc -pass env:P',
|
|
828
|
+
])('preserves public option values and unrelated commands: %s', (command) => {
|
|
829
|
+
expect(sanitizeCommand(command)).toBe(command);
|
|
830
|
+
});
|
|
831
|
+
it.each([
|
|
832
|
+
'TOOL=/usr/bin/openssl curl -u reader:fixture-password https://example.test',
|
|
833
|
+
'sudo -u openssl curl -u reader:fixture-password https://example.test',
|
|
834
|
+
'env -u openssl curl -u reader:fixture-password https://example.test',
|
|
835
|
+
'/usr/bin/time -o openssl curl -u reader:fixture-password https://example.test',
|
|
836
|
+
'xargs -a openssl curl -u reader:fixture-password https://example.test',
|
|
837
|
+
])('does not select a client from wrapper option values: %s', (command) => {
|
|
838
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
839
|
+
});
|
|
840
|
+
it.each([
|
|
841
|
+
'curl https://example.test/openssl -u reader:fixture-password',
|
|
842
|
+
'curl -o openssl -u reader:fixture-password https://example.test',
|
|
843
|
+
'http example.test/sshpass --auth reader:fixture-password',
|
|
844
|
+
'curl --output sshpass --proxy-user reader:fixture-password https://example.test',
|
|
845
|
+
'sshpass -p fixture-first curl -u reader:fixture-password https://example.test',
|
|
846
|
+
])('keeps client identity across argument names: %s', (command) => {
|
|
847
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
848
|
+
});
|
|
849
|
+
it.each(['url', 'preproxy', 'socks4', 'socks4a', 'socks5', 'socks5-hostname'])('redacts userinfo in the %s config field and flag', (key) => {
|
|
850
|
+
for (const command of [
|
|
851
|
+
`echo '${key} = reader:fixture-password@proxy.test:3128' | curl -K -`,
|
|
852
|
+
`curl --${key} reader:fixture-password@proxy.test:3128 https://example.test`,
|
|
853
|
+
]) {
|
|
854
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
855
|
+
expect(sanitizeCommand(command)).toContain('proxy.test:3128');
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
it.each([
|
|
859
|
+
`echo 'user = reader:fixture-password' | bash -c 'curl -K - https://example.test'`,
|
|
860
|
+
`printf 'cookie = "%s"' SID=fixture-password | env -S 'curl -K - https://example.test'`,
|
|
861
|
+
`echo 'user = reader:fixture-password' | sh -c "bash -c 'curl -K - https://example.test'"`,
|
|
862
|
+
])('detects nested config consumers from outer producers: %s', (command) => {
|
|
863
|
+
expect(sanitizeCommand(command)).not.toContain('fixture');
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
//# sourceMappingURL=sanitize-command.test.js.map
|