alerts_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. package apiserver
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "net/http/httptest"
  7. "strings"
  8. "sync"
  9. "testing"
  10. "github.com/crowdsecurity/crowdsec/pkg/csconfig"
  11. "github.com/crowdsecurity/crowdsec/pkg/csplugin"
  12. "github.com/crowdsecurity/crowdsec/pkg/models"
  13. "github.com/gin-gonic/gin"
  14. log "github.com/sirupsen/logrus"
  15. "github.com/stretchr/testify/assert"
  16. )
  17. type LAPI struct {
  18. router *gin.Engine
  19. loginResp models.WatcherAuthResponse
  20. bouncerKey string
  21. t *testing.T
  22. }
  23. func SetupLAPITest(t *testing.T) LAPI {
  24. t.Helper()
  25. router, loginResp, config, err := InitMachineTest()
  26. if err != nil {
  27. t.Fatal(err.Error())
  28. }
  29. APIKey, err := CreateTestBouncer(config.API.Server.DbConfig)
  30. if err != nil {
  31. t.Fatalf("%s", err.Error())
  32. }
  33. return LAPI{
  34. router: router,
  35. loginResp: loginResp,
  36. bouncerKey: APIKey,
  37. }
  38. }
  39. func (l *LAPI) InsertAlertFromFile(path string) *httptest.ResponseRecorder {
  40. alertReader := GetAlertReaderFromFile(path)
  41. return l.RecordResponse("POST", "/v1/alerts", alertReader)
  42. }
  43. func (l *LAPI) RecordResponse(verb string, url string, body *strings.Reader) *httptest.ResponseRecorder {
  44. w := httptest.NewRecorder()
  45. req, err := http.NewRequest(verb, url, body)
  46. if err != nil {
  47. l.t.Fatal(err)
  48. }
  49. req.Header.Add("X-Api-Key", l.bouncerKey)
  50. AddAuthHeaders(req, l.loginResp)
  51. l.router.ServeHTTP(w, req)
  52. return w
  53. }
  54. func InitMachineTest() (*gin.Engine, models.WatcherAuthResponse, csconfig.Config, error) {
  55. router, config, err := NewAPITest()
  56. if err != nil {
  57. return nil, models.WatcherAuthResponse{}, config, fmt.Errorf("unable to run local API: %s", err)
  58. }
  59. loginResp, err := LoginToTestAPI(router, config)
  60. if err != nil {
  61. return nil, models.WatcherAuthResponse{}, config, fmt.Errorf("%s", err.Error())
  62. }
  63. return router, loginResp, config, nil
  64. }
  65. func LoginToTestAPI(router *gin.Engine, config csconfig.Config) (models.WatcherAuthResponse, error) {
  66. body, err := CreateTestMachine(router)
  67. if err != nil {
  68. return models.WatcherAuthResponse{}, fmt.Errorf("%s", err.Error())
  69. }
  70. err = ValidateMachine("test", config.API.Server.DbConfig)
  71. if err != nil {
  72. log.Fatalln(err.Error())
  73. }
  74. w := httptest.NewRecorder()
  75. req, _ := http.NewRequest("POST", "/v1/watchers/login", strings.NewReader(body))
  76. req.Header.Add("User-Agent", UserAgent)
  77. router.ServeHTTP(w, req)
  78. loginResp := models.WatcherAuthResponse{}
  79. err = json.NewDecoder(w.Body).Decode(&loginResp)
  80. if err != nil {
  81. return models.WatcherAuthResponse{}, fmt.Errorf("%s", err.Error())
  82. }
  83. return loginResp, nil
  84. }
  85. func AddAuthHeaders(request *http.Request, authResponse models.WatcherAuthResponse) {
  86. request.Header.Add("User-Agent", UserAgent)
  87. request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authResponse.Token))
  88. }
  89. func TestSimulatedAlert(t *testing.T) {
  90. lapi := SetupLAPITest(t)
  91. lapi.InsertAlertFromFile("./tests/alert_minibulk+simul.json")
  92. alertContent := GetAlertReaderFromFile("./tests/alert_minibulk+simul.json")
  93. //exclude decision in simulation mode
  94. w := lapi.RecordResponse("GET", "/v1/alerts?simulated=false", alertContent)
  95. assert.Equal(t, 200, w.Code)
  96. assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over `)
  97. assert.NotContains(t, w.Body.String(), `"message":"Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over `)
  98. //include decision in simulation mode
  99. w = lapi.RecordResponse("GET", "/v1/alerts?simulated=true", alertContent)
  100. assert.Equal(t, 200, w.Code)
  101. assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over `)
  102. assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over `)
  103. }
  104. func TestCreateAlert(t *testing.T) {
  105. lapi := SetupLAPITest(t)
  106. // Create Alert with invalid format
  107. w := lapi.RecordResponse("POST", "/v1/alerts", strings.NewReader("test"))
  108. assert.Equal(t, 400, w.Code)
  109. assert.Equal(t, "{\"message\":\"invalid character 'e' in literal true (expecting 'r')\"}", w.Body.String())
  110. // Create Alert with invalid input
  111. alertContent := GetAlertReaderFromFile("./tests/invalidAlert_sample.json")
  112. w = lapi.RecordResponse("POST", "/v1/alerts", alertContent)
  113. assert.Equal(t, 500, w.Code)
  114. assert.Equal(t, "{\"message\":\"validation failure list:\\n0.scenario in body is required\\n0.scenario_hash in body is required\\n0.scenario_version in body is required\\n0.simulated in body is required\\n0.source in body is required\"}", w.Body.String())
  115. // Create Valid Alert
  116. w = lapi.InsertAlertFromFile("./tests/alert_sample.json")
  117. assert.Equal(t, 201, w.Code)
  118. assert.Equal(t, "[\"1\"]", w.Body.String())
  119. }
  120. func TestCreateAlertChannels(t *testing.T) {
  121. apiServer, config, err := NewAPIServer()
  122. if err != nil {
  123. log.Fatalln(err.Error())
  124. }
  125. apiServer.controller.PluginChannel = make(chan csplugin.ProfileAlert)
  126. apiServer.InitController()
  127. loginResp, err := LoginToTestAPI(apiServer.router, config)
  128. if err != nil {
  129. log.Fatalln(err.Error())
  130. }
  131. lapi := LAPI{router: apiServer.router, loginResp: loginResp}
  132. var pd csplugin.ProfileAlert
  133. var wg sync.WaitGroup
  134. wg.Add(1)
  135. go func() {
  136. pd = <-apiServer.controller.PluginChannel
  137. wg.Done()
  138. }()
  139. go lapi.InsertAlertFromFile("./tests/alert_ssh-bf.json")
  140. wg.Wait()
  141. assert.Equal(t, len(pd.Alert.Decisions), 1)
  142. apiServer.Close()
  143. }
  144. func TestAlertListFilters(t *testing.T) {
  145. lapi := SetupLAPITest(t)
  146. lapi.InsertAlertFromFile("./tests/alert_ssh-bf.json")
  147. alertContent := GetAlertReaderFromFile("./tests/alert_ssh-bf.json")
  148. //bad filter
  149. w := lapi.RecordResponse("GET", "/v1/alerts?test=test", alertContent)
  150. assert.Equal(t, 500, w.Code)
  151. assert.Equal(t, "{\"message\":\"Filter parameter 'test' is unknown (=test): invalid filter\"}", w.Body.String())
  152. //get without filters
  153. w = lapi.RecordResponse("GET", "/v1/alerts", emptyBody)
  154. assert.Equal(t, 200, w.Code)
  155. //check alert and decision
  156. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  157. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  158. //test decision_type filter (ok)
  159. w = lapi.RecordResponse("GET", "/v1/alerts?decision_type=ban", emptyBody)
  160. assert.Equal(t, 200, w.Code)
  161. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  162. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  163. //test decision_type filter (bad value)
  164. w = lapi.RecordResponse("GET", "/v1/alerts?decision_type=ratata", emptyBody)
  165. assert.Equal(t, 200, w.Code)
  166. assert.Equal(t, "null", w.Body.String())
  167. //test scope (ok)
  168. w = lapi.RecordResponse("GET", "/v1/alerts?scope=Ip", emptyBody)
  169. assert.Equal(t, 200, w.Code)
  170. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  171. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  172. //test scope (bad value)
  173. w = lapi.RecordResponse("GET", "/v1/alerts?scope=rarara", emptyBody)
  174. assert.Equal(t, 200, w.Code)
  175. assert.Equal(t, "null", w.Body.String())
  176. //test scenario (ok)
  177. w = lapi.RecordResponse("GET", "/v1/alerts?scenario=crowdsecurity/ssh-bf", emptyBody)
  178. assert.Equal(t, 200, w.Code)
  179. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  180. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  181. //test scenario (bad value)
  182. w = lapi.RecordResponse("GET", "/v1/alerts?scenario=crowdsecurity/nope", emptyBody)
  183. assert.Equal(t, 200, w.Code)
  184. assert.Equal(t, "null", w.Body.String())
  185. //test ip (ok)
  186. w = lapi.RecordResponse("GET", "/v1/alerts?ip=91.121.79.195", emptyBody)
  187. assert.Equal(t, 200, w.Code)
  188. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  189. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  190. //test ip (bad value)
  191. w = lapi.RecordResponse("GET", "/v1/alerts?ip=99.122.77.195", emptyBody)
  192. assert.Equal(t, 200, w.Code)
  193. assert.Equal(t, "null", w.Body.String())
  194. //test ip (invalid value)
  195. w = lapi.RecordResponse("GET", "/v1/alerts?ip=gruueq", emptyBody)
  196. assert.Equal(t, 500, w.Code)
  197. assert.Equal(t, `{"message":"unable to convert 'gruueq' to int: invalid address: invalid ip address / range"}`, w.Body.String())
  198. //test range (ok)
  199. w = lapi.RecordResponse("GET", "/v1/alerts?range=91.121.79.0/24&contains=false", emptyBody)
  200. assert.Equal(t, 200, w.Code)
  201. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  202. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  203. //test range
  204. w = lapi.RecordResponse("GET", "/v1/alerts?range=99.122.77.0/24&contains=false", emptyBody)
  205. assert.Equal(t, 200, w.Code)
  206. assert.Equal(t, "null", w.Body.String())
  207. //test range (invalid value)
  208. w = lapi.RecordResponse("GET", "/v1/alerts?range=ratata", emptyBody)
  209. assert.Equal(t, 500, w.Code)
  210. assert.Equal(t, `{"message":"unable to convert 'ratata' to int: invalid address: invalid ip address / range"}`, w.Body.String())
  211. //test since (ok)
  212. w = lapi.RecordResponse("GET", "/v1/alerts?since=1h", emptyBody)
  213. assert.Equal(t, 200, w.Code)
  214. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  215. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  216. //test since (ok but yields no results)
  217. w = lapi.RecordResponse("GET", "/v1/alerts?since=1ns", emptyBody)
  218. assert.Equal(t, 200, w.Code)
  219. assert.Equal(t, "null", w.Body.String())
  220. //test since (invalid value)
  221. w = lapi.RecordResponse("GET", "/v1/alerts?since=1zuzu", emptyBody)
  222. assert.Equal(t, 500, w.Code)
  223. assert.Contains(t, w.Body.String(), `{"message":"while parsing duration: time: unknown unit`)
  224. //test until (ok)
  225. w = lapi.RecordResponse("GET", "/v1/alerts?until=1ns", emptyBody)
  226. assert.Equal(t, 200, w.Code)
  227. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  228. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  229. //test until (ok but no return)
  230. w = lapi.RecordResponse("GET", "/v1/alerts?until=1m", emptyBody)
  231. assert.Equal(t, 200, w.Code)
  232. assert.Equal(t, "null", w.Body.String())
  233. //test until (invalid value)
  234. w = lapi.RecordResponse("GET", "/v1/alerts?until=1zuzu", emptyBody)
  235. assert.Equal(t, 500, w.Code)
  236. assert.Contains(t, w.Body.String(), `{"message":"while parsing duration: time: unknown unit`)
  237. //test simulated (ok)
  238. w = lapi.RecordResponse("GET", "/v1/alerts?simulated=true", emptyBody)
  239. assert.Equal(t, 200, w.Code)
  240. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  241. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  242. //test simulated (ok)
  243. w = lapi.RecordResponse("GET", "/v1/alerts?simulated=false", emptyBody)
  244. assert.Equal(t, 200, w.Code)
  245. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  246. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  247. //test has active decision
  248. w = lapi.RecordResponse("GET", "/v1/alerts?has_active_decision=true", emptyBody)
  249. assert.Equal(t, 200, w.Code)
  250. assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
  251. assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
  252. //test has active decision
  253. w = lapi.RecordResponse("GET", "/v1/alerts?has_active_decision=false", emptyBody)
  254. assert.Equal(t, 200, w.Code)
  255. assert.Equal(t, "null", w.Body.String())
  256. //test has active decision (invalid value)
  257. w = lapi.RecordResponse("GET", "/v1/alerts?has_active_decision=ratatqata", emptyBody)
  258. assert.Equal(t, 500, w.Code)
  259. assert.Equal(t, `{"message":"'ratatqata' is not a boolean: strconv.ParseBool: parsing \"ratatqata\": invalid syntax: unable to parse type"}`, w.Body.String())
  260. }
  261. func TestAlertBulkInsert(t *testing.T) {
  262. lapi := SetupLAPITest(t)
  263. //insert a bulk of 20 alerts to trigger bulk insert
  264. lapi.InsertAlertFromFile("./tests/alert_bulk.json")
  265. alertContent := GetAlertReaderFromFile("./tests/alert_bulk.json")
  266. w := lapi.RecordResponse("GET", "/v1/alerts", alertContent)
  267. assert.Equal(t, 200, w.Code)
  268. }
  269. func TestListAlert(t *testing.T) {
  270. lapi := SetupLAPITest(t)
  271. lapi.InsertAlertFromFile("./tests/alert_sample.json")
  272. // List Alert with invalid filter
  273. w := lapi.RecordResponse("GET", "/v1/alerts?test=test", emptyBody)
  274. assert.Equal(t, 500, w.Code)
  275. assert.Equal(t, "{\"message\":\"Filter parameter 'test' is unknown (=test): invalid filter\"}", w.Body.String())
  276. // List Alert
  277. w = lapi.RecordResponse("GET", "/v1/alerts", emptyBody)
  278. assert.Equal(t, 200, w.Code)
  279. assert.Contains(t, w.Body.String(), "crowdsecurity/test")
  280. }
  281. func TestCreateAlertErrors(t *testing.T) {
  282. lapi := SetupLAPITest(t)
  283. alertContent := GetAlertReaderFromFile("./tests/alert_sample.json")
  284. //test invalid bearer
  285. w := httptest.NewRecorder()
  286. req, _ := http.NewRequest("POST", "/v1/alerts", alertContent)
  287. req.Header.Add("User-Agent", UserAgent)
  288. req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "ratata"))
  289. lapi.router.ServeHTTP(w, req)
  290. assert.Equal(t, 401, w.Code)
  291. //test invalid bearer
  292. w = httptest.NewRecorder()
  293. req, _ = http.NewRequest("POST", "/v1/alerts", alertContent)
  294. req.Header.Add("User-Agent", UserAgent)
  295. req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", lapi.loginResp.Token+"s"))
  296. lapi.router.ServeHTTP(w, req)
  297. assert.Equal(t, 401, w.Code)
  298. }
  299. func TestDeleteAlert(t *testing.T) {
  300. lapi := SetupLAPITest(t)
  301. lapi.InsertAlertFromFile("./tests/alert_sample.json")
  302. // Fail Delete Alert
  303. w := httptest.NewRecorder()
  304. req, _ := http.NewRequest("DELETE", "/v1/alerts", strings.NewReader(""))
  305. AddAuthHeaders(req, lapi.loginResp)
  306. req.RemoteAddr = "127.0.0.2:4242"
  307. lapi.router.ServeHTTP(w, req)
  308. assert.Equal(t, 403, w.Code)
  309. assert.Equal(t, `{"message":"access forbidden from this IP (127.0.0.2)"}`, w.Body.String())
  310. // Delete Alert
  311. w = httptest.NewRecorder()
  312. req, _ = http.NewRequest("DELETE", "/v1/alerts", strings.NewReader(""))
  313. AddAuthHeaders(req, lapi.loginResp)
  314. req.RemoteAddr = "127.0.0.1:4242"
  315. lapi.router.ServeHTTP(w, req)
  316. assert.Equal(t, 200, w.Code)
  317. assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
  318. }
  319. func TestDeleteAlertTrustedIPS(t *testing.T) {
  320. cfg := LoadTestConfig()
  321. // IPv6 mocking doesn't seem to work.
  322. // cfg.API.Server.TrustedIPs = []string{"1.2.3.4", "1.2.4.0/24", "::"}
  323. cfg.API.Server.TrustedIPs = []string{"1.2.3.4", "1.2.4.0/24"}
  324. cfg.API.Server.ListenURI = "::8080"
  325. server, err := NewServer(cfg.API.Server)
  326. if err != nil {
  327. log.Fatal(err.Error())
  328. }
  329. err = server.InitController()
  330. if err != nil {
  331. log.Fatal(err.Error())
  332. }
  333. router, err := server.Router()
  334. if err != nil {
  335. log.Fatal(err.Error())
  336. }
  337. loginResp, err := LoginToTestAPI(router, cfg)
  338. if err != nil {
  339. log.Fatal(err.Error())
  340. }
  341. lapi := LAPI{
  342. router: router,
  343. loginResp: loginResp,
  344. t: t,
  345. }
  346. assertAlertDeleteFailedFromIP := func(ip string) {
  347. w := httptest.NewRecorder()
  348. req, _ := http.NewRequest("DELETE", "/v1/alerts", strings.NewReader(""))
  349. AddAuthHeaders(req, loginResp)
  350. req.RemoteAddr = ip + ":1234"
  351. router.ServeHTTP(w, req)
  352. assert.Equal(t, 403, w.Code)
  353. assert.Contains(t, w.Body.String(), fmt.Sprintf(`{"message":"access forbidden from this IP (%s)"}`, ip))
  354. }
  355. assertAlertDeletedFromIP := func(ip string) {
  356. w := httptest.NewRecorder()
  357. req, _ := http.NewRequest("DELETE", "/v1/alerts", strings.NewReader(""))
  358. AddAuthHeaders(req, loginResp)
  359. req.RemoteAddr = ip + ":1234"
  360. router.ServeHTTP(w, req)
  361. assert.Equal(t, 200, w.Code)
  362. assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
  363. }
  364. lapi.InsertAlertFromFile("./tests/alert_sample.json")
  365. assertAlertDeleteFailedFromIP("4.3.2.1")
  366. assertAlertDeletedFromIP("1.2.3.4")
  367. lapi.InsertAlertFromFile("./tests/alert_sample.json")
  368. assertAlertDeletedFromIP("1.2.4.0")
  369. lapi.InsertAlertFromFile("./tests/alert_sample.json")
  370. assertAlertDeletedFromIP("1.2.4.1")
  371. lapi.InsertAlertFromFile("./tests/alert_sample.json")
  372. assertAlertDeletedFromIP("1.2.4.255")
  373. lapi.InsertAlertFromFile("./tests/alert_sample.json")
  374. assertAlertDeletedFromIP("127.0.0.1")
  375. }