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 refpolicy
21import access
22import re
23import sys
24
25# Convenience functions
26
27def get_audit_boot_msgs():
28    """Obtain all of the avc and policy load messages from the audit
29    log. This function uses ausearch and requires that the current
30    process have sufficient rights to run ausearch.
31
32    Returns:
33       string contain all of the audit messages returned by ausearch.
34    """
35    import subprocess
36    import time
37    fd=open("/proc/uptime", "r")
38    off=float(fd.read().split()[0])
39    fd.close
40    s = time.localtime(time.time() - off)
41    bootdate = time.strftime("%x", s)
42    boottime = time.strftime("%X", s)
43    output = subprocess.Popen(["/sbin/ausearch", "-m", "AVC,USER_AVC,MAC_POLICY_LOAD,DAEMON_START,SELINUX_ERR", "-ts", bootdate, boottime],
44                              stdout=subprocess.PIPE).communicate()[0]
45    return output
46
47def get_audit_msgs():
48    """Obtain all of the avc and policy load messages from the audit
49    log. This function uses ausearch and requires that the current
50    process have sufficient rights to run ausearch.
51
52    Returns:
53       string contain all of the audit messages returned by ausearch.
54    """
55    import subprocess
56    output = subprocess.Popen(["/sbin/ausearch", "-m", "AVC,USER_AVC,MAC_POLICY_LOAD,DAEMON_START,SELINUX_ERR"],
57                              stdout=subprocess.PIPE).communicate()[0]
58    return output
59
60def get_dmesg_msgs():
61    """Obtain all of the avc and policy load messages from /bin/dmesg.
62
63    Returns:
64       string contain all of the audit messages returned by dmesg.
65    """
66    import subprocess
67    output = subprocess.Popen(["/bin/dmesg"],
68                              stdout=subprocess.PIPE).communicate()[0]
69    return output
70
71# Classes representing audit messages
72
73class AuditMessage:
74    """Base class for all objects representing audit messages.
75
76    AuditMessage is a base class for all audit messages and only
77    provides storage for the raw message (as a string) and a
78    parsing function that does nothing.
79    """
80    def __init__(self, message):
81        self.message = message
82        self.header = ""
83
84    def from_split_string(self, recs):
85        """Parse a string that has been split into records by space into
86        an audit message.
87
88        This method should be overridden by subclasses. Error reporting
89        should be done by raise ValueError exceptions.
90        """
91        for msg in recs:
92            fields = msg.split("=")
93            if len(fields) != 2:
94                if msg[:6] == "audit(":
95                    self.header = msg
96                    return
97                else:
98                    continue
99
100            if fields[0] == "msg":
101                self.header = fields[1]
102                return
103
104
105class InvalidMessage(AuditMessage):
106    """Class representing invalid audit messages. This is used to differentiate
107    between audit messages that aren't recognized (that should return None from
108    the audit message parser) and a message that is recognized but is malformed
109    in some way.
110    """
111    def __init__(self, message):
112        AuditMessage.__init__(self, message)
113
114class PathMessage(AuditMessage):
115    """Class representing a path message"""
116    def __init__(self, message):
117        AuditMessage.__init__(self, message)
118        self.path = ""
119
120    def from_split_string(self, recs):
121        AuditMessage.from_split_string(self, recs)
122
123        for msg in recs:
124            fields = msg.split("=")
125            if len(fields) != 2:
126                continue
127            if fields[0] == "path":
128                self.path = fields[1][1:-1]
129                return
130import selinux.audit2why as audit2why
131
132avcdict = {}
133
134class AVCMessage(AuditMessage):
135    """AVC message representing an access denial or granted message.
136
137    This is a very basic class and does not represent all possible fields
138    in an avc message. Currently the fields are:
139       scontext - context for the source (process) that generated the message
140       tcontext - context for the target
141       tclass - object class for the target (only one)
142       comm - the process name
143       exe - the on-disc binary
144       path - the path of the target
145       access - list of accesses that were allowed or denied
146       denial - boolean indicating whether this was a denial (True) or granted
147          (False) message.
148
149    An example audit message generated from the audit daemon looks like (line breaks
150    added):
151       'type=AVC msg=audit(1155568085.407:10877): avc:  denied  { search } for
152       pid=677 comm="python" name="modules" dev=dm-0 ino=13716388
153       scontext=user_u:system_r:setroubleshootd_t:s0
154       tcontext=system_u:object_r:modules_object_t:s0 tclass=dir'
155
156    An example audit message stored in syslog (not processed by the audit daemon - line
157    breaks added):
158       'Sep 12 08:26:43 dhcp83-5 kernel: audit(1158064002.046:4): avc:  denied  { read }
159       for  pid=2 496 comm="bluez-pin" name=".gdm1K3IFT" dev=dm-0 ino=3601333
160       scontext=user_u:system_r:bluetooth_helper_t:s0-s0:c0
161       tcontext=system_u:object_r:xdm_tmp_t:s0 tclass=file
162    """
163    def __init__(self, message):
164        AuditMessage.__init__(self, message)
165        self.scontext = refpolicy.SecurityContext()
166        self.tcontext = refpolicy.SecurityContext()
167        self.tclass = ""
168        self.comm = ""
169        self.exe = ""
170        self.path = ""
171        self.name = ""
172        self.accesses = []
173        self.denial = True
174        self.type = audit2why.TERULE
175
176    def __parse_access(self, recs, start):
177        # This is kind of sucky - the access that is in a space separated
178        # list like '{ read write }'. This doesn't fit particularly well with splitting
179        # the string on spaces. This function takes the list of recs and a starting
180        # position one beyond the open brace. It then adds the accesses until it finds
181        # the close brace or the end of the list (which is an error if reached without
182        # seeing a close brace).
183        found_close = False
184        i = start
185        if i == (len(recs) - 1):
186            raise ValueError("AVC message in invalid format [%s]\n" % self.message)
187        while i < len(recs):
188            if recs[i] == "}":
189                found_close = True
190                break
191            self.accesses.append(recs[i])
192            i = i + 1
193        if not found_close:
194            raise ValueError("AVC message in invalid format [%s]\n" % self.message)
195        return i + 1
196
197
198    def from_split_string(self, recs):
199        AuditMessage.from_split_string(self, recs)
200        # FUTURE - fully parse avc messages and store all possible fields
201        # Required fields
202        found_src = False
203        found_tgt = False
204        found_class = False
205        found_access = False
206
207        for i in range(len(recs)):
208            if recs[i] == "{":
209                i = self.__parse_access(recs, i + 1)
210                found_access = True
211                continue
212            elif recs[i] == "granted":
213                self.denial = False
214
215            fields = recs[i].split("=")
216            if len(fields) != 2:
217                continue
218            if fields[0] == "scontext":
219                self.scontext = refpolicy.SecurityContext(fields[1])
220                found_src = True
221            elif fields[0] == "tcontext":
222                self.tcontext = refpolicy.SecurityContext(fields[1])
223                found_tgt = True
224            elif fields[0] == "tclass":
225                self.tclass = fields[1]
226                found_class = True
227            elif fields[0] == "comm":
228                self.comm = fields[1][1:-1]
229            elif fields[0] == "exe":
230                self.exe = fields[1][1:-1]
231            elif fields[0] == "name":
232                self.name = fields[1][1:-1]
233
234        if not found_src or not found_tgt or not found_class or not found_access:
235            raise ValueError("AVC message in invalid format [%s]\n" % self.message)
236        self.analyze()
237
238    def analyze(self):
239        tcontext = self.tcontext.to_string()
240        scontext = self.scontext.to_string()
241        access_tuple = tuple( self.accesses)
242        self.data = []
243
244        if (scontext, tcontext, self.tclass, access_tuple) in avcdict.keys():
245            self.type, self.data = avcdict[(scontext, tcontext, self.tclass, access_tuple)]
246        else:
247            self.type, self.data = audit2why.analyze(scontext, tcontext, self.tclass, self.accesses);
248            if self.type == audit2why.NOPOLICY:
249                self.type = audit2why.TERULE
250            if self.type == audit2why.BADTCON:
251                raise ValueError("Invalid Target Context %s\n" % tcontext)
252            if self.type == audit2why.BADSCON:
253                raise ValueError("Invalid Source Context %s\n" % scontext)
254            if self.type == audit2why.BADSCON:
255                raise ValueError("Invalid Type Class %s\n" % self.tclass)
256            if self.type == audit2why.BADPERM:
257                raise ValueError("Invalid permission %s\n" % " ".join(self.accesses))
258            if self.type == audit2why.BADCOMPUTE:
259                raise ValueError("Error during access vector computation")
260
261            if self.type == audit2why.CONSTRAINT:
262                self.data = [ self.data ]
263                if self.scontext.user != self.tcontext.user:
264                    self.data.append(("user (%s)" % self.scontext.user, 'user (%s)' % self.tcontext.user))
265                if self.scontext.role != self.tcontext.role and self.tcontext.role != "object_r":
266                    self.data.append(("role (%s)" % self.scontext.role, 'role (%s)' % self.tcontext.role))
267                if self.scontext.level != self.tcontext.level:
268                    self.data.append(("level (%s)" % self.scontext.level, 'level (%s)' % self.tcontext.level))
269
270            avcdict[(scontext, tcontext, self.tclass, access_tuple)] = (self.type, self.data)
271
272class PolicyLoadMessage(AuditMessage):
273    """Audit message indicating that the policy was reloaded."""
274    def __init__(self, message):
275        AuditMessage.__init__(self, message)
276
277class DaemonStartMessage(AuditMessage):
278    """Audit message indicating that a daemon was started."""
279    def __init__(self, message):
280        AuditMessage.__init__(self, message)
281        self.auditd = False
282
283    def from_split_string(self, recs):
284        AuditMessage.from_split_string(self, recs)
285        if "auditd" in recs:
286            self.auditd = True
287
288
289class ComputeSidMessage(AuditMessage):
290    """Audit message indicating that a sid was not valid.
291
292    Compute sid messages are generated on attempting to create a security
293    context that is not valid. Security contexts are invalid if the role is
294    not authorized for the user or the type is not authorized for the role.
295
296    This class does not store all of the fields from the compute sid message -
297    just the type and role.
298    """
299    def __init__(self, message):
300        AuditMessage.__init__(self, message)
301        self.invalid_context = refpolicy.SecurityContext()
302        self.scontext = refpolicy.SecurityContext()
303        self.tcontext = refpolicy.SecurityContext()
304        self.tclass = ""
305
306    def from_split_string(self, recs):
307        AuditMessage.from_split_string(self, recs)
308        if len(recs) < 10:
309            raise ValueError("Split string does not represent a valid compute sid message")
310
311        try:
312            self.invalid_context = refpolicy.SecurityContext(recs[5])
313            self.scontext = refpolicy.SecurityContext(recs[7].split("=")[1])
314            self.tcontext = refpolicy.SecurityContext(recs[8].split("=")[1])
315            self.tclass = recs[9].split("=")[1]
316        except:
317            raise ValueError("Split string does not represent a valid compute sid message")
318    def output(self):
319        return "role %s types %s;\n" % (self.role, self.type)
320
321# Parser for audit messages
322
323class AuditParser:
324    """Parser for audit messages.
325
326    This class parses audit messages and stores them according to their message
327    type. This is not a general purpose audit message parser - it only extracts
328    selinux related messages.
329
330    Each audit messages are stored in one of four lists:
331       avc_msgs - avc denial or granted messages. Messages are stored in
332          AVCMessage objects.
333       comput_sid_messages - invalid sid messages. Messages are stored in
334          ComputSidMessage objects.
335       invalid_msgs - selinux related messages that are not valid. Messages
336          are stored in InvalidMessageObjects.
337       policy_load_messages - policy load messages. Messages are stored in
338          PolicyLoadMessage objects.
339
340    These lists will be reset when a policy load message is seen if
341    AuditParser.last_load_only is set to true. It is assumed that messages
342    are fed to the parser in chronological order - time stamps are not
343    parsed.
344    """
345    def __init__(self, last_load_only=False):
346        self.__initialize()
347        self.last_load_only = last_load_only
348
349    def __initialize(self):
350        self.avc_msgs = []
351        self.compute_sid_msgs = []
352        self.invalid_msgs = []
353        self.policy_load_msgs = []
354        self.path_msgs = []
355        self.by_header = { }
356        self.check_input_file = False
357
358    # Low-level parsing function - tries to determine if this audit
359    # message is an SELinux related message and then parses it into
360    # the appropriate AuditMessage subclass. This function deliberately
361    # does not impose policy (e.g., on policy load message) or store
362    # messages to make as simple and reusable as possible.
363    #
364    # Return values:
365    #   None - no recognized audit message found in this line
366    #
367    #   InvalidMessage - a recognized but invalid message was found.
368    #
369    #   AuditMessage (or subclass) - object representing a parsed
370    #      and valid audit message.
371    def __parse_line(self, line):
372        rec = line.split()
373        for i in rec:
374            found = False
375            if i == "avc:" or i == "message=avc:" or i == "msg='avc:":
376                msg = AVCMessage(line)
377                found = True
378            elif i == "security_compute_sid:":
379                msg = ComputeSidMessage(line)
380                found = True
381            elif i == "type=MAC_POLICY_LOAD" or i == "type=1403":
382                msg = PolicyLoadMessage(line)
383                found = True
384            elif i == "type=AVC_PATH":
385                msg = PathMessage(line)
386                found = True
387            elif i == "type=DAEMON_START":
388                msg = DaemonStartMessage(list)
389                found = True
390
391            if found:
392                self.check_input_file = True
393                try:
394                    msg.from_split_string(rec)
395                except ValueError:
396                    msg = InvalidMessage(line)
397                return msg
398        return None
399
400    # Higher-level parse function - take a line, parse it into an
401    # AuditMessage object, and store it in the appropriate list.
402    # This function will optionally reset all of the lists when
403    # it sees a load policy message depending on the value of
404    # self.last_load_only.
405    def __parse(self, line):
406        msg = self.__parse_line(line)
407        if msg is None:
408            return
409
410        # Append to the correct list
411        if isinstance(msg, PolicyLoadMessage):
412            if self.last_load_only:
413                self.__initialize()
414        elif isinstance(msg, DaemonStartMessage):
415            # We initialize every time the auditd is started. This
416            # is less than ideal, but unfortunately it is the only
417            # way to catch reboots since the initial policy load
418            # by init is not stored in the audit log.
419            if msg.auditd and self.last_load_only:
420                self.__initialize()
421            self.policy_load_msgs.append(msg)
422        elif isinstance(msg, AVCMessage):
423            self.avc_msgs.append(msg)
424        elif isinstance(msg, ComputeSidMessage):
425            self.compute_sid_msgs.append(msg)
426        elif isinstance(msg, InvalidMessage):
427            self.invalid_msgs.append(msg)
428        elif isinstance(msg, PathMessage):
429            self.path_msgs.append(msg)
430
431        # Group by audit header
432        if msg.header != "":
433            if self.by_header.has_key(msg.header):
434                self.by_header[msg.header].append(msg)
435            else:
436                self.by_header[msg.header] = [msg]
437
438
439    # Post processing will add additional information from AVC messages
440    # from related messages - only works on messages generated by
441    # the audit system.
442    def __post_process(self):
443        for value in self.by_header.values():
444            avc = []
445            path = None
446            for msg in value:
447                if isinstance(msg, PathMessage):
448                    path = msg
449                elif isinstance(msg, AVCMessage):
450                    avc.append(msg)
451            if len(avc) > 0 and path:
452                for a in avc:
453                    a.path = path.path
454
455    def parse_file(self, input):
456        """Parse the contents of a file object. This method can be called
457        multiple times (along with parse_string)."""
458        line = input.readline()
459        while line:
460            self.__parse(line)
461            line = input.readline()
462        if not self.check_input_file:
463            sys.stderr.write("Nothing to do\n")
464            sys.exit(0)
465        self.__post_process()
466
467    def parse_string(self, input):
468        """Parse a string containing audit messages - messages should
469        be separated by new lines. This method can be called multiple
470        times (along with parse_file)."""
471        lines = input.split('\n')
472        for l in lines:
473            self.__parse(l)
474        self.__post_process()
475
476    def to_role(self, role_filter=None):
477        """Return RoleAllowSet statements matching the specified filter
478
479        Filter out types that match the filer, or all roles
480
481        Params:
482           role_filter - [optional] Filter object used to filter the
483              output.
484        Returns:
485           Access vector set representing the denied access in the
486           audit logs parsed by this object.
487        """
488        role_types = access.RoleTypeSet()
489        for cs in self.compute_sid_msgs:
490            if not role_filter or role_filter.filter(cs):
491                role_types.add(cs.invalid_context.role, cs.invalid_context.type)
492
493        return role_types
494
495    def to_access(self, avc_filter=None, only_denials=True):
496        """Convert the audit logs access into a an access vector set.
497
498        Convert the audit logs into an access vector set, optionally
499        filtering the restults with the passed in filter object.
500
501        Filter objects are object instances with a .filter method
502        that takes and access vector and returns True if the message
503        should be included in the final output and False otherwise.
504
505        Params:
506           avc_filter - [optional] Filter object used to filter the
507              output.
508        Returns:
509           Access vector set representing the denied access in the
510           audit logs parsed by this object.
511        """
512        av_set = access.AccessVectorSet()
513        for avc in self.avc_msgs:
514            if avc.denial != True and only_denials:
515                continue
516            if avc_filter:
517                if avc_filter.filter(avc):
518                    av_set.add(avc.scontext.type, avc.tcontext.type, avc.tclass,
519                               avc.accesses, avc, avc_type=avc.type, data=avc.data)
520            else:
521                av_set.add(avc.scontext.type, avc.tcontext.type, avc.tclass,
522                           avc.accesses, avc, avc_type=avc.type, data=avc.data)
523        return av_set
524
525class AVCTypeFilter:
526    def __init__(self, regex):
527        self.regex = re.compile(regex)
528
529    def filter(self, avc):
530        if self.regex.match(avc.scontext.type):
531            return True
532        if self.regex.match(avc.tcontext.type):
533            return True
534        return False
535
536class ComputeSidTypeFilter:
537    def __init__(self, regex):
538        self.regex = re.compile(regex)
539
540    def filter(self, avc):
541        if self.regex.match(avc.invalid_context.type):
542            return True
543        if self.regex.match(avc.scontext.type):
544            return True
545        if self.regex.match(avc.tcontext.type):
546            return True
547        return False
548
549
550