broker_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. //go:build linux || freebsd || netbsd || openbsd || solaris || !windows
  2. package csplugin
  3. import (
  4. "encoding/json"
  5. "os"
  6. "os/exec"
  7. "path"
  8. "path/filepath"
  9. "reflect"
  10. "runtime"
  11. "testing"
  12. "time"
  13. log "github.com/sirupsen/logrus"
  14. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  15. "github.com/crowdsecurity/crowdsec/pkg/models"
  16. "github.com/pkg/errors"
  17. "github.com/stretchr/testify/assert"
  18. "gopkg.in/tomb.v2"
  19. "gopkg.in/yaml.v2"
  20. )
  21. var testPath string
  22. func setPluginPermTo744() {
  23. setPluginPermTo("744")
  24. }
  25. func setPluginPermTo722() {
  26. setPluginPermTo("722")
  27. }
  28. func setPluginPermTo724() {
  29. setPluginPermTo("724")
  30. }
  31. func TestGetPluginNameAndTypeFromPath(t *testing.T) {
  32. setUp()
  33. defer tearDown()
  34. type args struct {
  35. path string
  36. }
  37. tests := []struct {
  38. name string
  39. args args
  40. want string
  41. want1 string
  42. wantErr bool
  43. }{
  44. {
  45. name: "valid plugin name, single dash",
  46. args: args{
  47. path: path.Join(testPath, "notification-gitter"),
  48. },
  49. want: "notification",
  50. want1: "gitter",
  51. wantErr: false,
  52. },
  53. {
  54. name: "invalid plugin name",
  55. args: args{
  56. path: "./tests/gitter",
  57. },
  58. want: "",
  59. want1: "",
  60. wantErr: true,
  61. },
  62. {
  63. name: "valid plugin name, multiple dash",
  64. args: args{
  65. path: "./tests/notification-instant-slack",
  66. },
  67. want: "notification-instant",
  68. want1: "slack",
  69. wantErr: false,
  70. },
  71. }
  72. for _, tt := range tests {
  73. t.Run(tt.name, func(t *testing.T) {
  74. got, got1, err := getPluginTypeAndSubtypeFromPath(tt.args.path)
  75. if (err != nil) != tt.wantErr {
  76. t.Errorf("getPluginNameAndTypeFromPath() error = %v, wantErr %v", err, tt.wantErr)
  77. return
  78. }
  79. if got != tt.want {
  80. t.Errorf("getPluginNameAndTypeFromPath() got = %v, want %v", got, tt.want)
  81. }
  82. if got1 != tt.want1 {
  83. t.Errorf("getPluginNameAndTypeFromPath() got1 = %v, want %v", got1, tt.want1)
  84. }
  85. })
  86. }
  87. }
  88. func TestListFilesAtPath(t *testing.T) {
  89. setUp()
  90. defer tearDown()
  91. type args struct {
  92. path string
  93. }
  94. tests := []struct {
  95. name string
  96. args args
  97. want []string
  98. wantErr bool
  99. }{
  100. {
  101. name: "valid directory",
  102. args: args{
  103. path: testPath,
  104. },
  105. want: []string{
  106. filepath.Join(testPath, "notification-gitter"),
  107. filepath.Join(testPath, "slack"),
  108. },
  109. },
  110. {
  111. name: "invalid directory",
  112. args: args{
  113. path: "./foo/bar/",
  114. },
  115. wantErr: true,
  116. },
  117. }
  118. for _, tt := range tests {
  119. t.Run(tt.name, func(t *testing.T) {
  120. got, err := listFilesAtPath(tt.args.path)
  121. if (err != nil) != tt.wantErr {
  122. t.Errorf("listFilesAtPath() error = %v, wantErr %v", err, tt.wantErr)
  123. return
  124. }
  125. if !reflect.DeepEqual(got, tt.want) {
  126. t.Errorf("listFilesAtPath() = %v, want %v", got, tt.want)
  127. }
  128. })
  129. }
  130. }
  131. func TestBrokerInit(t *testing.T) {
  132. if runtime.GOOS == "windows" {
  133. t.Skip("Skipping test on windows")
  134. }
  135. tests := []struct {
  136. name string
  137. action func()
  138. errContains string
  139. wantErr bool
  140. procCfg csconfig.PluginCfg
  141. }{
  142. {
  143. name: "valid config",
  144. action: setPluginPermTo744,
  145. wantErr: false,
  146. },
  147. {
  148. name: "group writable binary",
  149. wantErr: true,
  150. errContains: "notification-dummy is world writable",
  151. action: setPluginPermTo722,
  152. },
  153. {
  154. name: "group writable binary",
  155. wantErr: true,
  156. errContains: "notification-dummy is group writable",
  157. action: setPluginPermTo724,
  158. },
  159. {
  160. name: "no plugin dir",
  161. wantErr: true,
  162. errContains: "no such file or directory",
  163. action: tearDown,
  164. },
  165. {
  166. name: "no plugin binary",
  167. wantErr: true,
  168. errContains: "binary for plugin dummy_default not found",
  169. action: func() {
  170. err := os.Remove(path.Join(testPath, "notification-dummy"))
  171. if err != nil {
  172. t.Fatal(err)
  173. }
  174. },
  175. },
  176. {
  177. name: "only specify user",
  178. wantErr: true,
  179. errContains: "both plugin user and group must be set",
  180. procCfg: csconfig.PluginCfg{
  181. User: "123445555551122toto",
  182. },
  183. action: setPluginPermTo744,
  184. },
  185. {
  186. name: "only specify group",
  187. wantErr: true,
  188. errContains: "both plugin user and group must be set",
  189. procCfg: csconfig.PluginCfg{
  190. Group: "123445555551122toto",
  191. },
  192. action: setPluginPermTo744,
  193. },
  194. {
  195. name: "Fails to run as root",
  196. wantErr: true,
  197. errContains: "operation not permitted",
  198. procCfg: csconfig.PluginCfg{
  199. User: "root",
  200. Group: "root",
  201. },
  202. action: setPluginPermTo744,
  203. },
  204. {
  205. name: "Invalid user and group",
  206. wantErr: true,
  207. errContains: "unknown user toto1234",
  208. procCfg: csconfig.PluginCfg{
  209. User: "toto1234",
  210. Group: "toto1234",
  211. },
  212. action: setPluginPermTo744,
  213. },
  214. {
  215. name: "Valid user and invalid group",
  216. wantErr: true,
  217. errContains: "unknown group toto1234",
  218. procCfg: csconfig.PluginCfg{
  219. User: "nobody",
  220. Group: "toto1234",
  221. },
  222. action: setPluginPermTo744,
  223. },
  224. }
  225. for _, test := range tests {
  226. t.Run(test.name, func(t *testing.T) {
  227. defer tearDown()
  228. buildDummyPlugin()
  229. if test.action != nil {
  230. test.action()
  231. }
  232. pb := PluginBroker{}
  233. profiles := csconfig.NewDefaultConfig().API.Server.Profiles
  234. profiles = append(profiles, &csconfig.ProfileCfg{
  235. Notifications: []string{"dummy_default"},
  236. })
  237. err := pb.Init(&test.procCfg, profiles, &csconfig.ConfigurationPaths{
  238. PluginDir: testPath,
  239. NotificationDir: "./tests/notifications",
  240. })
  241. defer pb.Kill()
  242. if test.wantErr {
  243. assert.ErrorContains(t, err, test.errContains)
  244. } else {
  245. assert.NoError(t, err)
  246. }
  247. })
  248. }
  249. }
  250. func readconfig(t *testing.T, path string) ([]byte, PluginConfig) {
  251. var config PluginConfig
  252. orig, err := os.ReadFile("tests/notifications/dummy.yaml")
  253. if err != nil {
  254. t.Fatalf("unable to read config file %s : %s", path, err)
  255. }
  256. if err := yaml.Unmarshal(orig, &config); err != nil {
  257. t.Fatalf("unable to unmarshal config file : %s", err)
  258. }
  259. return orig, config
  260. }
  261. func writeconfig(t *testing.T, config PluginConfig, path string) {
  262. data, err := yaml.Marshal(&config)
  263. if err != nil {
  264. t.Fatalf("unable to marshal config file : %s", err)
  265. }
  266. if err := os.WriteFile(path, data, 0644); err != nil {
  267. t.Fatalf("unable to write config file %s : %s", path, err)
  268. }
  269. }
  270. func TestBrokerNoThreshold(t *testing.T) {
  271. var alerts []models.Alert
  272. DefaultEmptyTicker = 50 * time.Millisecond
  273. buildDummyPlugin()
  274. setPluginPermTo744()
  275. defer tearDown()
  276. //init
  277. pluginCfg := csconfig.PluginCfg{}
  278. pb := PluginBroker{}
  279. profiles := csconfig.NewDefaultConfig().API.Server.Profiles
  280. profiles = append(profiles, &csconfig.ProfileCfg{
  281. Notifications: []string{"dummy_default"},
  282. })
  283. //default config
  284. err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
  285. PluginDir: testPath,
  286. NotificationDir: "./tests/notifications",
  287. })
  288. assert.NoError(t, err)
  289. tomb := tomb.Tomb{}
  290. go pb.Run(&tomb)
  291. defer pb.Kill()
  292. //send one item, it should be processed right now
  293. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  294. time.Sleep(200 * time.Millisecond)
  295. //we expect one now
  296. content, err := os.ReadFile("./out")
  297. if err != nil {
  298. log.Errorf("Error reading file: %s", err)
  299. }
  300. err = json.Unmarshal(content, &alerts)
  301. assert.NoError(t, err)
  302. assert.Equal(t, 1, len(alerts))
  303. //remove it
  304. os.Remove("./out")
  305. //and another one
  306. log.Printf("second send")
  307. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  308. time.Sleep(200 * time.Millisecond)
  309. //we expect one again, as we cleaned the file
  310. content, err = os.ReadFile("./out")
  311. if err != nil {
  312. log.Errorf("Error reading file: %s", err)
  313. }
  314. err = json.Unmarshal(content, &alerts)
  315. log.Printf("content-> %s", content)
  316. assert.NoError(t, err)
  317. assert.Equal(t, 1, len(alerts))
  318. }
  319. func TestBrokerRunGroupAndTimeThreshold_TimeFirst(t *testing.T) {
  320. //test grouping by "time"
  321. DefaultEmptyTicker = 50 * time.Millisecond
  322. buildDummyPlugin()
  323. setPluginPermTo744()
  324. defer tearDown()
  325. //init
  326. pluginCfg := csconfig.PluginCfg{}
  327. pb := PluginBroker{}
  328. profiles := csconfig.NewDefaultConfig().API.Server.Profiles
  329. profiles = append(profiles, &csconfig.ProfileCfg{
  330. Notifications: []string{"dummy_default"},
  331. })
  332. //set groupwait and groupthreshold, should honor whichever comes first
  333. raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
  334. cfg.GroupThreshold = 4
  335. cfg.GroupWait = 1 * time.Second
  336. writeconfig(t, cfg, "tests/notifications/dummy.yaml")
  337. err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
  338. PluginDir: testPath,
  339. NotificationDir: "./tests/notifications",
  340. })
  341. assert.NoError(t, err)
  342. tomb := tomb.Tomb{}
  343. go pb.Run(&tomb)
  344. defer pb.Kill()
  345. //send data
  346. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  347. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  348. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  349. time.Sleep(500 * time.Millisecond)
  350. //because of group threshold, we shouldn't have data yet
  351. assert.NoFileExists(t, "./out")
  352. time.Sleep(1 * time.Second)
  353. //after 1 seconds, we should have data
  354. content, err := os.ReadFile("./out")
  355. assert.NoError(t, err)
  356. var alerts []models.Alert
  357. err = json.Unmarshal(content, &alerts)
  358. assert.NoError(t, err)
  359. assert.Equal(t, 3, len(alerts))
  360. //restore config
  361. if err := os.WriteFile("tests/notifications/dummy.yaml", raw, 0644); err != nil {
  362. t.Fatalf("unable to write config file %s", err)
  363. }
  364. }
  365. func TestBrokerRunGroupAndTimeThreshold_CountFirst(t *testing.T) {
  366. DefaultEmptyTicker = 50 * time.Millisecond
  367. buildDummyPlugin()
  368. setPluginPermTo744()
  369. defer tearDown()
  370. //init
  371. pluginCfg := csconfig.PluginCfg{}
  372. pb := PluginBroker{}
  373. profiles := csconfig.NewDefaultConfig().API.Server.Profiles
  374. profiles = append(profiles, &csconfig.ProfileCfg{
  375. Notifications: []string{"dummy_default"},
  376. })
  377. //set groupwait and groupthreshold, should honor whichever comes first
  378. raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
  379. cfg.GroupThreshold = 4
  380. cfg.GroupWait = 4 * time.Second
  381. writeconfig(t, cfg, "tests/notifications/dummy.yaml")
  382. err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
  383. PluginDir: testPath,
  384. NotificationDir: "./tests/notifications",
  385. })
  386. assert.NoError(t, err)
  387. tomb := tomb.Tomb{}
  388. go pb.Run(&tomb)
  389. defer pb.Kill()
  390. //send data
  391. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  392. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  393. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  394. time.Sleep(100 * time.Millisecond)
  395. //because of group threshold, we shouldn't have data yet
  396. assert.NoFileExists(t, "./out")
  397. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  398. time.Sleep(100 * time.Millisecond)
  399. //and now we should
  400. content, err := os.ReadFile("./out")
  401. if err != nil {
  402. log.Errorf("Error reading file: %s", err)
  403. }
  404. var alerts []models.Alert
  405. err = json.Unmarshal(content, &alerts)
  406. assert.NoError(t, err)
  407. assert.Equal(t, 4, len(alerts))
  408. //restore config
  409. if err := os.WriteFile("tests/notifications/dummy.yaml", raw, 0644); err != nil {
  410. t.Fatalf("unable to write config file %s", err)
  411. }
  412. }
  413. func TestBrokerRunGroupThreshold(t *testing.T) {
  414. //test grouping by "size"
  415. DefaultEmptyTicker = 50 * time.Millisecond
  416. buildDummyPlugin()
  417. setPluginPermTo744()
  418. defer tearDown()
  419. //init
  420. pluginCfg := csconfig.PluginCfg{}
  421. pb := PluginBroker{}
  422. profiles := csconfig.NewDefaultConfig().API.Server.Profiles
  423. profiles = append(profiles, &csconfig.ProfileCfg{
  424. Notifications: []string{"dummy_default"},
  425. })
  426. //set groupwait
  427. raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
  428. cfg.GroupThreshold = 4
  429. writeconfig(t, cfg, "tests/notifications/dummy.yaml")
  430. err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
  431. PluginDir: testPath,
  432. NotificationDir: "./tests/notifications",
  433. })
  434. assert.NoError(t, err)
  435. tomb := tomb.Tomb{}
  436. go pb.Run(&tomb)
  437. defer pb.Kill()
  438. //send data
  439. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  440. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  441. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  442. time.Sleep(100 * time.Millisecond)
  443. //because of group threshold, we shouldn't have data yet
  444. assert.NoFileExists(t, "./out")
  445. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  446. time.Sleep(100 * time.Millisecond)
  447. //and now we should
  448. content, err := os.ReadFile("./out")
  449. if err != nil {
  450. log.Errorf("Error reading file: %s", err)
  451. }
  452. var alerts []models.Alert
  453. err = json.Unmarshal(content, &alerts)
  454. assert.NoError(t, err)
  455. assert.Equal(t, 4, len(alerts))
  456. //restore config
  457. if err := os.WriteFile("tests/notifications/dummy.yaml", raw, 0644); err != nil {
  458. t.Fatalf("unable to write config file %s", err)
  459. }
  460. }
  461. func TestBrokerRunTimeThreshold(t *testing.T) {
  462. DefaultEmptyTicker = 50 * time.Millisecond
  463. buildDummyPlugin()
  464. setPluginPermTo744()
  465. defer tearDown()
  466. //init
  467. pluginCfg := csconfig.PluginCfg{}
  468. pb := PluginBroker{}
  469. profiles := csconfig.NewDefaultConfig().API.Server.Profiles
  470. profiles = append(profiles, &csconfig.ProfileCfg{
  471. Notifications: []string{"dummy_default"},
  472. })
  473. //set groupwait
  474. raw, cfg := readconfig(t, "tests/notifications/dummy.yaml")
  475. cfg.GroupWait = time.Duration(1 * time.Second) //nolint:unconvert
  476. writeconfig(t, cfg, "tests/notifications/dummy.yaml")
  477. err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
  478. PluginDir: testPath,
  479. NotificationDir: "./tests/notifications",
  480. })
  481. assert.NoError(t, err)
  482. tomb := tomb.Tomb{}
  483. go pb.Run(&tomb)
  484. defer pb.Kill()
  485. //send data
  486. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  487. time.Sleep(200 * time.Millisecond)
  488. //we shouldn't have data yet
  489. assert.NoFileExists(t, "./out")
  490. time.Sleep(1 * time.Second)
  491. //and now we should
  492. content, err := os.ReadFile("./out")
  493. if err != nil {
  494. log.Errorf("Error reading file: %s", err)
  495. }
  496. var alerts []models.Alert
  497. err = json.Unmarshal(content, &alerts)
  498. assert.NoError(t, err)
  499. assert.Equal(t, 1, len(alerts))
  500. //restore config
  501. if err := os.WriteFile("tests/notifications/dummy.yaml", raw, 0644); err != nil {
  502. t.Fatalf("unable to write config file %s", err)
  503. }
  504. }
  505. func TestBrokerRunSimple(t *testing.T) {
  506. DefaultEmptyTicker = 50 * time.Millisecond
  507. buildDummyPlugin()
  508. setPluginPermTo744()
  509. defer tearDown()
  510. pluginCfg := csconfig.PluginCfg{}
  511. pb := PluginBroker{}
  512. profiles := csconfig.NewDefaultConfig().API.Server.Profiles
  513. profiles = append(profiles, &csconfig.ProfileCfg{
  514. Notifications: []string{"dummy_default"},
  515. })
  516. err := pb.Init(&pluginCfg, profiles, &csconfig.ConfigurationPaths{
  517. PluginDir: testPath,
  518. NotificationDir: "./tests/notifications",
  519. })
  520. assert.NoError(t, err)
  521. tomb := tomb.Tomb{}
  522. go pb.Run(&tomb)
  523. defer pb.Kill()
  524. assert.NoFileExists(t, "./out")
  525. defer os.Remove("./out")
  526. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  527. pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
  528. time.Sleep(time.Millisecond * 200)
  529. content, err := os.ReadFile("./out")
  530. if err != nil {
  531. log.Errorf("Error reading file: %s", err)
  532. }
  533. var alerts []models.Alert
  534. err = json.Unmarshal(content, &alerts)
  535. assert.NoError(t, err)
  536. assert.Equal(t, 2, len(alerts))
  537. }
  538. func buildDummyPlugin() {
  539. dir, err := os.MkdirTemp("./tests", "cs_plugin_test")
  540. if err != nil {
  541. log.Fatal(err)
  542. }
  543. cmd := exec.Command("go", "build", "-o", path.Join(dir, "notification-dummy"), "../../plugins/notifications/dummy/")
  544. if err := cmd.Run(); err != nil {
  545. log.Fatal(err)
  546. }
  547. testPath = dir
  548. os.Remove("./out")
  549. }
  550. func setPluginPermTo(perm string) {
  551. if runtime.GOOS != "windows" {
  552. if err := exec.Command("chmod", perm, path.Join(testPath, "notification-dummy")).Run(); err != nil {
  553. log.Fatal(errors.Wrapf(err, "chmod 744 %s", path.Join(testPath, "notification-dummy")))
  554. }
  555. }
  556. }
  557. func setUp() {
  558. dir, err := os.MkdirTemp("./", "cs_plugin_test")
  559. if err != nil {
  560. log.Fatal(err)
  561. }
  562. f, err := os.Create(path.Join(dir, "slack"))
  563. if err != nil {
  564. log.Fatal(err)
  565. }
  566. f.Close()
  567. f, err = os.Create(path.Join(dir, "notification-gitter"))
  568. if err != nil {
  569. log.Fatal(err)
  570. }
  571. f.Close()
  572. err = os.Mkdir(path.Join(dir, "dummy_dir"), 0666)
  573. if err != nil {
  574. log.Fatal(err)
  575. }
  576. testPath = dir
  577. }
  578. func tearDown() {
  579. err := os.RemoveAll(testPath)
  580. if err != nil {
  581. log.Fatal(err)
  582. }
  583. os.Remove("./out")
  584. }