| 1 | use codewhale_execpolicy::{ |
| 2 | AskForApproval, ExecApprovalRequirement, ExecPolicyContext, ExecPolicyEngine, PermissionAction, |
| 3 | Ruleset, ToolAskRule, shell_expand::expanded_commands, |
| 4 | }; |
| 5 | |
| 6 | fn context(command: &str, approval: AskForApproval) -> ExecPolicyContext<'_> { |
| 7 | ExecPolicyContext { |
| 8 | command, |
| 9 | cwd: "/workspace", |
| 10 | tool: Some("exec_shell"), |
| 11 | path: None, |
| 12 | ask_for_approval: approval, |
| 13 | sandbox_mode: None, |
| 14 | } |
| 15 | } |
| 16 | |
| 17 | #[test] |
| 18 | fn redirection_syntax_preserves_prefix_and_typed_denials() { |
| 19 | let engines = [ |
| 20 | ExecPolicyEngine::new(vec![], vec!["printf probe".to_string()]), |
| 21 | ExecPolicyEngine::with_rulesets(vec![Ruleset::user(vec![], vec![]).with_ask_rules(vec![ |
| 22 | ToolAskRule { |
| 23 | action: PermissionAction::Deny, |
| 24 | ..ToolAskRule::exec_shell("printf probe") |
| 25 | }, |
| 26 | ])]), |
| 27 | ]; |
| 28 | for command in [ |
| 29 | "printf probe", |
| 30 | "printf>marker probe", |
| 31 | "printf>>marker probe", |
| 32 | "printf<marker probe", |
| 33 | "printf<>marker probe", |
| 34 | "printf>|marker probe", |
| 35 | "printf>&1 probe", |
| 36 | "printf<&0 probe", |
| 37 | "printf&>marker probe", |
| 38 | "printf&>>marker probe", |
| 39 | "printf<<END probe\ntext\nEND", |
| 40 | "printf<<-END probe\n\ttext\nEND", |
| 41 | "printf<<<text probe", |
| 42 | ">marker printf probe", |
| 43 | "2>marker printf probe", |
| 44 | "{output}>marker printf probe", |
| 45 | "printf 2>marker probe", |
| 46 | "printf 2>&1 probe", |
| 47 | "printf 3<&0 probe", |
| 48 | "printf 3>&- probe", |
| 49 | "printf >'marker with spaces' probe", |
| 50 | "printf >\"marker with spaces\" probe", |
| 51 | "printf >marker\\ with\\ spaces probe", |
| 52 | "printf >one 2>two probe", |
| 53 | "printf >$(echo marker) probe", |
| 54 | "printf >`echo marker` probe", |
| 55 | "printf >${marker:-out} probe", |
| 56 | "printf > >(cat) probe", |
| 57 | "env >marker printf probe", |
| 58 | "sh >marker -c 'printf probe'", |
| 59 | "$(echo) sh >marker -c 'printf probe'", |
| 60 | "echo ok; >marker printf probe", |
| 61 | ] { |
| 62 | for engine in &engines { |
| 63 | let decision = engine |
| 64 | .check(context(command, AskForApproval::Never)) |
| 65 | .unwrap(); |
| 66 | assert!(!decision.allow, "{command:?}"); |
| 67 | assert!(!decision.requires_approval, "{command:?}"); |
| 68 | assert!(matches!( |
| 69 | decision.requirement, |
| 70 | ExecApprovalRequirement::Forbidden { .. } |
| 71 | )); |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | #[test] |
| 77 | fn substitutions_in_redirection_operands_keep_their_own_denials() { |
| 78 | let engine = ExecPolicyEngine::new(vec![], vec!["printf probe".to_string()]); |
| 79 | for command in [ |
| 80 | "echo >$(printf probe)", |
| 81 | "echo >`printf probe`", |
| 82 | "echo >\"$(printf probe)\"", |
| 83 | "echo >${output:-$(printf probe)}", |
| 84 | "echo > >(printf probe)", |
| 85 | "echo < <(printf probe)", |
| 86 | "echo <<<$(printf probe)", |
| 87 | ] { |
| 88 | assert!( |
| 89 | !engine |
| 90 | .check(context(command, AskForApproval::Never)) |
| 91 | .unwrap() |
| 92 | .allow, |
| 93 | "{command:?}" |
| 94 | ); |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | #[test] |
| 99 | fn quoted_operators_and_redirect_targets_remain_data() { |
| 100 | let engine = ExecPolicyEngine::new(vec![], vec!["printf".to_string()]); |
| 101 | for command in [ |
| 102 | "echo probe", |
| 103 | "'printf>marker' probe", |
| 104 | "\"printf<marker\" probe", |
| 105 | "printf\\>marker probe", |
| 106 | "echo >printf probe", |
| 107 | ">printf echo probe", |
| 108 | "echo >'$(printf probe)'", |
| 109 | "echo >marker\\>printf probe", |
| 110 | ] { |
| 111 | assert!( |
| 112 | engine |
| 113 | .check(context(command, AskForApproval::Never)) |
| 114 | .unwrap() |
| 115 | .allow, |
| 116 | "{command:?}" |
| 117 | ); |
| 118 | } |
| 119 | for (command, expected) in [ |
| 120 | ("echo '2'>marker probe", "echo 2 probe"), |
| 121 | ("echo \\2>marker probe", "echo 2 probe"), |
| 122 | ("echo 2 >marker probe", "echo 2 probe"), |
| 123 | ("printf2>marker probe", "printf2 probe"), |
| 124 | ("echo '{output}'>marker probe", "echo {output} probe"), |
| 125 | ] { |
| 126 | assert!(expanded_commands(command).iter().any(|c| c == expected)); |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | #[test] |
| 131 | fn removing_redirections_does_not_expand_trusted_grants() { |
| 132 | let engine = ExecPolicyEngine::new(vec!["printf".to_string()], vec![]); |
| 133 | let decision = engine |
| 134 | .check(context( |
| 135 | ">marker printf probe", |
| 136 | AskForApproval::UnlessTrusted, |
| 137 | )) |
| 138 | .unwrap(); |
| 139 | assert!(decision.allow && decision.requires_approval); |
| 140 | } |
| 141 | |
| 142 | #[cfg(unix)] |
| 143 | #[test] |
| 144 | fn harmless_shell_reference_agrees_with_the_denied_command_candidates() { |
| 145 | // Execute only this fixed harmless fixture in an owned temporary directory |
| 146 | // to compare the real shell's words with the policy's candidate commands. |
| 147 | let unique = std::time::SystemTime::now() |
| 148 | .duration_since(std::time::UNIX_EPOCH) |
| 149 | .unwrap() |
| 150 | .as_nanos(); |
| 151 | let dir = std::env::temp_dir().join(format!("cw-policy-{}-{unique}", std::process::id())); |
| 152 | std::fs::create_dir(&dir).unwrap(); |
| 153 | for command in [ |
| 154 | "printf>marker probe", |
| 155 | ">marker printf probe", |
| 156 | "printf 2>&1 >marker probe", |
| 157 | ] { |
| 158 | let status = std::process::Command::new("/bin/sh") |
| 159 | .args(["-c", command]) |
| 160 | .current_dir(&dir) |
| 161 | .status() |
| 162 | .unwrap(); |
| 163 | assert!(status.success()); |
| 164 | assert_eq!(std::fs::read(dir.join("marker")).unwrap(), b"probe"); |
| 165 | assert!( |
| 166 | expanded_commands(command) |
| 167 | .iter() |
| 168 | .any(|c| c == "printf probe") |
| 169 | ); |
| 170 | } |
| 171 | std::fs::remove_dir_all(dir).unwrap(); |
| 172 | } |
| 173 |