crowdsec/pkg/alertcontext/alertcontext_test.go
2025-02-24 15:20:33 +01:00

447 lines
10 KiB
Go

package alertcontext
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/crowdsecurity/go-cs-lib/ptr"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
)
func TestNewAlertContext(t *testing.T) {
tests := []struct {
name string
contextToSend map[string][]string
valueLength int
expectedErr error
}{
{
name: "basic config test",
contextToSend: map[string][]string{
"test": {"evt.Parsed.source_ip"},
},
valueLength: 100,
expectedErr: nil,
},
}
for _, test := range tests {
fmt.Printf("Running test '%s'\n", test.name)
err := NewAlertContext(test.contextToSend, test.valueLength)
require.ErrorIs(t, err, test.expectedErr)
}
}
func TestEventToContext(t *testing.T) {
tests := []struct {
name string
contextToSend map[string][]string
valueLength int
events []types.Event
expectedResult models.Meta
}{
{
name: "basic test",
contextToSend: map[string][]string{
"source_ip": {"evt.Parsed.source_ip"},
"nonexistent_field": {"evt.Parsed.nonexist"},
},
valueLength: 100,
events: []types.Event{
{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
},
},
},
expectedResult: []*models.MetaItems0{
{
Key: "source_ip",
Value: "[\"1.2.3.4\"]",
},
},
},
{
name: "test many events",
contextToSend: map[string][]string{
"source_ip": {"evt.Parsed.source_ip"},
"source_machine": {"evt.Parsed.source_machine"},
"cve": {"evt.Parsed.cve"},
},
valueLength: 100,
events: []types.Event{
{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
"cve": "CVE-2022-1234",
},
},
{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
"cve": "CVE-2022-1235",
},
},
{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
"cve": "CVE-2022-125",
},
},
},
expectedResult: []*models.MetaItems0{
{
Key: "source_ip",
Value: "[\"1.2.3.4\"]",
},
{
Key: "source_machine",
Value: "[\"mymachine\"]",
},
{
Key: "cve",
Value: "[\"CVE-2022-1234\",\"CVE-2022-1235\",\"CVE-2022-125\"]",
},
},
},
{
name: "test many events with result above max length (need truncate, keep only 2 on 3 elements)",
contextToSend: map[string][]string{
"source_ip": {"evt.Parsed.source_ip"},
"source_machine": {"evt.Parsed.source_machine"},
"uri": {"evt.Parsed.uri"},
},
valueLength: 100,
events: []types.Event{
{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
"uri": "/test/test/test/../../../../../../../../",
},
},
{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
"uri": "/admin/admin/admin/../../../../../../../../",
},
},
{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
"uri": "/login/login/login/../../../../../../../../../../../",
},
},
},
expectedResult: []*models.MetaItems0{
{
Key: "source_ip",
Value: "[\"1.2.3.4\"]",
},
{
Key: "source_machine",
Value: "[\"mymachine\"]",
},
{
Key: "uri",
Value: "[\"/test/test/test/../../../../../../../../\",\"/admin/admin/admin/../../../../../../../../\"]",
},
},
},
{
name: "test one events with result above max length (need truncate on one element)",
contextToSend: map[string][]string{
"source_ip": {"evt.Parsed.source_ip"},
"source_machine": {"evt.Parsed.source_machine"},
"uri": {"evt.Parsed.uri"},
},
valueLength: 100,
events: []types.Event{
{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
"uri": "/test/test/test/../../../../.should_truncate_just_after_this/../../../..../../../../../../../../../../../../../../../end",
},
},
},
expectedResult: []*models.MetaItems0{
{
Key: "source_machine",
Value: "[\"mymachine\"]",
},
{
Key: "uri",
Value: "[\"/test/test/test/../../../../.should_truncate_just_after_this...\"]",
},
{
Key: "source_ip",
Value: "[\"1.2.3.4\"]",
},
},
},
}
for _, test := range tests {
fmt.Printf("Running test '%s'\n", test.name)
err := NewAlertContext(test.contextToSend, test.valueLength)
require.NoError(t, err)
metas, _ := EventToContext(test.events)
assert.ElementsMatch(t, test.expectedResult, metas)
}
}
func TestValidateContextExpr(t *testing.T) {
tests := []struct {
name string
key string
exprs []string
expectedErr *string
}{
{
name: "basic config",
key: "source_ip",
exprs: []string{
"evt.Parsed.source_ip",
},
expectedErr: nil,
},
{
name: "basic config with non existent field",
key: "source_ip",
exprs: []string{
"evt.invalid.source_ip",
},
expectedErr: ptr.Of("compilation of 'evt.invalid.source_ip' failed: type types.Event has no field invalid"),
},
}
for _, test := range tests {
fmt.Printf("Running test '%s'\n", test.name)
err := ValidateContextExpr(test.key, test.exprs)
if test.expectedErr == nil {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, *test.expectedErr)
}
}
}
func TestAppsecEventToContext(t *testing.T) {
tests := []struct {
name string
contextToSend map[string][]string
match types.AppsecEvent
req *http.Request
expectedResult models.Meta
expectedErrLen int
}{
{
name: "basic test on match",
contextToSend: map[string][]string{
"id": {"match.id"},
},
match: types.AppsecEvent{
MatchedRules: types.MatchedRules{
{
"id": "test",
},
},
},
req: &http.Request{},
expectedResult: []*models.MetaItems0{
{
Key: "id",
Value: "[\"test\"]",
},
},
expectedErrLen: 0,
},
{
name: "basic test on req",
contextToSend: map[string][]string{
"ua": {"req.UserAgent()"},
},
match: types.AppsecEvent{
MatchedRules: types.MatchedRules{
{
"id": "test",
},
},
},
req: &http.Request{
Header: map[string][]string{
"User-Agent": {"test"},
},
},
expectedResult: []*models.MetaItems0{
{
Key: "ua",
Value: "[\"test\"]",
},
},
expectedErrLen: 0,
},
{
name: "test on req -> []string",
contextToSend: map[string][]string{
"foobarxx": {"req.Header.Values('Foobar')"},
},
match: types.AppsecEvent{
MatchedRules: types.MatchedRules{
{
"id": "test",
},
},
},
req: &http.Request{
Header: map[string][]string{
"User-Agent": {"test"},
"Foobar": {"test1", "test2"},
},
},
expectedResult: []*models.MetaItems0{
{
Key: "foobarxx",
Value: "[\"test1\",\"test2\"]",
},
},
expectedErrLen: 0,
},
{
name: "test on type int",
contextToSend: map[string][]string{
"foobarxx": {"len(req.Header.Values('Foobar'))"},
},
match: types.AppsecEvent{
MatchedRules: types.MatchedRules{
{
"id": "test",
},
},
},
req: &http.Request{
Header: map[string][]string{
"User-Agent": {"test"},
"Foobar": {"test1", "test2"},
},
},
expectedResult: []*models.MetaItems0{
{
Key: "foobarxx",
Value: "[\"2\"]",
},
},
expectedErrLen: 0,
},
{
name: "test JA4H - appsec event",
contextToSend: map[string][]string{
"ja4h": {"JA4H(req)"},
},
match: types.AppsecEvent{
MatchedRules: types.MatchedRules{
{
"id": "test",
},
},
},
req: &http.Request{
Header: map[string][]string{
"User-Agent": {"test"},
"Foobar": {"test1", "test2"},
},
ProtoMajor: 1,
ProtoMinor: 1,
Method: http.MethodGet,
},
expectedResult: []*models.MetaItems0{
{
Key: "ja4h",
Value: "[\"ge11nn020000_3a31a0f8fbf9_000000000000_000000000000\"]",
},
},
},
{
name: "test JA4H - no appsec event",
contextToSend: map[string][]string{
"ja4h": {"JA4H(req)"},
},
req: nil,
expectedResult: []*models.MetaItems0{},
},
}
for _, test := range tests {
// reset cache
alertContext = Context{}
// compile
if err := NewAlertContext(test.contextToSend, 100); err != nil {
t.Fatalf("failed to compile %s: %s", test.name, err)
}
// run
metas, errors := AppsecEventToContext(test.match, test.req)
assert.Len(t, errors, test.expectedErrLen)
assert.ElementsMatch(t, test.expectedResult, metas)
}
}
func TestEvalAlertContextRules(t *testing.T) {
tests := []struct {
name string
contextToSend map[string][]string
event types.Event
match types.MatchedRule
req *http.Request
expectedResult map[string][]string
expectedErrLen int
}{
{
name: "no appsec match",
contextToSend: map[string][]string{
"source_ip": {"evt.Parsed.source_ip"},
"id": {"match.id"},
},
event: types.Event{
Parsed: map[string]string{
"source_ip": "1.2.3.4",
"source_machine": "mymachine",
"uri": "/test/test/test/../../../../../../../../",
},
},
expectedResult: map[string][]string{
"source_ip": {"1.2.3.4"},
"id": {},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
contextDict := make(map[string][]string)
alertContext = Context{}
if err := NewAlertContext(test.contextToSend, 100); err != nil {
t.Fatalf("failed to compile %s: %s", test.name, err)
}
errs := EvalAlertContextRules(test.event, &test.match, test.req, contextDict)
assert.Len(t, errs, test.expectedErrLen)
assert.Equal(t, test.expectedResult, contextDict)
})
}
}