mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 00:14:44 +01:00
aa-notify: Enhanced Graphical User Interfaces
This commit is contained in:
parent
d9e9bb1a55
commit
ff1baf3851
12 changed files with 626 additions and 14 deletions
|
@ -77,7 +77,8 @@ test-utils:
|
|||
extends:
|
||||
- .ubuntu-before_script
|
||||
script:
|
||||
- apt-get install --no-install-recommends -y libc6-dev libjs-jquery libjs-jquery-throttle-debounce libjs-jquery-isonscreen libjs-jquery-tablesorter flake8 python3-coverage python3-notify2 python3-psutil python3-setuptools
|
||||
- apt-get install --no-install-recommends -y libc6-dev libjs-jquery libjs-jquery-throttle-debounce libjs-jquery-isonscreen libjs-jquery-tablesorter flake8 python3-coverage python3-notify2 python3-psutil python3-setuptools python3-tk python3-ttkthemes python3-gi
|
||||
|
||||
# See apparmor/apparmor#221
|
||||
- make -C parser/tst gen_dbus
|
||||
- make -C parser/tst gen_xtrans
|
||||
|
|
|
@ -354,6 +354,9 @@ The aa-notify tool's Python dependencies can be satisfied by installing the
|
|||
following packages (Debian package names, other distros may vary):
|
||||
* python3-notify2
|
||||
* python3-psutil
|
||||
* python3-tk
|
||||
* python3-ttkthemes
|
||||
* python3-gi
|
||||
|
||||
Perl is no longer needed since none of the utilities shipped to end users depend
|
||||
on it anymore.
|
||||
|
|
|
@ -51,7 +51,7 @@ po/${NAME}.pot: ${TOOLS} ${PYMODULES}
|
|||
.PHONY: install
|
||||
install: ${MANPAGES} ${HTMLMANPAGES}
|
||||
install -d ${CONFDIR}
|
||||
install -m 644 logprof.conf severity.db notify.conf ${CONFDIR}
|
||||
install -m 644 logprof.conf severity.db notify.conf default_unconfined.template ${CONFDIR}
|
||||
install -d ${BINDIR}
|
||||
# aa-easyprof is installed by python-tools-setup.py
|
||||
install -m 755 $(filter-out aa-easyprof, ${TOOLS}) ${BINDIR}
|
||||
|
|
231
utils/aa-notify
231
utils/aa-notify
|
@ -39,17 +39,29 @@ import re
|
|||
import sys
|
||||
import time
|
||||
|
||||
import subprocess
|
||||
|
||||
import notify2
|
||||
import psutil
|
||||
|
||||
import apparmor.aa as aa
|
||||
import apparmor.ui as aaui
|
||||
import apparmor.config as aaconfig
|
||||
import apparmor.update_profile as update_profile
|
||||
import LibAppArmor # C-library to parse one log line
|
||||
from apparmor.common import DebugLogger, open_file_read
|
||||
from apparmor.common import DebugLogger, open_file_read, AppArmorException
|
||||
from apparmor.fail import enable_aa_exception_handler
|
||||
from apparmor.notify import get_last_login_timestamp
|
||||
from apparmor.translations import init_translation
|
||||
from apparmor.logparser import ReadLog
|
||||
from apparmor.gui import UsernsGUI, AddProfileGUI, ErrorGUI, ShowMoreGUI, set_interface_theme
|
||||
|
||||
from dbus import DBusException
|
||||
import gi
|
||||
from gi.repository import GLib
|
||||
import threading
|
||||
|
||||
gi.require_version('GLib', '2.0')
|
||||
|
||||
|
||||
def get_user_login():
|
||||
|
@ -124,11 +136,23 @@ def notify_about_new_entries(logfile, filters, wait=0):
|
|||
# Follow the logfile and stream notifications
|
||||
# Rate limit to not show too many notifications
|
||||
try:
|
||||
# Before use, notify2 must be initialized and the DBUS channel
|
||||
# should be opened using the non-root user. This step needs to
|
||||
# be executed after the drop_privileges().
|
||||
notify2.init('aa-notify', mainloop='glib')
|
||||
except DBusException:
|
||||
sys.exit(_('Cannot initialize notify2. Please check that your terminal can use a graphical interface'))
|
||||
|
||||
try:
|
||||
thread = threading.Thread(target=start_glib_loop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
for event in follow_apparmor_events(logfile, wait):
|
||||
if not is_event_in_filter(event, filters):
|
||||
continue
|
||||
debug_logger.info(format_event(event, logfile))
|
||||
yield (format_event(event, logfile))
|
||||
yield event, format_event(event, logfile)
|
||||
except PermissionError:
|
||||
sys.exit(_("ERROR: Cannot read {}. Please check permissions.").format(logfile))
|
||||
|
||||
|
@ -396,6 +420,151 @@ def compile_filter_regex(filters):
|
|||
return filters
|
||||
|
||||
|
||||
def is_special_profile_userns(ev, special_profiles):
|
||||
if not special_profiles or ev['profile'] not in special_profiles:
|
||||
return False # We don't use special profiles or there is already a profile defined: we don't ask to add userns
|
||||
|
||||
if 'execpath' not in ev or not ev['execpath']:
|
||||
ev['execpath'] = aa.find_executable(ev['comm'])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def ask_for_user_ns_denied(path, name, profile_path):
|
||||
gui = UsernsGUI(name, path)
|
||||
ans = gui.show()
|
||||
if ans in {'allow', 'deny'}:
|
||||
update_profile_path = update_profile.__file__
|
||||
local_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_unconfined.template')
|
||||
if os.path.exists(local_template_path): # We are using local aa-notify -> we use local template
|
||||
template_path = local_template_path
|
||||
else:
|
||||
template_path = aa.CONFDIR + '/default_unconfined.template'
|
||||
|
||||
command = ['pkexec', '--keep-cwd', update_profile_path, 'create_userns', template_path, name, path, profile_path, ans]
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 126: # return code 126 means the user cancelled the request
|
||||
UsernsGUI.show_error_cannot_reload_profile(profile_path, e.returncode)
|
||||
else:
|
||||
debug_logger.debug('No action from the user for {}'.format(path))
|
||||
|
||||
|
||||
def prompt_userns(ev, special_profiles):
|
||||
"""If the user namespace creation denial was generated by an unconfined binary, displays a graphical notification.
|
||||
Creates a new profile to allow userns if the user wants it. Returns whether a notification was displayed to the user
|
||||
"""
|
||||
if not is_special_profile_userns(ev, special_profiles):
|
||||
return False
|
||||
|
||||
if ev['execpath'] is None:
|
||||
UsernsGUI.show_error_cannot_find_execpath(ev['comm'], os.path.dirname(os.path.abspath(__file__)) + '/default_unconfined.template')
|
||||
return True
|
||||
|
||||
aa.update_profiles()
|
||||
|
||||
if aa.get_profile_filename_from_profile_name(ev['comm']):
|
||||
# There is already a profile with this name: we show an error to the user.
|
||||
# We could use the full path as profile name like for the old profiles if we want to handle this case
|
||||
# but if execpath is not supported by the kernel it could also mean that we inferred a bad path
|
||||
# So we do nothing beyond showing this error.
|
||||
ErrorGUI(
|
||||
_('Application {0} tried to create an user namespace, but a profile already exists with this name.\n'
|
||||
'This is likely because there is several binaries named {0} thus the path inferred by AppArmor ({1}) is not correct.\n'
|
||||
'You should review your profiles (in {2}).').format(ev['comm'], ev['execpath'], aa.profile_dir),
|
||||
False).show()
|
||||
return True
|
||||
|
||||
profile_path = aa.get_profile_filename_from_profile_name(ev['comm'], True)
|
||||
if not profile_path:
|
||||
return False
|
||||
|
||||
ask_for_user_ns_denied(ev['execpath'], ev['comm'], profile_path)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# TODO reuse more code from aa-logprof in callbacks
|
||||
def cb_more_info(notification, action, _args):
|
||||
(raw_ev, rl) = _args
|
||||
notification.close()
|
||||
|
||||
parsed_event = rl.parse_record(raw_ev)
|
||||
out = _('Operation denied by AppArmor\n\n')
|
||||
|
||||
for key, value in parsed_event.items():
|
||||
if value:
|
||||
out += '\t{} = {}\n'.format(_(key), value)
|
||||
|
||||
out += _('\nThe software that declined this operation is {}\n').format(parsed_event['profile'])
|
||||
|
||||
rule = rl.create_rule_from_ev(parsed_event)
|
||||
|
||||
if rule:
|
||||
aa.update_profiles()
|
||||
profile_path = aa.get_profile_filename_from_profile_name(parsed_event['profile'])
|
||||
if profile_path:
|
||||
out += _('If you want to allow this operation you can add the line below in profile {}\n').format(profile_path)
|
||||
else:
|
||||
out += _('However {0} is not in {1}\nIt is likely that the profile was not stored in {1} or was removed.\n').format(parsed_event['profile'], aa.profile_dir)
|
||||
out += rule.get_clean()
|
||||
else: # Should not happen
|
||||
out += _('ERROR: Could not create rule from event.')
|
||||
return
|
||||
|
||||
ShowMoreGUI(profile_path, out, profile_path is not None).show()
|
||||
|
||||
|
||||
def cb_add_to_profile(notification, action, _args):
|
||||
(raw_ev, rl, special_profiles) = _args
|
||||
notification.close()
|
||||
parsed_event = rl.parse_record(raw_ev)
|
||||
|
||||
rule = rl.create_rule_from_ev(parsed_event)
|
||||
|
||||
if not rule:
|
||||
ErrorGUI.show(_('ERROR: Could not create rule from event.'))
|
||||
return
|
||||
|
||||
aa.update_profiles()
|
||||
|
||||
if customized_message['userns']['cond'](parsed_event, special_profiles):
|
||||
if parsed_event['execpath'] is None:
|
||||
UsernsGUI.show_error_cannot_find_execpath(parsed_event['comm'], os.path.dirname(os.path.abspath(__file__)) + '/default_unconfined.template')
|
||||
return
|
||||
profile_path = aa.get_profile_filename_from_profile_name(parsed_event['comm'], True)
|
||||
if not profile_path:
|
||||
ErrorGUI(_('Cannot get profile path for {}.').format(parsed_event['comm']), False).show()
|
||||
return
|
||||
ask_for_user_ns_denied(parsed_event['execpath'], parsed_event['comm'], profile_path)
|
||||
else:
|
||||
try:
|
||||
AddProfileGUI(rule.get_clean(), parsed_event['profile']).show()
|
||||
except AppArmorException:
|
||||
AddProfileGUI.show_error_cannot_find_profile(parsed_event['profile'])
|
||||
|
||||
|
||||
customized_message = {
|
||||
'userns': {
|
||||
'cond': lambda ev, special_profiles: (ev['operation'] == 'userns_create' or ev['operation'] == 'capable') and is_special_profile_userns(ev, special_profiles),
|
||||
'msg': 'Application {0} wants to create an user namespace which could be used to compromise your system\nDo you want to allow it next time {0} is run?'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def customize_notification_message(ev, msg, special_profiles):
|
||||
if customized_message['userns']['cond'](ev, special_profiles):
|
||||
msg = _(customized_message['userns']['msg']).format(ev['comm'])
|
||||
|
||||
return msg
|
||||
|
||||
|
||||
def start_glib_loop():
|
||||
loop = GLib.MainLoop()
|
||||
loop.run()
|
||||
|
||||
|
||||
def main():
|
||||
"""Run aa-notify.
|
||||
|
||||
|
@ -432,6 +601,7 @@ def main():
|
|||
parser.add_argument('-v', '--verbose', action='store_true', help=_('show messages with stats'))
|
||||
parser.add_argument('-u', '--user', type=str, help=_('user to drop privileges to when not using sudo'))
|
||||
parser.add_argument('-w', '--wait', type=int, metavar=('NUM'), help=_('wait NUM seconds before displaying notifications (with -p)'))
|
||||
parser.add_argument('--prompt-filter', type=str, metavar=('PF'), help=_('kind of operations which display a popup prompt'))
|
||||
parser.add_argument('--debug', action='store_true', help=_('debug mode'))
|
||||
parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
|
||||
|
||||
|
@ -476,6 +646,15 @@ def main():
|
|||
else: # fallback to env variable (or None if not set)
|
||||
confdir = os.getenv('__AA_CONFDIR')
|
||||
|
||||
# Todo: add more kinds of notifications
|
||||
supported_prompt_filter = {'userns'}
|
||||
|
||||
if args.prompt_filter:
|
||||
args.prompt_filter = set(args.prompt_filter.strip().split(','))
|
||||
unsupported = args.prompt_filter - supported_prompt_filter
|
||||
if unsupported:
|
||||
sys.exit(_('ERROR: using an unsupported prompt filter: {}\nSupported values: {}').format(', '.join(unsupported), ', '.join(supported_prompt_filter)))
|
||||
|
||||
aa.init_aa(confdir=confdir)
|
||||
|
||||
# Initialize aa.logfile
|
||||
|
@ -511,6 +690,9 @@ def main():
|
|||
- message_body
|
||||
- message_footer
|
||||
- use_group
|
||||
- userns_special_profiles
|
||||
- ignore_denied_capability
|
||||
- interface_theme
|
||||
- filter.profile,
|
||||
- filter.operation,
|
||||
- filter.name,
|
||||
|
@ -524,6 +706,9 @@ def main():
|
|||
# Warn about unknown keys in the config
|
||||
allowed_config_keys = [
|
||||
'use_group',
|
||||
'userns_special_profiles',
|
||||
'ignore_denied_capability',
|
||||
'interface_theme',
|
||||
'show_notifications',
|
||||
'message_body',
|
||||
'message_footer',
|
||||
|
@ -598,6 +783,21 @@ def main():
|
|||
# @TODO: Extend UI lib to have warning and error functions that
|
||||
# can be used in an uniform way with both text and JSON output.
|
||||
|
||||
if 'userns_special_profiles' in config['']:
|
||||
userns_special_profiles = config['']['userns_special_profiles'].strip().split(',')
|
||||
else:
|
||||
userns_special_profiles = ['unconfined'] # By default, unconfined is the only special profile
|
||||
|
||||
if 'ignore_denied_capability' in config['']:
|
||||
ignore_denied_capability = config['']['ignore_denied_capability'].strip().split(',')
|
||||
else:
|
||||
ignore_denied_capability = ['sudo', 'su']
|
||||
|
||||
if 'interface_theme' in config['']:
|
||||
set_interface_theme(config['']['interface_theme'].strip())
|
||||
else:
|
||||
set_interface_theme('ubuntu')
|
||||
|
||||
if args.file:
|
||||
logfile = args.file
|
||||
elif os.path.isfile('/var/run/auditd.pid') and os.path.isfile('/var/log/audit/audit.log'):
|
||||
|
@ -628,12 +828,26 @@ def main():
|
|||
if not args.user and os.getuid() == 0 and 'SUDO_USER' not in os.environ.keys():
|
||||
sys.exit("ERROR: Cannot be started a real root user. Use --user to define what user to use.")
|
||||
|
||||
# Required to parse_record.
|
||||
rl = ReadLog('', '', '')
|
||||
|
||||
# At this point this script needs to be able to read 'logfile' but once
|
||||
# the for loop starts, privileges can be dropped since the file descriptor
|
||||
# has been opened and access granted. Further reads of the file will not
|
||||
# trigger any new permission checks.
|
||||
# @TODO Plan to catch PermissionError here or..?
|
||||
for message in notify_about_new_entries(logfile, filters, args.wait):
|
||||
for (event, message) in notify_about_new_entries(logfile, filters, args.wait):
|
||||
ev = rl.parse_record(event)
|
||||
|
||||
# @TODO redo special behaviours with a more regular function
|
||||
# We ignore capability denials for binaries in ignore_denied_capability
|
||||
if ev['operation'] == 'capable' and ev['comm'] in ignore_denied_capability:
|
||||
continue
|
||||
|
||||
# Special behaivor for userns:
|
||||
if args.prompt_filter and 'userns' in args.prompt_filter and customized_message['userns']['cond'](ev, userns_special_profiles):
|
||||
if prompt_userns(ev, userns_special_profiles):
|
||||
continue # Notification already displayed for this event, we go to the next one.
|
||||
|
||||
# Notifications should not be run as root, since root probably is
|
||||
# the wrong desktop user and not the one getting the notifications.
|
||||
|
@ -643,16 +857,17 @@ def main():
|
|||
if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
|
||||
os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/run/user/{}/bus'.format(os.geteuid())
|
||||
|
||||
# Before use, notify2 must be initialized and the DBUS channel
|
||||
# should be opened using the non-root user. This step needs to
|
||||
# be executed after the drop_privileges().
|
||||
notify2.init('AppArmor')
|
||||
message = customize_notification_message(ev, message, userns_special_profiles)
|
||||
|
||||
n = notify2.Notification(
|
||||
_('AppArmor notification'),
|
||||
_('AppArmor security notice'),
|
||||
message,
|
||||
'gtk-dialog-warning'
|
||||
)
|
||||
|
||||
n.add_action('clicked', 'Allow', cb_add_to_profile, (event, rl, userns_special_profiles))
|
||||
n.add_action('more_clicked', 'Show More', cb_more_info, (event, rl))
|
||||
|
||||
n.show()
|
||||
|
||||
# When notification is sent, raise privileged back to root if the
|
||||
|
|
|
@ -1607,6 +1607,14 @@ def collapse_log(hashlog, ignore_null_profiles=True):
|
|||
return log_dict
|
||||
|
||||
|
||||
def update_profiles(ui_msg=False, skip_profiles=()):
|
||||
reset_aa()
|
||||
try:
|
||||
read_profiles(ui_msg, skip_profiles)
|
||||
except AppArmorException as e:
|
||||
print(_("Error while loading profiles: {}").format(e))
|
||||
|
||||
|
||||
def read_profiles(ui_msg=False, skip_profiles=()):
|
||||
# we'll read all profiles from disk, so reset the storage first (autodep() might have created/stored
|
||||
# a profile already, which would cause a 'Conflicting profile' error in attach_profile_data())
|
||||
|
|
233
utils/apparmor/gui.py
Normal file
233
utils/apparmor/gui.py
Normal file
|
@ -0,0 +1,233 @@
|
|||
import os
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
import tkinter.messagebox as messagebox
|
||||
import tkinter.font as font
|
||||
import subprocess
|
||||
import ttkthemes
|
||||
|
||||
import apparmor.aa as aa
|
||||
import apparmor.update_profile as update_profile
|
||||
from apparmor.common import AppArmorException
|
||||
|
||||
from apparmor.translations import init_translation
|
||||
|
||||
_ = init_translation()
|
||||
|
||||
notification_custom_msg = {
|
||||
'userns': _('Application {0} wants to create an user namespace which could be used to compromise your system\nDo you want to allow it next time {0} is run?\nApplication path is {1}')
|
||||
}
|
||||
|
||||
global interface_theme
|
||||
|
||||
|
||||
class GUI:
|
||||
def __init__(self):
|
||||
try:
|
||||
self.master = tk.Tk()
|
||||
except tk.TclError:
|
||||
print(_('ERROR: Cannot initialize Tkinter. Please check that your terminal can use a graphical interface'))
|
||||
os._exit(1)
|
||||
|
||||
style = ttkthemes.ThemedStyle(self.master)
|
||||
style.theme_use(interface_theme)
|
||||
self.bg_color = style.lookup('TLabel', 'background')
|
||||
self.master.configure(background=self.bg_color)
|
||||
|
||||
self.label_frame = ttk.Frame(self.master, padding=(20, 10))
|
||||
self.label_frame.pack()
|
||||
|
||||
self.button_frame = ttk.Frame(self.master, padding=(0, 10))
|
||||
self.button_frame.pack()
|
||||
|
||||
|
||||
class AddProfileGUI(GUI):
|
||||
def __init__(self, rule, profile_name, dbg=None):
|
||||
self.dbg = dbg
|
||||
self.rule = rule
|
||||
self.profile_name = profile_name
|
||||
self.profile_path = aa.get_profile_filename_from_profile_name(profile_name)
|
||||
|
||||
if not self.profile_path:
|
||||
raise AppArmorException('Cannot find profile for {}'.format(self.profile_name))
|
||||
super().__init__()
|
||||
|
||||
self.master.title(_('AppArmor - Add rule to profile'))
|
||||
|
||||
self.profile_label = ttk.Label(self.label_frame, text=_('Profile for: {}').format(self.profile_name))
|
||||
self.profile_label.pack()
|
||||
|
||||
self.entry_frame = ttk.Frame(self.master)
|
||||
self.entry_frame.pack()
|
||||
|
||||
self.rule = tk.StringVar(value=self.rule)
|
||||
self.rule_entry = ttk.Entry(self.entry_frame, font=font.nametofont("TkDefaultFont"), width=50, textvariable=self.rule)
|
||||
self.rule_entry.pack(side=tk.LEFT)
|
||||
|
||||
self.button_frame = ttk.Frame(self.master, padding=(10, 10))
|
||||
self.button_frame.pack()
|
||||
|
||||
self.add_to_profile_button = ttk.Button(self.button_frame, text=_('Add to Profile'), command=self.add_to_profile)
|
||||
self.add_to_profile_button.pack(side=tk.LEFT)
|
||||
|
||||
self.show_profile_button = ttk.Button(self.button_frame, text=_('Show Current Profile'), command=lambda: open_with_default_editor(self.profile_path))
|
||||
self.show_profile_button.pack(side=tk.LEFT)
|
||||
|
||||
self.cancel_button = ttk.Button(self.button_frame, text=_('Cancel'), command=self.master.destroy)
|
||||
self.cancel_button.pack(side=tk.LEFT)
|
||||
|
||||
def add_to_profile(self):
|
||||
if self.dbg:
|
||||
self.dbg.debug('Adding rule \'{}\' to profile at: {}'.format(self.rule.get(), self.profile_name))
|
||||
|
||||
# We get update_profile.py through this import so that it works in all cases
|
||||
update_profile_path = update_profile.__file__
|
||||
command = ['pkexec', '--keep-cwd', update_profile_path, 'add_rule', self.rule.get(), self.profile_name]
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if e.returncode != 126: # return code 126 means the user cancelled the request
|
||||
ErrorGUI(_('Failed to add rule {} to {}\n Error code = {}').format(self.rule.get(), self.profile_name, e.returncode), False).show()
|
||||
|
||||
self.master.destroy()
|
||||
|
||||
def show(self):
|
||||
self.master.mainloop()
|
||||
|
||||
@staticmethod
|
||||
def show_error_cannot_find_profile(profile_name):
|
||||
ErrorGUI(
|
||||
_(
|
||||
'Cannot find profile for {}\n\n'
|
||||
'It is likely that the profile was not stored in {} or was removed.'
|
||||
).format(profile_name, aa.profile_dir),
|
||||
False
|
||||
).show()
|
||||
|
||||
|
||||
class ShowMoreGUI(GUI):
|
||||
def __init__(self, profile_path, msg, profile_found=True):
|
||||
self.profile_path = profile_path
|
||||
self.msg = msg
|
||||
self.profile_found = profile_found
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.master.title(_('AppArmor - More info'))
|
||||
|
||||
self.label = tk.Label(self.label_frame, background=self.bg_color, text=self.msg, anchor='w', justify=tk.LEFT, wraplength=460)
|
||||
self.label.pack()
|
||||
|
||||
if self.profile_found:
|
||||
self.show_profile_button = ttk.Button(self.button_frame, text=_('Show Current Profile'), command=lambda: open_with_default_editor(self.profile_path))
|
||||
self.show_profile_button.pack()
|
||||
|
||||
def show(self):
|
||||
self.master.mainloop()
|
||||
|
||||
|
||||
class UsernsGUI(GUI):
|
||||
def __init__(self, name, path):
|
||||
self.name = name
|
||||
self.path = path
|
||||
self.result = None
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.master.title(_('AppArmor - User namespace creation restricted'))
|
||||
|
||||
label_text = notification_custom_msg['userns'].format(name, path)
|
||||
self.label = ttk.Label(self.label_frame, text=label_text, wraplength=460)
|
||||
self.label.pack()
|
||||
link = ttk.Label(self.master, text=_('More information'), foreground='blue', cursor='hand2')
|
||||
link.pack()
|
||||
link.bind('<Button-1>', self.more_info)
|
||||
|
||||
self.add_policy_button = ttk.Button(self.master, text=_('Allow'), command=lambda: self.set_result("allow"))
|
||||
self.add_policy_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
self.never_ask_button = ttk.Button(self.master, text=_('Deny'), command=lambda: self.set_result("deny"))
|
||||
self.never_ask_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
self.do_nothing_button = ttk.Button(self.master, text=_('Do nothing'), command=self.master.destroy)
|
||||
self.do_nothing_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
@staticmethod
|
||||
def more_info(ev):
|
||||
more_info_text = _("""
|
||||
In Linux, user namespaces enable non-root users to perform certain privileged operations. This feature can be useful for several legitimate use cases.
|
||||
|
||||
However, this feature also introduces security risks, (e.g. privilege escalation exploits).
|
||||
|
||||
This dialog allows you to choose whether you want to enable user namespaces for this application.""")
|
||||
messagebox.showinfo(
|
||||
_('AppArmor: More info'),
|
||||
more_info_text,
|
||||
)
|
||||
|
||||
def set_result(self, result):
|
||||
self.result = result
|
||||
self.master.destroy()
|
||||
|
||||
def show(self):
|
||||
self.master.mainloop()
|
||||
return self.result
|
||||
|
||||
@staticmethod
|
||||
def show_error_cannot_reload_profile(profile_path, error):
|
||||
ErrorGUI(_('Failed to create or load profile {}\n Error code = {}').format(profile_path, error), False).show()
|
||||
|
||||
@staticmethod
|
||||
def show_error_cannot_find_execpath(name, template_path):
|
||||
ErrorGUI(
|
||||
_(
|
||||
'Application {0} wants to create an user namespace which could be used to compromise your system\n\n'
|
||||
'However, apparmor cannot find {0}. If you want to allow it, please create a profile for it.\n\n'
|
||||
'A profile template is in {1}\n Profiles are in {2}'
|
||||
).format(name, template_path, aa.profile_dir),
|
||||
False
|
||||
).show()
|
||||
|
||||
|
||||
class ErrorGUI(GUI):
|
||||
def __init__(self, msg, is_fatal):
|
||||
self.msg = msg
|
||||
self.is_fatal = is_fatal
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.master.title('AppArmor Error')
|
||||
|
||||
# Create label to display the error message
|
||||
self.label = ttk.Label(self.label_frame, background=self.bg_color, text=self.msg, wraplength=460)
|
||||
self.label.pack()
|
||||
|
||||
# Create a button to close the dialog
|
||||
self.button = ttk.Button(self.button_frame, text="OK", command=self.destroy)
|
||||
self.button.pack()
|
||||
|
||||
def destroy(self):
|
||||
self.master.destroy()
|
||||
|
||||
if self.is_fatal:
|
||||
os._exit(1)
|
||||
|
||||
def show(self):
|
||||
self.master.mainloop()
|
||||
if self.is_fatal:
|
||||
os._exit(1)
|
||||
|
||||
|
||||
def set_interface_theme(theme):
|
||||
global interface_theme
|
||||
interface_theme = theme
|
||||
|
||||
|
||||
def open_with_default_editor(profile_path):
|
||||
try:
|
||||
default_app = subprocess.run(['xdg-mime', 'query', 'default', 'text/plain'], capture_output=True, text=True, check=True).stdout.strip()
|
||||
subprocess.run(['gtk-launch', default_app, profile_path], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
ErrorGUI(_('Failed to launch default editor'), False).show()
|
||||
except FileNotFoundError as e:
|
||||
ErrorGUI(_('Failed to open file: {}').format(e), False).show()
|
80
utils/apparmor/update_profile.py
Executable file
80
utils/apparmor/update_profile.py
Executable file
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# TODO: transform this script to a package to use local imports so that if called with ./aa-notify, we use ./apparmor.*
|
||||
from apparmor import aa
|
||||
from apparmor.logparser import ReadLog
|
||||
|
||||
from apparmor.translations import init_translation
|
||||
_ = init_translation()
|
||||
|
||||
|
||||
def create_userns(template_path, name, bin_path, profile_path, decision):
|
||||
with open(template_path, 'r') as f:
|
||||
profile_template = f.read()
|
||||
|
||||
rule = 'userns' if decision == 'allow' else 'audit deny userns'
|
||||
profile = profile_template.format(rule=rule, name=name, path=bin_path)
|
||||
|
||||
with open(profile_path, 'w') as file:
|
||||
file.write(profile)
|
||||
|
||||
try:
|
||||
subprocess.run(['apparmor_parser', '-r', profile_path], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
exit(_('Cannot reload updated profile'))
|
||||
|
||||
|
||||
def add_to_profile(rule, profile_name):
|
||||
aa.init_aa()
|
||||
aa.read_profiles()
|
||||
|
||||
rule_type, rule_class = ReadLog('', '', '').get_rule_type(rule)
|
||||
|
||||
rule_obj = rule_class.create_instance(rule)
|
||||
|
||||
if profile_name not in aa.aa or profile_name not in aa.aa[profile_name]:
|
||||
exit(_('Cannot find {} in profiles').format(profile_name))
|
||||
aa.aa[profile_name][profile_name][rule_type].add(rule_obj, cleanup=True)
|
||||
|
||||
# Save changes
|
||||
aa.write_profile_ui_feedback(profile_name)
|
||||
aa.reload_base(profile_name)
|
||||
|
||||
|
||||
def usage(is_help):
|
||||
print('This tool is a low level tool - do not use it directly')
|
||||
print('{} create_userns <template_path> <name> <bin_path> <profile_path> <decision>'.format(sys.argv[0]))
|
||||
print('{} add_rule <rule> <profile_name>'.format(sys.argv[0]))
|
||||
|
||||
if is_help:
|
||||
exit(0)
|
||||
else:
|
||||
exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
command = sys.argv[1]
|
||||
else:
|
||||
command = None # Handle the case where no command is provided
|
||||
|
||||
match command:
|
||||
case 'create_userns':
|
||||
if not len(sys.argv) == 7:
|
||||
usage(False)
|
||||
create_userns(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5], sys.argv[6])
|
||||
case 'add_rule':
|
||||
if not len(sys.argv) == 4:
|
||||
usage(False)
|
||||
add_to_profile(sys.argv[2], sys.argv[3])
|
||||
case 'help':
|
||||
usage(True)
|
||||
case _:
|
||||
usage(False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
30
utils/com.ubuntu.pkexec.aa-notify.policy
Normal file
30
utils/com.ubuntu.pkexec.aa-notify.policy
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE policyconfig PUBLIC
|
||||
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
|
||||
<policyconfig>
|
||||
|
||||
<action id="com.ubuntu.pkexec.aa-notify.modify_profile">
|
||||
<description>AppArmor: modifying security profile</description>
|
||||
<message>To modify an AppArmor security profile, you need to authenticate.</message>
|
||||
<defaults>
|
||||
<allow_any>auth_admin</allow_any>
|
||||
<allow_inactive>auth_admin</allow_inactive>
|
||||
<allow_active>auth_admin</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">{LIB_PATH}apparmor/update_profile.py</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.argv1">add_rule</annotate>
|
||||
</action>
|
||||
<action id="com.ubuntu.pkexec.aa-notify.create_userns">
|
||||
<description>AppArmor: adding userns profile</description>
|
||||
<message>To allow a program to use unprivileged user namespaces, you need to authenticate.</message>
|
||||
<defaults>
|
||||
<allow_any>auth_admin</allow_any>
|
||||
<allow_inactive>auth_admin</allow_inactive>
|
||||
<allow_active>auth_admin</allow_active>
|
||||
</defaults>
|
||||
<annotate key="org.freedesktop.policykit.exec.path">{LIB_PATH}apparmor/update_profile.py</annotate>
|
||||
<annotate key="org.freedesktop.policykit.exec.argv1">create_userns</annotate>
|
||||
</action>
|
||||
|
||||
</policyconfig>
|
14
utils/default_unconfined.template
Normal file
14
utils/default_unconfined.template
Normal file
|
@ -0,0 +1,14 @@
|
|||
# This profile was auto-generated by AppArmor
|
||||
# This profile allows everything and only exists to give the
|
||||
# application the possibility to use unprivileged user namespaces
|
||||
|
||||
abi <abi/4.0>,
|
||||
include <tunables/global>
|
||||
|
||||
|
||||
profile {name} {path} flags=(unconfined) {{
|
||||
{rule},
|
||||
|
||||
# Site-specific additions and overrides. See local/README for details.
|
||||
include if exists <local/{name}>
|
||||
}}
|
|
@ -11,6 +11,15 @@
|
|||
# Set to 'no' to disable AppArmor notifications globally
|
||||
show_notifications="yes"
|
||||
|
||||
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
|
||||
userns_special_profiles="unconfined,unprivileged_userns"
|
||||
# Theme to use for aa-notify GUI themes. See https://ttkthemes.readthedocs.io/en/latest/themes.html for available themes.
|
||||
interface_theme="ubuntu"
|
||||
|
||||
# Binaries for which we ignore userns-related capability denials
|
||||
ignore_denied_capability="sudo,su"
|
||||
|
||||
|
||||
# OPTIONAL - restrict using aa-notify to users in the given group
|
||||
# (if not set, everybody who has permissions to read the logfile can use it)
|
||||
# use_group="admin"
|
||||
|
|
|
@ -55,6 +55,24 @@ class Install(_install):
|
|||
for d in data:
|
||||
self.copy_tree(d, os.path.join(prefix + "/usr/share/apparmor/easyprof", os.path.basename(d)))
|
||||
|
||||
# Make update_profile.py executable
|
||||
update_profile_path = os.path.join(self.install_lib, 'apparmor/update_profile.py')
|
||||
print('changing mode of {} to 755'.format(update_profile_path))
|
||||
os.chmod(update_profile_path, 0o755)
|
||||
|
||||
pkexec_action_name = 'com.ubuntu.pkexec.aa-notify.policy'
|
||||
print('Installing {} to /usr/share/polkit-1/actions/ mode 644'.format(pkexec_action_name))
|
||||
with open(pkexec_action_name, 'r') as f:
|
||||
polkit_template = f.read()
|
||||
|
||||
polkit = polkit_template.format(LIB_PATH=self.install_lib)
|
||||
|
||||
if not os.path.exists(prefix + '/usr/share/polkit-1/actions/'):
|
||||
os.mkdir(prefix + '/usr/share/polkit-1/actions/')
|
||||
with open(prefix + '/usr/share/polkit-1/actions/' + pkexec_action_name, 'w') as f:
|
||||
f.write(polkit)
|
||||
os.chmod(prefix + '/usr/share/polkit-1/actions/' + pkexec_action_name, 0o644)
|
||||
|
||||
|
||||
if os.path.exists('staging'):
|
||||
shutil.rmtree('staging')
|
||||
|
|
|
@ -184,10 +184,10 @@ class AANotifyTest(AANotifyBase):
|
|||
expected_return_code = 0
|
||||
expected_output_1 = \
|
||||
'''usage: aa-notify [-h] [-p] [--display DISPLAY] [-f FILE] [-l] [-s NUM] [-v]
|
||||
[-u USER] [-w NUM] [--debug] [--filter.profile PROFILE]
|
||||
[--filter.operation OPERATION] [--filter.name NAME]
|
||||
[--filter.denied DENIED] [--filter.family FAMILY]
|
||||
[--filter.socket SOCKET]
|
||||
[-u USER] [-w NUM] [--prompt-filter PF] [--debug]
|
||||
[--filter.profile PROFILE] [--filter.operation OPERATION]
|
||||
[--filter.name NAME] [--filter.denied DENIED]
|
||||
[--filter.family FAMILY] [--filter.socket SOCKET]
|
||||
|
||||
Display AppArmor notifications or messages for DENIED entries.
|
||||
''' # noqa: E128
|
||||
|
@ -207,6 +207,7 @@ Display AppArmor notifications or messages for DENIED entries.
|
|||
-u USER, --user USER user to drop privileges to when not using sudo
|
||||
-w NUM, --wait NUM wait NUM seconds before displaying notifications (with
|
||||
-p)
|
||||
--prompt-filter PF kind of operations which display a popup prompt
|
||||
--debug debug mode
|
||||
|
||||
Filtering options:
|
||||
|
|
Loading…
Add table
Reference in a new issue