Merge branch 'coraza_poc_acquis' of github.com:crowdsecurity/crowdsec into coraza_poc_acquis
This commit is contained in:
commit
e49f33b4a7
8 changed files with 307 additions and 235 deletions
|
@ -202,6 +202,9 @@ func (r *WaapRunner) AccumulateTxToEvent(evt *types.Event, req waf.ParsedRequest
|
|||
"accuracy": rule.Rule().Accuracy(),
|
||||
"msg": rule.Message(),
|
||||
"severity": rule.Rule().Severity().String(),
|
||||
"name": "FIXFIXFIXFIXFIX",
|
||||
"hash": "FIXIFIX",
|
||||
"version": "FIXFIXFIX",
|
||||
}
|
||||
evt.Waap.MatchedRules = append(evt.Waap.MatchedRules, corazaRule)
|
||||
}
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
package waf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VPatchRule struct {
|
||||
//Those 2 together represent something like ARGS.foo
|
||||
//If only target is set, it's used for variables that are not a collection (REQUEST_METHOD, etc)
|
||||
Target string `yaml:"target"`
|
||||
Variable string `yaml:"var"`
|
||||
|
||||
Match string `yaml:"match"` //@rx
|
||||
Equals string `yaml:"equals"` //@eq
|
||||
Transform string `yaml:"transform"` //t:lowercase, t:uppercase, etc
|
||||
Detect string `yaml:"detect"` //@detectXSS, @detectSQLi, etc
|
||||
Logic string `yaml:"logic,omitempty"` // "AND", "OR", or empty if not applicable
|
||||
SubRules []VPatchRule `yaml:"sub_rules,omitempty"`
|
||||
}
|
||||
|
||||
func (v *VPatchRule) String() string {
|
||||
return strings.Trim(v.constructRule(0), "\n")
|
||||
}
|
||||
|
||||
func countTotalRules(rules []VPatchRule) int {
|
||||
count := 0
|
||||
for _, rule := range rules {
|
||||
count++
|
||||
if rule.Logic == "AND" {
|
||||
count += countTotalRules(rule.SubRules)
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (v *VPatchRule) constructRule(depth int) string {
|
||||
var result string
|
||||
result = v.singleRuleString()
|
||||
|
||||
if len(v.SubRules) == 0 {
|
||||
return result + "\n"
|
||||
}
|
||||
|
||||
switch v.Logic {
|
||||
case "AND":
|
||||
result = strings.TrimSuffix(result, `"`) + `,chain"` + "\n"
|
||||
for _, subRule := range v.SubRules {
|
||||
result += subRule.constructRule(depth + 1)
|
||||
}
|
||||
case "OR":
|
||||
skips := countTotalRules(v.SubRules) - 1
|
||||
if depth == 0 {
|
||||
skips++
|
||||
}
|
||||
result = strings.TrimSuffix(result, `"`) + fmt.Sprintf(`,skip:%d"`+"\n", skips)
|
||||
for _, subRule := range v.SubRules {
|
||||
skips--
|
||||
if skips > 0 {
|
||||
result += strings.TrimSuffix(subRule.singleRuleString(), `"`) + fmt.Sprintf(`,skip:%d"`+"\n", skips)
|
||||
} else {
|
||||
result += subRule.singleRuleString() + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (v *VPatchRule) singleRuleString() string {
|
||||
var operator string
|
||||
var ruleStr string
|
||||
|
||||
if v.Match != "" {
|
||||
operator = fmt.Sprintf("@rx %s", v.Match)
|
||||
} else if v.Equals != "" {
|
||||
operator = fmt.Sprintf("@eq %s", v.Equals)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
if v.Variable != "" {
|
||||
ruleStr = fmt.Sprintf(`SecRule %s:%s "%s"`, v.Target, v.Variable, operator)
|
||||
} else {
|
||||
ruleStr = fmt.Sprintf(`SecRule %s "%s"`, v.Target, operator)
|
||||
}
|
||||
|
||||
//FIXME: phase2 should probably not be hardcoded
|
||||
//Find a better way than using time.Now().UnixMilli() to generate a unique ID
|
||||
actions := fmt.Sprintf(` "id:%d,deny,log,phase:2`, time.Now().UnixNano())
|
||||
|
||||
// Handle transformation
|
||||
if v.Transform != "" {
|
||||
actions = actions + fmt.Sprintf(",t:%s", v.Transform)
|
||||
}
|
||||
actions = actions + `"`
|
||||
ruleStr = ruleStr + actions
|
||||
|
||||
return ruleStr
|
||||
}
|
67
pkg/waf/waap_rule/modsec_rule_test.go
Normal file
67
pkg/waf/waap_rule/modsec_rule_test.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package waap_rule
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestVPatchRuleString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rule CustomRule
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Base Rule",
|
||||
rule: CustomRule{
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
expected: `SecRule ARGS_GET:foo "@rx [^a-zA-Z]" "id:1136235475,phase:2,deny,log,msg:'Base Rule',t:lowercase"`,
|
||||
},
|
||||
{
|
||||
name: "Multiple Zones",
|
||||
rule: CustomRule{
|
||||
Zones: []string{"ARGS", "BODY_ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
Match: match{Type: "regex", Value: "[^a-zA-Z]"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
expected: `SecRule ARGS_GET:foo|ARGS_POST:foo "@rx [^a-zA-Z]" "id:2088895799,phase:2,deny,log,msg:'Multiple Zones',t:lowercase"`,
|
||||
},
|
||||
{
|
||||
name: "Basic AND",
|
||||
rule: CustomRule{
|
||||
And: []CustomRule{
|
||||
{
|
||||
|
||||
Zones: []string{"ARGS"},
|
||||
Variables: []string{"foo"},
|
||||
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]"},
|
||||
Transform: []string{"lowercase"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `SecRule ARGS_GET:foo "@rx [^a-zA-Z]" "id:2323451654,phase:2,deny,log,msg:'Basic AND_and_0',t:lowercase,chain"
|
||||
SecRule ARGS_GET:bar "@rx [^a-zA-Z]" "id:2075918819,phase:2,deny,log,msg:'Basic AND_and_1',t:lowercase"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual, err := tt.rule.Convert(ModsecurityRuleType, tt.name)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error converting rule: %s", err)
|
||||
}
|
||||
if actual != tt.expected {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", tt.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
152
pkg/waf/waap_rule/modsecurity.go
Normal file
152
pkg/waf/waap_rule/modsecurity.go
Normal file
|
@ -0,0 +1,152 @@
|
|||
package waap_rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ModsecurityRule struct {
|
||||
}
|
||||
|
||||
var zonesMap map[string]string = map[string]string{
|
||||
"ARGS": "ARGS_GET",
|
||||
"ARGS_NAMES": "ARGS_GET_NAMES",
|
||||
"BODY_ARGS": "ARGS_POST",
|
||||
"BODY_ARGS_NAMES": "ARGS_POST_NAMES",
|
||||
"HEADERS": "REQUEST_HEADERS",
|
||||
"METHOD": "REQUEST_METHOD",
|
||||
"PROTOCOL": "REQUEST_PROTOCOL",
|
||||
"URI": "REQUEST_URI",
|
||||
}
|
||||
|
||||
var transformMap map[string]string = map[string]string{
|
||||
"lowercase": "t:lowercase",
|
||||
"uppercase": "t:uppercase",
|
||||
"b64decode": "t:base64Decode",
|
||||
"hexdecode": "t:hexDecode",
|
||||
"length": "t:length",
|
||||
}
|
||||
|
||||
var matchMap map[string]string = map[string]string{
|
||||
"regex": "@rx",
|
||||
"equal": "@streq",
|
||||
"startsWith": "@beginsWith",
|
||||
"endsWith": "@endsWith",
|
||||
"contains": "@contains",
|
||||
"libinjectionSQL": "@detectSQLi",
|
||||
"libinjectionXSS": "@detectXSS",
|
||||
"gt": "@gt",
|
||||
"lt": "@lt",
|
||||
"ge": "@ge",
|
||||
"le": "@le",
|
||||
}
|
||||
|
||||
func (m ModsecurityRule) Build(rule *CustomRule, waapRuleName string) (string, error) {
|
||||
|
||||
rules, err := m.buildRules(rule, waapRuleName, false)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.Join(rules, "\n"), nil
|
||||
}
|
||||
|
||||
func (m ModsecurityRule) generateRuleID(rule *CustomRule, waapRuleName string) uint32 {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(waapRuleName))
|
||||
h.Write([]byte(rule.Match.Type))
|
||||
h.Write([]byte(rule.Match.Value))
|
||||
for _, zone := range rule.Zones {
|
||||
h.Write([]byte(zone))
|
||||
}
|
||||
for _, transform := range rule.Transform {
|
||||
h.Write([]byte(transform))
|
||||
}
|
||||
return h.Sum32()
|
||||
}
|
||||
|
||||
func (m ModsecurityRule) buildRules(rule *CustomRule, waapRuleName string, and bool) ([]string, error) {
|
||||
ret := make([]string, 0)
|
||||
|
||||
if rule.And != nil {
|
||||
for c, andRule := range rule.And {
|
||||
subName := fmt.Sprintf("%s_and_%d", waapRuleName, c)
|
||||
lastRule := c == len(rule.And)-1
|
||||
rules, err := m.buildRules(&andRule, subName, !lastRule)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, rules...)
|
||||
}
|
||||
}
|
||||
|
||||
if rule.Or != nil {
|
||||
for c, orRule := range rule.Or {
|
||||
subName := fmt.Sprintf("%s_or_%d", waapRuleName, c)
|
||||
rules, err := m.buildRules(&orRule, subName, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, rules...)
|
||||
}
|
||||
}
|
||||
|
||||
r := strings.Builder{}
|
||||
|
||||
r.WriteString("SecRule ")
|
||||
|
||||
if rule.Zones == nil {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
for idx, zone := range rule.Zones {
|
||||
mappedZone, ok := zonesMap[zone]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown zone '%s'", zone)
|
||||
}
|
||||
if len(rule.Variables) == 0 {
|
||||
r.WriteString(mappedZone)
|
||||
} else {
|
||||
for j, variable := range rule.Variables {
|
||||
if idx > 0 || j > 0 {
|
||||
r.WriteByte('|')
|
||||
}
|
||||
r.WriteString(fmt.Sprintf("%s:%s", mappedZone, variable))
|
||||
}
|
||||
}
|
||||
}
|
||||
r.WriteByte(' ')
|
||||
|
||||
if rule.Match.Type != "" {
|
||||
if match, ok := matchMap[rule.Match.Type]; ok {
|
||||
r.WriteString(fmt.Sprintf(`"%s %s"`, match, rule.Match.Value))
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown match type '%s'", rule.Match.Type)
|
||||
}
|
||||
}
|
||||
|
||||
//Should phase:2 be configurable?
|
||||
r.WriteString(fmt.Sprintf(` "id:%d,phase:2,deny,log,msg:'%s'`, m.generateRuleID(rule, waapRuleName), waapRuleName))
|
||||
|
||||
if rule.Transform != nil {
|
||||
for _, transform := range rule.Transform {
|
||||
r.WriteByte(',')
|
||||
if mappedTransform, ok := transformMap[transform]; ok {
|
||||
r.WriteString(mappedTransform)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown transform '%s'", transform)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if and {
|
||||
r.WriteString(",chain")
|
||||
}
|
||||
|
||||
r.WriteByte('"')
|
||||
|
||||
ret = append(ret, r.String())
|
||||
return ret, nil
|
||||
}
|
5
pkg/waf/waap_rule/types.go
Normal file
5
pkg/waf/waap_rule/types.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package waap_rule
|
||||
|
||||
const (
|
||||
ModsecurityRuleType = "modsecurity"
|
||||
)
|
65
pkg/waf/waap_rule/waap_rule.go
Normal file
65
pkg/waf/waap_rule/waap_rule.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package waap_rule
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
/*
|
||||
rules:
|
||||
- name: "test"
|
||||
and:
|
||||
- zones:
|
||||
- BODY_ARGS
|
||||
variables:
|
||||
- foo
|
||||
- bar
|
||||
transform:
|
||||
- lowercase|uppercase|b64decode|...
|
||||
match:
|
||||
type: regex
|
||||
value: "[^a-zA-Z]"
|
||||
- zones:
|
||||
- ARGS
|
||||
variables:
|
||||
- bla
|
||||
|
||||
*/
|
||||
|
||||
type match struct {
|
||||
Type string `yaml:"type"`
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
|
||||
type CustomRule struct {
|
||||
Name string `yaml:"name"`
|
||||
|
||||
Zones []string `yaml:"zones"`
|
||||
Variables []string `yaml:"variables"`
|
||||
|
||||
Match match `yaml:"match"`
|
||||
Transform []string `yaml:"transform"` //t:lowercase, t:uppercase, etc
|
||||
And []CustomRule `yaml:"and,omitempty"`
|
||||
Or []CustomRule `yaml:"or,omitempty"`
|
||||
}
|
||||
|
||||
func (v *CustomRule) Convert(ruleType string, waapRuleName string) (string, error) {
|
||||
|
||||
if v.Zones == nil && v.And == nil && v.Or == nil {
|
||||
return "", fmt.Errorf("no zones defined")
|
||||
}
|
||||
|
||||
if v.Match.Type == "" && v.And == nil && v.Or == nil {
|
||||
return "", fmt.Errorf("no match type defined")
|
||||
}
|
||||
|
||||
if v.Match.Value == "" && v.And == nil && v.Or == nil {
|
||||
return "", fmt.Errorf("no match value defined")
|
||||
}
|
||||
|
||||
switch ruleType {
|
||||
case ModsecurityRuleType:
|
||||
return ModsecurityRule{}.Build(v, waapRuleName)
|
||||
default:
|
||||
return "", fmt.Errorf("unknown rule format '%s'", ruleType)
|
||||
}
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
package waf
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestVPatchRuleString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rule VPatchRule
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Base Rule",
|
||||
rule: VPatchRule{
|
||||
Target: "ARGS",
|
||||
Variable: "foo",
|
||||
Match: "[^a-zA-Z]",
|
||||
Transform: "lowercase",
|
||||
},
|
||||
expected: `SecRule ARGS:foo "@rx [^a-zA-Z]" "id:0,deny,log,t:lowercase"`,
|
||||
},
|
||||
{
|
||||
name: "AND Logic Rule",
|
||||
rule: VPatchRule{
|
||||
Target: "ARGS",
|
||||
Variable: "bar",
|
||||
Match: "[0-9]",
|
||||
Logic: "AND",
|
||||
SubRules: []VPatchRule{
|
||||
{
|
||||
Target: "REQUEST_URI",
|
||||
Match: "/joomla/index.php/component/users/",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `SecRule ARGS:bar "@rx [0-9]" "id:0,deny,log,chain"
|
||||
SecRule REQUEST_URI "@rx /joomla/index.php/component/users/" "id:0,deny,log"`,
|
||||
},
|
||||
{
|
||||
name: "AND Logic Rule",
|
||||
rule: VPatchRule{
|
||||
Logic: "AND",
|
||||
SubRules: []VPatchRule{
|
||||
{
|
||||
Target: "REQUEST_URI",
|
||||
Match: "/joomla/index.php/component/users/",
|
||||
},
|
||||
{
|
||||
Target: "ARGS",
|
||||
Variable: "bar",
|
||||
Match: "[0-9]",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `SecRule ARGS:bar "@rx [0-9]" "id:0,deny,log,chain"
|
||||
SecRule REQUEST_URI "@rx /joomla/index.php/component/users/" "id:0,deny,log"`,
|
||||
},
|
||||
{
|
||||
name: "OR Logic Rule",
|
||||
rule: VPatchRule{
|
||||
Target: "REQUEST_HEADERS",
|
||||
Variable: "User-Agent",
|
||||
Match: "BadBot",
|
||||
Logic: "OR",
|
||||
SubRules: []VPatchRule{
|
||||
{
|
||||
Target: "REQUEST_HEADERS",
|
||||
Variable: "Referer",
|
||||
Match: "EvilReferer",
|
||||
},
|
||||
{
|
||||
Target: "REQUEST_METHOD",
|
||||
Equals: "POST",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `SecRule REQUEST_HEADERS:User-Agent "@rx BadBot" "id:0,deny,log,skip:2"
|
||||
SecRule REQUEST_HEADERS:Referer "@rx EvilReferer" "id:0,deny,log,skip:1"
|
||||
SecRule REQUEST_METHOD "@eq POST" "id:0,deny,log"`,
|
||||
},
|
||||
{
|
||||
name: "AND-OR Logic Mix",
|
||||
rule: VPatchRule{
|
||||
Target: "REQUEST_URI",
|
||||
Match: "/api/",
|
||||
Logic: "AND",
|
||||
SubRules: []VPatchRule{
|
||||
{
|
||||
Target: "ARGS",
|
||||
Variable: "username",
|
||||
Match: "admin",
|
||||
Logic: "OR",
|
||||
SubRules: []VPatchRule{
|
||||
{
|
||||
Target: "REQUEST_METHOD",
|
||||
Equals: "POST",
|
||||
Logic: "AND",
|
||||
SubRules: []VPatchRule{
|
||||
{
|
||||
Target: "ARGS",
|
||||
Variable: "action",
|
||||
Match: "delete",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `SecRule REQUEST_URI "@rx /api/" "id:0,deny,log,chain"
|
||||
SecRule ARGS:username "@rx admin" "id:0,deny,log,skip:2"
|
||||
SecRule REQUEST_METHOD "@eq POST" "id:0,deny,log,chain"
|
||||
SecRule ARGS:action "@rx delete" "id:0,deny,log"`,
|
||||
},
|
||||
// Additional OR test case would be here, but note that the OR logic representation with `skip` is very simplistic.
|
||||
// It may not be robust enough for complex OR rules in a real-world ModSecurity setup.
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
actual := tt.rule.String()
|
||||
if actual != tt.expected {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", tt.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
corazatypes "github.com/crowdsecurity/coraza/v3/types"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/waf/waap_rule"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -23,13 +24,13 @@ var WAAP_RULE = "waap-rule"
|
|||
|
||||
// to be filled w/ seb update
|
||||
type WaapCollectionConfig struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
SecLangFilesRules []string `yaml:"seclang_files_rules"`
|
||||
SecLangRules []string `yaml:"seclang_rules"`
|
||||
Rules []VPatchRule `yaml:"rules"`
|
||||
Data interface{} `yaml:"data"` //Ignore it
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
SecLangFilesRules []string `yaml:"seclang_files_rules"`
|
||||
SecLangRules []string `yaml:"seclang_rules"`
|
||||
Rules []waap_rule.CustomRule `yaml:"rules"`
|
||||
Data interface{} `yaml:"data"` //Ignore it
|
||||
}
|
||||
|
||||
func LoadCollection(collection string) (WaapCollection, error) {
|
||||
|
@ -115,8 +116,13 @@ func LoadCollection(collection string) (WaapCollection, error) {
|
|||
|
||||
if loadedRule.Rules != nil {
|
||||
for _, rule := range loadedRule.Rules {
|
||||
log.Infof("Adding rule %s", rule.String())
|
||||
waapCol.Rules = append(waapCol.Rules, rule.String())
|
||||
strRule, err := rule.Convert(waap_rule.ModsecurityRuleType, loadedRule.Name)
|
||||
if err != nil {
|
||||
log.Errorf("unable to convert rule %s : %s", rule.Name, err)
|
||||
return WaapCollection{}, err
|
||||
}
|
||||
log.Infof("Adding rule %s", strRule)
|
||||
waapCol.Rules = append(waapCol.Rules, strRule)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue