blob: 724d3ea612ece8b351faf286f1679cbf4ce76140 [file] [log] [blame]
Jeff Vander Stoep74e4f932016-02-08 15:27:10 -08001# Authors: Karl MacMillan <kmacmillan@mentalrootkit.com>
2#
3# Copyright (C) 2006 Red Hat
4# see file 'COPYING' for use and warranty information
5#
6# This program is free software; you can redistribute it and/or
7# modify it under the terms of the GNU General Public License as
8# published by the Free Software Foundation; version 2 only
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18#
19
20import re
21import sys
22
23from . import refpolicy
24from . import access
25from . import util
26# Convenience functions
27
28def get_audit_boot_msgs():
29 """Obtain all of the avc and policy load messages from the audit
30 log. This function uses ausearch and requires that the current
31 process have sufficient rights to run ausearch.
32
33 Returns:
34 string contain all of the audit messages returned by ausearch.
35 """
36 import subprocess
37 import time
38 fd=open("/proc/uptime", "r")
39 off=float(fd.read().split()[0])
40 fd.close
41 s = time.localtime(time.time() - off)
42 bootdate = time.strftime("%x", s)
43 boottime = time.strftime("%X", s)
44 output = subprocess.Popen(["/sbin/ausearch", "-m", "AVC,USER_AVC,MAC_POLICY_LOAD,DAEMON_START,SELINUX_ERR", "-ts", bootdate, boottime],
45 stdout=subprocess.PIPE).communicate()[0]
46 if util.PY3:
47 output = util.decode_input(output)
48 return output
49
50def get_audit_msgs():
51 """Obtain all of the avc and policy load messages from the audit
52 log. This function uses ausearch and requires that the current
53 process have sufficient rights to run ausearch.
54
55 Returns:
56 string contain all of the audit messages returned by ausearch.
57 """
58 import subprocess
59 output = subprocess.Popen(["/sbin/ausearch", "-m", "AVC,USER_AVC,MAC_POLICY_LOAD,DAEMON_START,SELINUX_ERR"],
60 stdout=subprocess.PIPE).communicate()[0]
61 if util.PY3:
62 output = util.decode_input(output)
63 return output
64
65def get_dmesg_msgs():
66 """Obtain all of the avc and policy load messages from /bin/dmesg.
67
68 Returns:
69 string contain all of the audit messages returned by dmesg.
70 """
71 import subprocess
72 output = subprocess.Popen(["/bin/dmesg"],
73 stdout=subprocess.PIPE).communicate()[0]
74 if util.PY3:
75 output = util.decode_input(output)
76 return output
77
78# Classes representing audit messages
79
80class AuditMessage:
81 """Base class for all objects representing audit messages.
82
83 AuditMessage is a base class for all audit messages and only
84 provides storage for the raw message (as a string) and a
85 parsing function that does nothing.
86 """
87 def __init__(self, message):
88 self.message = message
89 self.header = ""
90
91 def from_split_string(self, recs):
92 """Parse a string that has been split into records by space into
93 an audit message.
94
95 This method should be overridden by subclasses. Error reporting
96 should be done by raise ValueError exceptions.
97 """
98 for msg in recs:
99 fields = msg.split("=")
100 if len(fields) != 2:
101 if msg[:6] == "audit(":
102 self.header = msg
103 return
104 else:
105 continue
106
107 if fields[0] == "msg":
108 self.header = fields[1]
109 return
110
111
112class InvalidMessage(AuditMessage):
113 """Class representing invalid audit messages. This is used to differentiate
114 between audit messages that aren't recognized (that should return None from
115 the audit message parser) and a message that is recognized but is malformed
116 in some way.
117 """
118 def __init__(self, message):
119 AuditMessage.__init__(self, message)
120
121class PathMessage(AuditMessage):
122 """Class representing a path message"""
123 def __init__(self, message):
124 AuditMessage.__init__(self, message)
125 self.path = ""
126
127 def from_split_string(self, recs):
128 AuditMessage.from_split_string(self, recs)
129
130 for msg in recs:
131 fields = msg.split("=")
132 if len(fields) != 2:
133 continue
134 if fields[0] == "path":
135 self.path = fields[1][1:-1]
136 return
137import selinux.audit2why as audit2why
138
139avcdict = {}
140
141class AVCMessage(AuditMessage):
142 """AVC message representing an access denial or granted message.
143
144 This is a very basic class and does not represent all possible fields
145 in an avc message. Currently the fields are:
146 scontext - context for the source (process) that generated the message
147 tcontext - context for the target
148 tclass - object class for the target (only one)
149 comm - the process name
150 exe - the on-disc binary
151 path - the path of the target
152 access - list of accesses that were allowed or denied
153 denial - boolean indicating whether this was a denial (True) or granted
154 (False) message.
155
156 An example audit message generated from the audit daemon looks like (line breaks
157 added):
158 'type=AVC msg=audit(1155568085.407:10877): avc: denied { search } for
159 pid=677 comm="python" name="modules" dev=dm-0 ino=13716388
160 scontext=user_u:system_r:setroubleshootd_t:s0
161 tcontext=system_u:object_r:modules_object_t:s0 tclass=dir'
162
163 An example audit message stored in syslog (not processed by the audit daemon - line
164 breaks added):
165 'Sep 12 08:26:43 dhcp83-5 kernel: audit(1158064002.046:4): avc: denied { read }
166 for pid=2 496 comm="bluez-pin" name=".gdm1K3IFT" dev=dm-0 ino=3601333
167 scontext=user_u:system_r:bluetooth_helper_t:s0-s0:c0
168 tcontext=system_u:object_r:xdm_tmp_t:s0 tclass=file
169 """
170 def __init__(self, message):
171 AuditMessage.__init__(self, message)
172 self.scontext = refpolicy.SecurityContext()
173 self.tcontext = refpolicy.SecurityContext()
174 self.tclass = ""
175 self.comm = ""
176 self.exe = ""
177 self.path = ""
178 self.name = ""
179 self.accesses = []
180 self.denial = True
181 self.type = audit2why.TERULE
182
183 def __parse_access(self, recs, start):
184 # This is kind of sucky - the access that is in a space separated
185 # list like '{ read write }'. This doesn't fit particularly well with splitting
186 # the string on spaces. This function takes the list of recs and a starting
187 # position one beyond the open brace. It then adds the accesses until it finds
188 # the close brace or the end of the list (which is an error if reached without
189 # seeing a close brace).
190 found_close = False
191 i = start
192 if i == (len(recs) - 1):
193 raise ValueError("AVC message in invalid format [%s]\n" % self.message)
194 while i < len(recs):
195 if recs[i] == "}":
196 found_close = True
197 break
198 self.accesses.append(recs[i])
199 i = i + 1
200 if not found_close:
201 raise ValueError("AVC message in invalid format [%s]\n" % self.message)
202 return i + 1
203
204
205 def from_split_string(self, recs):
206 AuditMessage.from_split_string(self, recs)
207 # FUTURE - fully parse avc messages and store all possible fields
208 # Required fields
209 found_src = False
210 found_tgt = False
211 found_class = False
212 found_access = False
213
214 for i in range(len(recs)):
215 if recs[i] == "{":
216 i = self.__parse_access(recs, i + 1)
217 found_access = True
218 continue
219 elif recs[i] == "granted":
220 self.denial = False
221
222 fields = recs[i].split("=")
223 if len(fields) != 2:
224 continue
225 if fields[0] == "scontext":
226 self.scontext = refpolicy.SecurityContext(fields[1])
227 found_src = True
228 elif fields[0] == "tcontext":
229 self.tcontext = refpolicy.SecurityContext(fields[1])
230 found_tgt = True
231 elif fields[0] == "tclass":
232 self.tclass = fields[1]
233 found_class = True
234 elif fields[0] == "comm":
235 self.comm = fields[1][1:-1]
236 elif fields[0] == "exe":
237 self.exe = fields[1][1:-1]
238 elif fields[0] == "name":
239 self.name = fields[1][1:-1]
240
241 if not found_src or not found_tgt or not found_class or not found_access:
242 raise ValueError("AVC message in invalid format [%s]\n" % self.message)
243 self.analyze()
244
245 def analyze(self):
246 tcontext = self.tcontext.to_string()
247 scontext = self.scontext.to_string()
248 access_tuple = tuple( self.accesses)
249 self.data = []
250
251 if (scontext, tcontext, self.tclass, access_tuple) in avcdict.keys():
252 self.type, self.data = avcdict[(scontext, tcontext, self.tclass, access_tuple)]
253 else:
254 self.type, self.data = audit2why.analyze(scontext, tcontext, self.tclass, self.accesses);
255 if self.type == audit2why.NOPOLICY:
256 self.type = audit2why.TERULE
257 if self.type == audit2why.BADTCON:
258 raise ValueError("Invalid Target Context %s\n" % tcontext)
259 if self.type == audit2why.BADSCON:
260 raise ValueError("Invalid Source Context %s\n" % scontext)
261 if self.type == audit2why.BADSCON:
262 raise ValueError("Invalid Type Class %s\n" % self.tclass)
263 if self.type == audit2why.BADPERM:
264 raise ValueError("Invalid permission %s\n" % " ".join(self.accesses))
265 if self.type == audit2why.BADCOMPUTE:
266 raise ValueError("Error during access vector computation")
267
268 if self.type == audit2why.CONSTRAINT:
269 self.data = [ self.data ]
270 if self.scontext.user != self.tcontext.user:
271 self.data.append(("user (%s)" % self.scontext.user, 'user (%s)' % self.tcontext.user))
272 if self.scontext.role != self.tcontext.role and self.tcontext.role != "object_r":
273 self.data.append(("role (%s)" % self.scontext.role, 'role (%s)' % self.tcontext.role))
274 if self.scontext.level != self.tcontext.level:
275 self.data.append(("level (%s)" % self.scontext.level, 'level (%s)' % self.tcontext.level))
276
277 avcdict[(scontext, tcontext, self.tclass, access_tuple)] = (self.type, self.data)
278
279class PolicyLoadMessage(AuditMessage):
280 """Audit message indicating that the policy was reloaded."""
281 def __init__(self, message):
282 AuditMessage.__init__(self, message)
283
284class DaemonStartMessage(AuditMessage):
285 """Audit message indicating that a daemon was started."""
286 def __init__(self, message):
287 AuditMessage.__init__(self, message)
288 self.auditd = False
289
290 def from_split_string(self, recs):
291 AuditMessage.from_split_string(self, recs)
292 if "auditd" in recs:
293 self.auditd = True
294
295
296class ComputeSidMessage(AuditMessage):
297 """Audit message indicating that a sid was not valid.
298
299 Compute sid messages are generated on attempting to create a security
300 context that is not valid. Security contexts are invalid if the role is
301 not authorized for the user or the type is not authorized for the role.
302
303 This class does not store all of the fields from the compute sid message -
304 just the type and role.
305 """
306 def __init__(self, message):
307 AuditMessage.__init__(self, message)
308 self.invalid_context = refpolicy.SecurityContext()
309 self.scontext = refpolicy.SecurityContext()
310 self.tcontext = refpolicy.SecurityContext()
311 self.tclass = ""
312
313 def from_split_string(self, recs):
314 AuditMessage.from_split_string(self, recs)
315 if len(recs) < 10:
316 raise ValueError("Split string does not represent a valid compute sid message")
317
318 try:
319 self.invalid_context = refpolicy.SecurityContext(recs[5])
320 self.scontext = refpolicy.SecurityContext(recs[7].split("=")[1])
321 self.tcontext = refpolicy.SecurityContext(recs[8].split("=")[1])
322 self.tclass = recs[9].split("=")[1]
323 except:
324 raise ValueError("Split string does not represent a valid compute sid message")
325 def output(self):
326 return "role %s types %s;\n" % (self.role, self.type)
327
328# Parser for audit messages
329
330class AuditParser:
331 """Parser for audit messages.
332
333 This class parses audit messages and stores them according to their message
334 type. This is not a general purpose audit message parser - it only extracts
335 selinux related messages.
336
337 Each audit messages are stored in one of four lists:
338 avc_msgs - avc denial or granted messages. Messages are stored in
339 AVCMessage objects.
340 comput_sid_messages - invalid sid messages. Messages are stored in
341 ComputSidMessage objects.
342 invalid_msgs - selinux related messages that are not valid. Messages
343 are stored in InvalidMessageObjects.
344 policy_load_messages - policy load messages. Messages are stored in
345 PolicyLoadMessage objects.
346
347 These lists will be reset when a policy load message is seen if
348 AuditParser.last_load_only is set to true. It is assumed that messages
349 are fed to the parser in chronological order - time stamps are not
350 parsed.
351 """
352 def __init__(self, last_load_only=False):
353 self.__initialize()
354 self.last_load_only = last_load_only
355
356 def __initialize(self):
357 self.avc_msgs = []
358 self.compute_sid_msgs = []
359 self.invalid_msgs = []
360 self.policy_load_msgs = []
361 self.path_msgs = []
362 self.by_header = { }
363 self.check_input_file = False
364
365 # Low-level parsing function - tries to determine if this audit
366 # message is an SELinux related message and then parses it into
367 # the appropriate AuditMessage subclass. This function deliberately
368 # does not impose policy (e.g., on policy load message) or store
369 # messages to make as simple and reusable as possible.
370 #
371 # Return values:
372 # None - no recognized audit message found in this line
373 #
374 # InvalidMessage - a recognized but invalid message was found.
375 #
376 # AuditMessage (or subclass) - object representing a parsed
377 # and valid audit message.
378 def __parse_line(self, line):
379 rec = line.split()
380 for i in rec:
381 found = False
382 if i == "avc:" or i == "message=avc:" or i == "msg='avc:":
383 msg = AVCMessage(line)
384 found = True
385 elif i == "security_compute_sid:":
386 msg = ComputeSidMessage(line)
387 found = True
388 elif i == "type=MAC_POLICY_LOAD" or i == "type=1403":
389 msg = PolicyLoadMessage(line)
390 found = True
391 elif i == "type=AVC_PATH":
392 msg = PathMessage(line)
393 found = True
394 elif i == "type=DAEMON_START":
395 msg = DaemonStartMessage(list)
396 found = True
397
398 if found:
399 self.check_input_file = True
400 try:
401 msg.from_split_string(rec)
402 except ValueError:
403 msg = InvalidMessage(line)
404 return msg
405 return None
406
407 # Higher-level parse function - take a line, parse it into an
408 # AuditMessage object, and store it in the appropriate list.
409 # This function will optionally reset all of the lists when
410 # it sees a load policy message depending on the value of
411 # self.last_load_only.
412 def __parse(self, line):
413 msg = self.__parse_line(line)
414 if msg is None:
415 return
416
417 # Append to the correct list
418 if isinstance(msg, PolicyLoadMessage):
419 if self.last_load_only:
420 self.__initialize()
421 elif isinstance(msg, DaemonStartMessage):
422 # We initialize every time the auditd is started. This
423 # is less than ideal, but unfortunately it is the only
424 # way to catch reboots since the initial policy load
425 # by init is not stored in the audit log.
426 if msg.auditd and self.last_load_only:
427 self.__initialize()
428 self.policy_load_msgs.append(msg)
429 elif isinstance(msg, AVCMessage):
430 self.avc_msgs.append(msg)
431 elif isinstance(msg, ComputeSidMessage):
432 self.compute_sid_msgs.append(msg)
433 elif isinstance(msg, InvalidMessage):
434 self.invalid_msgs.append(msg)
435 elif isinstance(msg, PathMessage):
436 self.path_msgs.append(msg)
437
438 # Group by audit header
439 if msg.header != "":
440 if msg.header in self.by_header:
441 self.by_header[msg.header].append(msg)
442 else:
443 self.by_header[msg.header] = [msg]
444
445
446 # Post processing will add additional information from AVC messages
447 # from related messages - only works on messages generated by
448 # the audit system.
449 def __post_process(self):
450 for value in self.by_header.values():
451 avc = []
452 path = None
453 for msg in value:
454 if isinstance(msg, PathMessage):
455 path = msg
456 elif isinstance(msg, AVCMessage):
457 avc.append(msg)
458 if len(avc) > 0 and path:
459 for a in avc:
460 a.path = path.path
461
462 def parse_file(self, input):
463 """Parse the contents of a file object. This method can be called
464 multiple times (along with parse_string)."""
465 line = input.readline()
466 while line:
467 self.__parse(line)
468 line = input.readline()
469 if not self.check_input_file:
470 sys.stderr.write("Nothing to do\n")
471 sys.exit(0)
472 self.__post_process()
473
474 def parse_string(self, input):
475 """Parse a string containing audit messages - messages should
476 be separated by new lines. This method can be called multiple
477 times (along with parse_file)."""
478 lines = input.split('\n')
479 for l in lines:
480 self.__parse(l)
481 self.__post_process()
482
483 def to_role(self, role_filter=None):
484 """Return RoleAllowSet statements matching the specified filter
485
486 Filter out types that match the filer, or all roles
487
488 Params:
489 role_filter - [optional] Filter object used to filter the
490 output.
491 Returns:
492 Access vector set representing the denied access in the
493 audit logs parsed by this object.
494 """
495 role_types = access.RoleTypeSet()
496 for cs in self.compute_sid_msgs:
497 if not role_filter or role_filter.filter(cs):
498 role_types.add(cs.invalid_context.role, cs.invalid_context.type)
499
500 return role_types
501
502 def to_access(self, avc_filter=None, only_denials=True):
503 """Convert the audit logs access into a an access vector set.
504
505 Convert the audit logs into an access vector set, optionally
506 filtering the restults with the passed in filter object.
507
508 Filter objects are object instances with a .filter method
509 that takes and access vector and returns True if the message
510 should be included in the final output and False otherwise.
511
512 Params:
513 avc_filter - [optional] Filter object used to filter the
514 output.
515 Returns:
516 Access vector set representing the denied access in the
517 audit logs parsed by this object.
518 """
519 av_set = access.AccessVectorSet()
520 for avc in self.avc_msgs:
521 if avc.denial != True and only_denials:
522 continue
523 if avc_filter:
524 if avc_filter.filter(avc):
525 av_set.add(avc.scontext.type, avc.tcontext.type, avc.tclass,
526 avc.accesses, avc, avc_type=avc.type, data=avc.data)
527 else:
528 av_set.add(avc.scontext.type, avc.tcontext.type, avc.tclass,
529 avc.accesses, avc, avc_type=avc.type, data=avc.data)
530 return av_set
531
532class AVCTypeFilter:
533 def __init__(self, regex):
534 self.regex = re.compile(regex)
535
536 def filter(self, avc):
537 if self.regex.match(avc.scontext.type):
538 return True
539 if self.regex.match(avc.tcontext.type):
540 return True
541 return False
542
543class ComputeSidTypeFilter:
544 def __init__(self, regex):
545 self.regex = re.compile(regex)
546
547 def filter(self, avc):
548 if self.regex.match(avc.invalid_context.type):
549 return True
550 if self.regex.match(avc.scontext.type):
551 return True
552 if self.regex.match(avc.tcontext.type):
553 return True
554 return False
555
556