forked from envoyproxy/envoy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adds support for config. (envoyproxy#14)
This PR shapes the config to support both inline and embedded rules in the filter. ```json { "rules": [ {"inline": "SecRuleEngine On\nSecRule REQUEST_URI \"@Streq /admin\" \"id:101,phase:1,t:lowercase,deny\""}, {"include": "OWASP_CRS_REQUEST-903.9002-WORDPRESS-EXCLUSION-RULES"} ] } ``` In yaml it would be much nicer: ```yaml rules: - inline: | SecRuleEngine On SecRule REQUEST_URI "@Streq /admin" "id:101,phase:1,t:lowercase,deny" - include: "OWASP_CRS_REQUEST-903.9002-WORDPRESS-EXCLUSION-RULES" ``` Co-authored-by: Anuraag Agrawal <anuraaga@gmail.com>
- Loading branch information
Showing
7 changed files
with
345 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
// Copyright 2022 The OWASP Coraza contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
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 | ||
} | ||
|
||
func parsePluginConfiguration(data []byte) (pluginConfiguration, error) { | ||
config := pluginConfiguration{} | ||
|
||
data = bytes.TrimSpace(data) | ||
if len(data) == 0 { | ||
return config, nil | ||
} | ||
|
||
if !gjson.ValidBytes(data) { | ||
return config, fmt.Errorf("invalid json: %q", data) | ||
} | ||
|
||
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 | ||
} | ||
}) | ||
|
||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
// Copyright 2022 The OWASP Coraza contributors | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package main | ||
|
||
import ( | ||
"embed" | ||
"fmt" | ||
"io/fs" | ||
"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) | ||
} | ||
} | ||
|
||
func TestParsePluginConfiguration(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
config string | ||
expectErr error | ||
expectConfig pluginConfiguration | ||
}{ | ||
{ | ||
name: "empty config", | ||
}, | ||
{ | ||
name: "empty json", | ||
config: "{}", | ||
}, | ||
{ | ||
name: "inline", | ||
config: ` | ||
{ | ||
"rules": [ | ||
{ | ||
"inline": "SecRuleEngine On" | ||
} | ||
] | ||
} | ||
`, | ||
expectConfig: pluginConfiguration{ | ||
rules: []rule{ | ||
{inline: "SecRuleEngine On"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
name: "include", | ||
config: ` | ||
{ | ||
"rules": [ | ||
{ | ||
"include": "OWASP_CRS_SOMETHING" | ||
} | ||
] | ||
} | ||
`, | ||
expectConfig: pluginConfiguration{ | ||
rules: []rule{ | ||
{include: "OWASP_CRS_SOMETHING"}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
name: "inline & include", | ||
config: ` | ||
{ | ||
"rules": [ | ||
{ "inline": "SecRuleEngine On" }, | ||
{ | ||
"include": "OWASP_CRS_SOMETHING" | ||
}, | ||
{ "inline": "SecRuleEngine Off" } | ||
] | ||
} | ||
`, | ||
expectConfig: pluginConfiguration{ | ||
rules: []rule{ | ||
{inline: "SecRuleEngine On"}, | ||
{include: "OWASP_CRS_SOMETHING"}, | ||
{inline: "SecRuleEngine Off"}, | ||
}, | ||
}, | ||
}, | ||
} | ||
|
||
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) | ||
} | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.