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  }