Skip to content

Commit fc0a005

Browse files
committed
rules: move rules testing in rules
No need to have it done by the probe middleware so let's do it in the rules application.
1 parent 2756acd commit fc0a005

File tree

3 files changed

+290
-251
lines changed

3 files changed

+290
-251
lines changed

rules/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import json
4141
import IPy
4242

43+
from rules.tests_rules import TestRules
44+
4345
from django.contrib.auth.models import User
4446

4547
def validate_hostname(val):
@@ -614,7 +616,7 @@ def to_buffer(self):
614616

615617
def test_rule_buffer(self, rule_buffer, single = False):
616618
Probe = __import__(settings.RULESET_MIDDLEWARE)
617-
testor = Probe.common.Test()
619+
testor = TestRules()
618620
tmpdir = tempfile.mkdtemp()
619621
self.export_files(tmpdir)
620622
related_files = {}
@@ -2050,7 +2052,7 @@ def to_buffer(self):
20502052

20512053
def test_rule_buffer(self, rule_buffer, single = False):
20522054
Probe = __import__(settings.RULESET_MIDDLEWARE)
2053-
testor = Probe.common.Test()
2055+
testor = TestRules()
20542056
tmpdir = tempfile.mkdtemp()
20552057
self.export_files(tmpdir)
20562058
related_files = {}

rules/tests_rules.py

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
"""
2+
Copyright(C) 2018 Stamus Networks
3+
Written by Eric Leblond <[email protected]>
4+
5+
This file is part of Scirius.
6+
7+
Scirius is free software: you can redistribute it and/or modify
8+
it under the terms of the GNU General Public License as published by
9+
the Free Software Foundation, either version 3 of the License, or
10+
(at your option) any later version.
11+
12+
Scirius is distributed in the hope that it will be useful,
13+
but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
GNU General Public License for more details.
16+
17+
You should have received a copy of the GNU General Public License
18+
along with Scirius. If not, see <http://www.gnu.org/licenses/>.
19+
"""
20+
21+
import psutil
22+
import subprocess
23+
import tempfile
24+
import shutil
25+
import os
26+
import json
27+
import StringIO
28+
import re
29+
30+
from django.utils.html import escape
31+
32+
class TestRules():
33+
VARIABLE_ERROR = 101
34+
RULEFILE_ERRNO = [ 39, 42 ]
35+
USELESS_ERRNO = [ 40, 43, 44 ]
36+
CONFIG_FILE = """
37+
%YAML 1.1
38+
---
39+
logging:
40+
default-log-level: error
41+
outputs:
42+
- console:
43+
enabled: yes
44+
type: json
45+
vars:
46+
address-groups:
47+
HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]"
48+
EXTERNAL_NET: "!$HOME_NET"
49+
HTTP_SERVERS: "$HOME_NET"
50+
SMTP_SERVERS: "$HOME_NET"
51+
SQL_SERVERS: "$HOME_NET"
52+
DNS_SERVERS: "$HOME_NET"
53+
TELNET_SERVERS: "$HOME_NET"
54+
AIM_SERVERS: "$EXTERNAL_NET"
55+
DNP3_SERVER: "$HOME_NET"
56+
DNP3_CLIENT: "$HOME_NET"
57+
MODBUS_CLIENT: "$HOME_NET"
58+
MODBUS_SERVER: "$HOME_NET"
59+
ENIP_CLIENT: "$HOME_NET"
60+
ENIP_SERVER: "$HOME_NET"
61+
port-groups:
62+
HTTP_PORTS: "80"
63+
SHELLCODE_PORTS: "!80"
64+
ORACLE_PORTS: 1521
65+
SSH_PORTS: 22
66+
DNP3_PORTS: 20000
67+
MODBUS_PORTS: 502
68+
"""
69+
70+
REFERENCE_CONFIG = """
71+
# config reference: system URL
72+
73+
config reference: bugtraq http://www.securityfocus.com/bid/
74+
config reference: bid http://www.securityfocus.com/bid/
75+
config reference: cve http://cve.mitre.org/cgi-bin/cvename.cgi?name=
76+
#config reference: cve http://cvedetails.com/cve/
77+
config reference: secunia http://www.secunia.com/advisories/
78+
79+
#whitehats is unfortunately gone
80+
config reference: arachNIDS http://www.whitehats.com/info/IDS
81+
82+
config reference: McAfee http://vil.nai.com/vil/content/v_
83+
config reference: nessus http://cgi.nessus.org/plugins/dump.php3?id=
84+
config reference: url http://
85+
config reference: et http://doc.emergingthreats.net/
86+
config reference: etpro http://doc.emergingthreatspro.com/
87+
config reference: telus http://
88+
config reference: osvdb http://osvdb.org/show/osvdb/
89+
config reference: threatexpert http://www.threatexpert.com/report.aspx?md5=
90+
config reference: md5 http://www.threatexpert.com/report.aspx?md5=
91+
config reference: exploitdb http://www.exploit-db.com/exploits/
92+
config reference: openpacket https://www.openpacket.org/capture/grab/
93+
config reference: securitytracker http://securitytracker.com/id?
94+
config reference: secunia http://secunia.com/advisories/
95+
config reference: xforce http://xforce.iss.net/xforce/xfdb/
96+
config reference: msft http://technet.microsoft.com/security/bulletin/
97+
"""
98+
99+
CLASSIFICATION_CONFIG = """
100+
config classification: not-suspicious,Not Suspicious Traffic,3
101+
config classification: unknown,Unknown Traffic,3
102+
config classification: bad-unknown,Potentially Bad Traffic, 2
103+
config classification: attempted-recon,Attempted Information Leak,2
104+
config classification: successful-recon-limited,Information Leak,2
105+
config classification: successful-recon-largescale,Large Scale Information Leak,2
106+
config classification: attempted-dos,Attempted Denial of Service,2
107+
config classification: successful-dos,Denial of Service,2
108+
config classification: attempted-user,Attempted User Privilege Gain,1
109+
config classification: unsuccessful-user,Unsuccessful User Privilege Gain,1
110+
config classification: successful-user,Successful User Privilege Gain,1
111+
config classification: attempted-admin,Attempted Administrator Privilege Gain,1
112+
config classification: successful-admin,Successful Administrator Privilege Gain,1
113+
114+
115+
# NEW CLASSIFICATIONS
116+
config classification: rpc-portmap-decode,Decode of an RPC Query,2
117+
config classification: shellcode-detect,Executable code was detected,1
118+
config classification: string-detect,A suspicious string was detected,3
119+
config classification: suspicious-filename-detect,A suspicious filename was detected,2
120+
config classification: suspicious-login,An attempted login using a suspicious username was detected,2
121+
config classification: system-call-detect,A system call was detected,2
122+
config classification: tcp-connection,A TCP connection was detected,4
123+
config classification: trojan-activity,A Network Trojan was detected, 1
124+
config classification: unusual-client-port-connection,A client was using an unusual port,2
125+
config classification: network-scan,Detection of a Network Scan,3
126+
config classification: denial-of-service,Detection of a Denial of Service Attack,2
127+
config classification: non-standard-protocol,Detection of a non-standard protocol or event,2
128+
config classification: protocol-command-decode,Generic Protocol Command Decode,3
129+
config classification: web-application-activity,access to a potentially vulnerable web application,2
130+
config classification: web-application-attack,Web Application Attack,1
131+
config classification: misc-activity,Misc activity,3
132+
config classification: misc-attack,Misc Attack,2
133+
config classification: icmp-event,Generic ICMP event,3
134+
config classification: kickass-porn,SCORE! Get the lotion!,1
135+
config classification: policy-violation,Potential Corporate Privacy Violation,1
136+
config classification: default-login-attempt,Attempt to login by a default username and password,2
137+
"""
138+
139+
def parse_suricata_error(self, error, single = False):
140+
ret = {
141+
'errors': [],
142+
'warnings': [],
143+
}
144+
error_list = []
145+
warning_list = []
146+
variable_list = []
147+
error_stream = StringIO.StringIO(error)
148+
for line in error_stream:
149+
try:
150+
s_err = json.loads(line)
151+
except:
152+
ret['errors'].append({'message': error, 'format': 'raw'})
153+
return ret
154+
errno = s_err['engine']['error_code']
155+
if not single or errno not in self.RULEFILE_ERRNO:
156+
if errno == self.VARIABLE_ERROR:
157+
variable = s_err['engine']['message'].split("\"")[1]
158+
if not "$" + variable in variable_list:
159+
variable_list.append("$" + variable)
160+
s_err['engine']['message'] = "Custom address variable \"$%s\" is used and need to be defined in probes configuration" % (variable)
161+
ret['warnings'].append(s_err['engine'])
162+
continue
163+
if not errno in self.USELESS_ERRNO:
164+
# clean error message
165+
if errno == 39:
166+
# exclude error on varible
167+
found = False
168+
for variable in variable_list:
169+
if variable in s_err['engine']['message']:
170+
found = True
171+
break
172+
if found:
173+
continue
174+
s_err['engine']['message'] = s_err['engine']['message'].split(' from file')[0]
175+
getsid = re.compile("sid *:(\d+)")
176+
match = getsid.search(line)
177+
if match:
178+
s_err['engine']['sid'] = int(match.groups()[0])
179+
if errno == 42:
180+
s_err['engine']['message'] = s_err['engine']['message'].split(' from')[0]
181+
ret['errors'].append(s_err['engine'])
182+
return ret
183+
184+
def rule_buffer(self, rule_buffer, config_buffer = None, related_files = None, reference_config = None, classification_config = None):
185+
# create temp directory
186+
tmpdir = tempfile.mkdtemp()
187+
# write the rule file in temp dir
188+
rule_file = os.path.join(tmpdir, "file.rules")
189+
rf = open(rule_file, 'w')
190+
try:
191+
rf.write(rule_buffer)
192+
except UnicodeEncodeError:
193+
rf.write(rule_buffer.encode('utf-8'))
194+
rf.close()
195+
196+
if not reference_config:
197+
refence_config = self.REFERENCE_CONFIG
198+
reference_file = os.path.join(tmpdir, "reference.config")
199+
rf = open(reference_file, 'w')
200+
rf.write(refence_config)
201+
rf.close()
202+
203+
if not classification_config:
204+
classification_config = self.CLASSIFICATION_CONFIG
205+
classification_file = os.path.join(tmpdir, "classification.config")
206+
cf = open(classification_file, 'w')
207+
cf.write(classification_config)
208+
cf.close()
209+
210+
if not config_buffer:
211+
config_buffer = self.CONFIG_FILE
212+
config_file = os.path.join(tmpdir, "suricata.yaml")
213+
cf = open(config_file, 'w')
214+
# write the config file in temp dir
215+
cf.write(config_buffer)
216+
cf.write("default-rule-path: " + tmpdir + "\n")
217+
cf.write("default-reputation-path: " + tmpdir + "\n")
218+
cf.write("reference-config-file: " + tmpdir + "/reference.config\n")
219+
cf.write("classification-file: " + tmpdir + "/classification.config\n")
220+
cf.close()
221+
related_files = related_files or {}
222+
for rfile in related_files:
223+
related_file = os.path.join(tmpdir, rfile)
224+
rf = open(related_file, 'w')
225+
rf.write(related_files[rfile])
226+
rf.close()
227+
228+
suri_cmd = ['suricata', '-T', '-l', tmpdir, '-S', rule_file, '-c', config_file]
229+
# start suricata in test mode
230+
suriprocess = subprocess.Popen(suri_cmd , stdout=subprocess.PIPE, stderr=subprocess.PIPE)
231+
(outdata, errdata) = suriprocess.communicate()
232+
shutil.rmtree(tmpdir)
233+
# if success ok
234+
if suriprocess.returncode == 0:
235+
return {'status': True, 'errors': ''}
236+
# if not return error
237+
return {'status': False, 'errors': errdata}
238+
239+
def _escape_result(self, res):
240+
for key in ('warnings', 'errors'):
241+
lst = res.get(key, [])
242+
for msg in lst:
243+
msg['message'] = escape(msg['message'])
244+
return res
245+
246+
def check_rule_buffer(self, rule_buffer, config_buffer = None, related_files = None, single = False):
247+
related_files = related_files or {}
248+
prov_result = self.rule_buffer(rule_buffer, config_buffer = config_buffer, related_files = related_files)
249+
if prov_result['status'] and not prov_result.has_key('warnings'):
250+
return self._escape_result(prov_result)
251+
res = self.parse_suricata_error(prov_result['errors'], single = single)
252+
prov_result['errors'] = res['errors']
253+
prov_result['warnings'] = res['warnings']
254+
i = 6 # support only 6 unknown variables per rule
255+
prov_result['iter'] = 0;
256+
while len(res['warnings']) and i > 0:
257+
modified = False
258+
for warning in res['warnings']:
259+
if warning['error_code'] == self.VARIABLE_ERROR:
260+
var = warning['message'].split("\"")[1]
261+
# transform rule_buffer to remove the faulty variable
262+
rule_buffer = rule_buffer.replace("!" + var, "any");
263+
rule_buffer = rule_buffer.replace(var, "any");
264+
modified = True
265+
if modified == False:
266+
break
267+
result = self.rule_buffer(rule_buffer, config_buffer = config_buffer, related_files = related_files)
268+
res = self.parse_suricata_error(result['errors'], single = single)
269+
prov_result['errors'] = res['errors']
270+
if len(res['warnings']):
271+
prov_result['warnings'] = prov_result['warnings'] + res['warnings']
272+
i = i - 1
273+
prov_result['iter'] = prov_result['iter'] + 1
274+
if len(prov_result['errors']) == 0:
275+
prov_result['status'] = True
276+
return self._escape_result(prov_result)
277+
278+
def rule(self, rule_buffer, config_buffer = None, related_files = None):
279+
related_files = related_files or {}
280+
return self.check_rule_buffer(rule_buffer, config_buffer = config_buffer, related_files = related_files, single = True)
281+
282+
def rules(self, rule_buffer, config_buffer = None, related_files = None):
283+
related_files = related_files or {}
284+
return self.check_rule_buffer(rule_buffer, config_buffer = config_buffer, related_files = related_files, single = False)
285+

0 commit comments

Comments
 (0)