alerts_test.go 17 KB

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