github.com/crowdsecurity/crowdsec@v1.6.1/pkg/csprofiles/csprofiles.go (about)

     1  package csprofiles
     2  
     3  import (
     4  	"fmt"
     5  	"time"
     6  
     7  	"github.com/antonmedv/expr"
     8  	"github.com/antonmedv/expr/vm"
     9  	log "github.com/sirupsen/logrus"
    10  
    11  	"github.com/crowdsecurity/crowdsec/pkg/csconfig"
    12  	"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
    13  	"github.com/crowdsecurity/crowdsec/pkg/models"
    14  	"github.com/crowdsecurity/crowdsec/pkg/types"
    15  )
    16  
    17  type Runtime struct {
    18  	RuntimeFilters      []*vm.Program        `json:"-" yaml:"-"`
    19  	RuntimeDurationExpr *vm.Program          `json:"-" yaml:"-"`
    20  	Cfg                 *csconfig.ProfileCfg `json:"-" yaml:"-"`
    21  	Logger              *log.Entry           `json:"-" yaml:"-"`
    22  }
    23  
    24  const defaultDuration = "4h"
    25  
    26  func NewProfile(profilesCfg []*csconfig.ProfileCfg) ([]*Runtime, error) {
    27  	var err error
    28  
    29  	profilesRuntime := make([]*Runtime, 0)
    30  
    31  	for _, profile := range profilesCfg {
    32  		var runtimeFilter, runtimeDurationExpr *vm.Program
    33  
    34  		runtime := &Runtime{}
    35  
    36  		xlog := log.New()
    37  		if err := types.ConfigureLogger(xlog); err != nil {
    38  			log.Fatalf("While creating profiles-specific logger : %s", err)
    39  		}
    40  
    41  		xlog.SetLevel(log.InfoLevel)
    42  		runtime.Logger = xlog.WithFields(log.Fields{
    43  			"type": "profile",
    44  			"name": profile.Name,
    45  		})
    46  
    47  		runtime.RuntimeFilters = make([]*vm.Program, len(profile.Filters))
    48  		runtime.Cfg = profile
    49  
    50  		if runtime.Cfg.OnSuccess != "" && runtime.Cfg.OnSuccess != "continue" && runtime.Cfg.OnSuccess != "break" {
    51  			return nil, fmt.Errorf("invalid 'on_success' for '%s': %s", profile.Name, runtime.Cfg.OnSuccess)
    52  		}
    53  
    54  		if runtime.Cfg.OnFailure != "" && runtime.Cfg.OnFailure != "continue" && runtime.Cfg.OnFailure != "break" && runtime.Cfg.OnFailure != "apply" {
    55  			return nil, fmt.Errorf("invalid 'on_failure' for '%s' : %s", profile.Name, runtime.Cfg.OnFailure)
    56  		}
    57  
    58  		for fIdx, filter := range profile.Filters {
    59  			if runtimeFilter, err = expr.Compile(filter, exprhelpers.GetExprOptions(map[string]interface{}{"Alert": &models.Alert{}})...); err != nil {
    60  				return nil, fmt.Errorf("error compiling filter of '%s': %w", profile.Name, err)
    61  			}
    62  
    63  			runtime.RuntimeFilters[fIdx] = runtimeFilter
    64  			if profile.Debug != nil && *profile.Debug {
    65  				runtime.Logger.Logger.SetLevel(log.DebugLevel)
    66  			}
    67  		}
    68  
    69  		if profile.DurationExpr != "" {
    70  			if runtimeDurationExpr, err = expr.Compile(profile.DurationExpr, exprhelpers.GetExprOptions(map[string]interface{}{"Alert": &models.Alert{}})...); err != nil {
    71  				return nil, fmt.Errorf("error compiling duration_expr of %s: %w", profile.Name, err)
    72  			}
    73  
    74  			runtime.RuntimeDurationExpr = runtimeDurationExpr
    75  		}
    76  
    77  		for _, decision := range profile.Decisions {
    78  			if runtime.RuntimeDurationExpr == nil {
    79  				var duration string
    80  				if decision.Duration != nil {
    81  					duration = *decision.Duration
    82  				} else {
    83  					runtime.Logger.Warningf("No duration specified for %s, using default duration %s", profile.Name, defaultDuration)
    84  					duration = defaultDuration
    85  				}
    86  
    87  				if _, err := time.ParseDuration(duration); err != nil {
    88  					return nil, fmt.Errorf("error parsing duration '%s' of %s: %w", duration, profile.Name, err)
    89  				}
    90  			}
    91  		}
    92  
    93  		profilesRuntime = append(profilesRuntime, runtime)
    94  	}
    95  
    96  	return profilesRuntime, nil
    97  }
    98  
    99  func (Profile *Runtime) GenerateDecisionFromProfile(Alert *models.Alert) ([]*models.Decision, error) {
   100  	var decisions []*models.Decision
   101  
   102  	for _, refDecision := range Profile.Cfg.Decisions {
   103  		decision := models.Decision{}
   104  		/*the reference decision from profile is in simulated mode */
   105  		if refDecision.Simulated != nil && *refDecision.Simulated {
   106  			decision.Simulated = new(bool)
   107  			*decision.Simulated = true
   108  			/*the event is already in simulation mode */
   109  		} else if Alert.Simulated != nil && *Alert.Simulated {
   110  			decision.Simulated = new(bool)
   111  			*decision.Simulated = true
   112  		}
   113  		/*If the profile specifies a scope, this will prevail.
   114  		If not, we're going to get the scope from the source itself*/
   115  		decision.Scope = new(string)
   116  		if refDecision.Scope != nil && *refDecision.Scope != "" {
   117  			*decision.Scope = *refDecision.Scope
   118  		} else {
   119  			*decision.Scope = *Alert.Source.Scope
   120  		}
   121  		/*some fields are populated from the reference object : duration, scope, type*/
   122  
   123  		decision.Duration = new(string)
   124  		if refDecision.Duration != nil {
   125  			*decision.Duration = *refDecision.Duration
   126  		}
   127  
   128  		if Profile.Cfg.DurationExpr != "" && Profile.RuntimeDurationExpr != nil {
   129  			profileDebug := false
   130  			if Profile.Cfg.Debug != nil && *Profile.Cfg.Debug {
   131  				profileDebug = true
   132  			}
   133  
   134  			duration, err := exprhelpers.Run(Profile.RuntimeDurationExpr, map[string]interface{}{"Alert": Alert}, Profile.Logger, profileDebug)
   135  			if err != nil {
   136  				Profile.Logger.Warningf("Failed to run duration_expr : %v", err)
   137  			} else {
   138  				durationStr := fmt.Sprint(duration)
   139  				if _, err := time.ParseDuration(durationStr); err != nil {
   140  					Profile.Logger.Warningf("Failed to parse expr duration result '%s'", duration)
   141  				} else {
   142  					*decision.Duration = durationStr
   143  				}
   144  			}
   145  		}
   146  
   147  		decision.Type = new(string)
   148  		*decision.Type = *refDecision.Type
   149  
   150  		/*for the others, let's populate it from the alert and its source*/
   151  		decision.Value = new(string)
   152  		*decision.Value = *Alert.Source.Value
   153  		decision.Origin = new(string)
   154  		*decision.Origin = types.CrowdSecOrigin
   155  
   156  		if refDecision.Origin != nil {
   157  			*decision.Origin = fmt.Sprintf("%s/%s", *decision.Origin, *refDecision.Origin)
   158  		}
   159  
   160  		decision.Scenario = new(string)
   161  		*decision.Scenario = *Alert.Scenario
   162  		decisions = append(decisions, &decision)
   163  	}
   164  
   165  	return decisions, nil
   166  }
   167  
   168  // EvaluateProfile is going to evaluate an Alert against a profile to generate Decisions
   169  func (Profile *Runtime) EvaluateProfile(Alert *models.Alert) ([]*models.Decision, bool, error) {
   170  	var decisions []*models.Decision
   171  
   172  	matched := false
   173  
   174  	for eIdx, expression := range Profile.RuntimeFilters {
   175  		debugProfile := false
   176  		if Profile.Cfg.Debug != nil && *Profile.Cfg.Debug {
   177  			debugProfile = true
   178  		}
   179  
   180  		output, err := exprhelpers.Run(expression, map[string]interface{}{"Alert": Alert}, Profile.Logger, debugProfile)
   181  		if err != nil {
   182  			Profile.Logger.Warningf("failed to run profile expr for %s: %v", Profile.Cfg.Name, err)
   183  			return nil, matched, fmt.Errorf("while running expression %s: %w", Profile.Cfg.Filters[eIdx], err)
   184  		}
   185  
   186  		switch out := output.(type) {
   187  		case bool:
   188  			if out {
   189  				matched = true
   190  				/*the expression matched, create the associated decision*/
   191  				subdecisions, err := Profile.GenerateDecisionFromProfile(Alert)
   192  				if err != nil {
   193  					return nil, matched, fmt.Errorf("while generating decision from profile %s: %w", Profile.Cfg.Name, err)
   194  				}
   195  
   196  				decisions = append(decisions, subdecisions...)
   197  			} else {
   198  				Profile.Logger.Debugf("Profile %s filter is unsuccessful", Profile.Cfg.Name)
   199  				if Profile.Cfg.OnFailure == "break" {
   200  					break
   201  				}
   202  			}
   203  
   204  		default:
   205  			return nil, matched, fmt.Errorf("unexpected type %t (%v) while running '%s'", output, output, Profile.Cfg.Filters[eIdx])
   206  		}
   207  	}
   208  
   209  	return decisions, matched, nil
   210  }