broker_test.go 16 KB

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