@@ -59,11 +59,92 @@ describe("isAutoAllowedShellCall — sensitive-path arguments", () => {
5959 expect ( isAutoAllowedShellCall ( shellCall ( "cat .git-credentials" ) ) ) . toBe (
6060 false ,
6161 ) ;
62+ expect (
63+ isAutoAllowedShellCall (
64+ shellCall ( "env FILE=.envrc sh -c 'cat \"$FILE\"'" ) ,
65+ ) ,
66+ ) . toBe ( false ) ;
67+ expect ( isAutoAllowedShellCall ( shellCall ( "sed -Enf.flaskenv input" ) ) ) . toBe (
68+ false ,
69+ ) ;
70+ expect (
71+ isAutoAllowedShellCall ( shellCall ( "sed --fil=.envrc input.txt" ) ) ,
72+ ) . toBe ( false ) ;
73+ } ) ;
74+
75+ test ( "aliases, clusters, control prefixes, and ambiguity cannot auto-allow" , ( ) => {
76+ for ( const command of [
77+ "egrep -Jf.envrc needle" ,
78+ "grep -2f.flaskenv needle" ,
79+ "sed -anf.envrc input.txt" ,
80+ "{ awk -f.flaskenv input.txt; }" ,
81+ "! grep -Tf.envrc needle" ,
82+ "grep -Xf.envrc needle" ,
83+ "grep -uf.envrc needle" ,
84+ "cat $'.envrc'" ,
85+ "bash -c \"cat \\$'.envrc'\"" ,
86+ "bash -lc \"cat \\$'.envrc'\"" ,
87+ "bash -lc \"cat \\$'.flaskenv'\"" ,
88+ `bash -c "cat "'.envrc'` ,
89+ `sh -cc "cat "'.flaskenv'` ,
90+ "cat $'notes\\cQ'" ,
91+ ] ) {
92+ expect ( isAutoAllowedShellCall ( shellCall ( command ) ) ) . toBe ( false ) ;
93+ }
6294 } ) ;
6395
6496 test ( "still auto-allows reads of ordinary files" , ( ) => {
6597 expect ( isAutoAllowedShellCall ( shellCall ( "cat src/index.ts" ) ) ) . toBe ( true ) ;
6698 expect ( isAutoAllowedShellCall ( shellCall ( "cat .env.example" ) ) ) . toBe ( true ) ;
99+ expect ( isAutoAllowedShellCall ( shellCall ( "echo grep --file=.envrc" ) ) ) . toBe (
100+ true ,
101+ ) ;
102+ expect ( isAutoAllowedShellCall ( shellCall ( "echo dd if=.flaskenv" ) ) ) . toBe (
103+ true ,
104+ ) ;
105+ } ) ;
106+ } ) ;
107+
108+ describe ( "nested interpreter secret reads" , ( ) => {
109+ const nestedSecrets = [
110+ 'fish -c "cat .envrc"' ,
111+ 'fish -c "cat .env"' ,
112+ 'busybox sh -c "cat .envrc"' ,
113+ 'csh -c "cat .envrc"' ,
114+ 'tcsh -c "cat .envrc"' ,
115+ 'pwsh -c "cat .envrc"' ,
116+ ] ;
117+
118+ test ( "does not auto-allow secret reads behind nested interpreters" , ( ) => {
119+ for ( const command of nestedSecrets ) {
120+ expect ( isAutoAllowedShellCall ( shellCall ( command ) ) ) . toBe ( false ) ;
121+ expect ( autoShellRuleForCall ( shellCall ( command ) ) ) . toMatchObject ( {
122+ name : "sensitive-path" ,
123+ effect : "ask" ,
124+ } ) ;
125+ }
126+ } ) ;
127+
128+ test ( "auto mode does not allow nested-interpreter secret reads" , async ( ) => {
129+ for ( const command of nestedSecrets ) {
130+ const gate = createPermissionGate ( {
131+ approvals : [ ] ,
132+ interactive : false ,
133+ skipPermissions : false ,
134+ reactorGated : false ,
135+ auto : true ,
136+ } ) ;
137+ expect ( ( await gate . evaluate ( shellCall ( command ) ) ) . allowed ) . toBe ( false ) ;
138+ }
139+ } ) ;
140+
141+ test ( "templates behind nested interpreters stay unsensitive" , ( ) => {
142+ expect (
143+ autoShellRuleForCall ( shellCall ( 'fish -c "cat .env.example"' ) ) ?. name ,
144+ ) . not . toBe ( "sensitive-path" ) ;
145+ expect (
146+ autoShellRuleForCall ( shellCall ( 'busybox sh -c "cat .env.sample"' ) ) ?. name ,
147+ ) . not . toBe ( "sensitive-path" ) ;
67148 } ) ;
68149} ) ;
69150
@@ -428,13 +509,23 @@ describe("sensitive-path shell commands require approval, not a hard deny", () =
428509 } ) ;
429510
430511 test ( "auto mode forces ask for shell commands that reference secret files" , ( ) => {
431- const rule = autoShellRuleForCall (
432- shellCall ( "bun --env-file=.env.staging run publish.ts" ) ,
433- ) ;
512+ const rule = autoShellRuleForCall ( shellCall ( "cat .envrc" ) ) ;
434513 expect ( rule ?. name ) . toBe ( "sensitive-path" ) ;
435514 expect ( rule ?. effect ) . toBe ( "ask" ) ;
436515 } ) ;
437516
517+ test ( "auto mode asks for clustered bash secret reads" , ( ) => {
518+ for ( const command of [
519+ "bash -lc \"cat \\$'.envrc'\"" ,
520+ "bash -lc \"cat \\$'.flaskenv'\"" ,
521+ ] ) {
522+ expect ( autoShellRuleForCall ( shellCall ( command ) ) ) . toMatchObject ( {
523+ name : "sensitive-path" ,
524+ effect : "ask" ,
525+ } ) ;
526+ }
527+ } ) ;
528+
438529 test ( "operator approval lets a sensitive-path shell command through the gate" , async ( ) => {
439530 let asked = 0 ;
440531 const gate = createPermissionGate ( {
@@ -484,7 +575,7 @@ describe("sensitive-path shell commands require approval, not a hard deny", () =
484575 skipPermissions : false ,
485576 reactorGated : false ,
486577 } ) ;
487- const verdict = await gate . evaluate ( shellCall ( "cat .env " ) ) ;
578+ const verdict = await gate . evaluate ( shellCall ( "cat .flaskenv " ) ) ;
488579 expect ( verdict . allowed ) . toBe ( true ) ;
489580 expect ( asked ) . toBe ( 1 ) ;
490581 } ) ;
@@ -501,7 +592,7 @@ describe("sensitive-path shell commands require approval, not a hard deny", () =
501592 skipPermissions : false ,
502593 reactorGated : false ,
503594 } ) ;
504- const verdict = await gate . evaluate ( shellCall ( "cat README.md " ) ) ;
595+ const verdict = await gate . evaluate ( shellCall ( "cat ordinary=.envrc " ) ) ;
505596 expect ( verdict . allowed ) . toBe ( true ) ;
506597 expect ( asked ) . toBe ( 0 ) ;
507598 } ) ;
0 commit comments