-#
-# sudo.py
-#
-# Get number of sudo sessions for each user
-#
+"""
+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.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)