package apiclient import ( "context" "fmt" "net/http" "net/url" "testing" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/crowdsecurity/go-cs-lib/cstest" "github.com/crowdsecurity/go-cs-lib/ptr" "github.com/crowdsecurity/go-cs-lib/version" "github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/modelscapi" ) func TestDecisionsList(t *testing.T) { log.SetLevel(log.DebugLevel) mux, urlx, teardown := setup() defer teardown() mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") if r.URL.RawQuery == "ip=1.2.3.4" { assert.Equal(t, "ip=1.2.3.4", r.URL.RawQuery) assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) w.WriteHeader(http.StatusOK) w.Write([]byte(`[{"duration":"3h59m55.756182786s","id":4,"origin":"cscli","scenario":"manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'","scope":"Ip","type":"ban","value":"1.2.3.4"}]`)) } else { w.WriteHeader(http.StatusOK) w.Write([]byte(`null`)) //no results } }) apiURL, err := url.Parse(urlx + "/") require.NoError(t, err) //ok answer auth := &APIKeyTransport{ APIKey: "ixu", } newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client()) require.NoError(t, err) expected := &models.GetDecisionsResponse{ &models.Decision{ Duration: ptr.Of("3h59m55.756182786s"), ID: 4, Origin: ptr.Of("cscli"), Scenario: ptr.Of("manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"), Scope: ptr.Of("Ip"), Type: ptr.Of("ban"), Value: ptr.Of("1.2.3.4"), }, } // OK decisions decisionsFilter := DecisionsListOpts{IPEquals: ptr.Of("1.2.3.4")} decisions, resp, err := newcli.Decisions.List(context.Background(), decisionsFilter) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Response.StatusCode) assert.Equal(t, *expected, *decisions) //Empty return decisionsFilter = DecisionsListOpts{IPEquals: ptr.Of("1.2.3.5")} decisions, resp, err = newcli.Decisions.List(context.Background(), decisionsFilter) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Response.StatusCode) assert.Empty(t, *decisions) } func TestDecisionsStream(t *testing.T) { log.SetLevel(log.DebugLevel) mux, urlx, teardown := setup() defer teardown() mux.HandleFunc("/decisions/stream", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) testMethod(t, r, http.MethodGet) if r.Method == http.MethodGet { if r.URL.RawQuery == "startup=true" { w.WriteHeader(http.StatusOK) 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"}]}`)) } else { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"deleted":null,"new":null}`)) } } }) mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) testMethod(t, r, http.MethodDelete) if r.Method == http.MethodDelete { w.WriteHeader(http.StatusOK) } }) apiURL, err := url.Parse(urlx + "/") require.NoError(t, err) //ok answer auth := &APIKeyTransport{ APIKey: "ixu", } newcli, err := NewDefaultClient(apiURL, "v1", "toto", auth.Client()) require.NoError(t, err) expected := &models.DecisionsStreamResponse{ New: models.GetDecisionsResponse{ &models.Decision{ Duration: ptr.Of("3h59m55.756182786s"), ID: 4, Origin: ptr.Of("cscli"), Scenario: ptr.Of("manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"), Scope: ptr.Of("Ip"), Type: ptr.Of("ban"), Value: ptr.Of("1.2.3.4"), }, }, } decisions, resp, err := newcli.Decisions.GetStream(context.Background(), DecisionsStreamOpts{Startup: true}) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Response.StatusCode) assert.Equal(t, *expected, *decisions) //and second call, we get empty lists decisions, resp, err = newcli.Decisions.GetStream(context.Background(), DecisionsStreamOpts{Startup: false}) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Response.StatusCode) assert.Empty(t, decisions.New) assert.Empty(t, decisions.Deleted) //delete stream resp, err = newcli.Decisions.StopStream(context.Background()) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Response.StatusCode) } func TestDecisionsStreamV3Compatibility(t *testing.T) { log.SetLevel(log.DebugLevel) mux, urlx, teardown := setupWithPrefix("v3") defer teardown() mux.HandleFunc("/decisions/stream", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) testMethod(t, r, http.MethodGet) if r.Method == http.MethodGet { if r.URL.RawQuery == "startup=true" { w.WriteHeader(http.StatusOK) 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"}]}]}`)) } else { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"deleted":null,"new":null}`)) } } }) apiURL, err := url.Parse(urlx + "/") require.NoError(t, err) //ok answer auth := &APIKeyTransport{ APIKey: "ixu", } newcli, err := NewDefaultClient(apiURL, "v3", "toto", auth.Client()) require.NoError(t, err) torigin := "CAPI" tscope := "ip" ttype := "ban" expected := &models.DecisionsStreamResponse{ New: models.GetDecisionsResponse{ &models.Decision{ Duration: ptr.Of("3h59m55.756182786s"), Origin: &torigin, Scenario: ptr.Of("manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"), Scope: &tscope, Type: &ttype, Value: ptr.Of("1.2.3.4"), }, }, Deleted: models.GetDecisionsResponse{ &models.Decision{ Duration: ptr.Of("1h"), Origin: &torigin, Scenario: ptr.Of("deleted"), Scope: &tscope, Type: &ttype, Value: ptr.Of("1.2.3.5"), }, }, } // GetStream is supposed to consume v3 payload and return v2 response decisions, resp, err := newcli.Decisions.GetStream(context.Background(), DecisionsStreamOpts{Startup: true}) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Response.StatusCode) assert.Equal(t, *expected, *decisions) } func TestDecisionsStreamV3(t *testing.T) { log.SetLevel(log.DebugLevel) mux, urlx, teardown := setupWithPrefix("v3") defer teardown() mux.HandleFunc("/decisions/stream", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "ixu", r.Header.Get("X-Api-Key")) testMethod(t, r, http.MethodGet) if r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) 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"}]}], "links": {"blocklists":[{"name":"blocklist1","url":"/v3/blocklist","scope":"ip","remediation":"ban","duration":"24h"}]}}`)) } }) apiURL, err := url.Parse(urlx + "/") require.NoError(t, err) //ok answer auth := &APIKeyTransport{ APIKey: "ixu", } newcli, err := NewDefaultClient(apiURL, "v3", "toto", auth.Client()) require.NoError(t, err) tscope := "ip" expected := &modelscapi.GetDecisionsStreamResponse{ New: modelscapi.GetDecisionsStreamResponseNew{ &modelscapi.GetDecisionsStreamResponseNewItem{ Decisions: []*modelscapi.GetDecisionsStreamResponseNewItemDecisionsItems0{ { Duration: ptr.Of("3h59m55.756182786s"), Value: ptr.Of("1.2.3.4"), }, }, Scenario: ptr.Of("manual 'ban' from '82929df7ee394b73b81252fe3b4e50203yaT2u6nXiaN7Ix9'"), Scope: &tscope, }, }, Deleted: modelscapi.GetDecisionsStreamResponseDeleted{ &modelscapi.GetDecisionsStreamResponseDeletedItem{ Scope: &tscope, Decisions: []string{ "1.2.3.5", }, }, }, Links: &modelscapi.GetDecisionsStreamResponseLinks{ Blocklists: []*modelscapi.BlocklistLink{ { Duration: ptr.Of("24h"), Name: ptr.Of("blocklist1"), Remediation: ptr.Of("ban"), Scope: ptr.Of("ip"), URL: ptr.Of("/v3/blocklist"), }, }, }, } // GetStream is supposed to consume v3 payload and return v2 response decisions, resp, err := newcli.Decisions.GetStreamV3(context.Background(), DecisionsStreamOpts{Startup: true}) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.Response.StatusCode) assert.Equal(t, *expected, *decisions) } func TestDecisionsFromBlocklist(t *testing.T) { log.SetLevel(log.DebugLevel) mux, urlx, teardown := setupWithPrefix("v3") defer teardown() mux.HandleFunc("/blocklist", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodGet) if r.Header.Get("If-Modified-Since") == "Sun, 01 Jan 2023 01:01:01 GMT" { w.WriteHeader(http.StatusNotModified) return } if r.Method == http.MethodGet { w.WriteHeader(http.StatusOK) w.Write([]byte("1.2.3.4\r\n1.2.3.5")) } }) apiURL, err := url.Parse(urlx + "/") require.NoError(t, err) //ok answer auth := &APIKeyTransport{ APIKey: "ixu", } newcli, err := NewDefaultClient(apiURL, "v3", "toto", auth.Client()) require.NoError(t, err) tdurationBlocklist := "24h" tnameBlocklist := "blocklist1" tremediationBlocklist := "ban" tscopeBlocklist := "ip" turlBlocklist := urlx + "/v3/blocklist" torigin := "lists" expected := []*models.Decision{ { Duration: &tdurationBlocklist, Value: ptr.Of("1.2.3.4"), Scenario: &tnameBlocklist, Scope: &tscopeBlocklist, Type: &tremediationBlocklist, Origin: &torigin, }, { Duration: &tdurationBlocklist, Value: ptr.Of("1.2.3.5"), Scenario: &tnameBlocklist, Scope: &tscopeBlocklist, Type: &tremediationBlocklist, Origin: &torigin, }, } decisions, isModified, err := newcli.Decisions.GetDecisionsFromBlocklist(context.Background(), &modelscapi.BlocklistLink{ URL: &turlBlocklist, Scope: &tscopeBlocklist, Remediation: &tremediationBlocklist, Name: &tnameBlocklist, Duration: &tdurationBlocklist, }, nil) require.NoError(t, err) assert.True(t, isModified) log.Infof("decision1: %+v", decisions[0]) log.Infof("expected1: %+v", expected[0]) 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) log.Infof("expected : %s, %s, %s, %s, %s", *expected[0].Value, *expected[0].Duration, *expected[0].Scenario, *expected[0].Scope, *expected[0].Type) log.Infof("decisions: %s, %s, %s, %s, %s", *decisions[1].Value, *decisions[1].Duration, *decisions[1].Scenario, *decisions[1].Scope, *decisions[1].Type) assert.Equal(t, expected, decisions) // test cache control _, isModified, err = newcli.Decisions.GetDecisionsFromBlocklist(context.Background(), &modelscapi.BlocklistLink{ URL: &turlBlocklist, Scope: &tscopeBlocklist, Remediation: &tremediationBlocklist, Name: &tnameBlocklist, Duration: &tdurationBlocklist, }, ptr.Of("Sun, 01 Jan 2023 01:01:01 GMT")) require.NoError(t, err) assert.False(t, isModified) _, isModified, err = newcli.Decisions.GetDecisionsFromBlocklist(context.Background(), &modelscapi.BlocklistLink{ URL: &turlBlocklist, Scope: &tscopeBlocklist, Remediation: &tremediationBlocklist, Name: &tnameBlocklist, Duration: &tdurationBlocklist, }, ptr.Of("Mon, 02 Jan 2023 01:01:01 GMT")) require.NoError(t, err) assert.True(t, isModified) } func TestDeleteDecisions(t *testing.T) { mux, urlx, teardown := setup() mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`)) }) mux.HandleFunc("/decisions", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "DELETE") assert.Equal(t, "ip=1.2.3.4", r.URL.RawQuery) w.WriteHeader(http.StatusOK) w.Write([]byte(`{"nbDeleted":"1"}`)) //w.Write([]byte(`{"message":"0 deleted alerts"}`)) }) log.Printf("URL is %s", urlx) apiURL, err := url.Parse(urlx + "/") require.NoError(t, err) client, err := NewClient(&Config{ MachineID: "test_login", Password: "test_password", UserAgent: fmt.Sprintf("crowdsec/%s", version.String()), URL: apiURL, VersionPrefix: "v1", }) require.NoError(t, err) filters := DecisionsDeleteOpts{IPEquals: new(string)} *filters.IPEquals = "1.2.3.4" deleted, _, err := client.Decisions.Delete(context.Background(), filters) require.NoError(t, err) assert.Equal(t, "1", deleted.NbDeleted) defer teardown() } func TestDecisionsStreamOpts_addQueryParamsToURL(t *testing.T) { baseURLString := "http://localhost:8080/v1/decisions/stream" type fields struct { Startup bool Scopes string ScenariosContaining string ScenariosNotContaining string } tests := []struct { name string fields fields expected string expectedErr string }{ { name: "no filter", expected: baseURLString + "?", }, { name: "startup=true", fields: fields{ Startup: true, }, expected: baseURLString + "?startup=true", }, { name: "set all params", fields: fields{ Startup: true, Scopes: "ip,range", ScenariosContaining: "ssh", ScenariosNotContaining: "bf", }, expected: baseURLString + "?scenarios_containing=ssh&scenarios_not_containing=bf&scopes=ip%2Crange&startup=true", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { o := &DecisionsStreamOpts{ Startup: tt.fields.Startup, Scopes: tt.fields.Scopes, ScenariosContaining: tt.fields.ScenariosContaining, ScenariosNotContaining: tt.fields.ScenariosNotContaining, } got, err := o.addQueryParamsToURL(baseURLString) cstest.RequireErrorContains(t, err, tt.expectedErr) if tt.expectedErr != "" { return } gotURL, err := url.Parse(got) require.NoError(t, err) expectedURL, err := url.Parse(tt.expected) require.NoError(t, err) assert.Equal(t, *expectedURL, *gotURL) }) } } // func TestDeleteOneDecision(t *testing.T) { // mux, urlx, teardown := setup() // mux.HandleFunc("/watchers/login", func(w http.ResponseWriter, r *http.Request) { // w.WriteHeader(http.StatusOK) // w.Write([]byte(`{"code": 200, "expire": "2030-01-02T15:04:05Z", "token": "oklol"}`)) // }) // mux.HandleFunc("/decisions/1", func(w http.ResponseWriter, r *http.Request) { // testMethod(t, r, "DELETE") // w.WriteHeader(http.StatusOK) // w.Write([]byte(`{"nbDeleted":"1"}`)) // }) // log.Printf("URL is %s", urlx) // apiURL, err := url.Parse(urlx + "/") // if err != nil { // t.Fatalf("parsing api url: %s", apiURL) // } // client, err := NewClient(&Config{ // MachineID: "test_login", // Password: "test_password", // UserAgent: fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()), // URL: apiURL, // VersionPrefix: "v1", // }) // if err != nil { // t.Fatalf("new api client: %s", err) // } // filters := DecisionsDeleteOpts{IPEquals: new(string)} // *filters.IPEquals = "1.2.3.4" // deleted, _, err := client.Decisions.Delete(context.Background(), filters) // if err != nil { // t.Fatalf("unexpected err : %s", err) // } // assert.Equal(t, "1", deleted.NbDeleted) // defer teardown() // }