Test Setup Failed
Push — master ( 1594ef...d6f763 )
by
unknown
01:30
created

NaxsiRules.p_id()   A

Complexity

Conditions 4

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20
Metric Value
cc 4
dl 0
loc 11
ccs 0
cts 0
cp 0
crap 20
rs 9.2
1 1
from spike.model import db
2
from shlex import shlex
3
4 1
5 1
class NaxsiRules(db.Model):
6 1
    __bind_key__ = 'rules'
7
    __tablename__ = 'naxsi_rules'
8 1
9 1
    id = db.Column(db.Integer, primary_key=True)
10 1
    msg = db.Column(db.String(), nullable=False)
11 1
    detection = db.Column(db.String(1024), nullable=False)
12 1
    mz = db.Column(db.String(1024), nullable=False)
13 1
    score = db.Column(db.String(1024), nullable=False)
14 1
    sid = db.Column(db.Integer, nullable=False, unique=True)
15 1
    ruleset = db.Column(db.String(1024), nullable=False)
16 1
    rmks = db.Column(db.Text, nullable=True, server_default="")
17 1
    active = db.Column(db.Integer, nullable=False, server_default="1")
18 1
    negative = db.Column(db.Integer, nullable=False, server_default='0')
19
    timestamp = db.Column(db.Integer, nullable=False)
20 1
21 1
    warnings = []
22 1
    error = []
23 1
    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 1
    def __init__(self,  msg, detection, mz, score, sid, ruleset, rmks, active, negative, timestamp):
0 ignored issues
show
Coding Style introduced by
Exactly one space required after comma
def __init__(self, msg, detection, mz, score, sid, ruleset, rmks, active, negative, timestamp):
^
Loading history...
30 1
        self.msg = msg
31
        self.detection = detection
32
        self.mz = mz
33
        self.score = score
34
        self.sid = sid
35
        self.ruleset = ruleset
36
        self.rmks = rmks
37
        self.active = active
38
        self.negative = 1 if negative == 'checked' else 0
39
        self.timestamp = timestamp
40
41
    def fullstr(self):
42
        rdate = strftime("%F - %H:%M", localtime(float(str(self.timestamp))))
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'strftime'
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'localtime'
Loading history...
43
        rmks = "# ".join(self.rmks.strip().split("\n"))
44
        return "#\\n# sid: {0} | date: {1}\\n#\\n# {2}\\n#\\n{3}".format(self.sid, rdate, rmks, self.__str__())
45
46
    def __str__(self):
47
        negate = 'negative' if self.negative == 1 else ''
48
        return 'MainRule {} "{}" "msg:{}" "mz:{}" "s:{}" id:{} ;'.format(
49
            negate, self.detection, self.msg, self.mz, self.score, self.sid)
50
51
    def validate(self):
52
        self.p_mz(self.mz)
53
        self.p_id(self.sid)
54
        self.p_detection(self.detection)
55
56
        if not len(self.msg):
57
            self.warnings.append("Rule has no 'msg:'.")
58
        if not len(self.score):
59
            self.error.append("Rule has no score.")
60
61
    def fail(self, msg):
62
        self.error.append(msg)
63
        return False
64
65
    # Bellow are parsers for specific parts of a rule
66
    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 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...
67
        return True
68
69
    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...
70
        if not s.startswith("str:") and not s.startswith("rx:"):
71
            self.fail("detection {} is neither rx: or str:".format(s))
72
            return False
73
        if not s.islower():
74
            self.warnings.append("detection {} is not lower-case. naxsi is case-insensitive".format(s))
75
        if assign is True:
76
            self.detection = s
77
        return True
78
79
    def p_genericstr(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...
80
        if s and not s.islower():
81
            self.warnings.append("Pattern ({0}) is not lower-case.".format(s))
82
        return True
83
84
    def p_mz(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...
85
        has_zone = False
86
        mz_state = set()
87
        locs = s.split('|')
88
        for loc in locs:
89
            kw = loc
90
            arg = None
91
            if loc.startswith("$"):
92
                try:
93
                    kw, arg = loc.split(":")
94
                except ValueError:
95
                    return self.fail("Missing 2nd part after ':' in {0}".format(loc))
96
            # check it is a valid keyword
97
            if kw not in self.sub_mz:
98
                return self.fail("'{0}' no a known sub-part of mz : {1}".format(kw, self.sub_mz))
99
            mz_state.add(kw)
100
            # verify the rule doesn't attempt to target REGEX and STATIC _VAR/URL at the same time
101
            if len(self.rx_mz & mz_state) and len(self.static_mz & mz_state):
102
                return self.fail("You can't mix static $* with regex $*_X ({})".format(str(mz_state)))
103
            # just a gentle reminder
104
            if arg and arg.islower() is False:
105
                self.warnings.append("{0} in {1} is not lowercase. naxsi is case-insensitive".format(arg, loc))
106
            # the rule targets an actual zone
107
            if kw not in ["$URL", "$URL_X"] and kw 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...
108
                has_zone = True
109
        if has_zone is False:
110
            return self.fail("The rule/whitelist doesn't target any zone.")
111
        if assign is True:
112
            self.mz = s
113
        return True
114
115
    def p_id(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...
116
        try:
117
            x = int(s)
0 ignored issues
show
Coding Style Naming introduced by
The name x does not conform to the variable 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...
118
            if x < 10000:
119
                self.warnings.append("rule IDs below 10k are reserved ({0})".format(x))
120
        except ValueError:
121
            self.error.append("id:{0} is not numeric".format(s))
122
            return False
123
        if assign is True:
124
            self.sid = x
125
        return True
126
127
    def splitter(self, 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...
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...
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
    def parse_rule(self, x):
0 ignored issues
show
Coding Style Naming introduced by
The name x 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...
136
        """
137
        Parse and validate a full naxsi rule
138
        :param x: raw rule
139
        :return: [True|False, dict]
140
        """
141
        xfrag_kw = {"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
        # parse string
145
        split = self.splitter(x)
146
        # check if it's a MainRule/BasicRule, store&delete kw
147
        sect = set(self.mr_kw) & set(split)
148
        if len(sect) != 1:
149
            return self.fail("no (or multiple) mainrule/basicrule keyword.")
150
        split.remove(sect.pop())
151
        if ";" in split: split.remove(";")
0 ignored issues
show
Coding Style introduced by
More than one statement on a single line
Loading history...
152
        # iterate while there is data, as handlers can defer
153
        while True:
154
            # we are done
155
            if len(split) == 0:
156
                break
157
            for kw in split:
158
                okw = kw
159
                kw = kw.strip()
160
                # clean-up quotes or semicolon
161
                if kw.endswith(";"):
162
                    kw = kw[:-1]
163
                if kw.startswith(('"', "'")) and (kw[0] == kw[-1]):
164
                    kw = kw[1:-1]
165
                for frag_kw in xfrag_kw:
166
                    ret = False
167
                    if kw.startswith(frag_kw):
168
                        # parser funcs returns True/False
169
                        ret = xfrag_kw[frag_kw](kw[len(frag_kw):])
170
                        if ret is False:
171
                            return self.fail("parsing of element '{0}' failed.".format(kw))
172
                        if ret is True:
173
                            split.remove(okw)
174
                        break
175
                # we have an item that wasn't successfully parsed
176
                if okw in split and ret is not None:
177
                    return False
178
        return True
179