arguments_test.go 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205
  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 TestTypeSafetyCheckDnsName(t *testing.T) {
  686. tests := []struct {
  687. name string
  688. value string
  689. hasError bool
  690. }{
  691. {"Short name", "webserver", false},
  692. {"Localhost", "localhost", false},
  693. {"Simple domain", "example.com", false},
  694. {"Host with subdomain", "webserver.example.com", false},
  695. {"Deep subdomain", "a.b.c.example.co.uk", false},
  696. {"Label starting with digit", "1host.example.com", false},
  697. {"Trailing dot", "example.com.", false},
  698. {"Punycode IDN", "xn--bcher-kva.example", false},
  699. {"Underscore", "my_host.example.com", true},
  700. {"Space", "example .com", true},
  701. {"Leading hyphen label", "-host.example.com", true},
  702. {"Trailing hyphen label", "host-.example.com", true},
  703. {"Empty label", "example..com", true},
  704. {"IP address", "192.168.1.1", true},
  705. {"All numeric TLD", "example.123", true},
  706. {"All numeric short name", "12345", true},
  707. {"Special chars", "exam!ple.com", true},
  708. {"Unicode label", "bücher.example.com", true},
  709. }
  710. for _, tt := range tests {
  711. t.Run(tt.name, func(t *testing.T) {
  712. err := TypeSafetyCheck("host", tt.value, "dnsname")
  713. if tt.hasError {
  714. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  715. } else {
  716. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  717. }
  718. })
  719. }
  720. }
  721. func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
  722. tests := []struct {
  723. name string
  724. value string
  725. hasError bool
  726. }{
  727. {"Simple username", "alice123", false},
  728. {"Email username", "alice@example.com", false},
  729. {"Plus addressing", "alice+test@example.com", false},
  730. {"Hyphen underscore dot", "alice-test_user.example", false},
  731. {"Invalid space", "alice example", true},
  732. {"Invalid shell substitution", "$(whoami)", true},
  733. {"Invalid backtick", "`whoami`", true},
  734. {"Invalid semicolon", "alice;id", true},
  735. {"Invalid ampersand", "alice&id", true},
  736. {"Invalid pipe", "alice|id", true},
  737. {"Invalid quote", "alice'example", true},
  738. {"Invalid slash", "alice/example", true},
  739. }
  740. for _, tt := range tests {
  741. t.Run(tt.name, func(t *testing.T) {
  742. err := TypeSafetyCheck("username", tt.value, "shell_safe_identifier")
  743. if tt.hasError {
  744. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  745. } else {
  746. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  747. }
  748. })
  749. }
  750. }
  751. func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
  752. tests := []struct {
  753. name string
  754. field string
  755. value string
  756. hasError bool
  757. }{
  758. {"Valid sentence", "text", "Hello world", false},
  759. {"Valid with numbers", "text", "Test 123", false},
  760. {"Valid with commas", "text", "Hello, world", false},
  761. {"Valid with periods", "text", "Hello world.", false},
  762. {"Valid with multiple spaces", "text", "Hello world", false},
  763. {"Invalid with special chars", "text", "Hello@world", true},
  764. {"Invalid with parentheses", "text", "Hello (world)", true},
  765. {"Invalid unicode", "text", "Héllo world", true},
  766. }
  767. for _, tt := range tests {
  768. t.Run(tt.name, func(t *testing.T) {
  769. err := TypeSafetyCheck(tt.field, tt.value, "ascii_sentence")
  770. if tt.hasError {
  771. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  772. } else {
  773. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  774. }
  775. })
  776. }
  777. }
  778. func TestTypecheckActionArgumentEmptyName(t *testing.T) {
  779. arg := config.ActionArgument{
  780. Name: "",
  781. Type: "ascii",
  782. }
  783. action := config.Action{Title: "Test"}
  784. err := typecheckActionArgument(&arg, "test", &action)
  785. assert.NotNil(t, err)
  786. assert.Contains(t, err.Error(), "argument name cannot be empty")
  787. }
  788. func TestTypecheckActionArgumentConfirmation(t *testing.T) {
  789. arg := config.ActionArgument{
  790. Name: "confirm",
  791. Type: "confirmation",
  792. }
  793. action := config.Action{Title: "Test"}
  794. err := typecheckActionArgument(&arg, "any_value", &action)
  795. assert.Nil(t, err, "Confirmation type should always pass validation")
  796. }
  797. func TestTypecheckActionArgumentHtmlWithoutName(t *testing.T) {
  798. action := config.Action{
  799. Title: "Delete old backups",
  800. Shell: "rm -rf /opt/oliveTinOldBackups/ && sleep 5",
  801. Arguments: []config.ActionArgument{
  802. {Type: "html", Title: "Description"},
  803. {Type: "confirmation", Title: "Are you sure?!"},
  804. },
  805. }
  806. err := validateArguments(map[string]string{}, &action)
  807. assert.NoError(t, err)
  808. }
  809. func TestParseCommandForReplacements(t *testing.T) {
  810. tests := []struct {
  811. name string
  812. shellCommand string
  813. values map[string]string
  814. expectedOutput string
  815. expectError bool
  816. errorContains string
  817. }{
  818. {
  819. name: "Simple replacement",
  820. shellCommand: "echo {{ name }}",
  821. values: map[string]string{"name": "John"},
  822. expectedOutput: "echo John",
  823. expectError: false,
  824. },
  825. {
  826. name: "Multiple replacements",
  827. shellCommand: "echo {{ first }} {{ last }}",
  828. values: map[string]string{"first": "John", "last": "Doe"},
  829. expectedOutput: "echo John Doe",
  830. expectError: false,
  831. },
  832. {
  833. name: "Replacement with spaces in template",
  834. shellCommand: "echo {{ name }}",
  835. values: map[string]string{"name": "John"},
  836. expectedOutput: "echo John",
  837. expectError: false,
  838. },
  839. {
  840. name: "Missing argument",
  841. shellCommand: "echo {{ missing }}",
  842. values: map[string]string{},
  843. expectedOutput: "",
  844. expectError: true,
  845. errorContains: "required arg not provided: missing",
  846. },
  847. {
  848. name: "No replacements needed",
  849. shellCommand: "echo hello",
  850. values: map[string]string{},
  851. expectedOutput: "echo hello",
  852. expectError: false,
  853. },
  854. {
  855. name: "Multiple same argument",
  856. shellCommand: "echo {{ name }} says hello {{ name }}",
  857. values: map[string]string{"name": "Alice"},
  858. expectedOutput: "echo Alice says hello Alice",
  859. expectError: false,
  860. },
  861. }
  862. for _, tt := range tests {
  863. t.Run(tt.name, func(t *testing.T) {
  864. output, err := tpl.ParseTemplateWithActionContext(tt.shellCommand, nil, tt.values)
  865. if tt.expectError {
  866. assert.NotNil(t, err, "Expected error but got none")
  867. if tt.errorContains != "" {
  868. assert.Contains(t, err.Error(), tt.errorContains)
  869. }
  870. } else {
  871. assert.Nil(t, err, "Expected no error but got: %v", err)
  872. assert.Equal(t, tt.expectedOutput, output)
  873. }
  874. })
  875. }
  876. }
  877. func TestArgumentChoicesValidation(t *testing.T) {
  878. tests := []struct {
  879. name string
  880. req *ExecutionRequest
  881. expectError bool
  882. description string
  883. }{
  884. {
  885. name: "Valid choice",
  886. req: &ExecutionRequest{
  887. Binding: &ActionBinding{
  888. Action: &config.Action{
  889. Title: "Test choices",
  890. Shell: "echo {{ option }}",
  891. Arguments: []config.ActionArgument{
  892. {
  893. Name: "option",
  894. Type: "ascii",
  895. Choices: []config.ActionArgumentChoice{
  896. {Value: "option1", Title: "Option 1"},
  897. {Value: "option2", Title: "Option 2"},
  898. },
  899. },
  900. },
  901. },
  902. },
  903. Arguments: map[string]string{"option": "option1"},
  904. },
  905. expectError: false,
  906. description: "Should accept valid choice",
  907. },
  908. {
  909. name: "Invalid choice",
  910. req: &ExecutionRequest{
  911. Binding: &ActionBinding{
  912. Action: &config.Action{
  913. Title: "Test choices",
  914. Shell: "echo {{ option }}",
  915. Arguments: []config.ActionArgument{
  916. {
  917. Name: "option",
  918. Type: "ascii",
  919. Choices: []config.ActionArgumentChoice{
  920. {Value: "option1", Title: "Option 1"},
  921. {Value: "option2", Title: "Option 2"},
  922. },
  923. },
  924. },
  925. },
  926. },
  927. Arguments: map[string]string{"option": "invalid_option"},
  928. },
  929. expectError: true,
  930. description: "Should reject invalid choice",
  931. },
  932. {
  933. name: "Invalid choice",
  934. req: &ExecutionRequest{
  935. Binding: &ActionBinding{
  936. Action: &config.Action{
  937. Title: "Test choices",
  938. Shell: "echo {{ option }}",
  939. Arguments: []config.ActionArgument{
  940. {
  941. Name: "option",
  942. Type: "ascii",
  943. Choices: []config.ActionArgumentChoice{
  944. {Value: "option1", Title: "Option 1"},
  945. {Value: "option2", Title: "Option 2"},
  946. },
  947. },
  948. },
  949. },
  950. },
  951. Arguments: map[string]string{"option": "option1"},
  952. },
  953. expectError: false,
  954. description: "Should accept valid choice",
  955. },
  956. }
  957. for _, tt := range tests {
  958. t.Run(tt.name, func(t *testing.T) {
  959. _, err := parseActionArguments(tt.req)
  960. if tt.expectError {
  961. assert.NotNil(t, err, tt.description)
  962. assert.Contains(t, err.Error(), "predefined choices")
  963. } else {
  964. assert.Nil(t, err, tt.description)
  965. }
  966. })
  967. }
  968. }
  969. func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
  970. // This type should allow anything without validation
  971. tests := []string{
  972. "normal text",
  973. "_zomg_ c:/ haxxor ' bobby tables && rm -rf /",
  974. "$(rm -rf /)",
  975. "; DROP TABLE users; --",
  976. "../../../../etc/passwd",
  977. "",
  978. "unicode: 你好世界",
  979. "emojis: 🔥💀☠️",
  980. }
  981. for _, value := range tests {
  982. t.Run(fmt.Sprintf("Value: %s", value), func(t *testing.T) {
  983. err := TypeSafetyCheck("test", value, "very_dangerous_raw_string")
  984. assert.Nil(t, err, "very_dangerous_raw_string should accept any value including: %s", value)
  985. })
  986. }
  987. }
  988. func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
  989. req := newExecRequest()
  990. req.Binding.Action = &config.Action{
  991. Title: "Test entity prefix",
  992. Shell: "echo 'Processing {{ name }} for entity'",
  993. Arguments: []config.ActionArgument{
  994. {Name: "name", Type: "ascii"},
  995. },
  996. }
  997. req.Arguments = map[string]string{
  998. "name": "testuser",
  999. }
  1000. req.Binding.Entity = &entities.Entity{
  1001. Title: "entity_123",
  1002. }
  1003. // Test with entity prefix
  1004. output, err := parseActionArguments(req)
  1005. assert.Nil(t, err)
  1006. assert.Contains(t, output, "testuser")
  1007. }
  1008. func TestComplexRegexPatterns(t *testing.T) {
  1009. tests := []struct {
  1010. name string
  1011. pattern string
  1012. value string
  1013. hasError bool
  1014. }{
  1015. {
  1016. name: "Phone number pattern",
  1017. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  1018. value: "+1234567890",
  1019. hasError: false,
  1020. },
  1021. {
  1022. name: "Invalid phone number",
  1023. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  1024. value: "123abc",
  1025. hasError: true,
  1026. },
  1027. {
  1028. name: "Semantic version pattern",
  1029. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  1030. value: "1.2.3",
  1031. hasError: false,
  1032. },
  1033. {
  1034. name: "Invalid semantic version",
  1035. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  1036. value: "1.2",
  1037. hasError: true,
  1038. },
  1039. }
  1040. for _, tt := range tests {
  1041. t.Run(tt.name, func(t *testing.T) {
  1042. err := typeSafetyCheckRegex("test", tt.value, tt.pattern)
  1043. if tt.hasError {
  1044. assert.NotNil(t, err)
  1045. } else {
  1046. assert.Nil(t, err)
  1047. }
  1048. })
  1049. }
  1050. }