Revamp unit tests (#1368)

* Revamp unit tests
* Increase coverage
* Use go-acc to get cross packages coverage

Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
This commit is contained in:
Thibault "bui" Koechlin 2022-03-29 14:20:26 +02:00 committed by GitHub
parent 3f24bcdbcf
commit d8dc01cd94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 8536 additions and 989 deletions

View file

@ -62,11 +62,9 @@ jobs:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Build - name: Build
run: make build && go get -u github.com/jandelgado/gcov2lcov run: make build && go get -u github.com/jandelgado/gcov2lcov && go get -u github.com/ory/go-acc
- name: Build package
run: make package
- name: All tests - name: All tests
run: go test -coverprofile=coverage.out -covermode=atomic ./... run: go run github.com/ory/go-acc ./... -o coverage.out --ignore database,notifications,protobufs,cwversion,cstest,models
- name: gcov2lcov - name: gcov2lcov
uses: jandelgado/gcov2lcov-action@v1.0.2 uses: jandelgado/gcov2lcov-action@v1.0.2
with: with:

View file

@ -1,13 +1,13 @@
package main package main
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"github.com/crowdsecurity/crowdsec/pkg/cstest" "github.com/crowdsecurity/crowdsec/pkg/cstest"
"github.com/crowdsecurity/crowdsec/pkg/types"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -63,7 +63,7 @@ cscli explain --dsn "file://myfile.log" --type nginx
log.Fatalf("unable to get absolue path of '%s', exiting", logFile) log.Fatalf("unable to get absolue path of '%s', exiting", logFile)
} }
dsn = fmt.Sprintf("file://%s", absolutePath) dsn = fmt.Sprintf("file://%s", absolutePath)
lineCount := getLineCountForFile(absolutePath) lineCount := types.GetLineCountForFile(absolutePath)
if lineCount > 100 { if lineCount > 100 {
log.Warnf("log file contains %d lines. This may take lot of resources.", lineCount) log.Warnf("log file contains %d lines. This may take lot of resources.", lineCount)
} }
@ -112,17 +112,3 @@ cscli explain --dsn "file://myfile.log" --type nginx
return cmdExplain return cmdExplain
} }
func getLineCountForFile(filepath string) int {
f, err := os.Open(filepath)
if err != nil {
log.Fatalf("unable to open log file %s", filepath)
}
defer f.Close()
lc := 0
fs := bufio.NewScanner(f)
for fs.Scan() {
lc++
}
return lc
}

3
go.mod
View file

@ -38,6 +38,7 @@ require (
github.com/hashicorp/go-version v1.2.1 github.com/hashicorp/go-version v1.2.1
github.com/influxdata/go-syslog/v3 v3.0.0 github.com/influxdata/go-syslog/v3 v3.0.0
github.com/jackc/pgx/v4 v4.14.1 github.com/jackc/pgx/v4 v4.14.1
github.com/jarcoal/httpmock v1.1.0
github.com/jszwec/csvutil v1.5.1 github.com/jszwec/csvutil v1.5.1
github.com/lib/pq v1.10.4 github.com/lib/pq v1.10.4
github.com/mattn/go-sqlite3 v1.14.10 github.com/mattn/go-sqlite3 v1.14.10
@ -148,7 +149,7 @@ require (
go.mongodb.org/mongo-driver v1.4.4 // indirect go.mongodb.org/mongo-driver v1.4.4 // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220317022123-2c4bbad7e934 // indirect golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 // indirect
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect

6
go.sum
View file

@ -610,6 +610,8 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jarcoal/httpmock v1.1.0 h1:F47ChZj1Y2zFsCXxNkBPwNNKnAyOATcdQibk0qEdVCE=
github.com/jarcoal/httpmock v1.1.0/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
@ -1253,8 +1255,8 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220317022123-2c4bbad7e934 h1:GwUTNnIS5asZGjc34dMBLO/LLp4kEvyZr/8wlQs1Bt8= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4=
golang.org/x/sys v0.0.0-20220317022123-2c4bbad7e934/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=

View file

@ -63,8 +63,11 @@ func NewClient(config *Config) (*ApiClient, error) {
func NewDefaultClient(URL *url.URL, prefix string, userAgent string, client *http.Client) (*ApiClient, error) { func NewDefaultClient(URL *url.URL, prefix string, userAgent string, client *http.Client) (*ApiClient, error) {
if client == nil { if client == nil {
client = &http.Client{} client = &http.Client{}
if ht, ok := http.DefaultTransport.(*http.Transport); ok {
ht.TLSClientConfig = &tls.Config{InsecureSkipVerify: InsecureSkipVerify}
client.Transport = ht
}
} }
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: InsecureSkipVerify}
c := &ApiClient{client: client, BaseURL: URL, UserAgent: userAgent, URLPrefix: prefix} c := &ApiClient{client: client, BaseURL: URL, UserAgent: userAgent, URLPrefix: prefix}
c.common.client = c c.common.client = c
c.Decisions = (*DecisionsService)(&c.common) c.Decisions = (*DecisionsService)(&c.common)

View file

@ -3,13 +3,11 @@ package apiserver
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time"
"github.com/crowdsecurity/crowdsec/pkg/csplugin" "github.com/crowdsecurity/crowdsec/pkg/csplugin"
"github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/models"
@ -19,6 +17,48 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
type LAPI struct {
router *gin.Engine
loginResp models.WatcherAuthResponse
bouncerKey string
t *testing.T
}
func SetupLAPITest(t *testing.T) LAPI {
t.Helper()
router, loginResp, err := InitMachineTest()
if err != nil {
t.Fatal(err.Error())
}
APIKey, err := CreateTestBouncer()
if err != nil {
t.Fatalf("%s", err.Error())
}
return LAPI{
router: router,
loginResp: loginResp,
bouncerKey: APIKey,
}
}
func (l *LAPI) InsertAlertFromFile(path string) *httptest.ResponseRecorder {
alertReader := GetAlertReaderFromFile(path)
return l.RecordResponse("POST", "/v1/alerts", alertReader)
}
func (l *LAPI) RecordResponse(verb string, url string, body *strings.Reader) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
req, err := http.NewRequest(verb, url, body)
if err != nil {
l.t.Fatal(err)
}
req.Header.Add("X-Api-Key", l.bouncerKey)
AddAuthHeaders(req, l.loginResp)
l.router.ServeHTTP(w, req)
return w
}
func InitMachineTest() (*gin.Engine, models.WatcherAuthResponse, error) { func InitMachineTest() (*gin.Engine, models.WatcherAuthResponse, error) {
router, err := NewAPITest() router, err := NewAPITest()
if err != nil { if err != nil {
@ -61,82 +101,40 @@ func AddAuthHeaders(request *http.Request, authResponse models.WatcherAuthRespon
} }
func TestSimulatedAlert(t *testing.T) { func TestSimulatedAlert(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil { lapi.InsertAlertFromFile("./tests/alert_minibulk+simul.json")
log.Fatalln(err.Error()) alertContent := GetAlertReaderFromFile("./tests/alert_minibulk+simul.json")
}
alertContentBytes, err := ioutil.ReadFile("./tests/alert_minibulk+simul.json")
if err != nil {
log.Fatal(err)
}
alertContent := string(alertContentBytes)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
//exclude decision in simulation mode //exclude decision in simulation mode
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?simulated=false", strings.NewReader(alertContent)) w := lapi.RecordResponse("GET", "/v1/alerts?simulated=false", alertContent)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over `) assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over `)
assert.NotContains(t, w.Body.String(), `"message":"Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over `) assert.NotContains(t, w.Body.String(), `"message":"Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over `)
//include decision in simulation mode //include decision in simulation mode
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?simulated=true", strings.NewReader(alertContent)) w = lapi.RecordResponse("GET", "/v1/alerts?simulated=true", alertContent)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over `) assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over `)
assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over `) assert.Contains(t, w.Body.String(), `"message":"Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over `)
} }
func TestCreateAlert(t *testing.T) { func TestCreateAlert(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
// Create Alert with invalid format // Create Alert with invalid format
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader("test"))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
w := lapi.RecordResponse("POST", "/v1/alerts", strings.NewReader("test"))
assert.Equal(t, 400, w.Code) assert.Equal(t, 400, w.Code)
assert.Equal(t, "{\"message\":\"invalid character 'e' in literal true (expecting 'r')\"}", w.Body.String()) assert.Equal(t, "{\"message\":\"invalid character 'e' in literal true (expecting 'r')\"}", w.Body.String())
// Create Alert with invalid input // Create Alert with invalid input
alertContentBytes, err := ioutil.ReadFile("./tests/invalidAlert_sample.json") alertContent := GetAlertReaderFromFile("./tests/invalidAlert_sample.json")
if err != nil {
log.Fatal(err)
}
alertContent := string(alertContentBytes)
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
w = lapi.RecordResponse("POST", "/v1/alerts", alertContent)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Equal(t, "{\"message\":\"validation failure list:\\n0.scenario in body is required\\n0.scenario_hash in body is required\\n0.scenario_version in body is required\\n0.simulated in body is required\\n0.source in body is required\"}", w.Body.String()) assert.Equal(t, "{\"message\":\"validation failure list:\\n0.scenario in body is required\\n0.scenario_hash in body is required\\n0.scenario_version in body is required\\n0.simulated in body is required\\n0.source in body is required\"}", w.Body.String())
// Create Valid Alert // Create Valid Alert
alertContentBytes, err = ioutil.ReadFile("./tests/alert_sample.json") w = lapi.InsertAlertFromFile("./tests/alert_sample.json")
if err != nil {
log.Fatal(err)
}
alertContent = string(alertContentBytes)
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code) assert.Equal(t, 201, w.Code)
assert.Equal(t, "[\"1\"]", w.Body.String()) assert.Equal(t, "[\"1\"]", w.Body.String())
} }
@ -154,12 +152,7 @@ func TestCreateAlertChannels(t *testing.T) {
if err != nil { if err != nil {
log.Fatalln(err.Error()) log.Fatalln(err.Error())
} }
lapi := LAPI{router: apiServer.router, loginResp: loginResp}
alertContentBytes, err := ioutil.ReadFile("./tests/alert_ssh-bf.json")
if err != nil {
log.Fatal(err)
}
alertContent := string(alertContentBytes)
var pd csplugin.ProfileAlert var pd csplugin.ProfileAlert
var wg sync.WaitGroup var wg sync.WaitGroup
@ -170,389 +163,248 @@ func TestCreateAlertChannels(t *testing.T) {
wg.Done() wg.Done()
}() }()
go func() { go lapi.InsertAlertFromFile("./tests/alert_ssh-bf.json")
for {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
apiServer.controller.Router.ServeHTTP(w, req)
break
}
}()
wg.Wait() wg.Wait()
assert.Equal(t, len(pd.Alert.Decisions), 1) assert.Equal(t, len(pd.Alert.Decisions), 1)
apiServer.Close() apiServer.Close()
} }
func TestAlertListFilters(t *testing.T) { func TestAlertListFilters(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil { lapi.InsertAlertFromFile("./tests/alert_ssh-bf.json")
log.Fatalln(err.Error()) alertContent := GetAlertReaderFromFile("./tests/alert_ssh-bf.json")
}
alertContentBytes, err := ioutil.ReadFile("./tests/alert_ssh-bf.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts {
*alert.StartAt = time.Now().UTC().Format(time.RFC3339)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339)
}
alertContent, err := json.Marshal(alerts)
if err != nil {
log.Fatal(err)
}
//create one alert
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent)))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
//bad filter //bad filter
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?test=test", strings.NewReader(string(alertContent))) w := lapi.RecordResponse("GET", "/v1/alerts?test=test", alertContent)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Equal(t, "{\"message\":\"Filter parameter 'test' is unknown (=test): invalid filter\"}", w.Body.String()) assert.Equal(t, "{\"message\":\"Filter parameter 'test' is unknown (=test): invalid filter\"}", w.Body.String())
//get without filters //get without filters
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts", nil) w = lapi.RecordResponse("GET", "/v1/alerts", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
//check alert and decision //check alert and decision
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test decision_type filter (ok) //test decision_type filter (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?decision_type=ban", nil) w = lapi.RecordResponse("GET", "/v1/alerts?decision_type=ban", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test decision_type filter (bad value) //test decision_type filter (bad value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?decision_type=ratata", nil) w = lapi.RecordResponse("GET", "/v1/alerts?decision_type=ratata", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "null", w.Body.String()) assert.Equal(t, "null", w.Body.String())
//test scope (ok) //test scope (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?scope=Ip", nil) w = lapi.RecordResponse("GET", "/v1/alerts?scope=Ip", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test scope (bad value) //test scope (bad value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?scope=rarara", nil) w = lapi.RecordResponse("GET", "/v1/alerts?scope=rarara", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "null", w.Body.String()) assert.Equal(t, "null", w.Body.String())
//test scenario (ok) //test scenario (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?scenario=crowdsecurity/ssh-bf", nil) w = lapi.RecordResponse("GET", "/v1/alerts?scenario=crowdsecurity/ssh-bf", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test scenario (bad value) //test scenario (bad value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?scenario=crowdsecurity/nope", nil) w = lapi.RecordResponse("GET", "/v1/alerts?scenario=crowdsecurity/nope", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "null", w.Body.String()) assert.Equal(t, "null", w.Body.String())
//test ip (ok) //test ip (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?ip=91.121.79.195", nil) w = lapi.RecordResponse("GET", "/v1/alerts?ip=91.121.79.195", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test ip (bad value) //test ip (bad value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?ip=99.122.77.195", nil) w = lapi.RecordResponse("GET", "/v1/alerts?ip=99.122.77.195", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "null", w.Body.String()) assert.Equal(t, "null", w.Body.String())
//test ip (invalid value) //test ip (invalid value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?ip=gruueq", nil) w = lapi.RecordResponse("GET", "/v1/alerts?ip=gruueq", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Equal(t, `{"message":"unable to convert 'gruueq' to int: invalid address: invalid ip address / range"}`, w.Body.String()) assert.Equal(t, `{"message":"unable to convert 'gruueq' to int: invalid address: invalid ip address / range"}`, w.Body.String())
//test range (ok) //test range (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?range=91.121.79.0/24&contains=false", nil) w = lapi.RecordResponse("GET", "/v1/alerts?range=91.121.79.0/24&contains=false", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test range //test range
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?range=99.122.77.0/24&contains=false", nil) w = lapi.RecordResponse("GET", "/v1/alerts?range=99.122.77.0/24&contains=false", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "null", w.Body.String()) assert.Equal(t, "null", w.Body.String())
//test range (invalid value) //test range (invalid value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?range=ratata", nil) w = lapi.RecordResponse("GET", "/v1/alerts?range=ratata", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Equal(t, `{"message":"unable to convert 'ratata' to int: invalid address: invalid ip address / range"}`, w.Body.String()) assert.Equal(t, `{"message":"unable to convert 'ratata' to int: invalid address: invalid ip address / range"}`, w.Body.String())
//test since (ok) //test since (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?since=1h", nil) w = lapi.RecordResponse("GET", "/v1/alerts?since=1h", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test since (ok but yelds no results) //test since (ok but yelds no results)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?since=1ns", nil) w = lapi.RecordResponse("GET", "/v1/alerts?since=1ns", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "null", w.Body.String()) assert.Equal(t, "null", w.Body.String())
//test since (invalid value) //test since (invalid value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?since=1zuzu", nil) w = lapi.RecordResponse("GET", "/v1/alerts?since=1zuzu", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), `{"message":"while parsing duration: time: unknown unit`) assert.Contains(t, w.Body.String(), `{"message":"while parsing duration: time: unknown unit`)
//test until (ok) //test until (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?until=1ns", nil) w = lapi.RecordResponse("GET", "/v1/alerts?until=1ns", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test until (ok but no return) //test until (ok but no return)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?until=1m", nil) w = lapi.RecordResponse("GET", "/v1/alerts?until=1m", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "null", w.Body.String()) assert.Equal(t, "null", w.Body.String())
//test until (invalid value) //test until (invalid value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?until=1zuzu", nil) w = lapi.RecordResponse("GET", "/v1/alerts?until=1zuzu", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Contains(t, w.Body.String(), `{"message":"while parsing duration: time: unknown unit`) assert.Contains(t, w.Body.String(), `{"message":"while parsing duration: time: unknown unit`)
//test simulated (ok) //test simulated (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?simulated=true", nil) w = lapi.RecordResponse("GET", "/v1/alerts?simulated=true", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test simulated (ok) //test simulated (ok)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?simulated=false", nil) w = lapi.RecordResponse("GET", "/v1/alerts?simulated=false", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test has active decision //test has active decision
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?has_active_decision=true", nil) w = lapi.RecordResponse("GET", "/v1/alerts?has_active_decision=true", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ") assert.Contains(t, w.Body.String(), "Ip 91.121.79.195 performed 'crowdsecurity/ssh-bf' (6 events over ")
assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`) assert.Contains(t, w.Body.String(), `scope":"Ip","simulated":false,"type":"ban","value":"91.121.79.195"`)
//test has active decision //test has active decision
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?has_active_decision=false", nil) w = lapi.RecordResponse("GET", "/v1/alerts?has_active_decision=false", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "null", w.Body.String()) assert.Equal(t, "null", w.Body.String())
//test has active decision (invalid value) //test has active decision (invalid value)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?has_active_decision=ratatqata", nil) w = lapi.RecordResponse("GET", "/v1/alerts?has_active_decision=ratatqata", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Equal(t, `{"message":"'ratatqata' is not a boolean: strconv.ParseBool: parsing \"ratatqata\": invalid syntax: unable to parse type"}`, w.Body.String()) assert.Equal(t, `{"message":"'ratatqata' is not a boolean: strconv.ParseBool: parsing \"ratatqata\": invalid syntax: unable to parse type"}`, w.Body.String())
} }
func TestAlertBulkInsert(t *testing.T) { func TestAlertBulkInsert(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
//insert a bulk of 20 alerts to trigger bulk insert //insert a bulk of 20 alerts to trigger bulk insert
alertContentBytes, err := ioutil.ReadFile("./tests/alert_bulk.json") lapi.InsertAlertFromFile("./tests/alert_bulk.json")
if err != nil { alertContent := GetAlertReaderFromFile("./tests/alert_bulk.json")
log.Fatal(err)
}
alertContent := string(alertContentBytes)
w := httptest.NewRecorder() w := lapi.RecordResponse("GET", "/v1/alerts", alertContent)
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
} }
func TestListAlert(t *testing.T) { func TestListAlert(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil { lapi.InsertAlertFromFile("./tests/alert_sample.json")
log.Fatalln(err.Error())
}
alertContentBytes, err := ioutil.ReadFile("./tests/alert_sample.json")
if err != nil {
log.Fatal(err)
}
alertContent := string(alertContentBytes)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
// List Alert with invalid filter // List Alert with invalid filter
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts?test=test", nil) w := lapi.RecordResponse("GET", "/v1/alerts?test=test", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Equal(t, "{\"message\":\"Filter parameter 'test' is unknown (=test): invalid filter\"}", w.Body.String()) assert.Equal(t, "{\"message\":\"Filter parameter 'test' is unknown (=test): invalid filter\"}", w.Body.String())
// List Alert // List Alert
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/alerts", nil)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
w = lapi.RecordResponse("GET", "/v1/alerts", emptyBody)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "crowdsecurity/test") assert.Contains(t, w.Body.String(), "crowdsecurity/test")
} }
func TestCreateAlertErrors(t *testing.T) { func TestCreateAlertErrors(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil { alertContent := GetAlertReaderFromFile("./tests/alert_sample.json")
log.Fatalln(err.Error())
}
alertContentBytes, err := ioutil.ReadFile("./tests/alert_sample.json")
if err != nil {
log.Fatal(err)
}
alertContent := string(alertContentBytes)
//test invalid bearer //test invalid bearer
w := httptest.NewRecorder() w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent)) req, _ := http.NewRequest("POST", "/v1/alerts", alertContent)
req.Header.Add("User-Agent", UserAgent) req.Header.Add("User-Agent", UserAgent)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "ratata")) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", "ratata"))
router.ServeHTTP(w, req) lapi.router.ServeHTTP(w, req)
assert.Equal(t, 401, w.Code) assert.Equal(t, 401, w.Code)
//test invalid bearer //test invalid bearer
w = httptest.NewRecorder() w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent)) req, _ = http.NewRequest("POST", "/v1/alerts", alertContent)
req.Header.Add("User-Agent", UserAgent) req.Header.Add("User-Agent", UserAgent)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", loginResp.Token+"s")) req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", lapi.loginResp.Token+"s"))
router.ServeHTTP(w, req) lapi.router.ServeHTTP(w, req)
assert.Equal(t, 401, w.Code) assert.Equal(t, 401, w.Code)
} }
func TestDeleteAlert(t *testing.T) { func TestDeleteAlert(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil { lapi.InsertAlertFromFile("./tests/alert_sample.json")
log.Fatalln(err.Error())
}
alertContentBytes, err := ioutil.ReadFile("./tests/alert_sample.json")
if err != nil {
log.Fatal(err)
}
alertContent := string(alertContentBytes)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
// Fail Delete Alert // Fail Delete Alert
w = httptest.NewRecorder() w := httptest.NewRecorder()
req, _ = http.NewRequest("DELETE", "/v1/alerts", strings.NewReader("")) req, _ := http.NewRequest("DELETE", "/v1/alerts", strings.NewReader(""))
AddAuthHeaders(req, loginResp) AddAuthHeaders(req, lapi.loginResp)
req.RemoteAddr = "127.0.0.2:4242" req.RemoteAddr = "127.0.0.2:4242"
router.ServeHTTP(w, req) lapi.router.ServeHTTP(w, req)
assert.Equal(t, 403, w.Code) assert.Equal(t, 403, w.Code)
assert.Equal(t, `{"message":"access forbidden from this IP (127.0.0.2)"}`, w.Body.String()) assert.Equal(t, `{"message":"access forbidden from this IP (127.0.0.2)"}`, w.Body.String())
// Delete Alert // Delete Alert
w = httptest.NewRecorder() w = httptest.NewRecorder()
req, _ = http.NewRequest("DELETE", "/v1/alerts", strings.NewReader("")) req, _ = http.NewRequest("DELETE", "/v1/alerts", strings.NewReader(""))
AddAuthHeaders(req, loginResp) AddAuthHeaders(req, lapi.loginResp)
req.RemoteAddr = "127.0.0.1:4242" req.RemoteAddr = "127.0.0.1:4242"
router.ServeHTTP(w, req) lapi.router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
} }
@ -579,17 +431,10 @@ func TestDeleteAlertTrustedIPS(t *testing.T) {
if err != nil { if err != nil {
log.Fatal(err.Error()) log.Fatal(err.Error())
} }
lapi := LAPI{
insertAlert := func() { router: router,
alertContentBytes, err := ioutil.ReadFile("./tests/alert_sample.json") loginResp: loginResp,
if err != nil { t: t,
log.Fatal(err)
}
alertContent := string(alertContentBytes)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(alertContent))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
} }
assertAlertDeleteFailedFromIP := func(ip string) { assertAlertDeleteFailedFromIP := func(ip string) {
@ -598,6 +443,7 @@ func TestDeleteAlertTrustedIPS(t *testing.T) {
AddAuthHeaders(req, loginResp) AddAuthHeaders(req, loginResp)
req.RemoteAddr = ip + ":1234" req.RemoteAddr = ip + ":1234"
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, 403, w.Code) assert.Equal(t, 403, w.Code)
assert.Contains(t, w.Body.String(), fmt.Sprintf(`{"message":"access forbidden from this IP (%s)"}`, ip)) assert.Contains(t, w.Body.String(), fmt.Sprintf(`{"message":"access forbidden from this IP (%s)"}`, ip))
@ -608,23 +454,24 @@ func TestDeleteAlertTrustedIPS(t *testing.T) {
req, _ := http.NewRequest("DELETE", "/v1/alerts", strings.NewReader("")) req, _ := http.NewRequest("DELETE", "/v1/alerts", strings.NewReader(""))
AddAuthHeaders(req, loginResp) AddAuthHeaders(req, loginResp)
req.RemoteAddr = ip + ":1234" req.RemoteAddr = ip + ":1234"
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
} }
insertAlert() lapi.InsertAlertFromFile("./tests/alert_sample.json")
assertAlertDeleteFailedFromIP("4.3.2.1") assertAlertDeleteFailedFromIP("4.3.2.1")
assertAlertDeletedFromIP("1.2.3.4") assertAlertDeletedFromIP("1.2.3.4")
insertAlert() lapi.InsertAlertFromFile("./tests/alert_sample.json")
assertAlertDeletedFromIP("1.2.4.0") assertAlertDeletedFromIP("1.2.4.0")
insertAlert() lapi.InsertAlertFromFile("./tests/alert_sample.json")
assertAlertDeletedFromIP("1.2.4.1") assertAlertDeletedFromIP("1.2.4.1")
insertAlert() lapi.InsertAlertFromFile("./tests/alert_sample.json")
assertAlertDeletedFromIP("1.2.4.255") assertAlertDeletedFromIP("1.2.4.255")
insertAlert() lapi.InsertAlertFromFile("./tests/alert_sample.json")
assertAlertDeletedFromIP("127.0.0.1") assertAlertDeletedFromIP("127.0.0.1")
} }

View file

@ -24,12 +24,16 @@ import (
"gopkg.in/tomb.v2" "gopkg.in/tomb.v2"
) )
const ( var (
PullInterval = "2h" PullInterval = time.Hour * 2
PushInterval = "30s" PushInterval = time.Second * 30
MetricsInterval = "30m" MetricsInterval = time.Minute * 30
) )
var SCOPE_CAPI string = "CAPI"
var SCOPE_CAPI_ALIAS string = "crowdsecurity/community-blocklist" //we don't use "CAPI" directly, to make it less confusing for the user
var SCOPE_LISTS string = "lists"
type apic struct { type apic struct {
pullInterval time.Duration pullInterval time.Duration
pushInterval time.Duration pushInterval time.Duration
@ -47,15 +51,6 @@ type apic struct {
consoleConfig *csconfig.ConsoleConfig consoleConfig *csconfig.ConsoleConfig
} }
func IsInSlice(a string, b []string) bool {
for _, v := range b {
if a == v {
return true
}
}
return false
}
func (a *apic) FetchScenariosListFromDB() ([]string, error) { func (a *apic) FetchScenariosListFromDB() ([]string, error) {
scenarios := make([]string, 0) scenarios := make([]string, 0)
machines, err := a.dbClient.ListMachines() machines, err := a.dbClient.ListMachines()
@ -67,7 +62,7 @@ func (a *apic) FetchScenariosListFromDB() ([]string, error) {
machineScenarios := strings.Split(v.Scenarios, ",") machineScenarios := strings.Split(v.Scenarios, ",")
log.Debugf("%d scenarios for machine %d", len(machineScenarios), v.ID) log.Debugf("%d scenarios for machine %d", len(machineScenarios), v.ID)
for _, sv := range machineScenarios { for _, sv := range machineScenarios {
if !IsInSlice(sv, scenarios) && sv != "" { if !types.InSlice(sv, scenarios) && sv != "" {
scenarios = append(scenarios, sv) scenarios = append(scenarios, sv)
} }
} }
@ -76,7 +71,7 @@ func (a *apic) FetchScenariosListFromDB() ([]string, error) {
return scenarios, nil return scenarios, nil
} }
func AlertToSignal(alert *models.Alert, scenarioTrust string) *models.AddSignalsRequestItem { func alertToSignal(alert *models.Alert, scenarioTrust string) *models.AddSignalsRequestItem {
return &models.AddSignalsRequestItem{ return &models.AddSignalsRequestItem{
Message: alert.Message, Message: alert.Message,
Scenario: alert.Scenario, Scenario: alert.Scenario,
@ -104,19 +99,9 @@ func NewAPIC(config *csconfig.OnlineApiClientCfg, dbClient *database.Client, con
metricsTomb: tomb.Tomb{}, metricsTomb: tomb.Tomb{},
scenarioList: make([]string, 0), scenarioList: make([]string, 0),
consoleConfig: consoleConfig, consoleConfig: consoleConfig,
} pullInterval: PullInterval,
pushInterval: PushInterval,
ret.pullInterval, err = time.ParseDuration(PullInterval) metricsInterval: MetricsInterval,
if err != nil {
return ret, err
}
ret.pushInterval, err = time.ParseDuration(PushInterval)
if err != nil {
return ret, err
}
ret.metricsInterval, err = time.ParseDuration(MetricsInterval)
if err != nil {
return ret, err
} }
password := strfmt.Password(config.Credentials.Password) password := strfmt.Password(config.Credentials.Password)
@ -140,6 +125,7 @@ func NewAPIC(config *csconfig.OnlineApiClientCfg, dbClient *database.Client, con
return ret, err return ret, err
} }
// keep track of all alerts in cache and push it to CAPI every PushInterval.
func (a *apic) Push() error { func (a *apic) Push() error {
defer types.CatchPanic("lapi/pushToAPIC") defer types.CatchPanic("lapi/pushToAPIC")
@ -170,10 +156,18 @@ func (a *apic) Push() error {
case alerts := <-a.alertToPush: case alerts := <-a.alertToPush:
var signals []*models.AddSignalsRequestItem var signals []*models.AddSignalsRequestItem
for _, alert := range alerts { for _, alert := range alerts {
if *alert.Simulated { if ok := shouldShareAlert(alert, a.consoleConfig); ok {
log.Debugf("simulation enabled for alert (id:%d), will not be sent to CAPI", alert.ID) signals = append(signals, alertToSignal(alert, getScenarioTrustOfAlert(alert)))
continue
} }
}
a.mu.Lock()
cache = append(cache, signals...)
a.mu.Unlock()
}
}
}
func getScenarioTrustOfAlert(alert *models.Alert) string {
scenarioTrust := "certified" scenarioTrust := "certified"
if alert.ScenarioHash == nil || *alert.ScenarioHash == "" { if alert.ScenarioHash == nil || *alert.ScenarioHash == "" {
scenarioTrust = "custom" scenarioTrust = "custom"
@ -185,30 +179,32 @@ func (a *apic) Push() error {
scenarioTrust = "manual" scenarioTrust = "manual"
} }
} }
switch scenarioTrust { return scenarioTrust
}
func shouldShareAlert(alert *models.Alert, consoleConfig *csconfig.ConsoleConfig) bool {
if *alert.Simulated {
log.Debugf("simulation enabled for alert (id:%d), will not be sent to CAPI", alert.ID)
return false
}
switch scenarioTrust := getScenarioTrustOfAlert(alert); scenarioTrust {
case "manual": case "manual":
if !*a.consoleConfig.ShareManualDecisions { if !*consoleConfig.ShareManualDecisions {
log.Debugf("manual decision generated an alert, doesn't send it to CAPI because options is disabled") log.Debugf("manual decision generated an alert, doesn't send it to CAPI because options is disabled")
continue return false
} }
case "tainted": case "tainted":
if !*a.consoleConfig.ShareTaintedScenarios { if !*consoleConfig.ShareTaintedScenarios {
log.Debugf("tainted scenario generated an alert, doesn't send it to CAPI because options is disabled") log.Debugf("tainted scenario generated an alert, doesn't send it to CAPI because options is disabled")
continue return false
} }
case "custom": case "custom":
if !*a.consoleConfig.ShareCustomScenarios { if !*consoleConfig.ShareCustomScenarios {
log.Debugf("custom scenario generated an alert, doesn't send it to CAPI because options is disabled") log.Debugf("custom scenario generated an alert, doesn't send it to CAPI because options is disabled")
continue return false
}
}
signals = append(signals, AlertToSignal(alert, scenarioTrust))
}
a.mu.Lock()
cache = append(cache, signals...)
a.mu.Unlock()
} }
} }
return true
} }
func (a *apic) Send(cacheOrig *models.AddSignalsRequest) { func (a *apic) Send(cacheOrig *models.AddSignalsRequest) {
@ -256,54 +252,26 @@ func (a *apic) Send(cacheOrig *models.AddSignalsRequest) {
} }
} }
var SCOPE_CAPI string = "CAPI" func (a *apic) CAPIPullIsOld() (bool, error) {
var SCOPE_CAPI_ALIAS string = "crowdsecurity/community-blocklist" //we don't use "CAPI" directly, to make it less confusing for the user
var SCOPE_LISTS string = "lists"
func (a *apic) PullTop() error {
var err error
/*only pull community blocklist if it's older than 1h30 */ /*only pull community blocklist if it's older than 1h30 */
alerts := a.dbClient.Ent.Alert.Query() alerts := a.dbClient.Ent.Alert.Query()
alerts = alerts.Where(alert.HasDecisionsWith(decision.OriginEQ(database.CapiMachineID))) alerts = alerts.Where(alert.HasDecisionsWith(decision.OriginEQ(database.CapiMachineID)))
alerts = alerts.Where(alert.CreatedAtGTE(time.Now().UTC().Add(-time.Duration(1*time.Hour + 30*time.Minute)))) alerts = alerts.Where(alert.CreatedAtGTE(time.Now().UTC().Add(-time.Duration(1*time.Hour + 30*time.Minute))))
count, err := alerts.Count(a.dbClient.CTX) count, err := alerts.Count(a.dbClient.CTX)
if err != nil { if err != nil {
return errors.Wrap(err, "while looking for CAPI alert") return false, errors.Wrap(err, "while looking for CAPI alert")
} }
if count > 0 { if count > 0 {
log.Printf("last CAPI pull is newer than 1h30, skip.") log.Printf("last CAPI pull is newer than 1h30, skip.")
return nil return false, nil
} }
data, _, err := a.apiClient.Decisions.GetStream(context.Background(), apiclient.DecisionsStreamOpts{Startup: a.startup}) return true, nil
if err != nil { }
return errors.Wrap(err, "get stream")
}
if a.startup {
a.startup = false
}
/*to count additions/deletions accross lists*/
var add_counters map[string]map[string]int
var delete_counters map[string]map[string]int
add_counters = make(map[string]map[string]int) func (a *apic) HandleDeletedDecisions(deletedDecisions []*models.Decision, delete_counters map[string]map[string]int) (int, error) {
add_counters[SCOPE_CAPI] = make(map[string]int)
add_counters[SCOPE_LISTS] = make(map[string]int)
delete_counters = make(map[string]map[string]int)
delete_counters[SCOPE_CAPI] = make(map[string]int)
delete_counters[SCOPE_LISTS] = make(map[string]int)
var filter map[string][]string var filter map[string][]string
var nbDeleted int var nbDeleted int
// process deleted decisions for _, decision := range deletedDecisions {
for _, decision := range data.Deleted {
//count individual deletions
if *decision.Origin == SCOPE_CAPI {
delete_counters[SCOPE_CAPI][*decision.Scenario]++
} else if *decision.Origin == SCOPE_LISTS {
delete_counters[SCOPE_LISTS][*decision.Scenario]++
} else {
log.Warningf("Unknown origin %s", *decision.Origin)
}
if strings.ToLower(*decision.Scope) == "ip" { if strings.ToLower(*decision.Scope) == "ip" {
filter = make(map[string][]string, 1) filter = make(map[string][]string, 1)
filter["value"] = []string{*decision.Value} filter["value"] = []string{*decision.Value}
@ -311,36 +279,30 @@ func (a *apic) PullTop() error {
filter = make(map[string][]string, 3) filter = make(map[string][]string, 3)
filter["value"] = []string{*decision.Value} filter["value"] = []string{*decision.Value}
filter["type"] = []string{*decision.Type} filter["type"] = []string{*decision.Type}
filter["value"] = []string{*decision.Scope} filter["scopes"] = []string{*decision.Scope}
} }
filter["origin"] = []string{*decision.Origin}
dbCliRet, err := a.dbClient.SoftDeleteDecisionsWithFilter(filter) dbCliRet, err := a.dbClient.SoftDeleteDecisionsWithFilter(filter)
if err != nil { if err != nil {
return errors.Wrap(err, "deleting decisions error") return 0, errors.Wrap(err, "deleting decisions error")
} }
dbCliDel, err := strconv.Atoi(dbCliRet) dbCliDel, err := strconv.Atoi(dbCliRet)
if err != nil { if err != nil {
return errors.Wrapf(err, "converting db ret %d", dbCliDel) return 0, errors.Wrapf(err, "converting db ret %d", dbCliDel)
} }
updateCounterForDecision(delete_counters, decision, dbCliDel)
nbDeleted += dbCliDel nbDeleted += dbCliDel
} }
log.Printf("capi/community-blocklist : %d explicit deletions", nbDeleted) return nbDeleted, nil
if len(data.New) == 0 { }
log.Warnf("capi/community-blocklist : received 0 new entries, CAPI failure ?")
return nil
}
//we receive only one list of decisions, that we need to break-up : func createAlertsForDecisions(decisions []*models.Decision) []*models.Alert {
// one alert for "community blocklist" newAlerts := make([]*models.Alert, 0)
// one alert per list we're subscribed to for _, decision := range decisions {
var alertsFromCapi []*models.Alert
alertsFromCapi = make([]*models.Alert, 0)
//iterate over all new decisions, and simply create corresponding alerts
for _, decision := range data.New {
found := false found := false
for _, sub := range alertsFromCapi { for _, sub := range newAlerts {
if sub.Source.Scope == nil { if sub.Source.Scope == nil {
log.Warningf("nil scope in %+v", sub) log.Warningf("nil scope in %+v", sub)
continue continue
@ -366,42 +328,44 @@ func (a *apic) PullTop() error {
} }
if !found { if !found {
log.Debugf("Create entry for origin:%s scenario:%s", *decision.Origin, *decision.Scenario) log.Debugf("Create entry for origin:%s scenario:%s", *decision.Origin, *decision.Scenario)
newAlert := models.Alert{} newAlerts = append(newAlerts, createAlertForDecision(decision))
newAlert.Message = types.StrPtr("") }
}
return newAlerts
}
func createAlertForDecision(decision *models.Decision) *models.Alert {
newAlert := &models.Alert{}
newAlert.Source = &models.Source{} newAlert.Source = &models.Source{}
newAlert.Source.Scope = types.StrPtr("")
if *decision.Origin == SCOPE_CAPI { //to make things more user friendly, we replace CAPI with community-blocklist if *decision.Origin == SCOPE_CAPI { //to make things more user friendly, we replace CAPI with community-blocklist
newAlert.Source.Scope = types.StrPtr(SCOPE_CAPI)
newAlert.Scenario = types.StrPtr(SCOPE_CAPI) newAlert.Scenario = types.StrPtr(SCOPE_CAPI)
newAlert.Source.Scope = types.StrPtr(SCOPE_CAPI)
} else if *decision.Origin == SCOPE_LISTS { } else if *decision.Origin == SCOPE_LISTS {
newAlert.Source.Scope = types.StrPtr(SCOPE_LISTS)
newAlert.Scenario = types.StrPtr(*decision.Scenario) newAlert.Scenario = types.StrPtr(*decision.Scenario)
newAlert.Source.Scope = types.StrPtr(SCOPE_LISTS)
} else { } else {
log.Warningf("unknown origin %s", *decision.Origin) log.Warningf("unknown origin %s", *decision.Origin)
} }
newAlert.Message = types.StrPtr("")
newAlert.Source.Value = types.StrPtr("") newAlert.Source.Value = types.StrPtr("")
newAlert.StartAt = types.StrPtr(time.Now().UTC().Format(time.RFC3339)) newAlert.StartAt = types.StrPtr(time.Now().UTC().Format(time.RFC3339))
newAlert.StopAt = types.StrPtr(time.Now().UTC().Format(time.RFC3339)) newAlert.StopAt = types.StrPtr(time.Now().UTC().Format(time.RFC3339))
newAlert.Capacity = types.Int32Ptr(0) newAlert.Capacity = types.Int32Ptr(0)
newAlert.Simulated = types.BoolPtr(false) newAlert.Simulated = types.BoolPtr(false)
newAlert.EventsCount = types.Int32Ptr(int32(len(data.New))) newAlert.EventsCount = types.Int32Ptr(0)
newAlert.Leakspeed = types.StrPtr("") newAlert.Leakspeed = types.StrPtr("")
newAlert.ScenarioHash = types.StrPtr("") newAlert.ScenarioHash = types.StrPtr("")
newAlert.ScenarioVersion = types.StrPtr("") newAlert.ScenarioVersion = types.StrPtr("")
newAlert.MachineID = database.CapiMachineID newAlert.MachineID = database.CapiMachineID
alertsFromCapi = append(alertsFromCapi, &newAlert) return newAlert
} }
}
//iterate a second time and fill the alerts with the new decisions // This function takes in list of parent alerts and decisions and then pairs them up.
for _, decision := range data.New { func fillAlertsWithDecisions(alerts []*models.Alert, decisions []*models.Decision, add_counters map[string]map[string]int) []*models.Alert {
for _, decision := range decisions {
//count and create separate alerts for each list //count and create separate alerts for each list
if *decision.Origin == SCOPE_CAPI { updateCounterForDecision(add_counters, decision, 1)
add_counters[SCOPE_CAPI]["all"]++
} else if *decision.Origin == SCOPE_LISTS {
add_counters[SCOPE_LISTS][*decision.Scenario]++
} else {
log.Warningf("Unknown origin %s", *decision.Origin)
}
/*CAPI might send lower case scopes, unify it.*/ /*CAPI might send lower case scopes, unify it.*/
switch strings.ToLower(*decision.Scope) { switch strings.ToLower(*decision.Scope) {
@ -412,16 +376,16 @@ func (a *apic) PullTop() error {
} }
found := false found := false
//add the individual decisions to the right list //add the individual decisions to the right list
for idx, alert := range alertsFromCapi { for idx, alert := range alerts {
if *decision.Origin == SCOPE_CAPI { if *decision.Origin == SCOPE_CAPI {
if *alert.Source.Scope == SCOPE_CAPI { if *alert.Source.Scope == SCOPE_CAPI {
alertsFromCapi[idx].Decisions = append(alertsFromCapi[idx].Decisions, decision) alerts[idx].Decisions = append(alerts[idx].Decisions, decision)
found = true found = true
break break
} }
} else if *decision.Origin == SCOPE_LISTS { } else if *decision.Origin == SCOPE_LISTS {
if *alert.Source.Scope == SCOPE_LISTS && *alert.Scenario == *decision.Scenario { if *alert.Source.Scope == SCOPE_LISTS && *alert.Scenario == *decision.Scenario {
alertsFromCapi[idx].Decisions = append(alertsFromCapi[idx].Decisions, decision) alerts[idx].Decisions = append(alerts[idx].Decisions, decision)
found = true found = true
break break
} }
@ -433,18 +397,49 @@ func (a *apic) PullTop() error {
log.Warningf("Orphaned decision for %s - %s", *decision.Origin, *decision.Scenario) log.Warningf("Orphaned decision for %s - %s", *decision.Origin, *decision.Scenario)
} }
} }
return alerts
}
//we receive only one list of decisions, that we need to break-up :
// one alert for "community blocklist"
// one alert per list we're subscribed to
func (a *apic) PullTop() error {
var err error
if lastPullIsOld, err := a.CAPIPullIsOld(); err != nil {
return err
} else if !lastPullIsOld {
return nil
}
data, _, err := a.apiClient.Decisions.GetStream(context.Background(), apiclient.DecisionsStreamOpts{Startup: a.startup})
if err != nil {
return errors.Wrap(err, "get stream")
}
a.startup = false
/*to count additions/deletions accross lists*/
add_counters, delete_counters := makeAddAndDeleteCounters()
// process deleted decisions
if nbDeleted, err := a.HandleDeletedDecisions(data.Deleted, delete_counters); err != nil {
return err
} else {
log.Printf("capi/community-blocklist : %d explicit deletions", nbDeleted)
}
if len(data.New) == 0 {
log.Warnf("capi/community-blocklist : received 0 new entries, CAPI failure ?")
return nil
}
//we receive only one list of decisions, that we need to break-up :
// one alert for "community blocklist"
// one alert per list we're subscribed to
alertsFromCapi := createAlertsForDecisions(data.New)
alertsFromCapi = fillAlertsWithDecisions(alertsFromCapi, data.New, add_counters)
for idx, alert := range alertsFromCapi { for idx, alert := range alertsFromCapi {
formatted_update := "" alertsFromCapi[idx] = setAlertScenario(add_counters, delete_counters, alert)
if *alertsFromCapi[idx].Source.Scope == SCOPE_CAPI {
*alertsFromCapi[idx].Source.Scope = SCOPE_CAPI_ALIAS
formatted_update = fmt.Sprintf("update : +%d/-%d IPs", add_counters[SCOPE_CAPI]["all"], delete_counters[SCOPE_CAPI]["all"])
} else if *alertsFromCapi[idx].Source.Scope == SCOPE_LISTS {
*alertsFromCapi[idx].Source.Scope = fmt.Sprintf("%s:%s", SCOPE_LISTS, *alertsFromCapi[idx].Scenario)
formatted_update = fmt.Sprintf("update : +%d/-%d IPs", add_counters[SCOPE_LISTS][*alert.Scenario], delete_counters[SCOPE_LISTS][*alert.Scenario])
}
alertsFromCapi[idx].Scenario = types.StrPtr(formatted_update)
log.Debugf("%s has %d decisions", *alertsFromCapi[idx].Source.Scope, len(alertsFromCapi[idx].Decisions)) log.Debugf("%s has %d decisions", *alertsFromCapi[idx].Source.Scope, len(alertsFromCapi[idx].Decisions))
alertID, inserted, deleted, err := a.dbClient.UpdateCommunityBlocklist(alertsFromCapi[idx]) alertID, inserted, deleted, err := a.dbClient.UpdateCommunityBlocklist(alertsFromCapi[idx])
if err != nil { if err != nil {
@ -455,14 +450,27 @@ func (a *apic) PullTop() error {
return nil return nil
} }
func setAlertScenario(add_counters map[string]map[string]int, delete_counters map[string]map[string]int, alert *models.Alert) *models.Alert {
if *alert.Source.Scope == SCOPE_CAPI {
*alert.Source.Scope = SCOPE_CAPI_ALIAS
alert.Scenario = types.StrPtr(fmt.Sprintf("update : +%d/-%d IPs", add_counters[SCOPE_CAPI]["all"], delete_counters[SCOPE_CAPI]["all"]))
} else if *alert.Source.Scope == SCOPE_LISTS {
*alert.Source.Scope = fmt.Sprintf("%s:%s", SCOPE_LISTS, *alert.Scenario)
alert.Scenario = types.StrPtr(fmt.Sprintf("update : +%d/-%d IPs", add_counters[SCOPE_LISTS][*alert.Scenario], delete_counters[SCOPE_LISTS][*alert.Scenario]))
}
return alert
}
func (a *apic) Pull() error { func (a *apic) Pull() error {
defer types.CatchPanic("lapi/pullFromAPIC") defer types.CatchPanic("lapi/pullFromAPIC")
log.Infof("start crowdsec api pull (interval: %s)", PullInterval) log.Infof("start crowdsec api pull (interval: %s)", PullInterval)
var err error
scenario := a.scenarioList
toldOnce := false toldOnce := false
for { for {
scenario, err := a.FetchScenariosListFromDB()
if err != nil {
log.Errorf("unable to fetch scenarios from db: %s", err)
}
if len(scenario) > 0 { if len(scenario) > 0 {
break break
} }
@ -471,10 +479,6 @@ func (a *apic) Pull() error {
toldOnce = true toldOnce = true
} }
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
scenario, err = a.FetchScenariosListFromDB()
if err != nil {
log.Errorf("unable to fetch scenarios from db: %s", err)
}
} }
if err := a.PullTop(); err != nil { if err := a.PullTop(); err != nil {
log.Errorf("capi pull top: %s", err) log.Errorf("capi pull top: %s", err)
@ -496,9 +500,8 @@ func (a *apic) Pull() error {
} }
func (a *apic) GetMetrics() (*models.Metrics, error) { func (a *apic) GetMetrics() (*models.Metrics, error) {
version := cwversion.VersionStr()
metric := &models.Metrics{ metric := &models.Metrics{
ApilVersion: &version, ApilVersion: types.StrPtr(cwversion.VersionStr()),
Machines: make([]*models.MetricsAgentInfo, 0), Machines: make([]*models.MetricsAgentInfo, 0),
Bouncers: make([]*models.MetricsBouncerInfo, 0), Bouncers: make([]*models.MetricsBouncerInfo, 0),
} }
@ -578,3 +581,25 @@ func (a *apic) Shutdown() {
a.pullTomb.Kill(nil) a.pullTomb.Kill(nil)
a.metricsTomb.Kill(nil) a.metricsTomb.Kill(nil)
} }
func makeAddAndDeleteCounters() (map[string]map[string]int, map[string]map[string]int) {
add_counters := make(map[string]map[string]int)
add_counters[SCOPE_CAPI] = make(map[string]int)
add_counters[SCOPE_LISTS] = make(map[string]int)
delete_counters := make(map[string]map[string]int)
delete_counters[SCOPE_CAPI] = make(map[string]int)
delete_counters[SCOPE_LISTS] = make(map[string]int)
return add_counters, delete_counters
}
func updateCounterForDecision(counter map[string]map[string]int, decision *models.Decision, totalDecisions int) {
if *decision.Origin == SCOPE_CAPI {
counter[*decision.Origin]["all"] += totalDecisions
return
} else if *decision.Origin == SCOPE_LISTS {
counter[*decision.Origin][*decision.Scenario] += totalDecisions
}
log.Warningf("Unknown origin %s", *decision.Origin)
}

956
pkg/apiserver/apic_test.go Normal file
View file

@ -0,0 +1,956 @@
package apiserver
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"reflect"
"sort"
"sync"
"testing"
"time"
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/database/ent/decision"
"github.com/crowdsecurity/crowdsec/pkg/database/ent/machine"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/jarcoal/httpmock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"gopkg.in/tomb.v2"
)
func getDBClient(t *testing.T) *database.Client {
t.Helper()
dbPath, err := os.CreateTemp("", "*sqlite")
if err != nil {
t.Fatal(err)
}
dbClient, err := database.NewClient(&csconfig.DatabaseCfg{
Type: "sqlite",
DbName: "crowdsec",
DbPath: dbPath.Name(),
})
if err != nil {
t.Fatal(err)
}
return dbClient
}
func getAPIC(t *testing.T) *apic {
t.Helper()
dbClient := getDBClient(t)
return &apic{
alertToPush: make(chan []*models.Alert),
dbClient: dbClient,
mu: sync.Mutex{},
startup: true,
pullTomb: tomb.Tomb{},
pushTomb: tomb.Tomb{},
metricsTomb: tomb.Tomb{},
scenarioList: make([]string, 0),
consoleConfig: &csconfig.ConsoleConfig{
ShareManualDecisions: types.BoolPtr(false),
ShareTaintedScenarios: types.BoolPtr(false),
ShareCustomScenarios: types.BoolPtr(false),
},
}
}
func absDiff(a int, b int) (c int) {
if c = a - b; c < 0 {
return -1 * c
}
return c
}
func assertTotalDecisionCount(t *testing.T, dbClient *database.Client, count int) {
d := dbClient.Ent.Decision.Query().AllX(context.Background())
assert.Len(t, d, count)
}
func assertTotalValidDecisionCount(t *testing.T, dbClient *database.Client, count int) {
d := dbClient.Ent.Decision.Query().Where(
decision.UntilGT(time.Now()),
).AllX(context.Background())
assert.Len(t, d, count)
}
func jsonMarshalX(v interface{}) []byte {
data, err := json.Marshal(v)
if err != nil {
panic(err)
}
return data
}
func assertTotalAlertCount(t *testing.T, dbClient *database.Client, count int) {
d := dbClient.Ent.Alert.Query().AllX(context.Background())
assert.Len(t, d, count)
}
func TestAPICCAPIPullIsOld(t *testing.T) {
api := getAPIC(t)
isOld, err := api.CAPIPullIsOld()
if err != nil {
t.Fatal(err)
}
assert.True(t, isOld)
decision := api.dbClient.Ent.Decision.Create().
SetUntil(time.Now().Add(time.Hour)).
SetScenario("crowdsec/test").
SetType("IP").
SetScope("Country").
SetValue("Blah").
SetOrigin(SCOPE_CAPI).
SaveX(context.Background())
api.dbClient.Ent.Alert.Create().
SetCreatedAt(time.Now()).
SetScenario("crowdsec/test").
AddDecisions(
decision,
).
SaveX(context.Background())
isOld, err = api.CAPIPullIsOld()
if err != nil {
t.Fatal(err)
}
assert.False(t, isOld)
}
func TestAPICFetchScenariosListFromDB(t *testing.T) {
api := getAPIC(t)
testCases := []struct {
name string
machineIDsWithScenarios map[string]string
expectedScenarios []string
}{
{
name: "Simple one machine with two scenarios",
machineIDsWithScenarios: map[string]string{
"a": "crowdsecurity/http-bf,crowdsecurity/ssh-bf",
},
expectedScenarios: []string{"crowdsecurity/ssh-bf", "crowdsecurity/http-bf"},
},
{
name: "Multi machine with custom+hub scenarios",
machineIDsWithScenarios: map[string]string{
"a": "crowdsecurity/http-bf,crowdsecurity/ssh-bf,my_scenario",
"b": "crowdsecurity/http-bf,crowdsecurity/ssh-bf,foo_scenario",
},
expectedScenarios: []string{"crowdsecurity/ssh-bf", "crowdsecurity/http-bf", "my_scenario", "foo_scenario"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for machineID, scenarios := range tc.machineIDsWithScenarios {
api.dbClient.Ent.Machine.Create().
SetMachineId(machineID).
SetPassword(testPassword.String()).
SetIpAddress("1.2.3.4").
SetScenarios(scenarios).
ExecX(context.Background())
}
scenarios, err := api.FetchScenariosListFromDB()
for machineID := range tc.machineIDsWithScenarios {
api.dbClient.Ent.Machine.Delete().Where(machine.MachineIdEQ(machineID)).ExecX(context.Background())
}
if err != nil {
t.Fatal(err)
} else {
sort.Strings(scenarios)
sort.Strings(tc.expectedScenarios)
assert.Equal(t, scenarios, tc.expectedScenarios)
}
})
}
}
func TestNewAPIC(t *testing.T) {
var testConfig *csconfig.OnlineApiClientCfg
setConfig := func() {
testConfig = &csconfig.OnlineApiClientCfg{
Credentials: &csconfig.ApiCredentialsCfg{
URL: "foobar",
Login: "foo",
Password: "bar",
},
}
}
type args struct {
dbClient *database.Client
consoleConfig *csconfig.ConsoleConfig
}
tests := []struct {
name string
args args
wantErr bool
errorContains string
action func()
}{
{
name: "simple",
action: func() {},
args: args{
dbClient: getDBClient(t),
consoleConfig: LoadTestConfig().API.Server.ConsoleConfig,
},
},
{
name: "error in parsing URL",
action: func() { testConfig.Credentials.URL = "foobar http://" },
args: args{
dbClient: getDBClient(t),
consoleConfig: LoadTestConfig().API.Server.ConsoleConfig,
},
wantErr: true,
errorContains: "first path segment in URL cannot contain colon",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setConfig()
tt.action()
_, err := NewAPIC(testConfig, tt.args.dbClient, tt.args.consoleConfig)
if tt.wantErr {
assert.ErrorContains(t, err, tt.errorContains)
} else {
assert.NoError(t, err)
}
})
}
}
func TestAPICHandleDeletedDecisions(t *testing.T) {
api := getAPIC(t)
_, deleteCounters := makeAddAndDeleteCounters()
decision1 := api.dbClient.Ent.Decision.Create().
SetUntil(time.Now().Add(time.Hour)).
SetScenario("crowdsec/test").
SetType("ban").
SetScope("IP").
SetValue("1.2.3.4").
SetOrigin(SCOPE_CAPI).
SaveX(context.Background())
api.dbClient.Ent.Decision.Create().
SetUntil(time.Now().Add(time.Hour)).
SetScenario("crowdsec/test").
SetType("ban").
SetScope("IP").
SetValue("1.2.3.4").
SetOrigin(SCOPE_CAPI).
SaveX(context.Background())
assertTotalDecisionCount(t, api.dbClient, 2)
nbDeleted, err := api.HandleDeletedDecisions([]*models.Decision{{
Value: types.StrPtr("1.2.3.4"),
Origin: &SCOPE_CAPI,
Type: &decision1.Type,
Scenario: types.StrPtr("crowdsec/test"),
Scope: types.StrPtr("IP"),
}}, deleteCounters)
assert.NoError(t, err)
assert.Equal(t, nbDeleted, 2)
assert.Equal(t, deleteCounters[SCOPE_CAPI]["all"], 2)
}
func TestAPICGetMetrics(t *testing.T) {
api := getAPIC(t)
cleanUp := func() {
api.dbClient.Ent.Bouncer.Delete().ExecX(context.Background())
api.dbClient.Ent.Machine.Delete().ExecX(context.Background())
}
testCases := []struct {
name string
machineIDs []string
bouncers []string
expectedMetric *models.Metrics
}{
{
name: "simple",
machineIDs: []string{"a", "b", "c"},
bouncers: []string{"1", "2", "3"},
expectedMetric: &models.Metrics{
ApilVersion: types.StrPtr(cwversion.VersionStr()),
Bouncers: []*models.MetricsBouncerInfo{
{
CustomName: "1",
LastPull: time.Time{}.String(),
}, {
CustomName: "2",
LastPull: time.Time{}.String(),
}, {
CustomName: "3",
LastPull: time.Time{}.String(),
},
},
Machines: []*models.MetricsAgentInfo{
{
Name: "a",
LastPush: time.Time{}.String(),
LastUpdate: time.Time{}.String(),
},
{
Name: "b",
LastPush: time.Time{}.String(),
LastUpdate: time.Time{}.String(),
},
{
Name: "c",
LastPush: time.Time{}.String(),
LastUpdate: time.Time{}.String(),
},
},
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
cleanUp()
for i, machineID := range testCase.machineIDs {
api.dbClient.Ent.Machine.Create().
SetMachineId(machineID).
SetPassword(testPassword.String()).
SetIpAddress(fmt.Sprintf("1.2.3.%d", i)).
SetScenarios("crowdsecurity/test").
SetLastPush(time.Time{}).
SetUpdatedAt(time.Time{}).
ExecX(context.Background())
}
for i, bouncerName := range testCase.bouncers {
api.dbClient.Ent.Bouncer.Create().
SetIPAddress(fmt.Sprintf("1.2.3.%d", i)).
SetName(bouncerName).
SetAPIKey("foobar").
SetRevoked(false).
SetLastPull(time.Time{}).
ExecX(context.Background())
}
if foundMetrics, err := api.GetMetrics(); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, foundMetrics.Bouncers, testCase.expectedMetric.Bouncers)
assert.Equal(t, foundMetrics.Machines, testCase.expectedMetric.Machines)
}
})
}
}
func TestCreateAlertsForDecision(t *testing.T) {
httpBfDecisionList := &models.Decision{
Origin: &SCOPE_LISTS,
Scenario: types.StrPtr("crowdsecurity/http-bf"),
}
sshBfDecisionList := &models.Decision{
Origin: &SCOPE_LISTS,
Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
}
httpBfDecisionCommunity := &models.Decision{
Origin: &SCOPE_CAPI,
Scenario: types.StrPtr("crowdsecurity/http-bf"),
}
sshBfDecisionCommunity := &models.Decision{
Origin: &SCOPE_CAPI,
Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
}
type args struct {
decisions []*models.Decision
}
tests := []struct {
name string
args args
want []*models.Alert
}{
{
name: "2 decisions CAPI List Decisions should create 2 alerts",
args: args{
decisions: []*models.Decision{
httpBfDecisionList,
sshBfDecisionList,
},
},
want: []*models.Alert{
createAlertForDecision(httpBfDecisionList),
createAlertForDecision(sshBfDecisionList),
},
},
{
name: "2 decisions CAPI List same scenario decisions should create 1 alert",
args: args{
decisions: []*models.Decision{
httpBfDecisionList,
httpBfDecisionList,
},
},
want: []*models.Alert{
createAlertForDecision(httpBfDecisionList),
},
},
{
name: "5 decisions from community list should create 1 alert",
args: args{
decisions: []*models.Decision{
httpBfDecisionCommunity,
httpBfDecisionCommunity,
sshBfDecisionCommunity,
sshBfDecisionCommunity,
sshBfDecisionCommunity,
},
},
want: []*models.Alert{
createAlertForDecision(sshBfDecisionCommunity),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := createAlertsForDecisions(tt.args.decisions); !reflect.DeepEqual(got, tt.want) {
t.Errorf("createAlertsForDecisions() = %v, want %v", got, tt.want)
}
})
}
}
func TestFillAlertsWithDecisions(t *testing.T) {
httpBfDecisionCommunity := &models.Decision{
Origin: &SCOPE_CAPI,
Scenario: types.StrPtr("crowdsecurity/http-bf"),
Scope: types.StrPtr("ip"),
}
sshBfDecisionCommunity := &models.Decision{
Origin: &SCOPE_CAPI,
Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
Scope: types.StrPtr("ip"),
}
httpBfDecisionList := &models.Decision{
Origin: &SCOPE_LISTS,
Scenario: types.StrPtr("crowdsecurity/http-bf"),
Scope: types.StrPtr("ip"),
}
sshBfDecisionList := &models.Decision{
Origin: &SCOPE_LISTS,
Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
Scope: types.StrPtr("ip"),
}
type args struct {
alerts []*models.Alert
decisions []*models.Decision
}
tests := []struct {
name string
args args
want []*models.Alert
}{
{
name: "1 CAPI alert should pair up with n CAPI decisions",
args: args{
alerts: []*models.Alert{createAlertForDecision(httpBfDecisionCommunity)},
decisions: []*models.Decision{httpBfDecisionCommunity, sshBfDecisionCommunity, sshBfDecisionCommunity, httpBfDecisionCommunity},
},
want: []*models.Alert{
func() *models.Alert {
a := createAlertForDecision(httpBfDecisionCommunity)
a.Decisions = []*models.Decision{httpBfDecisionCommunity, sshBfDecisionCommunity, sshBfDecisionCommunity, httpBfDecisionCommunity}
return a
}(),
},
},
{
name: "List alert should pair up only with decisions having same scenario",
args: args{
alerts: []*models.Alert{createAlertForDecision(httpBfDecisionList), createAlertForDecision(sshBfDecisionList)},
decisions: []*models.Decision{httpBfDecisionList, httpBfDecisionList, sshBfDecisionList, sshBfDecisionList},
},
want: []*models.Alert{
func() *models.Alert {
a := createAlertForDecision(httpBfDecisionList)
a.Decisions = []*models.Decision{httpBfDecisionList, httpBfDecisionList}
return a
}(),
func() *models.Alert {
a := createAlertForDecision(sshBfDecisionList)
a.Decisions = []*models.Decision{sshBfDecisionList, sshBfDecisionList}
return a
}(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
add_counters, _ := makeAddAndDeleteCounters()
if got := fillAlertsWithDecisions(tt.args.alerts, tt.args.decisions, add_counters); !reflect.DeepEqual(got, tt.want) {
t.Errorf("fillAlertsWithDecisions() = %v, want %v", got, tt.want)
}
})
}
}
func TestAPICPullTop(t *testing.T) {
api := getAPIC(t)
api.dbClient.Ent.Decision.Create().
SetOrigin(SCOPE_LISTS).
SetType("ban").
SetValue("9.9.9.9").
SetScope("Ip").
SetScenario("crowdsecurity/ssh-bf").
SetUntil(time.Now().Add(time.Hour)).
ExecX(context.Background())
assertTotalDecisionCount(t, api.dbClient, 1)
assertTotalValidDecisionCount(t, api.dbClient, 1)
httpmock.Activate()
defer httpmock.DeactivateAndReset()
httpmock.RegisterResponder("GET", "http://api.crowdsec.net/api/decisions/stream", httpmock.NewBytesResponder(
200, jsonMarshalX(
models.DecisionsStreamResponse{
Deleted: models.GetDecisionsResponse{
&models.Decision{
Origin: &SCOPE_LISTS,
Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
Value: types.StrPtr("9.9.9.9"),
Scope: types.StrPtr("Ip"),
Duration: types.StrPtr("24h"),
Type: types.StrPtr("ban"),
}, // Thie is already present in DB
&models.Decision{
Origin: &SCOPE_LISTS,
Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
Value: types.StrPtr("9.1.9.9"),
Scope: types.StrPtr("Ip"),
Duration: types.StrPtr("24h"),
Type: types.StrPtr("ban"),
}, // This not present in DB.
},
New: models.GetDecisionsResponse{
&models.Decision{
Origin: &SCOPE_CAPI,
Scenario: types.StrPtr("crowdsecurity/test1"),
Value: types.StrPtr("1.2.3.4"),
Scope: types.StrPtr("Ip"),
Duration: types.StrPtr("24h"),
Type: types.StrPtr("ban"),
},
&models.Decision{
Origin: &SCOPE_CAPI,
Scenario: types.StrPtr("crowdsecurity/test2"),
Value: types.StrPtr("1.2.3.5"),
Scope: types.StrPtr("Ip"),
Duration: types.StrPtr("24h"),
Type: types.StrPtr("ban"),
}, // These two are from community list.
&models.Decision{
Origin: &SCOPE_LISTS,
Scenario: types.StrPtr("crowdsecurity/http-bf"),
Value: types.StrPtr("1.2.3.6"),
Scope: types.StrPtr("Ip"),
Duration: types.StrPtr("24h"),
Type: types.StrPtr("ban"),
},
&models.Decision{
Origin: &SCOPE_LISTS,
Scenario: types.StrPtr("crowdsecurity/ssh-bf"),
Value: types.StrPtr("1.2.3.7"),
Scope: types.StrPtr("Ip"),
Duration: types.StrPtr("24h"),
Type: types.StrPtr("ban"),
}, // These two are from list subscription.
},
},
),
))
url, err := url.ParseRequestURI("http://api.crowdsec.net/")
if err != nil {
t.Fatal(err)
}
apic, err := apiclient.NewDefaultClient(
url,
"/api",
fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
nil,
)
if err != nil {
t.Fatal(err)
}
api.apiClient = apic
err = api.PullTop()
if err != nil {
t.Fatal(err)
}
assertTotalDecisionCount(t, api.dbClient, 5)
assertTotalValidDecisionCount(t, api.dbClient, 4)
assertTotalAlertCount(t, api.dbClient, 3) // 2 for list sub , 1 for community list.
alerts := api.dbClient.Ent.Alert.Query().AllX(context.Background())
validDecisions := api.dbClient.Ent.Decision.Query().Where(
decision.UntilGT(time.Now())).
AllX(context.Background())
decisionScenarioFreq := make(map[string]int)
alertScenario := make(map[string]int)
for _, alert := range alerts {
alertScenario[alert.SourceScope]++
}
assert.Equal(t, len(alertScenario), 3)
assert.Equal(t, alertScenario[SCOPE_CAPI_ALIAS], 1)
assert.Equal(t, alertScenario["lists:crowdsecurity/ssh-bf"], 1)
assert.Equal(t, alertScenario["lists:crowdsecurity/http-bf"], 1)
for _, decisions := range validDecisions {
decisionScenarioFreq[decisions.Scenario]++
}
assert.Equal(t, decisionScenarioFreq["crowdsecurity/http-bf"], 1)
assert.Equal(t, decisionScenarioFreq["crowdsecurity/ssh-bf"], 1)
assert.Equal(t, decisionScenarioFreq["crowdsecurity/test1"], 1)
assert.Equal(t, decisionScenarioFreq["crowdsecurity/test2"], 1)
}
func TestAPICPush(t *testing.T) {
testCases := []struct {
name string
alerts []*models.Alert
expectedCalls int
}{
{
name: "simple single alert",
alerts: []*models.Alert{
{
Scenario: types.StrPtr("crowdsec/test"),
ScenarioHash: types.StrPtr("certified"),
ScenarioVersion: types.StrPtr("v1.0"),
Simulated: types.BoolPtr(false),
},
},
expectedCalls: 1,
},
{
name: "simulated alert is not pushed",
alerts: []*models.Alert{
{
Scenario: types.StrPtr("crowdsec/test"),
ScenarioHash: types.StrPtr("certified"),
ScenarioVersion: types.StrPtr("v1.0"),
Simulated: types.BoolPtr(true),
},
},
expectedCalls: 0,
},
{
name: "1 request per 50 alerts",
expectedCalls: 2,
alerts: func() []*models.Alert {
alerts := make([]*models.Alert, 100)
for i := 0; i < 100; i++ {
alerts[i] = &models.Alert{
Scenario: types.StrPtr("crowdsec/test"),
ScenarioHash: types.StrPtr("certified"),
ScenarioVersion: types.StrPtr("v1.0"),
Simulated: types.BoolPtr(false),
}
}
return alerts
}(),
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
api := getAPIC(t)
api.pushInterval = time.Millisecond
url, err := url.ParseRequestURI("http://api.crowdsec.net/")
if err != nil {
t.Fatal(err)
}
httpmock.Activate()
defer httpmock.DeactivateAndReset()
apic, err := apiclient.NewDefaultClient(
url,
"/api",
fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
nil,
)
if err != nil {
t.Fatal(err)
}
api.apiClient = apic
httpmock.RegisterResponder("POST", "http://api.crowdsec.net/api/signals", httpmock.NewBytesResponder(200, []byte{}))
go func() {
api.alertToPush <- testCase.alerts
time.Sleep(time.Second)
api.Shutdown()
}()
if err := api.Push(); err != nil {
t.Fatal(err)
}
assert.Equal(t, httpmock.GetTotalCallCount(), testCase.expectedCalls)
})
}
}
func TestAPICSendMetrics(t *testing.T) {
api := getAPIC(t)
testCases := []struct {
name string
duration time.Duration
expectedCalls int
setUp func()
metricsInterval time.Duration
}{
{
name: "basic",
duration: time.Millisecond * 5,
metricsInterval: time.Millisecond,
expectedCalls: 5,
setUp: func() {},
},
{
name: "with some metrics",
duration: time.Millisecond * 5,
metricsInterval: time.Millisecond,
expectedCalls: 5,
setUp: func() {
api.dbClient.Ent.Machine.Create().
SetMachineId("1234").
SetPassword(testPassword.String()).
SetIpAddress("1.2.3.4").
SetScenarios("crowdsecurity/test").
SetLastPush(time.Time{}).
SetUpdatedAt(time.Time{}).
ExecX(context.Background())
api.dbClient.Ent.Bouncer.Create().
SetIPAddress("1.2.3.6").
SetName("someBouncer").
SetAPIKey("foobar").
SetRevoked(false).
SetLastPull(time.Time{}).
ExecX(context.Background())
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
api = getAPIC(t)
api.pushInterval = time.Millisecond
url, err := url.ParseRequestURI("http://api.crowdsec.net/")
if err != nil {
t.Fatal(err)
}
httpmock.Activate()
defer httpmock.DeactivateAndReset()
apic, err := apiclient.NewDefaultClient(
url,
"/api",
fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
nil,
)
if err != nil {
t.Fatal(err)
}
api.apiClient = apic
api.metricsInterval = testCase.metricsInterval
httpmock.RegisterNoResponder(httpmock.NewBytesResponder(200, []byte{}))
testCase.setUp()
go func() {
if err := api.SendMetrics(); err != nil {
panic(err)
}
}()
time.Sleep(testCase.duration)
assert.LessOrEqual(t, absDiff(testCase.expectedCalls, httpmock.GetTotalCallCount()), 2)
})
}
}
func TestAPICPull(t *testing.T) {
api := getAPIC(t)
testCases := []struct {
name string
setUp func()
expectedDecisionCount int
logContains string
}{
{
name: "test pull if no scenarios are present",
setUp: func() {},
logContains: "scenario list is empty, will not pull yet",
},
{
name: "test pull",
setUp: func() {
api.dbClient.Ent.Machine.Create().
SetMachineId("1.2.3.4").
SetPassword(testPassword.String()).
SetIpAddress("1.2.3.4").
SetScenarios("crowdsecurity/ssh-bf").
ExecX(context.Background())
},
expectedDecisionCount: 1,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
api = getAPIC(t)
api.pullInterval = time.Millisecond
url, err := url.ParseRequestURI("http://api.crowdsec.net/")
if err != nil {
t.Fatal(err)
}
httpmock.Activate()
defer httpmock.DeactivateAndReset()
apic, err := apiclient.NewDefaultClient(
url,
"/api",
fmt.Sprintf("crowdsec/%s", cwversion.VersionStr()),
nil,
)
if err != nil {
t.Fatal(err)
}
api.apiClient = apic
httpmock.RegisterNoResponder(httpmock.NewBytesResponder(200, jsonMarshalX(
models.DecisionsStreamResponse{
New: models.GetDecisionsResponse{
&models.Decision{
Origin: &SCOPE_CAPI,
Scenario: types.StrPtr("crowdsecurity/test2"),
Value: types.StrPtr("1.2.3.5"),
Scope: types.StrPtr("Ip"),
Duration: types.StrPtr("24h"),
Type: types.StrPtr("ban"),
},
},
},
)))
testCase.setUp()
var buf bytes.Buffer
go func() {
logrus.SetOutput(&buf)
if err := api.Pull(); err != nil {
panic(err)
}
}()
time.Sleep(time.Millisecond * 10)
logrus.SetOutput(os.Stderr)
assert.Contains(t, buf.String(), testCase.logContains)
assertTotalDecisionCount(t, api.dbClient, testCase.expectedDecisionCount)
})
}
}
func TestShouldShareAlert(t *testing.T) {
testCases := []struct {
name string
consoleConfig *csconfig.ConsoleConfig
alert *models.Alert
expectedRet bool
expectedTrust string
}{
{
name: "custom alert should be shared if config enables it",
consoleConfig: &csconfig.ConsoleConfig{
ShareCustomScenarios: types.BoolPtr(true),
},
alert: &models.Alert{Simulated: types.BoolPtr(false)},
expectedRet: true,
expectedTrust: "custom",
},
{
name: "custom alert should not be shared if config disables it",
consoleConfig: &csconfig.ConsoleConfig{
ShareCustomScenarios: types.BoolPtr(false),
},
alert: &models.Alert{Simulated: types.BoolPtr(false)},
expectedRet: false,
expectedTrust: "custom",
},
{
name: "manual alert should be shared if config enables it",
consoleConfig: &csconfig.ConsoleConfig{
ShareManualDecisions: types.BoolPtr(true),
},
alert: &models.Alert{
Simulated: types.BoolPtr(false),
Decisions: []*models.Decision{{Origin: types.StrPtr("cscli")}},
},
expectedRet: true,
expectedTrust: "manual",
},
{
name: "manaul alert should not be shared if config disables it",
consoleConfig: &csconfig.ConsoleConfig{
ShareManualDecisions: types.BoolPtr(false),
},
alert: &models.Alert{
Simulated: types.BoolPtr(false),
Decisions: []*models.Decision{{Origin: types.StrPtr("cscli")}},
},
expectedRet: false,
expectedTrust: "manual",
},
{
name: "manual alert should be shared if config enables it",
consoleConfig: &csconfig.ConsoleConfig{
ShareTaintedScenarios: types.BoolPtr(true),
},
alert: &models.Alert{
Simulated: types.BoolPtr(false),
ScenarioHash: types.StrPtr("whateverHash"),
},
expectedRet: true,
expectedTrust: "tainted",
},
{
name: "manaul alert should not be shared if config disables it",
consoleConfig: &csconfig.ConsoleConfig{
ShareTaintedScenarios: types.BoolPtr(false),
},
alert: &models.Alert{
Simulated: types.BoolPtr(false),
ScenarioHash: types.StrPtr("whateverHash"),
},
expectedRet: false,
expectedTrust: "tainted",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
ret := shouldShareAlert(testCase.alert, testCase.consoleConfig)
assert.Equal(t, ret, testCase.expectedRet)
})
}
}

View file

@ -3,7 +3,6 @@ package apiserver
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@ -16,6 +15,7 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/models" "github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types" "github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/pkg/errors"
"github.com/crowdsecurity/crowdsec/pkg/csconfig" "github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database" "github.com/crowdsecurity/crowdsec/pkg/database"
@ -33,6 +33,7 @@ var MachineTest = models.WatcherAuthRequest{
} }
var UserAgent = fmt.Sprintf("crowdsec-test/%s", cwversion.Version) var UserAgent = fmt.Sprintf("crowdsec-test/%s", cwversion.Version)
var emptyBody = strings.NewReader("")
func LoadTestConfig() csconfig.Config { func LoadTestConfig() csconfig.Config {
config := csconfig.Config{} config := csconfig.Config{}
@ -177,6 +178,79 @@ func GetMachineIP(machineID string) (string, error) {
return "", nil return "", nil
} }
func GetAlertReaderFromFile(path string) *strings.Reader {
alertContentBytes, err := os.ReadFile(path)
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts {
*alert.StartAt = time.Now().UTC().Format(time.RFC3339)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339)
}
alertContent, err := json.Marshal(alerts)
if err != nil {
log.Fatal(err)
}
return strings.NewReader(string(alertContent))
}
func readDecisionsGetResp(resp *httptest.ResponseRecorder) ([]*models.Decision, int, error) {
var response []*models.Decision
if resp == nil {
return nil, 0, errors.New("response is nil")
}
err := json.Unmarshal(resp.Body.Bytes(), &response)
if err != nil {
return nil, resp.Code, err
}
return response, resp.Code, nil
}
func readDecisionsErrorResp(resp *httptest.ResponseRecorder) (map[string]string, int, error) {
var response map[string]string
if resp == nil {
return nil, 0, errors.New("response is nil")
}
err := json.Unmarshal(resp.Body.Bytes(), &response)
if err != nil {
return nil, resp.Code, err
}
return response, resp.Code, nil
}
func readDecisionsDeleteResp(resp *httptest.ResponseRecorder) (*models.DeleteDecisionResponse, int, error) {
var response models.DeleteDecisionResponse
if resp == nil {
return nil, 0, errors.New("response is nil")
}
err := json.Unmarshal(resp.Body.Bytes(), &response)
if err != nil {
return nil, resp.Code, err
}
return &response, resp.Code, nil
}
func readDecisionsStreamResp(resp *httptest.ResponseRecorder) (map[string][]*models.Decision, int, error) {
response := make(map[string][]*models.Decision)
if resp == nil {
return nil, 0, errors.New("response is nil")
}
err := json.Unmarshal(resp.Body.Bytes(), &response)
if err != nil {
return nil, resp.Code, err
}
return response, resp.Code, nil
}
func CreateTestMachine(router *gin.Engine) (string, error) { func CreateTestMachine(router *gin.Engine) (string, error) {
b, err := json.Marshal(MachineTest) b, err := json.Marshal(MachineTest)
if err != nil { if err != nil {
@ -306,7 +380,7 @@ func TestLoggingDebugToFileConfig(t *testing.T) {
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
//check file content //check file content
data, err := ioutil.ReadFile(expectedFile) data, err := os.ReadFile(expectedFile)
if err != nil { if err != nil {
t.Fatalf("failed to read file : %s", err) t.Fatalf("failed to read file : %s", err)
} }
@ -368,12 +442,11 @@ func TestLoggingErrorToFileConfig(t *testing.T) {
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
//check file content //check file content
x, err := ioutil.ReadFile(expectedFile) x, err := os.ReadFile(expectedFile)
if err == nil && len(x) > 0 { if err == nil && len(x) > 0 {
t.Fatalf("file should be empty, got '%s'", x) t.Fatalf("file should be empty, got '%s'", x)
} }
os.Remove("./crowdsec.log") os.Remove("./crowdsec.log")
os.Remove(expectedFile) os.Remove(expectedFile)
} }

View file

@ -1,571 +1,429 @@
package apiserver package apiserver
import ( import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing" "testing"
"time"
"github.com/crowdsecurity/crowdsec/pkg/models"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestDeleteDecisionRange(t *testing.T) { func TestDeleteDecisionRange(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
// Create Valid Alert // Create Valid Alert
alertContentBytes, err := ioutil.ReadFile("./tests/alert_minibulk.json") lapi.InsertAlertFromFile("./tests/alert_minibulk.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts {
*alert.StartAt = time.Now().UTC().Format(time.RFC3339)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339)
}
alertContent, err := json.Marshal(alerts)
if err != nil {
log.Fatal(err)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent)))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
// delete by ip wrong // delete by ip wrong
w = httptest.NewRecorder() w := lapi.RecordResponse("DELETE", "/v1/decisions?range=1.2.3.0/24", emptyBody)
req, _ = http.NewRequest("DELETE", "/v1/decisions?range=1.2.3.0/24", strings.NewReader(""))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"nbDeleted":"0"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"0"}`, w.Body.String())
// delete by range // delete by range
w = httptest.NewRecorder()
req, _ = http.NewRequest("DELETE", "/v1/decisions?range=91.121.79.0/24&contains=false", strings.NewReader("")) w = lapi.RecordResponse("DELETE", "/v1/decisions?range=91.121.79.0/24&contains=false", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"nbDeleted":"2"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"2"}`, w.Body.String())
// delete by range : ensure it was already deleted // delete by range : ensure it was already deleted
w = httptest.NewRecorder()
req, _ = http.NewRequest("DELETE", "/v1/decisions?range=91.121.79.0/24", strings.NewReader("")) w = lapi.RecordResponse("DELETE", "/v1/decisions?range=91.121.79.0/24", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"nbDeleted":"0"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"0"}`, w.Body.String())
} }
func TestDeleteDecisionFilter(t *testing.T) { func TestDeleteDecisionFilter(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
// Create Valid Alert // Create Valid Alert
alertContentBytes, err := ioutil.ReadFile("./tests/alert_minibulk.json") lapi.InsertAlertFromFile("./tests/alert_minibulk.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts {
*alert.StartAt = time.Now().UTC().Format(time.RFC3339)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339)
}
alertContent, err := json.Marshal(alerts)
if err != nil {
log.Fatal(err)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent)))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
// delete by ip wrong // delete by ip wrong
w = httptest.NewRecorder()
req, _ = http.NewRequest("DELETE", "/v1/decisions?ip=1.2.3.4", strings.NewReader("")) w := lapi.RecordResponse("DELETE", "/v1/decisions?ip=1.2.3.4", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"nbDeleted":"0"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"0"}`, w.Body.String())
// delete by ip good // delete by ip good
w = httptest.NewRecorder()
req, _ = http.NewRequest("DELETE", "/v1/decisions?ip=91.121.79.179", strings.NewReader("")) w = lapi.RecordResponse("DELETE", "/v1/decisions?ip=91.121.79.179", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
// delete by scope/value // delete by scope/value
w = httptest.NewRecorder()
req, _ = http.NewRequest("DELETE", "/v1/decisions?scopes=Ip&value=91.121.79.178", strings.NewReader("")) w = lapi.RecordResponse("DELETE", "/v1/decisions?scopes=Ip&value=91.121.79.178", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String()) assert.Equal(t, `{"nbDeleted":"1"}`, w.Body.String())
} }
func TestGetDecisionFilters(t *testing.T) { func TestGetDecisionFilters(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
// Create Valid Alert // Create Valid Alert
alertContentBytes, err := ioutil.ReadFile("./tests/alert_minibulk.json") lapi.InsertAlertFromFile("./tests/alert_minibulk.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts {
*alert.StartAt = time.Now().UTC().Format(time.RFC3339)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339)
}
alertContent, err := json.Marshal(alerts)
if err != nil {
log.Fatal(err)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent)))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
APIKey, err := CreateTestBouncer()
if err != nil {
log.Fatalf("%s", err.Error())
}
// Get Decision // Get Decision
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/decisions", strings.NewReader("")) w := lapi.RecordResponse("GET", "/v1/decisions", emptyBody)
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"id":1,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.179"`) decisions, code, err := readDecisionsGetResp(w)
assert.Contains(t, w.Body.String(), `"id":2,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.178"`) assert.Nil(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, 2, len(decisions))
assert.Equal(t, "crowdsecurity/ssh-bf", *decisions[0].Scenario)
assert.Equal(t, "91.121.79.179", *decisions[0].Value)
assert.Equal(t, int64(1), decisions[0].ID)
assert.Equal(t, "crowdsecurity/ssh-bf", *decisions[1].Scenario)
assert.Equal(t, "91.121.79.178", *decisions[1].Value)
assert.Equal(t, int64(2), decisions[1].ID)
// Get Decision : type filter // Get Decision : type filter
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/decisions?type=ban", strings.NewReader("")) w = lapi.RecordResponse("GET", "/v1/decisions?type=ban", emptyBody)
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"id":1,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.179"`) decisions, code, err = readDecisionsGetResp(w)
assert.Contains(t, w.Body.String(), `"id":2,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.178"`) assert.Nil(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, 2, len(decisions))
assert.Equal(t, "crowdsecurity/ssh-bf", *decisions[0].Scenario)
assert.Equal(t, "91.121.79.179", *decisions[0].Value)
assert.Equal(t, int64(1), decisions[0].ID)
assert.Equal(t, "crowdsecurity/ssh-bf", *decisions[1].Scenario)
assert.Equal(t, "91.121.79.178", *decisions[1].Value)
assert.Equal(t, int64(2), decisions[1].ID)
// assert.Contains(t, w.Body.String(), `"id":1,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.179"`)
// assert.Contains(t, w.Body.String(), `"id":2,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.178"`)
// Get Decision : scope/value // Get Decision : scope/value
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/decisions?scopes=Ip&value=91.121.79.179", strings.NewReader("")) w = lapi.RecordResponse("GET", "/v1/decisions?scopes=Ip&value=91.121.79.179", emptyBody)
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"id":1,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.179"`) decisions, code, err = readDecisionsGetResp(w)
assert.NotContains(t, w.Body.String(), `"id":2,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.178"`) assert.Nil(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, 1, len(decisions))
assert.Equal(t, "crowdsecurity/ssh-bf", *decisions[0].Scenario)
assert.Equal(t, "91.121.79.179", *decisions[0].Value)
assert.Equal(t, int64(1), decisions[0].ID)
// assert.Contains(t, w.Body.String(), `"id":1,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.179"`)
// assert.NotContains(t, w.Body.String(), `"id":2,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.178"`)
// Get Decision : ip filter // Get Decision : ip filter
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/decisions?ip=91.121.79.179", strings.NewReader("")) w = lapi.RecordResponse("GET", "/v1/decisions?ip=91.121.79.179", emptyBody)
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"id":1,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.179"`) decisions, code, err = readDecisionsGetResp(w)
assert.NotContains(t, w.Body.String(), `"id":2,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.178"`) assert.Nil(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, 1, len(decisions))
assert.Equal(t, "crowdsecurity/ssh-bf", *decisions[0].Scenario)
assert.Equal(t, "91.121.79.179", *decisions[0].Value)
assert.Equal(t, int64(1), decisions[0].ID)
// assert.Contains(t, w.Body.String(), `"id":1,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.179"`)
// assert.NotContains(t, w.Body.String(), `"id":2,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.178"`)
// Get decision : by range // Get decision : by range
w = httptest.NewRecorder() w = lapi.RecordResponse("GET", "/v1/decisions?range=91.121.79.0/24&contains=false", emptyBody)
req, _ = http.NewRequest("GET", "/v1/decisions?range=91.121.79.0/24&contains=false", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), `"id":1,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.179"`) decisions, code, err = readDecisionsGetResp(w)
assert.Contains(t, w.Body.String(), `"id":2,"origin":"crowdsec","scenario":"crowdsecurity/ssh-bf","scope":"Ip","type":"ban","value":"91.121.79.178"`) assert.Nil(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, 2, len(decisions))
assert.Contains(t, []string{*decisions[0].Value, *decisions[1].Value}, "91.121.79.179")
assert.Contains(t, []string{*decisions[0].Value, *decisions[1].Value}, "91.121.79.178")
} }
func TestGetDecision(t *testing.T) { func TestGetDecision(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
// Create Valid Alert // Create Valid Alert
alertContentBytes, err := ioutil.ReadFile("./tests/alert_sample.json") lapi.InsertAlertFromFile("./tests/alert_sample.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts {
*alert.StartAt = time.Now().UTC().Format(time.RFC3339)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339)
}
alertContent, err := json.Marshal(alerts)
if err != nil {
log.Fatal(err)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent)))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
APIKey, err := CreateTestBouncer()
if err != nil {
log.Fatalf("%s", err.Error())
}
// Get Decision // Get Decision
w = httptest.NewRecorder() w := lapi.RecordResponse("GET", "/v1/decisions", emptyBody)
req, _ = http.NewRequest("GET", "/v1/decisions", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "\"id\":3,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]") decisions, code, err := readDecisionsGetResp(w)
assert.Nil(t, err)
assert.Equal(t, 200, code)
assert.Equal(t, 3, len(decisions))
/*decisions get doesn't perform deduplication*/
assert.Equal(t, "crowdsecurity/test", *decisions[0].Scenario)
assert.Equal(t, "127.0.0.1", *decisions[0].Value)
assert.Equal(t, int64(1), decisions[0].ID)
assert.Equal(t, "crowdsecurity/test", *decisions[1].Scenario)
assert.Equal(t, "127.0.0.1", *decisions[1].Value)
assert.Equal(t, int64(2), decisions[1].ID)
assert.Equal(t, "crowdsecurity/test", *decisions[2].Scenario)
assert.Equal(t, "127.0.0.1", *decisions[2].Value)
assert.Equal(t, int64(3), decisions[2].ID)
// Get Decision with invalid filter. It should ignore this filter // Get Decision with invalid filter. It should ignore this filter
w = httptest.NewRecorder() w = lapi.RecordResponse("GET", "/v1/decisions?test=test", emptyBody)
req, _ = http.NewRequest("GET", "/v1/decisions?test=test", strings.NewReader(""))
req.Header.Add("User-Agent", UserAgent)
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "\"id\":3,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]") assert.Equal(t, 3, len(decisions))
} }
func TestDeleteDecisionByID(t *testing.T) { func TestDeleteDecisionByID(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
// Create Valid Alert // Create Valid Alert
alertContentBytes, err := ioutil.ReadFile("./tests/alert_sample.json") lapi.InsertAlertFromFile("./tests/alert_sample.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts { //Have one alerts
*alert.StartAt = time.Now().UTC().Format(time.RFC3339) w := lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339) decisions, code, err := readDecisionsStreamResp(w)
} assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
alertContent, err := json.Marshal(alerts) assert.Equal(t, len(decisions["deleted"]), 0)
if err != nil { assert.Equal(t, len(decisions["new"]), 1)
log.Fatal(err)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent)))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
// Delete alert with Invalid ID // Delete alert with Invalid ID
w = httptest.NewRecorder() w = lapi.RecordResponse("DELETE", "/v1/decisions/test", emptyBody)
req, _ = http.NewRequest("DELETE", "/v1/decisions/test", strings.NewReader(""))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 400, w.Code) assert.Equal(t, 400, w.Code)
assert.Equal(t, "{\"message\":\"decision_id must be valid integer\"}", w.Body.String()) err_resp, _, err := readDecisionsErrorResp(w)
assert.NoError(t, err)
assert.Equal(t, err_resp["message"], "decision_id must be valid integer")
// Delete alert with ID that not exist // Delete alert with ID that not exist
w = httptest.NewRecorder() w = lapi.RecordResponse("DELETE", "/v1/decisions/100", emptyBody)
req, _ = http.NewRequest("DELETE", "/v1/decisions/100", strings.NewReader(""))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Equal(t, "{\"message\":\"decision with id '100' doesn't exist: unable to delete\"}", w.Body.String()) err_resp, _, err = readDecisionsErrorResp(w)
assert.NoError(t, err)
assert.Equal(t, err_resp["message"], "decision with id '100' doesn't exist: unable to delete")
//Have one alerts
w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
decisions, code, err = readDecisionsStreamResp(w)
assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, len(decisions["new"]), 1)
// Delete alert with valid ID // Delete alert with valid ID
w = httptest.NewRecorder() w = lapi.RecordResponse("DELETE", "/v1/decisions/1", emptyBody)
req, _ = http.NewRequest("DELETE", "/v1/decisions/1", strings.NewReader(""))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "{\"nbDeleted\":\"1\"}", w.Body.String()) resp, _, err := readDecisionsDeleteResp(w)
assert.NoError(t, err)
assert.Equal(t, resp.NbDeleted, "1")
//Have one alert (because we delete an alert that has dup targets)
w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
decisions, code, err = readDecisionsStreamResp(w)
assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, len(decisions["new"]), 1)
} }
func TestDeleteDecision(t *testing.T) { func TestDeleteDecision(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
// Create Valid Alert // Create Valid Alert
alertContentBytes, err := ioutil.ReadFile("./tests/alert_sample.json") lapi.InsertAlertFromFile("./tests/alert_sample.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts {
*alert.StartAt = time.Now().UTC().Format(time.RFC3339)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339)
}
alertContent, err := json.Marshal(alerts)
if err != nil {
log.Fatal(err)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent)))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
// Delete alert with Invalid filter // Delete alert with Invalid filter
w = httptest.NewRecorder() w := lapi.RecordResponse("DELETE", "/v1/decisions?test=test", emptyBody)
req, _ = http.NewRequest("DELETE", "/v1/decisions?test=test", strings.NewReader(""))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 500, w.Code) assert.Equal(t, 500, w.Code)
assert.Equal(t, "{\"message\":\"'test' doesn't exist: invalid filter\"}", w.Body.String()) err_resp, _, err := readDecisionsErrorResp(w)
assert.NoError(t, err)
// Delete alert assert.Equal(t, err_resp["message"], "'test' doesn't exist: invalid filter")
w = httptest.NewRecorder()
req, _ = http.NewRequest("DELETE", "/v1/decisions", strings.NewReader(""))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
// Delete all alert
w = lapi.RecordResponse("DELETE", "/v1/decisions", emptyBody)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "{\"nbDeleted\":\"3\"}", w.Body.String()) resp, _, err := readDecisionsDeleteResp(w)
assert.NoError(t, err)
assert.Equal(t, resp.NbDeleted, "3")
} }
func TestStreamDecision(t *testing.T) { func TestStreamStartDecisionDedup(t *testing.T) {
router, loginResp, err := InitMachineTest() //Ensure that at stream startup we only get the longest decision
if err != nil { lapi := SetupLAPITest(t)
log.Fatalln(err.Error())
}
// Create Valid Alert // Create Valid Alert : 3 decisions for 127.0.0.1, longest has id=3
alertContentBytes, err := ioutil.ReadFile("./tests/alert_sample.json") lapi.InsertAlertFromFile("./tests/alert_sample.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts { // Get Stream, we only get one decision (the longest one)
*alert.StartAt = time.Now().UTC().Format(time.RFC3339) w := lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339) decisions, code, err := readDecisionsStreamResp(w)
} assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, len(decisions["new"]), 1)
assert.Equal(t, decisions["new"][0].ID, int64(3))
assert.Equal(t, *decisions["new"][0].Origin, "test")
assert.Equal(t, *decisions["new"][0].Value, "127.0.0.1")
alertContent, err := json.Marshal(alerts) // id=3 decision is deleted, this won't affect `deleted`, because there are decisions on the same ip
if err != nil { w = lapi.RecordResponse("DELETE", "/v1/decisions/3", emptyBody)
log.Fatal(err)
}
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent)))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", loginResp.Token))
router.ServeHTTP(w, req)
APIKey, err := CreateTestBouncer()
if err != nil {
log.Fatalf("%s", err.Error())
}
// Get Stream
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/decisions/stream", strings.NewReader(""))
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "{\"deleted\":null,\"new\":null}", w.Body.String())
// Get Stream just startup
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/decisions/stream?startup=true", strings.NewReader(""))
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
// the decision with id=3 is only returned because it's the longest decision
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "\"id\":3,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]}")
assert.NotContains(t, w.Body.String(), "\"id\":2")
assert.NotContains(t, w.Body.String(), "\"id\":1")
assert.Contains(t, w.Body.String(), "2h")
// id=3 decision is deleted, this won't affect `deleted`, because there are decisions
// targetting same IP
req, _ = http.NewRequest("DELETE", "/v1/decisions/3", strings.NewReader(""))
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
w = httptest.NewRecorder() // Get Stream, we only get one decision (the longest one, id=2)
req, _ = http.NewRequest("GET", "/v1/decisions/stream?startup=true", strings.NewReader("")) w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
req.Header.Add("X-Api-Key", APIKey) decisions, code, err = readDecisionsStreamResp(w)
router.ServeHTTP(w, req) assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, 200, w.Code) assert.Equal(t, len(decisions["deleted"]), 0)
// the decision with id=2 is only returned because it's the longest decision assert.Equal(t, len(decisions["new"]), 1)
assert.Contains(t, w.Body.String(), "\"id\":2,\"origin\":\"test\",\"scenario\":\"crowdsecurity/test\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"}]}") assert.Equal(t, decisions["new"][0].ID, int64(2))
assert.NotContains(t, w.Body.String(), "\"id\":3") assert.Equal(t, *decisions["new"][0].Origin, "test")
assert.NotContains(t, w.Body.String(), "\"id\":1") assert.Equal(t, *decisions["new"][0].Value, "127.0.0.1")
assert.Contains(t, w.Body.String(), "1h")
assert.Contains(t, w.Body.String(), "\"deleted\":null")
// We delete another decision, yet don't receive it in stream, since there's another decision on same IP // We delete another decision, yet don't receive it in stream, since there's another decision on same IP
req, _ = http.NewRequest("DELETE", "/v1/decisions/2", strings.NewReader("")) w = lapi.RecordResponse("DELETE", "/v1/decisions/2", emptyBody)
AddAuthHeaders(req, loginResp)
router.ServeHTTP(w, req)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/decisions/stream", strings.NewReader(""))
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code) assert.Equal(t, 200, w.Code)
assert.Equal(t, "{\"deleted\":null,\"new\":null}", w.Body.String())
// Now all decisions for this IP are deleted, we should receive it in stream // And get the remaining decision (1)
req, _ = http.NewRequest("DELETE", "/v1/decisions/1", strings.NewReader("")) w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
AddAuthHeaders(req, loginResp) decisions, code, err = readDecisionsStreamResp(w)
router.ServeHTTP(w, req) assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, len(decisions["new"]), 1)
assert.Equal(t, decisions["new"][0].ID, int64(1))
assert.Equal(t, *decisions["new"][0].Origin, "test")
assert.Equal(t, *decisions["new"][0].Value, "127.0.0.1")
// We delete the last decision, we receive the delete order
w = lapi.RecordResponse("DELETE", "/v1/decisions/1", emptyBody)
assert.Equal(t, 200, w.Code)
//and now we only get a deleted decision
w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
decisions, code, err = readDecisionsStreamResp(w)
assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 1)
assert.Equal(t, decisions["deleted"][0].ID, int64(1))
assert.Equal(t, *decisions["deleted"][0].Origin, "test")
assert.Equal(t, *decisions["deleted"][0].Value, "127.0.0.1")
assert.Equal(t, len(decisions["new"]), 0)
} }
func TestStreamDecisionDedup(t *testing.T) {
//Ensure that at stream startup we only get the longest decision
lapi := SetupLAPITest(t)
// Create Valid Alert : 3 decisions for 127.0.0.1, longest has id=3
lapi.InsertAlertFromFile("./tests/alert_sample.json")
// Get Stream, we only get one decision (the longest one)
w := lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
decisions, code, err := readDecisionsStreamResp(w)
assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, len(decisions["new"]), 1)
assert.Equal(t, decisions["new"][0].ID, int64(3))
assert.Equal(t, *decisions["new"][0].Origin, "test")
assert.Equal(t, *decisions["new"][0].Value, "127.0.0.1")
// id=3 decision is deleted, this won't affect `deleted`, because there are decisions on the same ip
w = lapi.RecordResponse("DELETE", "/v1/decisions/3", emptyBody)
assert.Equal(t, 200, w.Code)
w = lapi.RecordResponse("GET", "/v1/decisions/stream", emptyBody)
assert.Equal(t, err, nil)
decisions, code, err = readDecisionsStreamResp(w)
assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, len(decisions["new"]), 0)
// We delete another decision, yet don't receive it in stream, since there's another decision on same IP
w = lapi.RecordResponse("DELETE", "/v1/decisions/2", emptyBody)
assert.Equal(t, 200, w.Code)
w = lapi.RecordResponse("GET", "/v1/decisions/stream", emptyBody)
decisions, code, err = readDecisionsStreamResp(w)
assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, len(decisions["new"]), 0)
// We delete the last decision, we receive the delete order
w = lapi.RecordResponse("DELETE", "/v1/decisions/1", emptyBody)
assert.Equal(t, 200, w.Code)
w = lapi.RecordResponse("GET", "/v1/decisions/stream", emptyBody)
decisions, code, err = readDecisionsStreamResp(w)
assert.Equal(t, err, nil)
assert.Equal(t, code, 200)
assert.Equal(t, len(decisions["deleted"]), 1)
assert.Equal(t, decisions["deleted"][0].ID, int64(1))
assert.Equal(t, *decisions["deleted"][0].Origin, "test")
assert.Equal(t, *decisions["deleted"][0].Value, "127.0.0.1")
assert.Equal(t, len(decisions["new"]), 0)
}
func TestStreamDecisionFilters(t *testing.T) { func TestStreamDecisionFilters(t *testing.T) {
router, loginResp, err := InitMachineTest() lapi := SetupLAPITest(t)
if err != nil {
log.Fatalln(err.Error())
}
// Create Valid Alert // Create Valid Alert
alertContentBytes, err := ioutil.ReadFile("./tests/alert_stream_fixture.json") lapi.InsertAlertFromFile("./tests/alert_stream_fixture.json")
if err != nil {
log.Fatal(err)
}
alerts := make([]*models.Alert, 0)
if err := json.Unmarshal(alertContentBytes, &alerts); err != nil {
log.Fatal(err)
}
for _, alert := range alerts { w := lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true", emptyBody)
*alert.StartAt = time.Now().UTC().Format(time.RFC3339) decisions, code, err := readDecisionsStreamResp(w)
*alert.StopAt = time.Now().UTC().Format(time.RFC3339)
}
alertContent, err := json.Marshal(alerts) assert.Equal(t, 200, code)
if err != nil { assert.Equal(t, err, nil)
log.Fatal(err) assert.Equal(t, len(decisions["deleted"]), 0)
} assert.Equal(t, len(decisions["new"]), 3)
w := httptest.NewRecorder() assert.Equal(t, decisions["new"][0].ID, int64(1))
req, err := http.NewRequest("POST", "/v1/alerts", strings.NewReader(string(alertContent))) assert.Equal(t, *decisions["new"][0].Origin, "test1")
if err != nil { assert.Equal(t, *decisions["new"][0].Value, "127.0.0.1")
log.Fatalf("%s", err.Error()) assert.Equal(t, *decisions["new"][0].Scenario, "crowdsecurity/http_bf")
} assert.Equal(t, decisions["new"][1].ID, int64(2))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", loginResp.Token)) assert.Equal(t, *decisions["new"][1].Origin, "test2")
router.ServeHTTP(w, req) assert.Equal(t, *decisions["new"][1].Value, "127.0.0.1")
assert.Equal(t, *decisions["new"][1].Scenario, "crowdsecurity/ssh_bf")
APIKey, err := CreateTestBouncer() assert.Equal(t, decisions["new"][2].ID, int64(3))
if err != nil { assert.Equal(t, *decisions["new"][2].Origin, "test3")
log.Fatalf("%s", err.Error()) assert.Equal(t, *decisions["new"][2].Value, "127.0.0.1")
} assert.Equal(t, *decisions["new"][2].Scenario, "crowdsecurity/ddos")
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/v1/decisions/stream?startup=true", strings.NewReader(""))
req.Header.Add("X-Api-Key", APIKey)
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "\"id\":1,\"origin\":\"test1\",\"scenario\":\"crowdsecurity/http_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
assert.Contains(t, w.Body.String(), "\"id\":2,\"origin\":\"test2\",\"scenario\":\"crowdsecurity/ssh_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
assert.Contains(t, w.Body.String(), "\"id\":3,\"origin\":\"test3\",\"scenario\":\"crowdsecurity/ddos\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
// test filter scenarios_not_containing // test filter scenarios_not_containing
w = httptest.NewRecorder() w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true&scenarios_not_containing=http", emptyBody)
req, _ = http.NewRequest("GET", "/v1/decisions/stream?startup=true&scenarios_not_containing=http", strings.NewReader("")) decisions, code, err = readDecisionsStreamResp(w)
req.Header.Add("X-Api-Key", APIKey) assert.Equal(t, err, nil)
router.ServeHTTP(w, req) assert.Equal(t, 200, code)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, 200, w.Code) assert.Equal(t, len(decisions["new"]), 2)
assert.NotContains(t, w.Body.String(), "\"id\":1,\"origin\":\"test1\",\"scenario\":\"crowdsecurity/http_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"") assert.Equal(t, decisions["new"][0].ID, int64(2))
assert.Contains(t, w.Body.String(), "\"id\":2,\"origin\":\"test2\",\"scenario\":\"crowdsecurity/ssh_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"") assert.Equal(t, decisions["new"][1].ID, int64(3))
assert.Contains(t, w.Body.String(), "\"id\":3,\"origin\":\"test3\",\"scenario\":\"crowdsecurity/ddos\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
// test filter scenarios_containing // test filter scenarios_containing
w = httptest.NewRecorder() w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true&scenarios_containing=http", emptyBody)
req, _ = http.NewRequest("GET", "/v1/decisions/stream?startup=true&scenarios_containing=http", strings.NewReader("")) decisions, code, err = readDecisionsStreamResp(w)
req.Header.Add("X-Api-Key", APIKey) assert.Equal(t, err, nil)
router.ServeHTTP(w, req) assert.Equal(t, 200, code)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, 200, w.Code) assert.Equal(t, len(decisions["new"]), 1)
assert.Contains(t, w.Body.String(), "\"id\":1,\"origin\":\"test1\",\"scenario\":\"crowdsecurity/http_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"") assert.Equal(t, decisions["new"][0].ID, int64(1))
assert.NotContains(t, w.Body.String(), "\"id\":2,\"origin\":\"test2\",\"scenario\":\"crowdsecurity/ssh_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
assert.NotContains(t, w.Body.String(), "\"id\":3,\"origin\":\"test3\",\"scenario\":\"crowdsecurity/ddos\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
// test filters both by scenarios_not_containing and scenarios_containing // test filters both by scenarios_not_containing and scenarios_containing
w = httptest.NewRecorder() w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true&scenarios_not_containing=ssh&scenarios_containing=ddos", emptyBody)
req, _ = http.NewRequest("GET", "/v1/decisions/stream?startup=true&scenarios_not_containing=ssh&scenarios_containing=ddos", strings.NewReader("")) decisions, code, err = readDecisionsStreamResp(w)
req.Header.Add("X-Api-Key", APIKey) assert.Equal(t, err, nil)
router.ServeHTTP(w, req) assert.Equal(t, 200, code)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, 200, w.Code) assert.Equal(t, len(decisions["new"]), 1)
assert.NotContains(t, w.Body.String(), "\"id\":1,\"origin\":\"test1\",\"scenario\":\"crowdsecurity/http_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"") assert.Equal(t, decisions["new"][0].ID, int64(3))
assert.NotContains(t, w.Body.String(), "\"id\":2,\"origin\":\"test2\",\"scenario\":\"crowdsecurity/ssh_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
assert.Contains(t, w.Body.String(), "\"id\":3,\"origin\":\"test3\",\"scenario\":\"crowdsecurity/ddos\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
// test filter by origin // test filter by origin
w = httptest.NewRecorder() w = lapi.RecordResponse("GET", "/v1/decisions/stream?startup=true&origins=test1,test2", emptyBody)
req, _ = http.NewRequest("GET", "/v1/decisions/stream?startup=true&origins=test1,test2", strings.NewReader("")) decisions, code, err = readDecisionsStreamResp(w)
req.Header.Add("X-Api-Key", APIKey) assert.Equal(t, err, nil)
router.ServeHTTP(w, req) assert.Equal(t, 200, code)
assert.Equal(t, len(decisions["deleted"]), 0)
assert.Equal(t, 200, w.Code) assert.Equal(t, len(decisions["new"]), 2)
assert.Contains(t, w.Body.String(), "\"id\":1,\"origin\":\"test1\",\"scenario\":\"crowdsecurity/http_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"") assert.Equal(t, decisions["new"][0].ID, int64(1))
assert.Contains(t, w.Body.String(), "\"id\":2,\"origin\":\"test2\",\"scenario\":\"crowdsecurity/ssh_bf\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"") assert.Equal(t, decisions["new"][1].ID, int64(2))
assert.NotContains(t, w.Body.String(), "\"id\":3,\"origin\":\"test3\",\"scenario\":\"crowdsecurity/ddos\",\"scope\":\"Ip\",\"type\":\"ban\",\"value\":\"127.0.0.1\"")
} }

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,548 @@
[ [
{"capacity":5,"decisions":null,"events":[{"meta":[{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"ASNNumber","value":"16276"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"service","value":"ssh"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"target_user","value":"root"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"service","value":"ssh"},{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.179"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"service","value":"ssh"},{"key":"IsoCode","value":"FR"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"}],"events_count":6,"labels":null,"leakspeed":"10s","message":"Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over 46.375699ms) at 2020-10-26 12:52:58.200237122 +0100 CET m=+8.191478202","remediation":true,"scenario":"crowdsecurity/ssh-bf","scenario_hash":"4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f","scenario_version":"0.1","simulated":true,"source":{"as_name":"OVH SAS","cn":"FR","ip":"91.121.79.179","latitude":50.646,"longitude":3.0758,"range":"91.121.72.0/21","scope":"Ip","value":"91.121.79.179"},"start_at":"2020-10-26T12:52:58.153861334+01:00","stop_at":"2020-10-26T12:52:58.200236582+01:00"}, {
{"capacity":5,"decisions":null,"events":[{"meta":[{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"ASNNumber","value":"16276"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"service","value":"ssh"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"target_user","value":"root"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"service","value":"ssh"},{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.178"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"service","value":"ssh"},{"key":"IsoCode","value":"FR"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"}],"events_count":6,"labels":null,"leakspeed":"10s","message":"Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over 46.375699ms) at 2020-10-26 12:52:58.200237122 +0100 CET m=+8.191478202","remediation":true,"scenario":"crowdsecurity/ssh-bf","scenario_hash":"4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f","scenario_version":"0.1","simulated":false,"source":{"as_name":"OVH SAS","cn":"FR","ip":"91.121.79.178","latitude":50.646,"longitude":3.0758,"range":"91.121.72.0/21","scope":"Ip","value":"91.121.79.178"},"start_at":"2020-10-26T12:52:58.153861334+01:00","stop_at":"2020-10-26T12:52:58.200236582+01:00"} "capacity": 5,
"decisions": null,
"events": [
{
"meta": [
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "target_user",
"value": "root"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
}
],
"events_count": 6,
"labels": null,
"leakspeed": "10s",
"message": "Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over 46.375699ms) at 2020-10-26 12:52:58.200237122 +0100 CET m=+8.191478202",
"remediation": true,
"scenario": "crowdsecurity/ssh-bf",
"scenario_hash": "4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f",
"scenario_version": "0.1",
"simulated": true,
"source": {
"as_name": "OVH SAS",
"cn": "FR",
"ip": "91.121.79.179",
"latitude": 50.646,
"longitude": 3.0758,
"range": "91.121.72.0/21",
"scope": "Ip",
"value": "91.121.79.179"
},
"start_at": "2020-10-26T12:52:58.153861334+01:00",
"stop_at": "2020-10-26T12:52:58.200236582+01:00"
},
{
"capacity": 5,
"decisions": null,
"events": [
{
"meta": [
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "target_user",
"value": "root"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
}
],
"events_count": 6,
"labels": null,
"leakspeed": "10s",
"message": "Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over 46.375699ms) at 2020-10-26 12:52:58.200237122 +0100 CET m=+8.191478202",
"remediation": true,
"scenario": "crowdsecurity/ssh-bf",
"scenario_hash": "4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f",
"scenario_version": "0.1",
"simulated": false,
"source": {
"as_name": "OVH SAS",
"cn": "FR",
"ip": "91.121.79.178",
"latitude": 50.646,
"longitude": 3.0758,
"range": "91.121.72.0/21",
"scope": "Ip",
"value": "91.121.79.178"
},
"start_at": "2020-10-26T12:52:58.153861334+01:00",
"stop_at": "2020-10-26T12:52:58.200236582+01:00"
}
] ]

View file

@ -1,4 +1,548 @@
[ [
{"capacity":5,"decisions":null,"events":[{"meta":[{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"ASNNumber","value":"16276"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"service","value":"ssh"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"target_user","value":"root"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"service","value":"ssh"},{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.179"},{"key":"IsoCode","value":"FR"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.179"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"service","value":"ssh"},{"key":"IsoCode","value":"FR"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"}],"events_count":6,"labels":null,"leakspeed":"10s","message":"Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over 46.375699ms) at 2020-10-26 12:52:58.200237122 +0100 CET m=+8.191478202","remediation":true,"scenario":"crowdsecurity/ssh-bf","scenario_hash":"4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f","scenario_version":"0.1","simulated":false,"source":{"as_name":"OVH SAS","cn":"FR","ip":"91.121.79.179","latitude":50.646,"longitude":3.0758,"range":"91.121.72.0/21","scope":"Ip","value":"91.121.79.179"},"start_at":"2020-10-26T12:52:58.153861334+01:00","stop_at":"2020-10-26T12:52:58.200236582+01:00"}, {
{"capacity":5,"decisions":null,"events":[{"meta":[{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"ASNNumber","value":"16276"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"service","value":"ssh"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"target_user","value":"root"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"service","value":"ssh"},{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"service","value":"ssh"},{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.178"},{"key":"IsoCode","value":"FR"},{"key":"IsInEU","value":"true"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"}],"timestamp":"2020-10-02T17:09:08Z"},{"meta":[{"key":"log_type","value":"ssh_failed-auth"},{"key":"source_ip","value":"91.121.79.178"},{"key":"ASNNumber","value":"16276"},{"key":"ASNOrg","value":"OVH SAS"},{"key":"SourceRange","value":"91.121.72.0/21"},{"key":"target_user","value":"root"},{"key":"service","value":"ssh"},{"key":"IsoCode","value":"FR"},{"key":"IsInEU","value":"true"}],"timestamp":"2020-10-02T17:09:08Z"}],"events_count":6,"labels":null,"leakspeed":"10s","message":"Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over 46.375699ms) at 2020-10-26 12:52:58.200237122 +0100 CET m=+8.191478202","remediation":true,"scenario":"crowdsecurity/ssh-bf","scenario_hash":"4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f","scenario_version":"0.1","simulated":false,"source":{"as_name":"OVH SAS","cn":"FR","ip":"91.121.79.178","latitude":50.646,"longitude":3.0758,"range":"91.121.72.0/21","scope":"Ip","value":"91.121.79.178"},"start_at":"2020-10-26T12:52:58.153861334+01:00","stop_at":"2020-10-26T12:52:58.200236582+01:00"} "capacity": 5,
"decisions": null,
"events": [
{
"meta": [
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "target_user",
"value": "root"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.179"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
}
],
"events_count": 6,
"labels": null,
"leakspeed": "10s",
"message": "Ip 91.121.79.179 performed crowdsecurity/ssh-bf (6 events over 46.375699ms) at 2020-10-26 12:52:58.200237122 +0100 CET m=+8.191478202",
"remediation": true,
"scenario": "crowdsecurity/ssh-bf",
"scenario_hash": "4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f",
"scenario_version": "0.1",
"simulated": false,
"source": {
"as_name": "OVH SAS",
"cn": "FR",
"ip": "91.121.79.179",
"latitude": 50.646,
"longitude": 3.0758,
"range": "91.121.72.0/21",
"scope": "Ip",
"value": "91.121.79.179"
},
"start_at": "2020-10-26T12:52:58.153861334+01:00",
"stop_at": "2020-10-26T12:52:58.200236582+01:00"
},
{
"capacity": 5,
"decisions": null,
"events": [
{
"meta": [
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "target_user",
"value": "root"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "IsInEU",
"value": "true"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
}
],
"timestamp": "2020-10-02T17:09:08Z"
},
{
"meta": [
{
"key": "log_type",
"value": "ssh_failed-auth"
},
{
"key": "source_ip",
"value": "91.121.79.178"
},
{
"key": "ASNNumber",
"value": "16276"
},
{
"key": "ASNOrg",
"value": "OVH SAS"
},
{
"key": "SourceRange",
"value": "91.121.72.0/21"
},
{
"key": "target_user",
"value": "root"
},
{
"key": "service",
"value": "ssh"
},
{
"key": "IsoCode",
"value": "FR"
},
{
"key": "IsInEU",
"value": "true"
}
],
"timestamp": "2020-10-02T17:09:08Z"
}
],
"events_count": 6,
"labels": null,
"leakspeed": "10s",
"message": "Ip 91.121.79.178 performed crowdsecurity/ssh-bf (6 events over 46.375699ms) at 2020-10-26 12:52:58.200237122 +0100 CET m=+8.191478202",
"remediation": true,
"scenario": "crowdsecurity/ssh-bf",
"scenario_hash": "4441dcff07020f6690d998b7101e642359ba405c2abb83565bbbdcee36de280f",
"scenario_version": "0.1",
"simulated": false,
"source": {
"as_name": "OVH SAS",
"cn": "FR",
"ip": "91.121.79.178",
"latitude": 50.646,
"longitude": 3.0758,
"range": "91.121.72.0/21",
"scope": "Ip",
"value": "91.121.79.178"
},
"start_at": "2020-10-26T12:52:58.153861334+01:00",
"stop_at": "2020-10-26T12:52:58.200236582+01:00"
}
] ]

View file

@ -30,7 +30,6 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
var testMode bool = false
var pluginMutex sync.Mutex var pluginMutex sync.Mutex
const ( const (
@ -255,6 +254,9 @@ func (pb *PluginBroker) loadNotificationPlugin(name string, binaryPath string) (
} }
cmd := exec.Command(binaryPath) cmd := exec.Command(binaryPath)
if pb.pluginProcConfig.User != "" || pb.pluginProcConfig.Group != "" { if pb.pluginProcConfig.User != "" || pb.pluginProcConfig.Group != "" {
if !(pb.pluginProcConfig.User != "" && pb.pluginProcConfig.Group != "") {
return nil, errors.New("while getting process attributes: both plugin user and group must be set")
}
cmd.SysProcAttr, err = getProcessAttr(pb.pluginProcConfig.User, pb.pluginProcConfig.Group) cmd.SysProcAttr, err = getProcessAttr(pb.pluginProcConfig.User, pb.pluginProcConfig.Group)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "while getting process attributes") return nil, errors.Wrap(err, "while getting process attributes")
@ -360,9 +362,6 @@ func setRequiredFields(pluginCfg *PluginConfig) {
} }
func pluginIsValid(path string) error { func pluginIsValid(path string) error {
if testMode {
return nil
}
var details fs.FileInfo var details fs.FileInfo
var err error var err error
@ -387,7 +386,6 @@ func pluginIsValid(path string) error {
mode := details.Mode() mode := details.Mode()
perm := uint32(mode) perm := uint32(mode)
if (perm & 00002) != 0 { if (perm & 00002) != 0 {
return fmt.Errorf("plugin at %s is world writable, world writable plugins are invalid", path) return fmt.Errorf("plugin at %s is world writable, world writable plugins are invalid", path)
} }

View file

@ -1,17 +1,36 @@
package csplugin package csplugin
import ( import (
"io/ioutil"
"log" "log"
"os" "os"
"os/exec"
"path" "path"
"reflect" "reflect"
"testing" "testing"
"time"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"gopkg.in/tomb.v2"
) )
var testPath string var testPath string
func Test_getPluginNameAndTypeFromPath(t *testing.T) { func setPluginPermTo744() {
setPluginPermTo("744")
}
func setPluginPermTo722() {
setPluginPermTo("722")
}
func setPluginPermTo724() {
setPluginPermTo("724")
}
func TestGetPluginNameAndTypeFromPath(t *testing.T) {
setUp() setUp()
defer tearDown() defer tearDown()
type args struct { type args struct {
@ -69,7 +88,7 @@ func Test_getPluginNameAndTypeFromPath(t *testing.T) {
} }
} }
func Test_listFilesAtPath(t *testing.T) { func TestListFilesAtPath(t *testing.T) {
setUp() setUp()
defer tearDown() defer tearDown()
type args struct { type args struct {
@ -113,9 +132,176 @@ func Test_listFilesAtPath(t *testing.T) {
} }
} }
func TestBrokerInit(t *testing.T) {
tests := []struct {
name string
action func()
errContains string
wantErr bool
procCfg csconfig.PluginCfg
}{
{
name: "valid config",
action: setPluginPermTo744,
wantErr: false,
},
{
name: "group writable binary",
wantErr: true,
errContains: "notification-dummy is world writable",
action: setPluginPermTo722,
},
{
name: "group writable binary",
wantErr: true,
errContains: "notification-dummy is group writable",
action: setPluginPermTo724,
},
{
name: "no plugin dir",
wantErr: true,
errContains: "no such file or directory",
action: tearDown,
},
{
name: "no plugin binary",
wantErr: true,
errContains: "binary for plugin dummy_default not found",
action: func() {
err := os.Remove(path.Join(testPath, "notification-dummy"))
if err != nil {
t.Fatal(err)
}
},
},
{
name: "only specify user",
wantErr: true,
errContains: "both plugin user and group must be set",
procCfg: csconfig.PluginCfg{
User: "123445555551122toto",
},
action: setPluginPermTo744,
},
{
name: "only specify group",
wantErr: true,
errContains: "both plugin user and group must be set",
procCfg: csconfig.PluginCfg{
Group: "123445555551122toto",
},
action: setPluginPermTo744,
},
{
name: "Fails to run as root",
wantErr: true,
errContains: "operation not permitted",
procCfg: csconfig.PluginCfg{
User: "root",
Group: "root",
},
action: setPluginPermTo744,
},
{
name: "Invalid user and group",
wantErr: true,
errContains: "unknown user toto1234",
procCfg: csconfig.PluginCfg{
User: "toto1234",
Group: "toto1234",
},
action: setPluginPermTo744,
},
{
name: "Valid user and invalid group",
wantErr: true,
errContains: "unknown group toto1234",
procCfg: csconfig.PluginCfg{
User: "nobody",
Group: "toto1234",
},
action: setPluginPermTo744,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
defer tearDown()
buildDummyPlugin()
if test.action != nil {
test.action()
}
pb := PluginBroker{}
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
profiles = append(profiles, &csconfig.ProfileCfg{
Notifications: []string{"dummy_default"},
})
err := pb.Init(&test.procCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: testPath,
NotificationDir: "./tests/notifications",
})
defer pb.Kill()
if test.wantErr {
assert.ErrorContains(t, err, test.errContains)
} else {
assert.NoError(t, err)
}
})
}
}
func TestBrokerRun(t *testing.T) {
buildDummyPlugin()
setPluginPermTo744()
defer tearDown()
procCfg := csconfig.PluginCfg{}
pb := PluginBroker{}
profiles := csconfig.NewDefaultConfig().API.Server.Profiles
profiles = append(profiles, &csconfig.ProfileCfg{
Notifications: []string{"dummy_default"},
})
err := pb.Init(&procCfg, profiles, &csconfig.ConfigurationPaths{
PluginDir: testPath,
NotificationDir: "./tests/notifications",
})
assert.NoError(t, err)
tomb := tomb.Tomb{}
go pb.Run(&tomb)
defer pb.Kill()
assert.NoFileExists(t, "./out")
defer os.Remove("./out")
pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
pb.PluginChannel <- ProfileAlert{ProfileID: uint(0), Alert: &models.Alert{}}
time.Sleep(time.Second * 4)
assert.FileExists(t, "./out")
assert.Equal(t, types.GetLineCountForFile("./out"), 2)
}
func buildDummyPlugin() {
dir, err := os.MkdirTemp("./tests", "cs_plugin_test")
if err != nil {
log.Fatal(err)
}
cmd := exec.Command("go", "build", "-o", path.Join(dir, "notification-dummy"), "../../plugins/notifications/dummy/")
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
testPath = dir
}
func setPluginPermTo(perm string) {
if err := exec.Command("chmod", perm, path.Join(testPath, "notification-dummy")).Run(); err != nil {
log.Fatal(errors.Wrapf(err, "chmod 744 %s", path.Join(testPath, "notification-dummy")))
}
}
func setUp() { func setUp() {
testMode = true dir, err := os.MkdirTemp("./", "cs_plugin_test")
dir, err := ioutil.TempDir("./", "cs_plugin_test")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View file

@ -0,0 +1,22 @@
type: dummy # Don't change
name: dummy_default # Must match the registered plugin in the profile
# One of "trace", "debug", "info", "warn", "error", "off"
log_level: info
# group_wait: # Time to wait collecting alerts before relaying a message to this plugin, eg "30s"
# group_threshold: # Amount of alerts that triggers a message before <group_wait> has expired, eg "10"
# max_retry: # Number of attempts to relay messages to plugins in case of error
# timeout: # Time to wait for response from the plugin before considering the attempt a failure, eg "10s"
#-------------------------
# plugin-specific options
# The following template receives a list of models.Alert objects
# The output goes in the logs and to a text file, if defined
format: |
{{.|toJson}}
#
output_file: ./out # notifications will be appended here. optional

View file

@ -0,0 +1,107 @@
package csplugin
import (
"context"
"log"
"testing"
"time"
"github.com/crowdsecurity/crowdsec/pkg/models"
"gopkg.in/tomb.v2"
"gotest.tools/v3/assert"
)
var ctx = context.Background()
func resetTestTomb(testTomb *tomb.Tomb) {
testTomb.Kill(nil)
if err := testTomb.Wait(); err != nil {
log.Fatal(err)
}
}
func resetWatcherAlertCounter(pw *PluginWatcher) {
for k := range pw.AlertCountByPluginName {
pw.AlertCountByPluginName[k] = 0
}
}
func insertNAlertsToPlugin(pw *PluginWatcher, n int, pluginName string) {
for i := 0; i < n; i++ {
pw.Inserts <- pluginName
}
}
func listenChannelWithTimeout(ctx context.Context, channel chan string) error {
select {
case <-channel:
case <-ctx.Done():
return ctx.Err()
}
return nil
}
func TestPluginWatcherInterval(t *testing.T) {
pw := PluginWatcher{}
alertsByPluginName := make(map[string][]*models.Alert)
testTomb := tomb.Tomb{}
configs := map[string]PluginConfig{
"testPlugin": {
GroupWait: time.Millisecond,
},
}
pw.Init(configs, alertsByPluginName)
pw.Start(&testTomb)
ct, cancel := context.WithTimeout(ctx, time.Microsecond)
defer cancel()
err := listenChannelWithTimeout(ct, pw.PluginEvents)
assert.ErrorContains(t, err, "context deadline exceeded")
resetTestTomb(&testTomb)
testTomb = tomb.Tomb{}
pw.Start(&testTomb)
ct, cancel = context.WithTimeout(ctx, time.Millisecond*5)
defer cancel()
err = listenChannelWithTimeout(ct, pw.PluginEvents)
assert.NilError(t, err)
resetTestTomb(&testTomb)
// This is to avoid the int complaining
}
func TestPluginAlertCountWatcher(t *testing.T) {
pw := PluginWatcher{}
alertsByPluginName := make(map[string][]*models.Alert)
configs := map[string]PluginConfig{
"testPlugin": {
GroupThreshold: 5,
},
}
testTomb := tomb.Tomb{}
pw.Init(configs, alertsByPluginName)
pw.Start(&testTomb)
// Channel won't contain any events since threshold is not crossed.
ct, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
err := listenChannelWithTimeout(ct, pw.PluginEvents)
assert.ErrorContains(t, err, "context deadline exceeded")
// Channel won't contain any events since threshold is not crossed.
resetWatcherAlertCounter(&pw)
insertNAlertsToPlugin(&pw, 4, "testPlugin")
ct, cancel = context.WithTimeout(ctx, time.Second)
defer cancel()
err = listenChannelWithTimeout(ct, pw.PluginEvents)
assert.ErrorContains(t, err, "context deadline exceeded")
// Channel will contain an event since threshold is crossed.
resetWatcherAlertCounter(&pw)
insertNAlertsToPlugin(&pw, 5, "testPlugin")
ct, cancel = context.WithTimeout(ctx, time.Second)
defer cancel()
err = listenChannelWithTimeout(ct, pw.PluginEvents)
assert.NilError(t, err)
resetTestTomb(&testTomb)
}

View file

@ -408,7 +408,7 @@ func (c *Client) DeleteDecisionsWithFilter(filter map[string][]string) (string,
return strconv.Itoa(nbDeleted), nil return strconv.Itoa(nbDeleted), nil
} }
// SoftDeleteDecisionsWithFilter udpate the expiration time to now() for the decisions matching the filter // SoftDeleteDecisionsWithFilter updates the expiration time to now() for the decisions matching the filter
func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (string, error) { func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (string, error) {
var err error var err error
var start_ip, start_sfx, end_ip, end_sfx int64 var start_ip, start_sfx, end_ip, end_sfx int64
@ -426,6 +426,8 @@ func (c *Client) SoftDeleteDecisionsWithFilter(filter map[string][]string) (stri
} }
case "scopes": case "scopes":
decisions = decisions.Where(decision.ScopeEQ(value[0])) decisions = decisions.Where(decision.ScopeEQ(value[0]))
case "origin":
decisions = decisions.Where(decision.OriginEQ(value[0]))
case "value": case "value":
decisions = decisions.Where(decision.ValueEQ(value[0])) decisions = decisions.Where(decision.ValueEQ(value[0]))
case "type": case "type":

40
pkg/types/dataset_test.go Normal file
View file

@ -0,0 +1,40 @@
package types
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/jarcoal/httpmock"
)
func TestDownladFile(t *testing.T) {
httpmock.Activate()
defer httpmock.DeactivateAndReset()
//OK
httpmock.RegisterResponder(
"GET",
"https://example.com/xx",
httpmock.NewStringResponder(200, "example content oneoneone"),
)
httpmock.RegisterResponder(
"GET",
"https://example.com/x",
httpmock.NewStringResponder(404, "not found"),
)
err := downloadFile("https://example.com/xx", "./example.txt")
assert.NoError(t, err)
content, err := ioutil.ReadFile("./example.txt")
assert.Equal(t, "example content oneoneone", string(content))
assert.NoError(t, err)
//bad uri
err = downloadFile("https://zz.com", "./example.txt")
assert.Error(t, err)
//404
err = downloadFile("https://example.com/x", "./example.txt")
assert.Error(t, err)
//bad target
err = downloadFile("https://example.com/xx", "")
assert.Error(t, err)
}

View file

@ -1,6 +1,7 @@
package types package types
import ( import (
"bufio"
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"fmt" "fmt"
@ -243,3 +244,17 @@ func InSlice(str string, slice []string) bool {
func UtcNow() time.Time { func UtcNow() time.Time {
return time.Now().UTC() return time.Now().UTC()
} }
func GetLineCountForFile(filepath string) int {
f, err := os.Open(filepath)
if err != nil {
log.Fatalf("unable to open log file %s", filepath)
}
defer f.Close()
lc := 0
fs := bufio.NewScanner(f)
for fs.Scan() {
lc++
}
return lc
}

View file

@ -46,7 +46,7 @@ func (s *DummyPlugin) Notify(ctx context.Context, notification *protobufs.Notifi
if err != nil { if err != nil {
logger.Error(fmt.Sprintf("Cannot open notification file: %s", err)) logger.Error(fmt.Sprintf("Cannot open notification file: %s", err))
} }
if _, err := f.WriteString(notification.Text); err != nil { if _, err := f.WriteString(notification.Text + "\n"); err != nil {
f.Close() f.Close()
logger.Error(fmt.Sprintf("Cannot write notification to file: %s", err)) logger.Error(fmt.Sprintf("Cannot write notification to file: %s", err))
} }
@ -55,7 +55,7 @@ func (s *DummyPlugin) Notify(ctx context.Context, notification *protobufs.Notifi
logger.Error(fmt.Sprintf("Cannot close notification file: %s", err)) logger.Error(fmt.Sprintf("Cannot close notification file: %s", err))
} }
} }
fmt.Print(notification.Text) fmt.Println(notification.Text)
return &protobufs.Empty{}, nil return &protobufs.Empty{}, nil
} }