arguments_test.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167
  1. package executor
  2. import (
  3. "fmt"
  4. "strings"
  5. config "github.com/OliveTin/OliveTin/internal/config"
  6. "github.com/OliveTin/OliveTin/internal/entities"
  7. "github.com/OliveTin/OliveTin/internal/tpl"
  8. log "github.com/sirupsen/logrus"
  9. "testing"
  10. "github.com/stretchr/testify/assert"
  11. )
  12. func TestSanitizeUnsafe(t *testing.T) {
  13. assert.Nil(t, TypeSafetyCheck("", "_zomg_ c:/ haxxor ' bobby tables && rm -rf ", "very_dangerous_raw_string"))
  14. }
  15. func TestSanitizeUnimplemented(t *testing.T) {
  16. err := TypeSafetyCheck("", "I am a happy little argument", "greeting_type")
  17. assert.NotNil(t, err, "Test an argument type that does not exist")
  18. }
  19. func TestValidateArgumentCheckboxDefaultValues(t *testing.T) {
  20. arg := config.ActionArgument{
  21. Name: "confirm",
  22. Type: "checkbox",
  23. }
  24. action := config.Action{
  25. Title: "Test checkbox default values",
  26. }
  27. // Default checkbox values without choices should accept "1" and "0"
  28. err := ValidateArgument(&arg, "1", &action)
  29. assert.Nil(t, err, "Expected checkbox value \"1\" to be accepted without choices")
  30. err = ValidateArgument(&arg, "0", &action)
  31. assert.Nil(t, err, "Expected checkbox value \"0\" to be accepted without choices")
  32. }
  33. func TestMangleCheckboxValueWithChoices(t *testing.T) {
  34. log.SetLevel(log.PanicLevel)
  35. arg := config.ActionArgument{
  36. Name: "confirm",
  37. Type: "checkbox",
  38. Choices: []config.ActionArgumentChoice{
  39. {Title: "Enabled", Value: "on"},
  40. {Title: "Disabled", Value: "off"},
  41. },
  42. }
  43. // When the incoming value matches a choice title, it should be mapped to the choice value
  44. out := mangleCheckboxValue(&arg, "Enabled", "Test action")
  45. assert.Equal(t, "on", out, "Expected checkbox title to be mangled to its value")
  46. out = mangleCheckboxValue(&arg, "Disabled", "Test action")
  47. assert.Equal(t, "off", out, "Expected checkbox title to be mangled to its value")
  48. // When there is no matching title, the value should be returned unchanged
  49. out = mangleCheckboxValue(&arg, "something-else", "Test action")
  50. assert.Equal(t, "something-else", out, "Expected non-matching value to be returned unchanged")
  51. }
  52. func TestMangleArgumentValueCheckbox(t *testing.T) {
  53. log.SetLevel(log.PanicLevel)
  54. arg := config.ActionArgument{
  55. Name: "confirm",
  56. Type: "checkbox",
  57. Choices: []config.ActionArgumentChoice{
  58. {Title: "Yes", Value: "true-value"},
  59. {Title: "No", Value: "false-value"},
  60. },
  61. }
  62. out := MangleArgumentValue(&arg, "Yes", "Test action")
  63. assert.Equal(t, "true-value", out, "Expected MangleArgumentValue to delegate to mangleCheckboxValue for checkbox types")
  64. out = MangleArgumentValue(&arg, "No", "Test action")
  65. assert.Equal(t, "false-value", out)
  66. // For non-matching values, it should return the original value
  67. out = MangleArgumentValue(&arg, "maybe", "Test action")
  68. assert.Equal(t, "maybe", out)
  69. }
  70. func TestValidateArgumentCheckboxWithChoices(t *testing.T) {
  71. log.SetLevel(log.PanicLevel)
  72. arg := config.ActionArgument{
  73. Name: "confirm",
  74. Type: "checkbox",
  75. Choices: []config.ActionArgumentChoice{
  76. {Title: "Enabled", Value: "on"},
  77. {Title: "Disabled", Value: "off"},
  78. },
  79. }
  80. action := config.Action{
  81. Title: "Test checkbox with choices",
  82. }
  83. // Titles should be accepted once mangled to their values
  84. err := ValidateArgument(&arg, "Enabled", &action)
  85. assert.Nil(t, err, "Expected checkbox title \"Enabled\" to be accepted after mangling to choice value")
  86. err = ValidateArgument(&arg, "Disabled", &action)
  87. assert.Nil(t, err, "Expected checkbox title \"Disabled\" to be accepted after mangling to choice value")
  88. // Unknown titles should be rejected because they do not match any choice value
  89. err = ValidateArgument(&arg, "Maybe", &action)
  90. assert.NotNil(t, err, "Expected unknown checkbox title to be rejected against choices")
  91. }
  92. func checklistTestArg() config.ActionArgument {
  93. return config.ActionArgument{
  94. Name: "directories",
  95. Type: "checklist",
  96. Choices: []config.ActionArgumentChoice{
  97. {Title: "Documents", Value: "documents"},
  98. {Title: "Photos", Value: "photos"},
  99. {Title: "Music", Value: "music"},
  100. },
  101. }
  102. }
  103. func TestValidateArgumentChecklistSelections(t *testing.T) {
  104. log.SetLevel(log.PanicLevel)
  105. arg := checklistTestArg()
  106. action := config.Action{Title: "Test checklist"}
  107. err := ValidateArgument(&arg, "documents", &action)
  108. assert.Nil(t, err)
  109. err = ValidateArgument(&arg, `["documents","photos"]`, &action)
  110. assert.Nil(t, err)
  111. err = ValidateArgument(&arg, `["documents","unknown"]`, &action)
  112. assert.NotNil(t, err)
  113. }
  114. func TestValidateArgumentChecklistTitleMangling(t *testing.T) {
  115. log.SetLevel(log.PanicLevel)
  116. arg := checklistTestArg()
  117. action := config.Action{Title: "Test checklist title mangling"}
  118. err := ValidateArgument(&arg, `["Documents","Photos"]`, &action)
  119. assert.Nil(t, err)
  120. }
  121. func TestValidateArgumentChecklistEmptySelection(t *testing.T) {
  122. log.SetLevel(log.PanicLevel)
  123. arg := checklistTestArg()
  124. action := config.Action{Title: "Test checklist empty"}
  125. err := ValidateArgument(&arg, "", &action)
  126. assert.Nil(t, err)
  127. arg.RejectNull = true
  128. err = ValidateArgument(&arg, "", &action)
  129. assert.NotNil(t, err)
  130. }
  131. func TestValidateArgumentChecklistWithoutChoices(t *testing.T) {
  132. log.SetLevel(log.PanicLevel)
  133. arg := config.ActionArgument{
  134. Name: "directories",
  135. Type: "checklist",
  136. }
  137. action := config.Action{Title: "Test checklist without choices"}
  138. err := ValidateArgument(&arg, "documents", &action)
  139. assert.NotNil(t, err)
  140. }
  141. func TestValidateArgumentChecklistRejectsEmptySegment(t *testing.T) {
  142. log.SetLevel(log.PanicLevel)
  143. arg := checklistTestArg()
  144. action := config.Action{Title: "Test checklist empty segment"}
  145. err := ValidateArgument(&arg, `["documents","","photos"]`, &action)
  146. assert.NotNil(t, err)
  147. }
  148. func TestMangleArgumentValueChecklist(t *testing.T) {
  149. log.SetLevel(log.PanicLevel)
  150. arg := checklistTestArg()
  151. out := MangleArgumentValue(&arg, `["Documents","Music"]`, "Test action")
  152. assert.Equal(t, `["documents","music"]`, out)
  153. out = MangleArgumentValue(&arg, `["documents","photos"]`, "Test action")
  154. assert.Equal(t, `["documents","photos"]`, out)
  155. }
  156. func checklistEntityTestArg() config.ActionArgument {
  157. return config.ActionArgument{
  158. Name: "rooms",
  159. Type: "checklist",
  160. Entity: "room",
  161. Choices: []config.ActionArgumentChoice{
  162. {Title: "{{ room.hostname }}", Value: "{{ room.hostname }}"},
  163. },
  164. }
  165. }
  166. func TestValidateArgumentChecklistEntitySelections(t *testing.T) {
  167. log.SetLevel(log.PanicLevel)
  168. entities.AddEntity("room", "0", map[string]any{"hostname": "attic"})
  169. entities.AddEntity("room", "1", map[string]any{"hostname": "basement"})
  170. arg := checklistEntityTestArg()
  171. action := config.Action{Title: "Test checklist entity"}
  172. err := ValidateArgument(&arg, "attic", &action)
  173. assert.Nil(t, err)
  174. err = ValidateArgument(&arg, `["attic","basement"]`, &action)
  175. assert.Nil(t, err)
  176. err = ValidateArgument(&arg, `["attic","unknown"]`, &action)
  177. assert.NotNil(t, err)
  178. }
  179. func TestMangleArgumentValueChecklistEntityTitles(t *testing.T) {
  180. log.SetLevel(log.PanicLevel)
  181. entities.AddEntity("room", "0", map[string]any{"hostname": "attic"})
  182. entities.AddEntity("room", "1", map[string]any{"hostname": "basement"})
  183. arg := config.ActionArgument{
  184. Name: "rooms",
  185. Type: "checklist",
  186. Entity: "room",
  187. Choices: []config.ActionArgumentChoice{
  188. {Title: "{{ room.hostname }} room", Value: "{{ room.hostname }}"},
  189. },
  190. }
  191. out := MangleArgumentValue(&arg, `["attic room","basement room"]`, "Test checklist entity titles")
  192. assert.Equal(t, `["attic","basement"]`, out)
  193. }
  194. func TestParseActionArgumentsChecklistEmptySelection(t *testing.T) {
  195. req := newExecRequest()
  196. req.Binding.Action = &config.Action{
  197. Title: "Test checklist empty selection",
  198. Shell: "echo 'Selected segments: {{ segments }}'",
  199. Arguments: []config.ActionArgument{
  200. {
  201. Name: "segments",
  202. Type: "checklist",
  203. Choices: []config.ActionArgumentChoice{
  204. {Value: "kitchen"},
  205. {Value: "bedroom"},
  206. },
  207. },
  208. },
  209. }
  210. req.Arguments = map[string]string{
  211. "segments": "",
  212. }
  213. mangleInvalidArgumentValues(req)
  214. out, err := parseActionArguments(req)
  215. assert.Nil(t, err)
  216. assert.Equal(t, "echo 'Selected segments: '", out)
  217. }
  218. func newExecRequest() *ExecutionRequest {
  219. return &ExecutionRequest{
  220. Arguments: make(map[string]string),
  221. Binding: &ActionBinding{
  222. Action: &config.Action{},
  223. },
  224. }
  225. }
  226. func TestArgumentValueNullable(t *testing.T) {
  227. req := newExecRequest()
  228. req.Binding.Action = &config.Action{
  229. Title: "Release the hounds",
  230. Shell: "echo 'Releasing {{ count }} hounds'",
  231. Arguments: []config.ActionArgument{
  232. {
  233. Name: "count",
  234. Type: "int",
  235. RejectNull: false,
  236. },
  237. },
  238. }
  239. req.Arguments = map[string]string{
  240. "count": "",
  241. }
  242. out, err := parseActionArguments(req)
  243. assert.Equal(t, "echo 'Releasing hounds'", out)
  244. assert.Nil(t, err)
  245. req.Binding.Action.Arguments[0].RejectNull = true
  246. _, err = parseActionArguments(req)
  247. assert.NotNil(t, err)
  248. }
  249. func TestArgumentNameNumbers(t *testing.T) {
  250. req := newExecRequest()
  251. req.Binding.Action = &config.Action{
  252. Title: "Do some tickles",
  253. Shell: "echo 'Tickling {{ person1name }}'",
  254. Arguments: []config.ActionArgument{
  255. {
  256. Name: "person1name",
  257. Type: "ascii",
  258. },
  259. },
  260. }
  261. req.Arguments = map[string]string{
  262. "person1name": "Fred",
  263. }
  264. out, err := parseActionArguments(req)
  265. assert.Equal(t, "echo 'Tickling Fred'", out)
  266. assert.Nil(t, err)
  267. }
  268. func TestArgumentNotProvided(t *testing.T) {
  269. req := newExecRequest()
  270. req.Binding.Action = &config.Action{
  271. Title: "Do some tickles",
  272. Shell: "echo 'Tickling {{ personName }}'",
  273. Arguments: []config.ActionArgument{
  274. {
  275. Name: "person",
  276. Type: "ascii",
  277. },
  278. },
  279. }
  280. req.Arguments = map[string]string{}
  281. out, err := parseActionArguments(req)
  282. assert.Equal(t, "", out)
  283. assert.Equal(t, err.Error(), "required arg not provided: personName")
  284. }
  285. func TestExecArrayParsing(t *testing.T) {
  286. req := newExecRequest()
  287. req.Binding.Action = &config.Action{
  288. Title: "List files",
  289. Exec: []string{"ls", "-alh"},
  290. Arguments: []config.ActionArgument{},
  291. }
  292. req.Arguments = map[string]string{}
  293. out, err := parseActionExec(req.Arguments, req.Binding.Action, req.Binding.Entity)
  294. assert.Nil(t, err)
  295. assert.Equal(t, []string{"ls", "-alh"}, out)
  296. }
  297. func TestExecArrayWithTemplateReplacement(t *testing.T) {
  298. a1 := config.Action{
  299. Title: "List specific path",
  300. Exec: []string{"ls", "-alh", "{{path}}"},
  301. Arguments: []config.ActionArgument{
  302. {
  303. Name: "path",
  304. Type: "ascii_identifier",
  305. },
  306. },
  307. }
  308. values := map[string]string{
  309. "path": "tmp",
  310. }
  311. out, err := parseActionExec(values, &a1, nil)
  312. assert.Nil(t, err)
  313. assert.Equal(t, []string{"ls", "-alh", "tmp"}, out)
  314. }
  315. func TestCheckShellArgumentSafetyWithURL(t *testing.T) {
  316. a1 := config.Action{
  317. Title: "Download file",
  318. Shell: "curl {{url}}",
  319. Arguments: []config.ActionArgument{
  320. {
  321. Name: "url",
  322. Type: "url",
  323. },
  324. },
  325. }
  326. err := checkShellArgumentSafety(&a1)
  327. assert.NotNil(t, err)
  328. assert.Contains(t, err.Error(), "unsafe argument type 'url' cannot be used with Shell execution")
  329. assert.Contains(t, err.Error(), "https://docs.olivetin.app/action_execution/shellvsexec.html")
  330. }
  331. func TestCheckShellArgumentSafetyWithEmail(t *testing.T) {
  332. a1 := config.Action{
  333. Title: "Send email",
  334. Shell: "sendmail {{email}}",
  335. Arguments: []config.ActionArgument{
  336. {
  337. Name: "email",
  338. Type: "email",
  339. },
  340. },
  341. }
  342. err := checkShellArgumentSafety(&a1)
  343. assert.NotNil(t, err)
  344. assert.Contains(t, err.Error(), "unsafe argument type 'email' cannot be used with Shell execution")
  345. }
  346. func TestCheckShellArgumentSafetyWithExec(t *testing.T) {
  347. a1 := config.Action{
  348. Title: "Download file",
  349. Exec: []string{"curl", "{{url}}"},
  350. Arguments: []config.ActionArgument{
  351. {
  352. Name: "url",
  353. Type: "url",
  354. },
  355. },
  356. }
  357. err := checkShellArgumentSafety(&a1)
  358. assert.Nil(t, err)
  359. }
  360. func TestCheckShellArgumentSafetyWithSafeTypes(t *testing.T) {
  361. a1 := config.Action{
  362. Title: "List files",
  363. Shell: "ls {{path}}",
  364. Arguments: []config.ActionArgument{
  365. {
  366. Name: "path",
  367. Type: "ascii_identifier",
  368. },
  369. },
  370. }
  371. err := checkShellArgumentSafety(&a1)
  372. assert.Nil(t, err)
  373. }
  374. func TestCheckShellArgumentSafetyWithPassword(t *testing.T) {
  375. a1 := config.Action{
  376. Title: "Auth with password",
  377. Shell: "somecommand --password '{{password}}'",
  378. Arguments: []config.ActionArgument{
  379. {
  380. Name: "password",
  381. Type: "password",
  382. },
  383. },
  384. }
  385. err := checkShellArgumentSafety(&a1)
  386. assert.NotNil(t, err)
  387. assert.Contains(t, err.Error(), "unsafe argument type 'password' cannot be used with Shell execution")
  388. assert.Contains(t, err.Error(), "https://docs.olivetin.app/action_execution/shellvsexec.html")
  389. }
  390. func TestCheckShellArgumentSafetyWithPasswordAndExec(t *testing.T) {
  391. a1 := config.Action{
  392. Title: "Auth with password via exec",
  393. Exec: []string{"somecommand", "--password", "{{password}}"},
  394. Arguments: []config.ActionArgument{
  395. {
  396. Name: "password",
  397. Type: "password",
  398. },
  399. },
  400. }
  401. err := checkShellArgumentSafety(&a1)
  402. assert.Nil(t, err)
  403. }
  404. func TestCheckShellArgumentSafetyWithHTML(t *testing.T) {
  405. a1 := config.Action{
  406. Title: "HTML shell",
  407. Shell: "echo {{ body }}",
  408. Arguments: []config.ActionArgument{
  409. {Name: "body", Type: "html"},
  410. },
  411. }
  412. err := checkShellArgumentSafety(&a1)
  413. assert.NotNil(t, err)
  414. assert.Contains(t, err.Error(), "unsafe argument type 'html'")
  415. }
  416. func TestCheckShellArgumentSafetyWithConfirmation(t *testing.T) {
  417. a1 := config.Action{
  418. Title: "Confirm shell",
  419. Shell: "echo ok",
  420. Arguments: []config.ActionArgument{
  421. {Name: "agree", Type: "confirmation"},
  422. },
  423. }
  424. err := checkShellArgumentSafety(&a1)
  425. assert.NotNil(t, err)
  426. assert.Contains(t, err.Error(), "unsafe argument type 'confirmation'")
  427. }
  428. func TestCheckShellArgumentSafetyWithChoicelessCheckbox(t *testing.T) {
  429. a1 := config.Action{
  430. Title: "Checkbox shell",
  431. Shell: "echo {{ flag }}",
  432. Arguments: []config.ActionArgument{
  433. {Name: "flag", Type: "checkbox"},
  434. },
  435. }
  436. err := checkShellArgumentSafety(&a1)
  437. assert.NotNil(t, err)
  438. assert.Contains(t, err.Error(), "unsafe argument type 'checkbox'")
  439. }
  440. func TestCheckShellArgumentSafetyWithCustomRegex(t *testing.T) {
  441. a1 := config.Action{
  442. Title: "Regex shell",
  443. Shell: "curl {{ host }}",
  444. Arguments: []config.ActionArgument{
  445. {Name: "host", Type: "regex:[a-zA-Z0-9.-]+"},
  446. },
  447. }
  448. err := checkShellArgumentSafety(&a1)
  449. assert.NotNil(t, err)
  450. assert.Contains(t, err.Error(), "unsafe argument type 'regex:[a-zA-Z0-9.-]+'")
  451. }
  452. func TestTypeSafetyCheckUrl(t *testing.T) {
  453. assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
  454. assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
  455. assert.Nil(t, TypeSafetyCheck("test3", "http://localhost:80?foo=bar", "url"), "Test URL: localhost with query arguments")
  456. assert.Nil(t, TypeSafetyCheck("test7", "https://example.com/path", "url"), "Test URL: https scheme")
  457. assert.NotNil(t, TypeSafetyCheck("test4", "http://lo host:80", "url"), "Test a badly formed URL")
  458. assert.NotNil(t, TypeSafetyCheck("test5", "12345", "url"), "Test a badly formed URL")
  459. assert.NotNil(t, TypeSafetyCheck("test6", "_!23;", "url"), "Test a badly formed URL")
  460. assert.NotNil(t, TypeSafetyCheck("test8", "file:///etc/passwd", "url"), "file:// scheme must be rejected")
  461. assert.NotNil(t, TypeSafetyCheck("test9", "gopher://example.com", "url"), "gopher:// scheme must be rejected")
  462. }
  463. func TestTypeSafetyCheckRegex(t *testing.T) {
  464. tests := []struct {
  465. name string
  466. field string
  467. pattern string
  468. value string
  469. hasError bool
  470. }{
  471. {
  472. name: "Issue #578 - Domain",
  473. field: "domain",
  474. pattern: "regex:^(?:[a-zA-Z0-9-]{1,63}.)+[a-zA-Z]{2,63}$",
  475. value: "immich.example.dev",
  476. hasError: false,
  477. },
  478. {
  479. name: "Don't allow numbers in username",
  480. field: "Username",
  481. pattern: "regex:^[a-zA-Z]$",
  482. value: "James1234",
  483. hasError: true,
  484. },
  485. {
  486. name: "GHSA-gvxq - reject partial regex match",
  487. field: "host",
  488. pattern: "regex:[a-zA-Z0-9.-]+",
  489. value: "example.com; id",
  490. hasError: true,
  491. },
  492. {
  493. name: "reject alternation bypass when pattern looks anchored",
  494. field: "host",
  495. pattern: "regex:^safe$|bad",
  496. value: "xxxbad",
  497. hasError: true,
  498. },
  499. }
  500. for _, tt := range tests {
  501. t.Run(tt.name, func(t *testing.T) {
  502. err := typeSafetyCheckRegex(tt.field, tt.value, tt.pattern)
  503. if tt.hasError {
  504. assert.NotNil(t, err, "Expected error for value %s with pattern %s, but got no error", tt.value, tt.pattern)
  505. } else {
  506. assert.Nil(t, err, "Expected no error for value %s with pattern %s, but got error: %v", tt.value, tt.pattern, err)
  507. }
  508. })
  509. }
  510. }
  511. func TestRedactShellCommand(t *testing.T) {
  512. cmd := "echo 'The password for Fred is toomanysecrets'"
  513. args := []config.ActionArgument{
  514. {
  515. Name: "personName",
  516. Type: "ascii",
  517. },
  518. {
  519. Name: "password",
  520. Type: "password",
  521. },
  522. }
  523. values := map[string]string{
  524. "personName": "Fred",
  525. "password": "toomanysecrets",
  526. }
  527. res := redactShellCommand(cmd, args, values)
  528. assert.Equal(t, "echo 'The password for Fred is <redacted>'", res, "Redacted shell command should mask the password argument")
  529. // Test with empty password
  530. values["password"] = ""
  531. res = redactShellCommand(cmd, args, values)
  532. assert.Equal(t, cmd, res, "Empty password should not change the command")
  533. // Test with missing password argument
  534. delete(values, "password")
  535. res = redactShellCommand(cmd, args, values)
  536. assert.Equal(t, cmd, res, "Missing password argument should not change the command")
  537. }
  538. func TestTypeSafetyCheckEmail(t *testing.T) {
  539. tests := []struct {
  540. name string
  541. field string
  542. value string
  543. hasError bool
  544. }{
  545. {"Valid simple email", "email", "user@example.com", false},
  546. {"Valid email with subdomain", "email", "user@mail.example.com", false},
  547. {"Valid email with plus", "email", "user+test@example.com", false},
  548. {"Valid email with dash", "email", "user-name@example.com", false},
  549. {"Valid email with numbers", "email", "user123@example123.com", false},
  550. {"Invalid email no @", "email", "userexample.com", true},
  551. {"Invalid email no domain", "email", "user@", true},
  552. {"Invalid email no user", "email", "@example.com", true},
  553. {"Invalid email spaces", "email", "user name@example.com", true},
  554. {"Invalid email double @", "email", "user@@example.com", true},
  555. }
  556. for _, tt := range tests {
  557. t.Run(tt.name, func(t *testing.T) {
  558. err := TypeSafetyCheck(tt.field, tt.value, "email")
  559. if tt.hasError {
  560. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  561. } else {
  562. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  563. }
  564. })
  565. }
  566. }
  567. func TestTypeSafetyCheckDatetime(t *testing.T) {
  568. tests := []struct {
  569. name string
  570. field string
  571. value string
  572. hasError bool
  573. }{
  574. {"Valid datetime", "datetime", "2023-12-25T15:30:45", false},
  575. {"Valid datetime morning", "datetime", "2023-01-01T00:00:00", false},
  576. {"Valid datetime evening", "datetime", "2023-12-31T23:59:59", false},
  577. {"Invalid format missing T", "datetime", "2023-12-25 15:30:45", true},
  578. {"Invalid format missing seconds", "datetime", "2023-12-25T15:30", true},
  579. {"Invalid date", "datetime", "2023-13-25T15:30:45", true},
  580. {"Invalid time", "datetime", "2023-12-25T25:30:45", true},
  581. {"Random string", "datetime", "not-a-date", true},
  582. }
  583. for _, tt := range tests {
  584. t.Run(tt.name, func(t *testing.T) {
  585. err := TypeSafetyCheck(tt.field, tt.value, "datetime")
  586. if tt.hasError {
  587. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  588. } else {
  589. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  590. }
  591. })
  592. }
  593. }
  594. func TestTypeSafetyCheckRawStringMultiline(t *testing.T) {
  595. tests := []struct {
  596. name string
  597. field string
  598. value string
  599. }{
  600. {"Simple string", "content", "hello world"},
  601. {"Multiline string", "content", "line1\nline2\nline3"},
  602. {"String with special chars", "content", "!@#$%^&*()"},
  603. {"Unicode string", "content", "héllo wörld 🌍"},
  604. {"Very long string", "content", strings.Repeat("a", 1000)},
  605. }
  606. for _, tt := range tests {
  607. t.Run(tt.name, func(t *testing.T) {
  608. err := TypeSafetyCheck(tt.field, tt.value, "raw_string_multiline")
  609. assert.Nil(t, err, "raw_string_multiline should accept any value")
  610. })
  611. }
  612. }
  613. func TestTypeSafetyCheckUnicodeIdentifier(t *testing.T) {
  614. tests := []struct {
  615. name string
  616. field string
  617. value string
  618. expectsError bool
  619. }{
  620. {"Valid unicode identifier", "name", "hello_world", false},
  621. {"Valid with numbers", "name", "test123", false},
  622. {"Valid with dots", "name", "file.txt", false},
  623. {"Valid with underscores", "name", "my_file_name", false},
  624. {"Invalid with special chars", "name", "hello@world", true},
  625. {"Invalid with brackets", "name", "hello[world]", true},
  626. {"Invalid with spaces", "name", "hello world", true},
  627. {"Invalid with path separators", "name", "path/to/file", true},
  628. {"Invalid with backslashes", "name", "path\\to\\file", true},
  629. }
  630. for _, tt := range tests {
  631. t.Run(tt.name, func(t *testing.T) {
  632. err := TypeSafetyCheck(tt.field, tt.value, "unicode_identifier")
  633. validateTypeSafetyResult(t, tt.value, tt.expectsError, err)
  634. })
  635. }
  636. }
  637. func validateTypeSafetyResult(t *testing.T, value string, expectsError bool, err error) {
  638. if expectsError {
  639. assertErrorExpected(t, value, err)
  640. } else {
  641. assertNoErrorExpected(t, value, err)
  642. }
  643. }
  644. func assertErrorExpected(t *testing.T, value string, err error) {
  645. if err == nil {
  646. t.Errorf("Expected error for value '%s', but got none", value)
  647. } else {
  648. t.Logf("Received expected error for value '%s': %v", value, err)
  649. }
  650. }
  651. func assertNoErrorExpected(t *testing.T, value string, err error) {
  652. if err != nil {
  653. t.Errorf("Expected no error for value '%s', but got: %v", value, err)
  654. } else {
  655. t.Logf("No error for valid value '%s' as expected", value)
  656. }
  657. }
  658. func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
  659. tests := []struct {
  660. name string
  661. field string
  662. value string
  663. hasError bool
  664. }{
  665. {"Valid identifier", "name", "hello_world", false},
  666. {"Valid with numbers", "name", "test123", false},
  667. {"Valid with dots", "name", "file.txt", false},
  668. {"Valid with dashes", "name", "my-file", false},
  669. {"Valid with underscores", "name", "my_file", false},
  670. {"Invalid with spaces", "name", "hello world", true},
  671. {"Invalid with special chars", "name", "hello@world", true},
  672. {"Invalid unicode", "name", "héllo", true},
  673. }
  674. for _, tt := range tests {
  675. t.Run(tt.name, func(t *testing.T) {
  676. err := TypeSafetyCheck(tt.field, tt.value, "ascii_identifier")
  677. if tt.hasError {
  678. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  679. } else {
  680. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  681. }
  682. })
  683. }
  684. }
  685. func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
  686. tests := []struct {
  687. name string
  688. value string
  689. hasError bool
  690. }{
  691. {"Simple username", "alice123", false},
  692. {"Email username", "alice@example.com", false},
  693. {"Plus addressing", "alice+test@example.com", false},
  694. {"Hyphen underscore dot", "alice-test_user.example", false},
  695. {"Invalid space", "alice example", true},
  696. {"Invalid shell substitution", "$(whoami)", true},
  697. {"Invalid backtick", "`whoami`", true},
  698. {"Invalid semicolon", "alice;id", true},
  699. {"Invalid ampersand", "alice&id", true},
  700. {"Invalid pipe", "alice|id", true},
  701. {"Invalid quote", "alice'example", true},
  702. {"Invalid slash", "alice/example", true},
  703. }
  704. for _, tt := range tests {
  705. t.Run(tt.name, func(t *testing.T) {
  706. err := TypeSafetyCheck("username", tt.value, "shell_safe_identifier")
  707. if tt.hasError {
  708. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  709. } else {
  710. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  711. }
  712. })
  713. }
  714. }
  715. func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
  716. tests := []struct {
  717. name string
  718. field string
  719. value string
  720. hasError bool
  721. }{
  722. {"Valid sentence", "text", "Hello world", false},
  723. {"Valid with numbers", "text", "Test 123", false},
  724. {"Valid with commas", "text", "Hello, world", false},
  725. {"Valid with periods", "text", "Hello world.", false},
  726. {"Valid with multiple spaces", "text", "Hello world", false},
  727. {"Invalid with special chars", "text", "Hello@world", true},
  728. {"Invalid with parentheses", "text", "Hello (world)", true},
  729. {"Invalid unicode", "text", "Héllo world", true},
  730. }
  731. for _, tt := range tests {
  732. t.Run(tt.name, func(t *testing.T) {
  733. err := TypeSafetyCheck(tt.field, tt.value, "ascii_sentence")
  734. if tt.hasError {
  735. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  736. } else {
  737. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  738. }
  739. })
  740. }
  741. }
  742. func TestTypecheckActionArgumentEmptyName(t *testing.T) {
  743. arg := config.ActionArgument{
  744. Name: "",
  745. Type: "ascii",
  746. }
  747. action := config.Action{Title: "Test"}
  748. err := typecheckActionArgument(&arg, "test", &action)
  749. assert.NotNil(t, err)
  750. assert.Contains(t, err.Error(), "argument name cannot be empty")
  751. }
  752. func TestTypecheckActionArgumentConfirmation(t *testing.T) {
  753. arg := config.ActionArgument{
  754. Name: "confirm",
  755. Type: "confirmation",
  756. }
  757. action := config.Action{Title: "Test"}
  758. err := typecheckActionArgument(&arg, "any_value", &action)
  759. assert.Nil(t, err, "Confirmation type should always pass validation")
  760. }
  761. func TestTypecheckActionArgumentHtmlWithoutName(t *testing.T) {
  762. action := config.Action{
  763. Title: "Delete old backups",
  764. Shell: "rm -rf /opt/oliveTinOldBackups/ && sleep 5",
  765. Arguments: []config.ActionArgument{
  766. {Type: "html", Title: "Description"},
  767. {Type: "confirmation", Title: "Are you sure?!"},
  768. },
  769. }
  770. err := validateArguments(map[string]string{}, &action)
  771. assert.NoError(t, err)
  772. }
  773. func TestParseCommandForReplacements(t *testing.T) {
  774. tests := []struct {
  775. name string
  776. shellCommand string
  777. values map[string]string
  778. expectedOutput string
  779. expectError bool
  780. errorContains string
  781. }{
  782. {
  783. name: "Simple replacement",
  784. shellCommand: "echo {{ name }}",
  785. values: map[string]string{"name": "John"},
  786. expectedOutput: "echo John",
  787. expectError: false,
  788. },
  789. {
  790. name: "Multiple replacements",
  791. shellCommand: "echo {{ first }} {{ last }}",
  792. values: map[string]string{"first": "John", "last": "Doe"},
  793. expectedOutput: "echo John Doe",
  794. expectError: false,
  795. },
  796. {
  797. name: "Replacement with spaces in template",
  798. shellCommand: "echo {{ name }}",
  799. values: map[string]string{"name": "John"},
  800. expectedOutput: "echo John",
  801. expectError: false,
  802. },
  803. {
  804. name: "Missing argument",
  805. shellCommand: "echo {{ missing }}",
  806. values: map[string]string{},
  807. expectedOutput: "",
  808. expectError: true,
  809. errorContains: "required arg not provided: missing",
  810. },
  811. {
  812. name: "No replacements needed",
  813. shellCommand: "echo hello",
  814. values: map[string]string{},
  815. expectedOutput: "echo hello",
  816. expectError: false,
  817. },
  818. {
  819. name: "Multiple same argument",
  820. shellCommand: "echo {{ name }} says hello {{ name }}",
  821. values: map[string]string{"name": "Alice"},
  822. expectedOutput: "echo Alice says hello Alice",
  823. expectError: false,
  824. },
  825. }
  826. for _, tt := range tests {
  827. t.Run(tt.name, func(t *testing.T) {
  828. output, err := tpl.ParseTemplateWithActionContext(tt.shellCommand, nil, tt.values)
  829. if tt.expectError {
  830. assert.NotNil(t, err, "Expected error but got none")
  831. if tt.errorContains != "" {
  832. assert.Contains(t, err.Error(), tt.errorContains)
  833. }
  834. } else {
  835. assert.Nil(t, err, "Expected no error but got: %v", err)
  836. assert.Equal(t, tt.expectedOutput, output)
  837. }
  838. })
  839. }
  840. }
  841. func TestArgumentChoicesValidation(t *testing.T) {
  842. tests := []struct {
  843. name string
  844. req *ExecutionRequest
  845. expectError bool
  846. description string
  847. }{
  848. {
  849. name: "Valid choice",
  850. req: &ExecutionRequest{
  851. Binding: &ActionBinding{
  852. Action: &config.Action{
  853. Title: "Test choices",
  854. Shell: "echo {{ option }}",
  855. Arguments: []config.ActionArgument{
  856. {
  857. Name: "option",
  858. Type: "ascii",
  859. Choices: []config.ActionArgumentChoice{
  860. {Value: "option1", Title: "Option 1"},
  861. {Value: "option2", Title: "Option 2"},
  862. },
  863. },
  864. },
  865. },
  866. },
  867. Arguments: map[string]string{"option": "option1"},
  868. },
  869. expectError: false,
  870. description: "Should accept valid choice",
  871. },
  872. {
  873. name: "Invalid choice",
  874. req: &ExecutionRequest{
  875. Binding: &ActionBinding{
  876. Action: &config.Action{
  877. Title: "Test choices",
  878. Shell: "echo {{ option }}",
  879. Arguments: []config.ActionArgument{
  880. {
  881. Name: "option",
  882. Type: "ascii",
  883. Choices: []config.ActionArgumentChoice{
  884. {Value: "option1", Title: "Option 1"},
  885. {Value: "option2", Title: "Option 2"},
  886. },
  887. },
  888. },
  889. },
  890. },
  891. Arguments: map[string]string{"option": "invalid_option"},
  892. },
  893. expectError: true,
  894. description: "Should reject invalid choice",
  895. },
  896. {
  897. name: "Invalid choice",
  898. req: &ExecutionRequest{
  899. Binding: &ActionBinding{
  900. Action: &config.Action{
  901. Title: "Test choices",
  902. Shell: "echo {{ option }}",
  903. Arguments: []config.ActionArgument{
  904. {
  905. Name: "option",
  906. Type: "ascii",
  907. Choices: []config.ActionArgumentChoice{
  908. {Value: "option1", Title: "Option 1"},
  909. {Value: "option2", Title: "Option 2"},
  910. },
  911. },
  912. },
  913. },
  914. },
  915. Arguments: map[string]string{"option": "option1"},
  916. },
  917. expectError: false,
  918. description: "Should accept valid choice",
  919. },
  920. }
  921. for _, tt := range tests {
  922. t.Run(tt.name, func(t *testing.T) {
  923. _, err := parseActionArguments(tt.req)
  924. if tt.expectError {
  925. assert.NotNil(t, err, tt.description)
  926. assert.Contains(t, err.Error(), "predefined choices")
  927. } else {
  928. assert.Nil(t, err, tt.description)
  929. }
  930. })
  931. }
  932. }
  933. func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
  934. // This type should allow anything without validation
  935. tests := []string{
  936. "normal text",
  937. "_zomg_ c:/ haxxor ' bobby tables && rm -rf /",
  938. "$(rm -rf /)",
  939. "; DROP TABLE users; --",
  940. "../../../../etc/passwd",
  941. "",
  942. "unicode: 你好世界",
  943. "emojis: 🔥💀☠️",
  944. }
  945. for _, value := range tests {
  946. t.Run(fmt.Sprintf("Value: %s", value), func(t *testing.T) {
  947. err := TypeSafetyCheck("test", value, "very_dangerous_raw_string")
  948. assert.Nil(t, err, "very_dangerous_raw_string should accept any value including: %s", value)
  949. })
  950. }
  951. }
  952. func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
  953. req := newExecRequest()
  954. req.Binding.Action = &config.Action{
  955. Title: "Test entity prefix",
  956. Shell: "echo 'Processing {{ name }} for entity'",
  957. Arguments: []config.ActionArgument{
  958. {Name: "name", Type: "ascii"},
  959. },
  960. }
  961. req.Arguments = map[string]string{
  962. "name": "testuser",
  963. }
  964. req.Binding.Entity = &entities.Entity{
  965. Title: "entity_123",
  966. }
  967. // Test with entity prefix
  968. output, err := parseActionArguments(req)
  969. assert.Nil(t, err)
  970. assert.Contains(t, output, "testuser")
  971. }
  972. func TestComplexRegexPatterns(t *testing.T) {
  973. tests := []struct {
  974. name string
  975. pattern string
  976. value string
  977. hasError bool
  978. }{
  979. {
  980. name: "Phone number pattern",
  981. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  982. value: "+1234567890",
  983. hasError: false,
  984. },
  985. {
  986. name: "Invalid phone number",
  987. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  988. value: "123abc",
  989. hasError: true,
  990. },
  991. {
  992. name: "Semantic version pattern",
  993. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  994. value: "1.2.3",
  995. hasError: false,
  996. },
  997. {
  998. name: "Invalid semantic version",
  999. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  1000. value: "1.2",
  1001. hasError: true,
  1002. },
  1003. }
  1004. for _, tt := range tests {
  1005. t.Run(tt.name, func(t *testing.T) {
  1006. err := typeSafetyCheckRegex("test", tt.value, tt.pattern)
  1007. if tt.hasError {
  1008. assert.NotNil(t, err)
  1009. } else {
  1010. assert.Nil(t, err)
  1011. }
  1012. })
  1013. }
  1014. }