1# 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 ioctlcmd - ioctl 'request' parameter 156 157 An example audit message generated from the audit daemon looks like (line breaks 158 added): 159 'type=AVC msg=audit(1155568085.407:10877): avc: denied { search } for 160 pid=677 comm="python" name="modules" dev=dm-0 ino=13716388 161 scontext=user_u:system_r:setroubleshootd_t:s0 162 tcontext=system_u:object_r:modules_object_t:s0 tclass=dir' 163 164 An example audit message stored in syslog (not processed by the audit daemon - line 165 breaks added): 166 'Sep 12 08:26:43 dhcp83-5 kernel: audit(1158064002.046:4): avc: denied { read } 167 for pid=2 496 comm="bluez-pin" name=".gdm1K3IFT" dev=dm-0 ino=3601333 168 scontext=user_u:system_r:bluetooth_helper_t:s0-s0:c0 169 tcontext=system_u:object_r:xdm_tmp_t:s0 tclass=file 170 """ 171 def __init__(self, message): 172 AuditMessage.__init__(self, message) 173 self.scontext = refpolicy.SecurityContext() 174 self.tcontext = refpolicy.SecurityContext() 175 self.tclass = "" 176 self.comm = "" 177 self.exe = "" 178 self.path = "" 179 self.name = "" 180 self.accesses = [] 181 self.denial = True 182 self.ioctlcmd = None 183 self.type = audit2why.TERULE 184 185 def __parse_access(self, recs, start): 186 # This is kind of sucky - the access that is in a space separated 187 # list like '{ read write }'. This doesn't fit particularly well with splitting 188 # the string on spaces. This function takes the list of recs and a starting 189 # position one beyond the open brace. It then adds the accesses until it finds 190 # the close brace or the end of the list (which is an error if reached without 191 # seeing a close brace). 192 found_close = False 193 i = start 194 if i == (len(recs) - 1): 195 raise ValueError("AVC message in invalid format [%s]\n" % self.message) 196 while i < len(recs): 197 if recs[i] == "}": 198 found_close = True 199 break 200 self.accesses.append(recs[i]) 201 i = i + 1 202 if not found_close: 203 raise ValueError("AVC message in invalid format [%s]\n" % self.message) 204 return i + 1 205 206 207 def from_split_string(self, recs): 208 AuditMessage.from_split_string(self, recs) 209 # FUTURE - fully parse avc messages and store all possible fields 210 # Required fields 211 found_src = False 212 found_tgt = False 213 found_class = False 214 found_access = False 215 216 for i in range(len(recs)): 217 if recs[i] == "{": 218 i = self.__parse_access(recs, i + 1) 219 found_access = True 220 continue 221 elif recs[i] == "granted": 222 self.denial = False 223 224 fields = recs[i].split("=") 225 if len(fields) != 2: 226 continue 227 if fields[0] == "scontext": 228 self.scontext = refpolicy.SecurityContext(fields[1]) 229 found_src = True 230 elif fields[0] == "tcontext": 231 self.tcontext = refpolicy.SecurityContext(fields[1]) 232 found_tgt = True 233 elif fields[0] == "tclass": 234 self.tclass = fields[1] 235 found_class = True 236 elif fields[0] == "comm": 237 self.comm = fields[1][1:-1] 238 elif fields[0] == "exe": 239 self.exe = fields[1][1:-1] 240 elif fields[0] == "name": 241 self.name = fields[1][1:-1] 242 elif fields[0] == "ioctlcmd": 243 try: 244 self.ioctlcmd = int(fields[1], 16) 245 except ValueError: 246 pass 247 248 if not found_src or not found_tgt or not found_class or not found_access: 249 raise ValueError("AVC message in invalid format [%s]\n" % self.message) 250 self.analyze() 251 252 def analyze(self): 253 tcontext = self.tcontext.to_string() 254 scontext = self.scontext.to_string() 255 access_tuple = tuple( self.accesses) 256 self.data = [] 257 258 if (scontext, tcontext, self.tclass, access_tuple) in avcdict.keys(): 259 self.type, self.data = avcdict[(scontext, tcontext, self.tclass, access_tuple)] 260 else: 261 self.type, self.data = audit2why.analyze(scontext, tcontext, self.tclass, self.accesses) 262 if self.type == audit2why.NOPOLICY: 263 self.type = audit2why.TERULE 264 if self.type == audit2why.BADTCON: 265 raise ValueError("Invalid Target Context %s\n" % tcontext) 266 if self.type == audit2why.BADSCON: 267 raise ValueError("Invalid Source Context %s\n" % scontext) 268 if self.type == audit2why.BADSCON: 269 raise ValueError("Invalid Type Class %s\n" % self.tclass) 270 if self.type == audit2why.BADPERM: 271 raise ValueError("Invalid permission %s\n" % " ".join(self.accesses)) 272 if self.type == audit2why.BADCOMPUTE: 273 raise ValueError("Error during access vector computation") 274 275 if self.type == audit2why.CONSTRAINT: 276 self.data = [ self.data ] 277 if self.scontext.user != self.tcontext.user: 278 self.data.append(("user (%s)" % self.scontext.user, 'user (%s)' % self.tcontext.user)) 279 if self.scontext.role != self.tcontext.role and self.tcontext.role != "object_r": 280 self.data.append(("role (%s)" % self.scontext.role, 'role (%s)' % self.tcontext.role)) 281 if self.scontext.level != self.tcontext.level: 282 self.data.append(("level (%s)" % self.scontext.level, 'level (%s)' % self.tcontext.level)) 283 284 avcdict[(scontext, tcontext, self.tclass, access_tuple)] = (self.type, self.data) 285 286class PolicyLoadMessage(AuditMessage): 287 """Audit message indicating that the policy was reloaded.""" 288 def __init__(self, message): 289 AuditMessage.__init__(self, message) 290 291class DaemonStartMessage(AuditMessage): 292 """Audit message indicating that a daemon was started.""" 293 def __init__(self, message): 294 AuditMessage.__init__(self, message) 295 self.auditd = False 296 297 def from_split_string(self, recs): 298 AuditMessage.from_split_string(self, recs) 299 if "auditd" in recs: 300 self.auditd = True 301 302 303class ComputeSidMessage(AuditMessage): 304 """Audit message indicating that a sid was not valid. 305 306 Compute sid messages are generated on attempting to create a security 307 context that is not valid. Security contexts are invalid if the role is 308 not authorized for the user or the type is not authorized for the role. 309 310 This class does not store all of the fields from the compute sid message - 311 just the type and role. 312 """ 313 def __init__(self, message): 314 AuditMessage.__init__(self, message) 315 self.invalid_context = refpolicy.SecurityContext() 316 self.scontext = refpolicy.SecurityContext() 317 self.tcontext = refpolicy.SecurityContext() 318 self.tclass = "" 319 320 def from_split_string(self, recs): 321 AuditMessage.from_split_string(self, recs) 322 if len(recs) < 10: 323 raise ValueError("Split string does not represent a valid compute sid message") 324 325 try: 326 self.invalid_context = refpolicy.SecurityContext(recs[5]) 327 self.scontext = refpolicy.SecurityContext(recs[7].split("=")[1]) 328 self.tcontext = refpolicy.SecurityContext(recs[8].split("=")[1]) 329 self.tclass = recs[9].split("=")[1] 330 except: 331 raise ValueError("Split string does not represent a valid compute sid message") 332 def output(self): 333 return "role %s types %s;\n" % (self.role, self.type) 334 335# Parser for audit messages 336 337class AuditParser: 338 """Parser for audit messages. 339 340 This class parses audit messages and stores them according to their message 341 type. This is not a general purpose audit message parser - it only extracts 342 selinux related messages. 343 344 Each audit messages are stored in one of four lists: 345 avc_msgs - avc denial or granted messages. Messages are stored in 346 AVCMessage objects. 347 comput_sid_messages - invalid sid messages. Messages are stored in 348 ComputSidMessage objects. 349 invalid_msgs - selinux related messages that are not valid. Messages 350 are stored in InvalidMessageObjects. 351 policy_load_messages - policy load messages. Messages are stored in 352 PolicyLoadMessage objects. 353 354 These lists will be reset when a policy load message is seen if 355 AuditParser.last_load_only is set to true. It is assumed that messages 356 are fed to the parser in chronological order - time stamps are not 357 parsed. 358 """ 359 def __init__(self, last_load_only=False): 360 self.__initialize() 361 self.last_load_only = last_load_only 362 363 def __initialize(self): 364 self.avc_msgs = [] 365 self.compute_sid_msgs = [] 366 self.invalid_msgs = [] 367 self.policy_load_msgs = [] 368 self.path_msgs = [] 369 self.by_header = { } 370 self.check_input_file = False 371 372 # Low-level parsing function - tries to determine if this audit 373 # message is an SELinux related message and then parses it into 374 # the appropriate AuditMessage subclass. This function deliberately 375 # does not impose policy (e.g., on policy load message) or store 376 # messages to make as simple and reusable as possible. 377 # 378 # Return values: 379 # None - no recognized audit message found in this line 380 # 381 # InvalidMessage - a recognized but invalid message was found. 382 # 383 # AuditMessage (or subclass) - object representing a parsed 384 # and valid audit message. 385 def __parse_line(self, line): 386 # strip("\x1c\x1d\x1e\x85") is only needed for python2 387 # since str.split() in python3 already does this 388 rec = [x.strip("\x1c\x1d\x1e\x85") for x in line.split()] 389 for i in rec: 390 found = False 391 if i == "avc:" or i == "message=avc:" or i == "msg='avc:": 392 msg = AVCMessage(line) 393 found = True 394 elif i == "security_compute_sid:": 395 msg = ComputeSidMessage(line) 396 found = True 397 elif i == "type=MAC_POLICY_LOAD" or i == "type=1403": 398 msg = PolicyLoadMessage(line) 399 found = True 400 elif i == "type=AVC_PATH": 401 msg = PathMessage(line) 402 found = True 403 elif i == "type=DAEMON_START": 404 msg = DaemonStartMessage(list) 405 found = True 406 407 if found: 408 self.check_input_file = True 409 try: 410 msg.from_split_string(rec) 411 except ValueError: 412 msg = InvalidMessage(line) 413 return msg 414 return None 415 416 # Higher-level parse function - take a line, parse it into an 417 # AuditMessage object, and store it in the appropriate list. 418 # This function will optionally reset all of the lists when 419 # it sees a load policy message depending on the value of 420 # self.last_load_only. 421 def __parse(self, line): 422 msg = self.__parse_line(line) 423 if msg is None: 424 return 425 426 # Append to the correct list 427 if isinstance(msg, PolicyLoadMessage): 428 if self.last_load_only: 429 self.__initialize() 430 elif isinstance(msg, DaemonStartMessage): 431 # We initialize every time the auditd is started. This 432 # is less than ideal, but unfortunately it is the only 433 # way to catch reboots since the initial policy load 434 # by init is not stored in the audit log. 435 if msg.auditd and self.last_load_only: 436 self.__initialize() 437 self.policy_load_msgs.append(msg) 438 elif isinstance(msg, AVCMessage): 439 self.avc_msgs.append(msg) 440 elif isinstance(msg, ComputeSidMessage): 441 self.compute_sid_msgs.append(msg) 442 elif isinstance(msg, InvalidMessage): 443 self.invalid_msgs.append(msg) 444 elif isinstance(msg, PathMessage): 445 self.path_msgs.append(msg) 446 447 # Group by audit header 448 if msg.header != "": 449 if msg.header in self.by_header: 450 self.by_header[msg.header].append(msg) 451 else: 452 self.by_header[msg.header] = [msg] 453 454 455 # Post processing will add additional information from AVC messages 456 # from related messages - only works on messages generated by 457 # the audit system. 458 def __post_process(self): 459 for value in self.by_header.values(): 460 avc = [] 461 path = None 462 for msg in value: 463 if isinstance(msg, PathMessage): 464 path = msg 465 elif isinstance(msg, AVCMessage): 466 avc.append(msg) 467 if len(avc) > 0 and path: 468 for a in avc: 469 a.path = path.path 470 471 def parse_file(self, input): 472 """Parse the contents of a file object. This method can be called 473 multiple times (along with parse_string).""" 474 line = input.readline() 475 while line: 476 self.__parse(line) 477 line = input.readline() 478 if not self.check_input_file: 479 sys.stderr.write("Nothing to do\n") 480 sys.exit(0) 481 self.__post_process() 482 483 def parse_string(self, input): 484 """Parse a string containing audit messages - messages should 485 be separated by new lines. This method can be called multiple 486 times (along with parse_file).""" 487 lines = input.split('\n') 488 for l in lines: 489 self.__parse(l) 490 self.__post_process() 491 492 def to_role(self, role_filter=None): 493 """Return RoleAllowSet statements matching the specified filter 494 495 Filter out types that match the filer, or all roles 496 497 Params: 498 role_filter - [optional] Filter object used to filter the 499 output. 500 Returns: 501 Access vector set representing the denied access in the 502 audit logs parsed by this object. 503 """ 504 role_types = access.RoleTypeSet() 505 for cs in self.compute_sid_msgs: 506 if not role_filter or role_filter.filter(cs): 507 role_types.add(cs.invalid_context.role, cs.invalid_context.type) 508 509 return role_types 510 511 def to_access(self, avc_filter=None, only_denials=True): 512 """Convert the audit logs access into a an access vector set. 513 514 Convert the audit logs into an access vector set, optionally 515 filtering the restults with the passed in filter object. 516 517 Filter objects are object instances with a .filter method 518 that takes and access vector and returns True if the message 519 should be included in the final output and False otherwise. 520 521 Params: 522 avc_filter - [optional] Filter object used to filter the 523 output. 524 Returns: 525 Access vector set representing the denied access in the 526 audit logs parsed by this object. 527 """ 528 av_set = access.AccessVectorSet() 529 for avc in self.avc_msgs: 530 if avc.denial != True and only_denials: 531 continue 532 533 if not avc_filter or avc_filter.filter(avc): 534 av = access.AccessVector([avc.scontext.type, avc.tcontext.type, 535 avc.tclass] + avc.accesses) 536 av.data = avc.data 537 av.type = avc.type 538 539 if avc.ioctlcmd: 540 xperm_set = refpolicy.XpermSet() 541 xperm_set.add(avc.ioctlcmd) 542 av.xperms["ioctl"] = xperm_set 543 544 av_set.add_av(av, audit_msg=avc) 545 546 return av_set 547 548class AVCTypeFilter: 549 def __init__(self, regex): 550 self.regex = re.compile(regex) 551 552 def filter(self, avc): 553 if self.regex.match(avc.scontext.type): 554 return True 555 if self.regex.match(avc.tcontext.type): 556 return True 557 return False 558 559class ComputeSidTypeFilter: 560 def __init__(self, regex): 561 self.regex = re.compile(regex) 562 563 def filter(self, avc): 564 if self.regex.match(avc.invalid_context.type): 565 return True 566 if self.regex.match(avc.scontext.type): 567 return True 568 if self.regex.match(avc.tcontext.type): 569 return True 570 return False 571 572 573