Test Failed
Push — master ( f3e880...f876a3 )
by -
01:27
created

NaxsiRules.__validate_detection()   B

Complexity

Conditions 5

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.3906
Metric Value
cc 5
dl 0
loc 8
ccs 6
cts 8
cp 0.75
crap 5.3906
rs 8.5454
1 1
from time import strftime, localtime
2
3 1
from spike.model import db
4 1
from shlex import shlex
5
6
7 1
class NaxsiRules(db.Model):
8 1
    __bind_key__ = 'rules'
9 1
    __tablename__ = 'naxsi_rules'
10
11 1
    id = db.Column(db.Integer, primary_key=True)
12 1
    msg = db.Column(db.String(), nullable=False)
13 1
    detection = db.Column(db.String(1024), nullable=False)
14 1
    mz = db.Column(db.String(1024), nullable=False)
15 1
    score = db.Column(db.String(1024), nullable=False)
16 1
    sid = db.Column(db.Integer, nullable=False, unique=True)
17 1
    ruleset = db.Column(db.String(1024), nullable=False)
18 1
    rmks = db.Column(db.Text, nullable=True, server_default="")
19 1
    active = db.Column(db.Integer, nullable=False, server_default="1")
20 1
    negative = db.Column(db.Integer, nullable=False, server_default='0')
21 1
    timestamp = db.Column(db.Integer, nullable=False)
22
23 1
    warnings = []
24 1
    error = []
25 1
    mr_kw = ["MainRule", "BasicRule", "main_rule", "basic_rule"]
26 1
    static_mz = {"$ARGS_VAR", "$BODY_VAR", "$URL", "$HEADERS_VAR"}
27 1
    full_zones = {"ARGS", "BODY", "URL", "HEADERS", "FILE_EXT", "RAW_BODY"}
28 1
    rx_mz = {"$ARGS_VAR_X", "$BODY_VAR_X", "$URL_X", "$HEADERS_VAR_X"}
29 1
    sub_mz = list(static_mz) + list(full_zones) + list(rx_mz)
30
31 1
    def __init__(self, msg, detection, mz, score, sid, ruleset, rmks, active, negative, timestamp):
32 1
        self.msg = msg
33 1
        self.detection = detection
34 1
        self.mz = mz
35 1
        self.score = score
36 1
        self.sid = sid
37 1
        self.ruleset = ruleset
38 1
        self.rmks = rmks
39 1
        self.active = active
40 1
        self.negative = 1 if negative == 'checked' else 0
41 1
        self.timestamp = timestamp
42
43 1
    def fullstr(self):
44 1
        rdate = strftime("%F - %H:%M", localtime(float(str(self.timestamp))))
45 1
        rmks = "# ".join(self.rmks.strip().split("\n"))
46 1
        return "#\n# sid: {0} | date: {1}\n#\n# {2}\n#\n{3}".format(self.sid, rdate, rmks, self.__str__())
47
48 1
    def __str__(self):
49 1
        negate = 'negative' if self.negative == 1 else ''
50 1
        return 'MainRule {} "{}" "msg:{}" "mz:{}" "s:{}" id:{} ;'.format(
51
            negate, self.detection, self.msg, self.mz, self.score, self.sid)
52
53 1
    def validate(self):
54 1
        self.__validate_matchzone(self.mz)
55 1
        self.__validate_id(self.sid)
56 1
        self.__validate_detection(self.detection)
57
58 1
        if not self.msg:
59
            self.warnings.append("Rule has no 'msg:'.")
60 1
        if not self.score:
61
            self.error.append("Rule has no score.")
62
63 1
    def __fail(self, msg):
64
        self.error.append(msg)
65
        return False
66
67
    # Bellow are parsers for specific parts of a rule
68 1
    def __validate_dummy(self, s, assign=False):
0 ignored issues
show
Coding Style Naming introduced by
The name s does not conform to the argument naming conventions ([a-z_][a-z0-9_]{1,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
Unused Code introduced by
The argument assign seems to be unused.
Loading history...
Unused Code introduced by
The argument s seems to be unused.
Loading history...
Coding Style introduced by
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
69
        return True
70
71 1
    def __validate_detection(self, p_str, assign=False):
72 1
        if not p_str.startswith("str:") and not p_str.startswith("rx:"):
73
            self.__fail("detection {} is neither rx: or str:".format(p_str))
74 1
        if not p_str.islower():
75 1
            self.warnings.append("detection {} is not lower-case. naxsi is case-insensitive".format(p_str))
76 1
        if assign is True:
77
            self.detection = p_str
78 1
        return True
79
80 1
    def __validate_genericstr(self, p_str, assign=False):
0 ignored issues
show
Unused Code introduced by
The argument assign seems to be unused.
Loading history...
81
        if p_str and not p_str.islower():
82
            self.warnings.append("Pattern ({0}) is not lower-case.".format(p_str))
83
        return True
84
85 1
    def __validate_matchzone(self, p_str, assign=False):
86 1
        has_zone = False
87 1
        mz_state = set()
88 1
        locs = p_str.split('|')
89 1
        for loc in locs:
90 1
            keyword, arg = loc, None
91 1
            if loc.startswith("$"):
92
                if loc.find(":") == -1:
93
                    return self.__fail("Missing 2nd part after ':' in {0}".format(loc))
94
                keyword, arg = loc.split(":")
95
            # check it is a valid keyword
96 1
            if keyword not in self.sub_mz:
97
                return self.__fail("'{0}' no a known sub-part of mz : {1}".format(keyword, self.sub_mz))
98 1
            mz_state.add(keyword)
99
            # verify the rule doesn't attempt to target REGEX and STATIC _VAR/URL at the same time
100 1
            if len(self.rx_mz & mz_state) and len(self.static_mz & mz_state):
101
                return self.__fail("You can't mix static $* with regex $*_X ({})".format(str(mz_state)))
102
            # just a gentle reminder
103 1
            if arg and arg.islower() is False:
104
                self.warnings.append("{0} in {1} is not lowercase. naxsi is case-insensitive".format(arg, loc))
105
            # the rule targets an actual zone
106 1
            if keyword not in ["$URL", "$URL_X"] and keyword in (self.rx_mz | self.full_zones | self.static_mz):
0 ignored issues
show
Unused Code Coding Style introduced by
There is an unnecessary parenthesis after in.
Loading history...
107 1
                has_zone = True
108 1
        if has_zone is False:
109
            return self.__fail("The rule/whitelist doesn't target any zone.")
110 1
        if assign is True:
111
            self.mz = p_str
112 1
        return True
113
114 1
    def __validate_id(self, p_str, assign=False):
115 1
        try:
116 1
            num = int(p_str)
117 1
            if num < 10000:
118 1
                self.warnings.append("rule IDs below 10k are reserved ({0})".format(num))
119
        except ValueError:
120
            self.error.append("id:{0} is not numeric".format(p_str))
121
            return False
122 1
        if assign is True:
123
            self.sid = num
124 1
        return True
125
126 1
    @staticmethod
127
    def splitter(s):
0 ignored issues
show
Coding Style Naming introduced by
The name s does not conform to the argument naming conventions ([a-z_][a-z0-9_]{1,30}$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
128
        lexer = shlex(s)
129
        lexer.whitespace_split = True
130
        items = list(iter(lexer.get_token, ''))
131
        return items
132
133
    def parse_rule(self, full_str):
134
        """
135 1
        Parse and validate a full naxsi rule
136
        :param full_str: raw rule
137
        :return: [True|False, dict]
138
        """
139
        func_map = {"id:": self.__validate_id, "str:": self.__validate_genericstr,
140
                    "rx:": self.__validate_genericstr, "msg:": self.__validate_dummy, "mz:": self.__validate_matchzone,
141
                    "negative": self.__validate_dummy, "s:": self.__validate_dummy}
142
        ret = False
143
        split = self.splitter(full_str)  # parse string
144
        intersection = set(split).intersection(set(self.mr_kw))
145
146
        if len(intersection) != 1:
147
            return self.__fail("no (or multiple) mainrule/basicrule keyword.")
148
149
        split.remove(intersection[0])  # remove the mainrule/basicrule keyword
150
151
        if ";" in split:
152
            split.remove(";")
153
154
        while split:  # iterate while there is data, as handlers can defer
155
            for keyword in split:
156
                orig_kw = keyword
157
                keyword = keyword.strip()
158
159
                if keyword.endswith(";"):  # remove semi-colons
160
                    keyword = keyword[:-1]
161
                if keyword.startswith(('"', "'")) and (keyword[0] == keyword[-1]):  # remove (double-)quotes
162
                    keyword = keyword[1:-1]
163
                for frag_kw in func_map:
164
                    ret = False
165
                    if keyword.startswith(frag_kw):
166
                        # parser funcs returns True/False
167
                        ret = func_map[frag_kw](keyword[len(frag_kw):])
168
                        if ret is True:
169
                            split.remove(orig_kw)
170
                        else:
171
                            return self.__fail("parsing of element '{0}' failed.".format(keyword))
172
                        break
173
                # we have an item that wasn't successfully parsed
174
                if orig_kw in split and ret is not None:
175
                    return False
176
        return True
177