• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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