crowdsec/pkg/waf/waap.go

491 lines
14 KiB
Go
Raw Normal View History

2023-09-11 08:35:14 +00:00
package waf
import (
"fmt"
2023-09-12 16:17:58 +00:00
"os"
2023-09-11 08:35:14 +00:00
"regexp"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
2023-11-08 19:24:44 +00:00
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
2023-09-11 08:35:14 +00:00
log "github.com/sirupsen/logrus"
2023-09-12 16:17:58 +00:00
"gopkg.in/yaml.v2"
2023-09-11 08:35:14 +00:00
)
type Hook struct {
Filter string `yaml:"filter"`
FilterExpr *vm.Program `yaml:"-"`
OnSuccess string `yaml:"on_success"`
Apply []string `yaml:"apply"`
ApplyExpr []*vm.Program `yaml:"-"`
}
2023-11-27 09:43:32 +00:00
const (
hookOnLoad = iota
hookPreEval
hookOnMatch
)
2023-09-11 08:35:14 +00:00
2023-11-27 09:43:32 +00:00
// @tko : todo - debug mode
func (h *Hook) Build(hookStage int) error {
ctx := map[string]interface{}{}
switch hookStage {
case hookOnLoad:
ctx = GetOnLoadEnv(&WaapRuntimeConfig{})
case hookPreEval:
ctx = GetPreEvalEnv(&WaapRuntimeConfig{}, &ParsedRequest{})
case hookOnMatch:
ctx = GetOnMatchEnv(&WaapRuntimeConfig{}, &ParsedRequest{})
}
opts := GetExprWAFOptions(ctx)
2023-09-11 08:35:14 +00:00
if h.Filter != "" {
2023-11-27 09:43:32 +00:00
program, err := expr.Compile(h.Filter, opts...) //FIXME: opts
2023-09-11 08:35:14 +00:00
if err != nil {
return fmt.Errorf("unable to compile filter %s : %w", h.Filter, err)
}
h.FilterExpr = program
}
for _, apply := range h.Apply {
2023-11-27 09:43:32 +00:00
program, err := expr.Compile(apply, opts...)
2023-09-11 08:35:14 +00:00
if err != nil {
return fmt.Errorf("unable to compile apply %s : %w", apply, err)
}
h.ApplyExpr = append(h.ApplyExpr, program)
}
return nil
}
2023-09-13 15:12:09 +00:00
type WaapTempResponse struct {
InBandInterrupt bool
OutOfBandInterrupt bool
Action string //allow, deny, captcha, log
HTTPResponseCode int
SendEvent bool //do we send an internal event on rule match
2023-11-24 14:57:49 +00:00
SendAlert bool //do we send an alert on rule match
2023-09-13 15:12:09 +00:00
}
type WaapSubEngineOpts struct {
DisableBodyInspection bool `yaml:"disable_body_inspection"`
RequestBodyInMemoryLimit *int `yaml:"request_body_in_memory_limit"`
}
2023-09-11 08:35:14 +00:00
// runtime version of WaapConfig
type WaapRuntimeConfig struct {
2023-09-19 06:54:31 +00:00
Name string
OutOfBandRules []WaapCollection
InBandRules []WaapCollection
2023-09-11 08:35:14 +00:00
DefaultRemediation string
CompiledOnLoad []Hook
CompiledPreEval []Hook
CompiledOnMatch []Hook
CompiledVariablesTracking []*regexp.Regexp
2023-09-13 15:12:09 +00:00
Config *WaapConfig
2023-10-23 08:54:11 +00:00
//CorazaLogger debuglog.Logger
2023-09-13 15:12:09 +00:00
//those are ephemeral, created/destroyed with every req
OutOfBandTx ExtendedTransaction //is it a good idea ?
InBandTx ExtendedTransaction //is it a good idea ?
Response WaapTempResponse
2023-09-14 07:43:22 +00:00
//should we store matched rules here ?
2023-11-27 12:14:40 +00:00
Logger *log.Entry
2023-09-11 08:35:14 +00:00
}
type WaapConfig struct {
Name string `yaml:"name"`
OutOfBandRules []string `yaml:"outofband_rules"`
InBandRules []string `yaml:"inband_rules"`
DefaultRemediation string `yaml:"default_remediation"`
DefaultPassAction string `yaml:"default_pass_action"`
BlockedHTTPCode int `yaml:"blocked_http_code"`
PassedHTTPCode int `yaml:"passed_http_code"`
OnLoad []Hook `yaml:"on_load"`
PreEval []Hook `yaml:"pre_eval"`
OnMatch []Hook `yaml:"on_match"`
VariablesTracking []string `yaml:"variables_tracking"`
InbandOptions WaapSubEngineOpts `yaml:"inband_options"`
OutOfBandOptions WaapSubEngineOpts `yaml:"outofband_options"`
LogLevel *log.Level `yaml:"log_level"`
Logger *log.Entry `yaml:"-"`
2023-09-11 08:35:14 +00:00
}
2023-09-13 15:12:09 +00:00
func (w *WaapRuntimeConfig) ClearResponse() {
log.Debugf("#-> %p", w)
2023-09-13 15:12:09 +00:00
w.Response = WaapTempResponse{}
log.Debugf("-> %p", w.Config)
2023-09-13 15:12:09 +00:00
w.Response.Action = w.Config.DefaultPassAction
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
2023-11-29 16:45:06 +00:00
w.Response.SendEvent = false
w.Response.SendAlert = true
2023-09-13 15:12:09 +00:00
}
2023-11-08 19:24:44 +00:00
func (wc *WaapConfig) LoadByPath(file string) error {
2023-09-14 09:18:33 +00:00
wc.Logger.Debugf("loading config %s", file)
2023-09-12 16:17:58 +00:00
yamlFile, err := os.ReadFile(file)
if err != nil {
return fmt.Errorf("unable to read file %s : %s", file, err)
}
err = yaml.UnmarshalStrict(yamlFile, wc)
if err != nil {
return fmt.Errorf("unable to parse yaml file %s : %s", file, err)
}
2023-10-23 08:54:11 +00:00
2023-09-13 15:12:09 +00:00
if wc.Name == "" {
return fmt.Errorf("name cannot be empty")
}
2023-10-26 10:03:57 +00:00
if wc.LogLevel == nil {
lvl := log.InfoLevel
wc.LogLevel = &lvl
}
2023-10-23 08:54:11 +00:00
wc.Logger = wc.Logger.WithField("name", wc.Name)
2023-10-26 10:03:57 +00:00
wc.Logger.Logger.SetLevel(*wc.LogLevel)
2023-09-13 15:12:09 +00:00
if wc.DefaultRemediation == "" {
return fmt.Errorf("default_remediation cannot be empty")
}
switch wc.DefaultRemediation {
case "ban", "captcha", "log":
//those are the officially supported remediation(s)
default:
wc.Logger.Warningf("default '%s' remediation of %s is none of [ban,captcha,log] ensure bouncer compatbility!", wc.DefaultRemediation, file)
}
2023-09-13 15:12:09 +00:00
if wc.BlockedHTTPCode == 0 {
wc.BlockedHTTPCode = 403
}
if wc.PassedHTTPCode == 0 {
wc.PassedHTTPCode = 200
}
if wc.DefaultPassAction == "" {
wc.DefaultPassAction = "allow"
}
2023-09-12 16:17:58 +00:00
return nil
}
2023-11-08 19:24:44 +00:00
func (wc *WaapConfig) Load(configName string) error {
waapConfigs := hub.GetItemMap(cwhub.WAAP_CONFIGS)
for _, hubWaapConfigItem := range waapConfigs {
2023-11-21 16:28:10 +00:00
if !hubWaapConfigItem.State.Installed {
2023-11-08 19:24:44 +00:00
continue
}
if hubWaapConfigItem.Name != configName {
continue
}
2023-11-21 16:28:10 +00:00
wc.Logger.Infof("loading %s", hubWaapConfigItem.State.LocalPath)
err := wc.LoadByPath(hubWaapConfigItem.State.LocalPath)
2023-11-08 19:24:44 +00:00
if err != nil {
2023-11-21 16:28:10 +00:00
return fmt.Errorf("unable to load waap-config %s : %s", hubWaapConfigItem.State.LocalPath, err)
2023-11-08 19:24:44 +00:00
}
2023-11-08 19:37:05 +00:00
return nil
2023-11-08 19:24:44 +00:00
}
2023-11-08 19:37:05 +00:00
return fmt.Errorf("no waap-config found for %s", configName)
2023-11-08 19:24:44 +00:00
}
2023-11-10 16:56:04 +00:00
func (wc *WaapConfig) GetDataDir() string {
return hub.GetDataDir()
}
2023-09-11 08:35:14 +00:00
func (wc *WaapConfig) Build() (*WaapRuntimeConfig, error) {
2023-11-27 12:14:40 +00:00
ret := &WaapRuntimeConfig{Logger: wc.Logger.WithField("component", "waap_runtime_config")}
2023-09-11 08:35:14 +00:00
ret.Name = wc.Name
2023-09-13 15:12:09 +00:00
ret.Config = wc
2023-09-11 08:35:14 +00:00
ret.DefaultRemediation = wc.DefaultRemediation
//load rules
for _, rule := range wc.OutOfBandRules {
2023-09-19 11:16:33 +00:00
wc.Logger.Infof("loading outofband rule %s", rule)
2023-11-08 20:14:03 +00:00
collections, err := LoadCollection(rule)
2023-09-11 08:35:14 +00:00
if err != nil {
return nil, fmt.Errorf("unable to load outofband rule %s : %s", rule, err)
}
2023-11-08 20:14:03 +00:00
ret.OutOfBandRules = append(ret.OutOfBandRules, collections...)
2023-09-11 08:35:14 +00:00
}
2023-09-19 11:16:33 +00:00
wc.Logger.Infof("Loaded %d outofband rules", len(ret.OutOfBandRules))
2023-09-11 08:35:14 +00:00
for _, rule := range wc.InBandRules {
2023-09-19 11:16:33 +00:00
wc.Logger.Infof("loading inband rule %s", rule)
2023-11-08 20:14:03 +00:00
collections, err := LoadCollection(rule)
2023-09-11 08:35:14 +00:00
if err != nil {
return nil, fmt.Errorf("unable to load inband rule %s : %s", rule, err)
}
2023-11-08 20:14:03 +00:00
ret.InBandRules = append(ret.InBandRules, collections...)
2023-09-11 08:35:14 +00:00
}
2023-09-19 11:16:33 +00:00
wc.Logger.Infof("Loaded %d inband rules", len(ret.InBandRules))
2023-09-11 08:35:14 +00:00
//load hooks
for _, hook := range wc.OnLoad {
2023-11-27 09:43:32 +00:00
err := hook.Build(hookOnLoad)
2023-09-11 08:35:14 +00:00
if err != nil {
return nil, fmt.Errorf("unable to build on_load hook : %s", err)
}
ret.CompiledOnLoad = append(ret.CompiledOnLoad, hook)
}
for _, hook := range wc.PreEval {
2023-11-27 09:43:32 +00:00
err := hook.Build(hookPreEval)
2023-09-11 08:35:14 +00:00
if err != nil {
return nil, fmt.Errorf("unable to build pre_eval hook : %s", err)
}
ret.CompiledPreEval = append(ret.CompiledPreEval, hook)
}
for _, hook := range wc.OnMatch {
2023-11-27 09:43:32 +00:00
err := hook.Build(hookOnMatch)
2023-09-11 08:35:14 +00:00
if err != nil {
return nil, fmt.Errorf("unable to build on_match hook : %s", err)
}
ret.CompiledOnMatch = append(ret.CompiledOnMatch, hook)
}
//variable tracking
for _, variable := range wc.VariablesTracking {
compiledVariableRule, err := regexp.Compile(variable)
if err != nil {
return nil, fmt.Errorf("cannot compile variable regexp %s: %w", variable, err)
}
ret.CompiledVariablesTracking = append(ret.CompiledVariablesTracking, compiledVariableRule)
}
return ret, nil
}
2023-10-18 11:42:56 +00:00
func (w *WaapRuntimeConfig) ProcessOnLoadRules() error {
2023-11-23 13:51:05 +00:00
for _, rule := range w.CompiledOnLoad {
2023-10-18 11:42:56 +00:00
if rule.FilterExpr != nil {
2023-11-24 14:57:49 +00:00
output, err := expr.Run(rule.FilterExpr, GetOnLoadEnv(w))
2023-10-18 11:42:56 +00:00
if err != nil {
2023-11-27 09:43:32 +00:00
return fmt.Errorf("unable to run waap on_load filter %s : %w", rule.Filter, err)
2023-10-18 11:42:56 +00:00
}
switch t := output.(type) {
case bool:
if !t {
log.Infof("filter didnt match")
continue
}
default:
log.Errorf("Filter must return a boolean, can't filter")
continue
}
}
for _, applyExpr := range rule.ApplyExpr {
2023-11-24 14:57:49 +00:00
_, err := expr.Run(applyExpr, GetOnLoadEnv(w))
2023-10-18 11:42:56 +00:00
if err != nil {
2023-11-27 09:43:32 +00:00
log.Errorf("unable to apply waap on_load expr: %s", err)
2023-10-18 11:42:56 +00:00
continue
}
}
}
return nil
}
2023-11-27 09:43:32 +00:00
func (w *WaapRuntimeConfig) ProcessOnMatchRules(request *ParsedRequest) error {
2023-09-11 08:35:14 +00:00
for _, rule := range w.CompiledOnMatch {
if rule.FilterExpr != nil {
2023-11-24 14:57:49 +00:00
output, err := expr.Run(rule.FilterExpr, GetOnMatchEnv(w, request))
2023-09-11 08:35:14 +00:00
if err != nil {
2023-11-27 09:43:32 +00:00
return fmt.Errorf("unable to run waap on_match filter %s : %w", rule.Filter, err)
2023-09-11 08:35:14 +00:00
}
switch t := output.(type) {
case bool:
if !t {
log.Infof("filter didnt match")
continue
}
default:
log.Errorf("Filter must return a boolean, can't filter")
continue
}
}
for _, applyExpr := range rule.ApplyExpr {
2023-11-24 14:57:49 +00:00
_, err := expr.Run(applyExpr, GetOnMatchEnv(w, request))
2023-09-11 08:35:14 +00:00
if err != nil {
2023-11-27 09:43:32 +00:00
log.Errorf("unable to apply waap on_match expr: %s", err)
2023-09-11 08:35:14 +00:00
continue
}
}
}
return nil
}
2023-11-27 09:43:32 +00:00
func (w *WaapRuntimeConfig) ProcessPreEvalRules(request *ParsedRequest) error {
2023-09-11 08:35:14 +00:00
for _, rule := range w.CompiledPreEval {
if rule.FilterExpr != nil {
2023-11-24 14:57:49 +00:00
output, err := expr.Run(rule.FilterExpr, GetPreEvalEnv(w, request))
2023-09-11 08:35:14 +00:00
if err != nil {
2023-11-27 09:43:32 +00:00
return fmt.Errorf("unable to run waap pre_eval filter %s : %w", rule.Filter, err)
2023-09-11 08:35:14 +00:00
}
switch t := output.(type) {
case bool:
if !t {
log.Infof("filter didnt match")
continue
}
default:
log.Errorf("Filter must return a boolean, can't filter")
continue
}
}
// here means there is no filter or the filter matched
for _, applyExpr := range rule.ApplyExpr {
2023-11-24 14:57:49 +00:00
_, err := expr.Run(applyExpr, GetPreEvalEnv(w, request))
2023-09-11 08:35:14 +00:00
if err != nil {
2023-11-27 09:43:32 +00:00
log.Errorf("unable to apply waap pre_eval expr: %s", err)
2023-09-11 08:35:14 +00:00
continue
}
}
}
return nil
}
2023-09-13 08:57:29 +00:00
/* @sbl / @tko
add the helpers to:
- remove by id-range
- remove by tag
2023-09-13 15:12:09 +00:00
- set remediation by tag/id-range
2023-09-13 08:57:29 +00:00
*/
2023-11-24 14:57:49 +00:00
// func (w *WaapRuntimeConfig) RemoveInbandRuleByID(id int) error {
func (w *WaapRuntimeConfig) RemoveInbandRuleByID(params ...any) (any, error) {
id := params[0].(int)
2023-11-27 12:14:40 +00:00
w.Logger.Debugf("removing inband rule %d", id)
2023-11-24 14:57:49 +00:00
_ = w.InBandTx.RemoveRuleByIDWithError(id)
return nil, nil
2023-09-11 08:35:14 +00:00
}
2023-11-27 09:43:32 +00:00
// func (w *WaapRuntimeConfig) RemoveOutbandRuleByID(id int) error {
func (w *WaapRuntimeConfig) RemoveOutbandRuleByID(params ...any) (any, error) {
id := params[0].(int)
2023-11-27 12:14:40 +00:00
w.Logger.Debugf("removing outband rule %d", id)
2023-11-27 09:43:32 +00:00
_ = w.OutOfBandTx.RemoveRuleByIDWithError(id)
return nil, nil
}
2023-11-24 14:57:49 +00:00
func (w *WaapRuntimeConfig) CancelEvent(params ...any) (any, error) {
2023-11-27 12:14:40 +00:00
w.Logger.Debugf("canceling event")
2023-09-13 15:12:09 +00:00
w.Response.SendEvent = false
2023-11-24 14:57:49 +00:00
return nil, nil
2023-09-13 15:12:09 +00:00
}
2023-11-24 14:57:49 +00:00
// func (w *WaapRuntimeConfig) DisableInBandRuleByID(id int) error {
func (w *WaapRuntimeConfig) DisableInBandRuleByID(params ...any) (any, error) {
2023-09-19 06:54:31 +00:00
panic("not implemented")
2023-11-24 14:57:49 +00:00
return nil, nil
2023-09-19 06:54:31 +00:00
}
2023-11-24 14:57:49 +00:00
// func (w *WaapRuntimeConfig) DisableInBandRuleByTag(id int) error {
func (w *WaapRuntimeConfig) DisableInBandRuleByTag(params ...any) (any, error) {
2023-09-13 15:12:09 +00:00
panic("not implemented")
2023-11-24 14:57:49 +00:00
return nil, nil
}
// func (w *WaapRuntimeConfig) DisableOutBandRuleByID(tag string) error {
func (w *WaapRuntimeConfig) DisableOutBandRuleByID(params ...any) (any, error) {
panic("not implemented")
return nil, nil
}
// func (w *WaapRuntimeConfig) DisableOutBandRuleByTag(tag string) error {
func (w *WaapRuntimeConfig) DisableOutBandRuleByTag(params ...any) (any, error) {
panic("not implemented")
return nil, nil
}
func (w *WaapRuntimeConfig) SendEvent(params ...any) (any, error) {
2023-11-27 12:14:40 +00:00
w.Logger.Debugf("sending event")
2023-11-24 14:57:49 +00:00
w.Response.SendEvent = true
return nil, nil
2023-09-13 15:12:09 +00:00
}
2023-11-24 14:57:49 +00:00
func (w *WaapRuntimeConfig) SendAlert(params ...any) (any, error) {
2023-11-27 12:14:40 +00:00
w.Logger.Debugf("sending alert")
2023-11-24 14:57:49 +00:00
w.Response.SendAlert = true
return nil, nil
2023-09-13 15:12:09 +00:00
}
2023-11-24 14:57:49 +00:00
func (w *WaapRuntimeConfig) CancelAlert(params ...any) (any, error) {
2023-11-27 12:14:40 +00:00
w.Logger.Debugf("canceling alert")
2023-11-24 14:57:49 +00:00
w.Response.SendAlert = false
return nil, nil
}
// func (w *WaapRuntimeConfig) SetActionByTag(tag string, action string) error {
func (w *WaapRuntimeConfig) SetActionByTag(params ...any) (any, error) {
panic("not implemented")
return nil, nil
}
// func (w *WaapRuntimeConfig) SetActionByID(id int, action string) error {
func (w *WaapRuntimeConfig) SetActionByID(params ...any) (any, error) {
panic("not implemented")
return nil, nil
}
// func (w *WaapRuntimeConfig) SetAction(action string) error {
func (w *WaapRuntimeConfig) SetAction(params ...any) (any, error) {
2023-09-14 07:43:22 +00:00
//log.Infof("setting to %s", action)
2023-11-24 14:57:49 +00:00
action := params[0].(string)
2023-11-27 12:14:40 +00:00
w.Logger.Debugf("setting action to %s", action)
2023-09-13 16:03:03 +00:00
switch action {
case "allow":
w.Response.Action = action
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
2023-09-14 07:43:22 +00:00
//@tko how should we handle this ? it seems bouncer only understand bans, but it might be misleading ?
2023-09-13 16:03:03 +00:00
case "deny", "ban", "block":
w.Response.Action = "ban"
case "log":
w.Response.Action = action
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
case "captcha":
w.Response.Action = action
default:
2023-11-24 14:57:49 +00:00
return nil, fmt.Errorf("unknown action %s", action)
2023-09-13 16:03:03 +00:00
}
2023-11-24 14:57:49 +00:00
return nil, nil
2023-09-13 15:12:09 +00:00
}
2023-11-24 14:57:49 +00:00
// func (w *WaapRuntimeConfig) SetHTTPCode(code int) error {
func (w *WaapRuntimeConfig) SetHTTPCode(params ...any) (any, error) {
code := params[0].(int)
2023-11-27 12:14:40 +00:00
w.Logger.Debugf("setting http code to %d", code)
2023-09-13 15:12:09 +00:00
w.Response.HTTPResponseCode = code
2023-11-24 14:57:49 +00:00
return nil, nil
2023-09-11 08:35:14 +00:00
}
2023-09-13 15:12:09 +00:00
type BodyResponse struct {
Action string `json:"action"`
HTTPStatus int `json:"http_status"`
}
2023-11-28 10:02:29 +00:00
func (w *WaapRuntimeConfig) GenerateResponse(response WaapTempResponse) BodyResponse {
2023-09-13 15:12:09 +00:00
resp := BodyResponse{}
//if there is no interrupt, we should allow with default code
2023-11-28 10:02:29 +00:00
if !response.InBandInterrupt {
2023-09-13 15:12:09 +00:00
resp.Action = w.Config.DefaultPassAction
resp.HTTPStatus = w.Config.PassedHTTPCode
2023-09-19 11:16:33 +00:00
return resp
2023-09-13 15:12:09 +00:00
}
2023-11-28 10:02:29 +00:00
resp.Action = response.Action
2023-11-27 12:14:40 +00:00
if resp.Action == "" {
resp.Action = w.Config.DefaultRemediation
}
w.Logger.Debugf("action is %s", resp.Action)
2023-09-13 15:12:09 +00:00
2023-11-28 10:02:29 +00:00
resp.HTTPStatus = response.HTTPResponseCode
2023-11-27 12:14:40 +00:00
if resp.HTTPStatus == 0 {
resp.HTTPStatus = w.Config.BlockedHTTPCode
}
w.Logger.Debugf("http status is %d", resp.HTTPStatus)
2023-09-19 11:16:33 +00:00
return resp
2023-09-13 15:12:09 +00:00
}