alerts_test.go 16 KB

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