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

     1  package appsecacquisition
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"slices"
     7  	"time"
     8  
     9  	"github.com/crowdsecurity/coraza/v3"
    10  	corazatypes "github.com/crowdsecurity/coraza/v3/types"
    11  	"github.com/crowdsecurity/crowdsec/pkg/appsec"
    12  	"github.com/crowdsecurity/crowdsec/pkg/types"
    13  	"github.com/prometheus/client_golang/prometheus"
    14  	log "github.com/sirupsen/logrus"
    15  	"gopkg.in/tomb.v2"
    16  
    17  	_ "github.com/crowdsecurity/crowdsec/pkg/acquisition/modules/appsec/bodyprocessors"
    18  )
    19  
    20  // that's the runtime structure of the Application security engine as seen from the acquis
    21  type AppsecRunner struct {
    22  	outChan             chan types.Event
    23  	inChan              chan appsec.ParsedRequest
    24  	UUID                string
    25  	AppsecRuntime       *appsec.AppsecRuntimeConfig //this holds the actual appsec runtime config, rules, remediations, hooks etc.
    26  	AppsecInbandEngine  coraza.WAF
    27  	AppsecOutbandEngine coraza.WAF
    28  	Labels              map[string]string
    29  	logger              *log.Entry
    30  }
    31  
    32  func (r *AppsecRunner) Init(datadir string) error {
    33  	var err error
    34  	fs := os.DirFS(datadir)
    35  
    36  	inBandRules := ""
    37  	outOfBandRules := ""
    38  
    39  	for _, collection := range r.AppsecRuntime.InBandRules {
    40  		inBandRules += collection.String()
    41  	}
    42  
    43  	for _, collection := range r.AppsecRuntime.OutOfBandRules {
    44  		outOfBandRules += collection.String()
    45  	}
    46  	inBandLogger := r.logger.Dup().WithField("band", "inband")
    47  	outBandLogger := r.logger.Dup().WithField("band", "outband")
    48  
    49  	//setting up inband engine
    50  	inbandCfg := coraza.NewWAFConfig().WithDirectives(inBandRules).WithRootFS(fs).WithDebugLogger(appsec.NewCrzLogger(inBandLogger))
    51  	if !r.AppsecRuntime.Config.InbandOptions.DisableBodyInspection {
    52  		inbandCfg = inbandCfg.WithRequestBodyAccess()
    53  	} else {
    54  		log.Warningf("Disabling body inspection, Inband rules will not be able to match on body's content.")
    55  	}
    56  	if r.AppsecRuntime.Config.InbandOptions.RequestBodyInMemoryLimit != nil {
    57  		inbandCfg = inbandCfg.WithRequestBodyInMemoryLimit(*r.AppsecRuntime.Config.InbandOptions.RequestBodyInMemoryLimit)
    58  	}
    59  	r.AppsecInbandEngine, err = coraza.NewWAF(inbandCfg)
    60  	if err != nil {
    61  		return fmt.Errorf("unable to initialize inband engine : %w", err)
    62  	}
    63  
    64  	//setting up outband engine
    65  	outbandCfg := coraza.NewWAFConfig().WithDirectives(outOfBandRules).WithRootFS(fs).WithDebugLogger(appsec.NewCrzLogger(outBandLogger))
    66  	if !r.AppsecRuntime.Config.OutOfBandOptions.DisableBodyInspection {
    67  		outbandCfg = outbandCfg.WithRequestBodyAccess()
    68  	} else {
    69  		log.Warningf("Disabling body inspection, Out of band rules will not be able to match on body's content.")
    70  	}
    71  	if r.AppsecRuntime.Config.OutOfBandOptions.RequestBodyInMemoryLimit != nil {
    72  		outbandCfg = outbandCfg.WithRequestBodyInMemoryLimit(*r.AppsecRuntime.Config.OutOfBandOptions.RequestBodyInMemoryLimit)
    73  	}
    74  	r.AppsecOutbandEngine, err = coraza.NewWAF(outbandCfg)
    75  
    76  	if r.AppsecRuntime.DisabledInBandRulesTags != nil {
    77  		for _, tag := range r.AppsecRuntime.DisabledInBandRulesTags {
    78  			r.AppsecInbandEngine.GetRuleGroup().DeleteByTag(tag)
    79  		}
    80  	}
    81  
    82  	if r.AppsecRuntime.DisabledOutOfBandRulesTags != nil {
    83  		for _, tag := range r.AppsecRuntime.DisabledOutOfBandRulesTags {
    84  			r.AppsecOutbandEngine.GetRuleGroup().DeleteByTag(tag)
    85  		}
    86  	}
    87  
    88  	if r.AppsecRuntime.DisabledInBandRuleIds != nil {
    89  		for _, id := range r.AppsecRuntime.DisabledInBandRuleIds {
    90  			r.AppsecInbandEngine.GetRuleGroup().DeleteByID(id)
    91  		}
    92  	}
    93  
    94  	if r.AppsecRuntime.DisabledOutOfBandRuleIds != nil {
    95  		for _, id := range r.AppsecRuntime.DisabledOutOfBandRuleIds {
    96  			r.AppsecOutbandEngine.GetRuleGroup().DeleteByID(id)
    97  		}
    98  	}
    99  
   100  	r.logger.Tracef("Loaded inband rules: %+v", r.AppsecInbandEngine.GetRuleGroup().GetRules())
   101  	r.logger.Tracef("Loaded outband rules: %+v", r.AppsecOutbandEngine.GetRuleGroup().GetRules())
   102  
   103  	if err != nil {
   104  		return fmt.Errorf("unable to initialize outband engine : %w", err)
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  func (r *AppsecRunner) processRequest(tx appsec.ExtendedTransaction, request *appsec.ParsedRequest) error {
   111  	var in *corazatypes.Interruption
   112  	var err error
   113  
   114  	if request.Tx.IsRuleEngineOff() {
   115  		r.logger.Debugf("rule engine is off, skipping")
   116  		return nil
   117  	}
   118  
   119  	defer func() {
   120  		request.Tx.ProcessLogging()
   121  		//We don't close the transaction here, as it will reset coraza internal state and break variable tracking
   122  
   123  		err := r.AppsecRuntime.ProcessPostEvalRules(request)
   124  		if err != nil {
   125  			r.logger.Errorf("unable to process PostEval rules: %s", err)
   126  		}
   127  	}()
   128  
   129  	//pre eval (expr) rules
   130  	err = r.AppsecRuntime.ProcessPreEvalRules(request)
   131  	if err != nil {
   132  		r.logger.Errorf("unable to process PreEval rules: %s", err)
   133  		//FIXME: should we abort here ?
   134  	}
   135  
   136  	request.Tx.ProcessConnection(request.RemoteAddr, 0, "", 0)
   137  
   138  	for k, v := range request.Args {
   139  		for _, vv := range v {
   140  			request.Tx.AddGetRequestArgument(k, vv)
   141  		}
   142  	}
   143  
   144  	request.Tx.ProcessURI(request.URI, request.Method, request.Proto)
   145  
   146  	for k, vr := range request.Headers {
   147  		for _, v := range vr {
   148  			request.Tx.AddRequestHeader(k, v)
   149  		}
   150  	}
   151  
   152  	if request.ClientHost != "" {
   153  		request.Tx.AddRequestHeader("Host", request.ClientHost)
   154  		request.Tx.SetServerName(request.ClientHost)
   155  	}
   156  
   157  	if request.TransferEncoding != nil {
   158  		request.Tx.AddRequestHeader("Transfer-Encoding", request.TransferEncoding[0])
   159  	}
   160  
   161  	in = request.Tx.ProcessRequestHeaders()
   162  
   163  	if in != nil {
   164  		r.logger.Infof("inband rules matched for headers : %s", in.Action)
   165  		return nil
   166  	}
   167  
   168  	if request.Body != nil && len(request.Body) > 0 {
   169  		in, _, err = request.Tx.WriteRequestBody(request.Body)
   170  		if err != nil {
   171  			r.logger.Errorf("unable to write request body : %s", err)
   172  			return err
   173  		}
   174  		if in != nil {
   175  			return nil
   176  		}
   177  	}
   178  
   179  	in, err = request.Tx.ProcessRequestBody()
   180  
   181  	if err != nil {
   182  		r.logger.Errorf("unable to process request body : %s", err)
   183  		return err
   184  	}
   185  
   186  	if in != nil {
   187  		r.logger.Debugf("rules matched for body : %d", in.RuleID)
   188  	}
   189  
   190  	return nil
   191  }
   192  
   193  func (r *AppsecRunner) ProcessInBandRules(request *appsec.ParsedRequest) error {
   194  	tx := appsec.NewExtendedTransaction(r.AppsecInbandEngine, request.UUID)
   195  	r.AppsecRuntime.InBandTx = tx
   196  	request.Tx = tx
   197  	if len(r.AppsecRuntime.InBandRules) == 0 {
   198  		return nil
   199  	}
   200  	err := r.processRequest(tx, request)
   201  	return err
   202  }
   203  
   204  func (r *AppsecRunner) ProcessOutOfBandRules(request *appsec.ParsedRequest) error {
   205  	tx := appsec.NewExtendedTransaction(r.AppsecOutbandEngine, request.UUID)
   206  	r.AppsecRuntime.OutOfBandTx = tx
   207  	request.Tx = tx
   208  	if len(r.AppsecRuntime.OutOfBandRules) == 0 {
   209  		return nil
   210  	}
   211  	err := r.processRequest(tx, request)
   212  	return err
   213  }
   214  
   215  func (r *AppsecRunner) handleInBandInterrupt(request *appsec.ParsedRequest) {
   216  	//create the associated event for crowdsec itself
   217  	evt, err := EventFromRequest(request, r.Labels)
   218  	if err != nil {
   219  		//let's not interrupt the pipeline for this
   220  		r.logger.Errorf("unable to create event from request : %s", err)
   221  	}
   222  	err = r.AccumulateTxToEvent(&evt, request)
   223  	if err != nil {
   224  		r.logger.Errorf("unable to accumulate tx to event : %s", err)
   225  	}
   226  	if in := request.Tx.Interruption(); in != nil {
   227  		r.logger.Debugf("inband rules matched : %d", in.RuleID)
   228  		r.AppsecRuntime.Response.InBandInterrupt = true
   229  		r.AppsecRuntime.Response.BouncerHTTPResponseCode = r.AppsecRuntime.Config.BouncerBlockedHTTPCode
   230  		r.AppsecRuntime.Response.UserHTTPResponseCode = r.AppsecRuntime.Config.UserBlockedHTTPCode
   231  		r.AppsecRuntime.Response.Action = r.AppsecRuntime.DefaultRemediation
   232  
   233  		if _, ok := r.AppsecRuntime.RemediationById[in.RuleID]; ok {
   234  			r.AppsecRuntime.Response.Action = r.AppsecRuntime.RemediationById[in.RuleID]
   235  		}
   236  
   237  		for tag, remediation := range r.AppsecRuntime.RemediationByTag {
   238  			if slices.Contains[[]string, string](in.Tags, tag) {
   239  				r.AppsecRuntime.Response.Action = remediation
   240  			}
   241  		}
   242  
   243  		err = r.AppsecRuntime.ProcessOnMatchRules(request, evt)
   244  		if err != nil {
   245  			r.logger.Errorf("unable to process OnMatch rules: %s", err)
   246  			return
   247  		}
   248  
   249  		// Should the in band match trigger an overflow ?
   250  		if r.AppsecRuntime.Response.SendAlert {
   251  			appsecOvlfw, err := AppsecEventGeneration(evt)
   252  			if err != nil {
   253  				r.logger.Errorf("unable to generate appsec event : %s", err)
   254  				return
   255  			}
   256  			if appsecOvlfw != nil {
   257  				r.outChan <- *appsecOvlfw
   258  			}
   259  		}
   260  
   261  		// Should the in band match trigger an event ?
   262  		if r.AppsecRuntime.Response.SendEvent {
   263  			r.outChan <- evt
   264  		}
   265  
   266  	}
   267  }
   268  
   269  func (r *AppsecRunner) handleOutBandInterrupt(request *appsec.ParsedRequest) {
   270  	evt, err := EventFromRequest(request, r.Labels)
   271  	if err != nil {
   272  		//let's not interrupt the pipeline for this
   273  		r.logger.Errorf("unable to create event from request : %s", err)
   274  	}
   275  	err = r.AccumulateTxToEvent(&evt, request)
   276  	if err != nil {
   277  		r.logger.Errorf("unable to accumulate tx to event : %s", err)
   278  	}
   279  	if in := request.Tx.Interruption(); in != nil {
   280  		r.logger.Debugf("outband rules matched : %d", in.RuleID)
   281  		r.AppsecRuntime.Response.OutOfBandInterrupt = true
   282  
   283  		err = r.AppsecRuntime.ProcessOnMatchRules(request, evt)
   284  		if err != nil {
   285  			r.logger.Errorf("unable to process OnMatch rules: %s", err)
   286  			return
   287  		}
   288  		// Should the match trigger an event ?
   289  		if r.AppsecRuntime.Response.SendEvent {
   290  			r.outChan <- evt
   291  		}
   292  
   293  		// Should the match trigger an overflow ?
   294  		if r.AppsecRuntime.Response.SendAlert {
   295  			appsecOvlfw, err := AppsecEventGeneration(evt)
   296  			if err != nil {
   297  				r.logger.Errorf("unable to generate appsec event : %s", err)
   298  				return
   299  			}
   300  			r.outChan <- *appsecOvlfw
   301  		}
   302  	}
   303  }
   304  
   305  func (r *AppsecRunner) handleRequest(request *appsec.ParsedRequest) {
   306  	r.AppsecRuntime.Logger = r.AppsecRuntime.Logger.WithField("request_uuid", request.UUID)
   307  	logger := r.logger.WithField("request_uuid", request.UUID)
   308  	logger.Debug("Request received in runner")
   309  	r.AppsecRuntime.ClearResponse()
   310  
   311  	request.IsInBand = true
   312  	request.IsOutBand = false
   313  
   314  	//to measure the time spent in the Application Security Engine for InBand rules
   315  	startInBandParsing := time.Now()
   316  	startGlobalParsing := time.Now()
   317  
   318  	//inband appsec rules
   319  	err := r.ProcessInBandRules(request)
   320  	if err != nil {
   321  		logger.Errorf("unable to process InBand rules: %s", err)
   322  		return
   323  	}
   324  
   325  	// time spent to process in band rules
   326  	inBandParsingElapsed := time.Since(startInBandParsing)
   327  	AppsecInbandParsingHistogram.With(prometheus.Labels{"source": request.RemoteAddrNormalized, "appsec_engine": request.AppsecEngine}).Observe(inBandParsingElapsed.Seconds())
   328  
   329  	if request.Tx.IsInterrupted() {
   330  		r.handleInBandInterrupt(request)
   331  	}
   332  
   333  	// send back the result to the HTTP handler for the InBand part
   334  	request.ResponseChannel <- r.AppsecRuntime.Response
   335  
   336  	//Now let's process the out of band rules
   337  
   338  	request.IsInBand = false
   339  	request.IsOutBand = true
   340  	r.AppsecRuntime.Response.SendAlert = false
   341  	r.AppsecRuntime.Response.SendEvent = true
   342  
   343  	//FIXME: This is a bit of a hack to avoid confusion with the transaction if we do not have any inband rules.
   344  	//We should probably have different transaction (or even different request object) for inband and out of band rules
   345  	if len(r.AppsecRuntime.OutOfBandRules) > 0 {
   346  		//to measure the time spent in the Application Security Engine for OutOfBand rules
   347  		startOutOfBandParsing := time.Now()
   348  
   349  		err = r.ProcessOutOfBandRules(request)
   350  		if err != nil {
   351  			logger.Errorf("unable to process OutOfBand rules: %s", err)
   352  			return
   353  		}
   354  
   355  		// time spent to process out of band rules
   356  		outOfBandParsingElapsed := time.Since(startOutOfBandParsing)
   357  		AppsecOutbandParsingHistogram.With(prometheus.Labels{"source": request.RemoteAddrNormalized, "appsec_engine": request.AppsecEngine}).Observe(outOfBandParsingElapsed.Seconds())
   358  		if request.Tx.IsInterrupted() {
   359  			r.handleOutBandInterrupt(request)
   360  		}
   361  	}
   362  	// time spent to process inband AND out of band rules
   363  	globalParsingElapsed := time.Since(startGlobalParsing)
   364  	AppsecGlobalParsingHistogram.With(prometheus.Labels{"source": request.RemoteAddrNormalized, "appsec_engine": request.AppsecEngine}).Observe(globalParsingElapsed.Seconds())
   365  
   366  }
   367  
   368  func (r *AppsecRunner) Run(t *tomb.Tomb) error {
   369  	r.logger.Infof("Appsec Runner ready to process event")
   370  	for {
   371  		select {
   372  		case <-t.Dying():
   373  			r.logger.Infof("Appsec Runner is dying")
   374  			return nil
   375  		case request := <-r.inChan:
   376  			r.handleRequest(&request)
   377  		}
   378  	}
   379  }