63bd31b471
* 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
212 lines
5.2 KiB
Go
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
|
|
}
|