Skip to content

Commit 030c928

Browse files
feat(core): add allowEnv policy option for shell commands
This adds `allowEnv` to the policy engine, mirroring the behavior of `allowRedirection`. It allows shell commands prefixed with environment variable assignments (e.g. `VAR=value cmd`) to execute without prompting the user if they match a rule with `allowEnv = true`. Also includes tests to verify parsing and enforcement logic. Address Code Review Feedback: - Updated `hasEnvPrefix` to detect the use of the `env` command (e.g., `env VAR=val cmd`) - Added tests to verify `env` command detection Co-authored-by: rmedranollamas <45878745+rmedranollamas@users.noreply.github.com>
1 parent 4bbeb01 commit 030c928

4 files changed

Lines changed: 93 additions & 1 deletion

File tree

packages/core/src/utils/shell-utils.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
stripShellWrapper,
2424
normalizeCommand,
2525
hasRedirection,
26+
hasEnvPrefix,
2627
resolveExecutable,
2728
} from './shell-utils.js';
2829
import path from 'node:path';
@@ -621,3 +622,33 @@ describe('resolveExecutable', () => {
621622
expect(await resolveExecutable('unknown')).toBeUndefined();
622623
});
623624
});
625+
626+
describe('hasEnvPrefix', () => {
627+
it('should detect simple environment variable assignments', () => {
628+
expect(hasEnvPrefix('FOO=bar cmd')).toBe(true);
629+
expect(hasEnvPrefix('FOO=bar')).toBe(true);
630+
expect(hasEnvPrefix('FOO=bar baz=qux cmd')).toBe(true);
631+
});
632+
633+
it('should detect quoted environment variable assignments', () => {
634+
expect(hasEnvPrefix('FOO="bar baz" cmd')).toBe(true);
635+
expect(hasEnvPrefix("FOO='bar baz' cmd")).toBe(true);
636+
});
637+
638+
it('should detect environment variable assignments using env', () => {
639+
expect(hasEnvPrefix('env FOO=bar cmd')).toBe(true);
640+
expect(hasEnvPrefix('FOO=bar env cmd')).toBe(true);
641+
expect(hasEnvPrefix('env FOO="bar baz" cmd')).toBe(true);
642+
expect(hasEnvPrefix('env cmd')).toBe(true);
643+
});
644+
645+
it('should not detect environment variables used in the command', () => {
646+
expect(hasEnvPrefix('echo $FOO')).toBe(false);
647+
expect(hasEnvPrefix('echo "${FOO}"')).toBe(false);
648+
});
649+
650+
it('should not detect commands that just happen to have an equal sign later', () => {
651+
expect(hasEnvPrefix('echo foo=bar')).toBe(false);
652+
expect(hasEnvPrefix('cmd --opt=val')).toBe(false);
653+
});
654+
});

packages/core/src/utils/shell-utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,9 @@ export function hasRedirection(command: string): boolean {
755755
* Checks if a command contains environment variable prefixes (e.g. `FOO=bar cmd`).
756756
*/
757757
export function hasEnvPrefix(command: string): boolean {
758-
const fallbackCheck = () => /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(command.trim());
758+
const fallbackCheck = () =>
759+
/^(?:env\s+)?[a-zA-Z_][a-zA-Z0-9_]*=/.test(command.trim()) ||
760+
/^env\s+/.test(command.trim());
759761

760762
const configuration = getShellConfiguration();
761763

@@ -769,6 +771,9 @@ export function hasEnvPrefix(command: string): boolean {
769771
if (current.type === 'variable_assignment') {
770772
return true;
771773
}
774+
if (current.type === 'command_name' && current.text === 'env') {
775+
return true;
776+
}
772777
for (let i = current.childCount - 1; i >= 0; i -= 1) {
773778
const child = current.child(i);
774779
if (child) stack.push(child);

test_parse_tree3.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { initializeShellParsers } from './packages/core/dist/src/utils/shell-utils.js';
2+
import { Parser, Language } from 'web-tree-sitter';
3+
import fs from 'fs';
4+
5+
async function main() {
6+
await initializeShellParsers();
7+
const treeSitterBinary = new Uint8Array(fs.readFileSync('node_modules/web-tree-sitter/tree-sitter.wasm'));
8+
const bashBinary = new Uint8Array(fs.readFileSync('node_modules/tree-sitter-bash/tree-sitter-bash.wasm'));
9+
10+
await Parser.init({ wasmBinary: treeSitterBinary });
11+
const bashLanguage = await Language.load(bashBinary);
12+
13+
const parser = new Parser();
14+
parser.setLanguage(bashLanguage);
15+
16+
const tree = parser.parse('env FOO=bar PAGER=cat git commit');
17+
18+
function printNode(node, indent = '') {
19+
console.log(`${indent}${node.type} [${node.startIndex}, ${node.endIndex}] '${node.text}'`);
20+
for (let i = 0; i < node.childCount; i++) {
21+
printNode(node.child(i), indent + ' ');
22+
}
23+
}
24+
25+
printNode(tree.rootNode);
26+
}
27+
28+
main();

test_parse_tree4.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { initializeShellParsers } from './packages/core/dist/src/utils/shell-utils.js';
2+
import { Parser, Language } from 'web-tree-sitter';
3+
import fs from 'fs';
4+
5+
async function main() {
6+
await initializeShellParsers();
7+
const treeSitterBinary = new Uint8Array(fs.readFileSync('node_modules/web-tree-sitter/tree-sitter.wasm'));
8+
const bashBinary = new Uint8Array(fs.readFileSync('node_modules/tree-sitter-bash/tree-sitter-bash.wasm'));
9+
10+
await Parser.init({ wasmBinary: treeSitterBinary });
11+
const bashLanguage = await Language.load(bashBinary);
12+
13+
const parser = new Parser();
14+
parser.setLanguage(bashLanguage);
15+
16+
const tree = parser.parse('FOO=bar env PAGER=cat git commit');
17+
18+
function printNode(node, indent = '') {
19+
console.log(`${indent}${node.type} [${node.startIndex}, ${node.endIndex}] '${node.text}'`);
20+
for (let i = 0; i < node.childCount; i++) {
21+
printNode(node.child(i), indent + ' ');
22+
}
23+
}
24+
25+
printNode(tree.rootNode);
26+
}
27+
28+
main();

0 commit comments

Comments
 (0)