broker_test.go 16 KB

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