Passed
Branch master (cc1c3c)
by jvo
01:32
created

NaxsiRules   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 177
Duplicated Lines 0 %

Test Coverage

Coverage 58.09%
Metric Value
dl 0
loc 177
ccs 79
cts 136
cp 0.5809
rs 6
wmc 55

12 Methods

Rating   Name   Duplication   Size   Complexity  
A p_dummy() 0 2 1
A fullstr() 0 4 1
B p_detection() 0 8 5
A validate() 0 9 3
A p_genericstr() 0 4 3
A __init__() 0 11 2
A __str__() 0 4 2
A fail() 0 3 1
F p_mz() 0 28 13
B splitter() 0 8 5
F parse_rule() 0 49 15
A p_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 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.p_mz(self.mz)
55 1
        self.p_id(self.sid)
56 1
        self.p_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 p_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 s seems to be unused.
Loading history...
Unused Code introduced by
The argument assign 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 p_detection(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...
72 1
        if not s.startswith("str:") and not s.startswith("rx:"):
73
            self.fail("detection {} is neither rx: or str:".format(s))
74 1
        if not s.islower():
75 1
            self.warnings.append("detection {} is not lower-case. naxsi is case-insensitive".format(s))
76 1
        if assign is True:
77
            self.detection = s
78 1
        return True
79
80 1
    def p_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 p_mz(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 p_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(s))
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 's'
Loading history...
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.quotes = '"\''
130
        lexer.whitespace_split = True
131
        items = list(iter(lexer.get_token, ''))
132
        return ([i for i in items if i[0] in "\"'"] +
133
                [i for i in items if i[0] not in "\"'"])
134
135 1
    def parse_rule(self, full_str):
136
        """
137
        Parse and validate a full naxsi rule
138
        :param full_str: raw rule
139
        :return: [True|False, dict]
140
        """
141
        func_map = {"id:": self.p_id, "str:": self.p_genericstr,
142
                    "rx:": self.p_genericstr, "msg:": self.p_dummy, "mz:": self.p_mz,
143
                    "negative": self.p_dummy, "s:": self.p_dummy}
144
145
        split = self.splitter(full_str)  # parse string
146
        sect = set(self.mr_kw) & set(split)
147
148
        if len(sect) != 1:
149
            return self.fail("no (or multiple) mainrule/basicrule keyword.")
150
151
        split.remove(sect.pop())
152
153
        if ";" in split:
154
            split.remove(";")
155
156
        while True:  # iterate while there is data, as handlers can defer
157
158
            if not split:  # we are done
159
                break
160
161
            for keyword in split:
162
                orig_kw = keyword
163
                keyword = keyword.strip()
164
165
                # clean-up quotes or semicolon
166
                if keyword.endswith(";"):
167
                    keyword = keyword[:-1]
168
                if keyword.startswith(('"', "'")) and (keyword[0] == keyword[-1]):
169
                    keyword = keyword[1:-1]
170
                for frag_kw in func_map:
171
                    ret = False
172
                    if keyword.startswith(frag_kw):
173
                        # parser funcs returns True/False
174
                        ret = func_map[frag_kw](keyword[len(frag_kw):])
175
                        if ret is False:
176
                            return self.fail("parsing of element '{0}' failed.".format(keyword))
177
                        if ret is True:
178
                            split.remove(orig_kw)
179
                        break
180
                # we have an item that wasn't successfully parsed
181
                if orig_kw in split and ret is not None:
182
                    return False
183
        return True
184