You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

416 lines
16 KiB

  1. --- a/utils/aa-notify
  2. +++ b/utils/aa-notify
  3. @@ -13,17 +13,6 @@
  4. #
  5. # ----------------------------------------------------------------------
  6. #
  7. -# /etc/apparmor/notify.conf:
  8. -# # set to 'yes' to enable AppArmor DENIED notifications
  9. -# show_notifications="yes"
  10. -#
  11. -# # only people in use_group can run this script
  12. -# use_group="admin"
  13. -#
  14. -# $HOME/.apparmor/notify.conf can have:
  15. -# # set to 'yes' to enable AppArmor DENIED notifications
  16. -# show_notifications="yes"
  17. -#
  18. # In a typical desktop environment one would run as a service the
  19. # command:
  20. # /usr/bin/aa-notify -p -w 10
  21. @@ -35,7 +24,6 @@ import re
  22. import sys
  23. import time
  24. import struct
  25. -import notify2
  26. import psutil
  27. import pwd
  28. import grp
  29. @@ -60,56 +48,9 @@ def get_user_login():
  30. username = os.getlogin()
  31. return username
  32. -
  33. -def get_last_login_timestamp(username):
  34. - '''Directly read wtmp and get last login for user as epoch timestamp'''
  35. - timestamp = 0
  36. - filename = '/var/log/wtmp'
  37. - last_login = 0
  38. -
  39. - debug_logger.debug('Username: {}'.format(username))
  40. -
  41. - with open(filename, "rb") as wtmp_file:
  42. - offset = 0
  43. - wtmp_filesize = os.path.getsize(filename)
  44. - debug_logger.debug('WTMP filesize: {}'.format(wtmp_filesize))
  45. - while offset < wtmp_filesize:
  46. - wtmp_file.seek(offset)
  47. - offset += 384 # Increment for next entry
  48. -
  49. - type = struct.unpack("<L", wtmp_file.read(4))[0]
  50. - debug_logger.debug('WTMP entry type: {}'.format(type))
  51. -
  52. - # Only parse USER lines
  53. - if type == 7:
  54. - # Read each item and move pointer forward
  55. - pid = struct.unpack("<L", wtmp_file.read(4))[0]
  56. - line = wtmp_file.read(32).decode("utf-8", "replace").split('\0', 1)[0]
  57. - id = wtmp_file.read(4).decode("utf-8", "replace").split('\0', 1)[0]
  58. - user = wtmp_file.read(32).decode("utf-8", "replace").split('\0', 1)[0]
  59. - host = wtmp_file.read(256).decode("utf-8", "replace").split('\0', 1)[0]
  60. - term = struct.unpack("<H", wtmp_file.read(2))[0]
  61. - exit = struct.unpack("<H", wtmp_file.read(2))[0]
  62. - session = struct.unpack("<L", wtmp_file.read(4))[0]
  63. - timestamp = struct.unpack("<L", wtmp_file.read(4))[0]
  64. - usec = struct.unpack("<L", wtmp_file.read(4))[0]
  65. - entry = (pid, line, id, user, host, term, exit, session, timestamp, usec)
  66. - debug_logger.debug('WTMP entry: {}'.format(entry))
  67. -
  68. - # Store login timestamp for requested user
  69. - if user == username:
  70. - last_login = timestamp
  71. -
  72. - # When loop is done, last value should be the latest login timestamp
  73. - return last_login
  74. -
  75. -
  76. def format_event(event, logsource):
  77. output = []
  78. - if 'message_body' in config['']:
  79. - output += [config['']['message_body']]
  80. -
  81. if event.profile:
  82. output += ['Profile: {}'.format(event.profile)]
  83. if event.operation:
  84. @@ -126,7 +67,6 @@ def format_event(event, logsource):
  85. return "\n".join(output)
  86. -
  87. def notify_about_new_entries(logfile, wait=0):
  88. # Kill other instances of aa-notify if already running
  89. for process in psutil.process_iter():
  90. @@ -154,7 +94,6 @@ def notify_about_new_entries(logfile, wa
  91. # print("parent: %d, child: %d\n" % pids)
  92. os._exit(0) # Exit child without calling exit handlers etc
  93. -
  94. def show_entries_since_epoch(logfile, epoch_since):
  95. count = 0
  96. for event in get_apparmor_events(logfile, epoch_since):
  97. @@ -172,26 +111,7 @@ def show_entries_since_epoch(logfile, ep
  98. )
  99. if args.verbose:
  100. - if 'message_footer' in config['']:
  101. - print(config['']['message_footer'])
  102. - else:
  103. - print(_('For more information, please see: {}').format(debug_docs_url))
  104. -
  105. -
  106. -def show_entries_since_last_login(logfile, username=get_user_login()):
  107. - # If running as sudo, use username of sudo user instead of root
  108. - if 'SUDO_USER' in os.environ.keys():
  109. - username = os.environ['SUDO_USER']
  110. -
  111. - if args.verbose:
  112. - print(_('Showing entries since {} logged in').format(username))
  113. - print() # Newline
  114. - epoch_since = get_last_login_timestamp(username)
  115. - if epoch_since == 0:
  116. - print(_('ERROR: Could not find last login'), file=sys.stderr)
  117. - sys.exit(1)
  118. - show_entries_since_epoch(logfile, epoch_since)
  119. -
  120. + print(_('For more information, please see: {}').format(debug_docs_url))
  121. def show_entries_since_days(logfile, since_days):
  122. day_in_seconds = 60*60*24
  123. @@ -199,7 +119,6 @@ def show_entries_since_days(logfile, sin
  124. epoch_since = epoch_now - day_in_seconds * since_days
  125. show_entries_since_epoch(logfile, epoch_since)
  126. -
  127. def follow_apparmor_events(logfile, wait=0):
  128. '''Follow AppArmor events and yield relevant entries until process stops'''
  129. @@ -247,7 +166,6 @@ def follow_apparmor_events(logfile, wait
  130. time.sleep(1)
  131. -
  132. def reopen_logfile_if_needed(logfile, logdata, log_inode, log_size):
  133. retry = True
  134. @@ -279,7 +197,6 @@ def reopen_logfile_if_needed(logfile, lo
  135. return (logdata, log_inode, log_size)
  136. -
  137. def get_apparmor_events(logfile, since=0):
  138. '''Read audit events from log source and yield all relevant events'''
  139. @@ -293,7 +210,6 @@ def get_apparmor_events(logfile, since=0
  140. except PermissionError:
  141. sys.exit(_("ERROR: Cannot read {}. Please check permissions.".format(logfile)))
  142. -
  143. def parse_logdata(logsource):
  144. '''Traverse any iterable log source and extract relevant AppArmor events'''
  145. @@ -327,53 +243,6 @@ def parse_logdata(logsource):
  146. if event.operation[0:8] != 'profile_':
  147. yield event
  148. -
  149. -def drop_privileges():
  150. - '''If running as root, drop privileges to USER if known, or fall-back to nobody_user/group'''
  151. -
  152. - if os.geteuid() == 0:
  153. -
  154. - if 'SUDO_USER' in os.environ.keys():
  155. - next_username = os.environ['SUDO_USER']
  156. - next_uid = os.environ['SUDO_UID']
  157. - next_gid = os.environ['SUDO_GID']
  158. - else:
  159. - nobody_user_info = pwd.getpwnam(nobody_user)
  160. - next_username = nobody_user_info[0]
  161. - next_uid = nobody_user_info[2]
  162. - next_gid = nobody_user_info[3]
  163. -
  164. - debug_logger.debug('Dropping to user "{}" privileges'.format(next_username))
  165. -
  166. - # @TODO?
  167. - # Remove group privileges, including potential 'adm' group that might
  168. - # have had log read access but also other accesses.
  169. - # os.setgroups([])
  170. -
  171. - # Try setting the new uid/gid
  172. - # Set gid first, otherwise the latter step would fail on missing permissions
  173. - os.setegid(int(next_gid))
  174. - os.seteuid(int(next_uid))
  175. -
  176. -def raise_privileges():
  177. - '''If was running as user with saved user ID 0, raise back to root privileges'''
  178. -
  179. - if os.geteuid() != 0 and original_effective_user == 0:
  180. -
  181. - debug_logger.debug('Rasing privileges from UID {} back to UID 0 (root)'.format(os.geteuid()))
  182. -
  183. - # os.setgid(int(next_gid))
  184. - os.seteuid(original_effective_user)
  185. -
  186. -def read_notify_conf(path, shell_config):
  187. - try:
  188. - shell_config.CONF_DIR = path
  189. - conf_dict = shell_config.read_config('notify.conf')
  190. - debug_logger.debug('Found configuration file in {}/notify.conf'.format(shell_config.CONF_DIR))
  191. - return conf_dict
  192. - except FileNotFoundError:
  193. - return {}
  194. -
  195. def main():
  196. '''
  197. Main function of aa-notify that parses command line
  198. @@ -381,10 +250,9 @@ def main():
  199. '''
  200. global _, debug_logger, config, args
  201. - global debug_docs_url, nobody_user, original_effective_user, timeformat
  202. + global debug_docs_url, original_effective_user, timeformat
  203. debug_docs_url = "https://wiki.ubuntu.com/DebuggingApparmor"
  204. - nobody_user = "nobody"
  205. timeformat = "%c" # Automatically using locale format
  206. original_effective_user = os.geteuid()
  207. @@ -403,180 +271,37 @@ def main():
  208. debug_logger.debug("Starting aa-notify")
  209. parser = argparse.ArgumentParser(description=_('Display AppArmor notifications or messages for DENIED entries.'))
  210. - parser.add_argument('-p', '--poll', action='store_true', help=_('poll AppArmor logs and display notifications'))
  211. - parser.add_argument('--display', type=str, help=_('set the DISPLAY environment variable (might be needed if sudo resets $DISPLAY)'))
  212. - parser.add_argument('-f', '--file', type=str, help=_('search FILE for AppArmor messages'))
  213. - parser.add_argument('-l', '--since-last', action='store_true', help=_('display stats since last login'))
  214. - parser.add_argument('-s', '--since-days', type=int, metavar=('NUM'), help=_('show stats for last NUM days (can be used alone or with -p)'))
  215. - parser.add_argument('-v', '--verbose', action='store_true', help=_('show messages with stats'))
  216. - parser.add_argument('-u', '--user', type=str, help=_('user to drop privileges to when not using sudo'))
  217. - parser.add_argument('-w', '--wait', type=int, metavar=('NUM'), help=_('wait NUM seconds before displaying notifications (with -p)'))
  218. - parser.add_argument('--debug', action='store_true', help=_('debug mode'))
  219. - parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
  220. + parser.add_argument('-f', '--file', type=str, help=_('Logfile to parse for AppArmor messages'))
  221. + parser.add_argument('-s', '--since-days', type=int, metavar=('NUM'), help=_('Show stats for last NUM days'))
  222. + parser.add_argument('-v', '--verbose', action='store_true', help=_('Show messages with stats'))
  223. + parser.add_argument('--debug', action='store_true', help=_('Debug mode'))
  224. # If a TTY then assume running in test mode and fix output width
  225. if not sys.stdout.isatty():
  226. parser.formatter_class = lambda prog: argparse.HelpFormatter(prog, width=80)
  227. args = parser.parse_args()
  228. + args.user = 'root'
  229. # Debug mode can be invoked directly with --debug or env LOGPROF_DEBUG=3
  230. if args.debug:
  231. debug_logger.activateStderr()
  232. debug_logger.debug('Logging level: {}'.format(debug_logger.debug_level))
  233. debug_logger.debug('Running as uid: {0[0]}, euid: {0[1]}, suid: {0[2]}'.format(os.getresuid()))
  234. - if args.poll:
  235. - debug_logger.debug('Running with --debug and --poll. Will exit in 100s')
  236. - # Sanity checks
  237. - user_ids = os.getresuid()
  238. - groups_ids = os.getresgid()
  239. - if user_ids[1] != user_ids[2]:
  240. - sys.exit("ERROR: Cannot be started with suid set!")
  241. - if groups_ids[1] != groups_ids[2]:
  242. - sys.exit("ERROR: Cannot be started with sgid set!")
  243. - # Define global variables that will be populated by init_aa()
  244. - # conf = None
  245. logfile = None
  246. - if args.configdir: # prefer --configdir if given
  247. - confdir = args.configdir
  248. - else: # fallback to env variable (or None if not set)
  249. - confdir = os.getenv('__AA_CONFDIR')
  250. -
  251. - aa.init_aa(confdir=confdir)
  252. -
  253. # Initialize aa.logfile
  254. - aa.set_logfile(args.file)
  255. -
  256. - # Load global config reader
  257. - shell_config = aaconfig.Config('shell')
  258. -
  259. - # Load system's notify.conf
  260. - # By default aa.CONFDIR is /etc/apparmor on most production systems
  261. - system_config = read_notify_conf(aa.CONFDIR, shell_config)
  262. - # Set default is no system notify.conf was found
  263. - if not system_config:
  264. - system_config = {'': {'show_notifications': 'yes'}}
  265. -
  266. - # Load user's notify.conf
  267. - if os.path.isfile(os.environ['HOME'] + '/.apparmor/notify.conf'):
  268. - # Use legacy path if the conf file is there
  269. - user_config = read_notify_conf(os.environ['HOME'] + '/.apparmor', shell_config)
  270. - elif 'XDG_CONFIG_HOME' in os.environ and os.path.isfile(os.environ['XDG_CONFIG_HOME'] + '/apparmor/notify.conf'):
  271. - # Use XDG_CONFIG_HOME if it is defined
  272. - user_config = read_notify_conf(os.environ['XDG_CONFIG_HOME'] + '/apparmor', shell_config)
  273. - else:
  274. - # Fallback to the default value of XDG_CONFIG_HOME
  275. - user_config = read_notify_conf(os.environ['HOME'] + '/.config/apparmor', shell_config)
  276. -
  277. - # Merge the two config dicts in an accurate and idiomatic way (requires Python 3.5)
  278. - config = {**system_config, **user_config}
  279. -
  280. - """
  281. - Possible configuration options:
  282. - - show_notifications
  283. - - message_body
  284. - - message_footer
  285. - - use_group
  286. - """
  287. -
  288. - # # Config checks
  289. -
  290. - # Warn about unknown keys in the config
  291. - allowed_config_keys = [
  292. - 'use_group',
  293. - 'show_notifications',
  294. - 'message_body',
  295. - 'message_footer'
  296. - ]
  297. - found_config_keys = config[''].keys()
  298. - unknown_keys = [item for item in found_config_keys if item not in allowed_config_keys]
  299. - for item in unknown_keys:
  300. - print(_('Warning! Configuration item "{}" is unknown!').format(item))
  301. -
  302. - # Warn if use_group is defined and current group does not match defined
  303. - if 'use_group' in config['']:
  304. - user = pwd.getpwuid(os.geteuid())[0]
  305. - user_groups = [g.gr_name for g in grp.getgrall() if user in g.gr_mem]
  306. - gid = pwd.getpwnam(user).pw_gid
  307. - user_groups.append(grp.getgrgid(gid).gr_name)
  308. -
  309. - if config['']['use_group'] not in user_groups:
  310. - print(
  311. - _('ERROR! User {user} not member of {group} group!').format(
  312. - user=user,
  313. - group=config['']['use_group']
  314. - ),
  315. - file=sys.stderr
  316. - )
  317. - sys.exit(1)
  318. - # @TODO: Extend UI lib to have warning and error functions that
  319. - # can be used in an uniform way with both text and JSON output.
  320. -
  321. if args.file:
  322. logfile = args.file
  323. - elif os.path.isfile('/var/run/auditd.pid') and os.path.isfile('/var/log/audit/audit.log'):
  324. - # If auditd is running, look at /var/log/audit/audit.log
  325. - logfile = '/var/log/audit/audit.log'
  326. - elif os.path.isfile('/var/log/kern.log'):
  327. - # For aa-notify, the fallback is kern.log, not syslog from aa.logfile
  328. - logfile = '/var/log/kern.log'
  329. + aa.set_logfile(args.file)
  330. else:
  331. - # If all above failed, use aa cfg
  332. - logfile = aa.logfile
  333. + logfile = '/var/log/audit/audit.log'
  334. + aa.set_logfile('/var/log/audit/audit.log')
  335. if args.verbose:
  336. print(_('Using log file'), logfile)
  337. - if args.display:
  338. - os.environ['DISPLAY'] = args.display
  339. -
  340. - if args.poll:
  341. - # Exit immediately if show_notifications is no or any of the options below
  342. - if config['']['show_notifications'] in [False, 'no', 'false', '0']:
  343. - print(_('Showing notifications forbidden in notify.conf, aborting..'))
  344. - sys.exit(0)
  345. -
  346. - # Don't allow usage of aa-notify by root, must be some user. Desktop
  347. - # logins as root are not recommended and certainly not a use case for
  348. - # aa-notify notifications.
  349. - if not args.user and os.getuid() == 0 and 'SUDO_USER' not in os.environ.keys():
  350. - sys.exit("ERROR: Cannot be started a real root user. Use --user to define what user to use.")
  351. -
  352. - # At this point this script needs to be able to read 'logfile' but once
  353. - # the for loop starts, privileges can be dropped since the file descriptor
  354. - # has been opened and access granted. Further reads of the file will not
  355. - # trigger any new permission checks.
  356. - # @TODO Plan to catch PermissionError here or..?
  357. - for message in notify_about_new_entries(logfile, args.wait):
  358. -
  359. - # Notifications should not be run as root, since root probably is
  360. - # the wrong desktop user and not the one getting the notifications.
  361. - drop_privileges()
  362. -
  363. - # sudo does not preserve DBUS address, so we need to guess it based on UID
  364. - if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
  365. - os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/run/user/{}/bus'.format(os.geteuid())
  366. -
  367. - # Before use, notify2 must be initialized and the DBUS channel
  368. - # should be opened using the non-root user. This this step needs to
  369. - # be executed after the drop_privileges().
  370. - notify2.init('AppArmor')
  371. -
  372. - n = notify2.Notification(
  373. - _('AppArmor notification'),
  374. - message,
  375. - 'gtk-dialog-warning'
  376. - )
  377. - n.show()
  378. -
  379. - # When notification is sent, raise privileged back to root if the
  380. - # original effective user id was zero (to be able to read AppArmor logs)
  381. - raise_privileges()
  382. -
  383. - elif args.since_last:
  384. - show_entries_since_last_login(logfile)
  385. elif args.since_days:
  386. show_entries_since_days(logfile, args.since_days)
  387. else: