~apparmor-dev/apparmor/master

« back to all changes in this revision

Viewing changes to utils/test/test-libapparmor-test_multi.py

  • Committer: Steve Beattie
  • Date: 2019-02-19 09:38:13 UTC
  • Revision ID: sbeattie@ubuntu.com-20190219093813-ud526ee6hwn8nljz
The AppArmor project has been converted to git and is now hosted on
gitlab.

To get the converted repository, please do
  git clone https://gitlab.com/apparmor/apparmor

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/python3
2
 
# ------------------------------------------------------------------
3
 
#
4
 
#    Copyright (C) 2015 Christian Boltz <apparmor@cboltz.de>
5
 
#
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.
9
 
#
10
 
# ------------------------------------------------------------------
11
 
 
12
 
import unittest
13
 
from common_test import AATest, setup_all_loops, setup_aa, read_file
14
 
 
15
 
import os
16
 
from apparmor.common import open_file_read
17
 
 
18
 
import apparmor.aa
19
 
from apparmor.logparser import ReadLog
20
 
 
21
 
class TestLibapparmorTestMulti(AATest):
22
 
    '''Parse all libraries/libapparmor/testsuite/test_multi tests and compare the result with the *.out files'''
23
 
 
24
 
    tests = 'invalid'  # filled by parse_test_profiles()
25
 
 
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)
29
 
 
30
 
        with open_file_read('%s.in' % params) as f_in:
31
 
            loglines = f_in.readlines()
32
 
 
33
 
        loglines2 = []
34
 
        for line in loglines:
35
 
            if line.strip():
36
 
                loglines2 += [line]
37
 
 
38
 
        self.assertEqual(len(loglines2), 1, '%s.in should only contain one line!' % params)
39
 
 
40
 
        parser = ReadLog('', '', '', '')
41
 
        parsed_event = parser.parse_event(loglines2[0])
42
 
 
43
 
        if parsed_event and expected:
44
 
            parsed_items = dict(parsed_event.items())
45
 
 
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")
49
 
 
50
 
            for label in expected:
51
 
                if label in [
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
65
 
                        ]:
66
 
                    pass
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)
75
 
                else:
76
 
                    self.assertEqual(str(parsed_items[label]), expected[label], '%s differs' % label)
77
 
        elif expected:
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
80
 
        elif parsed_event:
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
83
 
        else:
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
87
 
 
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)
90
 
    label_map = {
91
 
        'Mask':             'request_mask',
92
 
        'Command':          'comm',
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',
102
 
        'Attribute':        'attr',
103
 
        'Epoch':            'time',
104
 
    }
105
 
 
106
 
    def _parse_libapparmor_test_multi(self, file_with_path):
107
 
        '''parse the libapparmor test_multi *.in tests and their expected result in *.out'''
108
 
 
109
 
        with open_file_read('%s.out' % file_with_path) as f_in:
110
 
            expected = f_in.readlines()
111
 
 
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]))
114
 
 
115
 
        expected.pop(0)
116
 
 
117
 
        exresult = dict()
118
 
        for line in expected:
119
 
            label, value = line.split(':', 1)
120
 
 
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()
126
 
 
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))
129
 
 
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'
135
 
 
136
 
        if exresult['event_type'] == 'AA_RECORD_INVALID':  # or exresult.get('error_code', 0) != 0:  # XXX should events with errors be ignored?
137
 
            exresult = None
138
 
 
139
 
        return exresult
140
 
 
141
 
 
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
146
 
 
147
 
    'testcase_mount_01',  # mount rules not yet supported in logparser
148
 
 
149
 
    'testcase_pivotroot_01',  # pivot_rot not yet supported in logparser
150
 
 
151
 
    # exec events
152
 
    'testcase01',
153
 
    'testcase12',
154
 
    'testcase13',
155
 
 
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',
176
 
]
177
 
 
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
181
 
 
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
184
 
 
185
 
    'testcase_changehat_01',  # interactive, asks to add a hat
186
 
]
187
 
 
188
 
class TestLogToProfile(AATest):
189
 
    '''Check if the libraries/libapparmor/testsuite/test_multi tests result in the expected profile'''
190
 
 
191
 
    tests = 'invalid'  # filled by parse_test_profiles()
192
 
 
193
 
    def _run_test(self, params, expected):
194
 
        logfile = '%s.in' % params
195
 
        profile_dummy_file = 'AATest_does_exist'
196
 
 
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))
200
 
 
201
 
        if not parsed_event:  # AA_RECORD_INVALID
202
 
            return
203
 
 
204
 
        if params.split('/')[-1] in log_to_profile_skip:
205
 
            return
206
 
 
207
 
        aamode = parsed_event['aamode']
208
 
 
209
 
        if aamode in['AUDIT', 'STATUS', 'HINT']: # ignore some event types  # XXX maybe we shouldn't ignore AUDIT events?
210
 
            return
211
 
 
212
 
        if aamode not in ['PERMITTING', 'REJECTING']:
213
 
            raise Exception('Unexpected aamode %s' % parsed_event['aamode'])
214
 
 
215
 
        # cleanup apparmor.aa storage
216
 
        apparmor.aa.log = dict()
217
 
        apparmor.aa.aa = apparmor.aa.hasher()
218
 
        apparmor.aa.prelog = apparmor.aa.hasher()
219
 
 
220
 
        profile = parsed_event['profile']
221
 
        hat = profile
222
 
        if '//' in profile:
223
 
            profile, hat = profile.split('//')
224
 
 
225
 
        apparmor.aa.existing_profiles = {profile: profile_dummy_file}
226
 
 
227
 
        log_reader = ReadLog(dict(), logfile, apparmor.aa.existing_profiles, '')
228
 
        log = log_reader.read_log('')
229
 
 
230
 
        for root in log:
231
 
            apparmor.aa.handle_children('', '', root)  # interactive for exec events!
232
 
 
233
 
        log_dict = apparmor.aa.collapse_log()
234
 
 
235
 
        apparmor.aa.filelist = apparmor.aa.hasher()
236
 
        apparmor.aa.filelist[profile_dummy_file]['profiles'][profile] = True
237
 
 
238
 
        new_profile = apparmor.aa.serialize_profile(log_dict[aamode][profile], profile, None)
239
 
 
240
 
        expected_profile = read_file('%s.profile' % params)
241
 
 
242
 
        if params.split('/')[-1] in log_to_profile_known_failures:
243
 
            self.assertNotEqual(new_profile, expected_profile)  # known failure
244
 
        else:
245
 
            self.assertEqual(new_profile, expected_profile)
246
 
 
247
 
 
248
 
def find_test_multi(log_dir):
249
 
    '''find all log sniplets in the given log_dir'''
250
 
 
251
 
    log_dir = os.path.abspath(log_dir)
252
 
 
253
 
    tests = []
254
 
    for root, dirs, files in os.walk(log_dir):
255
 
        for file in files:
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
259
 
 
260
 
            elif file.endswith('.out') or file.endswith('.err') or file.endswith('.profile'):
261
 
                pass
262
 
            else:
263
 
                raise Exception('Found unknown file %s in libapparmor test_multi' % file)
264
 
 
265
 
    return tests
266
 
 
267
 
 
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/')
271
 
 
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