Test Failed
Push — master ( ebe956...e43f38 )
by -
01:37
created

NaxsiRules   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Test Coverage

Coverage 89.78%
Metric Value
dl 0
loc 202
ccs 123
cts 137
cp 0.8978
rs 5.1724
wmc 57

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __str__() 0 4 2
A fullstr() 0 4 1
B __validate_detection() 0 8 5
F __validate_matchzone() 0 28 13
A splitter() 0 6 1
A __validate_genericstr() 0 4 3
A __validate_dummy() 0 2 1
F parse_rule() 0 49 14
A validate() 0 9 3
C explaination() 0 26 7
A __fail() 0 3 1
A __init__() 0 13 2
A __validate_id() 0 11 4

How to fix   Complexity   

Complex Class

Complex classes like NaxsiRules often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
    mr_kw = ["MainRule", "BasicRule", "main_rule", "basic_rule"]
24 1
    static_mz = {"$ARGS_VAR", "$BODY_VAR", "$URL", "$HEADERS_VAR"}
25 1
    full_zones = {"ARGS", "BODY", "URL", "HEADERS", "FILE_EXT", "RAW_BODY"}
26 1
    rx_mz = {"$ARGS_VAR_X", "$BODY_VAR_X", "$URL_X", "$HEADERS_VAR_X"}
27 1
    sub_mz = list(static_mz) + list(full_zones) + list(rx_mz)
28 1
29
    def __init__(self, msg="", detection="", mz="", score="", sid=42000, ruleset="", rmks="", active=0, negative=0, timestamp=0):
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (129/120).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
30 1
        self.msg = msg
31 1
        self.detection = detection
32 1
        self.mz = mz
33 1
        self.score = score
34 1
        self.sid = sid
35 1
        self.ruleset = ruleset
36 1
        self.rmks = rmks
37 1
        self.active = active
38 1
        self.negative = 1 if negative == 'checked' else 0
39 1
        self.timestamp = timestamp
40 1
        self.warnings = []
41 1
        self.error = []
42 1
43
    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 1
48
    def __str__(self):
49 1
        negate = 'negative' if self.negative == 1 else ''
50 1
        return 'MainRule {} "{}" "msg:{}" "mz:{}" "s:{}" id:{} ;'.format(
51 1
            negate, self.detection, self.msg, self.mz, self.score, self.sid)
52
53
    def explaination(self):
54 1
        """ Return a string explainign a rule """
55 1
        assoc = {'ARGS': 'request arguments', 'BODY': 'body', 'URL': 'url'}
56 1
        expl = 'The rule number <strong>%d</strong> is ' % self.sid
57 1
        if self.negative:
58
            expl += '<strong>not</strong> '
59 1
        expl += 'setting the '
60
        scores = []
61 1
        for score in self.score.split(','):
62
            scores.append('<strong>{0}</strong> to <strong>{1}</strong> '.format(*score.split(':')))
63
        expl += ', '.join(scores) + 'when it '
64 1
        if self.detection.startswith('str:'):
65 1
            expl += 'finds the string <strong>{}</strong> '.format(self.detection[4:])
66 1
        else:
67
            expl += 'matches the regexp <strong>{}</strong> '.format(self.detection[3:])
68
        expl += 'in '
69 1
        zones = []
70 1
        for mz in self.mz.split('|'):
71
            if mz.startswith('$'):
72 1
                if mz.lower().startswith('headers_var:cookie'):
73 1
                    zones.append('the cookies')
74
                else:
75 1
                    zones.append('the {0}, in the [1} field'.format(*mz.split(':')))
76 1
            else:
77 1
                zones.append('the <strong>{0}</strong>'.format(assoc[mz]))
78
        return expl + ', '.join(zones) + '.'
79 1
80
    def validate(self):
81 1
        self.__validate_matchzone(self.mz)
82 1
        self.__validate_id(self.sid)
83
        self.__validate_detection(self.detection)
84 1
85
        if not self.msg:
86 1
            self.warnings.append("Rule has no 'msg:'.")
87 1
        if not self.score:
88 1
            self.error.append("Rule has no score.")
89 1
90 1
    def __fail(self, msg):
91 1
        self.error.append(msg)
92 1
        return False
93 1
94
    # Bellow are parsers for specific parts of a rule
95 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...
96
        return True
97 1
98
    def __validate_detection(self, p_str, assign=False):
99 1
        if not p_str.startswith("str:") and not p_str.startswith("rx:"):
100
            self.__fail("detection {} is neither rx: or str:".format(p_str))
101 1
        if not p_str.islower():
102 1
            self.warnings.append("detection {} is not lower-case. naxsi is case-insensitive".format(p_str))
103
        if assign is True:
104 1
            self.detection = p_str
105 1
        return True
106
107 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...
108 1
        if p_str and not p_str.islower():
109 1
            self.warnings.append("Pattern ({0}) is not lower-case.".format(p_str))
110
        return True
111 1
112 1
    def __validate_matchzone(self, p_str, assign=False):
113 1
        has_zone = False
114
        mz_state = set()
115 1
        locs = p_str.split('|')
116 1
        for loc in locs:
117 1
            keyword, arg = loc, None
118 1
            if loc.startswith("$"):
119 1
                if loc.find(":") == -1:
120
                    return self.__fail("Missing 2nd part after ':' in {0}".format(loc))
121
                keyword, arg = loc.split(":")
122
            # check it is a valid keyword
123 1
            if keyword not in self.sub_mz:
124 1
                return self.__fail("'{0}' no a known sub-part of mz : {1}".format(keyword, self.sub_mz))
125 1
            mz_state.add(keyword)
126
            # verify the rule doesn't attempt to target REGEX and STATIC _VAR/URL at the same time
127 1
            if len(self.rx_mz & mz_state) and len(self.static_mz & mz_state):
128
                return self.__fail("You can't mix static $* with regex $*_X ({})".format(str(mz_state)))
129 1
            # just a gentle reminder
130 1
            if arg and arg.islower() is False:
131 1
                self.warnings.append("{0} in {1} is not lowercase. naxsi is case-insensitive".format(arg, loc))
132 1
            # the rule targets an actual zone
133
            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...
134 1
                has_zone = True
135
        if has_zone is False:
136
            return self.__fail("The rule/whitelist doesn't target any zone.")
137
        if assign is True:
138
            self.mz = p_str
139
        return True
140 1
141 1
    def __validate_id(self, p_str, assign=False):
142
        try:
143 1
            num = int(p_str)
144
            if num < 10000:
145
                self.warnings.append("rule IDs below 10k are reserved ({0})".format(num))
146 1
        except ValueError:
147 1
            self.error.append("id:{0} is not numeric".format(p_str))
148 1
            return False
149
        if assign is True:
150 1
            self.sid = num
151
        return True
152 1
153
    @staticmethod
154
    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...
155 1
        lexer = shlex(s)
156
        lexer.whitespace_split = True
157 1
        items = list(iter(lexer.get_token, ''))
158 1
        return items
159
160 1
    def parse_rule(self, full_str):
161 1
        """
162 1
        Parse and validate a full naxsi rule
163 1
        :param full_str: raw rule
164
        :return: [True|False, dict]
165 1
        """
166 1
        self.warnings = []
167 1
        self.error = []
168 1
169 1
        func_map = {"id:": self.__validate_id, "str:": self.__validate_genericstr,
170 1
                    "rx:": self.__validate_genericstr, "msg:": self.__validate_dummy, "mz:": self.__validate_matchzone,
171 1
                    "negative": self.__validate_dummy, "s:": self.__validate_dummy}
172
        ret = False
173 1
        split = self.splitter(full_str)  # parse string
174 1
        intersection = set(split).intersection(set(self.mr_kw))
175 1
176
        if not intersection:
177 1
            return self.__fail("No mainrule/basicrule keyword.")
178 1
        elif len(intersection) > 1:
179
            return self.__fail("Multiple mainrule/basicrule keywords.")
180 1
181
        split.remove(intersection.pop())  # remove the mainrule/basicrule keyword
182 1
183
        if ";" in split:
184
            split.remove(";")
185
186
        while split:  # iterate while there is data, as handlers can defer
187
            for keyword in split:
188
                orig_kw = keyword
189
                keyword = keyword.strip()
190
191
                if keyword.endswith(";"):  # remove semi-colons
192
                    keyword = keyword[:-1]
193
                if keyword.startswith(('"', "'")) and (keyword[0] == keyword[-1]):  # remove (double-)quotes
194
                    keyword = keyword[1:-1]
195
                for frag_kw in func_map:
196
                    ret = False
197
                    if keyword.startswith(frag_kw):
198
                        # parser funcs returns True/False
199
                        ret = func_map[frag_kw](keyword[len(frag_kw):], assign=True)
200
                        if ret is True:
201
                            split.remove(orig_kw)
202
                        else:
203
                            return self.__fail("parsing of element '{0}' failed.".format(keyword))
204
                        break
205
                # we have an item that wasn't successfully parsed
206
                if orig_kw in split and ret is not None:
207
                    return False
208
        return True
209