test(fleet): harden documentation command scanner
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-07-16 13:09:44 -05:00
parent 587ac423c2
commit 30fad7a2b3

View File

@@ -135,12 +135,64 @@ function commandBasename(token: string): string {
return token.slice(token.lastIndexOf('/') + 1); return token.slice(token.lastIndexOf('/') + 1);
} }
function shellCommandSegments(source: string): string[] {
const segments: string[] = [];
let segment = '';
let quote = '';
let escaped = false;
const flush = (): void => {
if (segment.trim() !== '') segments.push(segment);
segment = '';
};
for (const character of source) {
if (escaped) {
segment += character;
escaped = false;
continue;
}
if (character === '\\' && quote !== "'") {
segment += character;
escaped = true;
continue;
}
if (quote !== '') {
segment += character;
if (character === quote) quote = '';
continue;
}
if (character === "'" || character === '"') {
segment += character;
quote = character;
continue;
}
if (character === '\n' || character === ';' || character === '&' || character === '|') {
flush();
continue;
}
segment += character;
}
flush();
return segments;
}
function shellCommandTokens(source: string): string[] { function shellCommandTokens(source: string): string[] {
const tokens: string[] = []; const tokens: string[] = [];
let token = ''; let token = '';
let quote = ''; let quote = '';
let escaped = false;
for (const character of source) { for (const character of source) {
if (escaped) {
token += character;
escaped = false;
continue;
}
if (character === '\\' && quote !== "'") {
escaped = true;
continue;
}
if (quote !== '') { if (quote !== '') {
if (character === quote) quote = ''; if (character === quote) quote = '';
else token += character; else token += character;
@@ -159,6 +211,7 @@ function shellCommandTokens(source: string): string[] {
} }
token += character; token += character;
} }
if (escaped) token += '\\';
if (token !== '') tokens.push(token); if (token !== '') tokens.push(token);
return tokens; return tokens;
} }
@@ -177,22 +230,47 @@ function normalizedCommandTokens(segment: string): string[] {
index += 1; index += 1;
continue; continue;
} }
if (/^(?:-u|--unset|-C|--chdir)$/.test(token)) { if (token === '--') {
index += 1;
break;
}
if (/^(?:--unset|--chdir)$/.test(token)) {
index += 2; index += 2;
continue; continue;
} }
if (/^(?:-u|--unset=|-C|--chdir=)/.test(token)) { if (/^(?:--unset=|--chdir=)/.test(token)) {
index += 1; index += 1;
continue; continue;
} }
if (/^(?:-S|--split-string)$/.test(token)) { if (token === '--split-string') {
const splitTokens = shellCommandTokens(tokens[index + 1] ?? ''); const splitTokens = shellCommandTokens(tokens[index + 1] ?? '');
tokens.splice(index, 2, ...splitTokens); tokens.splice(index, 2, ...splitTokens);
continue; continue;
} }
const splitString = token.match(/^(?:-S|--split-string=)(.*)$/)?.[1]; const longSplitString = token.match(/^--split-string=(.*)$/)?.[1];
if (splitString !== undefined) { if (longSplitString !== undefined) {
tokens.splice(index, 1, ...shellCommandTokens(splitString)); tokens.splice(index, 1, ...shellCommandTokens(longSplitString));
continue;
}
if (/^-[^-]/.test(token)) {
const options = token.slice(1);
let consumed = false;
for (let optionIndex = 0; optionIndex < options.length; optionIndex += 1) {
const option = options[optionIndex] ?? '';
if (option === 'u' || option === 'C') {
index += options.slice(optionIndex + 1) === '' ? 2 : 1;
consumed = true;
break;
}
if (option === 'S') {
const attached = options.slice(optionIndex + 1);
const operand = attached === '' ? (tokens[index + 1] ?? '') : attached;
tokens.splice(index, attached === '' ? 2 : 1, ...shellCommandTokens(operand));
consumed = true;
break;
}
}
if (!consumed) index += 1;
continue; continue;
} }
if (token.startsWith('-')) { if (token.startsWith('-')) {
@@ -258,7 +336,7 @@ function containsPrivilegedCommand(source: string): boolean {
'nix-env', 'nix-env',
]); ]);
return source.split(/[\n;&|]+/).some((segment): boolean => { return shellCommandSegments(source).some((segment): boolean => {
const tokens = normalizedCommandTokens(segment); const tokens = normalizedCommandTokens(segment);
const command = commandBasename(tokens[0] ?? ''); const command = commandBasename(tokens[0] ?? '');
if (privilegedCommands.has(command)) return true; if (privilegedCommands.has(command)) return true;
@@ -272,7 +350,7 @@ function containsPrivilegedCommand(source: string): boolean {
break; break;
} }
if ( if (
/^(?:-c|--command|--session-command|-g|--group|-G|--supp-group|-s|--shell|-w|--whitelist-environment)$/.test( /^(?:--command|--session-command|--group|--supp-group|--shell|--whitelist-environment)$/.test(
token, token,
) )
) { ) {
@@ -287,13 +365,28 @@ function containsPrivilegedCommand(source: string): boolean {
index += 1; index += 1;
continue; continue;
} }
if (/^-[^-]/.test(token)) {
const options = token.slice(1);
let consumed = false;
for (let optionIndex = 0; optionIndex < options.length; optionIndex += 1) {
const option = options[optionIndex] ?? '';
if ('cgGsw'.includes(option)) {
index += options.slice(optionIndex + 1) === '' ? 2 : 1;
consumed = true;
break;
}
}
if (!consumed) index += 1;
continue;
}
if (token.startsWith('-')) { if (token.startsWith('-')) {
index += 1; index += 1;
continue; continue;
} }
break; break;
} }
return (tokens[index] ?? 'root') === 'root'; const user = tokens[index];
return user === undefined || user === 'root';
}); });
} }
@@ -357,6 +450,14 @@ const UNSAFE_EXAMPLE_FIXTURES = [
expected: 'privileged-command', expected: 'privileged-command',
source: '/usr/bin/env -u FOO /usr/bin/apt-get install example', source: '/usr/bin/env -u FOO /usr/bin/apt-get install example',
}, },
{
expected: 'privileged-command',
source: 'env -iu FOO /usr/bin/apt-get --version',
},
{
expected: 'privileged-command',
source: 'env -ivS/usr/bin/apt-get --version',
},
{ {
expected: 'privileged-command', expected: 'privileged-command',
source: 'env --unset FOO /usr/bin/apt-get install example', source: 'env --unset FOO /usr/bin/apt-get install example',
@@ -382,6 +483,7 @@ const UNSAFE_EXAMPLE_FIXTURES = [
}, },
{ expected: 'privileged-command', source: 'reboot' }, { expected: 'privileged-command', source: 'reboot' },
{ expected: 'privileged-command', source: 'su --command=/bin/sh' }, { expected: 'privileged-command', source: 'su --command=/bin/sh' },
{ expected: 'privileged-command', source: 'su -lc /usr/bin/id' },
{ expected: 'privileged-command', source: 'su -l root' }, { expected: 'privileged-command', source: 'su -l root' },
{ expected: 'privileged-command', source: 'su -l -s /bin/sh root' }, { expected: 'privileged-command', source: 'su -l -s /bin/sh root' },
{ expected: 'privileged-command', source: '/usr/bin/su --login root' }, { expected: 'privileged-command', source: '/usr/bin/su --login root' },
@@ -440,9 +542,24 @@ describe('documentation validation regressions', (): void => {
expect(exampleSafetyViolationKinds('command -v apt-get')).not.toContain('privileged-command'); expect(exampleSafetyViolationKinds('command -v apt-get')).not.toContain('privileged-command');
}); });
it('keeps quoted command separators as inert argument data', (): void => {
expect(
exampleSafetyViolationKinds("printf '%s\\n' 'safe; /usr/bin/apt-get update'"),
).not.toContain('privileged-command');
});
it('still detects commands after unquoted command separators', (): void => {
expect(exampleSafetyViolationKinds('printf safe; /usr/bin/apt-get update')).toContain(
'privileged-command',
);
});
it.each([ it.each([
"env --split-string='-i -- /usr/bin/printf safe'", "env --split-string='-i -- /usr/bin/printf safe'",
"env --split-string='FOO=x /usr/bin/printf safe'", "env --split-string='FOO=x /usr/bin/printf safe'",
'env -iu FOO /usr/bin/printf safe',
'env -ivS/usr/bin/printf safe',
'su -lc /usr/bin/id operator',
'su -l -s /bin/sh operator', 'su -l -s /bin/sh operator',
'su -s /bin/sh -- operator', 'su -s /bin/sh -- operator',
'su -c /usr/bin/id operator', 'su -c /usr/bin/id operator',