github.com/crowdsecurity/crowdsec@v1.6.1/pkg/acquisition/modules/appsec/utils.go (about)

     1  package appsecacquisition
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/crowdsecurity/coraza/v3/collection"
     9  	"github.com/crowdsecurity/coraza/v3/types/variables"
    10  	"github.com/crowdsecurity/crowdsec/pkg/appsec"
    11  	"github.com/crowdsecurity/crowdsec/pkg/models"
    12  	"github.com/crowdsecurity/crowdsec/pkg/types"
    13  	"github.com/crowdsecurity/go-cs-lib/ptr"
    14  	"github.com/prometheus/client_golang/prometheus"
    15  	log "github.com/sirupsen/logrus"
    16  )
    17  
    18  func AppsecEventGeneration(inEvt types.Event) (*types.Event, error) {
    19  	//if the request didnd't trigger inband rules, we don't want to generate an event to LAPI/CAPI
    20  	if !inEvt.Appsec.HasInBandMatches {
    21  		return nil, nil
    22  	}
    23  	evt := types.Event{}
    24  	evt.Type = types.APPSEC
    25  	evt.Process = true
    26  	source := models.Source{
    27  		Value: ptr.Of(inEvt.Parsed["source_ip"]),
    28  		IP:    inEvt.Parsed["source_ip"],
    29  		Scope: ptr.Of(types.Ip),
    30  	}
    31  
    32  	evt.Overflow.Sources = make(map[string]models.Source)
    33  	evt.Overflow.Sources["ip"] = source
    34  
    35  	alert := models.Alert{}
    36  	alert.Capacity = ptr.Of(int32(1))
    37  	alert.Events = make([]*models.Event, 0)
    38  	alert.Meta = make(models.Meta, 0)
    39  	for _, key := range []string{"target_uri", "method"} {
    40  
    41  		valueByte, err := json.Marshal([]string{inEvt.Parsed[key]})
    42  		if err != nil {
    43  			log.Debugf("unable to serialize key %s", key)
    44  			continue
    45  		}
    46  
    47  		meta := models.MetaItems0{
    48  			Key:   key,
    49  			Value: string(valueByte),
    50  		}
    51  		alert.Meta = append(alert.Meta, &meta)
    52  	}
    53  	matchedZones := inEvt.Appsec.GetMatchedZones()
    54  	if matchedZones != nil {
    55  		valueByte, err := json.Marshal(matchedZones)
    56  		if err != nil {
    57  			log.Debugf("unable to serialize key matched_zones")
    58  		} else {
    59  			meta := models.MetaItems0{
    60  				Key:   "matched_zones",
    61  				Value: string(valueByte),
    62  			}
    63  			alert.Meta = append(alert.Meta, &meta)
    64  		}
    65  	}
    66  
    67  	alert.EventsCount = ptr.Of(int32(1))
    68  	alert.Leakspeed = ptr.Of("")
    69  	alert.Scenario = ptr.Of(inEvt.Appsec.MatchedRules.GetName())
    70  	alert.ScenarioHash = ptr.Of(inEvt.Appsec.MatchedRules.GetHash())
    71  	alert.ScenarioVersion = ptr.Of(inEvt.Appsec.MatchedRules.GetVersion())
    72  	alert.Simulated = ptr.Of(false)
    73  	alert.Source = &source
    74  	msg := fmt.Sprintf("AppSec block: %s from %s (%s)", inEvt.Appsec.MatchedRules.GetName(),
    75  		alert.Source.IP, inEvt.Parsed["remediation_cmpt_ip"])
    76  	alert.Message = &msg
    77  	alert.StartAt = ptr.Of(time.Now().UTC().Format(time.RFC3339))
    78  	alert.StopAt = ptr.Of(time.Now().UTC().Format(time.RFC3339))
    79  	evt.Overflow.APIAlerts = []models.Alert{alert}
    80  	evt.Overflow.Alert = &alert
    81  	return &evt, nil
    82  }
    83  
    84  func EventFromRequest(r *appsec.ParsedRequest, labels map[string]string) (types.Event, error) {
    85  	evt := types.Event{}
    86  	//we might want to change this based on in-band vs out-of-band ?
    87  	evt.Type = types.LOG
    88  	evt.ExpectMode = types.LIVE
    89  	//def needs fixing
    90  	evt.Stage = "s00-raw"
    91  	evt.Parsed = map[string]string{
    92  		"source_ip":           r.ClientIP,
    93  		"target_host":         r.Host,
    94  		"target_uri":          r.URI,
    95  		"method":              r.Method,
    96  		"req_uuid":            r.Tx.ID(),
    97  		"source":              "crowdsec-appsec",
    98  		"remediation_cmpt_ip": r.RemoteAddrNormalized,
    99  		//TBD:
   100  		//http_status
   101  		//user_agent
   102  
   103  	}
   104  	evt.Line = types.Line{
   105  		Time: time.Now(),
   106  		//should we add some info like listen addr/port/path ?
   107  		Labels:  labels,
   108  		Process: true,
   109  		Module:  "appsec",
   110  		Src:     "appsec",
   111  		Raw:     "dummy-appsec-data", //we discard empty Line.Raw items :)
   112  	}
   113  	evt.Appsec = types.AppsecEvent{}
   114  
   115  	return evt, nil
   116  }
   117  
   118  func LogAppsecEvent(evt *types.Event, logger *log.Entry) {
   119  	req := evt.Parsed["target_uri"]
   120  	if len(req) > 12 {
   121  		req = req[:10] + ".."
   122  	}
   123  
   124  	if evt.Meta["appsec_interrupted"] == "true" {
   125  		logger.WithFields(log.Fields{
   126  			"module":     "appsec",
   127  			"source":     evt.Parsed["source_ip"],
   128  			"target_uri": req,
   129  		}).Infof("%s blocked on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
   130  	} else if evt.Parsed["outofband_interrupted"] == "true" {
   131  		logger.WithFields(log.Fields{
   132  			"module":     "appsec",
   133  			"source":     evt.Parsed["source_ip"],
   134  			"target_uri": req,
   135  		}).Infof("%s out-of-band blocking rules on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
   136  	} else {
   137  		logger.WithFields(log.Fields{
   138  			"module":     "appsec",
   139  			"source":     evt.Parsed["source_ip"],
   140  			"target_uri": req,
   141  		}).Debugf("%s triggered non-blocking rules on %s (%d rules) [%v]", evt.Parsed["source_ip"], req, len(evt.Appsec.MatchedRules), evt.Appsec.GetRuleIDs())
   142  	}
   143  
   144  }
   145  
   146  func (r *AppsecRunner) AccumulateTxToEvent(evt *types.Event, req *appsec.ParsedRequest) error {
   147  
   148  	if evt == nil {
   149  		//an error was already emitted, let's not spam the logs
   150  		return nil
   151  	}
   152  
   153  	if !req.Tx.IsInterrupted() {
   154  		//if the phase didn't generate an interruption, we don't have anything to add to the event
   155  		return nil
   156  	}
   157  	//if one interruption was generated, event is good for processing :)
   158  	evt.Process = true
   159  
   160  	if evt.Meta == nil {
   161  		evt.Meta = map[string]string{}
   162  	}
   163  	if evt.Parsed == nil {
   164  		evt.Parsed = map[string]string{}
   165  	}
   166  	if req.IsInBand {
   167  		evt.Meta["appsec_interrupted"] = "true"
   168  		evt.Meta["appsec_action"] = req.Tx.Interruption().Action
   169  		evt.Parsed["inband_interrupted"] = "true"
   170  		evt.Parsed["inband_action"] = req.Tx.Interruption().Action
   171  	} else {
   172  		evt.Parsed["outofband_interrupted"] = "true"
   173  		evt.Parsed["outofband_action"] = req.Tx.Interruption().Action
   174  	}
   175  
   176  	if evt.Appsec.Vars == nil {
   177  		evt.Appsec.Vars = map[string]string{}
   178  	}
   179  
   180  	req.Tx.Variables().All(func(v variables.RuleVariable, col collection.Collection) bool {
   181  		for _, variable := range col.FindAll() {
   182  			key := variable.Variable().Name()
   183  			if variable.Key() != "" {
   184  				key += "." + variable.Key()
   185  			}
   186  			if variable.Value() == "" {
   187  				continue
   188  			}
   189  			for _, collectionToKeep := range r.AppsecRuntime.CompiledVariablesTracking {
   190  				match := collectionToKeep.MatchString(key)
   191  				if match {
   192  					evt.Appsec.Vars[key] = variable.Value()
   193  					r.logger.Debugf("%s.%s = %s", variable.Variable().Name(), variable.Key(), variable.Value())
   194  				} else {
   195  					r.logger.Debugf("%s.%s != %s (%s) (not kept)", variable.Variable().Name(), variable.Key(), collectionToKeep, variable.Value())
   196  				}
   197  			}
   198  		}
   199  		return true
   200  	})
   201  
   202  	for _, rule := range req.Tx.MatchedRules() {
   203  		if rule.Message() == "" || rule.DisruptiveAction() == "pass" || rule.DisruptiveAction() == "allow" {
   204  			r.logger.Tracef("discarding rule %d (action: %s)", rule.Rule().ID(), rule.DisruptiveAction())
   205  			continue
   206  		}
   207  		kind := "outofband"
   208  		if req.IsInBand {
   209  			kind = "inband"
   210  			evt.Appsec.HasInBandMatches = true
   211  		} else {
   212  			evt.Appsec.HasOutBandMatches = true
   213  		}
   214  
   215  		var name string
   216  		version := ""
   217  		hash := ""
   218  		ruleNameProm := fmt.Sprintf("%d", rule.Rule().ID())
   219  
   220  		if details, ok := appsec.AppsecRulesDetails[rule.Rule().ID()]; ok {
   221  			//Only set them for custom rules, not for rules written in seclang
   222  			name = details.Name
   223  			version = details.Version
   224  			hash = details.Hash
   225  			ruleNameProm = details.Name
   226  			r.logger.Debugf("custom rule for event, setting name: %s, version: %s, hash: %s", name, version, hash)
   227  		} else {
   228  			name = fmt.Sprintf("native_rule:%d", rule.Rule().ID())
   229  		}
   230  
   231  		AppsecRuleHits.With(prometheus.Labels{"rule_name": ruleNameProm, "type": kind, "source": req.RemoteAddrNormalized, "appsec_engine": req.AppsecEngine}).Inc()
   232  
   233  		matchedZones := make([]string, 0)
   234  		for _, matchData := range rule.MatchedDatas() {
   235  			zone := matchData.Variable().Name()
   236  			varName := matchData.Key()
   237  			if varName != "" {
   238  				zone += "." + varName
   239  			}
   240  			matchedZones = append(matchedZones, zone)
   241  		}
   242  
   243  		corazaRule := map[string]interface{}{
   244  			"id":            rule.Rule().ID(),
   245  			"uri":           evt.Parsed["uri"],
   246  			"rule_type":     kind,
   247  			"method":        evt.Parsed["method"],
   248  			"disruptive":    rule.Disruptive(),
   249  			"tags":          rule.Rule().Tags(),
   250  			"file":          rule.Rule().File(),
   251  			"file_line":     rule.Rule().Line(),
   252  			"revision":      rule.Rule().Revision(),
   253  			"secmark":       rule.Rule().SecMark(),
   254  			"accuracy":      rule.Rule().Accuracy(),
   255  			"msg":           rule.Message(),
   256  			"severity":      rule.Rule().Severity().String(),
   257  			"name":          name,
   258  			"hash":          hash,
   259  			"version":       version,
   260  			"matched_zones": matchedZones,
   261  		}
   262  		evt.Appsec.MatchedRules = append(evt.Appsec.MatchedRules, corazaRule)
   263  	}
   264  
   265  	return nil
   266  
   267  }