Completed
Push — master ( 7424d3...b047e1 )
by -
01:35
created

NaxsiRules.fullstr()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1
Metric Value
cc 1
dl 0
loc 4
ccs 4
cts 4
cp 1
crap 1
rs 10
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
    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
29 1
    def __init__(self, msg="", detection="", mz="", score="", sid=42000, ruleset="", rmks="", active=0, negative=0,
30
                 timestamp=0):
31 1
        self.msg = msg
32 1
        self.detection = detection
33 1
        self.mz = mz
34 1
        self.score = score
35 1
        self.sid = sid
36 1
        self.ruleset = ruleset
37 1
        self.rmks = rmks
38 1
        self.active = active
39 1
        self.negative = 1 if negative == 'checked' else 0
40 1
        self.timestamp = timestamp
41 1
        self.warnings = []
42 1
        self.error = []
43
44 1
    def fullstr(self):
45 1
        rdate = strftime("%F - %H:%M", localtime(float(str(self.timestamp))))
46 1
        rmks = "# ".join(self.rmks.strip().split("\n"))
47 1
        return "#\n# sid: {0} | date: {1}\n#\n# {2}\n#\n{3}".format(self.sid, rdate, rmks, self.__str__())
48
49 1
    def __str__(self):
50 1
        negate = 'negative' if self.negative == 1 else ''
51 1
        return 'MainRule {} "{}" "msg:{}" "mz:{}" "s:{}" id:{} ;'.format(
52
            negate, self.detection, self.msg, self.mz, self.score, self.sid)
53
54 1
    def explain(self):
55
        """ Return a string explaining the rule
56
57
         :return str: A textual explanation of the rule
58
         """
59 1
        translation = {'ARGS': 'argument', 'BODY': 'body', 'URL': 'url', 'HEADER': 'header'}
60 1
        explanation = 'The rule number <strong>%d</strong> is ' % self.sid
61 1
        if self.negative:
62
            explanation += '<strong>not</strong> '
63 1
        explanation += 'setting the '
64
65 1
        scores = []
66 1
        for score in self.score.split(','):
67 1
            scores.append('<strong>{0}</strong> to <strong>{1}</strong> '.format(*score.split(':', 1)))
68 1
        explanation += ', '.join(scores) + 'when it '
69 1
        if self.detection.startswith('str:'):
70 1
            explanation += 'finds the string <strong>{}</strong> '.format(self.detection[4:])
71
        else:
72
            explanation += 'matches the regexp <strong>{}</strong> '.format(self.detection[3:])
73
74 1
        zones = []
75 1
        for mz in self.mz.split('|'):
76 1
            if mz.startswith('$'):
77
                current_zone, arg = mz.split(":", 1)
78
                zone_name = "?"
79
80
                for translated_name in translation:  # translate zone names
81
                    if translated_name in current_zone:
82
                        zone_name = translation[translated_name]
83
84
                if "$URL" in current_zone:
85
                    regexp = "matching regex" if current_zone == "$URL_X" else ""
86
                    explanation += "on the URL {} '{}' ".format(regexp, arg)
87
                else:
88
                    regexp = "matching regex" if current_zone.endswith("_X") else ""
89
                    explanation += "in the var with name {} '{}' of {} ".format(regexp, arg, zone_name)
90
            else:
91 1
                zones.append('the <strong>{0}</strong>'.format(translation[mz]))
92 1
        return explanation
93
94 1
    def validate(self):
95 1
        self.__validate_matchzone(self.mz)
96 1
        self.__validate_id(self.sid)
97 1
        self.__validate_detection(self.detection)
98
99 1
        if not self.msg:
100
            self.warnings.append("Rule has no 'msg:'.")
101 1
        if not self.score:
102
            self.error.append("Rule has no score.")
103
104 1
    def __fail(self, msg):
105 1
        self.error.append(msg)
106 1
        return False
107
108
    # Bellow are parsers for specific parts of a rule
109
110 1
    def __validate_detection(self, p_str, label="", assign=False):
111 1
        p_str = label + p_str
112 1
        if not p_str.islower():
113 1
            self.warnings.append("detection {} is not lower-case. naxsi is case-insensitive".format(p_str))
114
115 1
        if p_str.startswith("str:"):  # no further validation on strings
116 1
            pass
117 1
        elif p_str.startswith("rx:"):
118 1
            try:  # try to validate the regex with PCRE's python bindings
119 1
                import pcre
120
                try:  # if we can't compile the regex, it's likely invalid
121
                    pcre.compile(p_str[3:])
122
                except pcre.PCREError:
123
                    return self.__fail("{} is not a valid regex:".format(p_str))
124 1
            except ImportError:  # python-pcre is an optional dependency
0 ignored issues
show
Unused Code introduced by
This except handler seems to be unused and could be removed.

Except handlers which only contain pass and do not have an else clause can usually simply be removed:

try:
    raises_exception()
except:  # Could be removed
    pass
Loading history...
125 1
                pass
126
        else:
127 1
            return self.__fail("detection {} is neither rx: or str:".format(p_str))
128
129 1
        if assign is True:
130 1
            self.detection = p_str
131 1
        return True
132
133 1
    def __validate_genericstr(self, p_str, label="", assign=False):
134 1
        if assign is False:
135
            return True
136 1
        if label == "s:":
137 1
            self.score = p_str
138 1
        elif label == "msg:":
139 1
            self.msg = p_str
140
        elif label == "negative":
141
            self.negative = 1
142
        elif label != "":
143
            return self.__fail("Unknown fragment {}".format(label+p_str))
144 1
        return True
145
146 1
    def __validate_matchzone(self, p_str, label="", assign=False):
147 1
        has_zone = False
148 1
        mz_state = set()
149 1
        for loc in p_str.split('|'):
150 1
            keyword, arg = loc, None
151 1
            if loc.startswith("$"):
152 1
                if loc.find(":") == -1:
153
                    return self.__fail("Missing 2nd part after ':' in {0}".format(loc))
154 1
                keyword, arg = loc.split(":")
155
156 1
            if keyword not in self.sub_mz:  # check if `keyword` is a valid keyword
157
                return self.__fail("'{0}' no a known sub-part of mz : {1}".format(keyword, self.sub_mz))
158
159 1
            mz_state.add(keyword)
160
161
            # verify that the rule doesn't attempt to target REGEX and STATIC _VAR/URL at the same time
162 1
            if len(self.rx_mz & mz_state) and len(self.static_mz & mz_state):
163 1
                return self.__fail("You can't mix static $* with regex $*_X ({})".format(', '.join(mz_state)))
164
165 1
            if arg and not arg.islower():  # just a gentle reminder
166 1
                self.warnings.append("{0} in {1} is not lowercase. naxsi is case-insensitive".format(arg, loc))
167
168
            # the rule targets an actual zone
169 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...
170 1
                has_zone = True
171
172 1
        if has_zone is False:
173
            return self.__fail("The rule/whitelist doesn't target any zone.")
174
175 1
        if assign is True:
176 1
            self.mz = p_str
177
178 1
        return True
179
180 1
    def __validate_id(self, p_str, label="", assign=False):
181 1
        try:
182 1
            num = int(p_str)
183 1
            if num < 10000:
184 1
                self.warnings.append("rule IDs below 10k are reserved ({0})".format(num))
185
        except ValueError:
186
            self.error.append("id:{0} is not numeric".format(p_str))
187
            return False
188 1
        if assign is True:
189 1
            self.sid = num
190 1
        return True
191
192 1
    @staticmethod
193
    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...
194 1
        lexer = shlex(s)
195 1
        lexer.whitespace_split = True
196 1
        return list(iter(lexer.get_token, ''))
197
198 1
    def parse_rule(self, full_str):
199
        """
200
        Parse and validate a full naxsi rule
201
        :param full_str: raw rule
202
        :return: [True|False, dict]
203
        """
204 1
        self.warnings = []
205 1
        self.error = []
206
207 1
        func_map = {"id:": self.__validate_id, "str:": self.__validate_detection,
208
                    "rx:": self.__validate_detection, "msg:": self.__validate_genericstr,
209
                    "mz:": self.__validate_matchzone, "negative": self.__validate_genericstr,
210
                    "s:": self.__validate_genericstr}
211 1
        ret = False
212 1
        split = self.splitter(full_str)  # parse string
213 1
        intersection = set(split).intersection(set(self.mr_kw))
214
215 1
        if not intersection:
216
            return self.__fail("No mainrule/basicrule keyword.")
217 1
        elif len(intersection) > 1:
218
            return self.__fail("Multiple mainrule/basicrule keywords.")
219
220 1
        split.remove(intersection.pop())  # remove the mainrule/basicrule keyword
221
222 1
        if ";" in split:
223 1
            split.remove(";")
224
225 1
        while split:  # iterate while there is data, as handlers can defer
226 1
            for keyword in split:
227 1
                orig_kw = keyword
228 1
                keyword = keyword.strip()
229
230 1
                if keyword.endswith(";"):  # remove semi-colons
231 1
                    keyword = keyword[:-1]
232 1
                if keyword.startswith(('"', "'")) and (keyword[0] == keyword[-1]):  # remove (double-)quotes
233 1
                    keyword = keyword[1:-1]
234
235 1
                for frag_kw in func_map:
236 1
                    ret = False
237 1
                    if keyword.startswith(frag_kw):
238
                        # parser funcs returns True/False
239 1
                        ret = func_map[frag_kw](keyword[len(frag_kw):], label=frag_kw, assign=True)
240 1
                        if ret is True:
241 1
                            split.remove(orig_kw)
242
                        else:
243 1
                            return self.__fail("parsing of element '{0}' failed.".format(keyword))
244 1
                        break
245
246 1
                if orig_kw in split and ret is not None:  # we have an item that wasn't successfully parsed
247
                    return False
248
        return True
249