1
# ----------------------------------------------------------------------
2
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
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.
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.
13
# ----------------------------------------------------------------------
19
from apparmor.common import AppArmorException, open_file_read, DebugLogger
21
from apparmor.aamode import validate_log_mode, log_str_to_mode, hide_log_mode, AA_MAY_EXEC
23
# setup module translations
24
from apparmor.translations import init_translation
25
_ = init_translation()
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
37
OPERATION_TYPES = {'create': 'net',
49
'sock_shutdown': 'net'
52
def __init__(self, pid, filename, existing_profiles, profile_dir, log):
53
self.filename = filename
54
self.profile_dir = profile_dir
56
self.existing_profiles = existing_profiles
58
self.debug_logger = DebugLogger('ReadLog')
62
self.next_log_entry = None
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:
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
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
87
def throw_away_next_log_entry(self):
88
self.next_log_entry = None
90
def parse_log_record(self, record):
91
self.debug_logger.debug('parse_log_record: %s' % record)
93
record_event = self.parse_event(record)
96
def parse_event(self, msg):
97
"""Parse the event from log into key value pairs"""
99
self.debug_logger.info('parse_event: %s' % msg)
101
if sys.version_info < (3, 0):
102
# parse_record fails with u'foo' style strings hence typecasting to string
104
event = LibAppArmor.parse_record(msg)
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
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)
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'])
141
ev['denied_mask'] = mask
144
mask, name = log_str_to_mode(ev['profile'], rmask, ev['name2'])
145
ev['request_mask'] = mask
149
ev['time'] = int(time.time())
151
#for key in ev.keys():
152
# if not ev[key] or not re.search('[\w]+', ev[key]):
156
# Convert aamode values to their counter-parts
157
mode_convertor = {0: 'UNKNOWN',
166
ev['aamode'] = mode_convertor[ev['aamode']]
171
#debug_logger.debug(ev)
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):
182
hat = 'null-complain-profile'
184
self.pid[parent].append(arrayref)
185
self.pid[loc_pid] = arrayref
186
for ia in ['fork', loc_pid, profile, hat]:
188
# self.pid[parent].append(array_ref)
189
# self.pid[loc_pid] = array_ref
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)
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']):
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']):
209
elif re.search('(UNKNOWN\[1504\]|APPARMOR_HINT|1504)', e['type']):
211
elif re.search('(UNKNOWN\[1505\]|APPARMOR_STATUS|1505)', e['type']):
213
elif re.search('(UNKNOWN\[1506\]|APPARMOR_ERROR|1506)', e['type']):
218
if aamode in ['UNKNOWN', 'AUDIT', 'STATUS', 'ERROR']:
221
if 'profile_set' in e['operation']:
224
# Skip if AUDIT event was issued due to a change_hat in unconfined mode
225
if not e.get('profile', False):
228
# Convert new null profiles to old single level null profile
229
if '//null-' in e['profile']:
230
e['profile'] = 'null-complain-profile'
232
profile = e['profile']
235
if '//' in e['profile']:
236
profile, hat = e['profile'].split('//')[:2]
238
# Filter out change_hat events that aren't from learning
239
if e['operation'] == 'change_hat':
240
if aamode != 'HINT' and aamode != 'PERMITTING':
244
if '//' in e['name']:
245
profile, hat = e['name'].split('//')[:2]
250
# prog is no longer passed around consistently
253
if profile != 'null-complain-profile' and not self.profile_exists(profile):
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'], ''])
266
self.debug_logger.debug('add_event_to_tree: dropped exec event in %s' % e['profile'])
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()
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()
293
self.add_to_tree(e['pid'], e['parent'], 'exec',
294
[profile, hat, prog, aamode, e['denied_mask'], e['name'], e['name2']])
296
self.add_to_tree(e['pid'], e['parent'], 'path',
297
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
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'], ''])
303
elif e['operation'] == 'clone':
304
parent, child = e['pid'], e['task']
306
parent = 'null-complain-profile'
308
hat = 'null-complain-profile'
310
if self.pid.get(parent, False):
311
self.pid[parent].append(arrayref)
313
self.log.append(arrayref)
314
self.pid[child].append(arrayref)
315
for ia in ['fork', child, profile, hat]:
317
# if self.pid.get(parent, False):
318
# self.pid[parent] += [arrayref]
320
# self.log += [arrayref]
321
# self.pid[child] = arrayref
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])
330
self.debug_logger.debug('UNHANDLED: %s' % e)
332
def read_log(self, logmark):
333
self.logmark = logmark
340
#print(self.filename)
341
self.LOG = open_file_read(self.filename)
343
raise AppArmorException('Can not read AppArmor logfile: ' + self.filename)
344
#LOG = open_file_read(log_open)
347
line = self.get_next_log_entry()
351
self.debug_logger.debug('read_log: %s' % line)
352
if self.logmark in line:
355
self.debug_logger.debug('read_log: seenmark = %s' % seenmark)
359
event = self.parse_log_record(line)
362
self.add_event_to_tree(event)
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
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):
377
# Check the disk for profile
378
prof_path = self.get_profile_filename(program)
380
if os.path.isfile(prof_path):
381
# Add to cache of profile
382
self.existing_profiles[program] = prof_path
386
def get_profile_filename(self, profile):
387
"""Returns the full profile name"""
388
if profile.startswith('/'):
390
profile = profile[1:]
392
profile = "profile_" + profile
393
profile = profile.replace('/', '.')
394
full_profilename = self.profile_dir + '/' + profile
395
return full_profilename