crowdsec/pkg/appsec/appsec_rule/modsecurity.go
Thibault "bui" Koechlin 63bd31b471
Fix REQUEST_URI behavior + fix #2891 (#2917)
* fix our behavior to comply more with modsec, REQUEST_URI should be: path+query string

* fix #2891 as well

* add new transforms

* add transform tests
2024-03-29 17:57:54 +01:00

212 lines
5.2 KiB
Go

package appsec_rule
import (
"fmt"
"hash/fnv"
"strings"
)
type ModsecurityRule struct {
ids []uint32
}
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_NAMES": "REQUEST_HEADERS_NAMES",
"HEADERS": "REQUEST_HEADERS",
"METHOD": "REQUEST_METHOD",
"PROTOCOL": "REQUEST_PROTOCOL",
"URI": "REQUEST_FILENAME",
"URI_FULL": "REQUEST_URI",
"RAW_BODY": "REQUEST_BODY",
"FILENAMES": "FILES",
}
var transformMap map[string]string = map[string]string{
"lowercase": "t:lowercase",
"uppercase": "t:uppercase",
"b64decode": "t:base64Decode",
//"hexdecode": "t:hexDecode", -> not supported by coraza
"length": "t:length",
"urldecode": "t:urlDecode",
"trim": "t:trim",
"normalize_path": "t:normalizePath",
"normalizepath": "t:normalizePath",
"htmlentitydecode": "t:htmlEntityDecode",
"html_entity_decode": "t:htmlEntityDecode",
}
var matchMap map[string]string = map[string]string{
"regex": "@rx",
"equals": "@streq",
"startsWith": "@beginsWith",
"endsWith": "@endsWith",
"contains": "@contains",
"libinjectionSQL": "@detectSQLi",
"libinjectionXSS": "@detectXSS",
"gt": "@gt",
"lt": "@lt",
"gte": "@ge",
"lte": "@le",
"eq": "@eq",
}
var bodyTypeMatch map[string]string = map[string]string{
"json": "JSON",
"xml": "XML",
"multipart": "MULTIPART",
"urlencoded": "URLENCODED",
}
func (m *ModsecurityRule) Build(rule *CustomRule, appsecRuleName string) (string, []uint32, error) {
rules, err := m.buildRules(rule, appsecRuleName, false, 0, 0)
if err != nil {
return "", nil, err
}
//We return the id of the first generated rule, as it's the interesting one in case of chain or skip
return strings.Join(rules, "\n"), m.ids, nil
}
func (m *ModsecurityRule) generateRuleID(rule *CustomRule, appsecRuleName string, depth int) uint32 {
h := fnv.New32a()
h.Write([]byte(appsecRuleName))
h.Write([]byte(rule.Match.Type))
h.Write([]byte(rule.Match.Value))
h.Write([]byte(fmt.Sprintf("%d", depth)))
for _, zone := range rule.Zones {
h.Write([]byte(zone))
}
for _, transform := range rule.Transform {
h.Write([]byte(transform))
}
id := h.Sum32()
m.ids = append(m.ids, id)
return id
}
func (m *ModsecurityRule) buildRules(rule *CustomRule, appsecRuleName string, and bool, toSkip int, depth int) ([]string, error) {
ret := make([]string, 0)
if len(rule.And) != 0 && len(rule.Or) != 0 {
return nil, fmt.Errorf("cannot have both 'and' and 'or' in the same rule")
}
if rule.And != nil {
for c, andRule := range rule.And {
depth++
lastRule := c == len(rule.And)-1 // || len(rule.Or) == 0
rules, err := m.buildRules(&andRule, appsecRuleName, !lastRule, 0, depth)
if err != nil {
return nil, err
}
ret = append(ret, rules...)
}
}
if rule.Or != nil {
for c, orRule := range rule.Or {
depth++
skip := len(rule.Or) - c - 1
rules, err := m.buildRules(&orRule, appsecRuleName, false, skip, depth)
if err != nil {
return nil, err
}
ret = append(ret, rules...)
}
}
r := strings.Builder{}
r.WriteString("SecRule ")
if rule.Zones == nil {
return ret, nil
}
zone_prefix := ""
variable_prefix := ""
if rule.Transform != nil {
for tidx, transform := range rule.Transform {
if transform == "count" {
zone_prefix = "&"
rule.Transform[tidx] = ""
}
}
}
for idx, zone := range rule.Zones {
if idx > 0 {
r.WriteByte('|')
}
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 j > 0 {
r.WriteByte('|')
}
r.WriteString(fmt.Sprintf("%s%s:%s%s", zone_prefix, mappedZone, variable_prefix, variable))
}
}
}
r.WriteByte(' ')
if rule.Match.Type != "" {
if match, ok := matchMap[rule.Match.Type]; ok {
prefix := ""
if rule.Match.Not {
prefix = "!"
}
r.WriteString(fmt.Sprintf(`"%s%s %s"`, prefix, 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',tag:'crowdsec-%s'`, m.generateRuleID(rule, appsecRuleName, depth), appsecRuleName, appsecRuleName))
if rule.Transform != nil {
for _, transform := range rule.Transform {
if transform == "" {
continue
}
r.WriteByte(',')
if mappedTransform, ok := transformMap[transform]; ok {
r.WriteString(mappedTransform)
} else {
return nil, fmt.Errorf("unknown transform '%s'", transform)
}
}
}
if rule.BodyType != "" {
if mappedBodyType, ok := bodyTypeMatch[rule.BodyType]; ok {
r.WriteString(fmt.Sprintf(",ctl:requestBodyProcessor=%s", mappedBodyType))
} else {
return nil, fmt.Errorf("unknown body type '%s'", rule.BodyType)
}
}
if and {
r.WriteString(",chain")
}
if toSkip > 0 {
r.WriteString(fmt.Sprintf(",skip:%d", toSkip))
}
r.WriteByte('"')
ret = append(ret, r.String())
return ret, nil
}