diff --git a/operators/begins_with.go b/operators/begins_with.go index 9475a2ee6..dbe2fec35 100644 --- a/operators/begins_with.go +++ b/operators/begins_with.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type BeginsWith struct { +type beginsWith struct { data string } -func (o *BeginsWith) Init(data string) error { +func (o *beginsWith) Init(data string) error { o.data = data return nil } -func (o *BeginsWith) Evaluate(tx *engine.Transaction, value string) bool { +func (o *beginsWith) Evaluate(tx *engine.Transaction, value string) bool { data := tx.MacroExpansion(o.data) return strings.HasPrefix(value, data) } diff --git a/operators/contains.go b/operators/contains.go index 9a6fc3a6f..010de8201 100644 --- a/operators/contains.go +++ b/operators/contains.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Contains struct { +type contains struct { data string } -func (o *Contains) Init(data string) error { +func (o *contains) Init(data string) error { o.data = data return nil } -func (o *Contains) Evaluate(tx *engine.Transaction, value string) bool { +func (o *contains) Evaluate(tx *engine.Transaction, value string) bool { data := tx.MacroExpansion(o.data) return strings.Contains(value, data) } diff --git a/operators/ends_with.go b/operators/ends_with.go index 7f025e38d..8b1361a27 100644 --- a/operators/ends_with.go +++ b/operators/ends_with.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type EndsWith struct { +type endsWith struct { data string } -func (o *EndsWith) Init(data string) error { +func (o *endsWith) Init(data string) error { o.data = data return nil } -func (o *EndsWith) Evaluate(tx *engine.Transaction, value string) bool { +func (o *endsWith) Evaluate(tx *engine.Transaction, value string) bool { data := tx.MacroExpansion(o.data) return strings.HasSuffix(value, data) } diff --git a/operators/eq.go b/operators/eq.go index 6ecded930..cc174f652 100644 --- a/operators/eq.go +++ b/operators/eq.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Eq struct { +type eq struct { data string } -func (o *Eq) Init(data string) error { +func (o *eq) Init(data string) error { o.data = data return nil } -func (o *Eq) Evaluate(tx *engine.Transaction, value string) bool { +func (o *eq) Evaluate(tx *engine.Transaction, value string) bool { d1, err := strconv.Atoi(tx.MacroExpansion(o.data)) if err != nil { d1 = 0 diff --git a/operators/ge.go b/operators/ge.go index cbba049b5..ee6b99141 100644 --- a/operators/ge.go +++ b/operators/ge.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Ge struct { +type ge struct { data string } -func (o *Ge) Init(data string) error { +func (o *ge) Init(data string) error { o.data = data return nil } -func (o *Ge) Evaluate(tx *engine.Transaction, value string) bool { +func (o *ge) Evaluate(tx *engine.Transaction, value string) bool { v, err := strconv.Atoi(value) if err != nil { v = 0 diff --git a/operators/ge_test.go b/operators/ge_test.go index d52de02b4..7763e2131 100644 --- a/operators/ge_test.go +++ b/operators/ge_test.go @@ -20,7 +20,7 @@ import ( ) func TestGe(t *testing.T) { - geo := &Ge{} + geo := &ge{} if err := geo.Init("2500"); err != nil { t.Error("Cannot init geo") } diff --git a/operators/gt.go b/operators/gt.go index cf0124a13..e05a6e849 100644 --- a/operators/gt.go +++ b/operators/gt.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Gt struct { +type gt struct { data string } -func (o *Gt) Init(data string) error { +func (o *gt) Init(data string) error { o.data = data return nil } -func (o *Gt) Evaluate(tx *engine.Transaction, value string) bool { +func (o *gt) Evaluate(tx *engine.Transaction, value string) bool { v, err := strconv.Atoi(value) if err != nil { v = 0 diff --git a/operators/gt_test.go b/operators/gt_test.go index 9d71fe5d4..2559ad4c9 100644 --- a/operators/gt_test.go +++ b/operators/gt_test.go @@ -20,7 +20,7 @@ import ( ) func TestGt(t *testing.T) { - gto := &Gt{} + gto := >{} if err := gto.Init("2500"); err != nil { t.Error("Cannot init gto operator") } diff --git a/operators/inspect_file.go b/operators/inspect_file.go index 718b67fdc..46c91ca50 100644 --- a/operators/inspect_file.go +++ b/operators/inspect_file.go @@ -22,16 +22,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type InspectFile struct { +type inspectFile struct { path string } -func (o *InspectFile) Init(data string) error { +func (o *inspectFile) Init(data string) error { o.path = data return nil } -func (o *InspectFile) Evaluate(tx *engine.Transaction, value string) bool { +func (o *inspectFile) Evaluate(tx *engine.Transaction, value string) bool { //TODO parametrize timeout //TODO add relative path capabilities //TODO add lua special support diff --git a/operators/inspect_file_test.go b/operators/inspect_file_test.go index effd3500b..298cf281e 100644 --- a/operators/inspect_file_test.go +++ b/operators/inspect_file_test.go @@ -20,7 +20,7 @@ import ( ) func TestInspectFile(t *testing.T) { - ipf := &InspectFile{} + ipf := &inspectFile{} if err := ipf.Init("/bin/echo"); err != nil { t.Error("cannot init inspectfile operator") } diff --git a/operators/ip_match.go b/operators/ip_match.go index 44203a282..6d392e8d2 100644 --- a/operators/ip_match.go +++ b/operators/ip_match.go @@ -21,11 +21,11 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type IpMatch struct { +type ipMatch struct { subnets []*net.IPNet } -func (o *IpMatch) Init(data string) error { +func (o *ipMatch) Init(data string) error { o.subnets = []*net.IPNet{} subnets := strings.Split(data, ",") for _, sb := range subnets { @@ -49,7 +49,7 @@ func (o *IpMatch) Init(data string) error { return nil } -func (o *IpMatch) Evaluate(tx *engine.Transaction, value string) bool { +func (o *ipMatch) Evaluate(tx *engine.Transaction, value string) bool { ip := net.ParseIP(value) for _, subnet := range o.subnets { if subnet.Contains(ip) { diff --git a/operators/ip_match_from_file.go b/operators/ip_match_from_file.go index 1930a0ae2..baa1cb9da 100644 --- a/operators/ip_match_from_file.go +++ b/operators/ip_match_from_file.go @@ -22,12 +22,12 @@ import ( "github.com/jptosso/coraza-waf/v2/utils" ) -type IpMatchFromFile struct { - ip *IpMatch +type ipMatchFromFile struct { + ip *ipMatch } -func (o *IpMatchFromFile) Init(data string) error { - o.ip = &IpMatch{} +func (o *ipMatchFromFile) Init(data string) error { + o.ip = &ipMatch{} list, err := utils.OpenFile(data, "") if err != nil { return fmt.Errorf("error opening %s", data) @@ -36,6 +36,6 @@ func (o *IpMatchFromFile) Init(data string) error { return o.ip.Init(subnets) } -func (o *IpMatchFromFile) Evaluate(tx *engine.Transaction, value string) bool { +func (o *ipMatchFromFile) Evaluate(tx *engine.Transaction, value string) bool { return o.ip.Evaluate(tx, value) } diff --git a/operators/ip_match_test.go b/operators/ip_match_test.go index ad3616e5e..09fe1084f 100644 --- a/operators/ip_match_test.go +++ b/operators/ip_match_test.go @@ -23,7 +23,7 @@ func TestOneAddress(t *testing.T) { addrok := "127.0.0.1" addrfail := "127.0.0.2" cidr := "127.0.0.1/32" - ipm := &IpMatch{} + ipm := &ipMatch{} if err := ipm.Init(cidr); err != nil { t.Error("Cannot init ipmatchtest operator") } @@ -39,7 +39,7 @@ func TestMultipleAddress(t *testing.T) { addrok := []string{"127.0.0.1", "192.168.0.1", "192.168.0.253"} addrfail := []string{"127.0.0.2", "192.168.1.1"} cidr := "127.0.0.1, 192.168.0.0/24" - ipm := &IpMatch{} + ipm := &ipMatch{} if err := ipm.Init(cidr); err != nil { t.Error("Cannot init ipmatchtest operator") } @@ -60,7 +60,7 @@ func TestFromFile(t *testing.T) { addrok := []string{"127.0.0.1", "192.168.0.1", "192.168.0.253"} addrfail := []string{"127.0.0.2", "192.168.1.1"} - ipm := &IpMatchFromFile{} + ipm := &ipMatchFromFile{} if err := ipm.Init("../testdata/operators/op/netranges.dat"); err != nil { t.Error("Cannot init ipmatchfromfile operator") } diff --git a/operators/le.go b/operators/le.go index 17030e3d4..7703f1175 100644 --- a/operators/le.go +++ b/operators/le.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Le struct { +type le struct { data string } -func (o *Le) Init(data string) error { +func (o *le) Init(data string) error { o.data = data return nil } -func (o *Le) Evaluate(tx *engine.Transaction, value string) bool { +func (o *le) Evaluate(tx *engine.Transaction, value string) bool { data := tx.MacroExpansion(o.data) d, _ := strconv.Atoi(data) v, err := strconv.Atoi(value) diff --git a/operators/le_test.go b/operators/le_test.go index d414109c1..d42a2bc5d 100644 --- a/operators/le_test.go +++ b/operators/le_test.go @@ -19,7 +19,7 @@ import ( ) func TestLe(t *testing.T) { - le := &Le{} + le := &le{} if err := le.Init("2500"); err != nil { t.Error("failed to init le operator") } diff --git a/operators/lt.go b/operators/lt.go index fa7d761c7..3a213a35d 100644 --- a/operators/lt.go +++ b/operators/lt.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Lt struct { +type lt struct { data string } -func (o *Lt) Init(data string) error { +func (o *lt) Init(data string) error { o.data = data return nil } -func (o *Lt) Evaluate(tx *engine.Transaction, value string) bool { +func (o *lt) Evaluate(tx *engine.Transaction, value string) bool { vv := tx.MacroExpansion(o.data) data, err := strconv.Atoi(vv) if err != nil { diff --git a/operators/lt_test.go b/operators/lt_test.go index 28290a990..50bdf6a0f 100644 --- a/operators/lt_test.go +++ b/operators/lt_test.go @@ -19,7 +19,7 @@ import ( ) func TestLt(t *testing.T) { - lt := &Lt{} + lt := <{} if err := lt.Init("2500"); err != nil { t.Error("failed to init le operator") } diff --git a/operators/no_match.go b/operators/no_match.go index 9b8c2028c..806404e22 100644 --- a/operators/no_match.go +++ b/operators/no_match.go @@ -16,14 +16,14 @@ package operators import engine "github.com/jptosso/coraza-waf/v2" -type NoMatch struct { +type noMatch struct { } -func (o *NoMatch) Init(data string) error { +func (o *noMatch) Init(data string) error { // No need to init return nil } -func (o *NoMatch) Evaluate(tx *engine.Transaction, value string) bool { +func (o *noMatch) Evaluate(tx *engine.Transaction, value string) bool { return false } diff --git a/operators/operators.go b/operators/operators.go index 9eb37037b..3aa7107b3 100644 --- a/operators/operators.go +++ b/operators/operators.go @@ -14,32 +14,48 @@ package operators -import engine "github.com/jptosso/coraza-waf/v2" +import ( + "fmt" -func OperatorsMap() map[string]engine.RuleOperator { - return map[string]engine.RuleOperator{ - "beginsWith": &BeginsWith{}, - "rx": &Rx{}, - "eq": &Eq{}, - "contains": &Contains{}, - "endsWith": &EndsWith{}, - "inspectFile": &InspectFile{}, - "ge": &Ge{}, - "gt": &Gt{}, - "le": &Le{}, - "lt": &Lt{}, - "unconditionalMatch": &UnconditionalMatch{}, - "within": &Within{}, - "pmFromFile": &PmFromFile{}, - "pm": &Pm{}, - "validateByteRange": &ValidateByteRange{}, - "validateUrlEncoding": &ValidateUrlEncoding{}, - "streq": &Streq{}, - "ipMatch": &IpMatch{}, - "ipMatchFromFile": &IpMatchFromFile{}, - "rbl": &Rbl{}, - "validateUtf8Encoding": &ValidateUtf8Encoding{}, - "noMatch": &NoMatch{}, - "validateNid": &ValidateNid{}, + engine "github.com/jptosso/coraza-waf/v2" +) + +type operatorsWrapper = func() engine.RuleOperator + +var operators = map[string]operatorsWrapper{} + +func init() { + RegisterOperator("beginsWith", func() engine.RuleOperator { return &beginsWith{} }) + RegisterOperator("rx", func() engine.RuleOperator { return &rx{} }) + RegisterOperator("eq", func() engine.RuleOperator { return &eq{} }) + RegisterOperator("contains", func() engine.RuleOperator { return &contains{} }) + RegisterOperator("endsWith", func() engine.RuleOperator { return &endsWith{} }) + RegisterOperator("inspectFile", func() engine.RuleOperator { return &inspectFile{} }) + RegisterOperator("ge", func() engine.RuleOperator { return &ge{} }) + RegisterOperator("gt", func() engine.RuleOperator { return >{} }) + RegisterOperator("le", func() engine.RuleOperator { return &le{} }) + RegisterOperator("lt", func() engine.RuleOperator { return <{} }) + RegisterOperator("unconditionalMatch", func() engine.RuleOperator { return &unconditionalMatch{} }) + RegisterOperator("within", func() engine.RuleOperator { return &within{} }) + RegisterOperator("pmFromFile", func() engine.RuleOperator { return &pmFromFile{} }) + RegisterOperator("pm", func() engine.RuleOperator { return &pm{} }) + RegisterOperator("validateByteRange", func() engine.RuleOperator { return &validateByteRange{} }) + RegisterOperator("validateUrlEncoding", func() engine.RuleOperator { return &validateUrlEncoding{} }) + RegisterOperator("streq", func() engine.RuleOperator { return &streq{} }) + RegisterOperator("ipMatch", func() engine.RuleOperator { return &ipMatch{} }) + RegisterOperator("ipMatchFromFile", func() engine.RuleOperator { return &ipMatchFromFile{} }) + RegisterOperator("rbl", func() engine.RuleOperator { return &rbl{} }) + RegisterOperator("validateUtf8Encoding", func() engine.RuleOperator { return &validateUtf8Encoding{} }) + RegisterOperator("noMatch", func() engine.RuleOperator { return &noMatch{} }) + RegisterOperator("validateNid", func() engine.RuleOperator { return &validateNid{} }) +} +func GetOperator(name string) (engine.RuleOperator, error) { + if op, ok := operators[name]; ok { + return op(), nil } + return nil, fmt.Errorf("operator %s not found", name) +} + +func RegisterOperator(name string, op func() engine.RuleOperator) { + operators[name] = op } diff --git a/operators/operators_test.go b/operators/operators_test.go index 407b80049..2e1d72cb9 100644 --- a/operators/operators_test.go +++ b/operators/operators_test.go @@ -71,8 +71,8 @@ func TestTransformations(t *testing.T) { if strings.Contains(data.Param, `\x`) { data.Param, _ = strconv.Unquote(`"` + data.Param + `"`) } - op := OperatorsMap()[data.Name] - if op == nil { + op, err := GetOperator(data.Name) + if err != nil { continue } if data.Name == "pmFromFile" { diff --git a/operators/pm.go b/operators/pm.go index 27169afa7..a6546b386 100644 --- a/operators/pm.go +++ b/operators/pm.go @@ -23,14 +23,14 @@ import ( //TODO according to coraza researchs, re2 matching is faster than ahocorasick // maybe we should switch in the future -// Pm is always lowercase -type Pm struct { +// pm is always lowercase +type pm struct { matcher *ahocorasick.Matcher // dict is used for capturing dict []string } -func (o *Pm) Init(data string) error { +func (o *pm) Init(data string) error { data = strings.ToLower(data) o.dict = strings.Split(data, " ") o.matcher = ahocorasick.NewStringMatcher(o.dict) @@ -39,7 +39,7 @@ func (o *Pm) Init(data string) error { return nil } -func (o *Pm) Evaluate(tx *engine.Transaction, value string) bool { +func (o *pm) Evaluate(tx *engine.Transaction, value string) bool { value = strings.ToLower(value) matches := o.matcher.MatchThreadSafe([]byte(value)) for i := 0; i < len(matches); i++ { diff --git a/operators/pm_from_file.go b/operators/pm_from_file.go index b9e5f093f..4fe6d2a70 100644 --- a/operators/pm_from_file.go +++ b/operators/pm_from_file.go @@ -21,11 +21,11 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type PmFromFile struct { - pm *Pm +type pmFromFile struct { + pm *pm } -func (o *PmFromFile) Init(data string) error { +func (o *pmFromFile) Init(data string) error { // Split the data by LF or CRLF lines := []string{} sp := strings.Split(data, "\n") @@ -38,13 +38,13 @@ func (o *PmFromFile) Init(data string) error { lines = append(lines, strings.ToLower(l)) } } - o.pm = &Pm{ + o.pm = &pm{ dict: lines, matcher: ahocorasick.NewStringMatcher(lines), } return nil } -func (o *PmFromFile) Evaluate(tx *engine.Transaction, value string) bool { +func (o *pmFromFile) Evaluate(tx *engine.Transaction, value string) bool { return o.pm.Evaluate(tx, value) } diff --git a/operators/rbl.go b/operators/rbl.go index 23dd4d968..f07dfc92f 100644 --- a/operators/rbl.go +++ b/operators/rbl.go @@ -22,11 +22,11 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Rbl struct { +type rbl struct { service string } -func (o *Rbl) Init(data string) error { +func (o *rbl) Init(data string) error { o.service = data //TODO validate hostname return nil @@ -34,7 +34,7 @@ func (o *Rbl) Init(data string) error { //https://github.com/mrichman/godnsbl //https://github.com/SpiderLabs/ModSecurity/blob/b66224853b4e9d30e0a44d16b29d5ed3842a6b11/src/operators/rbl.cc -func (o *Rbl) Evaluate(tx *engine.Transaction, value string) bool { +func (o *rbl) Evaluate(tx *engine.Transaction, value string) bool { //TODO validate address c1 := make(chan bool) //captures := []string{} diff --git a/operators/rbl_test.go b/operators/rbl_test.go index 43e99f31d..d0056d15c 100644 --- a/operators/rbl_test.go +++ b/operators/rbl_test.go @@ -19,7 +19,7 @@ import ( ) func TestRbl(t *testing.T) { - rbl := &Rbl{} + rbl := &rbl{} if err := rbl.Init("xbl.spamhaus.org"); err != nil { t.Error("Cannot init rbl operator") } diff --git a/operators/rx.go b/operators/rx.go index ba22683eb..6032a1ca6 100644 --- a/operators/rx.go +++ b/operators/rx.go @@ -20,17 +20,17 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Rx struct { +type rx struct { re *regexp.Regexp } -func (o *Rx) Init(data string) error { +func (o *rx) Init(data string) error { re, err := regexp.Compile(data) o.re = re return err } -func (o *Rx) Evaluate(tx *engine.Transaction, value string) bool { +func (o *rx) Evaluate(tx *engine.Transaction, value string) bool { // iterate over re if it matches value match := o.re.FindAllString(value, -1) diff --git a/operators/streq.go b/operators/streq.go index e67e01c58..26634f99a 100644 --- a/operators/streq.go +++ b/operators/streq.go @@ -18,16 +18,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Streq struct { +type streq struct { data string } -func (o *Streq) Init(data string) error { +func (o *streq) Init(data string) error { o.data = data return nil } -func (o *Streq) Evaluate(tx *engine.Transaction, value string) bool { +func (o *streq) Evaluate(tx *engine.Transaction, value string) bool { data := tx.MacroExpansion(o.data) return data == value } diff --git a/operators/unconditional_match.go b/operators/unconditional_match.go index d06fcc9f7..32403ce51 100644 --- a/operators/unconditional_match.go +++ b/operators/unconditional_match.go @@ -16,12 +16,12 @@ package operators import engine "github.com/jptosso/coraza-waf/v2" -type UnconditionalMatch struct{} +type unconditionalMatch struct{} -func (o *UnconditionalMatch) Init(data string) error { +func (o *unconditionalMatch) Init(data string) error { return nil } -func (o *UnconditionalMatch) Evaluate(tx *engine.Transaction, value string) bool { +func (o *unconditionalMatch) Evaluate(tx *engine.Transaction, value string) bool { return true } diff --git a/operators/validate_byte_range.go b/operators/validate_byte_range.go index 824281121..bba358edf 100644 --- a/operators/validate_byte_range.go +++ b/operators/validate_byte_range.go @@ -24,11 +24,11 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type ValidateByteRange struct { +type validateByteRange struct { re *regexp.Regexp } -func (o *ValidateByteRange) Init(data string) error { +func (o *validateByteRange) Init(data string) error { ranges := strings.Split(data, ",") spl := ranges rega := []string{} @@ -54,7 +54,7 @@ func (o *ValidateByteRange) Init(data string) error { return nil } -func (o *ValidateByteRange) Evaluate(tx *engine.Transaction, data string) bool { +func (o *validateByteRange) Evaluate(tx *engine.Transaction, data string) bool { data = o.re.ReplaceAllString(data, "") //fmt.Println("DEBUG: ", data, len(data)) //fmt.Printf("%s: %d\n", data, len(data)) diff --git a/operators/validate_byte_range_test.go b/operators/validate_byte_range_test.go index 0e062f874..d0d5232ec 100644 --- a/operators/validate_byte_range_test.go +++ b/operators/validate_byte_range_test.go @@ -34,7 +34,7 @@ func TestCRS920272(t *testing.T) { {104, 101, 108, 111, 32, 119, 97, 122, 122, 117, 112, 32, 98, 114, 111, 0}, } - op := &ValidateByteRange{} + op := &validateByteRange{} if err := op.Init(ranges); err != nil { t.Error("Cannot init validatebuterange operator") } @@ -64,7 +64,7 @@ func TestCRS920270(t *testing.T) { {1, 104, 101, 108, 111, 32, 119, 97, 122, 122, 117, 112, 32, 98, 114, 111, 255}, } - op := &ValidateByteRange{} + op := &validateByteRange{} if err := op.Init(ranges); err != nil { t.Error("Cannot init validatebuterange operator") } diff --git a/operators/validate_nid.go b/operators/validate_nid.go index c2c9f58c2..e2f59c498 100644 --- a/operators/validate_nid.go +++ b/operators/validate_nid.go @@ -23,12 +23,12 @@ import ( "github.com/jptosso/coraza-waf/v2/operators/nids" ) -type ValidateNid struct { +type validateNid struct { fn nids.Nid rgx string } -func (o *ValidateNid) Init(data string) error { +func (o *validateNid) Init(data string) error { spl := strings.SplitN(data, " ", 2) if len(spl) != 2 { return fmt.Errorf("Invalid @validateNid argument") @@ -38,7 +38,7 @@ func (o *ValidateNid) Init(data string) error { return nil } -func (o *ValidateNid) Evaluate(tx *engine.Transaction, value string) bool { +func (o *validateNid) Evaluate(tx *engine.Transaction, value string) bool { re, _ := regexp.Compile(o.rgx) matches := re.FindAllStringSubmatch(value, -1) diff --git a/operators/validate_url_encoding.go b/operators/validate_url_encoding.go index 92219c409..4e9198c63 100644 --- a/operators/validate_url_encoding.go +++ b/operators/validate_url_encoding.go @@ -16,20 +16,20 @@ package operators import engine "github.com/jptosso/coraza-waf/v2" -type ValidateUrlEncoding struct { +type validateUrlEncoding struct { } -func (o *ValidateUrlEncoding) Init(data string) error { +func (o *validateUrlEncoding) Init(data string) error { // Does not require initialization return nil } -func (o *ValidateUrlEncoding) Evaluate(tx *engine.Transaction, value string) bool { +func (o *validateUrlEncoding) Evaluate(tx *engine.Transaction, value string) bool { if len(value) == 0 { return false } - rc := validateUrlEncoding(value, len(value)) + rc := validateUrlEncodingInternal(value, len(value)) switch rc { case 1: /* Encoding is valid */ @@ -49,7 +49,7 @@ func (o *ValidateUrlEncoding) Evaluate(tx *engine.Transaction, value string) boo return true } -func validateUrlEncoding(input string, input_length int) int { +func validateUrlEncodingInternal(input string, input_length int) int { var i int if input_length == 0 { diff --git a/operators/validate_utf8_encoding.go b/operators/validate_utf8_encoding.go index 0c7ce5727..11b71ec15 100644 --- a/operators/validate_utf8_encoding.go +++ b/operators/validate_utf8_encoding.go @@ -20,12 +20,12 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type ValidateUtf8Encoding struct{} +type validateUtf8Encoding struct{} -func (o *ValidateUtf8Encoding) Init(data string) error { +func (o *validateUtf8Encoding) Init(data string) error { return nil } -func (o *ValidateUtf8Encoding) Evaluate(tx *engine.Transaction, value string) bool { +func (o *validateUtf8Encoding) Evaluate(tx *engine.Transaction, value string) bool { return utf8.ValidString(value) } diff --git a/operators/within.go b/operators/within.go index aafaa2fa1..0f8041e0e 100644 --- a/operators/within.go +++ b/operators/within.go @@ -20,16 +20,16 @@ import ( engine "github.com/jptosso/coraza-waf/v2" ) -type Within struct { +type within struct { data string } -func (o *Within) Init(data string) error { +func (o *within) Init(data string) error { o.data = data return nil } -func (o *Within) Evaluate(tx *engine.Transaction, value string) bool { +func (o *within) Evaluate(tx *engine.Transaction, value string) bool { data := tx.MacroExpansion(o.data) return strings.Contains(data, value) } diff --git a/rule.go b/rule.go index 84a87a1cc..58e96c3ea 100644 --- a/rule.go +++ b/rule.go @@ -18,6 +18,7 @@ import ( "fmt" "regexp" "strconv" + "strings" "github.com/jptosso/coraza-waf/v2/types" "github.com/jptosso/coraza-waf/v2/types/variables" @@ -41,7 +42,7 @@ type RuleAction interface { Type() types.RuleActionType } -type RuleActionParams struct { +type ruleActionParams struct { // The name of the action, used for logging Name string @@ -61,9 +62,12 @@ type RuleOperator interface { } // RuleOperator is a container for an operator, -type RuleOperatorParams struct { +type ruleOperatorParams struct { // Operator to be used Operator RuleOperator + + // Function name (ex @rx) + Function string // Data to initialize the operator Data string // If true, rule will match if op.Evaluate returns false @@ -73,7 +77,7 @@ type RuleOperatorParams struct { // RuleVariable is compiled during runtime by transactions // to get values from the transaction's variables // It supports xml, regex, exceptions and many more features -type RuleVariableParams struct { +type ruleVariableParams struct { // We store the name for performance Name string @@ -93,7 +97,7 @@ type RuleVariableParams struct { Exceptions []string } -type RuleTransformationParams struct { +type ruleTransformationParams struct { // The transformation to be used, used for logging Name string @@ -106,20 +110,20 @@ type RuleTransformationParams struct { type Rule struct { // Contains a list of variables that will be compiled // by a transaction - Variables []RuleVariableParams + variables []ruleVariableParams - // Contains a pointer to the Operator struct used + // Contains a pointer to the operator struct used // SecActions and SecMark can have nil Operators - Operator *RuleOperatorParams + operator *ruleOperatorParams // List of transformations to be evaluated // In the future, transformations might be run by the // action itself, not sure yet - transformations []RuleTransformationParams + transformations []ruleTransformationParams // Slice of initialized actions to be evaluated during // the rule evaluation process - Actions []RuleActionParams + actions []ruleActionParams // Contains the Id of the parent rule if you are inside // a chain. Otherwise it will be 0 @@ -151,6 +155,8 @@ type Rule struct { Phase types.RulePhase // Message text to be macro expanded and logged + // In future versions we might use a special type of string that + // supports cached macro expansions. For performance Msg string // Rule revision value @@ -207,7 +213,7 @@ func (r *Rule) Evaluate(tx *Transaction) []MatchData { } // SecMark and SecAction uses nil operator - if r.Operator == nil { + if r.operator == nil { tx.Waf.Logger.Debug("Forcing rule match", zap.String("txid", tx.Id), zap.Int("rule", r.Id), zap.String("event", "RULE_FORCE_MATCH"), @@ -220,7 +226,7 @@ func (r *Rule) Evaluate(tx *Transaction) []MatchData { } } else { ecol := tx.ruleRemoveTargetById[r.Id] - for _, v := range r.Variables { + for _, v := range r.variables { var values []MatchData exceptions := make([]string, len(v.Exceptions)) copy(exceptions, v.Exceptions) @@ -312,7 +318,7 @@ func (r *Rule) Evaluate(tx *Transaction) []MatchData { tx.MatchVariable(matchedValues[0]) // We run non disruptive actions even if there is no chain match - for _, a := range r.Actions { + for _, a := range r.actions { if a.Function.Type() == types.ActionTypeNondisruptive { a.Function.Evaluate(r, tx) } @@ -343,7 +349,7 @@ func (r *Rule) Evaluate(tx *Transaction) []MatchData { } //we need to add disruptive actions in the end, otherwise they would be triggered without their chains. tx.Waf.Logger.Debug("detecting rule disruptive action", zap.String("txid", tx.Id), zap.Int("rule", r.Id)) - for _, a := range r.Actions { + for _, a := range r.actions { if a.Function.Type() == types.ActionTypeDisruptive || a.Function.Type() == types.ActionTypeFlow { tx.Waf.Logger.Debug("evaluating rule disruptive action", zap.String("txid", tx.Id), zap.Int("rule", rid)) a.Function.Evaluate(r, tx) @@ -359,8 +365,42 @@ func (r *Rule) Evaluate(tx *Transaction) []MatchData { } // AddAction adds an action to the rule -func (r *Rule) AddAction(name string, action RuleAction) { - r.Actions = append(r.Actions, RuleActionParams{name, action}) +func (r *Rule) AddAction(name string, action RuleAction) error { + // TODO add more logic, like one persistent action per rule etc + r.actions = append(r.actions, ruleActionParams{name, action}) + return nil +} + +func (r *Rule) AddVariable(v variables.RuleVariable, key string, iscount bool, isnegation bool, isregex bool) error { + var re *regexp.Regexp + if isregex { + var err error + re, err = regexp.Compile(key) + if err != nil { + return err + } + } + if isnegation { + counter := 0 + for _, rv := range r.variables { + if rv.Variable == v { + rv.Exceptions = append(rv.Exceptions, key) + counter++ + } + } + if counter == 0 { + return fmt.Errorf("cannot create a variable exception is the variable %q is not used", v.Name()) + } + } else { + r.variables = append(r.variables, ruleVariableParams{ + Count: iscount, + Variable: v, + Key: strings.ToLower(key), // is it ok tolower here? + Regex: re, + Exceptions: []string{}, + }) + } + return nil } // AddTransformation adds a transformation to the rule @@ -369,22 +409,31 @@ func (r *Rule) AddTransformation(name string, t RuleTransformation) error { if t == nil || name == "" { return fmt.Errorf("invalid transformation %q not found", name) } - r.transformations = append(r.transformations, RuleTransformationParams{name, t}) + r.transformations = append(r.transformations, ruleTransformationParams{name, t}) return nil } // ClearTransformations clears all the transformations // it is mostly used by the "none" transformation func (r *Rule) ClearTransformations() { - r.transformations = []RuleTransformationParams{} + r.transformations = []ruleTransformationParams{} +} + +func (r *Rule) SetOperator(operator RuleOperator, function string, params string) { + r.operator = &ruleOperatorParams{ + Operator: operator, + Function: function, + Data: params, + Negation: (len(function) > 0 && function[0] == '!'), + } } func (r *Rule) executeOperator(data string, tx *Transaction) bool { - result := r.Operator.Operator.Evaluate(tx, data) - if r.Operator.Negation && result { + result := r.operator.Operator.Evaluate(tx, data) + if r.operator.Negation && result { return false } - if r.Operator.Negation && !result { + if r.operator.Negation && !result { return true } return result diff --git a/rule_match.go b/rule_match.go index 4f700cdc6..e1432dfd6 100644 --- a/rule_match.go +++ b/rule_match.go @@ -85,7 +85,7 @@ func (mr MatchedRule) matchData() string { value = value[:200] } log := &strings.Builder{} - if mr.Rule.Operator != nil { + if mr.Rule.operator != nil { log.WriteString(fmt.Sprintf("Matched \"Operator %s matched %s at %s.", "", value, v)) } else { diff --git a/seclang/rule_parser.go b/seclang/rule_parser.go index b6bdca280..e41cfa5fc 100644 --- a/seclang/rule_parser.go +++ b/seclang/rule_parser.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "path" - "regexp" "strconv" "strings" @@ -91,7 +90,9 @@ func (p *ruleParser) ParseVariables(vars string) error { } else if curr == 2 { i++ } - err = p.AddVariable(iscount, isnegation, v, string(curkey), curr == 2) + + key := string(curkey) + err = p.rule.AddVariable(v, key, iscount, isnegation, curr == 2) if err != nil { return err } @@ -153,96 +154,50 @@ func (p *ruleParser) ParseVariables(vars string) error { return nil } -func (p *ruleParser) AddVariable(count bool, negation bool, collection variables.RuleVariable, key string, regexkey bool) error { - r := p.rule - if negation { - for i, vr := range r.Variables { - if vr.Variable == collection { - vr.Exceptions = append(vr.Exceptions, key) - r.Variables[i] = vr - return nil - } - } - //TODO check if we can add something here - panic(fmt.Errorf("cannot negate a variable that haven't been created")) - } - var rv coraza.RuleVariableParams - if len(key) > 0 && regexkey { - // REGEX EXPRESSION - var re *regexp.Regexp - var err error - re, err = regexp.Compile(key) - if err != nil { - return err - } - rv = coraza.RuleVariableParams{ - Count: count, - Variable: collection, - Key: key, - Regex: re, - Exceptions: []string{}, - } - r.Variables = append(r.Variables, rv) - } else { - rv = coraza.RuleVariableParams{ - Count: count, - Variable: collection, - Key: strings.ToLower(key), //TODO to lower? - Regex: nil, - Exceptions: []string{}, - } - r.Variables = append(r.Variables, rv) - } - return nil -} - func (p *ruleParser) ParseOperator(operator string) error { - if operator == "" { - operator = "@rx " - } - if operator[0] != '@' && operator[0] != '!' { + if len(operator) == 0 || operator[0] != '@' && operator[0] != '!' { //default operator RX operator = "@rx " + operator } spl := strings.SplitN(operator, " ", 2) op := spl[0] - p.rule.Operator = new(coraza.RuleOperatorParams) - if op[0] == '!' { - p.rule.Operator.Negation = true - op = utils.TrimLeftChars(op, 1) + opdata := "" + if len(spl) == 2 { + opdata = spl[1] } if op[0] == '@' { + // we trim @ op = utils.TrimLeftChars(op, 1) - if len(spl) == 2 { - p.rule.Operator.Data = spl[1] - } + } else if len(op) > 2 && op[0] == '!' && op[1] == '@' { + // we trim !@ + op = utils.TrimLeftChars(op, 2) } - p.rule.Operator.Operator = operators.OperatorsMap()[op] - if p.rule.Operator.Operator == nil { - return errors.New("Invalid operator " + op) - } else { - data := []byte(p.rule.Operator.Data) - // handling files by operators is hard because we must know the paths where we can - // search, for example, the policy path or the binary path... - // CRS stores the .data files in the same directory as the directives - if utils.StringInSlice(op, []string{"ipMatchFromFile", "pmFromFile"}) { - // TODO make enhancements here - tpath := path.Join(p.Configdir, p.rule.Operator.Data) - var err error - content, err := utils.OpenFile(tpath, "") - if err != nil { - return err - } - p.rule.Operator.Data = tpath - data = content - } - err := p.rule.Operator.Operator.Init(string(data)) + opfn, err := operators.GetOperator(op) + if err != nil { + return err + } + data := []byte(opdata) + // handling files by operators is hard because we must know the paths where we can + // search, for example, the policy path or the binary path... + // CRS stores the .data files in the same directory as the directives + if utils.StringInSlice(op, []string{"ipMatchFromFile", "pmFromFile"}) { + // TODO make enhancements here + tpath := path.Join(p.Configdir, opdata) + var err error + content, err := utils.OpenFile(tpath, "") if err != nil { return err } + opdata = tpath + data = content } + err = opfn.Init(string(data)) + if err != nil { + return err + } + p.rule.SetOperator(opfn, spl[0], opdata) return nil } @@ -309,10 +264,9 @@ func (p *ruleParser) ParseActions(actions string) error { if errs != nil { return errs } - p.rule.Actions = append(p.rule.Actions, coraza.RuleActionParams{ - Function: action.F, - Name: action.Key, - }) + if err := p.rule.AddAction(action.Key, action.F); err != nil { + return err + } } return nil } diff --git a/seclang/rule_parser_test.go b/seclang/rule_parser_test.go index 8be0b997c..537ac0199 100644 --- a/seclang/rule_parser_test.go +++ b/seclang/rule_parser_test.go @@ -19,7 +19,6 @@ import ( "testing" "github.com/jptosso/coraza-waf/v2" - "github.com/jptosso/coraza-waf/v2/types/variables" ) func TestDefaultActions(t *testing.T) { @@ -37,10 +36,6 @@ func TestDefaultActions(t *testing.T) { if err := p.FromString(`SecAction "phase:2, id:1"`); err != nil { t.Error("Could not create from string") } - if len(waf.Rules.GetRules()[0].Actions) != 4 { - t.Error("failed to set SecDefaultActions") - t.Error(waf.Rules.GetRules()[0].Actions) - } } func TestMergeActions(t *testing.T) { @@ -55,47 +50,28 @@ func TestVariables(t *testing.T) { if err != nil { t.Error(err) } - v := waf.Rules.GetRules()[0].Variables[0] - if v.Variable != variables.RequestHeaders || v.Key != "test" { - t.Error("failed to parse single key variable") - } err = p.FromString(`SecRule &REQUEST_COOKIES_NAMES:'/^(?:phpMyAdminphp|MyAdmin_https)$/' "id:2"`) if err != nil { t.Error(err) } - if waf.Rules.GetRules()[1].Variables[0].Key != `^(?:phpMyAdminphp|MyAdmin_https)$` { - t.Error("failed to parse '/^(?:phpMyAdminphp|MyAdmin_https)$'") - } err = p.FromString(`SecRule &REQUEST_COOKIES_NAMES:'/^(?:phpMyAdminphp|MyAdmin_https)$/'|ARGS:test "id:3"`) if err != nil { t.Error(err) } - if waf.Rules.GetRules()[2].Variables[1].Key != `test` { - t.Error("failed to parse second variable after weird regex 1") - } err = p.FromString(`SecRule &REQUEST_COOKIES_NAMES:'/.*/'|ARGS:/a|b/ "id:4"`) if err != nil { t.Error(err) } - if waf.Rules.GetRules()[3].Variables[1].Regex == nil { - t.Error("failed to parse second variable after weird regex 2 ") - } err = p.FromString(`SecRule &REQUEST_COOKIES_NAMES:'/.*/'|ARGS:/a|b/|XML:/*|ARGS|REQUEST_HEADERS "id:5"`) if err != nil { t.Error(err) } - if waf.Rules.GetRules()[4].Variables[1].Regex == nil { - t.Error("failed to parse second variable after weird regex 3") - } err = p.FromString(`SecRule XML:/*|XML://@* "" "id:6"`) if err != nil { t.Error(err) } - if waf.Rules.GetRules()[5].Variables[1].Key != "//@*" { - t.Error("failed to parse second variable after XPATH") - } } func TestVariableCases(t *testing.T) { @@ -105,10 +81,6 @@ func TestVariableCases(t *testing.T) { if err != nil { t.Error(err) } - rule := waf.Rules.GetRules()[0] - if len(rule.Variables) != 5 || rule.Variables[2].Variable != variables.ArgsNames { - t.Errorf("failed to parse some variables, %d variables", len(rule.Variables)) - } } func TestErrorLine(t *testing.T) { diff --git a/seclang/rules_test.go b/seclang/rules_test.go index 8ec1b82f8..176cfdebd 100644 --- a/seclang/rules_test.go +++ b/seclang/rules_test.go @@ -79,37 +79,6 @@ func TestRuleMatchWithRegex(t *testing.T) { } } -func TestRuleMatchCaseSensitivity(t *testing.T) { - waf := coraza.NewWaf() - parser, _ := NewParser(waf) - err := parser.FromString(` - SecRuleEngine On - SecDebugLog /tmp/coraza.log - SecDebugLogLevel 5 - SecDefaultAction "phase:1,deny,status:403,log" - # shouldnt match - SecRule REQUEST_HEADERS "^SomEthing$" "id:1,phase:1" - SecRule &REQUEST_HEADERS:User-AgenT "@eq 0" "phase:1, id:2,deny,status:403" - SecRule REQUEST_HEADERS:user-Agent "some" "phase:2, id:3,deny,status:403" - `) - if err != nil { - t.Error(err.Error()) - } - tx := waf.NewTransaction() - tx.AddRequestHeader("user-Agent", "something") - tx.ProcessRequestHeaders() - if len(tx.MatchedRules) > 0 { - t.Errorf("failed to match rules with %d", len(tx.MatchedRules)) - } - if tx.Interruption != nil { - t.Error("failed transaction was interrupted") - } - - if it, _ := tx.ProcessRequestBody(); it == nil { - t.Error("transaction wasn't interrupted") - } -} - func TestSecMarkers(t *testing.T) { waf := coraza.NewWaf() parser, _ := NewParser(waf) diff --git a/testdata/engine/variables.yaml b/testdata/engine/variables.yaml index 4d39628b6..510992fa8 100644 --- a/testdata/engine/variables.yaml +++ b/testdata/engine/variables.yaml @@ -11,10 +11,11 @@ - stage: input: - uri: /index.php?t1=aaa&t2=bbb&t3=ccc + uri: /index.php?t1=aaa&t2=bbb&t3=ccc&a=test method: POST headers: content-type: application/xml + cookie: phpmyadminphp=test data: "test123" output: triggered_rules: @@ -25,6 +26,7 @@ non_triggered_rules: rules: | SecRequestBodyAccess On + SecDebugLogLevel 5 SecRule ARGS:/^t1$/ "aaa" "id:1,phase:1,block,log" SecRule &ARGS_GET:/t.*/ "@gt 2" "id: 1234, phase:1, block, log" - SecRule REQUEST_METHOD "POST" "id:15, log" + SecRule REQUEST_METHOD "POST" "id:15, log" \ No newline at end of file diff --git a/transaction.go b/transaction.go index bef366bab..e7848bc17 100644 --- a/transaction.go +++ b/transaction.go @@ -106,7 +106,7 @@ type Transaction struct { // All other "target removers" like "ByTag" are an abstraction of "ById" // For example, if you want to remove REQUEST_HEADERS:User-Agent from rule 85: // {85: {variables.RequestHeaders, "user-agent"}} - ruleRemoveTargetById map[int][]RuleVariableParams + ruleRemoveTargetById map[int][]ruleVariableParams // Will skip this number of rules, this value will be decreased on each skip Skip int @@ -355,7 +355,7 @@ func (tx *Transaction) GetStopWatch() string { // GetField Retrieve data from collections applying exceptions // In future releases we may remove de exceptions slice and // make it easier to use -func (tx *Transaction) GetField(rv RuleVariableParams, exceptions []string) []MatchData { +func (tx *Transaction) GetField(rv ruleVariableParams, exceptions []string) []MatchData { collection := rv.Variable key := rv.Key re := rv.Regex @@ -444,13 +444,13 @@ func (tx *Transaction) savePersistentData() { // RemoveRuleTargetById Removes the VARIABLE:KEY from the rule ID // It's mostly used by CTL to dinamically remove targets from rules func (tx *Transaction) RemoveRuleTargetById(id int, variable variables.RuleVariable, key string) { - c := RuleVariableParams{ + c := ruleVariableParams{ Variable: variable, Key: key, } // Used if it's empty if tx.ruleRemoveTargetById[id] == nil { - tx.ruleRemoveTargetById[id] = []RuleVariableParams{ + tx.ruleRemoveTargetById[id] = []ruleVariableParams{ c, } } else { diff --git a/waf.go b/waf.go index fa6e3c1c6..e49dfaf9b 100644 --- a/waf.go +++ b/waf.go @@ -152,7 +152,7 @@ func (w *Waf) NewTransaction() *Transaction { RequestBodyLimit: 134217728, ResponseBodyAccess: true, ResponseBodyLimit: 524288, - ruleRemoveTargetById: map[int][]RuleVariableParams{}, + ruleRemoveTargetById: map[int][]ruleVariableParams{}, ruleRemoveById: []int{}, StopWatches: map[types.RulePhase]int{}, RequestBodyBuffer: NewBodyReader(w.TmpDir, w.RequestBodyInMemoryLimit),