github.com/crowdsecurity/crowdsec@v1.6.1/pkg/cticlient/client_test.go (about)

     1  package cticlient
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"net/http"
     7  	"net/url"
     8  	"os"
     9  	"strconv"
    10  	"strings"
    11  	"testing"
    12  
    13  	log "github.com/sirupsen/logrus"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/crowdsecurity/go-cs-lib/ptr"
    18  )
    19  
    20  const validApiKey = "my-api-key"
    21  
    22  // Copy pasted from actual API response
    23  var smokeResponses = map[string]string{
    24  	"1.1.1.1": `{"ip_range_score": 0, "ip": "1.1.1.1", "ip_range": "1.1.1.0/24", "as_name": "CLOUDFLARENET", "as_num": 13335, "location": {"country": null, "city": null, "latitude": null, "longitude": null}, "reverse_dns": "one.one.one.one", "behaviors": [{"name": "ssh:bruteforce", "label": "SSH Bruteforce", "description": "IP has been reported for performing brute force on ssh services."}, {"name": "tcp:scan", "label": "TCP Scan", "description": "IP has been reported for performing TCP port scanning."}, {"name": "http:scan", "label": "HTTP Scan", "description": "IP has been reported for performing actions related to HTTP vulnerability scanning and discovery."}], "history": {"first_seen": "2021-04-18T18:00:00+00:00", "last_seen": "2022-11-23T13:00:00+00:00", "full_age": 583, "days_age": 583}, "classifications": {"false_positives": [], "classifications": [{"name": "profile:insecure_services", "label": "Dangerous Services Exposed", "description": "IP exposes dangerous services (vnc, telnet, rdp), possibly due to a misconfiguration or because it's a honeypot."}, {"name": "profile:many_services", "label": "Many Services Exposed", "description": "IP exposes many open port, possibly due to a misconfiguration or because it's a honeypot."}]}, "attack_details": [{"name": "crowdsecurity/ssh-bf", "label": "SSH Bruteforce", "description": "Detect ssh brute force", "references": []}, {"name": "crowdsecurity/iptables-scan-multi_ports", "label": "Port Scanner", "description": "Detect tcp port scan", "references": []}, {"name": "crowdsecurity/ssh-slow-bf", "label": "Slow SSH Bruteforce", "description": "Detect slow ssh brute force", "references": []}, {"name": "crowdsecurity/http-probing", "label": "HTTP Scanner", "description": "Detect site scanning/probing from a single ip", "references": []}, {"name": "crowdsecurity/http-path-traversal-probing", "label": "Path Traversal Scanner", "description": "Detect path traversal attempt", "references": []}, {"name": "crowdsecurity/http-bad-user-agent", "label": "Known Bad User-Agent", "description": "Detect bad user-agents", "references": []}], "target_countries": {"DE": 33, "FR": 25, "US": 12, "CA": 8, "JP": 8, "AT": 4, "GB": 4, "AE": 4}, "background_noise_score": 4, "scores": {"overall": {"aggressiveness": 2, "threat": 2, "trust": 1, "anomaly": 2, "total": 2}, "last_day": {"aggressiveness": 0, "threat": 0, "trust": 0, "anomaly": 2, "total": 0}, "last_week": {"aggressiveness": 1, "threat": 2, "trust": 0, "anomaly": 2, "total": 1}, "last_month": {"aggressiveness": 3, "threat": 2, "trust": 0, "anomaly": 2, "total": 2}}, "references": []}`,
    25  }
    26  
    27  var fireResponses []string
    28  
    29  // RoundTripFunc .
    30  type RoundTripFunc func(req *http.Request) *http.Response
    31  
    32  // RoundTrip .
    33  func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
    34  	return f(req), nil
    35  }
    36  
    37  // wip
    38  func fireHandler(req *http.Request) *http.Response {
    39  	var err error
    40  
    41  	apiKey := req.Header.Get("x-api-key")
    42  	if apiKey != validApiKey {
    43  		log.Warningf("invalid api key: %s", apiKey)
    44  
    45  		return &http.Response{
    46  			StatusCode: http.StatusForbidden,
    47  			Body:       nil,
    48  			Header:     make(http.Header),
    49  		}
    50  	}
    51  
    52  	//unmarshal data
    53  	if fireResponses == nil {
    54  		page1, err := os.ReadFile("tests/fire-page1.json")
    55  		if err != nil {
    56  			panic("can't read file")
    57  		}
    58  
    59  		page2, err := os.ReadFile("tests/fire-page2.json")
    60  		if err != nil {
    61  			panic("can't read file")
    62  		}
    63  
    64  		fireResponses = []string{string(page1), string(page2)}
    65  	}
    66  	//let's assume we have two valid pages.
    67  	page := 1
    68  	if req.URL.Query().Get("page") != "" {
    69  		page, err = strconv.Atoi(req.URL.Query().Get("page"))
    70  		if err != nil {
    71  			log.Warningf("no page ?!")
    72  			return &http.Response{StatusCode: http.StatusInternalServerError}
    73  		}
    74  	}
    75  
    76  	//how to react if you give a page number that is too big ?
    77  	if page > len(fireResponses) {
    78  		log.Warningf(" page too big %d vs %d", page, len(fireResponses))
    79  
    80  		emptyResponse := `{
    81  			"_links": {
    82  			  "first": {
    83  				"href": "https://cti.api.crowdsec.net/v1/fire/"
    84  			  },
    85  			  "self": {
    86  				"href": "https://cti.api.crowdsec.net/v1/fire/?page=3&limit=3"
    87  			  }
    88  			},
    89  			"items": []
    90  		  }
    91  		  `
    92  
    93  		return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(emptyResponse))}
    94  	}
    95  
    96  	reader := io.NopCloser(strings.NewReader(fireResponses[page-1]))
    97  	//we should care about limit too
    98  	return &http.Response{
    99  		StatusCode: http.StatusOK,
   100  		// Send response to be tested
   101  		Body:          reader,
   102  		Header:        make(http.Header),
   103  		ContentLength: 0,
   104  	}
   105  }
   106  
   107  func smokeHandler(req *http.Request) *http.Response {
   108  	apiKey := req.Header.Get("x-api-key")
   109  	if apiKey != validApiKey {
   110  		return &http.Response{
   111  			StatusCode: http.StatusForbidden,
   112  			Body:       nil,
   113  			Header:     make(http.Header),
   114  		}
   115  	}
   116  
   117  	requestedIP := strings.Split(req.URL.Path, "/")[3]
   118  
   119  	response, ok := smokeResponses[requestedIP]
   120  	if !ok {
   121  		return &http.Response{
   122  			StatusCode: http.StatusNotFound,
   123  			Body:       io.NopCloser(strings.NewReader(`{"message": "IP address information not found"}`)),
   124  			Header:     make(http.Header),
   125  		}
   126  	}
   127  
   128  	reader := io.NopCloser(strings.NewReader(response))
   129  
   130  	return &http.Response{
   131  		StatusCode: http.StatusOK,
   132  		// Send response to be tested
   133  		Body:          reader,
   134  		Header:        make(http.Header),
   135  		ContentLength: 0,
   136  	}
   137  }
   138  
   139  func rateLimitedHandler(req *http.Request) *http.Response {
   140  	apiKey := req.Header.Get("x-api-key")
   141  	if apiKey != validApiKey {
   142  		return &http.Response{
   143  			StatusCode: http.StatusForbidden,
   144  			Body:       nil,
   145  			Header:     make(http.Header),
   146  		}
   147  	}
   148  
   149  	return &http.Response{
   150  		StatusCode: http.StatusTooManyRequests,
   151  		Body:       nil,
   152  		Header:     make(http.Header),
   153  	}
   154  }
   155  
   156  func searchHandler(req *http.Request) *http.Response {
   157  	apiKey := req.Header.Get("x-api-key")
   158  	if apiKey != validApiKey {
   159  		return &http.Response{
   160  			StatusCode: http.StatusForbidden,
   161  			Body:       nil,
   162  			Header:     make(http.Header),
   163  		}
   164  	}
   165  
   166  	url, _ := url.Parse(req.URL.String())
   167  
   168  	ipsParam := url.Query().Get("ips")
   169  	if ipsParam == "" {
   170  		return &http.Response{
   171  			StatusCode: http.StatusBadRequest,
   172  			Body:       nil,
   173  			Header:     make(http.Header),
   174  		}
   175  	}
   176  
   177  	totalIps := 0
   178  	notFound := 0
   179  
   180  	ips := strings.Split(ipsParam, ",")
   181  	for _, ip := range ips {
   182  		_, ok := smokeResponses[ip]
   183  		if ok {
   184  			totalIps++
   185  		} else {
   186  			notFound++
   187  		}
   188  	}
   189  
   190  	response := fmt.Sprintf(`{"total": %d, "not_found": %d, "items": [`, totalIps, notFound)
   191  	for _, ip := range ips {
   192  		response += smokeResponses[ip]
   193  	}
   194  
   195  	response += "]}"
   196  	reader := io.NopCloser(strings.NewReader(response))
   197  
   198  	return &http.Response{
   199  		StatusCode: http.StatusOK,
   200  		Body:       reader,
   201  		Header:     make(http.Header),
   202  	}
   203  }
   204  
   205  func TestBadFireAuth(t *testing.T) {
   206  	ctiClient := NewCrowdsecCTIClient(WithAPIKey("asdasd"), WithHTTPClient(&http.Client{
   207  		Transport: RoundTripFunc(fireHandler),
   208  	}))
   209  	_, err := ctiClient.Fire(FireParams{})
   210  	require.EqualError(t, err, ErrUnauthorized.Error())
   211  }
   212  
   213  func TestFireOk(t *testing.T) {
   214  	cticlient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
   215  		Transport: RoundTripFunc(fireHandler),
   216  	}))
   217  	data, err := cticlient.Fire(FireParams{})
   218  	require.NoError(t, err)
   219  	assert.Len(t, data.Items, 3)
   220  	assert.Equal(t, "1.2.3.4", data.Items[0].Ip)
   221  	//page 1 is the default
   222  	data, err = cticlient.Fire(FireParams{Page: ptr.Of(1)})
   223  	require.NoError(t, err)
   224  	assert.Len(t, data.Items, 3)
   225  	assert.Equal(t, "1.2.3.4", data.Items[0].Ip)
   226  	//page 2
   227  	data, err = cticlient.Fire(FireParams{Page: ptr.Of(2)})
   228  	require.NoError(t, err)
   229  	assert.Len(t, data.Items, 3)
   230  	assert.Equal(t, "4.2.3.4", data.Items[0].Ip)
   231  }
   232  
   233  func TestFirePaginator(t *testing.T) {
   234  	cticlient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
   235  		Transport: RoundTripFunc(fireHandler),
   236  	}))
   237  	paginator := NewFirePaginator(cticlient, FireParams{})
   238  	items, err := paginator.Next()
   239  	require.NoError(t, err)
   240  	assert.Len(t, items, 3)
   241  	assert.Equal(t, "1.2.3.4", items[0].Ip)
   242  	items, err = paginator.Next()
   243  	require.NoError(t, err)
   244  	assert.Len(t, items, 3)
   245  	assert.Equal(t, "4.2.3.4", items[0].Ip)
   246  	items, err = paginator.Next()
   247  	require.NoError(t, err)
   248  	assert.Empty(t, items)
   249  }
   250  
   251  func TestBadSmokeAuth(t *testing.T) {
   252  	ctiClient := NewCrowdsecCTIClient(WithAPIKey("asdasd"), WithHTTPClient(&http.Client{
   253  		Transport: RoundTripFunc(smokeHandler),
   254  	}))
   255  	_, err := ctiClient.GetIPInfo("1.1.1.1")
   256  	require.EqualError(t, err, ErrUnauthorized.Error())
   257  }
   258  
   259  func TestSmokeInfoValidIP(t *testing.T) {
   260  	ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
   261  		Transport: RoundTripFunc(smokeHandler),
   262  	}))
   263  
   264  	resp, err := ctiClient.GetIPInfo("1.1.1.1")
   265  	if err != nil {
   266  		t.Fatalf("failed to get ip info: %s", err)
   267  	}
   268  
   269  	assert.Equal(t, "1.1.1.1", resp.Ip)
   270  	assert.Equal(t, ptr.Of("1.1.1.0/24"), resp.IpRange)
   271  }
   272  
   273  func TestSmokeUnknownIP(t *testing.T) {
   274  	ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
   275  		Transport: RoundTripFunc(smokeHandler),
   276  	}))
   277  
   278  	resp, err := ctiClient.GetIPInfo("42.42.42.42")
   279  	if err != nil {
   280  		t.Fatalf("failed to get ip info: %s", err)
   281  	}
   282  
   283  	assert.Equal(t, "", resp.Ip)
   284  }
   285  
   286  func TestRateLimit(t *testing.T) {
   287  	ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
   288  		Transport: RoundTripFunc(rateLimitedHandler),
   289  	}))
   290  	_, err := ctiClient.GetIPInfo("1.1.1.1")
   291  	require.EqualError(t, err, ErrLimit.Error())
   292  }
   293  
   294  func TestSearchIPs(t *testing.T) {
   295  	ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
   296  		Transport: RoundTripFunc(searchHandler),
   297  	}))
   298  
   299  	resp, err := ctiClient.SearchIPs([]string{"1.1.1.1", "42.42.42.42"})
   300  	if err != nil {
   301  		t.Fatalf("failed to search ips: %s", err)
   302  	}
   303  
   304  	assert.Equal(t, 1, resp.Total)
   305  	assert.Equal(t, 1, resp.NotFound)
   306  	assert.Len(t, resp.Items, 1)
   307  	assert.Equal(t, "1.1.1.1", resp.Items[0].Ip)
   308  }
   309  
   310  //TODO: fire tests + pagination
   311  
   312  func TestFireInit(t *testing.T) {
   313  
   314  }