rename parsers, better journald integration
[logparse.git] / logparse / parsers / sudo.py
index 8a6dcd3954a0612836611d7ac8074a9d4c4bd7fe..f32581a77ed98c12cfedbbfaa3c6c4e5f8bd5167 100644 (file)
-#
-#   sudo.py
-#   
-#   Get number of sudo sessions for each user
-#
-#   NOTE: This file is now deprecated in favour of the newer journald mechanism
-#   used in sudo-journald.py. This parser is still functional but is slower and
-#   has less features. Please switch over if possible.
-#
+"""
+Get number of sudo sessions for each user
 
+NOTE: This parser supports reading from both journald and plain syslog files. 
+By default the plain logfiles will be used, but the journald option is 
+preferred for newer systems which support it. To use the journald mode, 
+specify the parser as `sudo_journald` instead of `sudo`.
+"""
+
+import datetime
 import re
+from subprocess import Popen, PIPE
 
 from logparse.formatting import *
-from logparse.util import readlog
 from logparse.config import prefs
 from logparse.load_parsers import Parser
 
+
+class SudoCommand():
+    """
+    Class representing a single sudo log entry. Used for both sudo and 
+    sudo_journald, so it accepts either a dictionary output by systemd.Journal 
+    or a line from a logfile upon initialisation.
+    """
+
+    def __init__(self, record, datefmt):
+        """
+        Get instance variables from log message or record object
+        """
+        if isinstance(record, str):
+            if not datefmt:
+                logger.error("Date format not provided - cannot parse this "
+                        "log message")
+            # Parse from a raw logfile string
+            self.date, self.init_user, self.pwd, self.su, self.command \
+                    = re.search(r"^(?P<time>.+)\s\w+\ssudo:\s+"
+                    "(?P<init_user>\w*) : TTY=.* ; PWD=(?P<pwd>\S*) ;"
+                    " USER=(?P<su>\w*) ; COMMAND=(?P<command>\S*)", record)\
+                            .groupdict().values()
+            self.date = datetime.datetime.strptime(self.date, datefmt)
+            if not "Y" in datefmt:
+                self.date = self.date.replace(year=datetime.datetime.now().year)
+        elif isinstance(record, dict):
+            self.date = record["_SOURCE_REALTIME_TIMESTAMP"]
+            self.init_user, self.pwd, self.su, self.command = re.search(
+                    r"\s+(?P<init_user>\S+) : TTY=.* ; PWD=(?P<pwd>\S*) ;"
+                    " USER=(?P<su>\w*) ; COMMAND=(?P<command>\S*)", 
+                    record["MESSAGE"]).groupdict().values()
+            self.command = " ".join(self.command.split())
+        else:
+            raise TypeError("record should be str or dict")
+
+    def truncate(self):
+        """
+        Hide the full directory path for any scripts or explicit binary
+        references in the command. e.g. `/usr/bin/cat` → `cat`
+        """
+        self.command = re.sub(r"(\s|^)/\S*/(\S+)", r"\1\2", self.command)
+        return self.command
+
+    def match_su(self, pattern):
+        """
+        Check if the user of this object matches against a regex string and 
+        return a boolean result of this comparison.
+        """
+        return re.fullmatch(pattern, self.su)
+
+    def match_init_user(self, pattern):
+        """
+        Check if the initialising user of this object matches against a regex 
+        string and return a boolean result of this comparison.
+        """
+        return re.fullmatch(pattern, self.init_user)
+
+    def match_cmd(self, pattern):
+        """
+        Check if the command of this object matches against a regex string and 
+        return a boolean result of this comparison.
+        """
+        return re.fullmatch(pattern, self.command)
+
+    def match_pwd(self, pattern):
+        """
+        Check if the directory of this object matches against a regex string 
+        and return a boolean result of this comparison.
+        """
+        return re.fullmatch(pattern, self.pwd)
+
+
 class Sudo(Parser):
 
     def __init__(self):
         super().__init__()
         self.name = "sudo"
         self.info = "Get number of sudo sessions for each user"
-        self.deprecated = True
-        self.successor = "sudo_journald"
+        self.journald = False
+
+    def _get_journald(self, startdate):
+        from systemd import journal
+        j = journal.Reader()
+        j.this_machine()
+        j.log_level(journal.LOG_INFO)
+        j.add_match(_COMM="sudo")
+        j.seek_realtime(startdate)
+        j.log_level(5)
+        return [entry for entry in j if "MESSAGE" in entry 
+                and "COMMAND=" in entry["MESSAGE"]]
+
+    def _get_logfile(self, path):
+        from logparse.util import readlog
+        return re.findall(r".+sudo.+TTY=.+PWD=.+USER=.+COMMAND=.+", 
+                readlog(path)) # Initial check to make sure all fields exist
+
 
     def parse_log(self):
+
         logger.debug("Starting sudo section")
         section = Section("sudo")
-        logger.debug("Searching for matches in {0}".format(prefs.get("logs", "auth")))
-        umatches = re.findall('.*sudo:session\): session opened.*', readlog(prefs.get("logs", "auth")))
-        num = sum(1 for line in umatches)    # total number of sessions
-        users = []
-        data = []
-        for match in umatches:
-            user = re.search('.*session opened for user root by (\S*)\(uid=.*\)', match).group(1)
-            exists = [i for i, item in enumerate(users) if re.search(user, item[0])]
-            if (exists == []):
-                users.append([user, 1])
-            else:
-                users[exists[0]][1] += 1
-        commands = []
-        cmatches = re.findall('sudo:.*COMMAND\=(.*)', readlog(prefs.get("logs", "auth")))
-        for cmd in cmatches:
-            commands.append(cmd)
-        logger.debug("Finished parsing sudo sessions")
-
-        auth_data = Data(subtitle=plural("sudo session", num) + " for")
-
-        if (len(users) == 1):
-            logger.debug("found " + str(num) + " sudo session(s) for user " + str(users[0]))
-            auth_data.subtitle += ' ' + users[0][0]
+
+        datefmt = config.prefs.get("sudo", "datetime-format")
+        if not datefmt:
+            datefmt = config.prefs.get("logparse", "datetime-format")
+        if not datefmt:
+            logger.error("Invalid datetime-format configuration parameter")
+            return None
+
+        if not (config.prefs.getboolean("sudo", "summary") 
+                or config.prefs.getboolean("sudo", "list-users")):
+            logger.warning("Both summary and list-users configuration options "
+                "are set to false, so no output will be generated. "
+                "Skipping this parser.")
+            return None
+    
+        
+        if self.journald:
+            logger.debug("Searching for sudo commands in journald")
+            messages = self._get_journald(section.period.startdate)
         else:
-            for user in users:
-                auth_data.items.append(user[0] + ' (' + str(user[1]) + ')')
-            logger.debug("found " + str(num) + " sudo sessions for users " + str(data))
-        section.append_data(auth_data)
-
-        if (len(commands) > 0):
-            command_data = Data(subtitle="top sudo commands")
-            commands = backticks(commands)
-            command_data.items = commands
-            command_data.orderbyfreq()
-            command_data.truncl(prefs.getint("logparse", "maxcmd"))
-            section.append_data(command_data)
+            logger.debug("Searching for matches in {0}".format(
+                prefs.get("logs", "auth")))
+            messages = self._get_logfile(config.prefs.get("logs", "auth"))
+
+
+        commands_objects = []   # list of command objects
+        init_users = {}         # keys are users, values are lists of commands
+
+        logger.debug("Parsing sudo log messages")
+
+        for msg in messages:
+
+            try:
+                cmd_obj = SudoCommand(msg, datefmt)
+            except Exception as e:
+                logger.warning("Malformed sudo log message: {0}. "
+                    "Error message: {1}".format(msg, str(e)))
+                continue
+            else:
+                if cmd_obj.date < section.period.startdate:
+                    continue
+                checks = [
+                        cmd_obj.match_init_user(
+                            config.prefs.get("sudo", "init-users")),
+                        cmd_obj.match_su(
+                            config.prefs.get("sudo", "superusers")),
+                        cmd_obj.match_cmd(
+                            config.prefs.get("sudo", "commands")),
+                        cmd_obj.match_pwd(
+                            config.prefs.get("sudo", "directories")),
+                        ]
+                if not all(checks):
+                    logger.debug("Ignoring sudo session by {0} with command "
+                        "{1} due to config".format(
+                            cmd_obj.init_user, cmd_obj.command))
+                    continue
+            if config.prefs.getboolean("sudo", "truncate-commands"):
+                cmd_obj.truncate()
+            commands_objects.append(cmd_obj)
+            if not cmd_obj.init_user in init_users:
+                init_users[cmd_obj.init_user] = []
+            init_users[cmd_obj.init_user].append(
+                    cmd_obj.su + ": " + cmd_obj.command)
+
+        logger.debug("Generating output")
+
+        if len(commands_objects) == 0:
+            logger.warning("No sudo commands found")
+            return
+
+        if config.prefs.getboolean("sudo", "summary"):
+
+            summary_data = Data()
+            summary_data.subtitle = plural(
+                    "sudo session", len(commands_objects))
+
+            if all(cmd.su == commands_objects[0].su 
+                    for cmd in commands_objects):
+                # Only one superuser
+                if len(set(cmd.init_user for cmd in commands_objects)) > 1:
+                    # Multiple initiating users
+                    summary_data.subtitle += " for superuser " \
+                            + commands_objects[0].su
+                    summary_data.items = ["{}: {}".format(
+                        cmd.init_user, cmd.command)
+                            for cmd in commands_objects]
+                else:
+                    # Only one initiating user
+                    summary_data.subtitle += " opened by " \
+                        + commands_objects[0].init_user + " for " \
+                        + commands_objects[0].su
+                    summary_data.items = [cmd.command 
+                            for cmd in commands_objects]
+            else:
+                # Multiple superusers
+                if len(set(cmd.init_user for cmd in commands_objects)) > 1:
+                    # Multiple initiating users
+                    summary_data.subtitle += " for " + plural("superuser",
+                            len(set(cmd.su for cmd in commands_objects)))
+                    summary_data.items = ["{}→{}: {}".format(
+                        cmd.init_user, cmd.su, cmd.command)
+                        for cmd in commands_objects]
+                else:
+                    # Only one initiating user
+                    summary_data.subtitle += " by " \
+                            + commands_objects[0].init_user \
+                            + " for " + plural("superuser",
+                                    len(set(cmd.su
+                                        for cmd in commands_objects)))
+                    summary_data.items = ["{}: {}".format(
+                        cmd.su, cmd.command) for cmd in commands_objects]
+            summary_data.orderbyfreq()
+            summary_data.truncl(config.prefs.getint("logparse", "maxcmd"))
+            section.append_data(summary_data)
+
+        if config.prefs.getboolean("sudo", "list-users") \
+                and len(set(cmd.init_user for cmd in commands_objects)) > 1:
+            for user, user_commands in init_users.items():
+                user_data = Data()
+                user_data.subtitle = plural("sudo session",
+                        len(user_commands)) + " for user " + user
+                user_data.items = user_commands
+                user_data.orderbyfreq()
+                user_data.truncl(config.prefs.getint("logparse", "maxcmd"))
+                section.append_data(user_data)
 
         logger.info("Finished sudo section")
 
         return section
+
+    def check_dependencies(self):
+
+        # Check if sudo exists
+        sudo_cmdline = "sudo --version"
+        if self._check_dependency_command(sudo_cmdline)[0] != 0:
+            return (False, ["sudo"])
+        else:
+            return (True, None)