logparse / parsers / sudo.pyon commit rename parsers, better journald integration (e1f7605)
   1"""
   2Get number of sudo sessions for each user
   3
   4NOTE: This parser supports reading from both journald and plain syslog files. 
   5By default the plain logfiles will be used, but the journald option is 
   6preferred for newer systems which support it. To use the journald mode, 
   7specify the parser as `sudo_journald` instead of `sudo`.
   8"""
   9
  10import datetime
  11import re
  12from subprocess import Popen, PIPE
  13
  14from logparse.formatting import *
  15from logparse.config import prefs
  16from logparse.load_parsers import Parser
  17
  18
  19class SudoCommand():
  20    """
  21    Class representing a single sudo log entry. Used for both sudo and 
  22    sudo_journald, so it accepts either a dictionary output by systemd.Journal 
  23    or a line from a logfile upon initialisation.
  24    """
  25
  26    def __init__(self, record, datefmt):
  27        """
  28        Get instance variables from log message or record object
  29        """
  30        if isinstance(record, str):
  31            if not datefmt:
  32                logger.error("Date format not provided - cannot parse this "
  33                        "log message")
  34            # Parse from a raw logfile string
  35            self.date, self.init_user, self.pwd, self.su, self.command \
  36                    = re.search(r"^(?P<time>.+)\s\w+\ssudo:\s+"
  37                    "(?P<init_user>\w*) : TTY=.* ; PWD=(?P<pwd>\S*) ;"
  38                    " USER=(?P<su>\w*) ; COMMAND=(?P<command>\S*)", record)\
  39                            .groupdict().values()
  40            self.date = datetime.datetime.strptime(self.date, datefmt)
  41            if not "Y" in datefmt:
  42                self.date = self.date.replace(year=datetime.datetime.now().year)
  43        elif isinstance(record, dict):
  44            self.date = record["_SOURCE_REALTIME_TIMESTAMP"]
  45            self.init_user, self.pwd, self.su, self.command = re.search(
  46                    r"\s+(?P<init_user>\S+) : TTY=.* ; PWD=(?P<pwd>\S*) ;"
  47                    " USER=(?P<su>\w*) ; COMMAND=(?P<command>\S*)", 
  48                    record["MESSAGE"]).groupdict().values()
  49            self.command = " ".join(self.command.split())
  50        else:
  51            raise TypeError("record should be str or dict")
  52
  53    def truncate(self):
  54        """
  55        Hide the full directory path for any scripts or explicit binary
  56        references in the command. e.g. `/usr/bin/cat` → `cat`
  57        """
  58        self.command = re.sub(r"(\s|^)/\S*/(\S+)", r"\1\2", self.command)
  59        return self.command
  60
  61    def match_su(self, pattern):
  62        """
  63        Check if the user of this object matches against a regex string and 
  64        return a boolean result of this comparison.
  65        """
  66        return re.fullmatch(pattern, self.su)
  67
  68    def match_init_user(self, pattern):
  69        """
  70        Check if the initialising user of this object matches against a regex 
  71        string and return a boolean result of this comparison.
  72        """
  73        return re.fullmatch(pattern, self.init_user)
  74
  75    def match_cmd(self, pattern):
  76        """
  77        Check if the command of this object matches against a regex string and 
  78        return a boolean result of this comparison.
  79        """
  80        return re.fullmatch(pattern, self.command)
  81
  82    def match_pwd(self, pattern):
  83        """
  84        Check if the directory of this object matches against a regex string 
  85        and return a boolean result of this comparison.
  86        """
  87        return re.fullmatch(pattern, self.pwd)
  88
  89
  90class Sudo(Parser):
  91
  92    def __init__(self):
  93        super().__init__()
  94        self.name = "sudo"
  95        self.info = "Get number of sudo sessions for each user"
  96        self.journald = False
  97
  98    def _get_journald(self, startdate):
  99        from systemd import journal
 100        j = journal.Reader()
 101        j.this_machine()
 102        j.log_level(journal.LOG_INFO)
 103        j.add_match(_COMM="sudo")
 104        j.seek_realtime(startdate)
 105        j.log_level(5)
 106        return [entry for entry in j if "MESSAGE" in entry 
 107                and "COMMAND=" in entry["MESSAGE"]]
 108
 109    def _get_logfile(self, path):
 110        from logparse.util import readlog
 111        return re.findall(r".+sudo.+TTY=.+PWD=.+USER=.+COMMAND=.+", 
 112                readlog(path)) # Initial check to make sure all fields exist
 113
 114
 115    def parse_log(self):
 116
 117        logger.debug("Starting sudo section")
 118        section = Section("sudo")
 119
 120        datefmt = config.prefs.get("sudo", "datetime-format")
 121        if not datefmt:
 122            datefmt = config.prefs.get("logparse", "datetime-format")
 123        if not datefmt:
 124            logger.error("Invalid datetime-format configuration parameter")
 125            return None
 126
 127        if not (config.prefs.getboolean("sudo", "summary") 
 128                or config.prefs.getboolean("sudo", "list-users")):
 129            logger.warning("Both summary and list-users configuration options "
 130                "are set to false, so no output will be generated. "
 131                "Skipping this parser.")
 132            return None
 133    
 134        
 135        if self.journald:
 136            logger.debug("Searching for sudo commands in journald")
 137            messages = self._get_journald(section.period.startdate)
 138        else:
 139            logger.debug("Searching for matches in {0}".format(
 140                prefs.get("logs", "auth")))
 141            messages = self._get_logfile(config.prefs.get("logs", "auth"))
 142
 143
 144        commands_objects = []   # list of command objects
 145        init_users = {}         # keys are users, values are lists of commands
 146
 147        logger.debug("Parsing sudo log messages")
 148
 149        for msg in messages:
 150
 151            try:
 152                cmd_obj = SudoCommand(msg, datefmt)
 153            except Exception as e:
 154                logger.warning("Malformed sudo log message: {0}. "
 155                    "Error message: {1}".format(msg, str(e)))
 156                continue
 157            else:
 158                if cmd_obj.date < section.period.startdate:
 159                    continue
 160                checks = [
 161                        cmd_obj.match_init_user(
 162                            config.prefs.get("sudo", "init-users")),
 163                        cmd_obj.match_su(
 164                            config.prefs.get("sudo", "superusers")),
 165                        cmd_obj.match_cmd(
 166                            config.prefs.get("sudo", "commands")),
 167                        cmd_obj.match_pwd(
 168                            config.prefs.get("sudo", "directories")),
 169                        ]
 170                if not all(checks):
 171                    logger.debug("Ignoring sudo session by {0} with command "
 172                        "{1} due to config".format(
 173                            cmd_obj.init_user, cmd_obj.command))
 174                    continue
 175            if config.prefs.getboolean("sudo", "truncate-commands"):
 176                cmd_obj.truncate()
 177            commands_objects.append(cmd_obj)
 178            if not cmd_obj.init_user in init_users:
 179                init_users[cmd_obj.init_user] = []
 180            init_users[cmd_obj.init_user].append(
 181                    cmd_obj.su + ": " + cmd_obj.command)
 182
 183        logger.debug("Generating output")
 184
 185        if len(commands_objects) == 0:
 186            logger.warning("No sudo commands found")
 187            return
 188
 189        if config.prefs.getboolean("sudo", "summary"):
 190
 191            summary_data = Data()
 192            summary_data.subtitle = plural(
 193                    "sudo session", len(commands_objects))
 194
 195            if all(cmd.su == commands_objects[0].su 
 196                    for cmd in commands_objects):
 197                # Only one superuser
 198                if len(set(cmd.init_user for cmd in commands_objects)) > 1:
 199                    # Multiple initiating users
 200                    summary_data.subtitle += " for superuser " \
 201                            + commands_objects[0].su
 202                    summary_data.items = ["{}: {}".format(
 203                        cmd.init_user, cmd.command)
 204                            for cmd in commands_objects]
 205                else:
 206                    # Only one initiating user
 207                    summary_data.subtitle += " opened by " \
 208                        + commands_objects[0].init_user + " for " \
 209                        + commands_objects[0].su
 210                    summary_data.items = [cmd.command 
 211                            for cmd in commands_objects]
 212            else:
 213                # Multiple superusers
 214                if len(set(cmd.init_user for cmd in commands_objects)) > 1:
 215                    # Multiple initiating users
 216                    summary_data.subtitle += " for " + plural("superuser",
 217                            len(set(cmd.su for cmd in commands_objects)))
 218                    summary_data.items = ["{}→{}: {}".format(
 219                        cmd.init_user, cmd.su, cmd.command)
 220                        for cmd in commands_objects]
 221                else:
 222                    # Only one initiating user
 223                    summary_data.subtitle += " by " \
 224                            + commands_objects[0].init_user \
 225                            + " for " + plural("superuser",
 226                                    len(set(cmd.su
 227                                        for cmd in commands_objects)))
 228                    summary_data.items = ["{}: {}".format(
 229                        cmd.su, cmd.command) for cmd in commands_objects]
 230            summary_data.orderbyfreq()
 231            summary_data.truncl(config.prefs.getint("logparse", "maxcmd"))
 232            section.append_data(summary_data)
 233
 234        if config.prefs.getboolean("sudo", "list-users") \
 235                and len(set(cmd.init_user for cmd in commands_objects)) > 1:
 236            for user, user_commands in init_users.items():
 237                user_data = Data()
 238                user_data.subtitle = plural("sudo session",
 239                        len(user_commands)) + " for user " + user
 240                user_data.items = user_commands
 241                user_data.orderbyfreq()
 242                user_data.truncl(config.prefs.getint("logparse", "maxcmd"))
 243                section.append_data(user_data)
 244
 245        logger.info("Finished sudo section")
 246
 247        return section
 248
 249    def check_dependencies(self):
 250
 251        # Check if sudo exists
 252        sudo_cmdline = "sudo --version"
 253        if self._check_dependency_command(sudo_cmdline)[0] != 0:
 254            return (False, ["sudo"])
 255        else:
 256            return (True, None)