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 # strip("\x1c\x1d\x1e\x85") is only needed for python2 380 # since str.split() in python3 already does this 381 rec = [x.strip("\x1c\x1d\x1e\x85") for x in line.split()] 382 for i in rec: 383 found = False 384 if i == "avc:" or i == "message=avc:" or i == "msg='avc:": 385 msg = AVCMessage(line) 386 found = True 387 elif i == "security_compute_sid:": 388 msg = ComputeSidMessage(line) 389 found = True 390 elif i == "type=MAC_POLICY_LOAD" or i == "type=1403": 391 msg = PolicyLoadMessage(line) 392 found = True 393 elif i == "type=AVC_PATH": 394 msg = PathMessage(line) 395 found = True 396 elif i == "type=DAEMON_START": 397 msg = DaemonStartMessage(list) 398 found = True 399 400 if found: 401 self.check_input_file = True 402 try: 403 msg.from_split_string(rec) 404 except ValueError: 405 msg = InvalidMessage(line) 406 return msg 407 return None 408 409 # Higher-level parse function - take a line, parse it into an 410 # AuditMessage object, and store it in the appropriate list. 411 # This function will optionally reset all of the lists when 412 # it sees a load policy message depending on the value of 413 # self.last_load_only. 414 def __parse(self, line): 415 msg = self.__parse_line(line) 416 if msg is None: 417 return 418 419 # Append to the correct list 420 if isinstance(msg, PolicyLoadMessage): 421 if self.last_load_only: 422 self.__initialize() 423 elif isinstance(msg, DaemonStartMessage): 424 # We initialize every time the auditd is started. This 425 # is less than ideal, but unfortunately it is the only 426 # way to catch reboots since the initial policy load 427 # by init is not stored in the audit log. 428 if msg.auditd and self.last_load_only: 429 self.__initialize() 430 self.policy_load_msgs.append(msg) 431 elif isinstance(msg, AVCMessage): 432 self.avc_msgs.append(msg) 433 elif isinstance(msg, ComputeSidMessage): 434 self.compute_sid_msgs.append(msg) 435 elif isinstance(msg, InvalidMessage): 436 self.invalid_msgs.append(msg) 437 elif isinstance(msg, PathMessage): 438 self.path_msgs.append(msg) 439 440 # Group by audit header 441 if msg.header != "": 442 if msg.header in self.by_header: 443 self.by_header[msg.header].append(msg) 444 else: 445 self.by_header[msg.header] = [msg] 446 447 448 # Post processing will add additional information from AVC messages 449 # from related messages - only works on messages generated by 450 # the audit system. 451 def __post_process(self): 452 for value in self.by_header.values(): 453 avc = [] 454 path = None 455 for msg in value: 456 if isinstance(msg, PathMessage): 457 path = msg 458 elif isinstance(msg, AVCMessage): 459 avc.append(msg) 460 if len(avc) > 0 and path: 461 for a in avc: 462 a.path = path.path 463 464 def parse_file(self, input): 465 """Parse the contents of a file object. This method can be called 466 multiple times (along with parse_string).""" 467 line = input.readline() 468 while line: 469 self.__parse(line) 470 line = input.readline() 471 if not self.check_input_file: 472 sys.stderr.write("Nothing to do\n") 473 sys.exit(0) 474 self.__post_process() 475 476 def parse_string(self, input): 477 """Parse a string containing audit messages - messages should 478 be separated by new lines. This method can be called multiple 479 times (along with parse_file).""" 480 lines = input.split('\n') 481 for l in lines: 482 self.__parse(l) 483 self.__post_process() 484 485 def to_role(self, role_filter=None): 486 """Return RoleAllowSet statements matching the specified filter 487 488 Filter out types that match the filer, or all roles 489 490 Params: 491 role_filter - [optional] Filter object used to filter the 492 output. 493 Returns: 494 Access vector set representing the denied access in the 495 audit logs parsed by this object. 496 """ 497 role_types = access.RoleTypeSet() 498 for cs in self.compute_sid_msgs: 499 if not role_filter or role_filter.filter(cs): 500 role_types.add(cs.invalid_context.role, cs.invalid_context.type) 501 502 return role_types 503 504 def to_access(self, avc_filter=None, only_denials=True): 505 """Convert the audit logs access into a an access vector set. 506 507 Convert the audit logs into an access vector set, optionally 508 filtering the restults with the passed in filter object. 509 510 Filter objects are object instances with a .filter method 511 that takes and access vector and returns True if the message 512 should be included in the final output and False otherwise. 513 514 Params: 515 avc_filter - [optional] Filter object used to filter the 516 output. 517 Returns: 518 Access vector set representing the denied access in the 519 audit logs parsed by this object. 520 """ 521 av_set = access.AccessVectorSet() 522 for avc in self.avc_msgs: 523 if avc.denial != True and only_denials: 524 continue 525 if avc_filter: 526 if avc_filter.filter(avc): 527 av_set.add(avc.scontext.type, avc.tcontext.type, avc.tclass, 528 avc.accesses, avc, avc_type=avc.type, data=avc.data) 529 else: 530 av_set.add(avc.scontext.type, avc.tcontext.type, avc.tclass, 531 avc.accesses, avc, avc_type=avc.type, data=avc.data) 532 return av_set 533 534class AVCTypeFilter: 535 def __init__(self, regex): 536 self.regex = re.compile(regex) 537 538 def filter(self, avc): 539 if self.regex.match(avc.scontext.type): 540 return True 541 if self.regex.match(avc.tcontext.type): 542 return True 543 return False 544 545class ComputeSidTypeFilter: 546 def __init__(self, regex): 547 self.regex = re.compile(regex) 548 549 def filter(self, avc): 550 if self.regex.match(avc.invalid_context.type): 551 return True 552 if self.regex.match(avc.scontext.type): 553 return True 554 if self.regex.match(avc.tcontext.type): 555 return True 556 return False 557 558 559