Merge branch 'coraza_poc_acquis' of github.com:crowdsecurity/crowdsec into coraza_poc_acquis

This commit is contained in:
bui 2023-10-26 12:04:12 +02:00
commit e49f33b4a7
8 changed files with 307 additions and 235 deletions

View file

@ -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)
}

View file

@ -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
}

View 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)
}
})
}
}

View 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
}

View file

@ -0,0 +1,5 @@
package waap_rule
const (
ModsecurityRuleType = "modsecurity"
)

View 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)
}
}

View file

@ -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)
}
})
}
}

View file

@ -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)
}
}