command_block_test.go 9.4 KB

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