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