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): |
|
|
|
|
69
|
|
|
return True |
70
|
|
|
|
71
|
1 |
|
def p_detection(self, s, assign=False): |
|
|
|
|
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, s, assign=False): |
|
|
|
|
81
|
|
|
if s and not s.islower(): |
82
|
|
|
self.warnings.append("Pattern ({0}) is not lower-case.".format(s)) |
83
|
|
|
return True |
84
|
|
|
|
85
|
1 |
|
def p_mz(self, s, assign=False): |
|
|
|
|
86
|
1 |
|
has_zone = False |
87
|
1 |
|
mz_state = set() |
88
|
1 |
|
locs = s.split('|') |
89
|
1 |
|
for loc in locs: |
90
|
1 |
|
kw = loc |
91
|
1 |
|
arg = None |
92
|
1 |
|
if loc.startswith("$"): |
93
|
|
|
try: |
94
|
|
|
kw, arg = loc.split(":") |
95
|
|
|
except ValueError: |
96
|
|
|
return self.fail("Missing 2nd part after ':' in {0}".format(loc)) |
97
|
|
|
# check it is a valid keyword |
98
|
1 |
|
if kw not in self.sub_mz: |
99
|
|
|
return self.fail("'{0}' no a known sub-part of mz : {1}".format(kw, self.sub_mz)) |
100
|
1 |
|
mz_state.add(kw) |
101
|
|
|
# verify the rule doesn't attempt to target REGEX and STATIC _VAR/URL at the same time |
102
|
1 |
|
if len(self.rx_mz & mz_state) and len(self.static_mz & mz_state): |
103
|
|
|
return self.fail("You can't mix static $* with regex $*_X ({})".format(str(mz_state))) |
104
|
|
|
# just a gentle reminder |
105
|
1 |
|
if arg and arg.islower() is False: |
106
|
|
|
self.warnings.append("{0} in {1} is not lowercase. naxsi is case-insensitive".format(arg, loc)) |
107
|
|
|
# the rule targets an actual zone |
108
|
1 |
|
if kw not in ["$URL", "$URL_X"] and kw in (self.rx_mz | self.full_zones | self.static_mz): |
|
|
|
|
109
|
1 |
|
has_zone = True |
110
|
1 |
|
if has_zone is False: |
111
|
|
|
return self.fail("The rule/whitelist doesn't target any zone.") |
112
|
1 |
|
if assign is True: |
113
|
|
|
self.mz = s |
114
|
1 |
|
return True |
115
|
|
|
|
116
|
1 |
|
def p_id(self, s, assign=False): |
|
|
|
|
117
|
1 |
|
try: |
118
|
1 |
|
x = int(s) |
|
|
|
|
119
|
1 |
|
if x < 10000: |
120
|
1 |
|
self.warnings.append("rule IDs below 10k are reserved ({0})".format(x)) |
121
|
|
|
except ValueError: |
122
|
|
|
self.error.append("id:{0} is not numeric".format(s)) |
123
|
|
|
return False |
124
|
1 |
|
if assign is True: |
125
|
|
|
self.sid = x |
126
|
1 |
|
return True |
127
|
|
|
|
128
|
1 |
|
@staticmethod |
129
|
|
|
def splitter(s): |
|
|
|
|
130
|
|
|
lexer = shlex(s) |
131
|
|
|
lexer.quotes = '"\'' |
132
|
|
|
lexer.whitespace_split = True |
133
|
|
|
items = list(iter(lexer.get_token, '')) |
134
|
|
|
return ([i for i in items if i[0] in "\"'"] + |
135
|
|
|
[i for i in items if i[0] not in "\"'"]) |
136
|
|
|
|
137
|
1 |
|
def parse_rule(self, x): |
|
|
|
|
138
|
|
|
""" |
139
|
|
|
Parse and validate a full naxsi rule |
140
|
|
|
:param x: raw rule |
141
|
|
|
:return: [True|False, dict] |
142
|
|
|
""" |
143
|
|
|
xfrag_kw = {"id:": self.p_id, "str:": self.p_genericstr, |
144
|
|
|
"rx:": self.p_genericstr, "msg:": self.p_dummy, "mz:": self.p_mz, |
145
|
|
|
"negative": self.p_dummy, "s:": self.p_dummy} |
146
|
|
|
|
147
|
|
|
split = self.splitter(x) # parse string |
148
|
|
|
|
149
|
|
|
|
150
|
|
|
sect = set(self.mr_kw) & set(split) # check if it's a MainRule/BasicRule, store&delete kw |
151
|
|
|
|
152
|
|
|
if len(sect) != 1: |
153
|
|
|
return self.fail("no (or multiple) mainrule/basicrule keyword.") |
154
|
|
|
|
155
|
|
|
split.remove(sect.pop()) |
156
|
|
|
|
157
|
|
|
if ";" in split: |
158
|
|
|
split.remove(";") |
159
|
|
|
|
160
|
|
|
while True: # iterate while there is data, as handlers can defer |
161
|
|
|
|
162
|
|
|
if not split: # we are done |
163
|
|
|
break |
164
|
|
|
|
165
|
|
|
for kw in split: |
166
|
|
|
okw = kw |
167
|
|
|
kw = kw.strip() |
168
|
|
|
|
169
|
|
|
# clean-up quotes or semicolon |
170
|
|
|
if kw.endswith(";"): |
171
|
|
|
kw = kw[:-1] |
172
|
|
|
if kw.startswith(('"', "'")) and (kw[0] == kw[-1]): |
173
|
|
|
kw = kw[1:-1] |
174
|
|
|
for frag_kw in xfrag_kw: |
175
|
|
|
ret = False |
176
|
|
|
if kw.startswith(frag_kw): |
177
|
|
|
# parser funcs returns True/False |
178
|
|
|
ret = xfrag_kw[frag_kw](kw[len(frag_kw):]) |
179
|
|
|
if ret is False: |
180
|
|
|
return self.fail("parsing of element '{0}' failed.".format(kw)) |
181
|
|
|
if ret is True: |
182
|
|
|
split.remove(okw) |
183
|
|
|
break |
184
|
|
|
# we have an item that wasn't successfully parsed |
185
|
|
|
if okw in split and ret is not None: |
186
|
|
|
return False |
187
|
|
|
return True |
188
|
|
|
|
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.