返回 CodeWhale
redirection_policy.rs
根目录 / crates / execpolicy / tests / redirection_policy.rs
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
173 lines RUST