github.com/crowdsecurity/crowdsec@v1.6.1/pkg/appsec/appsec_rule/modsecurity.go (about) 1 package appsec_rule 2 3 import ( 4 "fmt" 5 "hash/fnv" 6 "strings" 7 ) 8 9 type ModsecurityRule struct { 10 ids []uint32 11 } 12 13 var zonesMap map[string]string = map[string]string{ 14 "ARGS": "ARGS_GET", 15 "ARGS_NAMES": "ARGS_GET_NAMES", 16 "BODY_ARGS": "ARGS_POST", 17 "BODY_ARGS_NAMES": "ARGS_POST_NAMES", 18 "HEADERS_NAMES": "REQUEST_HEADERS_NAMES", 19 "HEADERS": "REQUEST_HEADERS", 20 "METHOD": "REQUEST_METHOD", 21 "PROTOCOL": "REQUEST_PROTOCOL", 22 "URI": "REQUEST_FILENAME", 23 "URI_FULL": "REQUEST_URI", 24 "RAW_BODY": "REQUEST_BODY", 25 "FILENAMES": "FILES", 26 } 27 28 var transformMap map[string]string = map[string]string{ 29 "lowercase": "t:lowercase", 30 "uppercase": "t:uppercase", 31 "b64decode": "t:base64Decode", 32 //"hexdecode": "t:hexDecode", -> not supported by coraza 33 "length": "t:length", 34 "urldecode": "t:urlDecode", 35 "trim": "t:trim", 36 "normalize_path": "t:normalizePath", 37 "normalizepath": "t:normalizePath", 38 "htmlentitydecode": "t:htmlEntityDecode", 39 "html_entity_decode": "t:htmlEntityDecode", 40 } 41 42 var matchMap map[string]string = map[string]string{ 43 "regex": "@rx", 44 "equals": "@streq", 45 "startsWith": "@beginsWith", 46 "endsWith": "@endsWith", 47 "contains": "@contains", 48 "libinjectionSQL": "@detectSQLi", 49 "libinjectionXSS": "@detectXSS", 50 "gt": "@gt", 51 "lt": "@lt", 52 "gte": "@ge", 53 "lte": "@le", 54 "eq": "@eq", 55 } 56 57 var bodyTypeMatch map[string]string = map[string]string{ 58 "json": "JSON", 59 "xml": "XML", 60 "multipart": "MULTIPART", 61 "urlencoded": "URLENCODED", 62 } 63 64 func (m *ModsecurityRule) Build(rule *CustomRule, appsecRuleName string) (string, []uint32, error) { 65 66 rules, err := m.buildRules(rule, appsecRuleName, false, 0, 0) 67 68 if err != nil { 69 return "", nil, err 70 } 71 72 //We return the id of the first generated rule, as it's the interesting one in case of chain or skip 73 return strings.Join(rules, "\n"), m.ids, nil 74 } 75 76 func (m *ModsecurityRule) generateRuleID(rule *CustomRule, appsecRuleName string, depth int) uint32 { 77 h := fnv.New32a() 78 h.Write([]byte(appsecRuleName)) 79 h.Write([]byte(rule.Match.Type)) 80 h.Write([]byte(rule.Match.Value)) 81 h.Write([]byte(fmt.Sprintf("%d", depth))) 82 for _, zone := range rule.Zones { 83 h.Write([]byte(zone)) 84 } 85 for _, transform := range rule.Transform { 86 h.Write([]byte(transform)) 87 } 88 id := h.Sum32() 89 m.ids = append(m.ids, id) 90 return id 91 } 92 93 func (m *ModsecurityRule) buildRules(rule *CustomRule, appsecRuleName string, and bool, toSkip int, depth int) ([]string, error) { 94 ret := make([]string, 0) 95 96 if len(rule.And) != 0 && len(rule.Or) != 0 { 97 return nil, fmt.Errorf("cannot have both 'and' and 'or' in the same rule") 98 } 99 100 if rule.And != nil { 101 for c, andRule := range rule.And { 102 depth++ 103 lastRule := c == len(rule.And)-1 // || len(rule.Or) == 0 104 rules, err := m.buildRules(&andRule, appsecRuleName, !lastRule, 0, depth) 105 if err != nil { 106 return nil, err 107 } 108 ret = append(ret, rules...) 109 } 110 } 111 112 if rule.Or != nil { 113 for c, orRule := range rule.Or { 114 depth++ 115 skip := len(rule.Or) - c - 1 116 rules, err := m.buildRules(&orRule, appsecRuleName, false, skip, depth) 117 if err != nil { 118 return nil, err 119 } 120 ret = append(ret, rules...) 121 } 122 } 123 124 r := strings.Builder{} 125 126 r.WriteString("SecRule ") 127 128 if rule.Zones == nil { 129 return ret, nil 130 } 131 132 zone_prefix := "" 133 variable_prefix := "" 134 if rule.Transform != nil { 135 for tidx, transform := range rule.Transform { 136 if transform == "count" { 137 zone_prefix = "&" 138 rule.Transform[tidx] = "" 139 } 140 } 141 } 142 for idx, zone := range rule.Zones { 143 if idx > 0 { 144 r.WriteByte('|') 145 } 146 mappedZone, ok := zonesMap[zone] 147 if !ok { 148 return nil, fmt.Errorf("unknown zone '%s'", zone) 149 } 150 if len(rule.Variables) == 0 { 151 r.WriteString(mappedZone) 152 } else { 153 for j, variable := range rule.Variables { 154 if j > 0 { 155 r.WriteByte('|') 156 } 157 r.WriteString(fmt.Sprintf("%s%s:%s%s", zone_prefix, mappedZone, variable_prefix, variable)) 158 } 159 } 160 } 161 r.WriteByte(' ') 162 163 if rule.Match.Type != "" { 164 if match, ok := matchMap[rule.Match.Type]; ok { 165 prefix := "" 166 if rule.Match.Not { 167 prefix = "!" 168 } 169 r.WriteString(fmt.Sprintf(`"%s%s %s"`, prefix, match, rule.Match.Value)) 170 } else { 171 return nil, fmt.Errorf("unknown match type '%s'", rule.Match.Type) 172 } 173 } 174 175 //Should phase:2 be configurable? 176 r.WriteString(fmt.Sprintf(` "id:%d,phase:2,deny,log,msg:'%s',tag:'crowdsec-%s'`, m.generateRuleID(rule, appsecRuleName, depth), appsecRuleName, appsecRuleName)) 177 178 if rule.Transform != nil { 179 for _, transform := range rule.Transform { 180 if transform == "" { 181 continue 182 } 183 r.WriteByte(',') 184 if mappedTransform, ok := transformMap[transform]; ok { 185 r.WriteString(mappedTransform) 186 } else { 187 return nil, fmt.Errorf("unknown transform '%s'", transform) 188 } 189 } 190 } 191 192 if rule.BodyType != "" { 193 if mappedBodyType, ok := bodyTypeMatch[rule.BodyType]; ok { 194 r.WriteString(fmt.Sprintf(",ctl:requestBodyProcessor=%s", mappedBodyType)) 195 } else { 196 return nil, fmt.Errorf("unknown body type '%s'", rule.BodyType) 197 } 198 } 199 200 if and { 201 r.WriteString(",chain") 202 } 203 204 if toSkip > 0 { 205 r.WriteString(fmt.Sprintf(",skip:%d", toSkip)) 206 } 207 208 r.WriteByte('"') 209 210 ret = append(ret, r.String()) 211 return ret, nil 212 }