command_block_test.go 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package shell
  2. import (
  3. "strings"
  4. "testing"
  5. "github.com/stretchr/testify/require"
  6. )
  7. func TestCommandBlocking(t *testing.T) {
  8. tests := []struct {
  9. name string
  10. blockFuncs []BlockFunc
  11. command string
  12. shouldBlock bool
  13. }{
  14. {
  15. name: "block simple command",
  16. blockFuncs: []BlockFunc{
  17. func(args []string) bool {
  18. return len(args) > 0 && args[0] == "curl"
  19. },
  20. },
  21. command: "curl https://example.com",
  22. shouldBlock: true,
  23. },
  24. {
  25. name: "allow non-blocked command",
  26. blockFuncs: []BlockFunc{
  27. func(args []string) bool {
  28. return len(args) > 0 && args[0] == "curl"
  29. },
  30. },
  31. command: "echo hello",
  32. shouldBlock: false,
  33. },
  34. {
  35. name: "block subcommand",
  36. blockFuncs: []BlockFunc{
  37. func(args []string) bool {
  38. return len(args) >= 2 && args[0] == "brew" && args[1] == "install"
  39. },
  40. },
  41. command: "brew install wget",
  42. shouldBlock: true,
  43. },
  44. {
  45. name: "allow different subcommand",
  46. blockFuncs: []BlockFunc{
  47. func(args []string) bool {
  48. return len(args) >= 2 && args[0] == "brew" && args[1] == "install"
  49. },
  50. },
  51. command: "brew list",
  52. shouldBlock: false,
  53. },
  54. {
  55. name: "block npm global install with -g",
  56. blockFuncs: []BlockFunc{
  57. ArgumentsBlocker("npm", []string{"install"}, []string{"-g"}),
  58. },
  59. command: "npm install -g typescript",
  60. shouldBlock: true,
  61. },
  62. {
  63. name: "block npm global install with --global",
  64. blockFuncs: []BlockFunc{
  65. ArgumentsBlocker("npm", []string{"install"}, []string{"--global"}),
  66. },
  67. command: "npm install --global typescript",
  68. shouldBlock: true,
  69. },
  70. {
  71. name: "allow npm local install",
  72. blockFuncs: []BlockFunc{
  73. ArgumentsBlocker("npm", []string{"install"}, []string{"-g"}),
  74. ArgumentsBlocker("npm", []string{"install"}, []string{"--global"}),
  75. },
  76. command: "npm install typescript",
  77. shouldBlock: false,
  78. },
  79. }
  80. for _, tt := range tests {
  81. t.Run(tt.name, func(t *testing.T) {
  82. // Create a temporary directory for each test
  83. tmpDir := t.TempDir()
  84. shell := NewShell(&Options{
  85. WorkingDir: tmpDir,
  86. BlockFuncs: tt.blockFuncs,
  87. })
  88. _, _, err := shell.Exec(t.Context(), tt.command)
  89. if tt.shouldBlock {
  90. if err == nil {
  91. t.Errorf("Expected command to be blocked, but it was allowed")
  92. } else if !strings.Contains(err.Error(), "not allowed for security reasons") {
  93. t.Errorf("Expected security error, got: %v", err)
  94. }
  95. } else {
  96. // For non-blocked commands, we might get other errors (like command not found)
  97. // but we shouldn't get the security error
  98. if err != nil && strings.Contains(err.Error(), "not allowed for security reasons") {
  99. t.Errorf("Command was unexpectedly blocked: %v", err)
  100. }
  101. }
  102. })
  103. }
  104. }
  105. func TestArgumentsBlocker(t *testing.T) {
  106. tests := []struct {
  107. name string
  108. cmd string
  109. args []string
  110. flags []string
  111. input []string
  112. shouldBlock bool
  113. }{
  114. // Basic command blocking
  115. {
  116. name: "block exact command match",
  117. cmd: "npm",
  118. args: []string{"install"},
  119. flags: nil,
  120. input: []string{"npm", "install", "package"},
  121. shouldBlock: true,
  122. },
  123. {
  124. name: "allow different command",
  125. cmd: "npm",
  126. args: []string{"install"},
  127. flags: nil,
  128. input: []string{"yarn", "install", "package"},
  129. shouldBlock: false,
  130. },
  131. {
  132. name: "allow different subcommand",
  133. cmd: "npm",
  134. args: []string{"install"},
  135. flags: nil,
  136. input: []string{"npm", "list"},
  137. shouldBlock: false,
  138. },
  139. // Flag-based blocking
  140. {
  141. name: "block with single flag",
  142. cmd: "npm",
  143. args: []string{"install"},
  144. flags: []string{"-g"},
  145. input: []string{"npm", "install", "-g", "typescript"},
  146. shouldBlock: true,
  147. },
  148. {
  149. name: "block with flag in different position",
  150. cmd: "npm",
  151. args: []string{"install"},
  152. flags: []string{"-g"},
  153. input: []string{"npm", "install", "typescript", "-g"},
  154. shouldBlock: true,
  155. },
  156. {
  157. name: "allow without required flag",
  158. cmd: "npm",
  159. args: []string{"install"},
  160. flags: []string{"-g"},
  161. input: []string{"npm", "install", "typescript"},
  162. shouldBlock: false,
  163. },
  164. {
  165. name: "block with multiple flags",
  166. cmd: "pip",
  167. args: []string{"install"},
  168. flags: []string{"--user"},
  169. input: []string{"pip", "install", "--user", "--upgrade", "package"},
  170. shouldBlock: true,
  171. },
  172. // Complex argument patterns
  173. {
  174. name: "block multi-arg subcommand",
  175. cmd: "yarn",
  176. args: []string{"global", "add"},
  177. flags: nil,
  178. input: []string{"yarn", "global", "add", "typescript"},
  179. shouldBlock: true,
  180. },
  181. {
  182. name: "allow partial multi-arg match",
  183. cmd: "yarn",
  184. args: []string{"global", "add"},
  185. flags: nil,
  186. input: []string{"yarn", "global", "list"},
  187. shouldBlock: false,
  188. },
  189. // Edge cases
  190. {
  191. name: "handle empty input",
  192. cmd: "npm",
  193. args: []string{"install"},
  194. flags: nil,
  195. input: []string{},
  196. shouldBlock: false,
  197. },
  198. {
  199. name: "handle command only",
  200. cmd: "npm",
  201. args: []string{"install"},
  202. flags: nil,
  203. input: []string{"npm"},
  204. shouldBlock: false,
  205. },
  206. {
  207. name: "block pacman with -S flag",
  208. cmd: "pacman",
  209. args: nil,
  210. flags: []string{"-S"},
  211. input: []string{"pacman", "-S", "package"},
  212. shouldBlock: true,
  213. },
  214. {
  215. name: "allow pacman without -S flag",
  216. cmd: "pacman",
  217. args: nil,
  218. flags: []string{"-S"},
  219. input: []string{"pacman", "-Q", "package"},
  220. shouldBlock: false,
  221. },
  222. // `go test -exec`
  223. {
  224. name: "go test exec",
  225. cmd: "go",
  226. args: []string{"test"},
  227. flags: []string{"-exec"},
  228. input: []string{"go", "test", "-exec", "bash -c 'echo hello'"},
  229. shouldBlock: true,
  230. },
  231. {
  232. name: "go test exec",
  233. cmd: "go",
  234. args: []string{"test"},
  235. flags: []string{"-exec"},
  236. input: []string{"go", "test", `-exec="bash -c 'echo hello'"`},
  237. shouldBlock: true,
  238. },
  239. }
  240. for _, tt := range tests {
  241. t.Run(tt.name, func(t *testing.T) {
  242. blocker := ArgumentsBlocker(tt.cmd, tt.args, tt.flags)
  243. result := blocker(tt.input)
  244. require.Equal(t, tt.shouldBlock, result,
  245. "Expected block=%v for input %v", tt.shouldBlock, tt.input)
  246. })
  247. }
  248. }
  249. func TestCommandsBlocker(t *testing.T) {
  250. tests := []struct {
  251. name string
  252. banned []string
  253. input []string
  254. shouldBlock bool
  255. }{
  256. {
  257. name: "block single banned command",
  258. banned: []string{"curl"},
  259. input: []string{"curl", "https://example.com"},
  260. shouldBlock: true,
  261. },
  262. {
  263. name: "allow non-banned command",
  264. banned: []string{"curl", "wget"},
  265. input: []string{"echo", "hello"},
  266. shouldBlock: false,
  267. },
  268. {
  269. name: "block from multiple banned",
  270. banned: []string{"curl", "wget", "nc"},
  271. input: []string{"wget", "https://example.com"},
  272. shouldBlock: true,
  273. },
  274. {
  275. name: "handle empty input",
  276. banned: []string{"curl"},
  277. input: []string{},
  278. shouldBlock: false,
  279. },
  280. {
  281. name: "case sensitive matching",
  282. banned: []string{"curl"},
  283. input: []string{"CURL", "https://example.com"},
  284. shouldBlock: false,
  285. },
  286. }
  287. for _, tt := range tests {
  288. t.Run(tt.name, func(t *testing.T) {
  289. blocker := CommandsBlocker(tt.banned)
  290. result := blocker(tt.input)
  291. require.Equal(t, tt.shouldBlock, result,
  292. "Expected block=%v for input %v", tt.shouldBlock, tt.input)
  293. })
  294. }
  295. }
  296. func TestSplitArgsFlags(t *testing.T) {
  297. tests := []struct {
  298. name string
  299. input []string
  300. wantArgs []string
  301. wantFlags []string
  302. }{
  303. {
  304. name: "only args",
  305. input: []string{"install", "package", "another"},
  306. wantArgs: []string{"install", "package", "another"},
  307. wantFlags: []string{},
  308. },
  309. {
  310. name: "only flags",
  311. input: []string{"-g", "--verbose", "-f"},
  312. wantArgs: []string{},
  313. wantFlags: []string{"-g", "--verbose", "-f"},
  314. },
  315. {
  316. name: "mixed args and flags",
  317. input: []string{"install", "-g", "package", "--verbose"},
  318. wantArgs: []string{"install", "package"},
  319. wantFlags: []string{"-g", "--verbose"},
  320. },
  321. {
  322. name: "empty input",
  323. input: []string{},
  324. wantArgs: []string{},
  325. wantFlags: []string{},
  326. },
  327. {
  328. name: "single dash flag",
  329. input: []string{"-S", "package"},
  330. wantArgs: []string{"package"},
  331. wantFlags: []string{"-S"},
  332. },
  333. {
  334. name: "flag with equals sign",
  335. input: []string{"-exec=bash", "package"},
  336. wantArgs: []string{"package"},
  337. wantFlags: []string{"-exec"},
  338. },
  339. {
  340. name: "long flag with equals sign",
  341. input: []string{"--config=/path/to/config", "run"},
  342. wantArgs: []string{"run"},
  343. wantFlags: []string{"--config"},
  344. },
  345. {
  346. name: "flag with complex value",
  347. input: []string{`-exec="bash -c 'echo hello'"`, "test"},
  348. wantArgs: []string{"test"},
  349. wantFlags: []string{"-exec"},
  350. },
  351. }
  352. for _, tt := range tests {
  353. t.Run(tt.name, func(t *testing.T) {
  354. args, flags := splitArgsFlags(tt.input)
  355. require.Equal(t, tt.wantArgs, args, "args mismatch")
  356. require.Equal(t, tt.wantFlags, flags, "flags mismatch")
  357. })
  358. }
  359. }