diff --git a/README.md b/README.md index 14663d8..cf68539 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ In order to run the coraza-proxy-wasm we need to spin up an envoy configuration "SecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\"" ] }, - "default_directive": "default", + "default_directives": "default", } vm_config: runtime: "envoy.wasm.runtime.v8" @@ -95,7 +95,7 @@ configuration: "Include @owasp_crs/*.conf" ] }, - "default_directive": "default", + "default_directives": "default", } ``` @@ -114,7 +114,7 @@ configuration: "Include @owasp_crs/REQUEST-901-INITIALIZATION.conf" ] }, - "default_directive": "default", + "default_directives": "default", } ``` diff --git a/example/envoy-config.yaml b/example/envoy-config.yaml index a707a13..fb88d9b 100644 --- a/example/envoy-config.yaml +++ b/example/envoy-config.yaml @@ -75,7 +75,7 @@ static_resources: "SecRule REQUEST_URI \"@streq /example\" \"id:101,phase:1,t:lowercase,deny\" \nSecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,deny\" \nSecRule RESPONSE_HEADERS::status \"@rx 406\" \"id:103,phase:3,t:lowercase,deny\" \nSecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,deny\"" ] }, - "default_directive": "rs1", + "default_directives": "rs1", "metric_labels": { "owner": "coraza", "identifier": "global" diff --git a/ftw/envoy-config.yaml b/ftw/envoy-config.yaml index 730c881..20c85eb 100644 --- a/ftw/envoy-config.yaml +++ b/ftw/envoy-config.yaml @@ -44,7 +44,7 @@ static_resources: "Include @owasp_crs/*.conf" ] }, - "default_directive": "default", + "default_directives": "default", "metric_labels": {}, "per_authority_directives": {} } diff --git a/main_test.go b/main_test.go index c401a79..0e26c0b 100644 --- a/main_test.go +++ b/main_test.go @@ -421,9 +421,9 @@ func TestLifecycle(t *testing.T) { tt := tc t.Run(tt.name, func(t *testing.T) { - conf := `{"directives_map": {"default": []}, "default_directive": "default"}` + conf := `{"directives_map": {"default": []}, "default_directives": "default"}` if inlineRules := strings.TrimSpace(tt.inlineRules); inlineRules != "" { - conf = fmt.Sprintf(`{"directives_map": {"default": ["%s"]}, "default_directive": "default"}`, inlineRules) + conf = fmt.Sprintf(`{"directives_map": {"default": ["%s"]}, "default_directives": "default"}`, inlineRules) } opt := proxytest. NewEmulatorOption(). @@ -587,7 +587,7 @@ func TestBadRequest(t *testing.T) { for _, tc := range tests { tt := tc t.Run(tt.name, func(t *testing.T) { - conf := `{"directives_map": {"default": []}, "default_directive": "default"}` + conf := `{"directives_map": {"default": []}, "default_directives": "default"}` opt := proxytest. NewEmulatorOption(). WithVMContext(vm). @@ -636,7 +636,7 @@ func TestBadResponse(t *testing.T) { for _, tc := range tests { tt := tc t.Run(tt.name, func(t *testing.T) { - conf := `{"directives_map": {"default": []}, "default_directive": "default"}` + conf := `{"directives_map": {"default": []}, "default_directives": "default"}` opt := proxytest. NewEmulatorOption(). WithVMContext(vm). @@ -666,7 +666,7 @@ func TestEmptyBody(t *testing.T) { opt := proxytest. NewEmulatorOption(). WithVMContext(vm). - WithPluginConfiguration([]byte(`{"directives_map": {"default": [ "SecRequestBodyAccess On", "SecResponseBodyAccess On" ]}, "default_directive": "default"}`)) + WithPluginConfiguration([]byte(`{"directives_map": {"default": [ "SecRequestBodyAccess On", "SecResponseBodyAccess On" ]}, "default_directives": "default"}`)) host, reset := proxytest.NewHostEmulator(opt) defer reset() @@ -761,7 +761,7 @@ func TestLogError(t *testing.T) { for _, tc := range tests { tt := tc t.Run(fmt.Sprintf("severity %d", tt.severity), func(t *testing.T) { - conf := fmt.Sprintf(`{"directives_map": {"default": ["SecRule REQUEST_HEADERS:X-CRS-Test \"@rx ^.*$\" \"id:999999,phase:1,log,severity:%d,msg:'%%{MATCHED_VAR}',pass,t:none\""]}, "default_directive": "default"}`, tt.severity) + conf := fmt.Sprintf(`{"directives_map": {"default": ["SecRule REQUEST_HEADERS:X-CRS-Test \"@rx ^.*$\" \"id:999999,phase:1,log,severity:%d,msg:'%%{MATCHED_VAR}',pass,t:none\""]}, "default_directives": "default"}`, tt.severity) opt := proxytest. NewEmulatorOption(). @@ -789,7 +789,7 @@ func TestParseCRS(t *testing.T) { opt := proxytest. NewEmulatorOption(). WithVMContext(vm). - WithPluginConfiguration([]byte(`{"directives_map": {"default": [ "Include @ftw-conf", "Include @recommended-conf", "Include @crs-setup-conf", "Include @owasp_crs/*.conf" ]}, "default_directive": "default"}`)) + WithPluginConfiguration([]byte(`{"directives_map": {"default": [ "Include @ftw-conf", "Include @recommended-conf", "Include @crs-setup-conf", "Include @owasp_crs/*.conf" ]}, "default_directives": "default"}`)) host, reset := proxytest.NewHostEmulator(opt) defer reset() @@ -859,7 +859,7 @@ SecRuleEngine On\nSecRule REQUEST_URI \"@streq /hello\" \"id:101,phase:4,t:lower t.Run(tt.name, func(t *testing.T) { conf := fmt.Sprintf(` - {"directives_map": {"default": ["%s"]}, "default_directive": "default"} + {"directives_map": {"default": ["%s"]}, "default_directives": "default"} `, strings.TrimSpace(tt.rules)) opt := proxytest. NewEmulatorOption(). @@ -967,7 +967,7 @@ func TestRetrieveAddressInfo(t *testing.T) { conf := `{}` if inlineRules := strings.TrimSpace(inlineRules); inlineRules != "" { - conf = fmt.Sprintf(`{"directives_map": {"default": ["%s"]}, "default_directive": "default"}`, inlineRules) + conf = fmt.Sprintf(`{"directives_map": {"default": ["%s"]}, "default_directives": "default"}`, inlineRules) } t.Run(tt.name, func(t *testing.T) { opt := proxytest. @@ -1030,7 +1030,7 @@ func TestParseServerName(t *testing.T) { conf := `{}` if inlineRules := strings.TrimSpace(inlineRules); inlineRules != "" { - conf = fmt.Sprintf(`{"directives_map": {"default": ["%s"]}, "default_directive": "default"}`, inlineRules) + conf = fmt.Sprintf(`{"directives_map": {"default": ["%s"]}, "default_directives": "default"}`, inlineRules) } t.Run(name, func(t *testing.T) { opt := proxytest. diff --git a/wasmplugin/config.go b/wasmplugin/config.go index 3a1eaa6..0734524 100644 --- a/wasmplugin/config.go +++ b/wasmplugin/config.go @@ -14,13 +14,13 @@ import ( type pluginConfiguration struct { directivesMap DirectivesMap metricLabels map[string]string - defaultDirective string + defaultDirectives string perAuthorityDirectives map[string]string } type DirectivesMap map[string][]string -func parsePluginConfiguration(data []byte) (pluginConfiguration, error) { +func parsePluginConfiguration(data []byte, infoLogger func(string)) (pluginConfiguration, error) { config := pluginConfiguration{} data = bytes.TrimSpace(data) @@ -56,14 +56,14 @@ func parsePluginConfiguration(data []byte) (pluginConfiguration, error) { return true }) - defaultDirective := jsonData.Get("default_directive") - if defaultDirective.Exists() { - defaultDirectiveName := defaultDirective.String() - if _, ok := config.directivesMap[defaultDirectiveName]; !ok { - return config, fmt.Errorf("directive map not found for default directive: %q", defaultDirectiveName) + defaultDirectives := jsonData.Get("default_directives") + if defaultDirectives.Exists() { + defaultDirectivesName := defaultDirectives.String() + if _, ok := config.directivesMap[defaultDirectivesName]; !ok { + return config, fmt.Errorf("directive map not found for default directive: %q", defaultDirectivesName) } - config.defaultDirective = defaultDirectiveName + config.defaultDirectives = defaultDirectivesName } config.perAuthorityDirectives = make(map[string]string) @@ -82,7 +82,9 @@ func parsePluginConfiguration(data []byte) (pluginConfiguration, error) { rules := jsonData.Get("rules") if rules.Exists() { - config.defaultDirective = "default" + infoLogger("Defaulting to deprecated 'rules' field") + + config.defaultDirectives = "default" var directive []string rules.ForEach(func(_, value gjson.Result) bool { diff --git a/wasmplugin/config_test.go b/wasmplugin/config_test.go index e417dc0..8eb3d20 100644 --- a/wasmplugin/config_test.go +++ b/wasmplugin/config_test.go @@ -7,7 +7,9 @@ import ( "errors" "testing" + "github.com/corazawaf/coraza/v3" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParsePluginConfiguration(t *testing.T) { @@ -26,7 +28,7 @@ func TestParsePluginConfiguration(t *testing.T) { expectConfig: pluginConfiguration{ directivesMap: DirectivesMap{}, metricLabels: map[string]string{}, - defaultDirective: "", + defaultDirectives: "", perAuthorityDirectives: map[string]string{}, }, }, @@ -42,7 +44,7 @@ func TestParsePluginConfiguration(t *testing.T) { "directives_map": { "default": ["SecRuleEngine On"] }, - "default_directive": "default" + "default_directives": "default" } `, expectConfig: pluginConfiguration{ @@ -50,7 +52,7 @@ func TestParsePluginConfiguration(t *testing.T) { "default": []string{"SecRuleEngine On"}, }, metricLabels: map[string]string{}, - defaultDirective: "default", + defaultDirectives: "default", perAuthorityDirectives: map[string]string{}, }, }, @@ -61,7 +63,7 @@ func TestParsePluginConfiguration(t *testing.T) { "directives_map": { "default": ["SecRuleEngine On", "Include @owasp_crs/*.conf\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\""] }, - "default_directive": "default" + "default_directives": "default" } `, expectConfig: pluginConfiguration{ @@ -69,7 +71,7 @@ func TestParsePluginConfiguration(t *testing.T) { "default": []string{"SecRuleEngine On", "Include @owasp_crs/*.conf\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\""}, }, metricLabels: map[string]string{}, - defaultDirective: "default", + defaultDirectives: "default", perAuthorityDirectives: map[string]string{}, }, }, @@ -80,7 +82,7 @@ func TestParsePluginConfiguration(t *testing.T) { "directives_map": { "default": ["SecRuleEngine On", "Include @owasp_crs/*.conf\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\""] }, - "default_directive": "default", + "default_directives": "default", "metric_labels": {"owner": "coraza","identifier": "global"} } `, @@ -92,7 +94,7 @@ func TestParsePluginConfiguration(t *testing.T) { "owner": "coraza", "identifier": "global", }, - defaultDirective: "default", + defaultDirectives: "default", perAuthorityDirectives: map[string]string{}, }, }, @@ -105,7 +107,7 @@ func TestParsePluginConfiguration(t *testing.T) { "custom-01": ["SecRuleEngine On"], "custom-02": ["SecRuleEngine On"] }, - "default_directive": "default", + "default_directives": "default", "metric_labels": {"owner": "coraza","identifier": "global"} } `, @@ -119,7 +121,7 @@ func TestParsePluginConfiguration(t *testing.T) { "owner": "coraza", "identifier": "global", }, - defaultDirective: "default", + defaultDirectives: "default", perAuthorityDirectives: map[string]string{}, }, }, @@ -132,7 +134,7 @@ func TestParsePluginConfiguration(t *testing.T) { "custom-01": ["SecRuleEngine On"], "custom-02": ["SecRuleEngine On"] }, - "default_directive": "default", + "default_directives": "default", "metric_labels": {"owner": "coraza","identifier": "global"}, "per_authority_directives": { "mydomain.com":"custom-01", @@ -150,7 +152,7 @@ func TestParsePluginConfiguration(t *testing.T) { "owner": "coraza", "identifier": "global", }, - defaultDirective: "default", + defaultDirectives: "default", perAuthorityDirectives: map[string]string{ "mydomain.com": "custom-01", "mydomain2.com": "custom-02", @@ -164,7 +166,7 @@ func TestParsePluginConfiguration(t *testing.T) { "directives_map": { "default": ["SecRuleEngine On", "Include @owasp_crs/*.conf\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\""] }, - "default_directive": "foo" + "default_directives": "foo" } `, expectErr: errors.New("directive map not found for default directive: \"foo\""), @@ -178,7 +180,7 @@ func TestParsePluginConfiguration(t *testing.T) { "custom-01": ["SecRuleEngine On"], "custom-02": ["SecRuleEngine On"] }, - "default_directive": "default", + "default_directives": "default", "metric_labels": {"owner": "coraza","identifier": "global"}, "per_authority_directives": { "mydomain.com":"custom-01", @@ -199,7 +201,7 @@ func TestParsePluginConfiguration(t *testing.T) { directivesMap: DirectivesMap{ "default": []string{"SecRuleEngine On", "Include @owasp_crs/*.conf\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\""}, }, - defaultDirective: "default", + defaultDirectives: "default", metricLabels: map[string]string{}, perAuthorityDirectives: map[string]string{}, }, @@ -212,14 +214,14 @@ func TestParsePluginConfiguration(t *testing.T) { "directives_map": { "foo": ["SecRuleEngine On", "Include @owasp_crs/*.conf\nSecRule REQUEST_URI \"@streq /directives\" \"id:101,phase:1,t:lowercase,deny\""] }, - "default_directive": "foo" + "default_directives": "foo" } `, expectConfig: pluginConfiguration{ directivesMap: DirectivesMap{ "foo": []string{"SecRuleEngine On", "Include @owasp_crs/*.conf\nSecRule REQUEST_URI \"@streq /directives\" \"id:101,phase:1,t:lowercase,deny\""}, }, - defaultDirective: "foo", + defaultDirectives: "foo", metricLabels: map[string]string{}, perAuthorityDirectives: map[string]string{}, }, @@ -228,15 +230,50 @@ func TestParsePluginConfiguration(t *testing.T) { for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - cfg, err := parsePluginConfiguration([]byte(testCase.config)) + cfg, err := parsePluginConfiguration([]byte(testCase.config), func(string) {}) assert.Equal(t, testCase.expectErr, err) if testCase.expectErr == nil { assert.Equal(t, testCase.expectConfig.directivesMap, cfg.directivesMap) assert.Equal(t, testCase.expectConfig.metricLabels, cfg.metricLabels) - assert.Equal(t, testCase.expectConfig.defaultDirective, cfg.defaultDirective) + assert.Equal(t, testCase.expectConfig.defaultDirectives, cfg.defaultDirectives) assert.Equal(t, testCase.expectConfig.perAuthorityDirectives, cfg.perAuthorityDirectives) } }) } } + +func TestWAFMap(t *testing.T) { + w, _ := coraza.NewWAF(coraza.NewWAFConfig()) + + wm := newWAFMap(1) + err := wm.put("foo", w) + require.NoError(t, err) + + t.Run("set unexisting default key", func(t *testing.T) { + err = wm.setDefaultKey("bar") + require.Error(t, err) + }) + + t.Run("get unexisting WAF with no default", func(t *testing.T) { + _, _, err := wm.getWAFOrDefault("bar") + require.Error(t, err) + }) + + err = wm.setDefaultKey("foo") + require.NoError(t, err) + + t.Run("get existing WAF", func(t *testing.T) { + expecteWAF, isDefault, err := wm.getWAFOrDefault("foo") + require.NotNil(t, expecteWAF) + require.False(t, isDefault) + require.NoError(t, err) + }) + + t.Run("get unexisting WAF", func(t *testing.T) { + expecteWAF, isDefault, err := wm.getWAFOrDefault("bar") + require.NotNil(t, expecteWAF) + require.True(t, isDefault) + require.NoError(t, err) + }) +} diff --git a/wasmplugin/metrics.go b/wasmplugin/metrics.go index 91797cb..1bd21ad 100644 --- a/wasmplugin/metrics.go +++ b/wasmplugin/metrics.go @@ -36,15 +36,15 @@ func (m *wafMetrics) CountTX() { m.incrementCounter("waf_filter.tx.total") } -func (m *wafMetrics) CountTXInterruption(phase string, ruleID int, metricLabels map[string]string) { +func (m *wafMetrics) CountTXInterruption(phase string, ruleID int, metricLabelsKV []string) { // This metric is processed as: waf_filter_tx_interruption{phase="http_request_body",rule_id="100",identifier="foo"}. // The extraction rule is defined in envoy.yaml as a bootstrap configuration. // See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/metrics/v3/stats.proto#config-metrics-v3-statsconfig. var sb strings.Builder sb.WriteString(fmt.Sprintf("waf_filter.tx.interruptions_ruleid=%d_phase=%s", ruleID, phase)) - for key, value := range metricLabels { - sb.WriteString(fmt.Sprintf("_%s=%s", key, value)) + for i := 0; i < len(metricLabelsKV); i += 2 { + sb.WriteString(fmt.Sprintf("_%s=%s", metricLabelsKV[i], metricLabelsKV[i+1])) } fqn := sb.String() diff --git a/wasmplugin/plugin.go b/wasmplugin/plugin.go index 128339b..06a1745 100644 --- a/wasmplugin/plugin.go +++ b/wasmplugin/plugin.go @@ -34,62 +34,74 @@ func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { return &corazaPlugin{} } -type corazaPlugin struct { - // Embed the default plugin context here, - // so that we don't need to reimplement all the methods. - types.DefaultPluginContext - wafSets wafSets +type wafMap struct { + kv map[string]coraza.WAF + defaultKey string +} - wafDefaultEnabled bool - wafDefaultDirective string +func newWAFMap(capacity int) wafMap { + return wafMap{ + kv: make(map[string]coraza.WAF, capacity), + } +} - perAuthorityWafSets perAuthorityWafSets - metricLabels map[string]string +func (m *wafMap) put(key string, waf coraza.WAF) error { + if len(key) == 0 { + return errors.New("empty WAF key") + } - metrics *wafMetrics + m.kv[key] = waf + return nil } -type wafSets []wafSet +func (m *wafMap) setDefaultKey(key string) error { + if len(key) == 0 { + return errors.New("empty default WAF key") + } + + if _, ok := m.kv[key]; ok { + m.defaultKey = key + return nil + } -func (wfs wafSets) newWAFSetsHttp(contextID uint32) wafSetsHttp { - var wafSetsHttp wafSetsHttp + return fmt.Errorf("unknown default WAF key %q", key) +} - for _, wafSet := range wfs { - var wafSetHttp wafSetHttp - wafSetHttp.tx = wafSet.waf.NewTransaction() - wafSetHttp.logger = wafSet.waf.NewTransaction(). - DebugLogger(). - With(debuglog.Uint("context_id", uint(contextID))) - wafSetHttp.name = wafSet.name +func (m *wafMap) getWAFOrDefault(key string) (coraza.WAF, bool, error) { + if w, ok := m.kv[key]; ok { + return w, false, nil + } - wafSetsHttp = append(wafSetsHttp, wafSetHttp) + if len(m.defaultKey) == 0 { + return nil, false, errors.New("no default WAF key") } - return wafSetsHttp + return m.kv[m.defaultKey], true, nil } -type wafSet struct { - waf coraza.WAF - name string +type corazaPlugin struct { + // Embed the default plugin context here, + // so that we don't need to reimplement all the methods. + types.DefaultPluginContext + perAuthorityWAFs wafMap + metricLabelsKV []string + metrics *wafMetrics } func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { data, err := proxywasm.GetPluginConfiguration() if err != nil && err != types.ErrorStatusNotFound { - proxywasm.LogCriticalf("error reading plugin configuration: %v", err) + proxywasm.LogCriticalf("Failed to read plugin configuration: %v", err) return types.OnPluginStartStatusFailed } - config, err := parsePluginConfiguration(data) + config, err := parsePluginConfiguration(data, proxywasm.LogInfo) if err != nil { proxywasm.LogCriticalf("Failed to parse plugin configuration: %v", err) return types.OnPluginStartStatusFailed } - var wafSets wafSets - for name, directive := range config.directivesMap { - var wafset wafSet - wafset.name = name - + perAuthorityWAFs := newWAFMap(len(config.directivesMap)) + for name, directives := range config.directivesMap { // First we initialize our waf and our seclang parser conf := coraza.NewWAFConfig(). WithErrorCallback(logError). @@ -101,24 +113,30 @@ func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPlug // buffering request body to files anyways. WithRootFS(root) - waf, err := coraza.NewWAF(conf.WithDirectives(strings.Join(directive, "\n"))) + waf, err := coraza.NewWAF(conf.WithDirectives(strings.Join(directives, "\n"))) if err != nil { - proxywasm.LogCriticalf("Failed to parse directive: %v", err) + proxywasm.LogCriticalf("Failed to parse directives: %v", err) return types.OnPluginStartStatusFailed } - wafset.waf = waf - wafSets = append(wafSets, wafset) + err = perAuthorityWAFs.put(name, waf) + if err != nil { + proxywasm.LogCriticalf("Failed to register authority WAF: %v", err) + return types.OnPluginStartStatusFailed + } } - if len(config.defaultDirective) != 0 { - ctx.wafDefaultEnabled = true - ctx.wafDefaultDirective = config.defaultDirective + if len(config.defaultDirectives) > 0 { + if err := perAuthorityWAFs.setDefaultKey(config.defaultDirectives); err != nil { + proxywasm.LogCriticalf("Failed to set the default directives: %v", err) + return types.OnPluginStartStatusFailed + } } - ctx.wafSets = wafSets - ctx.perAuthorityWafSets = config.perAuthorityDirectives - ctx.metricLabels = config.metricLabels + ctx.perAuthorityWAFs = perAuthorityWAFs + for k, v := range config.metricLabels { + ctx.metricLabelsKV = append(ctx.metricLabelsKV, k, v) + } ctx.metrics = NewWAFMetrics() return types.OnPluginStartStatusOK @@ -126,13 +144,10 @@ func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPlug func (ctx *corazaPlugin) NewHttpContext(contextID uint32) types.HttpContext { return &httpContext{ - contextID: contextID, - metrics: ctx.metrics, - metricLabels: ctx.metricLabels, - wafSets: ctx.wafSets.newWAFSetsHttp(contextID), - wafDefaultEnabled: ctx.wafDefaultEnabled, - wafDefaultDirective: ctx.wafDefaultDirective, - perAuthorityWafSets: ctx.perAuthorityWafSets, + contextID: contextID, + metrics: ctx.metrics, + metricLabelsKV: ctx.metricLabelsKV, + perAuthorityWAFs: ctx.perAuthorityWAFs, } } @@ -170,11 +185,8 @@ type httpContext struct { // so that we don't need to reimplement all the methods. types.DefaultHttpContext contextID uint32 + perAuthorityWAFs wafMap tx ctypes.Transaction - wafSets wafSetsHttp - wafDefaultEnabled bool - wafDefaultDirective string - perAuthorityWafSets perAuthorityWafSets httpProtocol string processedRequestBody bool processedResponseBody bool @@ -182,47 +194,7 @@ type httpContext struct { metrics *wafMetrics interruptedAt interruptionPhase logger debuglog.Logger - metricLabels map[string]string -} - -type perAuthorityWafSets map[string]string - -func (aws perAuthorityWafSets) getdirectivesName(authority string) (string, bool) { - for key, value := range aws { - if key == authority { - return value, true - } - } - - return "", false -} - -type wafSetsHttp []wafSetHttp - -func (wsh wafSetsHttp) getTx(name string) (ctypes.Transaction, bool) { - for _, wafSet := range wsh { - if wafSet.name == name { - return wafSet.tx, true - } - } - - return nil, false -} - -func (wsh wafSetsHttp) getlogger(name string) (debuglog.Logger, bool) { - for _, wafSet := range wsh { - if wafSet.name == name { - return wafSet.logger, true - } - } - - return nil, false -} - -type wafSetHttp struct { - tx ctypes.Transaction - logger debuglog.Logger - name string + metricLabelsKV []string } func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) types.Action { @@ -231,33 +203,30 @@ func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) t ctx.metrics.CountTX() authority, err := proxywasm.GetHttpRequestHeader(":authority") - if err != nil { - return types.ActionContinue - } + if err == nil { + if waf, isDefault, resolveWAFErr := ctx.perAuthorityWAFs.getWAFOrDefault(authority); resolveWAFErr == nil { + ctx.tx = waf.NewTransaction() - wafDirectiveName, exist := ctx.perAuthorityWafSets.getdirectivesName(authority) - if !exist && ctx.wafDefaultEnabled { - ctx.tx, exist = ctx.wafSets.getTx(ctx.wafDefaultDirective) - if !exist { - return types.ActionContinue - } + logFields := []debuglog.ContextField{debuglog.Uint("context_id", uint(ctx.contextID))} + if !isDefault { + logFields = append(logFields, debuglog.Str("authority", authority)) + } + ctx.logger = ctx.tx.DebugLogger().With(logFields...) - ctx.logger, exist = ctx.wafSets.getlogger(ctx.wafDefaultDirective) - if !exist { - return types.ActionContinue - } - } else { - ctx.tx, exist = ctx.wafSets.getTx(wafDirectiveName) - if !exist { - return types.ActionContinue - } + // CRS rules tend to expect Host even with HTTP/2 + ctx.tx.AddRequestHeader("Host", authority) + ctx.tx.SetServerName(parseServerName(ctx.logger, authority)) - ctx.logger, exist = ctx.wafSets.getlogger(wafDirectiveName) - if !exist { + if !isDefault { + ctx.metricLabelsKV = append(ctx.metricLabelsKV, "authority", authority) + } + } else { + proxywasm.LogWarnf("Failed to resolve WAF for authority %q: %v", authority, resolveWAFErr) return types.ActionContinue } - - ctx.metricLabels["authority"] = authority + } else { + proxywasm.LogWarnf("Failed to get the :authority pseudo-header: %v", err) + return types.ActionContinue } tx := ctx.tx @@ -265,10 +234,10 @@ func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) t // This currently relies on Envoy's behavior of mapping all requests to HTTP/2 semantics // and its request properties, but they may not be true of other proxies implementing // proxy-wasm. - if tx.IsRuleEngineOff() { return types.ActionContinue } + // OnHttpRequestHeaders does not terminate if IP/Port retrieve goes wrong srcIP, srcPort := retrieveAddressInfo(ctx.logger, "source") dstIP, dstPort := retrieveAddressInfo(ctx.logger, "destination") @@ -314,13 +283,6 @@ func (ctx *httpContext) OnHttpRequestHeaders(numHeaders int, endOfStream bool) t tx.AddRequestHeader(h[0], h[1]) } - // CRS rules tend to expect Host even with HTTP/2 - authority, err = proxywasm.GetHttpRequestHeader(":authority") - if err == nil { - tx.AddRequestHeader("Host", authority) - tx.SetServerName(parseServerName(ctx.logger, authority)) - } - interruption := tx.ProcessRequestHeaders() if interruption != nil { return ctx.handleInterruption(interruptionPhaseHttpRequestHeaders, interruption) @@ -626,7 +588,7 @@ func (ctx *httpContext) handleInterruption(phase interruptionPhase, interruption panic("Interruption already handled") } - ctx.metrics.CountTXInterruption(phase.String(), interruption.RuleID, ctx.metricLabels) + ctx.metrics.CountTXInterruption(phase.String(), interruption.RuleID, ctx.metricLabelsKV) ctx.logger.Info(). Str("action", interruption.Action). @@ -751,7 +713,6 @@ func parseServerName(logger debuglog.Logger, authority string) string { if err != nil { // missing port or bad format logger.Debug(). - Str("authority", authority). Err(err). Msg("Failed to parse server name from authority") host = authority