4
0

arguments_test.go 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160
  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. for _, tt := range tests {
  494. t.Run(tt.name, func(t *testing.T) {
  495. err := typeSafetyCheckRegex(tt.field, tt.value, tt.pattern)
  496. if tt.hasError {
  497. assert.NotNil(t, err, "Expected error for value %s with pattern %s, but got no error", tt.value, tt.pattern)
  498. } else {
  499. assert.Nil(t, err, "Expected no error for value %s with pattern %s, but got error: %v", tt.value, tt.pattern, err)
  500. }
  501. })
  502. }
  503. }
  504. func TestRedactShellCommand(t *testing.T) {
  505. cmd := "echo 'The password for Fred is toomanysecrets'"
  506. args := []config.ActionArgument{
  507. {
  508. Name: "personName",
  509. Type: "ascii",
  510. },
  511. {
  512. Name: "password",
  513. Type: "password",
  514. },
  515. }
  516. values := map[string]string{
  517. "personName": "Fred",
  518. "password": "toomanysecrets",
  519. }
  520. res := redactShellCommand(cmd, args, values)
  521. assert.Equal(t, "echo 'The password for Fred is <redacted>'", res, "Redacted shell command should mask the password argument")
  522. // Test with empty password
  523. values["password"] = ""
  524. res = redactShellCommand(cmd, args, values)
  525. assert.Equal(t, cmd, res, "Empty password should not change the command")
  526. // Test with missing password argument
  527. delete(values, "password")
  528. res = redactShellCommand(cmd, args, values)
  529. assert.Equal(t, cmd, res, "Missing password argument should not change the command")
  530. }
  531. func TestTypeSafetyCheckEmail(t *testing.T) {
  532. tests := []struct {
  533. name string
  534. field string
  535. value string
  536. hasError bool
  537. }{
  538. {"Valid simple email", "email", "user@example.com", false},
  539. {"Valid email with subdomain", "email", "user@mail.example.com", false},
  540. {"Valid email with plus", "email", "user+test@example.com", false},
  541. {"Valid email with dash", "email", "user-name@example.com", false},
  542. {"Valid email with numbers", "email", "user123@example123.com", false},
  543. {"Invalid email no @", "email", "userexample.com", true},
  544. {"Invalid email no domain", "email", "user@", true},
  545. {"Invalid email no user", "email", "@example.com", true},
  546. {"Invalid email spaces", "email", "user name@example.com", true},
  547. {"Invalid email double @", "email", "user@@example.com", true},
  548. }
  549. for _, tt := range tests {
  550. t.Run(tt.name, func(t *testing.T) {
  551. err := TypeSafetyCheck(tt.field, tt.value, "email")
  552. if tt.hasError {
  553. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  554. } else {
  555. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  556. }
  557. })
  558. }
  559. }
  560. func TestTypeSafetyCheckDatetime(t *testing.T) {
  561. tests := []struct {
  562. name string
  563. field string
  564. value string
  565. hasError bool
  566. }{
  567. {"Valid datetime", "datetime", "2023-12-25T15:30:45", false},
  568. {"Valid datetime morning", "datetime", "2023-01-01T00:00:00", false},
  569. {"Valid datetime evening", "datetime", "2023-12-31T23:59:59", false},
  570. {"Invalid format missing T", "datetime", "2023-12-25 15:30:45", true},
  571. {"Invalid format missing seconds", "datetime", "2023-12-25T15:30", true},
  572. {"Invalid date", "datetime", "2023-13-25T15:30:45", true},
  573. {"Invalid time", "datetime", "2023-12-25T25:30:45", true},
  574. {"Random string", "datetime", "not-a-date", true},
  575. }
  576. for _, tt := range tests {
  577. t.Run(tt.name, func(t *testing.T) {
  578. err := TypeSafetyCheck(tt.field, tt.value, "datetime")
  579. if tt.hasError {
  580. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  581. } else {
  582. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  583. }
  584. })
  585. }
  586. }
  587. func TestTypeSafetyCheckRawStringMultiline(t *testing.T) {
  588. tests := []struct {
  589. name string
  590. field string
  591. value string
  592. }{
  593. {"Simple string", "content", "hello world"},
  594. {"Multiline string", "content", "line1\nline2\nline3"},
  595. {"String with special chars", "content", "!@#$%^&*()"},
  596. {"Unicode string", "content", "héllo wörld 🌍"},
  597. {"Very long string", "content", strings.Repeat("a", 1000)},
  598. }
  599. for _, tt := range tests {
  600. t.Run(tt.name, func(t *testing.T) {
  601. err := TypeSafetyCheck(tt.field, tt.value, "raw_string_multiline")
  602. assert.Nil(t, err, "raw_string_multiline should accept any value")
  603. })
  604. }
  605. }
  606. func TestTypeSafetyCheckUnicodeIdentifier(t *testing.T) {
  607. tests := []struct {
  608. name string
  609. field string
  610. value string
  611. expectsError bool
  612. }{
  613. {"Valid unicode identifier", "name", "hello_world", false},
  614. {"Valid with numbers", "name", "test123", false},
  615. {"Valid with dots", "name", "file.txt", false},
  616. {"Valid with underscores", "name", "my_file_name", false},
  617. {"Invalid with special chars", "name", "hello@world", true},
  618. {"Invalid with brackets", "name", "hello[world]", true},
  619. {"Invalid with spaces", "name", "hello world", true},
  620. {"Invalid with path separators", "name", "path/to/file", true},
  621. {"Invalid with backslashes", "name", "path\\to\\file", true},
  622. }
  623. for _, tt := range tests {
  624. t.Run(tt.name, func(t *testing.T) {
  625. err := TypeSafetyCheck(tt.field, tt.value, "unicode_identifier")
  626. validateTypeSafetyResult(t, tt.value, tt.expectsError, err)
  627. })
  628. }
  629. }
  630. func validateTypeSafetyResult(t *testing.T, value string, expectsError bool, err error) {
  631. if expectsError {
  632. assertErrorExpected(t, value, err)
  633. } else {
  634. assertNoErrorExpected(t, value, err)
  635. }
  636. }
  637. func assertErrorExpected(t *testing.T, value string, err error) {
  638. if err == nil {
  639. t.Errorf("Expected error for value '%s', but got none", value)
  640. } else {
  641. t.Logf("Received expected error for value '%s': %v", value, err)
  642. }
  643. }
  644. func assertNoErrorExpected(t *testing.T, value string, err error) {
  645. if err != nil {
  646. t.Errorf("Expected no error for value '%s', but got: %v", value, err)
  647. } else {
  648. t.Logf("No error for valid value '%s' as expected", value)
  649. }
  650. }
  651. func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
  652. tests := []struct {
  653. name string
  654. field string
  655. value string
  656. hasError bool
  657. }{
  658. {"Valid identifier", "name", "hello_world", false},
  659. {"Valid with numbers", "name", "test123", false},
  660. {"Valid with dots", "name", "file.txt", false},
  661. {"Valid with dashes", "name", "my-file", false},
  662. {"Valid with underscores", "name", "my_file", false},
  663. {"Invalid with spaces", "name", "hello world", true},
  664. {"Invalid with special chars", "name", "hello@world", true},
  665. {"Invalid unicode", "name", "héllo", true},
  666. }
  667. for _, tt := range tests {
  668. t.Run(tt.name, func(t *testing.T) {
  669. err := TypeSafetyCheck(tt.field, tt.value, "ascii_identifier")
  670. if tt.hasError {
  671. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  672. } else {
  673. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  674. }
  675. })
  676. }
  677. }
  678. func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
  679. tests := []struct {
  680. name string
  681. value string
  682. hasError bool
  683. }{
  684. {"Simple username", "alice123", false},
  685. {"Email username", "alice@example.com", false},
  686. {"Plus addressing", "alice+test@example.com", false},
  687. {"Hyphen underscore dot", "alice-test_user.example", false},
  688. {"Invalid space", "alice example", true},
  689. {"Invalid shell substitution", "$(whoami)", true},
  690. {"Invalid backtick", "`whoami`", true},
  691. {"Invalid semicolon", "alice;id", true},
  692. {"Invalid ampersand", "alice&id", true},
  693. {"Invalid pipe", "alice|id", true},
  694. {"Invalid quote", "alice'example", true},
  695. {"Invalid slash", "alice/example", true},
  696. }
  697. for _, tt := range tests {
  698. t.Run(tt.name, func(t *testing.T) {
  699. err := TypeSafetyCheck("username", tt.value, "shell_safe_identifier")
  700. if tt.hasError {
  701. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  702. } else {
  703. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  704. }
  705. })
  706. }
  707. }
  708. func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
  709. tests := []struct {
  710. name string
  711. field string
  712. value string
  713. hasError bool
  714. }{
  715. {"Valid sentence", "text", "Hello world", false},
  716. {"Valid with numbers", "text", "Test 123", false},
  717. {"Valid with commas", "text", "Hello, world", false},
  718. {"Valid with periods", "text", "Hello world.", false},
  719. {"Valid with multiple spaces", "text", "Hello world", false},
  720. {"Invalid with special chars", "text", "Hello@world", true},
  721. {"Invalid with parentheses", "text", "Hello (world)", true},
  722. {"Invalid unicode", "text", "Héllo world", true},
  723. }
  724. for _, tt := range tests {
  725. t.Run(tt.name, func(t *testing.T) {
  726. err := TypeSafetyCheck(tt.field, tt.value, "ascii_sentence")
  727. if tt.hasError {
  728. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  729. } else {
  730. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  731. }
  732. })
  733. }
  734. }
  735. func TestTypecheckActionArgumentEmptyName(t *testing.T) {
  736. arg := config.ActionArgument{
  737. Name: "",
  738. Type: "ascii",
  739. }
  740. action := config.Action{Title: "Test"}
  741. err := typecheckActionArgument(&arg, "test", &action)
  742. assert.NotNil(t, err)
  743. assert.Contains(t, err.Error(), "argument name cannot be empty")
  744. }
  745. func TestTypecheckActionArgumentConfirmation(t *testing.T) {
  746. arg := config.ActionArgument{
  747. Name: "confirm",
  748. Type: "confirmation",
  749. }
  750. action := config.Action{Title: "Test"}
  751. err := typecheckActionArgument(&arg, "any_value", &action)
  752. assert.Nil(t, err, "Confirmation type should always pass validation")
  753. }
  754. func TestTypecheckActionArgumentHtmlWithoutName(t *testing.T) {
  755. action := config.Action{
  756. Title: "Delete old backups",
  757. Shell: "rm -rf /opt/oliveTinOldBackups/ && sleep 5",
  758. Arguments: []config.ActionArgument{
  759. {Type: "html", Title: "Description"},
  760. {Type: "confirmation", Title: "Are you sure?!"},
  761. },
  762. }
  763. err := validateArguments(map[string]string{}, &action)
  764. assert.NoError(t, err)
  765. }
  766. func TestParseCommandForReplacements(t *testing.T) {
  767. tests := []struct {
  768. name string
  769. shellCommand string
  770. values map[string]string
  771. expectedOutput string
  772. expectError bool
  773. errorContains string
  774. }{
  775. {
  776. name: "Simple replacement",
  777. shellCommand: "echo {{ name }}",
  778. values: map[string]string{"name": "John"},
  779. expectedOutput: "echo John",
  780. expectError: false,
  781. },
  782. {
  783. name: "Multiple replacements",
  784. shellCommand: "echo {{ first }} {{ last }}",
  785. values: map[string]string{"first": "John", "last": "Doe"},
  786. expectedOutput: "echo John Doe",
  787. expectError: false,
  788. },
  789. {
  790. name: "Replacement with spaces in template",
  791. shellCommand: "echo {{ name }}",
  792. values: map[string]string{"name": "John"},
  793. expectedOutput: "echo John",
  794. expectError: false,
  795. },
  796. {
  797. name: "Missing argument",
  798. shellCommand: "echo {{ missing }}",
  799. values: map[string]string{},
  800. expectedOutput: "",
  801. expectError: true,
  802. errorContains: "required arg not provided: missing",
  803. },
  804. {
  805. name: "No replacements needed",
  806. shellCommand: "echo hello",
  807. values: map[string]string{},
  808. expectedOutput: "echo hello",
  809. expectError: false,
  810. },
  811. {
  812. name: "Multiple same argument",
  813. shellCommand: "echo {{ name }} says hello {{ name }}",
  814. values: map[string]string{"name": "Alice"},
  815. expectedOutput: "echo Alice says hello Alice",
  816. expectError: false,
  817. },
  818. }
  819. for _, tt := range tests {
  820. t.Run(tt.name, func(t *testing.T) {
  821. output, err := tpl.ParseTemplateWithActionContext(tt.shellCommand, nil, tt.values)
  822. if tt.expectError {
  823. assert.NotNil(t, err, "Expected error but got none")
  824. if tt.errorContains != "" {
  825. assert.Contains(t, err.Error(), tt.errorContains)
  826. }
  827. } else {
  828. assert.Nil(t, err, "Expected no error but got: %v", err)
  829. assert.Equal(t, tt.expectedOutput, output)
  830. }
  831. })
  832. }
  833. }
  834. func TestArgumentChoicesValidation(t *testing.T) {
  835. tests := []struct {
  836. name string
  837. req *ExecutionRequest
  838. expectError bool
  839. description string
  840. }{
  841. {
  842. name: "Valid choice",
  843. req: &ExecutionRequest{
  844. Binding: &ActionBinding{
  845. Action: &config.Action{
  846. Title: "Test choices",
  847. Shell: "echo {{ option }}",
  848. Arguments: []config.ActionArgument{
  849. {
  850. Name: "option",
  851. Type: "ascii",
  852. Choices: []config.ActionArgumentChoice{
  853. {Value: "option1", Title: "Option 1"},
  854. {Value: "option2", Title: "Option 2"},
  855. },
  856. },
  857. },
  858. },
  859. },
  860. Arguments: map[string]string{"option": "option1"},
  861. },
  862. expectError: false,
  863. description: "Should accept valid choice",
  864. },
  865. {
  866. name: "Invalid choice",
  867. req: &ExecutionRequest{
  868. Binding: &ActionBinding{
  869. Action: &config.Action{
  870. Title: "Test choices",
  871. Shell: "echo {{ option }}",
  872. Arguments: []config.ActionArgument{
  873. {
  874. Name: "option",
  875. Type: "ascii",
  876. Choices: []config.ActionArgumentChoice{
  877. {Value: "option1", Title: "Option 1"},
  878. {Value: "option2", Title: "Option 2"},
  879. },
  880. },
  881. },
  882. },
  883. },
  884. Arguments: map[string]string{"option": "invalid_option"},
  885. },
  886. expectError: true,
  887. description: "Should reject invalid choice",
  888. },
  889. {
  890. name: "Invalid choice",
  891. req: &ExecutionRequest{
  892. Binding: &ActionBinding{
  893. Action: &config.Action{
  894. Title: "Test choices",
  895. Shell: "echo {{ option }}",
  896. Arguments: []config.ActionArgument{
  897. {
  898. Name: "option",
  899. Type: "ascii",
  900. Choices: []config.ActionArgumentChoice{
  901. {Value: "option1", Title: "Option 1"},
  902. {Value: "option2", Title: "Option 2"},
  903. },
  904. },
  905. },
  906. },
  907. },
  908. Arguments: map[string]string{"option": "option1"},
  909. },
  910. expectError: false,
  911. description: "Should accept valid choice",
  912. },
  913. }
  914. for _, tt := range tests {
  915. t.Run(tt.name, func(t *testing.T) {
  916. _, err := parseActionArguments(tt.req)
  917. if tt.expectError {
  918. assert.NotNil(t, err, tt.description)
  919. assert.Contains(t, err.Error(), "predefined choices")
  920. } else {
  921. assert.Nil(t, err, tt.description)
  922. }
  923. })
  924. }
  925. }
  926. func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
  927. // This type should allow anything without validation
  928. tests := []string{
  929. "normal text",
  930. "_zomg_ c:/ haxxor ' bobby tables && rm -rf /",
  931. "$(rm -rf /)",
  932. "; DROP TABLE users; --",
  933. "../../../../etc/passwd",
  934. "",
  935. "unicode: 你好世界",
  936. "emojis: 🔥💀☠️",
  937. }
  938. for _, value := range tests {
  939. t.Run(fmt.Sprintf("Value: %s", value), func(t *testing.T) {
  940. err := TypeSafetyCheck("test", value, "very_dangerous_raw_string")
  941. assert.Nil(t, err, "very_dangerous_raw_string should accept any value including: %s", value)
  942. })
  943. }
  944. }
  945. func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
  946. req := newExecRequest()
  947. req.Binding.Action = &config.Action{
  948. Title: "Test entity prefix",
  949. Shell: "echo 'Processing {{ name }} for entity'",
  950. Arguments: []config.ActionArgument{
  951. {Name: "name", Type: "ascii"},
  952. },
  953. }
  954. req.Arguments = map[string]string{
  955. "name": "testuser",
  956. }
  957. req.Binding.Entity = &entities.Entity{
  958. Title: "entity_123",
  959. }
  960. // Test with entity prefix
  961. output, err := parseActionArguments(req)
  962. assert.Nil(t, err)
  963. assert.Contains(t, output, "testuser")
  964. }
  965. func TestComplexRegexPatterns(t *testing.T) {
  966. tests := []struct {
  967. name string
  968. pattern string
  969. value string
  970. hasError bool
  971. }{
  972. {
  973. name: "Phone number pattern",
  974. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  975. value: "+1234567890",
  976. hasError: false,
  977. },
  978. {
  979. name: "Invalid phone number",
  980. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  981. value: "123abc",
  982. hasError: true,
  983. },
  984. {
  985. name: "Semantic version pattern",
  986. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  987. value: "1.2.3",
  988. hasError: false,
  989. },
  990. {
  991. name: "Invalid semantic version",
  992. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  993. value: "1.2",
  994. hasError: true,
  995. },
  996. }
  997. for _, tt := range tests {
  998. t.Run(tt.name, func(t *testing.T) {
  999. err := typeSafetyCheckRegex("test", tt.value, tt.pattern)
  1000. if tt.hasError {
  1001. assert.NotNil(t, err)
  1002. } else {
  1003. assert.Nil(t, err)
  1004. }
  1005. })
  1006. }
  1007. }