crowdsec/pkg/cticlient/client_test.go
mmetc 5622ac8338
CI: enable testifylint (#2696)
- reverse actual and expected values
 - use assert.False, assert.True
 - use assert.Len, assert.Emtpy
 - use require.Error, require.NoError
 - use assert.InDelta
2024-01-05 15:26:13 +01:00

314 lines
10 KiB
Go

package cticlient
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"testing"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/crowdsecurity/go-cs-lib/ptr"
)
const validApiKey = "my-api-key"
// Copy pasted from actual API response
var smokeResponses = map[string]string{
"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": []}`,
}
var fireResponses []string
// RoundTripFunc .
type RoundTripFunc func(req *http.Request) *http.Response
// RoundTrip .
func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req), nil
}
// wip
func fireHandler(req *http.Request) *http.Response {
var err error
apiKey := req.Header.Get("x-api-key")
if apiKey != validApiKey {
log.Warningf("invalid api key: %s", apiKey)
return &http.Response{
StatusCode: http.StatusForbidden,
Body: nil,
Header: make(http.Header),
}
}
//unmarshal data
if fireResponses == nil {
page1, err := os.ReadFile("tests/fire-page1.json")
if err != nil {
panic("can't read file")
}
page2, err := os.ReadFile("tests/fire-page2.json")
if err != nil {
panic("can't read file")
}
fireResponses = []string{string(page1), string(page2)}
}
//let's assume we have two valid pages.
page := 1
if req.URL.Query().Get("page") != "" {
page, err = strconv.Atoi(req.URL.Query().Get("page"))
if err != nil {
log.Warningf("no page ?!")
return &http.Response{StatusCode: http.StatusInternalServerError}
}
}
//how to react if you give a page number that is too big ?
if page > len(fireResponses) {
log.Warningf(" page too big %d vs %d", page, len(fireResponses))
emptyResponse := `{
"_links": {
"first": {
"href": "https://cti.api.crowdsec.net/v1/fire/"
},
"self": {
"href": "https://cti.api.crowdsec.net/v1/fire/?page=3&limit=3"
}
},
"items": []
}
`
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(emptyResponse))}
}
reader := io.NopCloser(strings.NewReader(fireResponses[page-1]))
//we should care about limit too
return &http.Response{
StatusCode: http.StatusOK,
// Send response to be tested
Body: reader,
Header: make(http.Header),
ContentLength: 0,
}
}
func smokeHandler(req *http.Request) *http.Response {
apiKey := req.Header.Get("x-api-key")
if apiKey != validApiKey {
return &http.Response{
StatusCode: http.StatusForbidden,
Body: nil,
Header: make(http.Header),
}
}
requestedIP := strings.Split(req.URL.Path, "/")[3]
response, ok := smokeResponses[requestedIP]
if !ok {
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(strings.NewReader(`{"message": "IP address information not found"}`)),
Header: make(http.Header),
}
}
reader := io.NopCloser(strings.NewReader(response))
return &http.Response{
StatusCode: http.StatusOK,
// Send response to be tested
Body: reader,
Header: make(http.Header),
ContentLength: 0,
}
}
func rateLimitedHandler(req *http.Request) *http.Response {
apiKey := req.Header.Get("x-api-key")
if apiKey != validApiKey {
return &http.Response{
StatusCode: http.StatusForbidden,
Body: nil,
Header: make(http.Header),
}
}
return &http.Response{
StatusCode: http.StatusTooManyRequests,
Body: nil,
Header: make(http.Header),
}
}
func searchHandler(req *http.Request) *http.Response {
apiKey := req.Header.Get("x-api-key")
if apiKey != validApiKey {
return &http.Response{
StatusCode: http.StatusForbidden,
Body: nil,
Header: make(http.Header),
}
}
url, _ := url.Parse(req.URL.String())
ipsParam := url.Query().Get("ips")
if ipsParam == "" {
return &http.Response{
StatusCode: http.StatusBadRequest,
Body: nil,
Header: make(http.Header),
}
}
totalIps := 0
notFound := 0
ips := strings.Split(ipsParam, ",")
for _, ip := range ips {
_, ok := smokeResponses[ip]
if ok {
totalIps++
} else {
notFound++
}
}
response := fmt.Sprintf(`{"total": %d, "not_found": %d, "items": [`, totalIps, notFound)
for _, ip := range ips {
response += smokeResponses[ip]
}
response += "]}"
reader := io.NopCloser(strings.NewReader(response))
return &http.Response{
StatusCode: http.StatusOK,
Body: reader,
Header: make(http.Header),
}
}
func TestBadFireAuth(t *testing.T) {
ctiClient := NewCrowdsecCTIClient(WithAPIKey("asdasd"), WithHTTPClient(&http.Client{
Transport: RoundTripFunc(fireHandler),
}))
_, err := ctiClient.Fire(FireParams{})
require.EqualError(t, err, ErrUnauthorized.Error())
}
func TestFireOk(t *testing.T) {
cticlient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
Transport: RoundTripFunc(fireHandler),
}))
data, err := cticlient.Fire(FireParams{})
require.NoError(t, err)
assert.Len(t, data.Items, 3)
assert.Equal(t, "1.2.3.4", data.Items[0].Ip)
//page 1 is the default
data, err = cticlient.Fire(FireParams{Page: ptr.Of(1)})
require.NoError(t, err)
assert.Len(t, data.Items, 3)
assert.Equal(t, "1.2.3.4", data.Items[0].Ip)
//page 2
data, err = cticlient.Fire(FireParams{Page: ptr.Of(2)})
require.NoError(t, err)
assert.Len(t, data.Items, 3)
assert.Equal(t, "4.2.3.4", data.Items[0].Ip)
}
func TestFirePaginator(t *testing.T) {
cticlient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
Transport: RoundTripFunc(fireHandler),
}))
paginator := NewFirePaginator(cticlient, FireParams{})
items, err := paginator.Next()
require.NoError(t, err)
assert.Len(t, items, 3)
assert.Equal(t, "1.2.3.4", items[0].Ip)
items, err = paginator.Next()
require.NoError(t, err)
assert.Len(t, items, 3)
assert.Equal(t, "4.2.3.4", items[0].Ip)
items, err = paginator.Next()
require.NoError(t, err)
assert.Empty(t, items)
}
func TestBadSmokeAuth(t *testing.T) {
ctiClient := NewCrowdsecCTIClient(WithAPIKey("asdasd"), WithHTTPClient(&http.Client{
Transport: RoundTripFunc(smokeHandler),
}))
_, err := ctiClient.GetIPInfo("1.1.1.1")
require.EqualError(t, err, ErrUnauthorized.Error())
}
func TestSmokeInfoValidIP(t *testing.T) {
ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
Transport: RoundTripFunc(smokeHandler),
}))
resp, err := ctiClient.GetIPInfo("1.1.1.1")
if err != nil {
t.Fatalf("failed to get ip info: %s", err)
}
assert.Equal(t, "1.1.1.1", resp.Ip)
assert.Equal(t, ptr.Of("1.1.1.0/24"), resp.IpRange)
}
func TestSmokeUnknownIP(t *testing.T) {
ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
Transport: RoundTripFunc(smokeHandler),
}))
resp, err := ctiClient.GetIPInfo("42.42.42.42")
if err != nil {
t.Fatalf("failed to get ip info: %s", err)
}
assert.Equal(t, "", resp.Ip)
}
func TestRateLimit(t *testing.T) {
ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
Transport: RoundTripFunc(rateLimitedHandler),
}))
_, err := ctiClient.GetIPInfo("1.1.1.1")
require.EqualError(t, err, ErrLimit.Error())
}
func TestSearchIPs(t *testing.T) {
ctiClient := NewCrowdsecCTIClient(WithAPIKey(validApiKey), WithHTTPClient(&http.Client{
Transport: RoundTripFunc(searchHandler),
}))
resp, err := ctiClient.SearchIPs([]string{"1.1.1.1", "42.42.42.42"})
if err != nil {
t.Fatalf("failed to search ips: %s", err)
}
assert.Equal(t, 1, resp.Total)
assert.Equal(t, 1, resp.NotFound)
assert.Len(t, resp.Items, 1)
assert.Equal(t, "1.1.1.1", resp.Items[0].Ip)
}
//TODO: fire tests + pagination
func TestFireInit(t *testing.T) {
}