github.com/crowdsecurity/crowdsec@v1.6.1/pkg/apiserver/apic_test.go (about) 1 package apiserver 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "net" 9 "net/http" 10 "net/url" 11 "os" 12 "reflect" 13 "sync" 14 "testing" 15 "time" 16 17 "github.com/jarcoal/httpmock" 18 "github.com/sirupsen/logrus" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 "gopkg.in/tomb.v2" 22 23 "github.com/crowdsecurity/go-cs-lib/cstest" 24 "github.com/crowdsecurity/go-cs-lib/ptr" 25 "github.com/crowdsecurity/go-cs-lib/version" 26 27 "github.com/crowdsecurity/crowdsec/pkg/apiclient" 28 "github.com/crowdsecurity/crowdsec/pkg/csconfig" 29 "github.com/crowdsecurity/crowdsec/pkg/database" 30 "github.com/crowdsecurity/crowdsec/pkg/database/ent/decision" 31 "github.com/crowdsecurity/crowdsec/pkg/database/ent/machine" 32 "github.com/crowdsecurity/crowdsec/pkg/models" 33 "github.com/crowdsecurity/crowdsec/pkg/modelscapi" 34 "github.com/crowdsecurity/crowdsec/pkg/types" 35 ) 36 37 func getDBClient(t *testing.T) *database.Client { 38 t.Helper() 39 40 dbPath, err := os.CreateTemp("", "*sqlite") 41 require.NoError(t, err) 42 dbClient, err := database.NewClient(&csconfig.DatabaseCfg{ 43 Type: "sqlite", 44 DbName: "crowdsec", 45 DbPath: dbPath.Name(), 46 }) 47 require.NoError(t, err) 48 49 return dbClient 50 } 51 52 func getAPIC(t *testing.T) *apic { 53 t.Helper() 54 dbClient := getDBClient(t) 55 56 return &apic{ 57 AlertsAddChan: make(chan []*models.Alert), 58 //DecisionDeleteChan: make(chan []*models.Decision), 59 dbClient: dbClient, 60 mu: sync.Mutex{}, 61 startup: true, 62 pullTomb: tomb.Tomb{}, 63 pushTomb: tomb.Tomb{}, 64 metricsTomb: tomb.Tomb{}, 65 scenarioList: make([]string, 0), 66 consoleConfig: &csconfig.ConsoleConfig{ 67 ShareManualDecisions: ptr.Of(false), 68 ShareTaintedScenarios: ptr.Of(false), 69 ShareCustomScenarios: ptr.Of(false), 70 ShareContext: ptr.Of(false), 71 }, 72 isPulling: make(chan bool, 1), 73 } 74 } 75 76 func absDiff(a int, b int) int { 77 c := a - b 78 if c < 0 { 79 return -1 * c 80 } 81 82 return c 83 } 84 85 func assertTotalDecisionCount(t *testing.T, dbClient *database.Client, count int) { 86 d := dbClient.Ent.Decision.Query().AllX(context.Background()) 87 assert.Len(t, d, count) 88 } 89 90 func assertTotalValidDecisionCount(t *testing.T, dbClient *database.Client, count int) { 91 d := dbClient.Ent.Decision.Query().Where( 92 decision.UntilGT(time.Now()), 93 ).AllX(context.Background()) 94 assert.Len(t, d, count) 95 } 96 97 func jsonMarshalX(v interface{}) []byte { 98 data, err := json.Marshal(v) 99 if err != nil { 100 panic(err) 101 } 102 103 return data 104 } 105 106 func assertTotalAlertCount(t *testing.T, dbClient *database.Client, count int) { 107 d := dbClient.Ent.Alert.Query().AllX(context.Background()) 108 assert.Len(t, d, count) 109 } 110 111 func TestAPICCAPIPullIsOld(t *testing.T) { 112 api := getAPIC(t) 113 114 isOld, err := api.CAPIPullIsOld() 115 require.NoError(t, err) 116 assert.True(t, isOld) 117 118 decision := api.dbClient.Ent.Decision.Create(). 119 SetUntil(time.Now().Add(time.Hour)). 120 SetScenario("crowdsec/test"). 121 SetType("IP"). 122 SetScope("Country"). 123 SetValue("Blah"). 124 SetOrigin(types.CAPIOrigin). 125 SaveX(context.Background()) 126 127 api.dbClient.Ent.Alert.Create(). 128 SetCreatedAt(time.Now()). 129 SetScenario("crowdsec/test"). 130 AddDecisions( 131 decision, 132 ). 133 SaveX(context.Background()) 134 135 isOld, err = api.CAPIPullIsOld() 136 require.NoError(t, err) 137 138 assert.False(t, isOld) 139 } 140 141 func TestAPICFetchScenariosListFromDB(t *testing.T) { 142 tests := []struct { 143 name string 144 machineIDsWithScenarios map[string]string 145 expectedScenarios []string 146 }{ 147 { 148 name: "Simple one machine with two scenarios", 149 machineIDsWithScenarios: map[string]string{ 150 "a": "crowdsecurity/http-bf,crowdsecurity/ssh-bf", 151 }, 152 expectedScenarios: []string{"crowdsecurity/ssh-bf", "crowdsecurity/http-bf"}, 153 }, 154 { 155 name: "Multi machine with custom+hub scenarios", 156 machineIDsWithScenarios: map[string]string{ 157 "a": "crowdsecurity/http-bf,crowdsecurity/ssh-bf,my_scenario", 158 "b": "crowdsecurity/http-bf,crowdsecurity/ssh-bf,foo_scenario", 159 }, 160 expectedScenarios: []string{"crowdsecurity/ssh-bf", "crowdsecurity/http-bf", "my_scenario", "foo_scenario"}, 161 }, 162 } 163 164 for _, tc := range tests { 165 tc := tc 166 t.Run(tc.name, func(t *testing.T) { 167 api := getAPIC(t) 168 for machineID, scenarios := range tc.machineIDsWithScenarios { 169 api.dbClient.Ent.Machine.Create(). 170 SetMachineId(machineID). 171 SetPassword(testPassword.String()). 172 SetIpAddress("1.2.3.4"). 173 SetScenarios(scenarios). 174 ExecX(context.Background()) 175 } 176 177 scenarios, err := api.FetchScenariosListFromDB() 178 for machineID := range tc.machineIDsWithScenarios { 179 api.dbClient.Ent.Machine.Delete().Where(machine.MachineIdEQ(machineID)).ExecX(context.Background()) 180 } 181 require.NoError(t, err) 182 183 assert.ElementsMatch(t, tc.expectedScenarios, scenarios) 184 }) 185 } 186 } 187 188 func TestNewAPIC(t *testing.T) { 189 var testConfig *csconfig.OnlineApiClientCfg 190 191 setConfig := func() { 192 testConfig = &csconfig.OnlineApiClientCfg{ 193 Credentials: &csconfig.ApiCredentialsCfg{ 194 URL: "http://foobar/", 195 Login: "foo", 196 Password: "bar", 197 }, 198 } 199 } 200 201 type args struct { 202 dbClient *database.Client 203 consoleConfig *csconfig.ConsoleConfig 204 } 205 206 tests := []struct { 207 name string 208 args args 209 expectedErr string 210 action func() 211 }{ 212 { 213 name: "simple", 214 action: func() {}, 215 args: args{ 216 dbClient: getDBClient(t), 217 consoleConfig: LoadTestConfig(t).API.Server.ConsoleConfig, 218 }, 219 }, 220 { 221 name: "error in parsing URL", 222 action: func() { testConfig.Credentials.URL = "foobar http://" }, 223 args: args{ 224 dbClient: getDBClient(t), 225 consoleConfig: LoadTestConfig(t).API.Server.ConsoleConfig, 226 }, 227 expectedErr: "first path segment in URL cannot contain colon", 228 }, 229 } 230 231 for _, tc := range tests { 232 tc := tc 233 t.Run(tc.name, func(t *testing.T) { 234 setConfig() 235 httpmock.Activate() 236 defer httpmock.DeactivateAndReset() 237 httpmock.RegisterResponder("POST", "http://foobar/v3/watchers/login", httpmock.NewBytesResponder( 238 200, jsonMarshalX( 239 models.WatcherAuthResponse{ 240 Code: 200, 241 Expire: "2023-01-12T22:51:43Z", 242 Token: "MyToken", 243 }, 244 ), 245 )) 246 tc.action() 247 _, err := NewAPIC(testConfig, tc.args.dbClient, tc.args.consoleConfig, nil) 248 cstest.RequireErrorContains(t, err, tc.expectedErr) 249 }) 250 } 251 } 252 253 func TestAPICHandleDeletedDecisions(t *testing.T) { 254 api := getAPIC(t) 255 _, deleteCounters := makeAddAndDeleteCounters() 256 257 decision1 := api.dbClient.Ent.Decision.Create(). 258 SetUntil(time.Now().Add(time.Hour)). 259 SetScenario("crowdsec/test"). 260 SetType("ban"). 261 SetScope("IP"). 262 SetValue("1.2.3.4"). 263 SetOrigin(types.CAPIOrigin). 264 SaveX(context.Background()) 265 266 api.dbClient.Ent.Decision.Create(). 267 SetUntil(time.Now().Add(time.Hour)). 268 SetScenario("crowdsec/test"). 269 SetType("ban"). 270 SetScope("IP"). 271 SetValue("1.2.3.4"). 272 SetOrigin(types.CAPIOrigin). 273 SaveX(context.Background()) 274 275 assertTotalDecisionCount(t, api.dbClient, 2) 276 277 nbDeleted, err := api.HandleDeletedDecisions([]*models.Decision{{ 278 Value: ptr.Of("1.2.3.4"), 279 Origin: ptr.Of(types.CAPIOrigin), 280 Type: &decision1.Type, 281 Scenario: ptr.Of("crowdsec/test"), 282 Scope: ptr.Of("IP"), 283 }}, deleteCounters) 284 285 require.NoError(t, err) 286 assert.Equal(t, 2, nbDeleted) 287 assert.Equal(t, 2, deleteCounters[types.CAPIOrigin]["all"]) 288 } 289 290 func TestAPICGetMetrics(t *testing.T) { 291 cleanUp := func(api *apic) { 292 api.dbClient.Ent.Bouncer.Delete().ExecX(context.Background()) 293 api.dbClient.Ent.Machine.Delete().ExecX(context.Background()) 294 } 295 tests := []struct { 296 name string 297 machineIDs []string 298 bouncers []string 299 expectedMetric *models.Metrics 300 }{ 301 { 302 name: "no bouncers nor machines should still have bouncers/machines keys in output", 303 machineIDs: []string{}, 304 bouncers: []string{}, 305 expectedMetric: &models.Metrics{ 306 ApilVersion: ptr.Of(version.String()), 307 Bouncers: []*models.MetricsBouncerInfo{}, 308 Machines: []*models.MetricsAgentInfo{}, 309 }, 310 }, 311 { 312 name: "simple", 313 machineIDs: []string{"a", "b", "c"}, 314 bouncers: []string{"1", "2", "3"}, 315 expectedMetric: &models.Metrics{ 316 ApilVersion: ptr.Of(version.String()), 317 Bouncers: []*models.MetricsBouncerInfo{ 318 { 319 CustomName: "1", 320 LastPull: time.Time{}.Format(time.RFC3339), 321 }, { 322 CustomName: "2", 323 LastPull: time.Time{}.Format(time.RFC3339), 324 }, { 325 CustomName: "3", 326 LastPull: time.Time{}.Format(time.RFC3339), 327 }, 328 }, 329 Machines: []*models.MetricsAgentInfo{ 330 { 331 Name: "a", 332 LastPush: time.Time{}.Format(time.RFC3339), 333 LastUpdate: time.Time{}.Format(time.RFC3339), 334 }, 335 { 336 Name: "b", 337 LastPush: time.Time{}.Format(time.RFC3339), 338 LastUpdate: time.Time{}.Format(time.RFC3339), 339 }, 340 { 341 Name: "c", 342 LastPush: time.Time{}.Format(time.RFC3339), 343 LastUpdate: time.Time{}.Format(time.RFC3339), 344 }, 345 }, 346 }, 347 }, 348 } 349 350 for _, tc := range tests { 351 tc := tc 352 t.Run(tc.name, func(t *testing.T) { 353 apiClient := getAPIC(t) 354 cleanUp(apiClient) 355 for i, machineID := range tc.machineIDs { 356 apiClient.dbClient.Ent.Machine.Create(). 357 SetMachineId(machineID). 358 SetPassword(testPassword.String()). 359 SetIpAddress(fmt.Sprintf("1.2.3.%d", i)). 360 SetScenarios("crowdsecurity/test"). 361 SetLastPush(time.Time{}). 362 SetUpdatedAt(time.Time{}). 363 ExecX(context.Background()) 364 } 365 366 for i, bouncerName := range tc.bouncers { 367 apiClient.dbClient.Ent.Bouncer.Create(). 368 SetIPAddress(fmt.Sprintf("1.2.3.%d", i)). 369 SetName(bouncerName). 370 SetAPIKey("foobar"). 371 SetRevoked(false). 372 SetLastPull(time.Time{}). 373 ExecX(context.Background()) 374 } 375 376 foundMetrics, err := apiClient.GetMetrics() 377 require.NoError(t, err) 378 379 assert.Equal(t, tc.expectedMetric.Bouncers, foundMetrics.Bouncers) 380 assert.Equal(t, tc.expectedMetric.Machines, foundMetrics.Machines) 381 }) 382 } 383 } 384 385 func TestCreateAlertsForDecision(t *testing.T) { 386 httpBfDecisionList := &models.Decision{ 387 Origin: ptr.Of(types.ListOrigin), 388 Scenario: ptr.Of("crowdsecurity/http-bf"), 389 } 390 391 sshBfDecisionList := &models.Decision{ 392 Origin: ptr.Of(types.ListOrigin), 393 Scenario: ptr.Of("crowdsecurity/ssh-bf"), 394 } 395 396 httpBfDecisionCommunity := &models.Decision{ 397 Origin: ptr.Of(types.CAPIOrigin), 398 Scenario: ptr.Of("crowdsecurity/http-bf"), 399 } 400 401 sshBfDecisionCommunity := &models.Decision{ 402 Origin: ptr.Of(types.CAPIOrigin), 403 Scenario: ptr.Of("crowdsecurity/ssh-bf"), 404 } 405 406 type args struct { 407 decisions []*models.Decision 408 } 409 410 tests := []struct { 411 name string 412 args args 413 want []*models.Alert 414 }{ 415 { 416 name: "2 decisions CAPI List Decisions should create 2 alerts", 417 args: args{ 418 decisions: []*models.Decision{ 419 httpBfDecisionList, 420 sshBfDecisionList, 421 }, 422 }, 423 want: []*models.Alert{ 424 createAlertForDecision(httpBfDecisionList), 425 createAlertForDecision(sshBfDecisionList), 426 }, 427 }, 428 { 429 name: "2 decisions CAPI List same scenario decisions should create 1 alert", 430 args: args{ 431 decisions: []*models.Decision{ 432 httpBfDecisionList, 433 httpBfDecisionList, 434 }, 435 }, 436 want: []*models.Alert{ 437 createAlertForDecision(httpBfDecisionList), 438 }, 439 }, 440 { 441 name: "5 decisions from community list should create 1 alert", 442 args: args{ 443 decisions: []*models.Decision{ 444 httpBfDecisionCommunity, 445 httpBfDecisionCommunity, 446 sshBfDecisionCommunity, 447 sshBfDecisionCommunity, 448 sshBfDecisionCommunity, 449 }, 450 }, 451 want: []*models.Alert{ 452 createAlertForDecision(sshBfDecisionCommunity), 453 }, 454 }, 455 } 456 457 for _, tc := range tests { 458 tc := tc 459 t.Run(tc.name, func(t *testing.T) { 460 if got := createAlertsForDecisions(tc.args.decisions); !reflect.DeepEqual(got, tc.want) { 461 t.Errorf("createAlertsForDecisions() = %v, want %v", got, tc.want) 462 } 463 }) 464 } 465 } 466 467 func TestFillAlertsWithDecisions(t *testing.T) { 468 httpBfDecisionCommunity := &models.Decision{ 469 Origin: ptr.Of(types.CAPIOrigin), 470 Scenario: ptr.Of("crowdsecurity/http-bf"), 471 Scope: ptr.Of("ip"), 472 } 473 474 sshBfDecisionCommunity := &models.Decision{ 475 Origin: ptr.Of(types.CAPIOrigin), 476 Scenario: ptr.Of("crowdsecurity/ssh-bf"), 477 Scope: ptr.Of("ip"), 478 } 479 480 httpBfDecisionList := &models.Decision{ 481 Origin: ptr.Of(types.ListOrigin), 482 Scenario: ptr.Of("crowdsecurity/http-bf"), 483 Scope: ptr.Of("ip"), 484 } 485 486 sshBfDecisionList := &models.Decision{ 487 Origin: ptr.Of(types.ListOrigin), 488 Scenario: ptr.Of("crowdsecurity/ssh-bf"), 489 Scope: ptr.Of("ip"), 490 } 491 492 type args struct { 493 alerts []*models.Alert 494 decisions []*models.Decision 495 } 496 497 tests := []struct { 498 name string 499 args args 500 want []*models.Alert 501 }{ 502 { 503 name: "1 CAPI alert should pair up with n CAPI decisions", 504 args: args{ 505 alerts: []*models.Alert{createAlertForDecision(httpBfDecisionCommunity)}, 506 decisions: []*models.Decision{httpBfDecisionCommunity, sshBfDecisionCommunity, sshBfDecisionCommunity, httpBfDecisionCommunity}, 507 }, 508 want: []*models.Alert{ 509 func() *models.Alert { 510 a := createAlertForDecision(httpBfDecisionCommunity) 511 a.Decisions = []*models.Decision{httpBfDecisionCommunity, sshBfDecisionCommunity, sshBfDecisionCommunity, httpBfDecisionCommunity} 512 return a 513 }(), 514 }, 515 }, 516 { 517 name: "List alert should pair up only with decisions having same scenario", 518 args: args{ 519 alerts: []*models.Alert{createAlertForDecision(httpBfDecisionList), createAlertForDecision(sshBfDecisionList)}, 520 decisions: []*models.Decision{httpBfDecisionList, httpBfDecisionList, sshBfDecisionList, sshBfDecisionList}, 521 }, 522 want: []*models.Alert{ 523 func() *models.Alert { 524 a := createAlertForDecision(httpBfDecisionList) 525 a.Decisions = []*models.Decision{httpBfDecisionList, httpBfDecisionList} 526 return a 527 }(), 528 func() *models.Alert { 529 a := createAlertForDecision(sshBfDecisionList) 530 a.Decisions = []*models.Decision{sshBfDecisionList, sshBfDecisionList} 531 return a 532 }(), 533 }, 534 }, 535 } 536 537 for _, tc := range tests { 538 tc := tc 539 t.Run(tc.name, func(t *testing.T) { 540 addCounters, _ := makeAddAndDeleteCounters() 541 if got := fillAlertsWithDecisions(tc.args.alerts, tc.args.decisions, addCounters); !reflect.DeepEqual(got, tc.want) { 542 t.Errorf("fillAlertsWithDecisions() = %v, want %v", got, tc.want) 543 } 544 }) 545 } 546 } 547 548 func TestAPICWhitelists(t *testing.T) { 549 api := getAPIC(t) 550 //one whitelist on IP, one on CIDR 551 api.whitelists = &csconfig.CapiWhitelist{} 552 api.whitelists.Ips = append(api.whitelists.Ips, net.ParseIP("9.2.3.4"), net.ParseIP("7.2.3.4")) 553 554 _, tnet, err := net.ParseCIDR("13.2.3.0/24") 555 require.NoError(t, err) 556 557 api.whitelists.Cidrs = append(api.whitelists.Cidrs, tnet) 558 559 _, tnet, err = net.ParseCIDR("11.2.3.0/24") 560 require.NoError(t, err) 561 562 api.whitelists.Cidrs = append(api.whitelists.Cidrs, tnet) 563 564 api.dbClient.Ent.Decision.Create(). 565 SetOrigin(types.CAPIOrigin). 566 SetType("ban"). 567 SetValue("9.9.9.9"). 568 SetScope("Ip"). 569 SetScenario("crowdsecurity/ssh-bf"). 570 SetUntil(time.Now().Add(time.Hour)). 571 ExecX(context.Background()) 572 assertTotalDecisionCount(t, api.dbClient, 1) 573 assertTotalValidDecisionCount(t, api.dbClient, 1) 574 httpmock.Activate() 575 576 defer httpmock.DeactivateAndReset() 577 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/api/decisions/stream", httpmock.NewBytesResponder( 578 200, jsonMarshalX( 579 modelscapi.GetDecisionsStreamResponse{ 580 Deleted: modelscapi.GetDecisionsStreamResponseDeleted{ 581 &modelscapi.GetDecisionsStreamResponseDeletedItem{ 582 Decisions: []string{ 583 "9.9.9.9", // This is already present in DB 584 "9.1.9.9", // This is not present in DB 585 }, 586 Scope: ptr.Of("Ip"), 587 }, // This is already present in DB 588 }, 589 New: modelscapi.GetDecisionsStreamResponseNew{ 590 &modelscapi.GetDecisionsStreamResponseNewItem{ 591 Scenario: ptr.Of("crowdsecurity/test1"), 592 Scope: ptr.Of("Ip"), 593 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 594 { 595 Value: ptr.Of("13.2.3.4"), //wl by cidr 596 Duration: ptr.Of("24h"), 597 }, 598 }, 599 }, 600 601 &modelscapi.GetDecisionsStreamResponseNewItem{ 602 Scenario: ptr.Of("crowdsecurity/test1"), 603 Scope: ptr.Of("Ip"), 604 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 605 { 606 Value: ptr.Of("2.2.3.4"), 607 Duration: ptr.Of("24h"), 608 }, 609 }, 610 }, 611 &modelscapi.GetDecisionsStreamResponseNewItem{ 612 Scenario: ptr.Of("crowdsecurity/test2"), 613 Scope: ptr.Of("Ip"), 614 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 615 { 616 Value: ptr.Of("13.2.3.5"), //wl by cidr 617 Duration: ptr.Of("24h"), 618 }, 619 }, 620 }, // These two are from community list. 621 &modelscapi.GetDecisionsStreamResponseNewItem{ 622 Scenario: ptr.Of("crowdsecurity/test1"), 623 Scope: ptr.Of("Ip"), 624 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 625 { 626 Value: ptr.Of("6.2.3.4"), 627 Duration: ptr.Of("24h"), 628 }, 629 }, 630 }, 631 &modelscapi.GetDecisionsStreamResponseNewItem{ 632 Scenario: ptr.Of("crowdsecurity/test1"), 633 Scope: ptr.Of("Ip"), 634 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 635 { 636 Value: ptr.Of("9.2.3.4"), //wl by ip 637 Duration: ptr.Of("24h"), 638 }, 639 }, 640 }, 641 }, 642 Links: &modelscapi.GetDecisionsStreamResponseLinks{ 643 Blocklists: []*modelscapi.BlocklistLink{ 644 { 645 URL: ptr.Of("http://api.crowdsec.net/blocklist1"), 646 Name: ptr.Of("blocklist1"), 647 Scope: ptr.Of("Ip"), 648 Remediation: ptr.Of("ban"), 649 Duration: ptr.Of("24h"), 650 }, 651 { 652 URL: ptr.Of("http://api.crowdsec.net/blocklist2"), 653 Name: ptr.Of("blocklist2"), 654 Scope: ptr.Of("Ip"), 655 Remediation: ptr.Of("ban"), 656 Duration: ptr.Of("24h"), 657 }, 658 }, 659 }, 660 }, 661 ), 662 )) 663 664 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", httpmock.NewStringResponder( 665 200, "1.2.3.6", 666 )) 667 668 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist2", httpmock.NewStringResponder( 669 200, "1.2.3.7", 670 )) 671 672 url, err := url.ParseRequestURI("http://api.crowdsec.net/") 673 require.NoError(t, err) 674 675 apic, err := apiclient.NewDefaultClient( 676 url, 677 "/api", 678 fmt.Sprintf("crowdsec/%s", version.String()), 679 nil, 680 ) 681 require.NoError(t, err) 682 683 api.apiClient = apic 684 err = api.PullTop(false) 685 require.NoError(t, err) 686 687 assertTotalDecisionCount(t, api.dbClient, 5) //2 from FIRE + 2 from bl + 1 existing 688 assertTotalValidDecisionCount(t, api.dbClient, 4) 689 assertTotalAlertCount(t, api.dbClient, 3) // 2 for list sub , 1 for community list. 690 alerts := api.dbClient.Ent.Alert.Query().AllX(context.Background()) 691 validDecisions := api.dbClient.Ent.Decision.Query().Where( 692 decision.UntilGT(time.Now())). 693 AllX(context.Background()) 694 695 decisionScenarioFreq := make(map[string]int) 696 decisionIP := make(map[string]int) 697 698 alertScenario := make(map[string]int) 699 700 for _, alert := range alerts { 701 alertScenario[alert.SourceScope]++ 702 } 703 704 assert.Len(t, alertScenario, 3) 705 assert.Equal(t, 1, alertScenario[types.CommunityBlocklistPullSourceScope]) 706 assert.Equal(t, 1, alertScenario["lists:blocklist1"]) 707 assert.Equal(t, 1, alertScenario["lists:blocklist2"]) 708 709 for _, decisions := range validDecisions { 710 decisionScenarioFreq[decisions.Scenario]++ 711 decisionIP[decisions.Value]++ 712 } 713 714 assert.Equal(t, 1, decisionIP["2.2.3.4"], 1) 715 assert.Equal(t, 1, decisionIP["6.2.3.4"], 1) 716 717 if _, ok := decisionIP["13.2.3.4"]; ok { 718 t.Errorf("13.2.3.4 is whitelisted") 719 } 720 721 if _, ok := decisionIP["13.2.3.5"]; ok { 722 t.Errorf("13.2.3.5 is whitelisted") 723 } 724 725 if _, ok := decisionIP["9.2.3.4"]; ok { 726 t.Errorf("9.2.3.4 is whitelisted") 727 } 728 729 assert.Equal(t, 1, decisionScenarioFreq["blocklist1"], 1) 730 assert.Equal(t, 1, decisionScenarioFreq["blocklist2"], 1) 731 assert.Equal(t, 2, decisionScenarioFreq["crowdsecurity/test1"], 2) 732 } 733 734 func TestAPICPullTop(t *testing.T) { 735 api := getAPIC(t) 736 api.dbClient.Ent.Decision.Create(). 737 SetOrigin(types.CAPIOrigin). 738 SetType("ban"). 739 SetValue("9.9.9.9"). 740 SetScope("Ip"). 741 SetScenario("crowdsecurity/ssh-bf"). 742 SetUntil(time.Now().Add(time.Hour)). 743 ExecX(context.Background()) 744 assertTotalDecisionCount(t, api.dbClient, 1) 745 assertTotalValidDecisionCount(t, api.dbClient, 1) 746 httpmock.Activate() 747 748 defer httpmock.DeactivateAndReset() 749 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/api/decisions/stream", httpmock.NewBytesResponder( 750 200, jsonMarshalX( 751 modelscapi.GetDecisionsStreamResponse{ 752 Deleted: modelscapi.GetDecisionsStreamResponseDeleted{ 753 &modelscapi.GetDecisionsStreamResponseDeletedItem{ 754 Decisions: []string{ 755 "9.9.9.9", // This is already present in DB 756 "9.1.9.9", // This is not present in DB 757 }, 758 Scope: ptr.Of("Ip"), 759 }, // This is already present in DB 760 }, 761 New: modelscapi.GetDecisionsStreamResponseNew{ 762 &modelscapi.GetDecisionsStreamResponseNewItem{ 763 Scenario: ptr.Of("crowdsecurity/test1"), 764 Scope: ptr.Of("Ip"), 765 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 766 { 767 Value: ptr.Of("1.2.3.4"), 768 Duration: ptr.Of("24h"), 769 }, 770 }, 771 }, 772 &modelscapi.GetDecisionsStreamResponseNewItem{ 773 Scenario: ptr.Of("crowdsecurity/test2"), 774 Scope: ptr.Of("Ip"), 775 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 776 { 777 Value: ptr.Of("1.2.3.5"), 778 Duration: ptr.Of("24h"), 779 }, 780 }, 781 }, // These two are from community list. 782 }, 783 Links: &modelscapi.GetDecisionsStreamResponseLinks{ 784 Blocklists: []*modelscapi.BlocklistLink{ 785 { 786 URL: ptr.Of("http://api.crowdsec.net/blocklist1"), 787 Name: ptr.Of("blocklist1"), 788 Scope: ptr.Of("Ip"), 789 Remediation: ptr.Of("ban"), 790 Duration: ptr.Of("24h"), 791 }, 792 { 793 URL: ptr.Of("http://api.crowdsec.net/blocklist2"), 794 Name: ptr.Of("blocklist2"), 795 Scope: ptr.Of("Ip"), 796 Remediation: ptr.Of("ban"), 797 Duration: ptr.Of("24h"), 798 }, 799 }, 800 }, 801 }, 802 ), 803 )) 804 805 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", httpmock.NewStringResponder( 806 200, "1.2.3.6", 807 )) 808 809 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist2", httpmock.NewStringResponder( 810 200, "1.2.3.7", 811 )) 812 813 url, err := url.ParseRequestURI("http://api.crowdsec.net/") 814 require.NoError(t, err) 815 816 apic, err := apiclient.NewDefaultClient( 817 url, 818 "/api", 819 fmt.Sprintf("crowdsec/%s", version.String()), 820 nil, 821 ) 822 require.NoError(t, err) 823 824 api.apiClient = apic 825 err = api.PullTop(false) 826 require.NoError(t, err) 827 828 assertTotalDecisionCount(t, api.dbClient, 5) 829 assertTotalValidDecisionCount(t, api.dbClient, 4) 830 assertTotalAlertCount(t, api.dbClient, 3) // 2 for list sub , 1 for community list. 831 alerts := api.dbClient.Ent.Alert.Query().AllX(context.Background()) 832 validDecisions := api.dbClient.Ent.Decision.Query().Where( 833 decision.UntilGT(time.Now())). 834 AllX(context.Background(), 835 ) 836 837 decisionScenarioFreq := make(map[string]int) 838 alertScenario := make(map[string]int) 839 840 for _, alert := range alerts { 841 alertScenario[alert.SourceScope]++ 842 } 843 844 assert.Len(t, alertScenario, 3) 845 assert.Equal(t, 1, alertScenario[types.CommunityBlocklistPullSourceScope]) 846 assert.Equal(t, 1, alertScenario["lists:blocklist1"]) 847 assert.Equal(t, 1, alertScenario["lists:blocklist2"]) 848 849 for _, decisions := range validDecisions { 850 decisionScenarioFreq[decisions.Scenario]++ 851 } 852 853 assert.Equal(t, 1, decisionScenarioFreq["blocklist1"], 1) 854 assert.Equal(t, 1, decisionScenarioFreq["blocklist2"], 1) 855 assert.Equal(t, 1, decisionScenarioFreq["crowdsecurity/test1"], 1) 856 assert.Equal(t, 1, decisionScenarioFreq["crowdsecurity/test2"], 1) 857 } 858 859 func TestAPICPullTopBLCacheFirstCall(t *testing.T) { 860 // no decision in db, no last modified parameter. 861 api := getAPIC(t) 862 863 httpmock.Activate() 864 defer httpmock.DeactivateAndReset() 865 866 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/api/decisions/stream", httpmock.NewBytesResponder( 867 200, jsonMarshalX( 868 modelscapi.GetDecisionsStreamResponse{ 869 New: modelscapi.GetDecisionsStreamResponseNew{ 870 &modelscapi.GetDecisionsStreamResponseNewItem{ 871 Scenario: ptr.Of("crowdsecurity/test1"), 872 Scope: ptr.Of("Ip"), 873 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 874 { 875 Value: ptr.Of("1.2.3.4"), 876 Duration: ptr.Of("24h"), 877 }, 878 }, 879 }, 880 }, 881 Links: &modelscapi.GetDecisionsStreamResponseLinks{ 882 Blocklists: []*modelscapi.BlocklistLink{ 883 { 884 URL: ptr.Of("http://api.crowdsec.net/blocklist1"), 885 Name: ptr.Of("blocklist1"), 886 Scope: ptr.Of("Ip"), 887 Remediation: ptr.Of("ban"), 888 Duration: ptr.Of("24h"), 889 }, 890 }, 891 }, 892 }, 893 ), 894 )) 895 896 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", func(req *http.Request) (*http.Response, error) { 897 assert.Equal(t, "", req.Header.Get("If-Modified-Since")) 898 return httpmock.NewStringResponse(200, "1.2.3.4"), nil 899 }) 900 901 url, err := url.ParseRequestURI("http://api.crowdsec.net/") 902 require.NoError(t, err) 903 904 apic, err := apiclient.NewDefaultClient( 905 url, 906 "/api", 907 fmt.Sprintf("crowdsec/%s", version.String()), 908 nil, 909 ) 910 require.NoError(t, err) 911 912 api.apiClient = apic 913 err = api.PullTop(false) 914 require.NoError(t, err) 915 916 blocklistConfigItemName := "blocklist:blocklist1:last_pull" 917 lastPullTimestamp, err := api.dbClient.GetConfigItem(blocklistConfigItemName) 918 require.NoError(t, err) 919 assert.NotEqual(t, "", *lastPullTimestamp) 920 921 // new call should return 304 and should not change lastPullTimestamp 922 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", func(req *http.Request) (*http.Response, error) { 923 assert.NotEqual(t, "", req.Header.Get("If-Modified-Since")) 924 return httpmock.NewStringResponse(304, ""), nil 925 }) 926 927 err = api.PullTop(false) 928 require.NoError(t, err) 929 secondLastPullTimestamp, err := api.dbClient.GetConfigItem(blocklistConfigItemName) 930 require.NoError(t, err) 931 assert.Equal(t, *lastPullTimestamp, *secondLastPullTimestamp) 932 } 933 934 func TestAPICPullTopBLCacheForceCall(t *testing.T) { 935 api := getAPIC(t) 936 937 httpmock.Activate() 938 defer httpmock.DeactivateAndReset() 939 940 // create a decision about to expire. It should force fetch 941 alertInstance := api.dbClient.Ent.Alert. 942 Create(). 943 SetScenario("update list"). 944 SetSourceScope("list:blocklist1"). 945 SetSourceValue("list:blocklist1"). 946 SaveX(context.Background()) 947 948 api.dbClient.Ent.Decision.Create(). 949 SetOrigin(types.ListOrigin). 950 SetType("ban"). 951 SetValue("9.9.9.9"). 952 SetScope("Ip"). 953 SetScenario("blocklist1"). 954 SetUntil(time.Now().Add(time.Hour)). 955 SetOwnerID(alertInstance.ID). 956 ExecX(context.Background()) 957 958 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/api/decisions/stream", httpmock.NewBytesResponder( 959 200, jsonMarshalX( 960 modelscapi.GetDecisionsStreamResponse{ 961 New: modelscapi.GetDecisionsStreamResponseNew{ 962 &modelscapi.GetDecisionsStreamResponseNewItem{ 963 Scenario: ptr.Of("crowdsecurity/test1"), 964 Scope: ptr.Of("Ip"), 965 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 966 { 967 Value: ptr.Of("1.2.3.4"), 968 Duration: ptr.Of("24h"), 969 }, 970 }, 971 }, 972 }, 973 Links: &modelscapi.GetDecisionsStreamResponseLinks{ 974 Blocklists: []*modelscapi.BlocklistLink{ 975 { 976 URL: ptr.Of("http://api.crowdsec.net/blocklist1"), 977 Name: ptr.Of("blocklist1"), 978 Scope: ptr.Of("Ip"), 979 Remediation: ptr.Of("ban"), 980 Duration: ptr.Of("24h"), 981 }, 982 }, 983 }, 984 }, 985 ), 986 )) 987 988 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", func(req *http.Request) (*http.Response, error) { 989 assert.Equal(t, "", req.Header.Get("If-Modified-Since")) 990 return httpmock.NewStringResponse(304, ""), nil 991 }) 992 993 url, err := url.ParseRequestURI("http://api.crowdsec.net/") 994 require.NoError(t, err) 995 996 apic, err := apiclient.NewDefaultClient( 997 url, 998 "/api", 999 fmt.Sprintf("crowdsec/%s", version.String()), 1000 nil, 1001 ) 1002 require.NoError(t, err) 1003 1004 api.apiClient = apic 1005 err = api.PullTop(false) 1006 require.NoError(t, err) 1007 } 1008 1009 func TestAPICPullBlocklistCall(t *testing.T) { 1010 api := getAPIC(t) 1011 1012 httpmock.Activate() 1013 defer httpmock.DeactivateAndReset() 1014 1015 httpmock.RegisterResponder("GET", "http://api.crowdsec.net/blocklist1", func(req *http.Request) (*http.Response, error) { 1016 assert.Equal(t, "", req.Header.Get("If-Modified-Since")) 1017 return httpmock.NewStringResponse(200, "1.2.3.4"), nil 1018 }) 1019 1020 url, err := url.ParseRequestURI("http://api.crowdsec.net/") 1021 require.NoError(t, err) 1022 1023 apic, err := apiclient.NewDefaultClient( 1024 url, 1025 "/api", 1026 fmt.Sprintf("crowdsec/%s", version.String()), 1027 nil, 1028 ) 1029 require.NoError(t, err) 1030 1031 api.apiClient = apic 1032 err = api.PullBlocklist(&modelscapi.BlocklistLink{ 1033 URL: ptr.Of("http://api.crowdsec.net/blocklist1"), 1034 Name: ptr.Of("blocklist1"), 1035 Scope: ptr.Of("Ip"), 1036 Remediation: ptr.Of("ban"), 1037 Duration: ptr.Of("24h"), 1038 }, true) 1039 require.NoError(t, err) 1040 } 1041 1042 func TestAPICPush(t *testing.T) { 1043 tests := []struct { 1044 name string 1045 alerts []*models.Alert 1046 expectedCalls int 1047 }{ 1048 { 1049 name: "simple single alert", 1050 alerts: []*models.Alert{ 1051 { 1052 Scenario: ptr.Of("crowdsec/test"), 1053 ScenarioHash: ptr.Of("certified"), 1054 ScenarioVersion: ptr.Of("v1.0"), 1055 Simulated: ptr.Of(false), 1056 Source: &models.Source{}, 1057 }, 1058 }, 1059 expectedCalls: 1, 1060 }, 1061 { 1062 name: "simulated alert is not pushed", 1063 alerts: []*models.Alert{ 1064 { 1065 Scenario: ptr.Of("crowdsec/test"), 1066 ScenarioHash: ptr.Of("certified"), 1067 ScenarioVersion: ptr.Of("v1.0"), 1068 Simulated: ptr.Of(true), 1069 Source: &models.Source{}, 1070 }, 1071 }, 1072 expectedCalls: 0, 1073 }, 1074 { 1075 name: "1 request per 50 alerts", 1076 expectedCalls: 2, 1077 alerts: func() []*models.Alert { 1078 alerts := make([]*models.Alert, 100) 1079 for i := 0; i < 100; i++ { 1080 alerts[i] = &models.Alert{ 1081 Scenario: ptr.Of("crowdsec/test"), 1082 ScenarioHash: ptr.Of("certified"), 1083 ScenarioVersion: ptr.Of("v1.0"), 1084 Simulated: ptr.Of(false), 1085 Source: &models.Source{}, 1086 } 1087 } 1088 1089 return alerts 1090 }(), 1091 }, 1092 } 1093 1094 for _, tc := range tests { 1095 tc := tc 1096 t.Run(tc.name, func(t *testing.T) { 1097 api := getAPIC(t) 1098 api.pushInterval = time.Millisecond 1099 api.pushIntervalFirst = time.Millisecond 1100 url, err := url.ParseRequestURI("http://api.crowdsec.net/") 1101 require.NoError(t, err) 1102 1103 httpmock.Activate() 1104 defer httpmock.DeactivateAndReset() 1105 apic, err := apiclient.NewDefaultClient( 1106 url, 1107 "/api", 1108 fmt.Sprintf("crowdsec/%s", version.String()), 1109 nil, 1110 ) 1111 require.NoError(t, err) 1112 1113 api.apiClient = apic 1114 httpmock.RegisterResponder("POST", "http://api.crowdsec.net/api/signals", httpmock.NewBytesResponder(200, []byte{})) 1115 go func() { 1116 api.AlertsAddChan <- tc.alerts 1117 time.Sleep(time.Second) 1118 api.Shutdown() 1119 }() 1120 err = api.Push() 1121 require.NoError(t, err) 1122 assert.Equal(t, tc.expectedCalls, httpmock.GetTotalCallCount()) 1123 }) 1124 } 1125 } 1126 1127 func TestAPICPull(t *testing.T) { 1128 api := getAPIC(t) 1129 tests := []struct { 1130 name string 1131 setUp func() 1132 expectedDecisionCount int 1133 logContains string 1134 }{ 1135 { 1136 name: "test pull if no scenarios are present", 1137 setUp: func() {}, 1138 logContains: "scenario list is empty, will not pull yet", 1139 }, 1140 { 1141 name: "test pull", 1142 setUp: func() { 1143 api.dbClient.Ent.Machine.Create(). 1144 SetMachineId("1.2.3.4"). 1145 SetPassword(testPassword.String()). 1146 SetIpAddress("1.2.3.4"). 1147 SetScenarios("crowdsecurity/ssh-bf"). 1148 ExecX(context.Background()) 1149 }, 1150 expectedDecisionCount: 1, 1151 }, 1152 } 1153 1154 for _, tc := range tests { 1155 tc := tc 1156 t.Run(tc.name, func(t *testing.T) { 1157 api = getAPIC(t) 1158 api.pullInterval = time.Millisecond 1159 api.pullIntervalFirst = time.Millisecond 1160 url, err := url.ParseRequestURI("http://api.crowdsec.net/") 1161 require.NoError(t, err) 1162 httpmock.Activate() 1163 defer httpmock.DeactivateAndReset() 1164 apic, err := apiclient.NewDefaultClient( 1165 url, 1166 "/api", 1167 fmt.Sprintf("crowdsec/%s", version.String()), 1168 nil, 1169 ) 1170 require.NoError(t, err) 1171 api.apiClient = apic 1172 httpmock.RegisterNoResponder(httpmock.NewBytesResponder(200, jsonMarshalX( 1173 modelscapi.GetDecisionsStreamResponse{ 1174 New: modelscapi.GetDecisionsStreamResponseNew{ 1175 &modelscapi.GetDecisionsStreamResponseNewItem{ 1176 Scenario: ptr.Of("crowdsecurity/ssh-bf"), 1177 Scope: ptr.Of("Ip"), 1178 Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ 1179 { 1180 Value: ptr.Of("1.2.3.5"), 1181 Duration: ptr.Of("24h"), 1182 }, 1183 }, 1184 }, 1185 }, 1186 }, 1187 ))) 1188 tc.setUp() 1189 var buf bytes.Buffer 1190 go func() { 1191 logrus.SetOutput(&buf) 1192 if err := api.Pull(); err != nil { 1193 panic(err) 1194 } 1195 }() 1196 //Slightly long because the CI runner for windows are slow, and this can lead to random failure 1197 time.Sleep(time.Millisecond * 500) 1198 logrus.SetOutput(os.Stderr) 1199 assert.Contains(t, buf.String(), tc.logContains) 1200 assertTotalDecisionCount(t, api.dbClient, tc.expectedDecisionCount) 1201 }) 1202 } 1203 } 1204 1205 func TestShouldShareAlert(t *testing.T) { 1206 tests := []struct { 1207 name string 1208 consoleConfig *csconfig.ConsoleConfig 1209 alert *models.Alert 1210 expectedRet bool 1211 expectedTrust string 1212 }{ 1213 { 1214 name: "custom alert should be shared if config enables it", 1215 consoleConfig: &csconfig.ConsoleConfig{ 1216 ShareCustomScenarios: ptr.Of(true), 1217 }, 1218 alert: &models.Alert{Simulated: ptr.Of(false)}, 1219 expectedRet: true, 1220 expectedTrust: "custom", 1221 }, 1222 { 1223 name: "custom alert should not be shared if config disables it", 1224 consoleConfig: &csconfig.ConsoleConfig{ 1225 ShareCustomScenarios: ptr.Of(false), 1226 }, 1227 alert: &models.Alert{Simulated: ptr.Of(false)}, 1228 expectedRet: false, 1229 expectedTrust: "custom", 1230 }, 1231 { 1232 name: "manual alert should be shared if config enables it", 1233 consoleConfig: &csconfig.ConsoleConfig{ 1234 ShareManualDecisions: ptr.Of(true), 1235 }, 1236 alert: &models.Alert{ 1237 Simulated: ptr.Of(false), 1238 Decisions: []*models.Decision{{Origin: ptr.Of(types.CscliOrigin)}}, 1239 }, 1240 expectedRet: true, 1241 expectedTrust: "manual", 1242 }, 1243 { 1244 name: "manual alert should not be shared if config disables it", 1245 consoleConfig: &csconfig.ConsoleConfig{ 1246 ShareManualDecisions: ptr.Of(false), 1247 }, 1248 alert: &models.Alert{ 1249 Simulated: ptr.Of(false), 1250 Decisions: []*models.Decision{{Origin: ptr.Of(types.CscliOrigin)}}, 1251 }, 1252 expectedRet: false, 1253 expectedTrust: "manual", 1254 }, 1255 { 1256 name: "manual alert should be shared if config enables it", 1257 consoleConfig: &csconfig.ConsoleConfig{ 1258 ShareTaintedScenarios: ptr.Of(true), 1259 }, 1260 alert: &models.Alert{ 1261 Simulated: ptr.Of(false), 1262 ScenarioHash: ptr.Of("whateverHash"), 1263 }, 1264 expectedRet: true, 1265 expectedTrust: "tainted", 1266 }, 1267 { 1268 name: "manual alert should not be shared if config disables it", 1269 consoleConfig: &csconfig.ConsoleConfig{ 1270 ShareTaintedScenarios: ptr.Of(false), 1271 }, 1272 alert: &models.Alert{ 1273 Simulated: ptr.Of(false), 1274 ScenarioHash: ptr.Of("whateverHash"), 1275 }, 1276 expectedRet: false, 1277 expectedTrust: "tainted", 1278 }, 1279 } 1280 1281 for _, tc := range tests { 1282 tc := tc 1283 t.Run(tc.name, func(t *testing.T) { 1284 ret := shouldShareAlert(tc.alert, tc.consoleConfig) 1285 assert.Equal(t, tc.expectedRet, ret) 1286 }) 1287 } 1288 }