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