github.com/crowdsecurity/crowdsec@v1.6.1/pkg/apiclient/decisions_service_test.go (about) 1 package apiclient 2 3 import ( 4 "context" 5 "fmt" 6 "net/http" 7 "net/url" 8 "testing" 9 10 log "github.com/sirupsen/logrus" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 14 "github.com/crowdsecurity/go-cs-lib/cstest" 15 "github.com/crowdsecurity/go-cs-lib/ptr" 16 "github.com/crowdsecurity/go-cs-lib/version" 17 18 "github.com/crowdsecurity/crowdsec/pkg/models" 19 "github.com/crowdsecurity/crowdsec/pkg/modelscapi" 20 ) 21 22 func TestDecisionsList(t *testing.T) { 23 log.SetLevel(log.DebugLevel) 24 25 mux, urlx, teardown := setup() 26 defer teardown() 27 28 mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) { 29 testMethod(t, r, "GET") 30 if r.URL.RawQuery == "ip=1.2.3.4" { 31 assert.Equal(t, "ip=1.2.3.4", r.URL.RawQuery) 32 assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) 33 w.WriteHeader(http.StatusOK) 34 w.Write([]byte(`[{"duration":"3h59m55.756182786s","id":4,"origin":"cscli","scenario":"manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'","scope":"Ip","type":"ban","value":"1.2.3.4"}]`)) 35 } else { 36 w.WriteHeader(http.StatusOK) 37 w.Write([]byte(`null`)) 38 //no results 39 } 40 }) 41 42 apiURL, err := url.Parse(urlx + "/") 43 require.NoError(t, err) 44 45 //ok answer 46 auth := &APIKeyTransport{ 47 APIKey: "ixu", 48 } 49 50 newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client()) 51 require.NoError(t, err) 52 53 expected := &models.GetDecisionsResponse{ 54 &models.Decision{ 55 Duration: ptr.Of("3h59m55.756182786s"), 56 ID: 4, 57 Origin: ptr.Of("cscli"), 58 Scenario: ptr.Of("manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"), 59 Scope: ptr.Of("Ip"), 60 Type: ptr.Of("ban"), 61 Value: ptr.Of("1.2.3.4"), 62 }, 63 } 64 65 // OK decisions 66 decisionsFilter := DecisionsListOpts{IPEquals: ptr.Of("1.2.3.4")} 67 decisions, resp, err := newcli.Decisions.List(context.Background(), decisionsFilter) 68 require.NoError(t, err) 69 assert.Equal(t, http.StatusOK, resp.Response.StatusCode) 70 assert.Equal(t, *expected, *decisions) 71 72 //Empty return 73 decisionsFilter = DecisionsListOpts{IPEquals: ptr.Of("1.2.3.5")} 74 decisions, resp, err = newcli.Decisions.List(context.Background(), decisionsFilter) 75 require.NoError(t, err) 76 assert.Equal(t, http.StatusOK, resp.Response.StatusCode) 77 assert.Empty(t, *decisions) 78 } 79 80 func TestDecisionsStream(t *testing.T) { 81 log.SetLevel(log.DebugLevel) 82 83 mux, urlx, teardown := setup() 84 defer teardown() 85 86 mux.HandleFunc("/decisions/stream", func(w http.ResponseWriter, r *http.Request) { 87 assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) 88 testMethod(t, r, http.MethodGet) 89 if r.Method == http.MethodGet { 90 if r.URL.RawQuery == "startup=true" { 91 w.WriteHeader(http.StatusOK) 92 w.Write([]byte(`{"deleted":null,"new":[{"duration":"3h59m55.756182786s","id":4,"origin":"cscli","scenario":"manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'","scope":"Ip","type":"ban","value":"1.2.3.4"}]}`)) 93 } else { 94 w.WriteHeader(http.StatusOK) 95 w.Write([]byte(`{"deleted":null,"new":null}`)) 96 } 97 } 98 }) 99 100 mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) { 101 assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) 102 testMethod(t, r, http.MethodDelete) 103 if r.Method == http.MethodDelete { 104 w.WriteHeader(http.StatusOK) 105 } 106 }) 107 108 apiURL, err := url.Parse(urlx + "/") 109 require.NoError(t, err) 110 111 //ok answer 112 auth := &APIKeyTransport{ 113 APIKey: "ixu", 114 } 115 116 newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client()) 117 require.NoError(t, err) 118 119 expected := &models.DecisionsStreamResponse{ 120 New: models.GetDecisionsResponse{ 121 &models.Decision{ 122 Duration: ptr.Of("3h59m55.756182786s"), 123 ID: 4, 124 Origin: ptr.Of("cscli"), 125 Scenario: ptr.Of("manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"), 126 Scope: ptr.Of("Ip"), 127 Type: ptr.Of("ban"), 128 Value: ptr.Of("1.2.3.4"), 129 }, 130 }, 131 } 132 133 decisions, resp, err := newcli.Decisions.GetStream(context.Background(), DecisionsStreamOpts{Startup: true}) 134 require.NoError(t, err) 135 assert.Equal(t, http.StatusOK, resp.Response.StatusCode) 136 assert.Equal(t, *expected, *decisions) 137 138 //and second call, we get empty lists 139 decisions, resp, err = newcli.Decisions.GetStream(context.Background(), DecisionsStreamOpts{Startup: false}) 140 require.NoError(t, err) 141 assert.Equal(t, http.StatusOK, resp.Response.StatusCode) 142 assert.Empty(t, decisions.New) 143 assert.Empty(t, decisions.Deleted) 144 145 //delete stream 146 resp, err = newcli.Decisions.StopStream(context.Background()) 147 require.NoError(t, err) 148 assert.Equal(t, http.StatusOK, resp.Response.StatusCode) 149 } 150 151 func TestDecisionsStreamV3Compatibility(t *testing.T) { 152 log.SetLevel(log.DebugLevel) 153 154 mux, urlx, teardown := setupWithPrefix("v3") 155 defer teardown() 156 157 mux.HandleFunc("/decisions/stream", func(w http.ResponseWriter, r *http.Request) { 158 assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) 159 testMethod(t, r, http.MethodGet) 160 if r.Method == http.MethodGet { 161 if r.URL.RawQuery == "startup=true" { 162 w.WriteHeader(http.StatusOK) 163 w.Write([]byte(`{"deleted":[{"scope":"ip","decisions":["1.2.3.5"]}],"new":[{"scope":"ip", "scenario": "manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'", "decisions":[{"duration":"3h59m55.756182786s","value":"1.2.3.4"}]}]}`)) 164 } else { 165 w.WriteHeader(http.StatusOK) 166 w.Write([]byte(`{"deleted":null,"new":null}`)) 167 } 168 } 169 }) 170 171 apiURL, err := url.Parse(urlx + "/") 172 require.NoError(t, err) 173 174 //ok answer 175 auth := &APIKeyTransport{ 176 APIKey: "ixu", 177 } 178 179 newcli, err := NewDefaultClient(apiURL, "v3", "toto", auth.Client()) 180 require.NoError(t, err) 181 182 torigin := "CAPI" 183 tscope := "ip" 184 ttype := "ban" 185 expected := &models.DecisionsStreamResponse{ 186 New: models.GetDecisionsResponse{ 187 &models.Decision{ 188 Duration: ptr.Of("3h59m55.756182786s"), 189 Origin: &torigin, 190 Scenario: ptr.Of("manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"), 191 Scope: &tscope, 192 Type: &ttype, 193 Value: ptr.Of("1.2.3.4"), 194 }, 195 }, 196 Deleted: models.GetDecisionsResponse{ 197 &models.Decision{ 198 Duration: ptr.Of("1h"), 199 Origin: &torigin, 200 Scenario: ptr.Of("deleted"), 201 Scope: &tscope, 202 Type: &ttype, 203 Value: ptr.Of("1.2.3.5"), 204 }, 205 }, 206 } 207 208 // GetStream is supposed to consume v3 payload and return v2 response 209 decisions, resp, err := newcli.Decisions.GetStream(context.Background(), DecisionsStreamOpts{Startup: true}) 210 require.NoError(t, err) 211 assert.Equal(t, http.StatusOK, resp.Response.StatusCode) 212 assert.Equal(t, *expected, *decisions) 213 } 214 215 func TestDecisionsStreamV3(t *testing.T) { 216 log.SetLevel(log.DebugLevel) 217 218 mux, urlx, teardown := setupWithPrefix("v3") 219 defer teardown() 220 221 mux.HandleFunc("/decisions/stream", func(w http.ResponseWriter, r *http.Request) { 222 assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) 223 testMethod(t, r, http.MethodGet) 224 if r.Method == http.MethodGet { 225 w.WriteHeader(http.StatusOK) 226 w.Write([]byte(`{"deleted":[{"scope":"ip","decisions":["1.2.3.5"]}], 227 "new":[{"scope":"ip", "scenario": "manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'", "decisions":[{"duration":"3h59m55.756182786s","value":"1.2.3.4"}]}], 228 "links": {"blocklists":[{"name":"blocklist1","url":"/v3/blocklist","scope":"ip","remediation":"ban","duration":"24h"}]}}`)) 229 } 230 }) 231 232 apiURL, err := url.Parse(urlx + "/") 233 require.NoError(t, err) 234 235 //ok answer 236 auth := &APIKeyTransport{ 237 APIKey: "ixu", 238 } 239 240 newcli, err := NewDefaultClient(apiURL, "v3", "toto", auth.Client()) 241 require.NoError(t, err) 242 243 tscope := "ip" 244 expected := &modelscapi.GetDecisionsStreamResponse{ 245 New: modelscapi.GetDecisionsStreamResponseNew{ 246 &modelscapi.GetDecisionsStreamResponseNewItem{ 247 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 248 { 249 Duration: ptr.Of("3h59m55.756182786s"), 250 Value: ptr.Of("1.2.3.4"), 251 }, 252 }, 253 Scenario: ptr.Of("manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"), 254 Scope: &tscope, 255 }, 256 }, 257 Deleted: modelscapi.GetDecisionsStreamResponseDeleted{ 258 &modelscapi.GetDecisionsStreamResponseDeletedItem{ 259 Scope: &tscope, 260 Decisions: []string{ 261 "1.2.3.5", 262 }, 263 }, 264 }, 265 Links: &modelscapi.GetDecisionsStreamResponseLinks{ 266 Blocklists: []*modelscapi.BlocklistLink{ 267 { 268 Duration: ptr.Of("24h"), 269 Name: ptr.Of("blocklist1"), 270 Remediation: ptr.Of("ban"), 271 Scope: ptr.Of("ip"), 272 URL: ptr.Of("/v3/blocklist"), 273 }, 274 }, 275 }, 276 } 277 278 // GetStream is supposed to consume v3 payload and return v2 response 279 decisions, resp, err := newcli.Decisions.GetStreamV3(context.Background(), DecisionsStreamOpts{Startup: true}) 280 require.NoError(t, err) 281 assert.Equal(t, http.StatusOK, resp.Response.StatusCode) 282 assert.Equal(t, *expected, *decisions) 283 } 284 285 func TestDecisionsFromBlocklist(t *testing.T) { 286 log.SetLevel(log.DebugLevel) 287 288 mux, urlx, teardown := setupWithPrefix("v3") 289 defer teardown() 290 291 mux.HandleFunc("/blocklist", func(w http.ResponseWriter, r *http.Request) { 292 testMethod(t, r, http.MethodGet) 293 294 if r.Header.Get("If-Modified-Since") == "Sun, 01 Jan 2023 01:01:01 GMT" { 295 w.WriteHeader(http.StatusNotModified) 296 297 return 298 } 299 300 if r.Method == http.MethodGet { 301 w.WriteHeader(http.StatusOK) 302 w.Write([]byte("1.2.3.4\r\n1.2.3.5")) 303 } 304 }) 305 306 apiURL, err := url.Parse(urlx + "/") 307 require.NoError(t, err) 308 309 //ok answer 310 auth := &APIKeyTransport{ 311 APIKey: "ixu", 312 } 313 314 newcli, err := NewDefaultClient(apiURL, "v3", "toto", auth.Client()) 315 require.NoError(t, err) 316 317 tdurationBlocklist := "24h" 318 tnameBlocklist := "blocklist1" 319 tremediationBlocklist := "ban" 320 tscopeBlocklist := "ip" 321 turlBlocklist := urlx + "/v3/blocklist" 322 torigin := "lists" 323 expected := []*models.Decision{ 324 { 325 Duration: &tdurationBlocklist, 326 Value: ptr.Of("1.2.3.4"), 327 Scenario: &tnameBlocklist, 328 Scope: &tscopeBlocklist, 329 Type: &tremediationBlocklist, 330 Origin: &torigin, 331 }, 332 { 333 Duration: &tdurationBlocklist, 334 Value: ptr.Of("1.2.3.5"), 335 Scenario: &tnameBlocklist, 336 Scope: &tscopeBlocklist, 337 Type: &tremediationBlocklist, 338 Origin: &torigin, 339 }, 340 } 341 decisions, isModified, err := newcli.Decisions.GetDecisionsFromBlocklist(context.Background(), &modelscapi.BlocklistLink{ 342 URL: &turlBlocklist, 343 Scope: &tscopeBlocklist, 344 Remediation: &tremediationBlocklist, 345 Name: &tnameBlocklist, 346 Duration: &tdurationBlocklist, 347 }, nil) 348 require.NoError(t, err) 349 assert.True(t, isModified) 350 351 log.Infof("decision1: %+v", decisions[0]) 352 log.Infof("expected1: %+v", expected[0]) 353 log.Infof("decisions: %s, %s, %s, %s, %s, %s", *decisions[0].Value, *decisions[0].Duration, *decisions[0].Scenario, *decisions[0].Scope, *decisions[0].Type, *decisions[0].Origin) 354 log.Infof("expected : %s, %s, %s, %s, %s", *expected[0].Value, *expected[0].Duration, *expected[0].Scenario, *expected[0].Scope, *expected[0].Type) 355 log.Infof("decisions: %s, %s, %s, %s, %s", *decisions[1].Value, *decisions[1].Duration, *decisions[1].Scenario, *decisions[1].Scope, *decisions[1].Type) 356 357 assert.Equal(t, expected, decisions) 358 359 // test cache control 360 _, isModified, err = newcli.Decisions.GetDecisionsFromBlocklist(context.Background(), &modelscapi.BlocklistLink{ 361 URL: &turlBlocklist, 362 Scope: &tscopeBlocklist, 363 Remediation: &tremediationBlocklist, 364 Name: &tnameBlocklist, 365 Duration: &tdurationBlocklist, 366 }, ptr.Of("Sun, 01 Jan 2023 01:01:01 GMT")) 367 368 require.NoError(t, err) 369 assert.False(t, isModified) 370 371 _, isModified, err = newcli.Decisions.GetDecisionsFromBlocklist(context.Background(), &modelscapi.BlocklistLink{ 372 URL: &turlBlocklist, 373 Scope: &tscopeBlocklist, 374 Remediation: &tremediationBlocklist, 375 Name: &tnameBlocklist, 376 Duration: &tdurationBlocklist, 377 }, ptr.Of("Mon, 02 Jan 2023 01:01:01 GMT")) 378 379 require.NoError(t, err) 380 assert.True(t, isModified) 381 } 382 383 func TestDeleteDecisions(t *testing.T) { 384 mux, urlx, teardown := setup() 385 mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) { 386 w.WriteHeader(http.StatusOK) 387 w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`)) 388 }) 389 390 mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) { 391 testMethod(t, r, "DELETE") 392 assert.Equal(t, "ip=1.2.3.4", r.URL.RawQuery) 393 w.WriteHeader(http.StatusOK) 394 w.Write([]byte(`{"nbDeleted":"1"}`)) 395 //w.Write([]byte(`{"message":"0 deleted alerts"}`)) 396 }) 397 398 log.Printf("URL is %s", urlx) 399 400 apiURL, err := url.Parse(urlx + "/") 401 require.NoError(t, err) 402 403 client, err := NewClient(&Config{ 404 MachineID: "test_login", 405 Password: "test_password", 406 UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), 407 URL: apiURL, 408 VersionPrefix: "v1", 409 }) 410 require.NoError(t, err) 411 412 filters := DecisionsDeleteOpts{IPEquals: new(string)} 413 *filters.IPEquals = "1.2.3.4" 414 415 deleted, _, err := client.Decisions.Delete(context.Background(), filters) 416 require.NoError(t, err) 417 assert.Equal(t, "1", deleted.NbDeleted) 418 419 defer teardown() 420 } 421 422 func TestDecisionsStreamOpts_addQueryParamsToURL(t *testing.T) { 423 baseURLString := "http://localhost:8080/v1/decisions/stream" 424 425 type fields struct { 426 Startup bool 427 Scopes string 428 ScenariosContaining string 429 ScenariosNotContaining string 430 } 431 432 tests := []struct { 433 name string 434 fields fields 435 expected string 436 expectedErr string 437 }{ 438 { 439 name: "no filter", 440 expected: baseURLString + "?", 441 }, 442 { 443 name: "startup=true", 444 fields: fields{ 445 Startup: true, 446 }, 447 expected: baseURLString + "?startup=true", 448 }, 449 { 450 name: "set all params", 451 fields: fields{ 452 Startup: true, 453 Scopes: "ip,range", 454 ScenariosContaining: "ssh", 455 ScenariosNotContaining: "bf", 456 }, 457 expected: baseURLString + "?scenarios_containing=ssh&scenarios_not_containing=bf&scopes=ip%2Crange&startup=true", 458 }, 459 } 460 461 for _, tt := range tests { 462 tt := tt 463 t.Run(tt.name, func(t *testing.T) { 464 o := &DecisionsStreamOpts{ 465 Startup: tt.fields.Startup, 466 Scopes: tt.fields.Scopes, 467 ScenariosContaining: tt.fields.ScenariosContaining, 468 ScenariosNotContaining: tt.fields.ScenariosNotContaining, 469 } 470 471 got, err := o.addQueryParamsToURL(baseURLString) 472 cstest.RequireErrorContains(t, err, tt.expectedErr) 473 if tt.expectedErr != "" { 474 return 475 } 476 477 gotURL, err := url.Parse(got) 478 require.NoError(t, err) 479 480 expectedURL, err := url.Parse(tt.expected) 481 require.NoError(t, err) 482 483 assert.Equal(t, *expectedURL, *gotURL) 484 }) 485 } 486 } 487 488 // func TestDeleteOneDecision(t *testing.T) { 489 // mux, urlx, teardown := setup() 490 // mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) { 491 // w.WriteHeader(http.StatusOK) 492 // w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`)) 493 // }) 494 // mux.HandleFunc("/decisions/1", func(w http.ResponseWriter, r *http.Request) { 495 // testMethod(t, r, "DELETE") 496 // w.WriteHeader(http.StatusOK) 497 // w.Write([]byte(`{"nbDeleted":"1"}`)) 498 // }) 499 // log.Printf("URL is %s", urlx) 500 // apiURL, err := url.Parse(urlx + "/") 501 // if err != nil { 502 // t.Fatalf("parsing api url: %s", apiURL) 503 // } 504 // client, err := NewClient(&Config{ 505 // MachineID: "test_login", 506 // Password: "test_password", 507 // UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()), 508 // URL: apiURL, 509 // VersionPrefix: "v1", 510 // }) 511 512 // if err != nil { 513 // t.Fatalf("new api client: %s", err) 514 // } 515 516 // filters := DecisionsDeleteOpts{IPEquals: new(string)} 517 // *filters.IPEquals = "1.2.3.4" 518 // deleted, _, err := client.Decisions.Delete(context.Background(), filters) 519 // if err != nil { 520 // t.Fatalf("unexpected err : %s", err) 521 // } 522 // assert.Equal(t, "1", deleted.NbDeleted) 523 524 // defer teardown() 525 // }