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 }