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 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 557