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