~apparmor-dev/apparmor/apparmor-ubuntu-citrain-trusty

« back to all changes in this revision

Viewing changes to utils/apparmor/logparser.py

  • Committer: Seth Arnold
  • Date: 2014-03-12 02:05:16 UTC
  • mto: This revision was merged to the branch mainline in revision 1496.
  • Revision ID: seth.arnold@canonical.com-20140312020516-zjike3pmw6hi861h
[ Jamie Strandboge ]
 * debian/debhelper/dh_apparmor: exit with error if aa-easyprof does not
   exist
 * debian/control: drop Depends on apparmor-easyprof to Suggests for
   dh-apparmor
[ Seth Arnold, Jamie Strandboge, Steve Beattie, John Johansen, Tyler Hicks ]
* New upstream snapshot (LP: #1278702, #1061693, #1285653) dropping very
  large Ubuntu delta and fixing the following bugs:
  - Adjust fonts abstraction for libthai (LP: #1278702)
  - Support translated XDG user directories (LP: #1061693)
  - Adjust abstractions/web-data to include /var/www/html (LP: #1285653)
    Refresh 0002-add-debian-integration-to-lighttpd.patch to include
    /etc/lighttpd/conf-available/*.conf
  - Adjust debian/libapparmor1.symbols to reflect new upstream versioning
    for the aa_query_label() function
  - Raise exceptions in Python bindings when something fails
* ship new Python replacements for previous Perl-based tools
  - debian/apparmor-utils.install: remove usr/share/perl5/Immunix/*.pm and add
    usr/sbin/aa-autodep, usr/sbin/aa-cleanprof and usr/sbin/aa-mergeprof
  - debian/control:
    + remove various Perl dependencies
    + add python-apparmor and python3-apparmor
    + python3-apparmor Breaks: apparmor-easyprof to move the file since it
      ships dist-packages/apparmor/__init__.py now
  - debian/apparmor-utils.manpages: ship new manpages for aa-cleanprof and
    aa-mergeprof
  - debian/rules: build and install Python tools
* debian/apparmor.install:
  - install apparmorfs, dovecot, kernelvars, securityfs, sys,
    and xdg-user-dirs tunables and xdg-user-dirs.d directory
* debian/apparmor.dirs:
  - install /etc/apparmor.d/tunables/xdg-user-dirs.d
* debian/apparmor.postinst: create xdg-user-dirs.d
* debian/apparmor.postrm: remove xdg-user-dirs.d
* Remaining patches:
  - 0001-add-chromium-browser.patch
  - 0002-add-debian-integration-to-lighttpd.patch
  - 0003-ubuntu-manpage-updates.patch
  - 0004-libapparmor-layout-deb.patch (renamed from 0008)
  - 0005-libapparmor-mention-dbus-method-in-getcon-man.patch (renamed from
    0068)
  - 0006-etc-writable.patch (renamed from 0070)
  - 0007-aa-utils_are_bilingual.patch (renamed from 0077)
  - 0008-remove-ptrace.patch
  - 0009-convert-to-rules.patch
  - 0010-list-fns.patch
  - 0011-parse-mode.patch
  - 0012-add-decimal-interp.patch
  - 0013-policy_mediates.patch
  - 0014-fix-failpath.patch
  - 0015-feature_file.patch
  - 0016-fix-network.patch
  - 0017-aare-to-class.patch
  - 0018-add-mediation-unix.patch
  - 0019-parser_version.patch
  - 0020-caching.patch
  - 0021-label-class.patch
  - 0022-signal.patch
  - 0023-fix-lexer-debug.patch
  - 0024-ptrace.patch
  - 0025-use-diff-encode.patch
  - 0026-fix-serialize.patch
  - 0027-fix-af.patch
  - 0028-opt_arg.patch
  - 0029-tests-cond-dbus.patch
  - 0030-tests.diff
* Move manpages from libapparmor1 to libapparmor-dev
  - debian/libapparmor-dev.manpages: install aa_change_hat.2,
    aa_change_profile.2, aa_find_mountpoint.2, aa_getcon.2
  - debian/control: libapparmor-dev Replaces: and Breaks: libapparmor1
* Move /usr/lib/python3/dist-packages/apparmor/__init__.py from
  apparmor-easyprof to python3-apparmor
  - debian/control: python3-apparmor Breaks: apparmor-easyprof
  - debian/apparmor-easyprof.install: remove
    usr/lib/python*.*/site-packages/apparmor*
* New profiles and abstractions:
  - debian/apparmor.install: tunables/dovecot, tunables/kernelvars,
    tunables/xdg-user-dirs, tunables/xdg-user-dirs.d
* Test merge from upstream new pyutils branch (rev 2385)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# ----------------------------------------------------------------------
 
2
#    Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
 
3
#
 
4
#    This program is free software; you can redistribute it and/or
 
5
#    modify it under the terms of version 2 of the GNU General Public
 
6
#    License as published by the Free Software Foundation.
 
7
#
 
8
#    This program is distributed in the hope that it will be useful,
 
9
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
#    GNU General Public License for more details.
 
12
#
 
13
# ----------------------------------------------------------------------
 
14
import os
 
15
import re
 
16
import sys
 
17
import time
 
18
import LibAppArmor
 
19
from apparmor.common import AppArmorException, open_file_read, DebugLogger
 
20
 
 
21
from apparmor.aamode import validate_log_mode, log_str_to_mode, hide_log_mode, AA_MAY_EXEC
 
22
 
 
23
# setup module translations
 
24
from apparmor.translations import init_translation
 
25
_ = init_translation()
 
26
 
 
27
class ReadLog:
 
28
    RE_LOG_v2_6_syslog = re.compile('kernel:\s+(\[[\d\.\s]+\]\s+)?type=\d+\s+audit\([\d\.\:]+\):\s+apparmor=')
 
29
    RE_LOG_v2_6_audit = re.compile('type=AVC\s+(msg=)?audit\([\d\.\:]+\):\s+apparmor=')
 
30
    MODE_MAP_RE = re.compile('r|w|l|m|k|a|x|i|u|p|c|n|I|U|P|C|N')
 
31
    LOG_MODE_RE = re.compile('r|w|l|m|k|a|x|ix|ux|px|cx|nx|pix|cix|Ix|Ux|Px|PUx|Cx|Nx|Pix|Cix')
 
32
    PROFILE_MODE_RE = re.compile('r|w|l|m|k|a|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix')
 
33
    PROFILE_MODE_NT_RE = re.compile('r|w|l|m|k|a|x|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix')
 
34
    PROFILE_MODE_DENY_RE = re.compile('r|w|l|m|k|a|x')
 
35
    # Used by netdomain to identify the operation types
 
36
    # New socket names
 
37
    OPERATION_TYPES = {'create': 'net',
 
38
                       'post_create': 'net',
 
39
                       'bind': 'net',
 
40
                       'connect': 'net',
 
41
                       'listen': 'net',
 
42
                       'accept': 'net',
 
43
                       'sendmsg': 'net',
 
44
                       'recvmsg': 'net',
 
45
                       'getsockname': 'net',
 
46
                       'getpeername': 'net',
 
47
                       'getsockopt': 'net',
 
48
                       'setsockopt': 'net',
 
49
                       'sock_shutdown': 'net'
 
50
                       }
 
51
 
 
52
    def __init__(self, pid, filename, existing_profiles, profile_dir, log):
 
53
        self.filename = filename
 
54
        self.profile_dir = profile_dir
 
55
        self.pid = pid
 
56
        self.existing_profiles = existing_profiles
 
57
        self.log = log
 
58
        self.debug_logger = DebugLogger('ReadLog')
 
59
        self.LOG = None
 
60
        self.logmark = ''
 
61
        self.seenmark = None
 
62
        self.next_log_entry = None
 
63
 
 
64
    def prefetch_next_log_entry(self):
 
65
        if self.next_log_entry:
 
66
            sys.stderr.out('A log entry already present: %s' % self.next_log_entry)
 
67
        self.next_log_entry = self.LOG.readline()
 
68
        while not self.RE_LOG_v2_6_syslog.search(self.next_log_entry) and not self.RE_LOG_v2_6_audit.search(self.next_log_entry) and not (self.logmark and self.logmark in self.next_log_entry):
 
69
            self.next_log_entry = self.LOG.readline()
 
70
            if not self.next_log_entry:
 
71
                break
 
72
 
 
73
    def get_next_log_entry(self):
 
74
        # If no next log entry fetch it
 
75
        if not self.next_log_entry:
 
76
            self.prefetch_next_log_entry()
 
77
        log_entry = self.next_log_entry
 
78
        self.next_log_entry = None
 
79
        return log_entry
 
80
 
 
81
    def peek_at_next_log_entry(self):
 
82
        # Take a peek at the next log entry
 
83
        if not self.next_log_entry:
 
84
            self.prefetch_next_log_entry()
 
85
        return self.next_log_entry
 
86
 
 
87
    def throw_away_next_log_entry(self):
 
88
        self.next_log_entry = None
 
89
 
 
90
    def parse_log_record(self, record):
 
91
        self.debug_logger.debug('parse_log_record: %s' % record)
 
92
 
 
93
        record_event = self.parse_event(record)
 
94
        return record_event
 
95
 
 
96
    def parse_event(self, msg):
 
97
        """Parse the event from log into key value pairs"""
 
98
        msg = msg.strip()
 
99
        self.debug_logger.info('parse_event: %s' % msg)
 
100
        #print(repr(msg))
 
101
        if sys.version_info < (3, 0):
 
102
            # parse_record fails with u'foo' style strings hence typecasting to string
 
103
            msg = str(msg)
 
104
        event = LibAppArmor.parse_record(msg)
 
105
        ev = dict()
 
106
        ev['resource'] = event.info
 
107
        ev['active_hat'] = event.active_hat
 
108
        ev['aamode'] = event.event
 
109
        ev['time'] = event.epoch
 
110
        ev['operation'] = event.operation
 
111
        ev['profile'] = event.profile
 
112
        ev['name'] = event.name
 
113
        ev['name2'] = event.name2
 
114
        ev['attr'] = event.attribute
 
115
        ev['parent'] = event.parent
 
116
        ev['pid'] = event.pid
 
117
        ev['task'] = event.task
 
118
        ev['info'] = event.info
 
119
        dmask = event.denied_mask
 
120
        rmask = event.requested_mask
 
121
        ev['magic_token'] = event.magic_token
 
122
        if ev['operation'] and self.op_type(ev['operation']) == 'net':
 
123
            ev['family'] = event.net_family
 
124
            ev['protocol'] = event.net_protocol
 
125
            ev['sock_type'] = event.net_sock_type
 
126
        LibAppArmor.free_record(event)
 
127
        # Map c (create) to a and d (delete) to w, logprof doesn't support c and d
 
128
        if rmask:
 
129
            rmask = rmask.replace('c', 'a')
 
130
            rmask = rmask.replace('d', 'w')
 
131
            if not validate_log_mode(hide_log_mode(rmask)):
 
132
                raise AppArmorException(_('Log contains unknown mode %s') % rmask)
 
133
        if dmask:
 
134
            dmask = dmask.replace('c', 'a')
 
135
            dmask = dmask.replace('d', 'w')
 
136
            if not validate_log_mode(hide_log_mode(dmask)):
 
137
                raise AppArmorException(_('Log contains unknown mode %s') % dmask)
 
138
        #print('parse_event:', ev['profile'], dmask, ev['name2'])
 
139
        mask, name = log_str_to_mode(ev['profile'], dmask, ev['name2'])
 
140
 
 
141
        ev['denied_mask'] = mask
 
142
        ev['name2'] = name
 
143
 
 
144
        mask, name = log_str_to_mode(ev['profile'], rmask, ev['name2'])
 
145
        ev['request_mask'] = mask
 
146
        ev['name2'] = name
 
147
 
 
148
        if not ev['time']:
 
149
            ev['time'] = int(time.time())
 
150
        # Remove None keys
 
151
        #for key in ev.keys():
 
152
        #    if not ev[key] or not re.search('[\w]+', ev[key]):
 
153
        #        ev.pop(key)
 
154
 
 
155
        if ev['aamode']:
 
156
            # Convert aamode values to their counter-parts
 
157
            mode_convertor = {0: 'UNKNOWN',
 
158
                              1: 'ERROR',
 
159
                              2: 'AUDITING',
 
160
                              3: 'PERMITTING',
 
161
                              4: 'REJECTING',
 
162
                              5: 'HINT',
 
163
                              6: 'STATUS'
 
164
                              }
 
165
            try:
 
166
                ev['aamode'] = mode_convertor[ev['aamode']]
 
167
            except KeyError:
 
168
                ev['aamode'] = None
 
169
 
 
170
        if ev['aamode']:
 
171
            #debug_logger.debug(ev)
 
172
            return ev
 
173
        else:
 
174
            return None
 
175
 
 
176
    def add_to_tree(self, loc_pid, parent, type, event):
 
177
        self.debug_logger.info('add_to_tree: pid [%s] type [%s] event [%s]' % (loc_pid, type, event))
 
178
        if not self.pid.get(loc_pid, False):
 
179
            profile, hat = event[:2]
 
180
            if parent and self.pid.get(parent, False):
 
181
                if not hat:
 
182
                    hat = 'null-complain-profile'
 
183
                arrayref = []
 
184
                self.pid[parent].append(arrayref)
 
185
                self.pid[loc_pid] = arrayref
 
186
                for ia in ['fork', loc_pid, profile, hat]:
 
187
                    arrayref.append(ia)
 
188
#                 self.pid[parent].append(array_ref)
 
189
#                 self.pid[loc_pid] = array_ref
 
190
            else:
 
191
                arrayref = []
 
192
                self.log.append(arrayref)
 
193
                self.pid[loc_pid] = arrayref
 
194
#                 self.log.append(array_ref)
 
195
#                 self.pid[loc_pid] = array_ref
 
196
        self.pid[loc_pid].append([type, loc_pid] + event)
 
197
        #print("\n\npid",self.pid)
 
198
        #print("log",self.log)
 
199
 
 
200
    def add_event_to_tree(self, e):
 
201
        aamode = e.get('aamode', 'UNKNOWN')
 
202
        if e.get('type', False):
 
203
            if re.search('(UNKNOWN\[1501\]|APPARMOR_AUDIT|1501)', e['type']):
 
204
                aamode = 'AUDIT'
 
205
            elif re.search('(UNKNOWN\[1502\]|APPARMOR_ALLOWED|1502)', e['type']):
 
206
                aamode = 'PERMITTING'
 
207
            elif re.search('(UNKNOWN\[1503\]|APPARMOR_DENIED|1503)', e['type']):
 
208
                aamode = 'REJECTING'
 
209
            elif re.search('(UNKNOWN\[1504\]|APPARMOR_HINT|1504)', e['type']):
 
210
                aamode = 'HINT'
 
211
            elif re.search('(UNKNOWN\[1505\]|APPARMOR_STATUS|1505)', e['type']):
 
212
                aamode = 'STATUS'
 
213
            elif re.search('(UNKNOWN\[1506\]|APPARMOR_ERROR|1506)', e['type']):
 
214
                aamode = 'ERROR'
 
215
            else:
 
216
                aamode = 'UNKNOWN'
 
217
 
 
218
        if aamode in ['UNKNOWN', 'AUDIT', 'STATUS', 'ERROR']:
 
219
            return None
 
220
 
 
221
        if 'profile_set' in e['operation']:
 
222
            return None
 
223
 
 
224
        # Skip if AUDIT event was issued due to a change_hat in unconfined mode
 
225
        if not e.get('profile', False):
 
226
            return None
 
227
 
 
228
        # Convert new null profiles to old single level null profile
 
229
        if '//null-' in e['profile']:
 
230
            e['profile'] = 'null-complain-profile'
 
231
 
 
232
        profile = e['profile']
 
233
        hat = None
 
234
 
 
235
        if '//' in e['profile']:
 
236
            profile, hat = e['profile'].split('//')[:2]
 
237
 
 
238
        # Filter out change_hat events that aren't from learning
 
239
        if e['operation'] == 'change_hat':
 
240
            if aamode != 'HINT' and aamode != 'PERMITTING':
 
241
                return None
 
242
            profile = e['name']
 
243
            #hat = None
 
244
            if '//' in e['name']:
 
245
                profile, hat = e['name'].split('//')[:2]
 
246
 
 
247
        if not hat:
 
248
            hat = profile
 
249
 
 
250
        # prog is no longer passed around consistently
 
251
        prog = 'HINT'
 
252
 
 
253
        if profile != 'null-complain-profile' and not self.profile_exists(profile):
 
254
            return None
 
255
        if e['operation'] == 'exec':
 
256
            if e.get('info', False) and e['info'] == 'mandatory profile missing':
 
257
                self.add_to_tree(e['pid'], e['parent'], 'exec',
 
258
                                 [profile, hat, aamode, 'PERMITTING', e['denied_mask'], e['name'], e['name2']])
 
259
            elif e.get('name2', False) and '\\null-/' in e['name2']:
 
260
                self.add_to_tree(e['pid'], e['parent'], 'exec',
 
261
                                 [profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
 
262
            elif e.get('name', False):
 
263
                self.add_to_tree(e['pid'], e['parent'], 'exec',
 
264
                                 [profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
 
265
            else:
 
266
                self.debug_logger.debug('add_event_to_tree: dropped exec event in %s' % e['profile'])
 
267
 
 
268
        elif 'file_' in e['operation']:
 
269
            self.add_to_tree(e['pid'], e['parent'], 'path',
 
270
                             [profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
 
271
        elif e['operation'] in ['open', 'truncate', 'mkdir', 'mknod', 'rename_src',
 
272
                                'rename_dest', 'unlink', 'rmdir', 'symlink_create', 'link']:
 
273
            #print(e['operation'], e['name'])
 
274
            self.add_to_tree(e['pid'], e['parent'], 'path',
 
275
                             [profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
 
276
        elif e['operation'] == 'capable':
 
277
            self.add_to_tree(e['pid'], e['parent'], 'capability',
 
278
                             [profile, hat, prog, aamode, e['name'], ''])
 
279
        elif e['operation'] == 'setattr' or 'xattr' in e['operation']:
 
280
            self.add_to_tree(e['pid'], e['parent'], 'path',
 
281
                             [profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
 
282
        elif 'inode_' in e['operation']:
 
283
            is_domain_change = False
 
284
            if e['operation'] == 'inode_permission' and (e['denied_mask'] & AA_MAY_EXEC) and aamode == 'PERMITTING':
 
285
                following = self.peek_at_next_log_entry()
 
286
                if following:
 
287
                    entry = self.parse_log_record(following)
 
288
                    if entry and entry.get('info', False) == 'set profile':
 
289
                        is_domain_change = True
 
290
                        self.throw_away_next_log_entry()
 
291
 
 
292
            if is_domain_change:
 
293
                self.add_to_tree(e['pid'], e['parent'], 'exec',
 
294
                                 [profile, hat, prog, aamode, e['denied_mask'], e['name'], e['name2']])
 
295
            else:
 
296
                self.add_to_tree(e['pid'], e['parent'], 'path',
 
297
                                 [profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
 
298
 
 
299
        elif e['operation'] == 'sysctl':
 
300
            self.add_to_tree(e['pid'], e['parent'], 'path',
 
301
                             [profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
 
302
 
 
303
        elif e['operation'] == 'clone':
 
304
            parent, child = e['pid'], e['task']
 
305
            if not parent:
 
306
                parent = 'null-complain-profile'
 
307
            if not hat:
 
308
                hat = 'null-complain-profile'
 
309
            arrayref = []
 
310
            if self.pid.get(parent, False):
 
311
                self.pid[parent].append(arrayref)
 
312
            else:
 
313
                self.log.append(arrayref)
 
314
            self.pid[child].append(arrayref)
 
315
            for ia in ['fork', child, profile, hat]:
 
316
                arrayref.append(ia)
 
317
#             if self.pid.get(parent, False):
 
318
#                 self.pid[parent] += [arrayref]
 
319
#             else:
 
320
#                 self.log += [arrayref]
 
321
#             self.pid[child] = arrayref
 
322
 
 
323
        elif self.op_type(e['operation']) == 'net':
 
324
            self.add_to_tree(e['pid'], e['parent'], 'netdomain',
 
325
                             [profile, hat, prog, aamode, e['family'], e['sock_type'], e['protocol']])
 
326
        elif e['operation'] == 'change_hat':
 
327
            self.add_to_tree(e['pid'], e['parent'], 'unknown_hat',
 
328
                             [profile, hat, aamode, hat])
 
329
        else:
 
330
            self.debug_logger.debug('UNHANDLED: %s' % e)
 
331
 
 
332
    def read_log(self, logmark):
 
333
        self.logmark = logmark
 
334
        seenmark = True
 
335
        if self.logmark:
 
336
            seenmark = False
 
337
        #last = None
 
338
        #event_type = None
 
339
        try:
 
340
            #print(self.filename)
 
341
            self.LOG = open_file_read(self.filename)
 
342
        except IOError:
 
343
            raise AppArmorException('Can not read AppArmor logfile: ' + self.filename)
 
344
        #LOG = open_file_read(log_open)
 
345
        line = True
 
346
        while line:
 
347
            line = self.get_next_log_entry()
 
348
            if not line:
 
349
                break
 
350
            line = line.strip()
 
351
            self.debug_logger.debug('read_log: %s' % line)
 
352
            if self.logmark in line:
 
353
                seenmark = True
 
354
 
 
355
            self.debug_logger.debug('read_log: seenmark = %s' % seenmark)
 
356
            if not seenmark:
 
357
                continue
 
358
 
 
359
            event = self.parse_log_record(line)
 
360
            #print(event)
 
361
            if event:
 
362
                self.add_event_to_tree(event)
 
363
        self.LOG.close()
 
364
        self.logmark = ''
 
365
        return self.log
 
366
 
 
367
    def op_type(self, operation):
 
368
        """Returns the operation type if known, unkown otherwise"""
 
369
        operation_type = self.OPERATION_TYPES.get(operation, 'unknown')
 
370
        return operation_type
 
371
 
 
372
    def profile_exists(self, program):
 
373
        """Returns True if profile exists, False otherwise"""
 
374
        # Check cache of profiles
 
375
        if self.existing_profiles.get(program, False):
 
376
            return True
 
377
        # Check the disk for profile
 
378
        prof_path = self.get_profile_filename(program)
 
379
        #print(prof_path)
 
380
        if os.path.isfile(prof_path):
 
381
            # Add to cache of profile
 
382
            self.existing_profiles[program] = prof_path
 
383
            return True
 
384
        return False
 
385
 
 
386
    def get_profile_filename(self, profile):
 
387
        """Returns the full profile name"""
 
388
        if profile.startswith('/'):
 
389
            # Remove leading /
 
390
            profile = profile[1:]
 
391
        else:
 
392
            profile = "profile_" + profile
 
393
        profile = profile.replace('/', '.')
 
394
        full_profilename = self.profile_dir + '/' + profile
 
395
        return full_profilename