2
# ------------------------------------------------------------------
4
# Copyright (C) 2015 Christian Boltz <apparmor@cboltz.de>
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of version 2 of the GNU General Public
8
# License published by the Free Software Foundation.
10
# ------------------------------------------------------------------
13
from common_test import AATest, setup_all_loops, setup_aa, read_file
16
from apparmor.common import open_file_read
19
from apparmor.logparser import ReadLog
21
class TestLibapparmorTestMulti(AATest):
22
'''Parse all libraries/libapparmor/testsuite/test_multi tests and compare the result with the *.out files'''
24
tests = 'invalid' # filled by parse_test_profiles()
26
def _run_test(self, params, expected):
27
# tests[][expected] is a dummy, replace it with the real values
28
expected = self._parse_libapparmor_test_multi(params)
30
with open_file_read('%s.in' % params) as f_in:
31
loglines = f_in.readlines()
38
self.assertEqual(len(loglines2), 1, '%s.in should only contain one line!' % params)
40
parser = ReadLog('', '', '', '')
41
parsed_event = parser.parse_event(loglines2[0])
43
if parsed_event and expected:
44
parsed_items = dict(parsed_event.items())
46
# check if the line passes the regex in logparser.py
47
if not parser.RE_LOG_ALL.search(loglines2[0]):
48
raise Exception("Log event doesn't match RE_LOG_ALL")
50
for label in expected:
52
'file', # filename of the *.in file
53
'event_type', # mapped to aamode
54
'audit_id', 'audit_sub_id', # not set nor relevant
55
'comm', # not set, and not too useful
56
# XXX most of the keywords listed below mean "TODO"
57
'fsuid', 'ouid', # file events
58
'flags', 'fs_type', # mount
59
'namespace', # file_lock only?? (at least the tests don't contain this in other event types with namespace)
60
'net_local_addr', 'net_foreign_addr', 'net_local_port', 'net_foreign_port', # detailed network events
61
'peer', 'signal', # signal
62
'src_name', # pivotroot
63
'dbus_bus', 'dbus_interface', 'dbus_member', 'dbus_path', # dbus
64
'peer_pid', 'peer_profile', # dbus
67
elif parsed_items['operation'] == 'exec' and label in ['sock_type', 'family', 'protocol']:
68
pass # XXX 'exec' + network? really?
69
elif parsed_items['operation'] == 'ptrace' and label == 'name2' and params.endswith('/ptrace_garbage_lp1689667_1'):
70
pass # libapparmor would better qualify this case as invalid event
71
elif not parsed_items.get(label, None):
72
raise Exception('parsed_items[%s] not set' % label)
73
elif not expected.get(label, None):
74
raise Exception('expected[%s] not set' % label)
76
self.assertEqual(str(parsed_items[label]), expected[label], '%s differs' % label)
78
self.assertIsNone(parsed_event) # that's why we end up here
79
self.assertEqual(dict(), expected, 'parsed_event is none') # effectively print the content of expected
81
self.assertIsNone(expected) # that's why we end up here
82
self.assertEqual(parsed_event, dict(), 'expected is none') # effectively print the content of parsed_event
84
self.assertIsNone(expected) # that's why we end up here
85
self.assertIsNone(parsed_event) # that's why we end up here
86
self.assertEqual(parsed_event, expected) # both are None
88
# list of labels that use a different name in logparser.py than in the test_multi *.out files
89
# (additionally, .lower() is applied to all labels)
91
'Mask': 'request_mask',
93
'Token': 'magic_token',
94
'ErrorCode': 'error_code',
95
'Network family': 'family',
96
'Socket type': 'sock_type',
97
'Local addr': 'net_local_addr',
98
'Foreign addr': 'net_foreign_addr',
99
'Local port': 'net_local_port',
100
'Foreign port': 'net_foreign_port',
101
'Audit subid': 'audit_sub_id',
106
def _parse_libapparmor_test_multi(self, file_with_path):
107
'''parse the libapparmor test_multi *.in tests and their expected result in *.out'''
109
with open_file_read('%s.out' % file_with_path) as f_in:
110
expected = f_in.readlines()
112
if expected[0].rstrip('\n') != 'START':
113
raise Exception("%s.out doesn't have 'START' in its first line! (%s)" % ( file_with_path, expected[0]))
118
for line in expected:
119
label, value = line.split(':', 1)
121
# test_multi doesn't always use the original labels :-/
122
if label in self.label_map.keys():
123
label = self.label_map[label]
124
label = label.replace(' ', '_').lower()
125
exresult[label] = value.strip()
127
if not exresult['event_type'].startswith('AA_RECORD_'):
128
raise Exception("event_type doesn't start with AA_RECORD_: %s in file %s" % (exresult['event_type'], file_with_path))
130
exresult['aamode'] = exresult['event_type'].replace('AA_RECORD_', '')
131
if exresult['aamode'] == 'ALLOWED':
132
exresult['aamode'] = 'PERMITTING'
133
if exresult['aamode'] == 'DENIED':
134
exresult['aamode'] = 'REJECTING'
136
if exresult['event_type'] == 'AA_RECORD_INVALID': # or exresult.get('error_code', 0) != 0: # XXX should events with errors be ignored?
142
# tests that do not produce the expected profile (checked with assertNotEqual)
143
log_to_profile_known_failures = [
144
'testcase_dmesg_changeprofile_01', # change_profile not yet supported in logparser
145
'testcase_changeprofile_01', # change_profile not yet supported in logparser
147
'testcase_mount_01', # mount rules not yet supported in logparser
149
'testcase_pivotroot_01', # pivot_rot not yet supported in logparser
156
# null-* hats get ignored by handle_children() if it didn't see an exec event for that null-* hat
157
'syslog_datetime_01',
158
'syslog_datetime_02',
159
'syslog_datetime_03',
160
'syslog_datetime_04',
161
'syslog_datetime_05',
162
'syslog_datetime_06',
163
'syslog_datetime_07',
164
'syslog_datetime_08',
165
'syslog_datetime_09',
166
'syslog_datetime_10',
167
'syslog_datetime_11',
168
'syslog_datetime_12',
169
'syslog_datetime_13',
170
'syslog_datetime_14',
171
'syslog_datetime_15',
172
'syslog_datetime_16',
173
'syslog_datetime_17',
174
'syslog_datetime_18',
175
'testcase_network_send_receive',
178
# tests that cause crashes or need user interaction (will be skipped)
179
log_to_profile_skip = [
180
'testcase31', # XXX AppArmorBug: Log contains unknown mode mrwIxl
182
'testcase_dmesg_changehat_negative_error', # fails in write_header -> quote_if_needed because data is None
183
'testcase_syslog_changehat_negative_error', # fails in write_header -> quote_if_needed because data is None
185
'testcase_changehat_01', # interactive, asks to add a hat
188
class TestLogToProfile(AATest):
189
'''Check if the libraries/libapparmor/testsuite/test_multi tests result in the expected profile'''
191
tests = 'invalid' # filled by parse_test_profiles()
193
def _run_test(self, params, expected):
194
logfile = '%s.in' % params
195
profile_dummy_file = 'AATest_does_exist'
197
# we need to find out the profile name and aamode (complain vs. enforce mode) so that the test can access the correct place in storage
198
parser = ReadLog('', '', '', '')
199
parsed_event = parser.parse_event(read_file(logfile))
201
if not parsed_event: # AA_RECORD_INVALID
204
if params.split('/')[-1] in log_to_profile_skip:
207
aamode = parsed_event['aamode']
209
if aamode in['AUDIT', 'STATUS', 'HINT']: # ignore some event types # XXX maybe we shouldn't ignore AUDIT events?
212
if aamode not in ['PERMITTING', 'REJECTING']:
213
raise Exception('Unexpected aamode %s' % parsed_event['aamode'])
215
# cleanup apparmor.aa storage
216
apparmor.aa.log = dict()
217
apparmor.aa.aa = apparmor.aa.hasher()
218
apparmor.aa.prelog = apparmor.aa.hasher()
220
profile = parsed_event['profile']
223
profile, hat = profile.split('//')
225
apparmor.aa.existing_profiles = {profile: profile_dummy_file}
227
log_reader = ReadLog(dict(), logfile, apparmor.aa.existing_profiles, '')
228
log = log_reader.read_log('')
231
apparmor.aa.handle_children('', '', root) # interactive for exec events!
233
log_dict = apparmor.aa.collapse_log()
235
apparmor.aa.filelist = apparmor.aa.hasher()
236
apparmor.aa.filelist[profile_dummy_file]['profiles'][profile] = True
238
new_profile = apparmor.aa.serialize_profile(log_dict[aamode][profile], profile, None)
240
expected_profile = read_file('%s.profile' % params)
242
if params.split('/')[-1] in log_to_profile_known_failures:
243
self.assertNotEqual(new_profile, expected_profile) # known failure
245
self.assertEqual(new_profile, expected_profile)
248
def find_test_multi(log_dir):
249
'''find all log sniplets in the given log_dir'''
251
log_dir = os.path.abspath(log_dir)
254
for root, dirs, files in os.walk(log_dir):
256
if file.endswith('.in'):
257
file_with_path = os.path.join(root, file[:-3]) # filename without '.in'
258
tests.append([file_with_path, True]) # True is a dummy testresult, parsing of the *.out files is done while running the tests
260
elif file.endswith('.out') or file.endswith('.err') or file.endswith('.profile'):
263
raise Exception('Found unknown file %s in libapparmor test_multi' % file)
268
print('Testing libapparmor test_multi tests...')
269
TestLibapparmorTestMulti.tests = find_test_multi('../../libraries/libapparmor/testsuite/test_multi/')
270
TestLogToProfile.tests = find_test_multi('../../libraries/libapparmor/testsuite/test_multi/')
272
setup_aa(apparmor.aa)
273
setup_all_loops(__name__)
274
if __name__ == '__main__':
275
unittest.main(verbosity=1) # reduced verbosity due to the big number of tests