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  // }