safe_chains/handlers/
mod.rs1macro_rules! handler_module {
2 ($($sub:ident),+ $(,)?) => {
3 $(mod $sub;)+
4
5 pub(crate) fn dispatch(cmd: &str, tokens: &[crate::parse::Token]) -> Option<crate::verdict::Verdict> {
6 None$(.or_else(|| $sub::dispatch(cmd, tokens)))+
7 }
8
9 pub fn command_docs() -> Vec<crate::docs::CommandDoc> {
10 let mut docs = Vec::new();
11 $(docs.extend($sub::command_docs());)+
12 docs
13 }
14
15 #[cfg(test)]
16 pub(super) fn full_registry() -> Vec<&'static super::CommandEntry> {
17 let mut v = Vec::new();
18 $(v.extend($sub::REGISTRY);)+
19 v
20 }
21 };
22}
23
24pub mod android;
25pub mod coreutils;
26pub mod forges;
27pub mod fuzzy;
28pub mod jvm;
29pub mod magick;
30pub mod network;
31pub mod node;
32pub mod perl;
33pub mod php;
34pub mod ruby;
35pub mod shell;
36pub mod system;
37pub mod vcs;
38pub mod wrappers;
39
40use std::collections::HashMap;
41
42use crate::parse::Token;
43use crate::verdict::Verdict;
44
45type HandlerFn = fn(&[Token]) -> Verdict;
46
47pub fn custom_cmd_handlers() -> HashMap<&'static str, HandlerFn> {
48 HashMap::from([
49 ("magick", magick::is_safe_magick as HandlerFn),
50 ("php", php::is_safe_php as HandlerFn),
51 ("sysctl", system::sysctl::is_safe_sysctl as HandlerFn),
52 ])
53}
54
55pub fn custom_sub_handlers() -> HashMap<&'static str, HandlerFn> {
56 HashMap::from([
57 ("bun_x", node::bun::check_bun_x as HandlerFn),
58 ("bundle_config", ruby::bundle::check_bundle_config as HandlerFn),
59 ("bundle_exec", ruby::bundle::check_bundle_exec as HandlerFn),
60 ("git_remote", vcs::git::check_git_remote as HandlerFn),
61 ("laravel_cache_clear", php::check_laravel_cache_clear as HandlerFn),
62 ("plutil_convert", system::plutil::check_plutil_convert as HandlerFn),
63 ])
64}
65
66pub fn dispatch(tokens: &[Token]) -> Verdict {
67 let cmd = tokens[0].command_name();
68 None
69 .or_else(|| shell::dispatch(cmd, tokens))
70 .or_else(|| wrappers::dispatch(cmd, tokens))
71 .or_else(|| forges::dispatch(cmd, tokens))
72 .or_else(|| node::dispatch(cmd, tokens))
73 .or_else(|| jvm::dispatch(cmd, tokens))
74 .or_else(|| android::dispatch(cmd, tokens))
75 .or_else(|| network::dispatch(cmd, tokens))
76 .or_else(|| system::dispatch(cmd, tokens))
77 .or_else(|| perl::dispatch(cmd, tokens))
78 .or_else(|| coreutils::dispatch(cmd, tokens))
79 .or_else(|| fuzzy::dispatch(cmd, tokens))
80 .or_else(|| vcs::dispatch(cmd, tokens))
81 .or_else(|| crate::registry::toml_dispatch(tokens))
82 .unwrap_or(Verdict::Denied)
83}
84
85pub fn handler_docs() -> Vec<crate::docs::CommandDoc> {
86 let mut docs = Vec::new();
87 docs.extend(forges::command_docs());
88 docs.extend(node::command_docs());
89 docs.extend(jvm::command_docs());
90 docs.extend(android::command_docs());
91 docs.extend(network::command_docs());
92 docs.extend(system::command_docs());
93 docs.extend(perl::command_docs());
94 docs.extend(coreutils::command_docs());
95 docs.extend(fuzzy::command_docs());
96 docs.extend(shell::command_docs());
97 docs.extend(wrappers::command_docs());
98 docs.extend(vcs::command_docs());
99 docs.extend(crate::registry::toml_command_docs());
100 docs
101}
102
103#[cfg(test)]
104#[derive(Debug)]
105pub(crate) enum CommandEntry {
106 Positional { #[allow(dead_code)] cmd: &'static str },
107 Custom { cmd: &'static str, valid_prefix: Option<&'static str> },
108 Paths { cmd: &'static str, bare_ok: bool, paths: &'static [&'static str] },
109 Delegation { #[allow(dead_code)] cmd: &'static str },
110}
111
112pub fn all_opencode_patterns() -> Vec<String> {
113 let mut patterns = Vec::new();
114 patterns.sort();
115 patterns.dedup();
116 patterns
117}
118
119#[cfg(test)]
120fn full_registry() -> Vec<&'static CommandEntry> {
121 let mut entries = Vec::new();
122 entries.extend(shell::REGISTRY);
123 entries.extend(wrappers::REGISTRY);
124 entries.extend(forges::full_registry());
125 entries.extend(node::full_registry());
126 entries.extend(jvm::full_registry());
127 entries.extend(android::full_registry());
128 entries.extend(network::REGISTRY);
129 entries.extend(system::full_registry());
130 entries.extend(perl::REGISTRY);
131 entries.extend(coreutils::full_registry());
132 entries.extend(fuzzy::full_registry());
133 entries
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 const UNKNOWN_FLAG: &str = "--xyzzy-unknown-42";
141 const UNKNOWN_SUB: &str = "xyzzy-unknown-42";
142
143 fn check_entry(entry: &CommandEntry, failures: &mut Vec<String>) {
144 match entry {
145 CommandEntry::Positional { .. } | CommandEntry::Delegation { .. } => {}
146 CommandEntry::Custom { cmd, valid_prefix } => {
147 let base = valid_prefix.unwrap_or(cmd);
148 let test = format!("{base} {UNKNOWN_FLAG}");
149 if crate::is_safe_command(&test) {
150 failures.push(format!("{cmd}: accepted unknown flag: {test}"));
151 }
152 }
153 CommandEntry::Paths { cmd, bare_ok, paths } => {
154 if !bare_ok && crate::is_safe_command(cmd) {
155 failures.push(format!("{cmd}: accepted bare invocation"));
156 }
157 let test = format!("{cmd} {UNKNOWN_SUB}");
158 if crate::is_safe_command(&test) {
159 failures.push(format!("{cmd}: accepted unknown subcommand: {test}"));
160 }
161 for path in *paths {
162 let test = format!("{path} {UNKNOWN_FLAG}");
163 if crate::is_safe_command(&test) {
164 failures.push(format!("{path}: accepted unknown flag: {test}"));
165 }
166 }
167 }
168 }
169 }
170
171 #[test]
172 fn all_commands_reject_unknown() {
173 let registry = full_registry();
174 let mut failures = Vec::new();
175 for entry in ®istry {
176 check_entry(entry, &mut failures);
177 }
178 assert!(
179 failures.is_empty(),
180 "unknown flags/subcommands accepted:\n{}",
181 failures.join("\n")
182 );
183 }
184
185 #[test]
186 fn process_substitution_safe_inner() {
187 let safe = ["echo <(cat /etc/passwd)", "grep pattern <(ls)", "diff <(sort a.txt) <(sort b.txt)", "comm -23 file.txt <(sort other.txt)"];
188 for cmd in &safe {
189 assert!(crate::is_safe_command(cmd), "safe process substitution rejected: {cmd}");
190 }
191 }
192
193 #[test]
194 fn process_substitution_unsafe_inner() {
195 let unsafe_cmds = ["echo >(rm -rf /)", "diff <(sort a.txt) <(rm -rf /)"];
196 for cmd in &unsafe_cmds {
197 assert!(!crate::is_safe_command(cmd), "unsafe process substitution approved: {cmd}");
198 }
199 }
200
201}