1"""
2Get information about executed cron commands - number of commands, list
3of commands, and list of commands per user. Uses either journald or plain
4logfiles (path specified in config).
56
NOTE: This parser supports reading from both journald and plain syslog files.
7By default the plain logfiles will be used, but the journald option is
8preferred for newer systems which support it. To use the journald mode,
9specify the parser as `cron_journald` instead of `cron`.
1011
NOTE: If using journald, the log level for cron.service should be at least 2
12(default is 1). This can be changed with `sudo systemctl edit cron --full`,
13and ammend `-L 2` to the ExecStart command.
1415
TODO: also output a list of scheduled (future) jobs
16"""
1718
import datetime
19import re
2021
from logparse.formatting import *
22from logparse.util import readlog
23from logparse import config
24from logparse.load_parsers import Parser
2526
class CronCommand:
27"""
28Class representing a single cron session. Assigns its own variables of
29date, user and cmd when given a `systemd.journal.Record` object or a plain
30log message string on initialisation. NOTE: This class is used in both
31`cron.py` and `cron_journald.py`.
32"""
3334
def __init__(self, record, datefmt=""):
35"""
36Parse the date, user and command from the logfile string or record
37"""
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 string
43self.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")
5859
def truncate(self):
60"""
61Hide the full directory path for any scripts or explicit binary
62references 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.cmd
6667
def match_user(self, pattern):
68"""
69Check if the user of this object matches against a regex string and
70return a boolean result of this comparison.
71"""
72user_match = False
73for p in pattern:
74user_match = re.fullmatch(p, self.user) \
75or user_match
76return user_match
7778
def match_cmd(self, pattern):
79"""
80Check if the command of this object matches against a regex string and
81return a boolean result of this comparison.
82"""
83cmd_match = False
84for p in pattern:
85cmd_match = re.fullmatch(p, self.user) \
86or cmd_match
87return cmd_match
8889
90
class Cron(Parser):
9192
def __init__(self):
93super().__init__()
94self.name = "cron"
95self.info = "List the logged (executed) cron jobs and their commands"
96self.journald = False
9798
def _get_journald(self, startdate):
99from systemd import journal
100j = 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 entry
106and " CMD " in entry["MESSAGE"]]
107108
def _get_logfile(self, path):
109from logparse.util import readlog
110return [x for x in readlog(path).splitlines() if " CMD " in x]
111112
def parse_log(self):
113114
logger.debug("Starting cron section")
115section = Section("cron")
116117
if 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 None
123124
datefmt = 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 None
130131
command_objects = []
132users = {}
133oldlog_buffer = 0
134135
if 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"))
142143
if len(messages) < 1:
144logger.error("Couldn't find any cron log messages")
145return
146147
for msg in messages:
148149
try:
150cmd_obj = CronCommand(msg, datefmt)
151except Exception as e:
152logger.warning("Malformed cron session log: {0}. "
153"Error message: {1}".format(msg, str(e)))
154continue
155else:
156if cmd_obj.date < section.period.startdate:
157continue
158if 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))
163continue
164165
if config.prefs.getboolean("cron", "truncate-commands"):
166cmd_obj.truncate()
167168
command_objects.append(cmd_obj)
169if not cmd_obj.user in users:
170users[cmd_obj.user] = []
171users[cmd_obj.user].append(cmd_obj.cmd)
172173
if len(command_objects) == 0:
174logger.error("No valid cron commands found")
175return
176177
logger.info("Found {0} cron jobs".format(len(command_objects)))
178179
if 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)
189190
if 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)) > 1
196else "")
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)
201202
logger.info("Finished cron section")
203return section