Add duration expr to add duration formula (#1556)
* add duration expr to add duration formula
This commit is contained in:
parent
a6ed08b239
commit
3d6f015211
22 changed files with 748 additions and 216 deletions
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/acquisition"
|
"github.com/crowdsecurity/crowdsec/pkg/acquisition"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
|
||||||
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
|
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/parser"
|
"github.com/crowdsecurity/crowdsec/pkg/parser"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||||
|
@ -20,10 +19,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func initCrowdsec(cConfig *csconfig.Config) (*parser.Parsers, error) {
|
func initCrowdsec(cConfig *csconfig.Config) (*parser.Parsers, error) {
|
||||||
err := exprhelpers.Init()
|
var err error
|
||||||
if err != nil {
|
|
||||||
return &parser.Parsers{}, fmt.Errorf("Failed to init expr helpers : %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate cwhub package tools
|
// Populate cwhub package tools
|
||||||
if err := cwhub.GetHubIdx(cConfig.Hub); err != nil {
|
if err := cwhub.GetHubIdx(cConfig.Hub); err != nil {
|
||||||
|
|
|
@ -10,6 +10,8 @@ import (
|
||||||
"github.com/pkg/errors"
|
"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/exprhelpers"
|
||||||
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
|
leaky "github.com/crowdsecurity/crowdsec/pkg/leakybucket"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -163,12 +165,12 @@ func shutdownCrowdsec() error {
|
||||||
func shutdown(sig os.Signal, cConfig *csconfig.Config) error {
|
func shutdown(sig os.Signal, cConfig *csconfig.Config) error {
|
||||||
if !cConfig.DisableAgent {
|
if !cConfig.DisableAgent {
|
||||||
if err := shutdownCrowdsec(); err != nil {
|
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 !cConfig.DisableAPI {
|
||||||
if err := shutdownAPI(); err != nil {
|
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
|
return nil
|
||||||
|
@ -227,6 +229,24 @@ func Serve(cConfig *csconfig.Config) error {
|
||||||
apiTomb = tomb.Tomb{}
|
apiTomb = tomb.Tomb{}
|
||||||
crowdsecTomb = tomb.Tomb{}
|
crowdsecTomb = tomb.Tomb{}
|
||||||
pluginTomb = 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 {
|
if !cConfig.DisableAPI {
|
||||||
apiServer, err := initAPIServer(cConfig)
|
apiServer, err := initAPIServer(cConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -5,6 +5,7 @@ filters:
|
||||||
decisions:
|
decisions:
|
||||||
- type: ban
|
- type: ban
|
||||||
duration: 4h
|
duration: 4h
|
||||||
|
#duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)
|
||||||
# notifications:
|
# notifications:
|
||||||
# - slack_default # Set the webhook in /etc/crowdsec/notifications/slack.yaml before enabling this.
|
# - 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.
|
# - splunk_default # Set the splunk url and token in /etc/crowdsec/notifications/splunk.yaml before enabling this.
|
||||||
|
|
|
@ -61,7 +61,7 @@ func (c *Controller) NewV1() error {
|
||||||
v1Config := v1.ControllerV1Config{
|
v1Config := v1.ControllerV1Config{
|
||||||
DbClient: c.DBClient,
|
DbClient: c.DBClient,
|
||||||
Ctx: c.Ectx,
|
Ctx: c.Ectx,
|
||||||
Profiles: c.Profiles,
|
ProfilesCfg: c.Profiles,
|
||||||
CapiChan: c.CAPIChan,
|
CapiChan: c.CAPIChan,
|
||||||
PluginChannel: c.PluginChannel,
|
PluginChannel: c.PluginChannel,
|
||||||
ConsoleConfig: *c.ConsoleConfig,
|
ConsoleConfig: *c.ConsoleConfig,
|
||||||
|
|
|
@ -11,7 +11,6 @@ import (
|
||||||
jwt "github.com/appleboy/gin-jwt/v2"
|
jwt "github.com/appleboy/gin-jwt/v2"
|
||||||
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
|
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csprofiles"
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
|
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -135,7 +134,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
|
||||||
alert.MachineID = machineID
|
alert.MachineID = machineID
|
||||||
if len(alert.Decisions) != 0 {
|
if len(alert.Decisions) != 0 {
|
||||||
for pIdx, profile := range c.Profiles {
|
for pIdx, profile := range c.Profiles {
|
||||||
_, matched, err := csprofiles.EvaluateProfile(profile, alert)
|
_, matched, err := profile.EvaluateProfile(alert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||||
return
|
return
|
||||||
|
@ -144,7 +143,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
c.sendAlertToPluginChannel(alert, uint(pIdx))
|
c.sendAlertToPluginChannel(alert, uint(pIdx))
|
||||||
if profile.OnSuccess == "break" {
|
if profile.Cfg.OnSuccess == "break" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,7 +155,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for pIdx, profile := range c.Profiles {
|
for pIdx, profile := range c.Profiles {
|
||||||
profileDecisions, matched, err := csprofiles.EvaluateProfile(profile, alert)
|
profileDecisions, matched, err := profile.EvaluateProfile(alert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
gctx.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()})
|
||||||
return
|
return
|
||||||
|
@ -171,7 +170,7 @@ func (c *Controller) CreateAlert(gctx *gin.Context) {
|
||||||
}
|
}
|
||||||
profileAlert := *alert
|
profileAlert := *alert
|
||||||
c.sendAlertToPluginChannel(&profileAlert, uint(pIdx))
|
c.sendAlertToPluginChannel(&profileAlert, uint(pIdx))
|
||||||
if profile.OnSuccess == "break" {
|
if profile.Cfg.OnSuccess == "break" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,10 @@ import (
|
||||||
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
|
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
|
"github.com/crowdsecurity/crowdsec/pkg/csplugin"
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/csprofiles"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/database"
|
"github.com/crowdsecurity/crowdsec/pkg/database"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
|
@ -18,7 +20,7 @@ type Controller struct {
|
||||||
DBClient *database.Client
|
DBClient *database.Client
|
||||||
APIKeyHeader string
|
APIKeyHeader string
|
||||||
Middlewares *middlewares.Middlewares
|
Middlewares *middlewares.Middlewares
|
||||||
Profiles []*csconfig.ProfileCfg
|
Profiles []*csprofiles.Runtime
|
||||||
CAPIChan chan []*models.Alert
|
CAPIChan chan []*models.Alert
|
||||||
PluginChannel chan csplugin.ProfileAlert
|
PluginChannel chan csplugin.ProfileAlert
|
||||||
ConsoleConfig csconfig.ConsoleConfig
|
ConsoleConfig csconfig.ConsoleConfig
|
||||||
|
@ -28,7 +30,7 @@ type Controller struct {
|
||||||
type ControllerV1Config struct {
|
type ControllerV1Config struct {
|
||||||
DbClient *database.Client
|
DbClient *database.Client
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
Profiles []*csconfig.ProfileCfg
|
ProfilesCfg []*csconfig.ProfileCfg
|
||||||
CapiChan chan []*models.Alert
|
CapiChan chan []*models.Alert
|
||||||
PluginChannel chan csplugin.ProfileAlert
|
PluginChannel chan csplugin.ProfileAlert
|
||||||
ConsoleConfig csconfig.ConsoleConfig
|
ConsoleConfig csconfig.ConsoleConfig
|
||||||
|
@ -37,11 +39,17 @@ type ControllerV1Config struct {
|
||||||
|
|
||||||
func New(cfg *ControllerV1Config) (*Controller, error) {
|
func New(cfg *ControllerV1Config) (*Controller, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
profiles, err := csprofiles.NewProfile(cfg.ProfilesCfg)
|
||||||
|
if err != nil {
|
||||||
|
return &Controller{}, errors.Wrapf(err, "failed to compile profiles")
|
||||||
|
}
|
||||||
|
|
||||||
v1 := &Controller{
|
v1 := &Controller{
|
||||||
Ectx: cfg.Ctx,
|
Ectx: cfg.Ctx,
|
||||||
DBClient: cfg.DbClient,
|
DBClient: cfg.DbClient,
|
||||||
APIKeyHeader: middlewares.APIKeyHeader,
|
APIKeyHeader: middlewares.APIKeyHeader,
|
||||||
Profiles: cfg.Profiles,
|
Profiles: profiles,
|
||||||
CAPIChan: cfg.CapiChan,
|
CAPIChan: cfg.CapiChan,
|
||||||
PluginChannel: cfg.PluginChannel,
|
PluginChannel: cfg.PluginChannel,
|
||||||
ConsoleConfig: cfg.ConsoleConfig,
|
ConsoleConfig: cfg.ConsoleConfig,
|
||||||
|
|
|
@ -231,7 +231,7 @@ func TestLoadAPIServer(t *testing.T) {
|
||||||
err: "",
|
err: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "basic valid configuration",
|
name: "basic invalid configuration",
|
||||||
Input: &Config{
|
Input: &Config{
|
||||||
Self: []byte(configData),
|
Self: []byte(configData),
|
||||||
API: &APICfg{
|
API: &APICfg{
|
||||||
|
|
|
@ -4,29 +4,23 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"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/models"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/yamlpatch"
|
"github.com/crowdsecurity/crowdsec/pkg/yamlpatch"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
//Profile structure(s) are used by the local API to "decide" what kind of decision should be applied when a scenario with an active remediation has been triggered
|
//Profile structure(s) are used by the local API to "decide" what kind of decision should be applied when a scenario with an active remediation has been triggered
|
||||||
type ProfileCfg struct {
|
type ProfileCfg struct {
|
||||||
Name string `yaml:"name,omitempty"`
|
Name string `yaml:"name,omitempty"`
|
||||||
Debug *bool `yaml:"debug,omitempty"`
|
Debug *bool `yaml:"debug,omitempty"`
|
||||||
Filters []string `yaml:"filters,omitempty"` //A list of OR'ed expressions. the models.Alert object
|
Filters []string `yaml:"filters,omitempty"` //A list of OR'ed expressions. the models.Alert object
|
||||||
RuntimeFilters []*vm.Program `json:"-" yaml:"-"`
|
Decisions []models.Decision `yaml:"decisions,omitempty"`
|
||||||
DebugFilters []*exprhelpers.ExprDebugger `json:"-" yaml:"-"`
|
DurationExpr string `yaml:"duration_expr,omitempty"`
|
||||||
Decisions []models.Decision `yaml:"decisions,omitempty"`
|
OnSuccess string `yaml:"on_success,omitempty"` //continue or break
|
||||||
OnSuccess string `yaml:"on_success,omitempty"` //continue or break
|
OnFailure string `yaml:"on_failure,omitempty"` //continue or break
|
||||||
OnFailure string `yaml:"on_failure,omitempty"` //continue or break
|
Notifications []string `yaml:"notifications,omitempty"`
|
||||||
Notifications []string `yaml:"notifications,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LocalApiServerCfg) LoadProfiles() error {
|
func (c *LocalApiServerCfg) LoadProfiles() error {
|
||||||
|
@ -56,33 +50,6 @@ func (c *LocalApiServerCfg) LoadProfiles() error {
|
||||||
c.Profiles = append(c.Profiles, &t)
|
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 {
|
if len(c.Profiles) == 0 {
|
||||||
return fmt.Errorf("zero profiles loaded for LAPI")
|
return fmt.Errorf("zero profiles loaded for LAPI")
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,3 +29,13 @@ decisions:
|
||||||
- type: ratatatata
|
- type: ratatatata
|
||||||
duration: 1h
|
duration: 1h
|
||||||
on_success: break
|
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
|
||||||
|
|
|
@ -2,8 +2,10 @@ package csprofiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/antonmedv/expr"
|
"github.com/antonmedv/expr"
|
||||||
|
"github.com/antonmedv/expr/vm"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||||
|
@ -12,10 +14,86 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
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
|
var decisions []*models.Decision
|
||||||
|
|
||||||
for _, refDecision := range Profile.Decisions {
|
for _, refDecision := range Profile.Cfg.Decisions {
|
||||||
decision := models.Decision{}
|
decision := models.Decision{}
|
||||||
/*the reference decision from profile is in sumulated mode */
|
/*the reference decision from profile is in sumulated mode */
|
||||||
if refDecision.Simulated != nil && *refDecision.Simulated {
|
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*/
|
/*some fields are populated from the reference object : duration, scope, type*/
|
||||||
decision.Duration = new(string)
|
decision.Duration = new(string)
|
||||||
*decision.Duration = *refDecision.Duration
|
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 = new(string)
|
||||||
*decision.Type = *refDecision.Type
|
*decision.Type = *refDecision.Type
|
||||||
|
|
||||||
|
@ -55,54 +153,44 @@ func GenerateDecisionFromProfile(Profile *csconfig.ProfileCfg, Alert *models.Ale
|
||||||
return decisions, nil
|
return decisions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var clog *log.Entry
|
|
||||||
|
|
||||||
//EvaluateProfile is going to evaluate an Alert against a profile to generate Decisions
|
//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
|
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
|
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}))
|
output, err := expr.Run(expression, exprhelpers.GetExprEnv(map[string]interface{}{"Alert": Alert}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warningf("failed to run whitelist expr : %v", err)
|
Profile.Logger.Warningf("failed to run whitelist expr : %v", err)
|
||||||
return nil, matched, errors.Wrapf(err, "while running expression %s", profile.Filters[eIdx])
|
return nil, matched, errors.Wrapf(err, "while running expression %s", Profile.Cfg.Filters[eIdx])
|
||||||
}
|
}
|
||||||
switch out := output.(type) {
|
switch out := output.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
if profile.Debug != nil && *profile.Debug {
|
if Profile.Cfg.Debug != nil && *Profile.Cfg.Debug {
|
||||||
profile.DebugFilters[eIdx].Run(clog, out, exprhelpers.GetExprEnv(map[string]interface{}{"Alert": Alert}))
|
Profile.DebugFilters[eIdx].Run(Profile.Logger, out, exprhelpers.GetExprEnv(map[string]interface{}{"Alert": Alert}))
|
||||||
}
|
}
|
||||||
if out {
|
if out {
|
||||||
matched = true
|
matched = true
|
||||||
/*the expression matched, create the associated decision*/
|
/*the expression matched, create the associated decision*/
|
||||||
subdecisions, err := GenerateDecisionFromProfile(profile, Alert)
|
subdecisions, err := Profile.GenerateDecisionFromProfile(Alert)
|
||||||
if err != nil {
|
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...)
|
decisions = append(decisions, subdecisions...)
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("Profile %s filter is unsuccessful", profile.Name)
|
Profile.Logger.Debugf("Profile %s filter is unsuccessful", Profile.Cfg.Name)
|
||||||
if profile.OnFailure == "break" {
|
if Profile.Cfg.OnFailure == "break" {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
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
|
return decisions, matched, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,40 +5,117 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/antonmedv/expr"
|
|
||||||
"github.com/antonmedv/expr/vm"
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
|
||||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
scope = "Country"
|
scope = "Country"
|
||||||
typ = "ban"
|
typ = "ban"
|
||||||
simulated = false
|
boolFalse = false
|
||||||
|
boolTrue = true
|
||||||
duration = "1h"
|
duration = "1h"
|
||||||
|
|
||||||
value = "CH"
|
value = "CH"
|
||||||
scenario = "ssh-bf"
|
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) {
|
func TestEvaluateProfile(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
profile *csconfig.ProfileCfg
|
profileCfg *csconfig.ProfileCfg
|
||||||
Alert *models.Alert
|
Alert *models.Alert
|
||||||
}
|
}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
args args
|
args args
|
||||||
expectedDecisionCount int // count of expected decisions
|
expectedDecisionCount int // count of expected decisions
|
||||||
|
expectedDuration string
|
||||||
expectedMatchStatus bool
|
expectedMatchStatus bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "simple pass single expr",
|
name: "simple pass single expr",
|
||||||
args: args{
|
args: args{
|
||||||
profile: &csconfig.ProfileCfg{
|
profileCfg: &csconfig.ProfileCfg{
|
||||||
Filters: []string{fmt.Sprintf("Alert.GetScenario() == \"%s\"", scenario)},
|
Filters: []string{fmt.Sprintf("Alert.GetScenario() == \"%s\"", scenario)},
|
||||||
RuntimeFilters: []*vm.Program{},
|
Debug: &boolFalse,
|
||||||
},
|
},
|
||||||
Alert: &models.Alert{Remediation: true, Scenario: &scenario},
|
Alert: &models.Alert{Remediation: true, Scenario: &scenario},
|
||||||
},
|
},
|
||||||
|
@ -48,9 +125,8 @@ func TestEvaluateProfile(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "simple fail single expr",
|
name: "simple fail single expr",
|
||||||
args: args{
|
args: args{
|
||||||
profile: &csconfig.ProfileCfg{
|
profileCfg: &csconfig.ProfileCfg{
|
||||||
Filters: []string{"Alert.GetScenario() == \"Foo\""},
|
Filters: []string{"Alert.GetScenario() == \"Foo\""},
|
||||||
RuntimeFilters: []*vm.Program{},
|
|
||||||
},
|
},
|
||||||
Alert: &models.Alert{Remediation: true},
|
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",
|
name: "1 expr fail 1 expr pass should still eval to match",
|
||||||
args: args{
|
args: args{
|
||||||
profile: &csconfig.ProfileCfg{
|
profileCfg: &csconfig.ProfileCfg{
|
||||||
Filters: []string{"1==1", "1!=1"},
|
Filters: []string{"1==1", "1!=1"},
|
||||||
RuntimeFilters: []*vm.Program{},
|
|
||||||
},
|
},
|
||||||
Alert: &models.Alert{Remediation: true},
|
Alert: &models.Alert{Remediation: true},
|
||||||
},
|
},
|
||||||
|
@ -72,12 +147,11 @@ func TestEvaluateProfile(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "simple filter with 2 decision",
|
name: "simple filter with 2 decision",
|
||||||
args: args{
|
args: args{
|
||||||
profile: &csconfig.ProfileCfg{
|
profileCfg: &csconfig.ProfileCfg{
|
||||||
Filters: []string{"1==1"},
|
Filters: []string{"1==1"},
|
||||||
RuntimeFilters: []*vm.Program{},
|
|
||||||
Decisions: []models.Decision{
|
Decisions: []models.Decision{
|
||||||
{Type: &typ, Scope: &scope, Simulated: &simulated, Duration: &duration},
|
{Type: &typ, Scope: &scope, Simulated: &boolTrue, Duration: &duration},
|
||||||
{Type: &typ, Scope: &scope, Simulated: &simulated, Duration: &duration},
|
{Type: &typ, Scope: &scope, Simulated: &boolFalse, Duration: &duration},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Alert: &models.Alert{Remediation: true, Scenario: &scenario, Source: &models.Source{Value: &value}},
|
Alert: &models.Alert{Remediation: true, Scenario: &scenario, Source: &models.Source{Value: &value}},
|
||||||
|
@ -85,20 +159,42 @@ func TestEvaluateProfile(t *testing.T) {
|
||||||
expectedDecisionCount: 2,
|
expectedDecisionCount: 2,
|
||||||
expectedMatchStatus: true,
|
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 {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
for _, filter := range tt.args.profile.Filters {
|
profilesCfg := []*csconfig.ProfileCfg{
|
||||||
runtimeFilter, _ := expr.Compile(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"Alert": &models.Alert{}})))
|
tt.args.profileCfg,
|
||||||
tt.args.profile.RuntimeFilters = append(tt.args.profile.RuntimeFilters, runtimeFilter)
|
|
||||||
}
|
}
|
||||||
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) {
|
if !reflect.DeepEqual(len(got), tt.expectedDecisionCount) {
|
||||||
t.Errorf("EvaluateProfile() got = %+v, want %+v", got, tt.expectedDecisionCount)
|
t.Errorf("EvaluateProfile() got = %+v, want %+v", got, tt.expectedDecisionCount)
|
||||||
}
|
}
|
||||||
if got1 != tt.expectedMatchStatus {
|
if got1 != tt.expectedMatchStatus {
|
||||||
t.Errorf("EvaluateProfile() got1 = %v, want %v", 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")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -188,7 +188,7 @@ func (c *Client) UpdateCommunityBlocklist(alertItem *models.Alert) (int, int, in
|
||||||
}
|
}
|
||||||
duration, err := time.ParseDuration(*decisionItem.Duration)
|
duration, err := time.ParseDuration(*decisionItem.Duration)
|
||||||
if err != nil {
|
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 {
|
if decisionItem.Scope == nil {
|
||||||
log.Warning("nil scope in community decision")
|
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)
|
duration, err := time.ParseDuration(*decisionItem.Duration)
|
||||||
if err != nil {
|
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 */
|
/*if the scope is IP or Range, convert the value to integers */
|
||||||
|
|
|
@ -115,74 +115,9 @@ func BuildDecisionRequestWithFilter(query *ent.DecisionQuery, filter map[string]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip_sz == 4 {
|
query, err = applyStartIpEndIpFilter(query, contains, ip_sz, start_ip, start_sfx, end_ip, end_sfx)
|
||||||
|
if err != nil {
|
||||||
if contains { /*decision contains {start_ip,end_ip}*/
|
return nil, nil, errors.Wrapf(err, "fail to apply StartIpEndIpFilter")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
return query, joinPredicate, nil
|
return query, joinPredicate, nil
|
||||||
}
|
}
|
||||||
|
@ -594,6 +529,133 @@ func (c *Client) SoftDeleteDecisionByID(decisionID int) (int, error) {
|
||||||
return nbUpdated, nil
|
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 {
|
func decisionPredicatesFromStr(s string, predicateFunc func(string) predicate.Decision) []predicate.Decision {
|
||||||
words := strings.Split(s, ",")
|
words := strings.Split(s, ",")
|
||||||
predicates := make([]predicate.Decision, len(words))
|
predicates := make([]predicate.Decision, len(words))
|
||||||
|
|
|
@ -14,12 +14,14 @@ import (
|
||||||
|
|
||||||
"github.com/c-robinson/iplib"
|
"github.com/c-robinson/iplib"
|
||||||
|
|
||||||
|
"github.com/crowdsecurity/crowdsec/pkg/database"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dataFile map[string][]string
|
var dataFile map[string][]string
|
||||||
var dataFileRegex map[string][]*regexp.Regexp
|
var dataFileRegex map[string][]*regexp.Regexp
|
||||||
|
var dbClient *database.Client
|
||||||
|
|
||||||
func Atof(x string) float64 {
|
func Atof(x string) float64 {
|
||||||
log.Debugf("debug atof %s", x)
|
log.Debugf("debug atof %s", x)
|
||||||
|
@ -40,28 +42,31 @@ func Lower(s string) string {
|
||||||
|
|
||||||
func GetExprEnv(ctx map[string]interface{}) map[string]interface{} {
|
func GetExprEnv(ctx map[string]interface{}) map[string]interface{} {
|
||||||
var ExprLib = map[string]interface{}{
|
var ExprLib = map[string]interface{}{
|
||||||
"Atof": Atof,
|
"Atof": Atof,
|
||||||
"JsonExtract": JsonExtract,
|
"JsonExtract": JsonExtract,
|
||||||
"JsonExtractUnescape": JsonExtractUnescape,
|
"JsonExtractUnescape": JsonExtractUnescape,
|
||||||
"JsonExtractLib": JsonExtractLib,
|
"JsonExtractLib": JsonExtractLib,
|
||||||
"JsonExtractSlice": JsonExtractSlice,
|
"JsonExtractSlice": JsonExtractSlice,
|
||||||
"JsonExtractObject": JsonExtractObject,
|
"JsonExtractObject": JsonExtractObject,
|
||||||
"ToJsonString": ToJson,
|
"ToJsonString": ToJson,
|
||||||
"File": File,
|
"File": File,
|
||||||
"RegexpInFile": RegexpInFile,
|
"RegexpInFile": RegexpInFile,
|
||||||
"Upper": Upper,
|
"Upper": Upper,
|
||||||
"Lower": Lower,
|
"Lower": Lower,
|
||||||
"IpInRange": IpInRange,
|
"IpInRange": IpInRange,
|
||||||
"TimeNow": TimeNow,
|
"TimeNow": TimeNow,
|
||||||
"ParseUri": ParseUri,
|
"ParseUri": ParseUri,
|
||||||
"PathUnescape": PathUnescape,
|
"PathUnescape": PathUnescape,
|
||||||
"QueryUnescape": QueryUnescape,
|
"QueryUnescape": QueryUnescape,
|
||||||
"PathEscape": PathEscape,
|
"PathEscape": PathEscape,
|
||||||
"QueryEscape": QueryEscape,
|
"QueryEscape": QueryEscape,
|
||||||
"XMLGetAttributeValue": XMLGetAttributeValue,
|
"XMLGetAttributeValue": XMLGetAttributeValue,
|
||||||
"XMLGetNodeValue": XMLGetNodeValue,
|
"XMLGetNodeValue": XMLGetNodeValue,
|
||||||
"IpToRange": IpToRange,
|
"IpToRange": IpToRange,
|
||||||
"IsIPV6": IsIPV6,
|
"IsIPV6": IsIPV6,
|
||||||
|
"GetDecisionsCount": GetDecisionsCount,
|
||||||
|
"GetDecisionsSinceCount": GetDecisionsSinceCount,
|
||||||
|
"Sprintf": fmt.Sprintf,
|
||||||
}
|
}
|
||||||
for k, v := range ctx {
|
for k, v := range ctx {
|
||||||
ExprLib[k] = v
|
ExprLib[k] = v
|
||||||
|
@ -69,9 +74,10 @@ func GetExprEnv(ctx map[string]interface{}) map[string]interface{} {
|
||||||
return ExprLib
|
return ExprLib
|
||||||
}
|
}
|
||||||
|
|
||||||
func Init() error {
|
func Init(databaseClient *database.Client) error {
|
||||||
dataFile = make(map[string][]string)
|
dataFile = make(map[string][]string)
|
||||||
dataFileRegex = make(map[string][]*regexp.Regexp)
|
dataFileRegex = make(map[string][]*regexp.Regexp)
|
||||||
|
dbClient = databaseClient
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,3 +248,35 @@ func KeyExists(key string, dict map[string]interface{}) bool {
|
||||||
_, ok := dict[key]
|
_, ok := dict[key]
|
||||||
return ok
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
package exprhelpers
|
package exprhelpers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"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"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -17,8 +25,25 @@ var (
|
||||||
TestFolder = "tests"
|
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) {
|
func TestVisitor(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,7 +130,7 @@ func TestVisitor(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegexpInFile(t *testing.T) {
|
func TestRegexpInFile(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,7 +187,7 @@ func TestRegexpInFile(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFileInit(t *testing.T) {
|
func TestFileInit(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,7 +255,7 @@ func TestFileInit(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFile(t *testing.T) {
|
func TestFile(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -731,3 +756,218 @@ func TestLower(t *testing.T) {
|
||||||
log.Printf("test '%s' : OK", test.name)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestJsonExtract(t *testing.T) {
|
func TestJsonExtract(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ func TestJsonExtract(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
func TestJsonExtractUnescape(t *testing.T) {
|
func TestJsonExtractUnescape(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ func TestJsonExtractUnescape(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJsonExtractSlice(t *testing.T) {
|
func TestJsonExtractSlice(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,7 +144,7 @@ func TestJsonExtractSlice(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJsonExtractObject(t *testing.T) {
|
func TestJsonExtractObject(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestXMLGetAttributeValue(t *testing.T) {
|
func TestXMLGetAttributeValue(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ func TestXMLGetAttributeValue(t *testing.T) {
|
||||||
|
|
||||||
}
|
}
|
||||||
func TestXMLGetNodeValue(t *testing.T) {
|
func TestXMLGetNodeValue(t *testing.T) {
|
||||||
if err := Init(); err != nil {
|
if err := Init(nil); err != nil {
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ func TestBucket(t *testing.T) {
|
||||||
envSetting = os.Getenv("TEST_ONLY")
|
envSetting = os.Getenv("TEST_ONLY")
|
||||||
tomb *tomb.Tomb = &tomb.Tomb{}
|
tomb *tomb.Tomb = &tomb.Tomb{}
|
||||||
)
|
)
|
||||||
err := exprhelpers.Init()
|
err := exprhelpers.Init(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("exprhelpers init failed: %s", err)
|
log.Fatalf("exprhelpers init failed: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,13 @@ func (a *Alert) GetScope() string {
|
||||||
return *a.Source.Scope
|
return *a.Source.Scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Alert) GetValue() string {
|
||||||
|
if a.Source.Value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return *a.Source.Value
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Alert) GetScenario() string {
|
func (a *Alert) GetScenario() string {
|
||||||
if a.Scenario == nil {
|
if a.Scenario == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -58,7 +58,7 @@ type Node struct {
|
||||||
//Statics can be present in any type of node and is executed last
|
//Statics can be present in any type of node and is executed last
|
||||||
Statics []types.ExtraField `yaml:"statics,omitempty"`
|
Statics []types.ExtraField `yaml:"statics,omitempty"`
|
||||||
//Whitelists
|
//Whitelists
|
||||||
Whitelist types.Whitelist `yaml:"whitelist,omitempty"`
|
Whitelist Whitelist `yaml:"whitelist,omitempty"`
|
||||||
Data []*types.DataSource `yaml:"data,omitempty"`
|
Data []*types.DataSource `yaml:"data,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,7 +531,7 @@ func (n *Node) compile(pctx *UnixParserCtx, ectx EnricherCtx) error {
|
||||||
valid = true
|
valid = true
|
||||||
}
|
}
|
||||||
for _, filter := range n.Whitelist.Exprs {
|
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{}})))
|
expression.Filter, err = expr.Compile(filter, expr.Env(exprhelpers.GetExprEnv(map[string]interface{}{"evt": &types.Event{}})))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.Logger.Fatalf("Unable to compile whitelist expression '%s' : %v.", filter, err)
|
n.Logger.Fatalf("Unable to compile whitelist expression '%s' : %v.", filter, err)
|
||||||
|
|
|
@ -146,7 +146,7 @@ func prepTests() (*UnixParserCtx, EnricherCtx, error) {
|
||||||
ectx EnricherCtx
|
ectx EnricherCtx
|
||||||
)
|
)
|
||||||
|
|
||||||
err = exprhelpers.Init()
|
err = exprhelpers.Init(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("exprhelpers init failed: %s", err)
|
log.Fatalf("exprhelpers init failed: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package types
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
Loading…
Reference in a new issue