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)