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