broker_test.go 16 KB

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