1"""2Get information about executed cron commands - number of commands, list3of commands, and list of commands per user. Uses either journald or plain4logfiles (path specified in config).56NOTE: This parser supports reading from both journald and plain syslog files.7By default the plain logfiles will be used, but the journald option is8preferred for newer systems which support it. To use the journald mode,9specify the parser as `cron_journald` instead of `cron`.1011NOTE: If using journald, the log level for cron.service should be at least 212(default is 1). This can be changed with `sudo systemctl edit cron --full`,13and ammend `-L 2` to the ExecStart command.1415TODO: also output a list of scheduled (future) jobs16"""1718import datetime19import re2021from logparse.formatting import *22from logparse.util import readlog23from logparse import config24from logparse.load_parsers import Parser2526class CronCommand:27"""28Class representing a single cron session. Assigns its own variables of29date, user and cmd when given a `systemd.journal.Record` object or a plain30log message string on initialisation. NOTE: This class is used in both31`cron.py` and `cron_journald.py`.32"""3334def __init__(self, record, datefmt=""):35"""36Parse the date, user and command from the logfile string or record37"""38if isinstance(record, str):39if not datefmt:40logger.error("Date format not provided - cannot parse this "41"log message")42# Parse from a raw logfile string43self.date, self.user, self.cmd = re.search(44r"^(?P<time>.+)\s\w+\sCRON"45"\[\d+\]:\s\((?P<user>\S*)\)\sCMD\s\(+(\[\d+\]\s)?(?P<cmd>.*)\)+",46record).groupdict().values()47self.date = datetime.datetime.strptime(self.date, datefmt)48if not "Y" in datefmt:49self.date = self.date.replace(year=datetime.datetime.now().year)50elif isinstance(record, dict):51self.date = record["_SOURCE_REALTIME_TIMESTAMP"]52self.user, self.cmd = re.search(r"\((?P<user>\S+)\) "53"CMD \((\[\d+\] )?(?P<cmd>.*)\)", record["MESSAGE"]) \54.groupdict().values()55self.cmd = " ".join(self.cmd.split())56else:57raise TypeError("record should be str or dict")5859def truncate(self):60"""61Hide the full directory path for any scripts or explicit binary62references in the command. e.g. `/usr/bin/cat` → `cat`63"""64self.cmd = re.sub(r"(\s|^)/\S*/(\S+)", r"\1\2", self.cmd)65return self.cmd6667def match_user(self, pattern):68"""69Check if the user of this object matches against a regex string and70return a boolean result of this comparison.71"""72user_match = False73for p in pattern:74user_match = re.fullmatch(p, self.user) \75or user_match76return user_match7778def match_cmd(self, pattern):79"""80Check if the command of this object matches against a regex string and81return a boolean result of this comparison.82"""83cmd_match = False84for p in pattern:85cmd_match = re.fullmatch(p, self.user) \86or cmd_match87return cmd_match888990class Cron(Parser):9192def __init__(self):93super().__init__()94self.name = "cron"95self.info = "List the logged (executed) cron jobs and their commands"96self.journald = False9798def _get_journald(self, startdate):99from systemd import journal100j = journal.Reader()101j.this_machine()102j.log_level(journal.LOG_INFO)103j.add_match(_COMM="cron")104j.seek_realtime(startdate)105return [entry for entry in j if "MESSAGE" in entry106and " CMD " in entry["MESSAGE"]]107108def _get_logfile(self, path):109from logparse.util import readlog110return [x for x in readlog(path).splitlines() if " CMD " in x]111112def parse_log(self):113114logger.debug("Starting cron section")115section = Section("cron")116117if not (config.prefs.getboolean("cron", "summary")118or config.prefs.getboolean("cron", "list-users")):119logger.warning("Both summary and list-users configuration options "120"are set to false, so no output will be generated. "121"Skipping this parser.")122return None123124datefmt = config.prefs.get("cron", "datetime-format")125if not datefmt:126datefmt = config.prefs.get("logparse", "datetime-format")127if not datefmt:128logger.error("Invalid datetime-format configuration parameter")129return None130131command_objects = []132users = {}133oldlog_buffer = 0134135if self.journald:136logger.debug("Searching for cron commands in journald")137messages = self._get_journald(section.period.startdate)138else:139logger.debug("Searching for matches in {0}".format(140config.prefs.get("logs", "cron")))141messages = self._get_logfile(config.prefs.get("logs", "cron"))142143if len(messages) < 1:144logger.error("Couldn't find any cron log messages")145return146147for msg in messages:148149try:150cmd_obj = CronCommand(msg, datefmt)151except Exception as e:152logger.warning("Malformed cron session log: {0}. "153"Error message: {1}".format(msg, str(e)))154continue155else:156if cmd_obj.date < section.period.startdate:157continue158if not (cmd_obj.match_user(config.prefs.get("cron", "users")159.split()) and cmd_obj.match_cmd(config.prefs.get(160"cron", "commands").split())):161logger.debug("Ignoring cron session by {0} with command "162"{1} due to config".format(cmd_obj.user, cmd_obj.cmd))163continue164165if config.prefs.getboolean("cron", "truncate-commands"):166cmd_obj.truncate()167168command_objects.append(cmd_obj)169if not cmd_obj.user in users:170users[cmd_obj.user] = []171users[cmd_obj.user].append(cmd_obj.cmd)172173if len(command_objects) == 0:174logger.error("No valid cron commands found")175return176177logger.info("Found {0} cron jobs".format(len(command_objects)))178179if config.prefs.getboolean("cron", "summary"):180summary_data = Data()181summary_data.subtitle = "Total of " + plural("cron session",182len(command_objects)) + " for " + plural("user",183len(users))184summary_data.items = ["{}: `{}`".format(c.user, c.cmd)185for c in command_objects]186summary_data.orderbyfreq()187summary_data.truncl(config.prefs.getint("logparse", "maxcmd"))188section.append_data(summary_data)189190if config.prefs.getboolean("cron", "list-users"):191for user, cmdlist in users.items():192user_data = Data()193user_data.subtitle = plural("session", len(cmdlist)) \194+ " for " + user + (" (" + plural("unique command",195len(set(cmdlist))) + ")" if len(set(cmdlist)) > 1196else "")197user_data.items = ["`{}`".format(cmd) for cmd in cmdlist]198user_data.orderbyfreq()199user_data.truncl(config.prefs.getint("logparse", "maxcmd"))200section.append_data(user_data)201202logger.info("Finished cron section")203return section