733f5e165b
* return nil with errors * errors.Wrap -> fmt.Errorf * var -> const * fix default decision duration * lint (whitespace)
210 lines
6.8 KiB
Go
210 lines
6.8 KiB
Go
package csprofiles
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/antonmedv/expr"
|
|
"github.com/antonmedv/expr/vm"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
|
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
|
"github.com/crowdsecurity/crowdsec/pkg/models"
|
|
"github.com/crowdsecurity/crowdsec/pkg/types"
|
|
)
|
|
|
|
type Runtime struct {
|
|
RuntimeFilters []*vm.Program `json:"-" yaml:"-"`
|
|
RuntimeDurationExpr *vm.Program `json:"-" yaml:"-"`
|
|
Cfg *csconfig.ProfileCfg `json:"-" yaml:"-"`
|
|
Logger *log.Entry `json:"-" yaml:"-"`
|
|
}
|
|
|
|
const 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
|
|
|
|
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.Cfg = profile
|
|
|
|
if runtime.Cfg.OnSuccess != "" && runtime.Cfg.OnSuccess != "continue" && runtime.Cfg.OnSuccess != "break" {
|
|
return nil, fmt.Errorf("invalid 'on_success' for '%s': %s", profile.Name, runtime.Cfg.OnSuccess)
|
|
}
|
|
|
|
if runtime.Cfg.OnFailure != "" && runtime.Cfg.OnFailure != "continue" && runtime.Cfg.OnFailure != "break" && runtime.Cfg.OnFailure != "apply" {
|
|
return nil, fmt.Errorf("invalid 'on_failure' for '%s' : %s", profile.Name, runtime.Cfg.OnFailure)
|
|
}
|
|
|
|
for fIdx, filter := range profile.Filters {
|
|
if runtimeFilter, err = expr.Compile(filter, exprhelpers.GetExprOptions(map[string]interface{}{"Alert": &models.Alert{}})...); err != nil {
|
|
return nil, fmt.Errorf("error compiling filter of '%s': %w", profile.Name, err)
|
|
}
|
|
|
|
runtime.RuntimeFilters[fIdx] = runtimeFilter
|
|
if profile.Debug != nil && *profile.Debug {
|
|
runtime.Logger.Logger.SetLevel(log.DebugLevel)
|
|
}
|
|
}
|
|
|
|
if profile.DurationExpr != "" {
|
|
if runtimeDurationExpr, err = expr.Compile(profile.DurationExpr, exprhelpers.GetExprOptions(map[string]interface{}{"Alert": &models.Alert{}})...); err != nil {
|
|
return nil, fmt.Errorf("error compiling duration_expr of %s: %w", profile.Name, err)
|
|
}
|
|
|
|
runtime.RuntimeDurationExpr = runtimeDurationExpr
|
|
}
|
|
|
|
for _, decision := range profile.Decisions {
|
|
if runtime.RuntimeDurationExpr == nil {
|
|
var duration string
|
|
if decision.Duration != nil {
|
|
duration = *decision.Duration
|
|
} else {
|
|
runtime.Logger.Warningf("No duration specified for %s, using default duration %s", profile.Name, defaultDuration)
|
|
duration = defaultDuration
|
|
}
|
|
|
|
if _, err := time.ParseDuration(duration); err != nil {
|
|
return nil, fmt.Errorf("error parsing duration '%s' of %s: %w", duration, profile.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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.Cfg.Decisions {
|
|
decision := models.Decision{}
|
|
/*the reference decision from profile is in simulated mode */
|
|
if refDecision.Simulated != nil && *refDecision.Simulated {
|
|
decision.Simulated = new(bool)
|
|
*decision.Simulated = true
|
|
/*the event is already in simulation mode */
|
|
} else if Alert.Simulated != nil && *Alert.Simulated {
|
|
decision.Simulated = new(bool)
|
|
*decision.Simulated = true
|
|
}
|
|
/*If the profile specifies a scope, this will prevail.
|
|
If not, we're going to get the scope from the source itself*/
|
|
decision.Scope = new(string)
|
|
if refDecision.Scope != nil && *refDecision.Scope != "" {
|
|
*decision.Scope = *refDecision.Scope
|
|
} else {
|
|
*decision.Scope = *Alert.Source.Scope
|
|
}
|
|
/*some fields are populated from the reference object : duration, scope, type*/
|
|
|
|
decision.Duration = new(string)
|
|
if refDecision.Duration != nil {
|
|
*decision.Duration = *refDecision.Duration
|
|
}
|
|
|
|
if Profile.Cfg.DurationExpr != "" && Profile.RuntimeDurationExpr != nil {
|
|
profileDebug := false
|
|
if Profile.Cfg.Debug != nil && *Profile.Cfg.Debug {
|
|
profileDebug = true
|
|
}
|
|
|
|
duration, err := exprhelpers.Run(Profile.RuntimeDurationExpr, map[string]interface{}{"Alert": Alert}, Profile.Logger, profileDebug)
|
|
if err != nil {
|
|
Profile.Logger.Warningf("Failed to run duration_expr : %v", err)
|
|
} else {
|
|
durationStr := fmt.Sprint(duration)
|
|
if _, err := time.ParseDuration(durationStr); err != nil {
|
|
Profile.Logger.Warningf("Failed to parse expr duration result '%s'", duration)
|
|
} else {
|
|
*decision.Duration = durationStr
|
|
}
|
|
}
|
|
}
|
|
|
|
decision.Type = new(string)
|
|
*decision.Type = *refDecision.Type
|
|
|
|
/*for the others, let's populate it from the alert and its source*/
|
|
decision.Value = new(string)
|
|
*decision.Value = *Alert.Source.Value
|
|
decision.Origin = new(string)
|
|
*decision.Origin = types.CrowdSecOrigin
|
|
|
|
if refDecision.Origin != nil {
|
|
*decision.Origin = fmt.Sprintf("%s/%s", *decision.Origin, *refDecision.Origin)
|
|
}
|
|
|
|
decision.Scenario = new(string)
|
|
*decision.Scenario = *Alert.Scenario
|
|
decisions = append(decisions, &decision)
|
|
}
|
|
|
|
return decisions, nil
|
|
}
|
|
|
|
// EvaluateProfile is going to evaluate an Alert against a profile to generate Decisions
|
|
func (Profile *Runtime) EvaluateProfile(Alert *models.Alert) ([]*models.Decision, bool, error) {
|
|
var decisions []*models.Decision
|
|
|
|
matched := false
|
|
|
|
for eIdx, expression := range Profile.RuntimeFilters {
|
|
debugProfile := false
|
|
if Profile.Cfg.Debug != nil && *Profile.Cfg.Debug {
|
|
debugProfile = true
|
|
}
|
|
|
|
output, err := exprhelpers.Run(expression, map[string]interface{}{"Alert": Alert}, Profile.Logger, debugProfile)
|
|
if err != nil {
|
|
Profile.Logger.Warningf("failed to run profile expr for %s: %v", Profile.Cfg.Name, err)
|
|
return nil, matched, fmt.Errorf("while running expression %s: %w", Profile.Cfg.Filters[eIdx], err)
|
|
}
|
|
|
|
switch out := output.(type) {
|
|
case bool:
|
|
if out {
|
|
matched = true
|
|
/*the expression matched, create the associated decision*/
|
|
subdecisions, err := Profile.GenerateDecisionFromProfile(Alert)
|
|
if err != nil {
|
|
return nil, matched, fmt.Errorf("while generating decision from profile %s: %w", Profile.Cfg.Name, err)
|
|
}
|
|
|
|
decisions = append(decisions, subdecisions...)
|
|
} else {
|
|
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.Cfg.Filters[eIdx])
|
|
}
|
|
}
|
|
|
|
return decisions, matched, nil
|
|
}
|