arguments_test.go 34 KB

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