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-09-13 15:12:09 +00:00
|
|
|
// @tko : todo - debug mode
|
2023-09-11 08:35:14 +00:00
|
|
|
func (h *Hook) Build() error {
|
|
|
|
|
|
|
|
if h.Filter != "" {
|
|
|
|
program, err := expr.Compile(h.Filter) //FIXME: opts
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to compile filter %s : %w", h.Filter, err)
|
|
|
|
}
|
|
|
|
h.FilterExpr = program
|
|
|
|
}
|
|
|
|
for _, apply := range h.Apply {
|
2023-09-13 16:03:03 +00:00
|
|
|
program, err := expr.Compile(apply, GetExprWAFOptions(GetHookEnv(&WaapRuntimeConfig{}, ParsedRequest{}))...)
|
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-10-26 12:46:08 +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
|
2023-10-26 12:46:08 +00:00
|
|
|
|
|
|
|
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-10-26 12:46:08 +00:00
|
|
|
|
2023-09-11 08:35:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type WaapConfig struct {
|
2023-10-26 12:46:08 +00:00
|
|
|
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() {
|
2023-10-20 11:32:20 +00:00
|
|
|
log.Debugf("#-> %p", w)
|
2023-09-13 15:12:09 +00:00
|
|
|
w.Response = WaapTempResponse{}
|
2023-10-20 11:32:20 +00:00
|
|
|
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
|
|
|
|
w.Response.SendEvent = true
|
|
|
|
}
|
|
|
|
|
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")
|
|
|
|
}
|
2023-10-20 11:32:20 +00:00
|
|
|
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 {
|
|
|
|
hub, err := cwhub.GetHub()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to load hub : %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
waapConfigs := hub.GetItemMap(cwhub.WAAP_CONFIGS)
|
|
|
|
|
|
|
|
for _, hubWaapConfigItem := range waapConfigs {
|
|
|
|
if !hubWaapConfigItem.Installed {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if hubWaapConfigItem.Name != configName {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
wc.Logger.Infof("loading %s", hubWaapConfigItem.LocalPath)
|
|
|
|
err = wc.LoadByPath(hubWaapConfigItem.LocalPath)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to load waap-config %s : %s", hubWaapConfigItem.LocalPath, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-09-11 08:35:14 +00:00
|
|
|
func (wc *WaapConfig) Build() (*WaapRuntimeConfig, error) {
|
|
|
|
ret := &WaapRuntimeConfig{}
|
|
|
|
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-09-11 08:35:14 +00:00
|
|
|
collection, err := LoadCollection(rule)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to load outofband rule %s : %s", rule, err)
|
|
|
|
}
|
|
|
|
ret.OutOfBandRules = append(ret.OutOfBandRules, collection)
|
|
|
|
}
|
|
|
|
|
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-09-11 08:35:14 +00:00
|
|
|
collection, err := LoadCollection(rule)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to load inband rule %s : %s", rule, err)
|
|
|
|
}
|
|
|
|
ret.InBandRules = append(ret.InBandRules, collection)
|
|
|
|
}
|
|
|
|
|
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 {
|
|
|
|
err := hook.Build()
|
|
|
|
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 {
|
|
|
|
err := hook.Build()
|
|
|
|
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 {
|
|
|
|
err := hook.Build()
|
|
|
|
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 {
|
|
|
|
for _, rule := range w.CompiledOnMatch {
|
|
|
|
if rule.FilterExpr != nil {
|
|
|
|
output, err := expr.Run(rule.FilterExpr, GetHookEnv(w, ParsedRequest{}))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to run filter %s : %w", rule.Filter, err)
|
|
|
|
}
|
|
|
|
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 {
|
|
|
|
_, err := expr.Run(applyExpr, GetHookEnv(w, ParsedRequest{}))
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("unable to apply filter: %s", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-09-13 15:34:53 +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-09-14 07:43:22 +00:00
|
|
|
output, err := expr.Run(rule.FilterExpr, GetHookEnv(w, request))
|
2023-09-11 08:35:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to run filter %s : %w", rule.Filter, err)
|
|
|
|
}
|
|
|
|
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 {
|
|
|
|
_, err := expr.Run(applyExpr, map[string]interface{}{
|
2023-09-13 15:12:09 +00:00
|
|
|
// "req": request,
|
|
|
|
// "RemoveInbandRuleByID": w.RemoveInbandRuleByID,
|
|
|
|
// "RemoveOutbandRuleByID": w.RemoveOutbandRuleByID,
|
|
|
|
// "SetAction": response.SetAction,
|
|
|
|
// "SetRemediationByID": response.SetRemediationByID,
|
|
|
|
// "CancelEvent": response.CancelEvent,
|
2023-09-11 08:35:14 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("unable to apply filter: %s", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *WaapRuntimeConfig) ProcessPreEvalRules(request ParsedRequest) error {
|
|
|
|
for _, rule := range w.CompiledPreEval {
|
|
|
|
if rule.FilterExpr != nil {
|
2023-09-13 16:03:03 +00:00
|
|
|
output, err := expr.Run(rule.FilterExpr, GetHookEnv(w, request))
|
2023-09-11 08:35:14 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to run filter %s : %w", rule.Filter, err)
|
|
|
|
}
|
|
|
|
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-09-13 16:03:03 +00:00
|
|
|
_, err := expr.Run(applyExpr, GetHookEnv(w, request))
|
2023-09-11 08:35:14 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Errorf("unable to apply filter: %s", err)
|
|
|
|
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-09-13 15:12:09 +00:00
|
|
|
func (w *WaapRuntimeConfig) RemoveInbandRuleByID(id int) error {
|
|
|
|
return w.InBandTx.RemoveRuleByIDWithError(id)
|
2023-09-11 08:35:14 +00:00
|
|
|
}
|
|
|
|
|
2023-09-13 15:12:09 +00:00
|
|
|
func (w *WaapRuntimeConfig) CancelEvent() error {
|
|
|
|
w.Response.SendEvent = false
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-09-19 06:54:31 +00:00
|
|
|
func (w *WaapRuntimeConfig) SetActionByTag(tag string, action string) error {
|
|
|
|
panic("not implemented")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *WaapRuntimeConfig) SetActionByID(id int, action string) error {
|
2023-09-13 15:12:09 +00:00
|
|
|
panic("not implemented")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *WaapRuntimeConfig) RemoveOutbandRuleByID(id int) error {
|
|
|
|
return w.OutOfBandTx.RemoveRuleByIDWithError(id)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *WaapRuntimeConfig) SetAction(action string) error {
|
2023-09-14 07:43:22 +00:00
|
|
|
//log.Infof("setting 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"
|
|
|
|
w.Response.HTTPResponseCode = w.Config.BlockedHTTPCode
|
|
|
|
case "log":
|
|
|
|
w.Response.Action = action
|
|
|
|
w.Response.HTTPResponseCode = w.Config.PassedHTTPCode
|
|
|
|
case "captcha":
|
|
|
|
w.Response.Action = action
|
|
|
|
w.Response.HTTPResponseCode = w.Config.BlockedHTTPCode
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unknown action %s", action)
|
|
|
|
}
|
2023-09-13 15:12:09 +00:00
|
|
|
return nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *WaapRuntimeConfig) SetHTTPCode(code int) error {
|
|
|
|
w.Response.HTTPResponseCode = code
|
|
|
|
return 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-09-19 11:16:33 +00:00
|
|
|
func (w *WaapRuntimeConfig) GenerateResponse(interrupted bool) BodyResponse {
|
2023-09-13 15:12:09 +00:00
|
|
|
resp := BodyResponse{}
|
|
|
|
//if there is no interrupt, we should allow with default code
|
|
|
|
if !interrupted {
|
|
|
|
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
|
|
|
}
|
|
|
|
resp.Action = w.Config.DefaultRemediation
|
|
|
|
resp.HTTPStatus = w.Config.BlockedHTTPCode
|
|
|
|
|
2023-09-19 11:16:33 +00:00
|
|
|
return resp
|
2023-09-13 15:12:09 +00:00
|
|
|
}
|