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 }