66 parseCommand ,
77 isAutoApprovedSingleCommand ,
88 isAutoDeniedSingleCommand ,
9- isAutoApprovedCommand ,
10- isAutoDeniedCommand ,
119 findLongestPrefixMatch ,
1210 getCommandDecision ,
1311 getSingleCommandDecision ,
@@ -167,7 +165,7 @@ ls -la || echo "Failed"`
167165 } )
168166 } )
169167
170- describe ( "isAutoApprovedSingleCommand (legacy behavior) " , ( ) => {
168+ describe ( "isAutoApprovedSingleCommand" , ( ) => {
171169 const allowedCommands = [ "npm test" , "npm run" , "echo" ]
172170
173171 it ( "matches commands case-insensitively" , ( ) => {
@@ -193,93 +191,6 @@ ls -la || echo "Failed"`
193191 expect ( isAutoApprovedSingleCommand ( "npm test" , [ ] ) ) . toBe ( false )
194192 } )
195193 } )
196-
197- describe ( "isAutoApprovedCommand (legacy behavior)" , ( ) => {
198- const allowedCommands = [ "npm test" , "npm run" , "echo" , "Select-String" ]
199-
200- it ( "validates simple commands" , ( ) => {
201- expect ( isAutoApprovedCommand ( "npm test" , allowedCommands ) ) . toBe ( true )
202- expect ( isAutoApprovedCommand ( "npm run build" , allowedCommands ) ) . toBe ( true )
203- expect ( isAutoApprovedCommand ( "dangerous" , allowedCommands ) ) . toBe ( false )
204- } )
205-
206- it ( "validates chained commands" , ( ) => {
207- expect ( isAutoApprovedCommand ( "npm test && npm run build" , allowedCommands ) ) . toBe ( true )
208- expect ( isAutoApprovedCommand ( "npm test && dangerous" , allowedCommands ) ) . toBe ( false )
209- expect ( isAutoApprovedCommand ( 'npm test | Select-String "Error"' , allowedCommands ) ) . toBe ( true )
210- expect ( isAutoApprovedCommand ( "npm test | rm -rf /" , allowedCommands ) ) . toBe ( false )
211- } )
212-
213- it ( "handles quoted content correctly" , ( ) => {
214- expect ( isAutoApprovedCommand ( 'npm test "param with | inside"' , allowedCommands ) ) . toBe ( true )
215- expect ( isAutoApprovedCommand ( 'echo "hello | world"' , allowedCommands ) ) . toBe ( true )
216- expect ( isAutoApprovedCommand ( 'npm test "param with && inside"' , allowedCommands ) ) . toBe ( true )
217- } )
218-
219- it ( "handles subshell execution attempts" , ( ) => {
220- // Without denylist, subshells should be allowed if all subcommands are allowed
221- expect ( isAutoApprovedCommand ( "npm test $(echo hello)" , allowedCommands ) ) . toBe ( true )
222- expect ( isAutoApprovedCommand ( "npm test `echo world`" , allowedCommands ) ) . toBe ( true )
223-
224- // With denylist, subshells should be blocked regardless of subcommands
225- expect ( isAutoApprovedCommand ( "npm test $(echo hello)" , allowedCommands , [ "rm" ] ) ) . toBe ( false )
226- expect ( isAutoApprovedCommand ( "npm test `echo world`" , allowedCommands , [ "rm" ] ) ) . toBe ( false )
227- } )
228-
229- it ( "handles PowerShell patterns" , ( ) => {
230- expect ( isAutoApprovedCommand ( 'npm test 2>&1 | Select-String "Error"' , allowedCommands ) ) . toBe ( true )
231- expect (
232- isAutoApprovedCommand (
233- 'npm test | Select-String -NotMatch "node_modules" | Select-String "FAIL|Error"' ,
234- allowedCommands ,
235- ) ,
236- ) . toBe ( true )
237- expect ( isAutoApprovedCommand ( "npm test | Select-String | dangerous" , allowedCommands ) ) . toBe ( false )
238- } )
239-
240- it ( "handles empty input" , ( ) => {
241- expect ( isAutoApprovedCommand ( "" , allowedCommands ) ) . toBe ( true )
242- expect ( isAutoApprovedCommand ( " " , allowedCommands ) ) . toBe ( true )
243- } )
244-
245- it ( "allows all commands when wildcard is present" , ( ) => {
246- const wildcardAllowedCommands = [ "*" ]
247- // Should allow any command, including dangerous ones
248- expect ( isAutoApprovedCommand ( "rm -rf /" , wildcardAllowedCommands ) ) . toBe ( true )
249- expect ( isAutoApprovedCommand ( "dangerous-command" , wildcardAllowedCommands ) ) . toBe ( true )
250- expect ( isAutoApprovedCommand ( "npm test && rm -rf /" , wildcardAllowedCommands ) ) . toBe ( true )
251- // Should allow subshell commands with wildcard when no denylist is present
252- expect ( isAutoApprovedCommand ( "npm test $(echo dangerous)" , wildcardAllowedCommands ) ) . toBe ( true )
253- expect ( isAutoApprovedCommand ( "npm test `rm -rf /`" , wildcardAllowedCommands ) ) . toBe ( true )
254-
255- // But should block subshells when denylist is present
256- expect ( isAutoApprovedCommand ( "npm test $(echo dangerous)" , wildcardAllowedCommands , [ "rm" ] ) ) . toBe ( false )
257- expect ( isAutoApprovedCommand ( "npm test `rm -rf /`" , wildcardAllowedCommands , [ "rm" ] ) ) . toBe ( false )
258- } )
259-
260- it ( "respects denylist even with wildcard in allowlist" , ( ) => {
261- const wildcardAllowedCommands = [ "*" ]
262- const deniedCommands = [ "rm -rf" , "dangerous" ]
263-
264- // Wildcard should allow most commands
265- expect ( isAutoApprovedCommand ( "npm test" , wildcardAllowedCommands , deniedCommands ) ) . toBe ( true )
266- expect ( isAutoApprovedCommand ( "echo hello" , wildcardAllowedCommands , deniedCommands ) ) . toBe ( true )
267- expect ( isAutoApprovedCommand ( "git status" , wildcardAllowedCommands , deniedCommands ) ) . toBe ( true )
268-
269- // But denylist should still block specific commands
270- expect ( isAutoApprovedCommand ( "rm -rf /" , wildcardAllowedCommands , deniedCommands ) ) . toBe ( false )
271- expect ( isAutoApprovedCommand ( "dangerous-command" , wildcardAllowedCommands , deniedCommands ) ) . toBe ( false )
272-
273- // Chained commands with denied subcommands should be blocked
274- expect ( isAutoApprovedCommand ( "npm test && rm -rf /" , wildcardAllowedCommands , deniedCommands ) ) . toBe ( false )
275- expect (
276- isAutoApprovedCommand ( "echo hello && dangerous-command" , wildcardAllowedCommands , deniedCommands ) ,
277- ) . toBe ( false )
278-
279- // But chained commands with all allowed subcommands should work
280- expect ( isAutoApprovedCommand ( "npm test && echo done" , wildcardAllowedCommands , deniedCommands ) ) . toBe ( true )
281- } )
282- } )
283194} )
284195
285196/**
@@ -510,52 +421,6 @@ echo "Successfully converted $count .jsx files to .tsx"`
510421 } )
511422 } )
512423
513- describe ( "isAutoApprovedCommand (legacy behavior)" , ( ) => {
514- it ( "should validate allowed commands" , ( ) => {
515- const result = isAutoApprovedCommand ( "echo hello" , [ "echo" ] )
516- expect ( result ) . toBe ( true )
517- } )
518-
519- it ( "should reject disallowed commands" , ( ) => {
520- const result = isAutoApprovedCommand ( "rm -rf /" , [ "echo" , "ls" ] )
521- expect ( result ) . toBe ( false )
522- } )
523-
524- it ( "should not fail validation for commands with simple $RANDOM variable" , ( ) => {
525- const commandWithRandom = "echo $RANDOM"
526-
527- expect ( ( ) => {
528- isAutoApprovedCommand ( commandWithRandom , [ "echo" ] )
529- } ) . not . toThrow ( )
530- } )
531-
532- it ( "should not fail validation for commands with simple array indexing using $RANDOM" , ( ) => {
533- const commandWithRandomIndex = "echo ${array[$RANDOM]}"
534-
535- expect ( ( ) => {
536- isAutoApprovedCommand ( commandWithRandomIndex , [ "echo" ] )
537- } ) . not . toThrow ( )
538- } )
539-
540- it ( "should return false for the full log generator command due to subshell detection when denylist is present" , ( ) => {
541- // This is the exact command from the original error message
542- const logGeneratorCommand = `while true; do \\
543- levels=(INFO WARN ERROR DEBUG); \\
544- msgs=("User logged in" "Connection timeout" "Processing request" "Cache miss" "Database query"); \\
545- level=\${levels[$RANDOM % \${#levels[@]}]}; \\
546- msg=\${msgs[$RANDOM % \${#msgs[@]}]}; \\
547- echo "\$(date '+%Y-%m-%d %H:%M:%S') [$level] $msg"; \\
548- sleep 1; \\
549- done`
550-
551- // Without denylist, should allow subshells if all subcommands are allowed (use wildcard)
552- expect ( isAutoApprovedCommand ( logGeneratorCommand , [ "*" ] ) ) . toBe ( true )
553-
554- // With denylist, should return false due to subshell detection
555- expect ( isAutoApprovedCommand ( logGeneratorCommand , [ "*" ] , [ "rm" ] ) ) . toBe ( false )
556- } )
557- } )
558-
559424 describe ( "Denylist Command Validation" , ( ) => {
560425 describe ( "findLongestPrefixMatch" , ( ) => {
561426 it ( "finds the longest matching prefix" , ( ) => {
@@ -584,7 +449,7 @@ done`
584449 } )
585450 } )
586451
587- describe ( "Legacy isAllowedSingleCommand behavior (now using isAutoApprovedSingleCommand) " , ( ) => {
452+ describe ( "isAutoApprovedSingleCommand" , ( ) => {
588453 const allowedCommands = [ "npm" , "echo" , "git" ]
589454 const deniedCommands = [ "npm test" , "git push" ]
590455
@@ -761,71 +626,6 @@ done`
761626 } )
762627 } )
763628 } )
764-
765- describe ( "Command-level three-tier validation" , ( ) => {
766- const allowedCommands = [ "npm" , "echo" ]
767- const deniedCommands = [ "npm test" ]
768-
769- describe ( "isAutoApprovedCommand" , ( ) => {
770- it ( "auto-approves commands with all sub-commands auto-approved" , ( ) => {
771- expect ( isAutoApprovedCommand ( "npm install" , allowedCommands , deniedCommands ) ) . toBe ( true )
772- expect ( isAutoApprovedCommand ( "npm install && echo done" , allowedCommands , deniedCommands ) ) . toBe (
773- true ,
774- )
775- } )
776-
777- it ( "does not auto-approve commands with any sub-command not auto-approved" , ( ) => {
778- expect ( isAutoApprovedCommand ( "npm test" , allowedCommands , deniedCommands ) ) . toBe ( false )
779- expect ( isAutoApprovedCommand ( "npm install && npm test" , allowedCommands , deniedCommands ) ) . toBe (
780- false ,
781- )
782- } )
783-
784- it ( "blocks subshell commands only when denylist is present" , ( ) => {
785- // Without denylist, should allow subshells
786- expect ( isAutoApprovedCommand ( "npm install $(echo test)" , allowedCommands ) ) . toBe ( true )
787- expect ( isAutoApprovedCommand ( "npm install `echo test`" , allowedCommands ) ) . toBe ( true )
788-
789- // With denylist, should block subshells
790- expect ( isAutoApprovedCommand ( "npm install $(echo test)" , allowedCommands , deniedCommands ) ) . toBe (
791- false ,
792- )
793- expect ( isAutoApprovedCommand ( "npm install `echo test`" , allowedCommands , deniedCommands ) ) . toBe (
794- false ,
795- )
796- } )
797- } )
798-
799- describe ( "isAutoDeniedCommand" , ( ) => {
800- it ( "auto-denies commands with any sub-command auto-denied" , ( ) => {
801- expect ( isAutoDeniedCommand ( "npm test" , allowedCommands , deniedCommands ) ) . toBe ( true )
802- expect ( isAutoDeniedCommand ( "npm install && npm test" , allowedCommands , deniedCommands ) ) . toBe (
803- true ,
804- )
805- } )
806-
807- it ( "does not auto-deny commands with all sub-commands not auto-denied" , ( ) => {
808- expect ( isAutoDeniedCommand ( "npm install" , allowedCommands , deniedCommands ) ) . toBe ( false )
809- expect ( isAutoDeniedCommand ( "npm install && echo done" , allowedCommands , deniedCommands ) ) . toBe (
810- false ,
811- )
812- } )
813-
814- it ( "auto-denies subshell commands only when denylist is present" , ( ) => {
815- // Without denylist, should not auto-deny subshells
816- expect ( isAutoDeniedCommand ( "npm install $(echo test)" , allowedCommands ) ) . toBe ( false )
817- expect ( isAutoDeniedCommand ( "npm install `echo test`" , allowedCommands ) ) . toBe ( false )
818-
819- // With denylist, should auto-deny subshells
820- expect ( isAutoDeniedCommand ( "npm install $(echo test)" , allowedCommands , deniedCommands ) ) . toBe (
821- true ,
822- )
823- expect ( isAutoDeniedCommand ( "npm install `echo test`" , allowedCommands , deniedCommands ) ) . toBe (
824- true ,
825- )
826- } )
827- } )
828- } )
829629 } )
830630 } )
831631} )
@@ -912,9 +712,19 @@ describe("Unified Command Decision Functions", () => {
912712 expect ( getCommandDecision ( "npm install && dangerous" , allowedCommands , deniedCommands ) ) . toBe ( "ask_user" )
913713 } )
914714
915- it ( "returns auto_deny for subshell commands when denylist is present" , ( ) => {
916- expect ( getCommandDecision ( "npm install $(echo test)" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
917- expect ( getCommandDecision ( "npm install `echo test`" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
715+ it ( "returns auto_deny for subshell commands only when they contain denied prefixes" , ( ) => {
716+ // Subshells without denied prefixes should not be auto-denied
717+ expect ( getCommandDecision ( "npm install $(echo test)" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
718+ expect ( getCommandDecision ( "npm install `echo test`" , allowedCommands , deniedCommands ) ) . toBe ( "auto_approve" )
719+
720+ // Subshells with denied prefixes should be auto-denied
721+ expect ( getCommandDecision ( "npm install $(npm test)" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
722+ expect ( getCommandDecision ( "npm install `npm test --coverage`" , allowedCommands , deniedCommands ) ) . toBe (
723+ "auto_deny" ,
724+ )
725+
726+ // Main command with denied prefix should also be auto-denied
727+ expect ( getCommandDecision ( "npm test $(echo hello)" , allowedCommands , deniedCommands ) ) . toBe ( "auto_deny" )
918728 } )
919729
920730 it ( "allows subshell commands when no denylist is present" , ( ) => {
@@ -961,39 +771,6 @@ describe("Unified Command Decision Functions", () => {
961771 } )
962772 } )
963773
964- describe ( "Integration with existing functions" , ( ) => {
965- it ( "maintains backward compatibility with existing behavior" , ( ) => {
966- const allowedCommands = [ "npm" , "echo" ]
967- const deniedCommands = [ "npm test" ]
968-
969- // Test that new unified functions produce same results as old separate functions
970- const testCommands = [
971- "npm install" , // should be auto-approved
972- "npm test" , // should be auto-denied
973- "dangerous" , // should ask user
974- "echo hello" , // should be auto-approved
975- ]
976-
977- testCommands . forEach ( ( cmd ) => {
978- const decision = getCommandDecision ( cmd , allowedCommands , deniedCommands )
979- const oldApproved = isAutoApprovedCommand ( cmd , allowedCommands , deniedCommands )
980- const oldDenied = isAutoDeniedCommand ( cmd , allowedCommands , deniedCommands )
981-
982- // Verify consistency
983- if ( decision === "auto_approve" ) {
984- expect ( oldApproved ) . toBe ( true )
985- expect ( oldDenied ) . toBe ( false )
986- } else if ( decision === "auto_deny" ) {
987- expect ( oldApproved ) . toBe ( false )
988- expect ( oldDenied ) . toBe ( true )
989- } else if ( decision === "ask_user" ) {
990- expect ( oldApproved ) . toBe ( false )
991- expect ( oldDenied ) . toBe ( false )
992- }
993- } )
994- } )
995- } )
996-
997774 describe ( "CommandValidator Integration Tests" , ( ) => {
998775 describe ( "CommandValidator Class" , ( ) => {
999776 let validator : CommandValidator
@@ -1067,7 +844,12 @@ describe("Unified Command Decision Functions", () => {
1067844 it ( "detects subshells correctly" , ( ) => {
1068845 const details = validator . getValidationDetails ( "npm install $(echo test)" )
1069846 expect ( details . hasSubshells ) . toBe ( true )
1070- expect ( details . decision ) . toBe ( "auto_deny" ) // blocked due to subshells with denylist
847+ expect ( details . decision ) . toBe ( "auto_approve" ) // not blocked since echo doesn't match denied prefixes
848+
849+ // Test with denied prefix in subshell
850+ const detailsWithDenied = validator . getValidationDetails ( "npm install $(npm test)" )
851+ expect ( detailsWithDenied . hasSubshells ) . toBe ( true )
852+ expect ( detailsWithDenied . decision ) . toBe ( "auto_deny" ) // blocked due to npm test in subshell
1071853 } )
1072854
1073855 it ( "handles complex command chains" , ( ) => {
@@ -1162,6 +944,41 @@ describe("Unified Command Decision Functions", () => {
1162944 } )
1163945 } )
1164946
947+ describe ( "Subshell edge cases" , ( ) => {
948+ it ( "handles multiple subshells correctly" , ( ) => {
949+ const validator = createCommandValidator ( [ "echo" , "npm" ] , [ "rm" , "sudo" ] )
950+
951+ // Multiple subshells, none with denied prefixes but subshell commands not in allowlist
952+ // parseCommand extracts subshells as separate commands, so date and pwd are not allowed
953+ expect ( validator . validateCommand ( "echo $(date) $(pwd)" ) ) . toBe ( "ask_user" )
954+
955+ // Multiple subshells, one with denied prefix
956+ expect ( validator . validateCommand ( "echo $(date) $(rm file)" ) ) . toBe ( "auto_deny" )
957+
958+ // Nested subshells - inner commands are extracted and not in allowlist
959+ expect ( validator . validateCommand ( "echo $(echo $(date))" ) ) . toBe ( "ask_user" )
960+ expect ( validator . validateCommand ( "echo $(echo $(rm file))" ) ) . toBe ( "auto_deny" )
961+ } )
962+
963+ it ( "handles complex commands with subshells" , ( ) => {
964+ const validator = createCommandValidator ( [ "npm" , "git" , "echo" ] , [ "git push" , "npm publish" ] )
965+
966+ // Subshell with allowed command - git status is extracted as separate command
967+ // Since "git status" starts with "git" which is allowed, it's approved
968+ expect ( validator . validateCommand ( "npm run $(git status)" ) ) . toBe ( "auto_approve" )
969+
970+ // Subshell with denied command
971+ expect ( validator . validateCommand ( "npm run $(git push origin)" ) ) . toBe ( "auto_deny" )
972+
973+ // Main command denied, subshell allowed
974+ expect ( validator . validateCommand ( "git push $(echo origin)" ) ) . toBe ( "auto_deny" )
975+
976+ // Complex chain with subshells - need echo in allowlist
977+ expect ( validator . validateCommand ( "npm install && echo $(git status) && npm test" ) ) . toBe ( "auto_approve" )
978+ expect ( validator . validateCommand ( "npm install && echo $(git push) && npm test" ) ) . toBe ( "auto_deny" )
979+ } )
980+ } )
981+
1165982 describe ( "Real-world integration scenarios" , ( ) => {
1166983 describe ( "Development workflow validation" , ( ) => {
1167984 let devValidator : CommandValidator
0 commit comments