github.com/crowdsecurity/crowdsec@v1.6.1/pkg/appsec/appsec.go (about) 1 package appsec 2 3 import ( 4 "fmt" 5 "net/http" 6 "os" 7 "regexp" 8 9 "github.com/antonmedv/expr" 10 "github.com/antonmedv/expr/vm" 11 "github.com/crowdsecurity/crowdsec/pkg/cwhub" 12 "github.com/crowdsecurity/crowdsec/pkg/exprhelpers" 13 "github.com/crowdsecurity/crowdsec/pkg/types" 14 log "github.com/sirupsen/logrus" 15 "gopkg.in/yaml.v2" 16 ) 17 18 type Hook struct { 19 Filter string `yaml:"filter"` 20 FilterExpr *vm.Program `yaml:"-"` 21 22 OnSuccess string `yaml:"on_success"` 23 Apply []string `yaml:"apply"` 24 ApplyExpr []*vm.Program `yaml:"-"` 25 } 26 27 const ( 28 hookOnLoad = iota 29 hookPreEval 30 hookPostEval 31 hookOnMatch 32 ) 33 34 const ( 35 BanRemediation = "ban" 36 CaptchaRemediation = "captcha" 37 AllowRemediation = "allow" 38 ) 39 40 func (h *Hook) Build(hookStage int) error { 41 42 ctx := map[string]interface{}{} 43 switch hookStage { 44 case hookOnLoad: 45 ctx = GetOnLoadEnv(&AppsecRuntimeConfig{}) 46 case hookPreEval: 47 ctx = GetPreEvalEnv(&AppsecRuntimeConfig{}, &ParsedRequest{}) 48 case hookPostEval: 49 ctx = GetPostEvalEnv(&AppsecRuntimeConfig{}, &ParsedRequest{}) 50 case hookOnMatch: 51 ctx = GetOnMatchEnv(&AppsecRuntimeConfig{}, &ParsedRequest{}, types.Event{}) 52 } 53 opts := exprhelpers.GetExprOptions(ctx) 54 if h.Filter != "" { 55 program, err := expr.Compile(h.Filter, opts...) //FIXME: opts 56 if err != nil { 57 return fmt.Errorf("unable to compile filter %s : %w", h.Filter, err) 58 } 59 h.FilterExpr = program 60 } 61 for _, apply := range h.Apply { 62 program, err := expr.Compile(apply, opts...) 63 if err != nil { 64 return fmt.Errorf("unable to compile apply %s : %w", apply, err) 65 } 66 h.ApplyExpr = append(h.ApplyExpr, program) 67 } 68 return nil 69 } 70 71 type AppsecTempResponse struct { 72 InBandInterrupt bool 73 OutOfBandInterrupt bool 74 Action string //allow, deny, captcha, log 75 UserHTTPResponseCode int //The response code to send to the user 76 BouncerHTTPResponseCode int //The response code to send to the remediation component 77 SendEvent bool //do we send an internal event on rule match 78 SendAlert bool //do we send an alert on rule match 79 } 80 81 type AppsecSubEngineOpts struct { 82 DisableBodyInspection bool `yaml:"disable_body_inspection"` 83 RequestBodyInMemoryLimit *int `yaml:"request_body_in_memory_limit"` 84 } 85 86 // runtime version of AppsecConfig 87 type AppsecRuntimeConfig struct { 88 Name string 89 OutOfBandRules []AppsecCollection 90 91 InBandRules []AppsecCollection 92 93 DefaultRemediation string 94 RemediationByTag map[string]string //Also used for ByName, as the name (for modsec rules) is a tag crowdsec-NAME 95 RemediationById map[int]string 96 CompiledOnLoad []Hook 97 CompiledPreEval []Hook 98 CompiledPostEval []Hook 99 CompiledOnMatch []Hook 100 CompiledVariablesTracking []*regexp.Regexp 101 Config *AppsecConfig 102 //CorazaLogger debuglog.Logger 103 104 //those are ephemeral, created/destroyed with every req 105 OutOfBandTx ExtendedTransaction //is it a good idea ? 106 InBandTx ExtendedTransaction //is it a good idea ? 107 Response AppsecTempResponse 108 //should we store matched rules here ? 109 110 Logger *log.Entry 111 112 //Set by on_load to ignore some rules on loading 113 DisabledInBandRuleIds []int 114 DisabledInBandRulesTags []string //Also used for ByName, as the name (for modsec rules) is a tag crowdsec-NAME 115 116 DisabledOutOfBandRuleIds []int 117 DisabledOutOfBandRulesTags []string //Also used for ByName, as the name (for modsec rules) is a tag crowdsec-NAME 118 } 119 120 type AppsecConfig struct { 121 Name string `yaml:"name"` 122 OutOfBandRules []string `yaml:"outofband_rules"` 123 InBandRules []string `yaml:"inband_rules"` 124 DefaultRemediation string `yaml:"default_remediation"` 125 DefaultPassAction string `yaml:"default_pass_action"` 126 BouncerBlockedHTTPCode int `yaml:"blocked_http_code"` //returned to the bouncer 127 BouncerPassedHTTPCode int `yaml:"passed_http_code"` //returned to the bouncer 128 UserBlockedHTTPCode int `yaml:"user_blocked_http_code"` //returned to the user 129 UserPassedHTTPCode int `yaml:"user_passed_http_code"` //returned to the user 130 131 OnLoad []Hook `yaml:"on_load"` 132 PreEval []Hook `yaml:"pre_eval"` 133 PostEval []Hook `yaml:"post_eval"` 134 OnMatch []Hook `yaml:"on_match"` 135 VariablesTracking []string `yaml:"variables_tracking"` 136 InbandOptions AppsecSubEngineOpts `yaml:"inband_options"` 137 OutOfBandOptions AppsecSubEngineOpts `yaml:"outofband_options"` 138 139 LogLevel *log.Level `yaml:"log_level"` 140 Logger *log.Entry `yaml:"-"` 141 } 142 143 func (w *AppsecRuntimeConfig) ClearResponse() { 144 w.Response = AppsecTempResponse{} 145 w.Response.Action = w.Config.DefaultPassAction 146 w.Response.BouncerHTTPResponseCode = w.Config.BouncerPassedHTTPCode 147 w.Response.UserHTTPResponseCode = w.Config.UserPassedHTTPCode 148 w.Response.SendEvent = true 149 w.Response.SendAlert = true 150 } 151 152 func (wc *AppsecConfig) LoadByPath(file string) error { 153 154 wc.Logger.Debugf("loading config %s", file) 155 156 yamlFile, err := os.ReadFile(file) 157 if err != nil { 158 return fmt.Errorf("unable to read file %s : %s", file, err) 159 } 160 err = yaml.UnmarshalStrict(yamlFile, wc) 161 if err != nil { 162 return fmt.Errorf("unable to parse yaml file %s : %s", file, err) 163 } 164 165 if wc.Name == "" { 166 return fmt.Errorf("name cannot be empty") 167 } 168 if wc.LogLevel == nil { 169 lvl := wc.Logger.Logger.GetLevel() 170 wc.LogLevel = &lvl 171 } 172 wc.Logger = wc.Logger.Dup().WithField("name", wc.Name) 173 wc.Logger.Logger.SetLevel(*wc.LogLevel) 174 return nil 175 } 176 177 func (wc *AppsecConfig) Load(configName string) error { 178 appsecConfigs := hub.GetItemMap(cwhub.APPSEC_CONFIGS) 179 180 for _, hubAppsecConfigItem := range appsecConfigs { 181 if !hubAppsecConfigItem.State.Installed { 182 continue 183 } 184 if hubAppsecConfigItem.Name != configName { 185 continue 186 } 187 wc.Logger.Infof("loading %s", hubAppsecConfigItem.State.LocalPath) 188 err := wc.LoadByPath(hubAppsecConfigItem.State.LocalPath) 189 if err != nil { 190 return fmt.Errorf("unable to load appsec-config %s : %s", hubAppsecConfigItem.State.LocalPath, err) 191 } 192 return nil 193 } 194 195 return fmt.Errorf("no appsec-config found for %s", configName) 196 } 197 198 func (wc *AppsecConfig) GetDataDir() string { 199 return hub.GetDataDir() 200 } 201 202 func (wc *AppsecConfig) Build() (*AppsecRuntimeConfig, error) { 203 ret := &AppsecRuntimeConfig{Logger: wc.Logger.WithField("component", "appsec_runtime_config")} 204 205 if wc.BouncerBlockedHTTPCode == 0 { 206 wc.BouncerBlockedHTTPCode = http.StatusForbidden 207 } 208 if wc.BouncerPassedHTTPCode == 0 { 209 wc.BouncerPassedHTTPCode = http.StatusOK 210 } 211 212 if wc.UserBlockedHTTPCode == 0 { 213 wc.UserBlockedHTTPCode = http.StatusForbidden 214 } 215 if wc.UserPassedHTTPCode == 0 { 216 wc.UserPassedHTTPCode = http.StatusOK 217 } 218 if wc.DefaultPassAction == "" { 219 wc.DefaultPassAction = AllowRemediation 220 } 221 if wc.DefaultRemediation == "" { 222 wc.DefaultRemediation = BanRemediation 223 } 224 225 //set the defaults 226 switch wc.DefaultRemediation { 227 case BanRemediation, CaptchaRemediation, AllowRemediation: 228 //those are the officially supported remediation(s) 229 default: 230 wc.Logger.Warningf("default '%s' remediation of %s is none of [%s,%s,%s] ensure bouncer compatbility!", wc.DefaultRemediation, wc.Name, BanRemediation, CaptchaRemediation, AllowRemediation) 231 } 232 233 ret.Name = wc.Name 234 ret.Config = wc 235 ret.DefaultRemediation = wc.DefaultRemediation 236 237 wc.Logger.Tracef("Loading config %+v", wc) 238 //load rules 239 for _, rule := range wc.OutOfBandRules { 240 wc.Logger.Infof("loading outofband rule %s", rule) 241 collections, err := LoadCollection(rule, wc.Logger.WithField("component", "appsec_collection_loader")) 242 if err != nil { 243 return nil, fmt.Errorf("unable to load outofband rule %s : %s", rule, err) 244 } 245 ret.OutOfBandRules = append(ret.OutOfBandRules, collections...) 246 } 247 248 wc.Logger.Infof("Loaded %d outofband rules", len(ret.OutOfBandRules)) 249 for _, rule := range wc.InBandRules { 250 wc.Logger.Infof("loading inband rule %s", rule) 251 collections, err := LoadCollection(rule, wc.Logger.WithField("component", "appsec_collection_loader")) 252 if err != nil { 253 return nil, fmt.Errorf("unable to load inband rule %s : %s", rule, err) 254 } 255 ret.InBandRules = append(ret.InBandRules, collections...) 256 } 257 258 wc.Logger.Infof("Loaded %d inband rules", len(ret.InBandRules)) 259 260 //load hooks 261 for _, hook := range wc.OnLoad { 262 err := hook.Build(hookOnLoad) 263 if err != nil { 264 return nil, fmt.Errorf("unable to build on_load hook : %s", err) 265 } 266 ret.CompiledOnLoad = append(ret.CompiledOnLoad, hook) 267 } 268 269 for _, hook := range wc.PreEval { 270 err := hook.Build(hookPreEval) 271 if err != nil { 272 return nil, fmt.Errorf("unable to build pre_eval hook : %s", err) 273 } 274 ret.CompiledPreEval = append(ret.CompiledPreEval, hook) 275 } 276 277 for _, hook := range wc.PostEval { 278 err := hook.Build(hookPostEval) 279 if err != nil { 280 return nil, fmt.Errorf("unable to build post_eval hook : %s", err) 281 } 282 ret.CompiledPostEval = append(ret.CompiledPostEval, hook) 283 } 284 285 for _, hook := range wc.OnMatch { 286 err := hook.Build(hookOnMatch) 287 if err != nil { 288 return nil, fmt.Errorf("unable to build on_match hook : %s", err) 289 } 290 ret.CompiledOnMatch = append(ret.CompiledOnMatch, hook) 291 } 292 293 //variable tracking 294 for _, variable := range wc.VariablesTracking { 295 compiledVariableRule, err := regexp.Compile(variable) 296 if err != nil { 297 return nil, fmt.Errorf("cannot compile variable regexp %s: %w", variable, err) 298 } 299 ret.CompiledVariablesTracking = append(ret.CompiledVariablesTracking, compiledVariableRule) 300 } 301 return ret, nil 302 } 303 304 func (w *AppsecRuntimeConfig) ProcessOnLoadRules() error { 305 for _, rule := range w.CompiledOnLoad { 306 if rule.FilterExpr != nil { 307 output, err := exprhelpers.Run(rule.FilterExpr, GetOnLoadEnv(w), w.Logger, w.Logger.Level >= log.DebugLevel) 308 if err != nil { 309 return fmt.Errorf("unable to run appsec on_load filter %s : %w", rule.Filter, err) 310 } 311 switch t := output.(type) { 312 case bool: 313 if !t { 314 w.Logger.Debugf("filter didnt match") 315 continue 316 } 317 default: 318 w.Logger.Errorf("Filter must return a boolean, can't filter") 319 continue 320 } 321 } 322 for _, applyExpr := range rule.ApplyExpr { 323 o, err := exprhelpers.Run(applyExpr, GetOnLoadEnv(w), w.Logger, w.Logger.Level >= log.DebugLevel) 324 if err != nil { 325 w.Logger.Errorf("unable to apply appsec on_load expr: %s", err) 326 continue 327 } 328 switch t := o.(type) { 329 case error: 330 w.Logger.Errorf("unable to apply appsec on_load expr: %s", t) 331 continue 332 default: 333 } 334 } 335 } 336 return nil 337 } 338 339 func (w *AppsecRuntimeConfig) ProcessOnMatchRules(request *ParsedRequest, evt types.Event) error { 340 341 for _, rule := range w.CompiledOnMatch { 342 if rule.FilterExpr != nil { 343 output, err := exprhelpers.Run(rule.FilterExpr, GetOnMatchEnv(w, request, evt), w.Logger, w.Logger.Level >= log.DebugLevel) 344 if err != nil { 345 return fmt.Errorf("unable to run appsec on_match filter %s : %w", rule.Filter, err) 346 } 347 switch t := output.(type) { 348 case bool: 349 if !t { 350 w.Logger.Debugf("filter didnt match") 351 continue 352 } 353 default: 354 w.Logger.Errorf("Filter must return a boolean, can't filter") 355 continue 356 } 357 } 358 for _, applyExpr := range rule.ApplyExpr { 359 o, err := exprhelpers.Run(applyExpr, GetOnMatchEnv(w, request, evt), w.Logger, w.Logger.Level >= log.DebugLevel) 360 if err != nil { 361 w.Logger.Errorf("unable to apply appsec on_match expr: %s", err) 362 continue 363 } 364 switch t := o.(type) { 365 case error: 366 w.Logger.Errorf("unable to apply appsec on_match expr: %s", t) 367 continue 368 default: 369 } 370 } 371 } 372 return nil 373 } 374 375 func (w *AppsecRuntimeConfig) ProcessPreEvalRules(request *ParsedRequest) error { 376 w.Logger.Debugf("processing %d pre_eval rules", len(w.CompiledPreEval)) 377 for _, rule := range w.CompiledPreEval { 378 if rule.FilterExpr != nil { 379 output, err := exprhelpers.Run(rule.FilterExpr, GetPreEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel) 380 if err != nil { 381 return fmt.Errorf("unable to run appsec pre_eval filter %s : %w", rule.Filter, err) 382 } 383 switch t := output.(type) { 384 case bool: 385 if !t { 386 w.Logger.Debugf("filter didnt match") 387 continue 388 } 389 default: 390 w.Logger.Errorf("Filter must return a boolean, can't filter") 391 continue 392 } 393 } 394 // here means there is no filter or the filter matched 395 for _, applyExpr := range rule.ApplyExpr { 396 o, err := exprhelpers.Run(applyExpr, GetPreEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel) 397 if err != nil { 398 w.Logger.Errorf("unable to apply appsec pre_eval expr: %s", err) 399 continue 400 } 401 switch t := o.(type) { 402 case error: 403 w.Logger.Errorf("unable to apply appsec pre_eval expr: %s", t) 404 continue 405 default: 406 } 407 } 408 } 409 410 return nil 411 } 412 413 func (w *AppsecRuntimeConfig) ProcessPostEvalRules(request *ParsedRequest) error { 414 for _, rule := range w.CompiledPostEval { 415 if rule.FilterExpr != nil { 416 output, err := exprhelpers.Run(rule.FilterExpr, GetPostEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel) 417 if err != nil { 418 return fmt.Errorf("unable to run appsec post_eval filter %s : %w", rule.Filter, err) 419 } 420 switch t := output.(type) { 421 case bool: 422 if !t { 423 w.Logger.Debugf("filter didnt match") 424 continue 425 } 426 default: 427 w.Logger.Errorf("Filter must return a boolean, can't filter") 428 continue 429 } 430 } 431 // here means there is no filter or the filter matched 432 for _, applyExpr := range rule.ApplyExpr { 433 o, err := exprhelpers.Run(applyExpr, GetPostEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel) 434 435 if err != nil { 436 w.Logger.Errorf("unable to apply appsec post_eval expr: %s", err) 437 continue 438 } 439 440 switch t := o.(type) { 441 case error: 442 w.Logger.Errorf("unable to apply appsec post_eval expr: %s", t) 443 continue 444 default: 445 } 446 } 447 } 448 449 return nil 450 } 451 452 func (w *AppsecRuntimeConfig) RemoveInbandRuleByID(id int) error { 453 w.Logger.Debugf("removing inband rule %d", id) 454 return w.InBandTx.RemoveRuleByIDWithError(id) 455 } 456 457 func (w *AppsecRuntimeConfig) RemoveOutbandRuleByID(id int) error { 458 w.Logger.Debugf("removing outband rule %d", id) 459 return w.OutOfBandTx.RemoveRuleByIDWithError(id) 460 } 461 462 func (w *AppsecRuntimeConfig) RemoveInbandRuleByTag(tag string) error { 463 w.Logger.Debugf("removing inband rule with tag %s", tag) 464 return w.InBandTx.RemoveRuleByTagWithError(tag) 465 } 466 467 func (w *AppsecRuntimeConfig) RemoveOutbandRuleByTag(tag string) error { 468 w.Logger.Debugf("removing outband rule with tag %s", tag) 469 return w.OutOfBandTx.RemoveRuleByTagWithError(tag) 470 } 471 472 func (w *AppsecRuntimeConfig) RemoveInbandRuleByName(name string) error { 473 tag := fmt.Sprintf("crowdsec-%s", name) 474 w.Logger.Debugf("removing inband rule %s", tag) 475 return w.InBandTx.RemoveRuleByTagWithError(tag) 476 } 477 478 func (w *AppsecRuntimeConfig) RemoveOutbandRuleByName(name string) error { 479 tag := fmt.Sprintf("crowdsec-%s", name) 480 w.Logger.Debugf("removing outband rule %s", tag) 481 return w.OutOfBandTx.RemoveRuleByTagWithError(tag) 482 } 483 484 func (w *AppsecRuntimeConfig) CancelEvent() error { 485 w.Logger.Debugf("canceling event") 486 w.Response.SendEvent = false 487 return nil 488 } 489 490 // Disable a rule at load time, meaning it will not run for any request 491 func (w *AppsecRuntimeConfig) DisableInBandRuleByID(id int) error { 492 w.DisabledInBandRuleIds = append(w.DisabledInBandRuleIds, id) 493 return nil 494 } 495 496 // Disable a rule at load time, meaning it will not run for any request 497 func (w *AppsecRuntimeConfig) DisableInBandRuleByName(name string) error { 498 tagValue := fmt.Sprintf("crowdsec-%s", name) 499 w.DisabledInBandRulesTags = append(w.DisabledInBandRulesTags, tagValue) 500 return nil 501 } 502 503 // Disable a rule at load time, meaning it will not run for any request 504 func (w *AppsecRuntimeConfig) DisableInBandRuleByTag(tag string) error { 505 w.DisabledInBandRulesTags = append(w.DisabledInBandRulesTags, tag) 506 return nil 507 } 508 509 // Disable a rule at load time, meaning it will not run for any request 510 func (w *AppsecRuntimeConfig) DisableOutBandRuleByID(id int) error { 511 w.DisabledOutOfBandRuleIds = append(w.DisabledOutOfBandRuleIds, id) 512 return nil 513 } 514 515 // Disable a rule at load time, meaning it will not run for any request 516 func (w *AppsecRuntimeConfig) DisableOutBandRuleByName(name string) error { 517 tagValue := fmt.Sprintf("crowdsec-%s", name) 518 w.DisabledOutOfBandRulesTags = append(w.DisabledOutOfBandRulesTags, tagValue) 519 return nil 520 } 521 522 // Disable a rule at load time, meaning it will not run for any request 523 func (w *AppsecRuntimeConfig) DisableOutBandRuleByTag(tag string) error { 524 w.DisabledOutOfBandRulesTags = append(w.DisabledOutOfBandRulesTags, tag) 525 return nil 526 } 527 528 func (w *AppsecRuntimeConfig) SendEvent() error { 529 w.Logger.Debugf("sending event") 530 w.Response.SendEvent = true 531 return nil 532 } 533 534 func (w *AppsecRuntimeConfig) SendAlert() error { 535 w.Logger.Debugf("sending alert") 536 w.Response.SendAlert = true 537 return nil 538 } 539 540 func (w *AppsecRuntimeConfig) CancelAlert() error { 541 w.Logger.Debugf("canceling alert") 542 w.Response.SendAlert = false 543 return nil 544 } 545 546 func (w *AppsecRuntimeConfig) SetActionByTag(tag string, action string) error { 547 if w.RemediationByTag == nil { 548 w.RemediationByTag = make(map[string]string) 549 } 550 w.Logger.Debugf("setting action of %s to %s", tag, action) 551 w.RemediationByTag[tag] = action 552 return nil 553 } 554 555 func (w *AppsecRuntimeConfig) SetActionByID(id int, action string) error { 556 if w.RemediationById == nil { 557 w.RemediationById = make(map[int]string) 558 } 559 w.Logger.Debugf("setting action of %d to %s", id, action) 560 w.RemediationById[id] = action 561 return nil 562 } 563 564 func (w *AppsecRuntimeConfig) SetActionByName(name string, action string) error { 565 if w.RemediationByTag == nil { 566 w.RemediationByTag = make(map[string]string) 567 } 568 tag := fmt.Sprintf("crowdsec-%s", name) 569 w.Logger.Debugf("setting action of %s to %s", tag, action) 570 w.RemediationByTag[tag] = action 571 return nil 572 } 573 574 func (w *AppsecRuntimeConfig) SetAction(action string) error { 575 //log.Infof("setting to %s", action) 576 w.Logger.Debugf("setting action to %s", action) 577 w.Response.Action = action 578 return nil 579 } 580 581 func (w *AppsecRuntimeConfig) SetHTTPCode(code int) error { 582 w.Logger.Debugf("setting http code to %d", code) 583 w.Response.UserHTTPResponseCode = code 584 return nil 585 } 586 587 type BodyResponse struct { 588 Action string `json:"action"` 589 HTTPStatus int `json:"http_status"` 590 } 591 592 func (w *AppsecRuntimeConfig) GenerateResponse(response AppsecTempResponse, logger *log.Entry) (int, BodyResponse) { 593 var bouncerStatusCode int 594 595 resp := BodyResponse{Action: response.Action} 596 if response.Action == AllowRemediation { 597 resp.HTTPStatus = w.Config.UserPassedHTTPCode 598 bouncerStatusCode = w.Config.BouncerPassedHTTPCode 599 } else { //ban, captcha and anything else 600 resp.HTTPStatus = response.UserHTTPResponseCode 601 if resp.HTTPStatus == 0 { 602 resp.HTTPStatus = w.Config.UserBlockedHTTPCode 603 } 604 bouncerStatusCode = response.BouncerHTTPResponseCode 605 if bouncerStatusCode == 0 { 606 bouncerStatusCode = w.Config.BouncerBlockedHTTPCode 607 } 608 } 609 610 return bouncerStatusCode, resp 611 }