Add duration expr to add duration formula (#1556)

* add duration expr to add duration formula
This commit is contained in:
he2ss 2022-06-22 11:29:52 +02:00 committed by GitHub
parent a6ed08b239
commit 3d6f015211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 748 additions and 216 deletions

View file

@ -10,7 +10,6 @@ import (
"github.com/crowdsecurity/crowdsec/pkg/acquisition"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
"github.com/crowdsecurity/crowdsec/pkg/parser"
"github.com/crowdsecurity/crowdsec/pkg/types"
@ -20,10 +19,7 @@ import (
)
func initCrowdsec(cConfig *csconfig.Config) (*parser.Parsers, error) {
err := exprhelpers.Init()
if err != nil {
return &parser.Parsers{}, fmt.Errorf("Failed to init expr helpers : %s", err)
}
var err error
// Populate cwhub package tools
if err := cwhub.GetHubIdx(cConfig.Hub); err != nil {

View file

@ -10,6 +10,8 @@ import (
"github.com/pkg/errors"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
"github.com/crowdsecurity/crowdsec/pkg/types"
log "github.com/sirupsen/logrus"
@ -163,12 +165,12 @@ func shutdownCrowdsec() error {
func shutdown(sig os.Signal, cConfig *csconfig.Config) error {
if !cConfig.DisableAgent {
if err := shutdownCrowdsec(); err != nil {
return errors.Wrap(err, "Failed to shut down crowdsec")
return errors.Wrap(err, "failed to shut down crowdsec")
}
}
if !cConfig.DisableAPI {
if err := shutdownAPI(); err != nil {
return errors.Wrap(err, "Failed to shut down api routines")
return errors.Wrap(err, "failed to shut down api routines")
}
}
return nil
@ -227,6 +229,24 @@ func Serve(cConfig *csconfig.Config) error {
apiTomb = tomb.Tomb{}
crowdsecTomb = tomb.Tomb{}
pluginTomb = tomb.Tomb{}
if cConfig.API.Server != nil && cConfig.API.Server.DbConfig != nil {
dbClient, err := database.NewClient(cConfig.API.Server.DbConfig)
if err != nil {
return errors.Wrap(err, "failed to get database client")
}
err = exprhelpers.Init(dbClient)
if err != nil {
return errors.Wrap(err, "failed to init expr helpers")
}
} else {
err := exprhelpers.Init(nil)
if err != nil {
return errors.Wrap(err, "failed to init expr helpers")
}
log.Warningln("Exprhelpers loaded without database client.")
}
if !cConfig.DisableAPI {
apiServer, err := initAPIServer(cConfig)
if err != nil {

View file

@ -5,6 +5,7 @@ filters:
decisions:
- type: ban
duration: 4h
#duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)
# notifications:
# - slack_default # Set the webhook in /etc/crowdsec/notifications/slack.yaml before enabling this.
# - splunk_default # Set the splunk url and token in /etc/crowdsec/notifications/splunk.yaml before enabling this.

View file

@ -61,7 +61,7 @@ func (c *Controller) NewV1() error {
v1Config := v1.ControllerV1Config{
DbClient: c.DBClient,
Ctx: c.Ectx,
Profiles: c.Profiles,
ProfilesCfg: c.Profiles,
CapiChan: c.CAPIChan,
PluginChannel: c.PluginChannel,
ConsoleConfig: *c.ConsoleConfig,

View file

@ -11,7 +11,6 @@ import (
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
"github.com/crowdsecurity/crowdsec/pkg/csprofiles"
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/gin-gonic/gin"
@ -135,7 +134,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
alert.MachineID = machineID
if len(alert.Decisions) != 0 {
for pIdx, profile := range c.Profiles {
_, matched, err := csprofiles.EvaluateProfile(profile, alert)
_, matched, err := profile.EvaluateProfile(alert)
if err != nil {
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
@ -144,7 +143,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
continue
}
c.sendAlertToPluginChannel(alert, uint(pIdx))
if profile.OnSuccess == "break" {
if profile.Cfg.OnSuccess == "break" {
break
}
}
@ -156,7 +155,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
}
for pIdx, profile := range c.Profiles {
profileDecisions, matched, err := csprofiles.EvaluateProfile(profile, alert)
profileDecisions, matched, err := profile.EvaluateProfile(alert)
if err != nil {
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
return
@ -171,7 +170,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
}
profileAlert := *alert
c.sendAlertToPluginChannel(&profileAlert, uint(pIdx))
if profile.OnSuccess == "break" {
if profile.Cfg.OnSuccess == "break" {
break
}
}

View file

@ -9,8 +9,10 @@ import (
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
"github.com/crowdsecurity/crowdsec/pkg/csprofiles"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/pkg/errors"
)
type Controller struct {
@ -18,7 +20,7 @@ type Controller struct {
DBClient *database.Client
APIKeyHeader string
Middlewares *middlewares.Middlewares
Profiles []*csconfig.ProfileCfg
Profiles []*csprofiles.Runtime
CAPIChan chan []*models.Alert
PluginChannel chan csplugin.ProfileAlert
ConsoleConfig csconfig.ConsoleConfig
@ -28,7 +30,7 @@ type Controller struct {
type ControllerV1Config struct {
DbClient *database.Client
Ctx context.Context
Profiles []*csconfig.ProfileCfg
ProfilesCfg []*csconfig.ProfileCfg
CapiChan chan []*models.Alert
PluginChannel chan csplugin.ProfileAlert
ConsoleConfig csconfig.ConsoleConfig
@ -37,11 +39,17 @@ type ControllerV1Config struct {
func New(cfg *ControllerV1Config) (*Controller, error) {
var err error
profiles, err := csprofiles.NewProfile(cfg.ProfilesCfg)
if err != nil {
return &Controller{}, errors.Wrapf(err, "failed to compile profiles")
}
v1 := &Controller{
Ectx: cfg.Ctx,
DBClient: cfg.DbClient,
APIKeyHeader: middlewares.APIKeyHeader,
Profiles: cfg.Profiles,
Profiles: profiles,
CAPIChan: cfg.CapiChan,
PluginChannel: cfg.PluginChannel,
ConsoleConfig: cfg.ConsoleConfig,

View file

@ -231,7 +231,7 @@ func TestLoadAPIServer(t *testing.T) {
err: "",
},
{
name: "basic valid configuration",
name: "basic invalid configuration",
Input: &Config{
Self: []byte(configData),
API: &APICfg{

View file

@ -4,15 +4,10 @@ import (
"bytes"
"fmt"
"io"
"time"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/yamlpatch"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
@ -21,9 +16,8 @@ type ProfileCfg struct {
Name string `yaml:"name,omitempty"`
Debug *bool `yaml:"debug,omitempty"`
Filters []string `yaml:"filters,omitempty"` //A list of OR'ed expressions. the models.Alert object
RuntimeFilters []*vm.Program `json:"-" yaml:"-"`
DebugFilters []*exprhelpers.ExprDebugger `json:"-" yaml:"-"`
Decisions []models.Decision `yaml:"decisions,omitempty"`
DurationExpr string `yaml:"duration_expr,omitempty"`
OnSuccess string `yaml:"on_success,omitempty"` //continue or break
OnFailure string `yaml:"on_failure,omitempty"` //continue or break
Notifications []string `yaml:"notifications,omitempty"`
@ -56,33 +50,6 @@ func (c *LocalApiServerCfg) LoadProfiles() error {
c.Profiles = append(c.Profiles, &t)
}
for pIdx, profile := range c.Profiles {
var runtimeFilter *vm.Program
var debugFilter *exprhelpers.ExprDebugger
c.Profiles[pIdx].RuntimeFilters = make([]*vm.Program, len(profile.Filters))
c.Profiles[pIdx].DebugFilters = make([]*exprhelpers.ExprDebugger, len(profile.Filters))
for fIdx, filter := range profile.Filters {
if runtimeFilter, err = expr.Compile(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}}))); err != nil {
return errors.Wrapf(err, "Error compiling filter of %s", profile.Name)
}
c.Profiles[pIdx].RuntimeFilters[fIdx] = runtimeFilter
if debugFilter, err = exprhelpers.NewDebugger(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}}))); err != nil {
log.Debugf("Error compiling debug filter of %s : %s", profile.Name, err)
// Don't fail if we can't compile the filter - for now
// return errors.Wrapf(err, "Error compiling debug filter of %s", profile.Name)
}
c.Profiles[pIdx].DebugFilters[fIdx] = debugFilter
}
for _, decision := range profile.Decisions {
if _, err := time.ParseDuration(*decision.Duration); err != nil {
return errors.Wrapf(err, "Error parsing duration '%s' of %s", *decision.Duration, profile.Name)
}
}
}
if len(c.Profiles) == 0 {
return fmt.Errorf("zero profiles loaded for LAPI")
}

View file

@ -29,3 +29,13 @@ decisions:
- type: ratatatata
duration: 1h
on_success: break
---
name: duration_expression
#debug: true
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 1h
duration_expr: sprintf('%dh', 4*4)
on_success: break

View file

@ -2,8 +2,10 @@ package csprofiles
import (
"fmt"
"time"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/models"
@ -12,10 +14,86 @@ import (
log "github.com/sirupsen/logrus"
)
func GenerateDecisionFromProfile(Profile *csconfig.ProfileCfg, Alert *models.Alert) ([]*models.Decision, error) {
type Runtime struct {
RuntimeFilters []*vm.Program `json:"-" yaml:"-"`
DebugFilters []*exprhelpers.ExprDebugger `json:"-" yaml:"-"`
RuntimeDurationExpr *vm.Program `json:"-" yaml:"-"`
DebugDurationExpr *exprhelpers.ExprDebugger `json:"-" yaml:"-"`
Cfg *csconfig.ProfileCfg `json:"-" yaml:"-"`
Logger *log.Entry `json:"-" yaml:"-"`
}
var defaultDuration = "4h"
func NewProfile(profilesCfg []*csconfig.ProfileCfg) ([]*Runtime, error) {
var err error
profilesRuntime := make([]*Runtime, 0)
for _, profile := range profilesCfg {
var runtimeFilter, runtimeDurationExpr *vm.Program
var debugFilter, debugDurationExpr *exprhelpers.ExprDebugger
runtime := &Runtime{}
xlog := log.New()
if err := types.ConfigureLogger(xlog); err != nil {
log.Fatalf("While creating profiles-specific logger : %s", err)
}
xlog.SetLevel(log.InfoLevel)
runtime.Logger = xlog.WithFields(log.Fields{
"type": "profile",
"name": profile.Name,
})
runtime.RuntimeFilters = make([]*vm.Program, len(profile.Filters))
runtime.DebugFilters = make([]*exprhelpers.ExprDebugger, len(profile.Filters))
runtime.Cfg = profile
for fIdx, filter := range profile.Filters {
if runtimeFilter, err = expr.Compile(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}}))); err != nil {
return []*Runtime{}, errors.Wrapf(err, "error compiling filter of '%s'", profile.Name)
}
runtime.RuntimeFilters[fIdx] = runtimeFilter
if profile.Debug != nil && *profile.Debug {
if debugFilter, err = exprhelpers.NewDebugger(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}}))); err != nil {
log.Debugf("Error compiling debug filter of %s : %s", profile.Name, err)
// Don't fail if we can't compile the filter - for now
// return errors.Wrapf(err, "Error compiling debug filter of %s", profile.Name)
}
runtime.DebugFilters[fIdx] = debugFilter
runtime.Logger.Logger.SetLevel(log.DebugLevel)
}
}
if profile.DurationExpr != "" {
if runtimeDurationExpr, err = expr.Compile(profile.DurationExpr, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}}))); err != nil {
return []*Runtime{}, errors.Wrapf(err, "error compiling duration_expr of %s", profile.Name)
}
runtime.RuntimeDurationExpr = runtimeDurationExpr
if profile.Debug != nil && *profile.Debug {
if debugDurationExpr, err = exprhelpers.NewDebugger(profile.DurationExpr, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}}))); err != nil {
log.Debugf("Error compiling debug duration_expr of %s : %s", profile.Name, err)
}
runtime.DebugDurationExpr = debugDurationExpr
}
}
for _, decision := range profile.Decisions {
if runtime.RuntimeDurationExpr == nil {
if _, err := time.ParseDuration(*decision.Duration); err != nil {
return []*Runtime{}, errors.Wrapf(err, "error parsing duration '%s' of %s", *decision.Duration, profile.Name)
}
}
}
profilesRuntime = append(profilesRuntime, runtime)
}
return profilesRuntime, nil
}
func (Profile *Runtime) GenerateDecisionFromProfile(Alert *models.Alert) ([]*models.Decision, error) {
var decisions []*models.Decision
for _, refDecision := range Profile.Decisions {
for _, refDecision := range Profile.Cfg.Decisions {
decision := models.Decision{}
/*the reference decision from profile is in sumulated mode */
if refDecision.Simulated != nil && *refDecision.Simulated {
@ -36,7 +114,27 @@ func GenerateDecisionFromProfile(Profile *csconfig.ProfileCfg, Alert *models.Ale
}
/*some fields are populated from the reference object : duration, scope, type*/
decision.Duration = new(string)
if Profile.Cfg.DurationExpr != "" && Profile.RuntimeDurationExpr != nil {
duration, err := expr.Run(Profile.RuntimeDurationExpr, exprhelpers.GetExprEnv(map[string]interface{}{"Alert": Alert}))
if err != nil {
Profile.Logger.Warningf("Failed to run duration_expr : %v", err)
*decision.Duration = *refDecision.Duration
} else {
durationStr := fmt.Sprint(duration)
if _, err := time.ParseDuration(durationStr); err != nil {
Profile.Logger.Warningf("Failed to parse expr duration result '%s'", duration)
*decision.Duration = *refDecision.Duration
} else {
*decision.Duration = durationStr
}
}
} else {
if refDecision.Duration == nil {
*decision.Duration = defaultDuration
}
*decision.Duration = *refDecision.Duration
}
decision.Type = new(string)
*decision.Type = *refDecision.Type
@ -55,54 +153,44 @@ func GenerateDecisionFromProfile(Profile *csconfig.ProfileCfg, Alert *models.Ale
return decisions, nil
}
var clog *log.Entry
//EvaluateProfile is going to evaluate an Alert against a profile to generate Decisions
func EvaluateProfile(profile *csconfig.ProfileCfg, Alert *models.Alert) ([]*models.Decision, bool, error) {
func (Profile *Runtime) EvaluateProfile(Alert *models.Alert) ([]*models.Decision, bool, error) {
var decisions []*models.Decision
if clog == nil {
xlog := log.New()
if err := types.ConfigureLogger(xlog); err != nil {
log.Fatalf("While creating profiles-specific logger : %s", err)
}
xlog.SetLevel(log.TraceLevel)
clog = xlog.WithFields(log.Fields{
"type": "profile",
})
}
matched := false
for eIdx, expression := range profile.RuntimeFilters {
for eIdx, expression := range Profile.RuntimeFilters {
output, err := expr.Run(expression, exprhelpers.GetExprEnv(map[string]interface{}{"Alert": Alert}))
if err != nil {
log.Warningf("failed to run whitelist expr : %v", err)
return nil, matched, errors.Wrapf(err, "while running expression %s", profile.Filters[eIdx])
Profile.Logger.Warningf("failed to run whitelist expr : %v", err)
return nil, matched, errors.Wrapf(err, "while running expression %s", Profile.Cfg.Filters[eIdx])
}
switch out := output.(type) {
case bool:
if profile.Debug != nil && *profile.Debug {
profile.DebugFilters[eIdx].Run(clog, out, exprhelpers.GetExprEnv(map[string]interface{}{"Alert": Alert}))
if Profile.Cfg.Debug != nil && *Profile.Cfg.Debug {
Profile.DebugFilters[eIdx].Run(Profile.Logger, out, exprhelpers.GetExprEnv(map[string]interface{}{"Alert": Alert}))
}
if out {
matched = true
/*the expression matched, create the associated decision*/
subdecisions, err := GenerateDecisionFromProfile(profile, Alert)
subdecisions, err := Profile.GenerateDecisionFromProfile(Alert)
if err != nil {
return nil, matched, errors.Wrapf(err, "while generating decision from profile %s", profile.Name)
return nil, matched, errors.Wrapf(err, "while generating decision from profile %s", Profile.Cfg.Name)
}
decisions = append(decisions, subdecisions...)
} else {
log.Debugf("Profile %s filter is unsuccessful", profile.Name)
if profile.OnFailure == "break" {
Profile.Logger.Debugf("Profile %s filter is unsuccessful", Profile.Cfg.Name)
if Profile.Cfg.OnFailure == "break" {
break
}
}
default:
return nil, matched, fmt.Errorf("unexpected type %t (%v) while running '%s'", output, output, profile.Filters[eIdx])
return nil, matched, fmt.Errorf("unexpected type %t (%v) while running '%s'", output, output, Profile.Cfg.Filters[eIdx])
}
}
return decisions, matched, nil
}

View file

@ -5,40 +5,117 @@ import (
"reflect"
"testing"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
"github.com/crowdsecurity/crowdsec/pkg/models"
"gotest.tools/v3/assert"
)
var (
scope = "Country"
typ = "ban"
simulated = false
boolFalse = false
boolTrue = true
duration = "1h"
value = "CH"
scenario = "ssh-bf"
)
func TestNewProfile(t *testing.T) {
tests := []struct {
name string
profileCfg *csconfig.ProfileCfg
expectedNbProfile int
}{
{
name: "filter ok and duration_expr ok",
profileCfg: &csconfig.ProfileCfg{
Filters: []string{
"1==1",
},
DurationExpr: "1==1",
Debug: &boolFalse,
Decisions: []models.Decision{
{Type: &typ, Scope: &scope, Simulated: &boolTrue, Duration: &duration},
},
},
expectedNbProfile: 1,
},
{
name: "filter NOK and duration_expr ok",
profileCfg: &csconfig.ProfileCfg{
Filters: []string{
"1==1",
"unknownExprHelper() == 'foo'",
},
DurationExpr: "1==1",
Debug: &boolFalse,
Decisions: []models.Decision{
{Type: &typ, Scope: &scope, Simulated: &boolFalse, Duration: &duration},
},
},
expectedNbProfile: 0,
},
{
name: "filter ok and duration_expr NOK",
profileCfg: &csconfig.ProfileCfg{
Filters: []string{
"1==1",
},
DurationExpr: "unknownExprHelper() == 'foo'",
Debug: &boolFalse,
Decisions: []models.Decision{
{Type: &typ, Scope: &scope, Simulated: &boolFalse, Duration: &duration},
},
},
expectedNbProfile: 0,
},
{
name: "filter ok and duration_expr ok + DEBUG",
profileCfg: &csconfig.ProfileCfg{
Filters: []string{
"1==1",
},
DurationExpr: "1==1",
Debug: &boolTrue,
Decisions: []models.Decision{
{Type: &typ, Scope: &scope, Simulated: &boolFalse, Duration: &duration},
},
},
expectedNbProfile: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
profilesCfg := []*csconfig.ProfileCfg{
test.profileCfg,
}
profile, _ := NewProfile(profilesCfg)
fmt.Printf("expected : %+v | result : %+v", test.expectedNbProfile, len(profile))
assert.Equal(t, test.expectedNbProfile, len(profile))
})
}
}
func TestEvaluateProfile(t *testing.T) {
type args struct {
profile *csconfig.ProfileCfg
profileCfg *csconfig.ProfileCfg
Alert *models.Alert
}
tests := []struct {
name string
args args
expectedDecisionCount int // count of expected decisions
expectedDuration string
expectedMatchStatus bool
}{
{
name: "simple pass single expr",
args: args{
profile: &csconfig.ProfileCfg{
profileCfg: &csconfig.ProfileCfg{
Filters: []string{fmt.Sprintf("Alert.GetScenario() == \"%s\"", scenario)},
RuntimeFilters: []*vm.Program{},
Debug: &boolFalse,
},
Alert: &models.Alert{Remediation: true, Scenario: &scenario},
},
@ -48,9 +125,8 @@ func TestEvaluateProfile(t *testing.T) {
{
name: "simple fail single expr",
args: args{
profile: &csconfig.ProfileCfg{
profileCfg: &csconfig.ProfileCfg{
Filters: []string{"Alert.GetScenario() == \"Foo\""},
RuntimeFilters: []*vm.Program{},
},
Alert: &models.Alert{Remediation: true},
},
@ -60,9 +136,8 @@ func TestEvaluateProfile(t *testing.T) {
{
name: "1 expr fail 1 expr pass should still eval to match",
args: args{
profile: &csconfig.ProfileCfg{
profileCfg: &csconfig.ProfileCfg{
Filters: []string{"1==1", "1!=1"},
RuntimeFilters: []*vm.Program{},
},
Alert: &models.Alert{Remediation: true},
},
@ -72,12 +147,11 @@ func TestEvaluateProfile(t *testing.T) {
{
name: "simple filter with 2 decision",
args: args{
profile: &csconfig.ProfileCfg{
profileCfg: &csconfig.ProfileCfg{
Filters: []string{"1==1"},
RuntimeFilters: []*vm.Program{},
Decisions: []models.Decision{
{Type: &typ, Scope: &scope, Simulated: &simulated, Duration: &duration},
{Type: &typ, Scope: &scope, Simulated: &simulated, Duration: &duration},
{Type: &typ, Scope: &scope, Simulated: &boolTrue, Duration: &duration},
{Type: &typ, Scope: &scope, Simulated: &boolFalse, Duration: &duration},
},
},
Alert: &models.Alert{Remediation: true, Scenario: &scenario, Source: &models.Source{Value: &value}},
@ -85,20 +159,42 @@ func TestEvaluateProfile(t *testing.T) {
expectedDecisionCount: 2,
expectedMatchStatus: true,
},
{
name: "simple filter with decision_expr",
args: args{
profileCfg: &csconfig.ProfileCfg{
Filters: []string{"1==1"},
Decisions: []models.Decision{
{Type: &typ, Scope: &scope, Simulated: &boolFalse},
},
DurationExpr: "Sprintf('%dh', 4*4)",
},
Alert: &models.Alert{Remediation: true, Scenario: &scenario, Source: &models.Source{Value: &value}},
},
expectedDecisionCount: 1,
expectedDuration: "16h",
expectedMatchStatus: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, filter := range tt.args.profile.Filters {
runtimeFilter, _ := expr.Compile(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}})))
tt.args.profile.RuntimeFilters = append(tt.args.profile.RuntimeFilters, runtimeFilter)
profilesCfg := []*csconfig.ProfileCfg{
tt.args.profileCfg,
}
got, got1, _ := EvaluateProfile(tt.args.profile, tt.args.Alert)
profile, err := NewProfile(profilesCfg)
if err != nil {
t.Errorf("failed to get newProfile : %+v", err)
}
got, got1, _ := profile[0].EvaluateProfile(tt.args.Alert)
if !reflect.DeepEqual(len(got), tt.expectedDecisionCount) {
t.Errorf("EvaluateProfile() got = %+v, want %+v", got, tt.expectedDecisionCount)
}
if got1 != tt.expectedMatchStatus {
t.Errorf("EvaluateProfile() got1 = %v, want %v", got1, tt.expectedMatchStatus)
}
if tt.expectedDuration != "" {
assert.Equal(t, tt.expectedDuration, *got[0].Duration, "The two durations should be the same")
}
})
}
}

View file

@ -188,7 +188,7 @@ func (c *Client) UpdateCommunityBlocklist(alertItem *models.Alert) (int, int, in
}
duration, err := time.ParseDuration(*decisionItem.Duration)
if err != nil {
return 0, 0, 0, errors.Wrapf(ParseDurationFail, "decision duration '%v' : %s", decisionItem.Duration, err)
return 0, 0, 0, errors.Wrapf(ParseDurationFail, "decision duration '%+v' : %s", *decisionItem.Duration, err)
}
if decisionItem.Scope == nil {
log.Warning("nil scope in community decision")
@ -425,7 +425,7 @@ func (c *Client) CreateAlertBulk(machineId string, alertList []*models.Alert) ([
duration, err := time.ParseDuration(*decisionItem.Duration)
if err != nil {
return []string{}, errors.Wrapf(ParseDurationFail, "decision duration '%v' : %s", decisionItem.Duration, err)
return []string{}, errors.Wrapf(ParseDurationFail, "decision duration '%+v' : %s", *decisionItem.Duration, err)
}
/*if the scope is IP or Range, convert the value to integers */

View file

@ -115,74 +115,9 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
}
}
if ip_sz == 4 {
if contains { /*decision contains {start_ip,end_ip}*/
query = query.Where(decision.And(
decision.StartIPLTE(start_ip),
decision.EndIPGTE(end_ip),
decision.IPSizeEQ(int64(ip_sz)),
))
} else { /*decision is contained within {start_ip,end_ip}*/
query = query.Where(decision.And(
decision.StartIPGTE(start_ip),
decision.EndIPLTE(end_ip),
decision.IPSizeEQ(int64(ip_sz)),
))
}
} else if ip_sz == 16 {
if contains { /*decision contains {start_ip,end_ip}*/
query = query.Where(decision.And(
//matching addr size
decision.IPSizeEQ(int64(ip_sz)),
decision.Or(
//decision.start_ip < query.start_ip
decision.StartIPLT(start_ip),
decision.And(
//decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip),
//decision.start_suffix <= query.start_suffix
decision.StartSuffixLTE(start_sfx),
)),
decision.Or(
//decision.end_ip > query.end_ip
decision.EndIPGT(end_ip),
decision.And(
//decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip),
//decision.end_suffix >= query.end_suffix
decision.EndSuffixGTE(end_sfx),
),
),
))
} else { /*decision is contained {start_ip,end_ip}*/
query = query.Where(decision.And(
//matching addr size
decision.IPSizeEQ(int64(ip_sz)),
decision.Or(
//decision.start_ip > query.start_ip
decision.StartIPGT(start_ip),
decision.And(
//decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip),
//decision.start_suffix >= query.start_suffix
decision.StartSuffixGTE(start_sfx),
)),
decision.Or(
//decision.end_ip < query.end_ip
decision.EndIPLT(end_ip),
decision.And(
//decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip),
//decision.end_suffix <= query.end_suffix
decision.EndSuffixLTE(end_sfx),
),
),
))
}
} else if ip_sz != 0 {
return nil, nil, errors.Wrapf(InvalidFilter, "Unknown ip size %d", ip_sz)
query, err = applyStartIpEndIpFilter(query, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx)
if err != nil {
return nil, nil, errors.Wrapf(err, "fail to apply StartIpEndIpFilter")
}
return query, joinPredicate, nil
}
@ -594,6 +529,133 @@ func (c *Client) SoftDeleteDecisionByID(decisionID int) (int, error) {
return nbUpdated, nil
}
func (c *Client) CountDecisionsByValue(decisionValue string) (int, error) {
var err error
var start_ip, start_sfx, end_ip, end_sfx int64
var ip_sz, count int
ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(decisionValue)
if err != nil {
return 0, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", decisionValue, err)
}
contains := true
decisions := c.Ent.Decision.Query()
decisions, err = applyStartIpEndIpFilter(decisions, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx)
if err != nil {
return 0, errors.Wrapf(err, "fail to apply StartIpEndIpFilter")
}
count, err = decisions.Count(c.CTX)
if err != nil {
return 0, errors.Wrapf(err, "fail to count decisions")
}
return count, nil
}
func (c *Client) CountDecisionsSinceByValue(decisionValue string, since time.Time) (int, error) {
var err error
var start_ip, start_sfx, end_ip, end_sfx int64
var ip_sz, count int
ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(decisionValue)
if err != nil {
return 0, errors.Wrapf(InvalidIPOrRange, "unable to convert '%s' to int: %s", decisionValue, err)
}
contains := true
decisions := c.Ent.Decision.Query().Where(
decision.CreatedAtGT(since),
decision.UntilGT(time.Now().UTC()),
)
decisions, err = applyStartIpEndIpFilter(decisions, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx)
if err != nil {
return 0, errors.Wrapf(err, "fail to apply StartIpEndIpFilter")
}
count, err = decisions.Count(c.CTX)
if err != nil {
return 0, errors.Wrapf(err, "fail to count decisions")
}
return count, nil
}
func applyStartIpEndIpFilter(decisions *ent.DecisionQuery, contains bool, ip_sz int, start_ip int64, start_sfx int64, end_ip int64, end_sfx int64) (*ent.DecisionQuery, error) {
if ip_sz == 4 {
if contains {
/*Decision contains {start_ip,end_ip}*/
decisions = decisions.Where(decision.And(
decision.StartIPLTE(start_ip),
decision.EndIPGTE(end_ip),
decision.IPSizeEQ(int64(ip_sz)),
))
} else {
/*Decision is contained within {start_ip,end_ip}*/
decisions = decisions.Where(decision.And(
decision.StartIPGTE(start_ip),
decision.EndIPLTE(end_ip),
decision.IPSizeEQ(int64(ip_sz)),
))
}
} else if ip_sz == 16 {
/*decision contains {start_ip,end_ip}*/
if contains {
decisions = decisions.Where(decision.And(
//matching addr size
decision.IPSizeEQ(int64(ip_sz)),
decision.Or(
//decision.start_ip < query.start_ip
decision.StartIPLT(start_ip),
decision.And(
//decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip),
//decision.start_suffix <= query.start_suffix
decision.StartSuffixLTE(start_sfx),
)),
decision.Or(
//decision.end_ip > query.end_ip
decision.EndIPGT(end_ip),
decision.And(
//decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip),
//decision.end_suffix >= query.end_suffix
decision.EndSuffixGTE(end_sfx),
),
),
))
} else {
/*decision is contained within {start_ip,end_ip}*/
decisions = decisions.Where(decision.And(
//matching addr size
decision.IPSizeEQ(int64(ip_sz)),
decision.Or(
//decision.start_ip > query.start_ip
decision.StartIPGT(start_ip),
decision.And(
//decision.start_ip == query.start_ip
decision.StartIPEQ(start_ip),
//decision.start_suffix >= query.start_suffix
decision.StartSuffixGTE(start_sfx),
)),
decision.Or(
//decision.end_ip < query.end_ip
decision.EndIPLT(end_ip),
decision.And(
//decision.end_ip == query.end_ip
decision.EndIPEQ(end_ip),
//decision.end_suffix <= query.end_suffix
decision.EndSuffixLTE(end_sfx),
),
),
))
}
} else if ip_sz != 0 {
return nil, errors.Wrapf(InvalidFilter, "unknown ip size %d", ip_sz)
}
return decisions, nil
}
func decisionPredicatesFromStr(s string, predicateFunc func(string) predicate.Decision) []predicate.Decision {
words := strings.Split(s, ",")
predicates := make([]predicate.Decision, len(words))

View file

@ -14,12 +14,14 @@ import (
"github.com/c-robinson/iplib"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/davecgh/go-spew/spew"
log "github.com/sirupsen/logrus"
)
var dataFile map[string][]string
var dataFileRegex map[string][]*regexp.Regexp
var dbClient *database.Client
func Atof(x string) float64 {
log.Debugf("debug atof %s", x)
@ -62,6 +64,9 @@ func GetExprEnv(ctx map[string]interface{}) map[string]interface{} {
"XMLGetNodeValue": XMLGetNodeValue,
"IpToRange": IpToRange,
"IsIPV6": IsIPV6,
"GetDecisionsCount": GetDecisionsCount,
"GetDecisionsSinceCount": GetDecisionsSinceCount,
"Sprintf": fmt.Sprintf,
}
for k, v := range ctx {
ExprLib[k] = v
@ -69,9 +74,10 @@ func GetExprEnv(ctx map[string]interface{}) map[string]interface{} {
return ExprLib
}
func Init() error {
func Init(databaseClient *database.Client) error {
dataFile = make(map[string][]string)
dataFileRegex = make(map[string][]*regexp.Regexp)
dbClient = databaseClient
return nil
}
@ -242,3 +248,35 @@ func KeyExists(key string, dict map[string]interface{}) bool {
_, ok := dict[key]
return ok
}
func GetDecisionsCount(value string) int {
if dbClient == nil {
log.Error("No database config to call GetDecisionsCount()")
return 0
}
count, err := dbClient.CountDecisionsByValue(value)
if err != nil {
log.Errorf("Failed to get decisions count from value '%s'", value)
return 0
}
return count
}
func GetDecisionsSinceCount(value string, since string) int {
if dbClient == nil {
log.Error("No database config to call GetDecisionsCount()")
return 0
}
sinceDuration, err := time.ParseDuration(since)
if err != nil {
log.Errorf("Failed to parse since parameter '%s' : %s", since, err)
return 0
}
sinceTime := time.Now().UTC().Add(-sinceDuration)
count, err := dbClient.CountDecisionsSinceByValue(value, sinceTime)
if err != nil {
log.Errorf("Failed to get decisions count from value '%s'", value)
return 0
}
return count
}

View file

@ -1,9 +1,17 @@
package exprhelpers
import (
"context"
"fmt"
"os"
"time"
"github.com/pkg/errors"
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
"github.com/crowdsecurity/crowdsec/pkg/database"
"github.com/crowdsecurity/crowdsec/pkg/models"
"github.com/crowdsecurity/crowdsec/pkg/types"
log "github.com/sirupsen/logrus"
"testing"
@ -17,8 +25,25 @@ var (
TestFolder = "tests"
)
func getDBClient(t *testing.T) *database.Client {
t.Helper()
dbPath, err := os.CreateTemp("", "*sqlite")
if err != nil {
t.Fatal(err)
}
testDbClient, err := database.NewClient(&csconfig.DatabaseCfg{
Type: "sqlite",
DbName: "crowdsec",
DbPath: dbPath.Name(),
})
if err != nil {
t.Fatal(err)
}
return testDbClient
}
func TestVisitor(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}
@ -105,7 +130,7 @@ func TestVisitor(t *testing.T) {
}
func TestRegexpInFile(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}
@ -162,7 +187,7 @@ func TestRegexpInFile(t *testing.T) {
}
func TestFileInit(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}
@ -230,7 +255,7 @@ func TestFileInit(t *testing.T) {
}
func TestFile(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}
@ -731,3 +756,218 @@ func TestLower(t *testing.T) {
log.Printf("test '%s' : OK", test.name)
}
}
func TestGetDecisionsCount(t *testing.T) {
var err error
var start_ip, start_sfx, end_ip, end_sfx int64
var ip_sz int
existingIP := "1.2.3.4"
unknownIP := "1.2.3.5"
ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(existingIP)
if err != nil {
t.Errorf("unable to convert '%s' to int: %s", existingIP, err)
}
// Add sample data to DB
dbClient = getDBClient(t)
decision := dbClient.Ent.Decision.Create().
SetUntil(time.Now().Add(time.Hour)).
SetScenario("crowdsec/test").
SetStartIP(start_ip).
SetStartSuffix(start_sfx).
SetEndIP(end_ip).
SetEndSuffix(end_sfx).
SetIPSize(int64(ip_sz)).
SetType("ban").
SetScope("IP").
SetValue(existingIP).
SetOrigin("CAPI").
SaveX(context.Background())
if decision == nil {
assert.Error(t, errors.Errorf("Failed to create sample decision"))
}
tests := []struct {
name string
env map[string]interface{}
code string
result string
err string
}{
{
name: "GetDecisionsCount() test: existing IP count",
env: map[string]interface{}{
"Alert": &models.Alert{
Source: &models.Source{
Value: &existingIP,
},
Decisions: []*models.Decision{
{
Value: &existingIP,
},
},
},
"GetDecisionsCount": GetDecisionsCount,
"sprintf": fmt.Sprintf,
},
code: "sprintf('%d', GetDecisionsCount(Alert.GetValue()))",
result: "1",
err: "",
},
{
name: "GetDecisionsCount() test: unknown IP count",
env: map[string]interface{}{
"Alert": &models.Alert{
Source: &models.Source{
Value: &unknownIP,
},
Decisions: []*models.Decision{
{
Value: &unknownIP,
},
},
},
"GetDecisionsCount": GetDecisionsCount,
"sprintf": fmt.Sprintf,
},
code: "sprintf('%d', GetDecisionsCount(Alert.GetValue()))",
result: "0",
err: "",
},
}
for _, test := range tests {
program, err := expr.Compile(test.code, expr.Env(GetExprEnv(test.env)))
require.NoError(t, err)
output, err := expr.Run(program, GetExprEnv(test.env))
require.NoError(t, err)
require.Equal(t, test.result, output)
log.Printf("test '%s' : OK", test.name)
}
}
func TestGetDecisionsSinceCount(t *testing.T) {
var err error
var start_ip, start_sfx, end_ip, end_sfx int64
var ip_sz int
existingIP := "1.2.3.4"
unknownIP := "1.2.3.5"
ip_sz, start_ip, start_sfx, end_ip, end_sfx, err = types.Addr2Ints(existingIP)
if err != nil {
t.Errorf("unable to convert '%s' to int: %s", existingIP, err)
}
// Add sample data to DB
dbClient = getDBClient(t)
decision := dbClient.Ent.Decision.Create().
SetUntil(time.Now().Add(time.Hour)).
SetScenario("crowdsec/test").
SetStartIP(start_ip).
SetStartSuffix(start_sfx).
SetEndIP(end_ip).
SetEndSuffix(end_sfx).
SetIPSize(int64(ip_sz)).
SetType("ban").
SetScope("IP").
SetValue(existingIP).
SetOrigin("CAPI").
SaveX(context.Background())
if decision == nil {
assert.Error(t, errors.Errorf("Failed to create sample decision"))
}
decision2 := dbClient.Ent.Decision.Create().
SetCreatedAt(time.Now().AddDate(0, 0, -1)).
SetUntil(time.Now().Add(time.Hour)).
SetScenario("crowdsec/test").
SetStartIP(start_ip).
SetStartSuffix(start_sfx).
SetEndIP(end_ip).
SetEndSuffix(end_sfx).
SetIPSize(int64(ip_sz)).
SetType("ban").
SetScope("IP").
SetValue(existingIP).
SetOrigin("CAPI").
SaveX(context.Background())
if decision2 == nil {
assert.Error(t, errors.Errorf("Failed to create sample decision"))
}
tests := []struct {
name string
env map[string]interface{}
code string
result string
err string
}{
{
name: "GetDecisionsSinceCount() test: existing IP count since more than 1 day",
env: map[string]interface{}{
"Alert": &models.Alert{
Source: &models.Source{
Value: &existingIP,
},
Decisions: []*models.Decision{
{
Value: &existingIP,
},
},
},
"GetDecisionsSinceCount": GetDecisionsSinceCount,
"sprintf": fmt.Sprintf,
},
code: "sprintf('%d', GetDecisionsSinceCount(Alert.GetValue(), '25h'))",
result: "2",
err: "",
},
{
name: "GetDecisionsSinceCount() test: existing IP count since more than 1 hour",
env: map[string]interface{}{
"Alert": &models.Alert{
Source: &models.Source{
Value: &existingIP,
},
Decisions: []*models.Decision{
{
Value: &existingIP,
},
},
},
"GetDecisionsSinceCount": GetDecisionsSinceCount,
"sprintf": fmt.Sprintf,
},
code: "sprintf('%d', GetDecisionsSinceCount(Alert.GetValue(), '1h'))",
result: "1",
err: "",
},
{
name: "GetDecisionsSinceCount() test: unknown IP count",
env: map[string]interface{}{
"Alert": &models.Alert{
Source: &models.Source{
Value: &unknownIP,
},
Decisions: []*models.Decision{
{
Value: &unknownIP,
},
},
},
"GetDecisionsSinceCount": GetDecisionsSinceCount,
"sprintf": fmt.Sprintf,
},
code: "sprintf('%d', GetDecisionsSinceCount(Alert.GetValue(), '1h'))",
result: "0",
err: "",
},
}
for _, test := range tests {
program, err := expr.Compile(test.code, expr.Env(GetExprEnv(test.env)))
require.NoError(t, err)
output, err := expr.Run(program, GetExprEnv(test.env))
require.NoError(t, err)
require.Equal(t, test.result, output)
log.Printf("test '%s' : OK", test.name)
}
}

View file

@ -8,7 +8,7 @@ import (
)
func TestJsonExtract(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}
@ -54,7 +54,7 @@ func TestJsonExtract(t *testing.T) {
}
func TestJsonExtractUnescape(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}
@ -94,7 +94,7 @@ func TestJsonExtractUnescape(t *testing.T) {
}
func TestJsonExtractSlice(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}
@ -144,7 +144,7 @@ func TestJsonExtractSlice(t *testing.T) {
}
func TestJsonExtractObject(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}

View file

@ -8,7 +8,7 @@ import (
)
func TestXMLGetAttributeValue(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}
@ -67,7 +67,7 @@ func TestXMLGetAttributeValue(t *testing.T) {
}
func TestXMLGetNodeValue(t *testing.T) {
if err := Init(); err != nil {
if err := Init(nil); err != nil {
log.Fatalf(err.Error())
}

View file

@ -33,7 +33,7 @@ func TestBucket(t *testing.T) {
envSetting = os.Getenv("TEST_ONLY")
tomb *tomb.Tomb = &tomb.Tomb{}
)
err := exprhelpers.Init()
err := exprhelpers.Init(nil)
if err != nil {
log.Fatalf("exprhelpers init failed: %s", err)
}

View file

@ -11,6 +11,13 @@ func (a *Alert) GetScope() string {
return *a.Source.Scope
}
func (a *Alert) GetValue() string {
if a.Source.Value == nil {
return ""
}
return *a.Source.Value
}
func (a *Alert) GetScenario() string {
if a.Scenario == nil {
return ""

View file

@ -58,7 +58,7 @@ type Node struct {
//Statics can be present in any type of node and is executed last
Statics []types.ExtraField `yaml:"statics,omitempty"`
//Whitelists
Whitelist types.Whitelist `yaml:"whitelist,omitempty"`
Whitelist Whitelist `yaml:"whitelist,omitempty"`
Data []*types.DataSource `yaml:"data,omitempty"`
}
@ -531,7 +531,7 @@ func (n *Node) compile(pctx *UnixParserCtx, ectx EnricherCtx) error {
valid = true
}
for _, filter := range n.Whitelist.Exprs {
expression := &types.ExprWhitelist{}
expression := &ExprWhitelist{}
expression.Filter, err = expr.Compile(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"evt": &types.Event{}})))
if err != nil {
n.Logger.Fatalf("Unable to compile whitelist expression '%s' : %v.", filter, err)

View file

@ -146,7 +146,7 @@ func prepTests() (*UnixParserCtx, EnricherCtx, error) {
ectx EnricherCtx
)
err = exprhelpers.Init()
err = exprhelpers.Init(nil)
if err != nil {
log.Fatalf("exprhelpers init failed: %s", err)
}

View file

@ -1,4 +1,4 @@
package types
package parser
import (
"net"