Skip to content

Commit

Permalink
Simplify config (envoyproxy#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcchavezs authored Oct 17, 2022
1 parent eec99ea commit 10fc837
Show file tree
Hide file tree
Showing 9 changed files with 42 additions and 245 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ using `go run mage.go updateLibs`.

### Running the filter in an Envoy process

In order to run the coraza-proxy-wasm we need to spin up an envoy configuration including this as the filter config:
In order to run the coraza-proxy-wasm we need to spin up an envoy configuration including this as the filter config

```yaml
...
Expand Down
99 changes: 5 additions & 94 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,14 @@ package main

import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"strings"

"github.com/tidwall/gjson"
)

type rule struct {
inline string
include string
}

// pluginConfiguration is a type to represent an example configuration for this wasm plugin.
type pluginConfiguration struct {
rules []rule
rules []string
}

func parsePluginConfiguration(data []byte) (pluginConfiguration, error) {
Expand All @@ -37,90 +28,10 @@ func parsePluginConfiguration(data []byte) (pluginConfiguration, error) {
}

jsonData := gjson.ParseBytes(data)
rules := jsonData.Get("rules")
rules.ForEach(func(_, value gjson.Result) bool {
if inline := value.Get("inline"); inline.Exists() {
config.rules = append(config.rules, rule{inline: inline.String()})
return true
} else if include := value.Get("include"); include.Exists() {
config.rules = append(config.rules, rule{include: include.String()})
return true
} else {
return false
}
config.rules = []string{}
jsonData.Get("rules").ForEach(func(_, value gjson.Result) bool {
config.rules = append(config.rules, value.String())
return true
})

return config, nil
}

func resolveIncludes(rs []rule, crsRules fs.FS) (string, error) {
if len(rs) == 0 {
return "", nil
}

srs := strings.Builder{}
defer srs.Reset()
for _, r := range rs {
switch {
case r.inline != "":
srs.WriteString(strings.TrimSpace(r.inline))

case r.include != "":
if r.include == "OWASP_CRS" {
ors := strings.Builder{}

err := fs.WalkDir(crsRules, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

if d.IsDir() {
return nil
}

if !strings.HasPrefix(path, "REQUEST-") && !strings.HasPrefix(path, "RESPONSE-") {
return nil
}

f, err := crsRules.Open(path)
if err != nil {
return fmt.Errorf("failed to open embedded rule %q: %s", path, err.Error())
}

fc, err := io.ReadAll(f)
f.Close()
if err != nil {
return fmt.Errorf("failed to read embedded rule file %q: %s", path, err.Error())
}

_, err = ors.Write(bytes.TrimSpace(fc))
return err
})
if err != nil {
return "", fmt.Errorf("failed to walk embedded rules: %s", err.Error())
}

owaspCRSContent := strings.TrimSpace(ors.String())
ors.Reset()
srs.WriteString(owaspCRSContent)
} else {
f, err := crsRules.Open(r.include[len("OWASP_CRS_"):] + ".conf")
if err != nil {
return "", fmt.Errorf("failed to open embedded rule %q: %s", r.include, err.Error())
}
content, err := io.ReadAll(f)
f.Close()
if err != nil {
return "", fmt.Errorf("failed to read embedded rule file: %s", err.Error())
}
content = bytes.TrimSpace(content)
srs.Write(content)
}
default:
return "", errors.New("empty rule")
}
srs.WriteString("\n")
}

return strings.TrimSpace(srs.String()), nil
}
125 changes: 15 additions & 110 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,67 +4,11 @@
package main

import (
"embed"
"fmt"
"io/fs"
"errors"
"testing"
)

//go:embed testdata/fake_crs
var fakeCRS embed.FS

func getFakeCRS(t *testing.T) fs.FS {
subCRS, err := fs.Sub(fakeCRS, "testdata/fake_crs")
if err != nil {
t.Fatalf("failed to access CRS filesystem: %s", err.Error())
}
return subCRS
}

func TestResolveIncludesEntireOWASPCRS(t *testing.T) {
rs := []rule{
{
inline: "SecRuleEngine On",
},
{
include: "OWASP_CRS",
},
}

srs, err := resolveIncludes(rs, getFakeCRS(t))
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}

expectedRules := `SecRuleEngine On
# just a comment`

if want, have := expectedRules, srs; want != have {
t.Errorf("unexpected rules, want %q, have %q", want, have)
}
}

func TestResolveIncludesSingleCRS(t *testing.T) {
rs := []rule{
{
inline: "SecRuleEngine On",
},
{
include: "OWASP_CRS_REQUEST-911",
},
}
srs, err := resolveIncludes(rs, getFakeCRS(t))
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}

expectedRules := `SecRuleEngine On
# just a comment`

if want, have := expectedRules, srs; want != have {
t.Errorf("unexpected rules, want %q, have %q", want, have)
}
}
"github.com/stretchr/testify/assert"
)

func TestParsePluginConfiguration(t *testing.T) {
testCases := []struct {
Expand All @@ -81,78 +25,39 @@ func TestParsePluginConfiguration(t *testing.T) {
config: "{}",
},
{
name: "inline",
config: `
{
"rules": [
{
"inline": "SecRuleEngine On"
}
]
}
`,
expectConfig: pluginConfiguration{
rules: []rule{
{inline: "SecRuleEngine On"},
},
},
name: "bad config",
config: "abc",
expectErr: errors.New("invalid json: \"abc\""),
},
{
name: "include",
name: "inline",
config: `
{
"rules": [
{
"include": "OWASP_CRS_SOMETHING"
}
]
"rules": ["SecRuleEngine On"]
}
`,
expectConfig: pluginConfiguration{
rules: []rule{
{include: "OWASP_CRS_SOMETHING"},
},
rules: []string{"SecRuleEngine On"},
},
},
{
name: "inline & include",
name: "inline many entries",
config: `
{
"rules": [
{ "inline": "SecRuleEngine On" },
{
"include": "OWASP_CRS_SOMETHING"
},
{ "inline": "SecRuleEngine Off" }
]
{
"rules": ["SecRuleEngine On", "Include crs/*.conf\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\""]
}
`,
expectConfig: pluginConfiguration{
rules: []rule{
{inline: "SecRuleEngine On"},
{include: "OWASP_CRS_SOMETHING"},
{inline: "SecRuleEngine Off"},
},
rules: []string{"SecRuleEngine On", "Include crs/*.conf\nSecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,deny\""},
},
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
cfg, err := parsePluginConfiguration([]byte(testCase.config))
if want, have := fmt.Sprint(testCase.expectErr), fmt.Sprint(err); want != have {
t.Errorf("unexpected error, want %q, have %q", want, have)
}

if want, have := len(testCase.expectConfig.rules), len(cfg.rules); want != have {
t.Errorf("unexpected number of rules, want %d, have %d", want, have)
}

for i, r := range testCase.expectConfig.rules {
if want, have := r, cfg.rules[i]; want != have {
t.Errorf("unexpected rules, want %q, have %q", want, have)
}
}
assert.Equal(t, testCase.expectErr, err)
assert.ElementsMatch(t, testCase.expectConfig.rules, cfg.rules)
})
}
}
14 changes: 7 additions & 7 deletions example/envoy-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ static_resources:
value: |
{
"rules": [
{"inline": "Include coraza-demo.conf"},
{"inline": "Include crs-setup-demo.conf"},
{"inline": "SecDebugLogLevel 3"},
{"inline": "Include crs/*.conf"},
{"inline": "SecRule REQUEST_URI \"@streq /admin\" \"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\""}
"Include coraza-demo.conf",
"Include crs-setup-demo.conf",
"SecDebugLogLevel 3",
"Include crs/*.conf",
"SecRule REQUEST_URI \"@streq /admin\" \"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\""
]
}
vm_config:
Expand All @@ -62,8 +62,8 @@ static_resources:
clusters:
- name: local_server
connect_timeout: 6000s
type: strict_dns
lb_policy: round_robin
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: local_server
endpoints:
Expand Down
12 changes: 6 additions & 6 deletions ftw/envoy-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ static_resources:
value: |
{
"rules": [
{"inline": "Include coraza.conf-recommended.conf"},
{"inline": "Include ftw-config.conf"},
{"inline": "Include crs-setup.conf.example"},
{"inline": "Include crs/*.conf"}
"Include coraza.conf-recommended.conf",
"Include ftw-config.conf",
"Include crs-setup.conf.example",
"Include crs/*.conf"
]
}
vm_config:
Expand All @@ -56,8 +56,8 @@ static_resources:
clusters:
- name: local_server
connect_timeout: 6000s
type: strict_dns
lb_policy: round_robin
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: local_server
endpoints:
Expand Down
2 changes: 1 addition & 1 deletion internal/operators/rx.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var _ rules.Operator = (*rx)(nil)

func (o *rx) Init(options rules.OperatorOptions) error {
data := options.Arguments
// fmt.Println(data)

if data == `(?:\$(?:\((?:\(.*\)|.*)\)|\{.*})|\/\w*\[!?.+\]|[<>]\(.*\))` {
o.debug = true
fmt.Println("enabling rx debug!")
Expand Down
20 changes: 3 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"embed"
"io/fs"
"strconv"
"strings"

"github.com/corazawaf/coraza/v3"
ctypes "github.com/corazawaf/coraza/v3/types"
Expand Down Expand Up @@ -67,27 +68,12 @@ func (ctx *corazaPlugin) OnPluginStart(pluginConfigurationSize int) types.OnPlug
WithDebugLogger(&debugLogger{}).
WithRequestBodyAccess(coraza.NewRequestBodyConfig().
WithLimit(1024 * 1024 * 1024).
// TinyGo compilation will prevent buffering request body to files anyways, so this is
// effectively no-op but make clear our expectations.
// TinyGo compilation will prevent buffering request body to files anyways.
// TODO(anuraaga): Make this configurable in plugin configuration.
WithInMemoryLimit(1024 * 1024 * 1024)).
WithRootFS(root)

crs, err := fs.Sub(crs, "custom_rules")
if err != nil {
proxywasm.LogCriticalf("failed to access CRS filesystem: %v", err)
return types.OnPluginStartStatusFailed
}

rules, err := resolveIncludes(config.rules, crs)
if err != nil {
proxywasm.LogCriticalf("failed to load embedded rules: %v", err)
return types.OnPluginStartStatusFailed
}

conf = conf.WithDirectives(rules)

waf, err := coraza.NewWAF(conf)
waf, err := coraza.NewWAF(conf.WithDirectives(strings.Join(config.rules, "\n")))
if err != nil {
proxywasm.LogCriticalf("failed to parse rules: %v", err)
return types.OnPluginStartStatusFailed
Expand Down
Loading

0 comments on commit 10fc837

Please sign in to comment.