mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 08:24:42 +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:
|
extends:
|
||||||
- .ubuntu-before_script
|
- .ubuntu-before_script
|
||||||
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
|
# See apparmor/apparmor#221
|
||||||
- make -C parser/tst gen_dbus
|
- make -C parser/tst gen_dbus
|
||||||
- make -C parser/tst gen_xtrans
|
- 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):
|
following packages (Debian package names, other distros may vary):
|
||||||
* python3-notify2
|
* python3-notify2
|
||||||
* python3-psutil
|
* python3-psutil
|
||||||
|
* python3-tk
|
||||||
|
* python3-ttkthemes
|
||||||
|
* python3-gi
|
||||||
|
|
||||||
Perl is no longer needed since none of the utilities shipped to end users depend
|
Perl is no longer needed since none of the utilities shipped to end users depend
|
||||||
on it anymore.
|
on it anymore.
|
||||||
|
|
|
@ -51,7 +51,7 @@ po/${NAME}.pot: ${TOOLS} ${PYMODULES}
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
install: ${MANPAGES} ${HTMLMANPAGES}
|
install: ${MANPAGES} ${HTMLMANPAGES}
|
||||||
install -d ${CONFDIR}
|
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}
|
install -d ${BINDIR}
|
||||||
# aa-easyprof is installed by python-tools-setup.py
|
# aa-easyprof is installed by python-tools-setup.py
|
||||||
install -m 755 $(filter-out aa-easyprof, ${TOOLS}) ${BINDIR}
|
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 sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import notify2
|
import notify2
|
||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
import apparmor.aa as aa
|
import apparmor.aa as aa
|
||||||
import apparmor.ui as aaui
|
import apparmor.ui as aaui
|
||||||
import apparmor.config as aaconfig
|
import apparmor.config as aaconfig
|
||||||
|
import apparmor.update_profile as update_profile
|
||||||
import LibAppArmor # C-library to parse one log line
|
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.fail import enable_aa_exception_handler
|
||||||
from apparmor.notify import get_last_login_timestamp
|
from apparmor.notify import get_last_login_timestamp
|
||||||
from apparmor.translations import init_translation
|
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():
|
def get_user_login():
|
||||||
|
@ -124,11 +136,23 @@ def notify_about_new_entries(logfile, filters, wait=0):
|
||||||
# Follow the logfile and stream notifications
|
# Follow the logfile and stream notifications
|
||||||
# Rate limit to not show too many notifications
|
# Rate limit to not show too many notifications
|
||||||
try:
|
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):
|
for event in follow_apparmor_events(logfile, wait):
|
||||||
if not is_event_in_filter(event, filters):
|
if not is_event_in_filter(event, filters):
|
||||||
continue
|
continue
|
||||||
debug_logger.info(format_event(event, logfile))
|
debug_logger.info(format_event(event, logfile))
|
||||||
yield (format_event(event, logfile))
|
yield event, format_event(event, logfile)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
sys.exit(_("ERROR: Cannot read {}. Please check permissions.").format(logfile))
|
sys.exit(_("ERROR: Cannot read {}. Please check permissions.").format(logfile))
|
||||||
|
|
||||||
|
@ -396,6 +420,151 @@ def compile_filter_regex(filters):
|
||||||
return 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():
|
def main():
|
||||||
"""Run aa-notify.
|
"""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('-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('-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('-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('--debug', action='store_true', help=_('debug mode'))
|
||||||
parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
|
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)
|
else: # fallback to env variable (or None if not set)
|
||||||
confdir = os.getenv('__AA_CONFDIR')
|
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)
|
aa.init_aa(confdir=confdir)
|
||||||
|
|
||||||
# Initialize aa.logfile
|
# Initialize aa.logfile
|
||||||
|
@ -511,6 +690,9 @@ def main():
|
||||||
- message_body
|
- message_body
|
||||||
- message_footer
|
- message_footer
|
||||||
- use_group
|
- use_group
|
||||||
|
- userns_special_profiles
|
||||||
|
- ignore_denied_capability
|
||||||
|
- interface_theme
|
||||||
- filter.profile,
|
- filter.profile,
|
||||||
- filter.operation,
|
- filter.operation,
|
||||||
- filter.name,
|
- filter.name,
|
||||||
|
@ -524,6 +706,9 @@ def main():
|
||||||
# Warn about unknown keys in the config
|
# Warn about unknown keys in the config
|
||||||
allowed_config_keys = [
|
allowed_config_keys = [
|
||||||
'use_group',
|
'use_group',
|
||||||
|
'userns_special_profiles',
|
||||||
|
'ignore_denied_capability',
|
||||||
|
'interface_theme',
|
||||||
'show_notifications',
|
'show_notifications',
|
||||||
'message_body',
|
'message_body',
|
||||||
'message_footer',
|
'message_footer',
|
||||||
|
@ -598,6 +783,21 @@ def main():
|
||||||
# @TODO: Extend UI lib to have warning and error functions that
|
# @TODO: Extend UI lib to have warning and error functions that
|
||||||
# can be used in an uniform way with both text and JSON output.
|
# 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:
|
if args.file:
|
||||||
logfile = args.file
|
logfile = args.file
|
||||||
elif os.path.isfile('/var/run/auditd.pid') and os.path.isfile('/var/log/audit/audit.log'):
|
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():
|
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.")
|
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
|
# 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
|
# 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
|
# has been opened and access granted. Further reads of the file will not
|
||||||
# trigger any new permission checks.
|
# trigger any new permission checks.
|
||||||
# @TODO Plan to catch PermissionError here or..?
|
# @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
|
# Notifications should not be run as root, since root probably is
|
||||||
# the wrong desktop user and not the one getting the notifications.
|
# 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:
|
if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
|
||||||
os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/run/user/{}/bus'.format(os.geteuid())
|
os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/run/user/{}/bus'.format(os.geteuid())
|
||||||
|
|
||||||
# Before use, notify2 must be initialized and the DBUS channel
|
message = customize_notification_message(ev, message, userns_special_profiles)
|
||||||
# should be opened using the non-root user. This step needs to
|
|
||||||
# be executed after the drop_privileges().
|
|
||||||
notify2.init('AppArmor')
|
|
||||||
|
|
||||||
n = notify2.Notification(
|
n = notify2.Notification(
|
||||||
_('AppArmor notification'),
|
_('AppArmor security notice'),
|
||||||
message,
|
message,
|
||||||
'gtk-dialog-warning'
|
'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()
|
n.show()
|
||||||
|
|
||||||
# When notification is sent, raise privileged back to root if the
|
# 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
|
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=()):
|
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
|
# 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())
|
# 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
|
# Set to 'no' to disable AppArmor notifications globally
|
||||||
show_notifications="yes"
|
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
|
# 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)
|
# (if not set, everybody who has permissions to read the logfile can use it)
|
||||||
# use_group="admin"
|
# use_group="admin"
|
||||||
|
|
|
@ -55,6 +55,24 @@ class Install(_install):
|
||||||
for d in data:
|
for d in data:
|
||||||
self.copy_tree(d, os.path.join(prefix + "/usr/share/apparmor/easyprof", os.path.basename(d)))
|
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'):
|
if os.path.exists('staging'):
|
||||||
shutil.rmtree('staging')
|
shutil.rmtree('staging')
|
||||||
|
|
|
@ -184,10 +184,10 @@ class AANotifyTest(AANotifyBase):
|
||||||
expected_return_code = 0
|
expected_return_code = 0
|
||||||
expected_output_1 = \
|
expected_output_1 = \
|
||||||
'''usage: aa-notify [-h] [-p] [--display DISPLAY] [-f FILE] [-l] [-s NUM] [-v]
|
'''usage: aa-notify [-h] [-p] [--display DISPLAY] [-f FILE] [-l] [-s NUM] [-v]
|
||||||
[-u USER] [-w NUM] [--debug] [--filter.profile PROFILE]
|
[-u USER] [-w NUM] [--prompt-filter PF] [--debug]
|
||||||
[--filter.operation OPERATION] [--filter.name NAME]
|
[--filter.profile PROFILE] [--filter.operation OPERATION]
|
||||||
[--filter.denied DENIED] [--filter.family FAMILY]
|
[--filter.name NAME] [--filter.denied DENIED]
|
||||||
[--filter.socket SOCKET]
|
[--filter.family FAMILY] [--filter.socket SOCKET]
|
||||||
|
|
||||||
Display AppArmor notifications or messages for DENIED entries.
|
Display AppArmor notifications or messages for DENIED entries.
|
||||||
''' # noqa: E128
|
''' # 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
|
-u USER, --user USER user to drop privileges to when not using sudo
|
||||||
-w NUM, --wait NUM wait NUM seconds before displaying notifications (with
|
-w NUM, --wait NUM wait NUM seconds before displaying notifications (with
|
||||||
-p)
|
-p)
|
||||||
|
--prompt-filter PF kind of operations which display a popup prompt
|
||||||
--debug debug mode
|
--debug debug mode
|
||||||
|
|
||||||
Filtering options:
|
Filtering options:
|
||||||
|
|
Loading…
Add table
Reference in a new issue