From 08794c5b6de154ec3b6acd5ef37039f3386c5cd7 Mon Sep 17 00:00:00 2001 From: mmetc <92726601+mmetc@users.noreply.github.com> Date: Tue, 16 Jan 2024 11:39:23 +0100 Subject: [PATCH] [appsec] waf tester (#2746) --- pkg/acquisition/modules/appsec/appsec_test.go | 726 ++++++++++++++++++ pkg/appsec/appsec.go | 39 +- pkg/appsec/appsec_rule/appsec_rule.go | 4 +- pkg/appsec/appsec_rule/modsec_rule_test.go | 30 +- 4 files changed, 763 insertions(+), 36 deletions(-) create mode 100644 pkg/acquisition/modules/appsec/appsec_test.go diff --git a/pkg/acquisition/modules/appsec/appsec_test.go b/pkg/acquisition/modules/appsec/appsec_test.go new file mode 100644 index 000000000..9a54a94d7 --- /dev/null +++ b/pkg/acquisition/modules/appsec/appsec_test.go @@ -0,0 +1,726 @@ +package appsecacquisition + +import ( + "net/url" + "testing" + "time" + + "github.com/crowdsecurity/crowdsec/pkg/appsec" + "github.com/crowdsecurity/crowdsec/pkg/appsec/appsec_rule" + "github.com/crowdsecurity/crowdsec/pkg/types" + "github.com/davecgh/go-spew/spew" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +/* +Missing tests (wip): + - GenerateResponse + - evt.Appsec and it's subobjects and methods +*/ + +type appsecRuleTest struct { + name string + expected_load_ok bool + inband_rules []appsec_rule.CustomRule + outofband_rules []appsec_rule.CustomRule + on_load []appsec.Hook + pre_eval []appsec.Hook + post_eval []appsec.Hook + on_match []appsec.Hook + input_request appsec.ParsedRequest + output_asserts func(events []types.Event, responses []appsec.AppsecTempResponse) +} + +func TestAppsecOnMatchHooks(t *testing.T) { + tests := []appsecRuleTest{ + { + name: "no rule : check return code", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Equal(t, types.LOG, events[0].Type) + require.Equal(t, types.APPSEC, events[1].Type) + require.Len(t, responses, 1) + require.Equal(t, 403, responses[0].HTTPResponseCode) + require.Equal(t, "ban", responses[0].Action) + + }, + }, + { + name: "on_match: change return code", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + on_match: []appsec.Hook{ + {Filter: "IsInBand == true", Apply: []string{"SetReturnCode(413)"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Equal(t, types.LOG, events[0].Type) + require.Equal(t, types.APPSEC, events[1].Type) + require.Len(t, responses, 1) + require.Equal(t, 413, responses[0].HTTPResponseCode) + require.Equal(t, "ban", responses[0].Action) + }, + }, + { + name: "on_match: change action to another standard one (log)", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + on_match: []appsec.Hook{ + {Filter: "IsInBand == true", Apply: []string{"SetRemediation('log')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Equal(t, types.LOG, events[0].Type) + require.Equal(t, types.APPSEC, events[1].Type) + require.Len(t, responses, 1) + require.Equal(t, "log", responses[0].Action) + }, + }, + { + name: "on_match: change action to another standard one (allow)", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + on_match: []appsec.Hook{ + {Filter: "IsInBand == true", Apply: []string{"SetRemediation('allow')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Equal(t, types.LOG, events[0].Type) + require.Equal(t, types.APPSEC, events[1].Type) + require.Len(t, responses, 1) + require.Equal(t, "allow", responses[0].Action) + }, + }, + { + name: "on_match: change action to another standard one (deny/ban/block)", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + on_match: []appsec.Hook{ + {Filter: "IsInBand == true", Apply: []string{"SetRemediation('deny')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, responses, 1) + //note: SetAction normalizes deny, ban and block to ban + require.Equal(t, "ban", responses[0].Action) + }, + }, + { + name: "on_match: change action to another standard one (captcha)", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + on_match: []appsec.Hook{ + {Filter: "IsInBand == true", Apply: []string{"SetRemediation('captcha')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, responses, 1) + //note: SetAction normalizes deny, ban and block to ban + require.Equal(t, "captcha", responses[0].Action) + }, + }, + { + name: "on_match: change action to a non standard one", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + on_match: []appsec.Hook{ + {Filter: "IsInBand == true", Apply: []string{"SetRemediation('foobar')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Equal(t, types.LOG, events[0].Type) + require.Equal(t, types.APPSEC, events[1].Type) + require.Len(t, responses, 1) + require.Equal(t, "foobar", responses[0].Action) + }, + }, + { + name: "on_match: cancel alert", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule42", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + on_match: []appsec.Hook{ + {Filter: "IsInBand == true && LogInfo('XX -> %s', evt.Appsec.MatchedRules.GetName())", Apply: []string{"CancelAlert()"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 1) + require.Equal(t, types.LOG, events[0].Type) + require.Len(t, responses, 1) + require.Equal(t, "ban", responses[0].Action) + }, + }, + { + name: "on_match: cancel event", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule42", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + on_match: []appsec.Hook{ + {Filter: "IsInBand == true", Apply: []string{"CancelEvent()"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 1) + require.Equal(t, types.APPSEC, events[0].Type) + require.Len(t, responses, 1) + require.Equal(t, "ban", responses[0].Action) + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + loadAppSecEngine(test, t) + }) + } +} + +func TestAppsecPreEvalHooks(t *testing.T) { + /* + [x] basic working hook + [x] basic failing hook + [ ] test the "OnSuccess" feature + [ ] test multiple competing hooks + [ ] test the variety of helpers + */ + tests := []appsecRuleTest{ + { + name: "Basic on_load hook to disable inband rule", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + pre_eval: []appsec.Hook{ + {Filter: "1 == 1", Apply: []string{"RemoveInBandRuleByName('rule1')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Empty(t, events) + require.Len(t, responses, 1) + require.False(t, responses[0].InBandInterrupt) + require.False(t, responses[0].OutOfBandInterrupt) + }, + }, + { + name: "Basic on_load fails to disable rule", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + pre_eval: []appsec.Hook{ + {Filter: "1 ==2", Apply: []string{"RemoveInBandRuleByName('rule1')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Equal(t, types.LOG, events[0].Type) + require.True(t, events[0].Appsec.HasInBandMatches) + require.Len(t, events[0].Appsec.MatchedRules, 1) + require.Equal(t, "rule1", events[0].Appsec.MatchedRules[0]["msg"]) + require.Equal(t, types.APPSEC, events[1].Type) + require.Len(t, responses, 1) + require.True(t, responses[0].InBandInterrupt) + }, + }, + { + name: "on_load : disable inband by tag", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rulez", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + pre_eval: []appsec.Hook{ + {Apply: []string{"RemoveInBandRuleByTag('crowdsec-rulez')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Empty(t, events) + require.Len(t, responses, 1) + require.False(t, responses[0].InBandInterrupt) + require.False(t, responses[0].OutOfBandInterrupt) + }, + }, + { + name: "on_load : disable inband by ID", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rulez", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + pre_eval: []appsec.Hook{ + {Apply: []string{"RemoveInBandRuleByID(1516470898)"}}, //rule ID is generated at runtime. If you change rule, it will break the test (: + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Empty(t, events) + require.Len(t, responses, 1) + require.False(t, responses[0].InBandInterrupt) + require.False(t, responses[0].OutOfBandInterrupt) + }, + }, + { + name: "on_load : disable inband by name", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rulez", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + pre_eval: []appsec.Hook{ + {Apply: []string{"RemoveInBandRuleByName('rulez')"}}, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Empty(t, events) + require.Len(t, responses, 1) + require.False(t, responses[0].InBandInterrupt) + require.False(t, responses[0].OutOfBandInterrupt) + }, + }, + { + name: "on_load : outofband default behavior", + expected_load_ok: true, + outofband_rules: []appsec_rule.CustomRule{ + { + Name: "rulez", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 1) + require.Equal(t, types.LOG, events[0].Type) + require.True(t, events[0].Appsec.HasOutBandMatches) + require.False(t, events[0].Appsec.HasInBandMatches) + require.Len(t, events[0].Appsec.MatchedRules, 1) + require.Equal(t, "rulez", events[0].Appsec.MatchedRules[0]["msg"]) + //maybe surprising, but response won't mention OOB event, as it's sent as soon as the inband phase is over. + require.Len(t, responses, 1) + require.False(t, responses[0].InBandInterrupt) + require.False(t, responses[0].OutOfBandInterrupt) + }, + }, + { + name: "on_load : set remediation by tag", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rulez", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + pre_eval: []appsec.Hook{ + {Apply: []string{"SetRemediationByTag('crowdsec-rulez', 'foobar')"}}, //rule ID is generated at runtime. If you change rule, it will break the test (: + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Len(t, responses, 1) + require.Equal(t, "foobar", responses[0].Action) + }, + }, + { + name: "on_load : set remediation by name", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rulez", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + pre_eval: []appsec.Hook{ + {Apply: []string{"SetRemediationByName('rulez', 'foobar')"}}, //rule ID is generated at runtime. If you change rule, it will break the test (: + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Len(t, responses, 1) + require.Equal(t, "foobar", responses[0].Action) + }, + }, + { + name: "on_load : set remediation by ID", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rulez", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + pre_eval: []appsec.Hook{ + {Apply: []string{"SetRemediationByID(1516470898, 'foobar')"}}, //rule ID is generated at runtime. If you change rule, it will break the test (: + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Len(t, responses, 1) + require.Equal(t, "foobar", responses[0].Action) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + loadAppSecEngine(test, t) + }) + } +} +func TestAppsecRuleMatches(t *testing.T) { + + /* + [x] basic matching rule + [x] basic non-matching rule + [ ] test the transformation + [ ] ? + */ + tests := []appsecRuleTest{ + { + name: "Basic matching rule", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"toto"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Len(t, events, 2) + require.Equal(t, types.LOG, events[0].Type) + require.True(t, events[0].Appsec.HasInBandMatches) + require.Len(t, events[0].Appsec.MatchedRules, 1) + require.Equal(t, "rule1", events[0].Appsec.MatchedRules[0]["msg"]) + require.Equal(t, types.APPSEC, events[1].Type) + require.Len(t, responses, 1) + require.True(t, responses[0].InBandInterrupt) + }, + }, + { + name: "Basic non-matching rule", + expected_load_ok: true, + inband_rules: []appsec_rule.CustomRule{ + { + Name: "rule1", + Zones: []string{"ARGS"}, + Variables: []string{"foo"}, + Match: appsec_rule.Match{Type: "regex", Value: "^toto"}, + Transform: []string{"lowercase"}, + }, + }, + input_request: appsec.ParsedRequest{ + RemoteAddr: "1.2.3.4", + Method: "GET", + URI: "/urllll", + Args: url.Values{"foo": []string{"tutu"}}, + }, + output_asserts: func(events []types.Event, responses []appsec.AppsecTempResponse) { + require.Empty(t, events) + require.Len(t, responses, 1) + require.False(t, responses[0].InBandInterrupt) + require.False(t, responses[0].OutOfBandInterrupt) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + loadAppSecEngine(test, t) + }) + } +} + +func loadAppSecEngine(test appsecRuleTest, t *testing.T) { + if testing.Verbose() { + log.SetLevel(log.TraceLevel) + } else { + log.SetLevel(log.WarnLevel) + } + inbandRules := []string{} + outofbandRules := []string{} + InChan := make(chan appsec.ParsedRequest) + OutChan := make(chan types.Event) + + logger := log.WithFields(log.Fields{"test": test.name}) + + //build rules + for ridx, rule := range test.inband_rules { + strRule, _, err := rule.Convert(appsec_rule.ModsecurityRuleType, rule.Name) + if err != nil { + t.Fatalf("failed compilation of rule %d/%d of %s : %s", ridx, len(test.inband_rules), test.name, err) + } + inbandRules = append(inbandRules, strRule) + + } + for ridx, rule := range test.outofband_rules { + strRule, _, err := rule.Convert(appsec_rule.ModsecurityRuleType, rule.Name) + if err != nil { + t.Fatalf("failed compilation of rule %d/%d of %s : %s", ridx, len(test.outofband_rules), test.name, err) + } + outofbandRules = append(outofbandRules, strRule) + } + + appsecCfg := appsec.AppsecConfig{Logger: logger, OnLoad: test.on_load, PreEval: test.pre_eval, PostEval: test.post_eval, OnMatch: test.on_match} + AppsecRuntime, err := appsecCfg.Build() + if err != nil { + t.Fatalf("unable to build appsec runtime : %s", err) + } + AppsecRuntime.InBandRules = []appsec.AppsecCollection{{Rules: inbandRules}} + AppsecRuntime.OutOfBandRules = []appsec.AppsecCollection{{Rules: outofbandRules}} + appsecRunnerUUID := uuid.New().String() + //we copy AppsecRutime for each runner + wrt := *AppsecRuntime + wrt.Logger = logger + runner := AppsecRunner{ + inChan: InChan, + UUID: appsecRunnerUUID, + logger: logger, + AppsecRuntime: &wrt, + Labels: map[string]string{"foo": "bar"}, + outChan: OutChan, + } + err = runner.Init("/tmp/") + if err != nil { + t.Fatalf("unable to initialize runner : %s", err) + } + + input := test.input_request + input.ResponseChannel = make(chan appsec.AppsecTempResponse) + OutputEvents := make([]types.Event, 0) + OutputResponses := make([]appsec.AppsecTempResponse, 0) + go func() { + for { + //log.Printf("reading from %p", input.ResponseChannel) + out := <-input.ResponseChannel + OutputResponses = append(OutputResponses, out) + //log.Errorf("response -> %s", spew.Sdump(out)) + } + }() + go func() { + for { + out := <-OutChan + OutputEvents = append(OutputEvents, out) + //log.Errorf("outchan -> %s", spew.Sdump(out)) + } + }() + + runner.handleRequest(&input) + time.Sleep(50 * time.Millisecond) + log.Infof("events : %s", spew.Sdump(OutputEvents)) + log.Infof("responses : %s", spew.Sdump(OutputResponses)) + test.output_asserts(OutputEvents, OutputResponses) + +} diff --git a/pkg/appsec/appsec.go b/pkg/appsec/appsec.go index 011b371f7..cbf9b5876 100644 --- a/pkg/appsec/appsec.go +++ b/pkg/appsec/appsec.go @@ -161,24 +161,6 @@ func (wc *AppsecConfig) LoadByPath(file string) error { } wc.Logger = wc.Logger.Dup().WithField("name", wc.Name) wc.Logger.Logger.SetLevel(*wc.LogLevel) - 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) - } - if wc.BlockedHTTPCode == 0 { - wc.BlockedHTTPCode = 403 - } - if wc.PassedHTTPCode == 0 { - wc.PassedHTTPCode = 200 - } - if wc.DefaultPassAction == "" { - wc.DefaultPassAction = "allow" - } return nil } @@ -209,6 +191,24 @@ func (wc *AppsecConfig) GetDataDir() string { func (wc *AppsecConfig) Build() (*AppsecRuntimeConfig, error) { ret := &AppsecRuntimeConfig{Logger: wc.Logger.WithField("component", "appsec_runtime_config")} + //set the defaults + switch wc.DefaultRemediation { + case "": + wc.DefaultRemediation = "ban" + 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, wc.Name) + } + if wc.BlockedHTTPCode == 0 { + wc.BlockedHTTPCode = 403 + } + if wc.PassedHTTPCode == 0 { + wc.PassedHTTPCode = 200 + } + if wc.DefaultPassAction == "" { + wc.DefaultPassAction = "allow" + } ret.Name = wc.Name ret.Config = wc ret.DefaultRemediation = wc.DefaultRemediation @@ -340,6 +340,7 @@ func (w *AppsecRuntimeConfig) ProcessOnMatchRules(request *ParsedRequest, evt ty } func (w *AppsecRuntimeConfig) ProcessPreEvalRules(request *ParsedRequest) error { + log.Debugf("processing %d pre_eval rules", len(w.CompiledPreEval)) for _, rule := range w.CompiledPreEval { if rule.FilterExpr != nil { output, err := exprhelpers.Run(rule.FilterExpr, GetPreEvalEnv(w, request), w.Logger, w.Logger.Level >= log.DebugLevel) @@ -539,7 +540,7 @@ func (w *AppsecRuntimeConfig) SetAction(action string) error { case "captcha": w.Response.Action = action default: - return fmt.Errorf("unknown action %s", action) + w.Response.Action = action } return nil } diff --git a/pkg/appsec/appsec_rule/appsec_rule.go b/pkg/appsec/appsec_rule/appsec_rule.go index 4bc46ef50..289405ef1 100644 --- a/pkg/appsec/appsec_rule/appsec_rule.go +++ b/pkg/appsec/appsec_rule/appsec_rule.go @@ -25,7 +25,7 @@ rules: */ -type match struct { +type Match struct { Type string `yaml:"type"` Value string `yaml:"value"` Not bool `yaml:"not,omitempty"` @@ -37,7 +37,7 @@ type CustomRule struct { Zones []string `yaml:"zones"` Variables []string `yaml:"variables"` - Match match `yaml:"match"` + Match Match `yaml:"match"` Transform []string `yaml:"transform"` //t:lowercase, t:uppercase, etc And []CustomRule `yaml:"and,omitempty"` Or []CustomRule `yaml:"or,omitempty"` diff --git a/pkg/appsec/appsec_rule/modsec_rule_test.go b/pkg/appsec/appsec_rule/modsec_rule_test.go index 80411411d..ffb8a15ff 100644 --- a/pkg/appsec/appsec_rule/modsec_rule_test.go +++ b/pkg/appsec/appsec_rule/modsec_rule_test.go @@ -13,7 +13,7 @@ func TestVPatchRuleString(t *testing.T) { rule: CustomRule{ Zones: []string{"ARGS"}, Variables: []string{"foo"}, - Match: match{Type: "eq", Value: "1"}, + Match: Match{Type: "eq", Value: "1"}, Transform: []string{"count"}, }, expected: `SecRule &ARGS_GET:foo "@eq 1" "id:853070236,phase:2,deny,log,msg:'Collection count',tag:'crowdsec-Collection count'"`, @@ -23,7 +23,7 @@ func TestVPatchRuleString(t *testing.T) { rule: CustomRule{ Zones: []string{"ARGS"}, Variables: []string{"foo"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, expected: `SecRule ARGS_GET:foo "@rx [^a-zA-Z]" "id:2203944045,phase:2,deny,log,msg:'Base Rule',tag:'crowdsec-Base Rule',t:lowercase"`, @@ -33,7 +33,7 @@ func TestVPatchRuleString(t *testing.T) { rule: CustomRule{ Zones: []string{"ARGS"}, Variables: []string{"foo", "bar"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, expected: `SecRule ARGS_GET:foo|ARGS_GET:bar "@rx [^a-zA-Z]" "id:385719930,phase:2,deny,log,msg:'One zone, multi var',tag:'crowdsec-One zone, multi var',t:lowercase"`, @@ -42,7 +42,7 @@ func TestVPatchRuleString(t *testing.T) { name: "Base Rule #2", rule: CustomRule{ Zones: []string{"METHOD"}, - Match: match{Type: "startsWith", Value: "toto"}, + Match: Match{Type: "startsWith", Value: "toto"}, }, expected: `SecRule REQUEST_METHOD "@beginsWith toto" "id:2759779019,phase:2,deny,log,msg:'Base Rule #2',tag:'crowdsec-Base Rule #2'"`, }, @@ -50,7 +50,7 @@ func TestVPatchRuleString(t *testing.T) { name: "Base Negative Rule", rule: CustomRule{ Zones: []string{"METHOD"}, - Match: match{Type: "startsWith", Value: "toto", Not: true}, + Match: Match{Type: "startsWith", Value: "toto", Not: true}, }, expected: `SecRule REQUEST_METHOD "!@beginsWith toto" "id:3966251995,phase:2,deny,log,msg:'Base Negative Rule',tag:'crowdsec-Base Negative Rule'"`, }, @@ -59,7 +59,7 @@ func TestVPatchRuleString(t *testing.T) { rule: CustomRule{ Zones: []string{"ARGS", "BODY_ARGS"}, Variables: []string{"foo"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, expected: `SecRule ARGS_GET:foo|ARGS_POST:foo "@rx [^a-zA-Z]" "id:3387135861,phase:2,deny,log,msg:'Multiple Zones',tag:'crowdsec-Multiple Zones',t:lowercase"`, @@ -69,7 +69,7 @@ func TestVPatchRuleString(t *testing.T) { rule: CustomRule{ Zones: []string{"ARGS", "BODY_ARGS"}, Variables: []string{"foo", "bar"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, expected: `SecRule ARGS_GET:foo|ARGS_GET:bar|ARGS_POST:foo|ARGS_POST:bar "@rx [^a-zA-Z]" "id:1119773585,phase:2,deny,log,msg:'Multiple Zones Multi Var',tag:'crowdsec-Multiple Zones Multi Var',t:lowercase"`, @@ -78,7 +78,7 @@ func TestVPatchRuleString(t *testing.T) { name: "Multiple Zones No Vars", rule: CustomRule{ Zones: []string{"ARGS", "BODY_ARGS"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, expected: `SecRule ARGS_GET|ARGS_POST "@rx [^a-zA-Z]" "id:2020110336,phase:2,deny,log,msg:'Multiple Zones No Vars',tag:'crowdsec-Multiple Zones No Vars',t:lowercase"`, @@ -91,13 +91,13 @@ func TestVPatchRuleString(t *testing.T) { Zones: []string{"ARGS"}, Variables: []string{"foo"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, { Zones: []string{"ARGS"}, Variables: []string{"bar"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, }, @@ -112,13 +112,13 @@ SecRule ARGS_GET:bar "@rx [^a-zA-Z]" "id:1865217529,phase:2,deny,log,msg:'Basic { Zones: []string{"ARGS"}, Variables: []string{"foo"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, { Zones: []string{"ARGS"}, Variables: []string{"bar"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, }, @@ -133,19 +133,19 @@ SecRule ARGS_GET:bar "@rx [^a-zA-Z]" "id:271441587,phase:2,deny,log,msg:'Basic O { Zones: []string{"ARGS"}, Variables: []string{"foo"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, Or: []CustomRule{ { Zones: []string{"ARGS"}, Variables: []string{"foo"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, { Zones: []string{"ARGS"}, Variables: []string{"bar"}, - Match: match{Type: "regex", Value: "[^a-zA-Z]"}, + Match: Match{Type: "regex", Value: "[^a-zA-Z]"}, Transform: []string{"lowercase"}, }, },