mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 08:24:42 +01:00
aa-notify: Simplify user interfaces and update man page
This commit is contained in:
parent
37cac653d1
commit
2b32130280
10 changed files with 242 additions and 167 deletions
138
utils/aa-notify
138
utils/aa-notify
|
@ -49,12 +49,13 @@ import apparmor.ui as aaui
|
||||||
import apparmor.config as aaconfig
|
import apparmor.config as aaconfig
|
||||||
import apparmor.update_profile as update_profile
|
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, AppArmorException
|
from apparmor.common import DebugLogger, open_file_read
|
||||||
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.logparser import ReadLog
|
||||||
from apparmor.gui import UsernsGUI, AddProfileGUI, ErrorGUI, ShowMoreGUI, set_interface_theme
|
from apparmor.gui import UsernsGUI, ErrorGUI, ShowMoreGUI, set_interface_theme
|
||||||
|
from apparmor.rule.file import FileRule
|
||||||
|
|
||||||
from dbus import DBusException
|
from dbus import DBusException
|
||||||
import gi
|
import gi
|
||||||
|
@ -420,6 +421,13 @@ def compile_filter_regex(filters):
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
|
def can_allow_rule(ev, special_profiles):
|
||||||
|
if customized_message['userns']['cond'](ev, special_profiles):
|
||||||
|
return not aa.get_profile_filename_from_profile_name(ev['comm'])
|
||||||
|
else:
|
||||||
|
return aa.get_profile_filename_from_profile_name(ev['profile']) is not None
|
||||||
|
|
||||||
|
|
||||||
def is_special_profile_userns(ev, special_profiles):
|
def is_special_profile_userns(ev, special_profiles):
|
||||||
if not special_profiles or ev['profile'] not in 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
|
return False # We don't use special profiles or there is already a profile defined: we don't ask to add userns
|
||||||
|
@ -430,23 +438,41 @@ def is_special_profile_userns(ev, special_profiles):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def ask_for_user_ns_denied(path, name, profile_path):
|
def create_userns_profile(name, path, ans):
|
||||||
gui = UsernsGUI(name, path)
|
|
||||||
ans = gui.show()
|
|
||||||
if ans in {'allow', 'deny'}:
|
|
||||||
update_profile_path = update_profile.__file__
|
update_profile_path = update_profile.__file__
|
||||||
|
|
||||||
local_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_unconfined.template')
|
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
|
if os.path.exists(local_template_path): # We are using local aa-notify -> we use local template
|
||||||
template_path = local_template_path
|
template_path = local_template_path
|
||||||
else:
|
else:
|
||||||
template_path = aa.CONFDIR + '/default_unconfined.template'
|
template_path = aa.CONFDIR + '/default_unconfined.template'
|
||||||
|
|
||||||
|
if path is None:
|
||||||
|
UsernsGUI.show_error_cannot_find_execpath(name, template_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
profile_path = aa.get_profile_filename_from_profile_name(name, True)
|
||||||
|
if not profile_path:
|
||||||
|
ErrorGUI(_('Cannot get profile path for {}.').format(name), False).show()
|
||||||
|
return
|
||||||
|
|
||||||
command = ['pkexec', '--keep-cwd', update_profile_path, 'create_userns', template_path, name, path, profile_path, ans]
|
command = ['pkexec', '--keep-cwd', update_profile_path, 'create_userns', template_path, name, path, profile_path, ans]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(command, check=True)
|
subprocess.run(command, check=True)
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
if e.returncode != 126: # return code 126 means the user cancelled the request
|
if e.returncode != 126: # return code 126 means the user cancelled the request
|
||||||
UsernsGUI.show_error_cannot_reload_profile(profile_path, e.returncode)
|
UsernsGUI.show_error_cannot_reload_profile(profile_path, e.returncode)
|
||||||
|
|
||||||
|
|
||||||
|
def ask_for_user_ns_denied(path, name, interactive=True):
|
||||||
|
if interactive:
|
||||||
|
gui = UsernsGUI(name, path)
|
||||||
|
ans = gui.show()
|
||||||
|
else:
|
||||||
|
ans = 'allow'
|
||||||
|
if ans in {'allow', 'deny'}:
|
||||||
|
create_userns_profile(name, path, ans)
|
||||||
else:
|
else:
|
||||||
debug_logger.debug('No action from the user for {}'.format(path))
|
debug_logger.debug('No action from the user for {}'.format(path))
|
||||||
|
|
||||||
|
@ -470,24 +496,20 @@ def prompt_userns(ev, special_profiles):
|
||||||
# but if execpath is not supported by the kernel it could also mean that we inferred a bad path
|
# 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.
|
# So we do nothing beyond showing this error.
|
||||||
ErrorGUI(
|
ErrorGUI(
|
||||||
_('Application {0} tried to create an user namespace, but a profile already exists with this name.\n'
|
_('Application {profile} 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'
|
'This is likely because there is several binaries named {profile} thus the path inferred by AppArmor ({inferred_path}) is not correct.\n'
|
||||||
'You should review your profiles (in {2}).').format(ev['comm'], ev['execpath'], aa.profile_dir),
|
'You should review your profiles (in {profile_dir}).').format(profile=ev['comm'], inferred_path=ev['execpath'], profile_dir=aa.profile_dir),
|
||||||
False).show()
|
False).show()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
profile_path = aa.get_profile_filename_from_profile_name(ev['comm'], True)
|
ask_for_user_ns_denied(ev['execpath'], ev['comm'])
|
||||||
if not profile_path:
|
|
||||||
return False
|
|
||||||
|
|
||||||
ask_for_user_ns_denied(ev['execpath'], ev['comm'], profile_path)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
# TODO reuse more code from aa-logprof in callbacks
|
# TODO reuse more code from aa-logprof in callbacks
|
||||||
def cb_more_info(notification, action, _args):
|
def cb_more_info(notification, action, _args):
|
||||||
(raw_ev, rl) = _args
|
(raw_ev, rl, special_profiles) = _args
|
||||||
notification.close()
|
notification.close()
|
||||||
|
|
||||||
parsed_event = rl.parse_record(raw_ev)
|
parsed_event = rl.parse_record(raw_ev)
|
||||||
|
@ -501,19 +523,54 @@ def cb_more_info(notification, action, _args):
|
||||||
|
|
||||||
rule = rl.create_rule_from_ev(parsed_event)
|
rule = rl.create_rule_from_ev(parsed_event)
|
||||||
|
|
||||||
|
# Exec events are created with the default FileRule.ANY_EXEC. We use Pix for actual rules
|
||||||
|
if type(rule) is FileRule and rule.exec_perms == FileRule.ANY_EXEC:
|
||||||
|
rule.exec_perms = 'Pix'
|
||||||
|
|
||||||
if rule:
|
if rule:
|
||||||
aa.update_profiles()
|
aa.update_profiles()
|
||||||
|
if customized_message['userns']['cond'](parsed_event, special_profiles):
|
||||||
|
profile_path = None
|
||||||
|
out += _('You may allow it through a dedicated unconfined profile for {}.').format(parsed_event['comm'])
|
||||||
|
else:
|
||||||
profile_path = aa.get_profile_filename_from_profile_name(parsed_event['profile'])
|
profile_path = aa.get_profile_filename_from_profile_name(parsed_event['profile'])
|
||||||
if profile_path:
|
if profile_path:
|
||||||
out += _('If you want to allow this operation you can add the line below in profile {}\n').format(profile_path)
|
out += _('If you want to allow this operation you can add the line below in profile {}\n').format(profile_path)
|
||||||
out += rule.get_clean()
|
out += rule.get_clean()
|
||||||
else:
|
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 += _('However {profile} is not in {profile_dir}\nIt is likely that the profile was not stored in {profile_dir} or was removed.\n').format(profile=parsed_event['profile'], profile_dir=aa.profile_dir)
|
||||||
else: # Should not happen
|
else: # Should not happen
|
||||||
out += _('ERROR: Could not create rule from event.')
|
out += _('ERROR: Could not create rule from event.')
|
||||||
return
|
return
|
||||||
|
|
||||||
ShowMoreGUI(profile_path, out, profile_path is not None).show()
|
ans = ShowMoreGUI(profile_path, out, rule.get_clean(), parsed_event['profile'], profile_path is not None).show()
|
||||||
|
if ans == 'add_rule':
|
||||||
|
add_to_profile(rule.get_clean(), parsed_event['profile'])
|
||||||
|
elif ans in {'allow', 'deny'}:
|
||||||
|
create_userns_profile(parsed_event['comm'], parsed_event['execpath'], ans)
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_profile(rule, profile_name):
|
||||||
|
# We get update_profile.py through this import so that it works in all cases
|
||||||
|
profile_path = aa.get_profile_filename_from_profile_name(profile_name)
|
||||||
|
|
||||||
|
if not profile_path:
|
||||||
|
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()
|
||||||
|
return
|
||||||
|
|
||||||
|
update_profile_path = update_profile.__file__
|
||||||
|
command = ['pkexec', '--keep-cwd', update_profile_path, 'add_rule', rule, 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 {rule} to {profile}\nError code = {retcode}').format(rule=rule, profile=profile_name, retcode=e.returncode), False).show()
|
||||||
|
|
||||||
|
|
||||||
def cb_add_to_profile(notification, action, _args):
|
def cb_add_to_profile(notification, action, _args):
|
||||||
|
@ -523,26 +580,20 @@ def cb_add_to_profile(notification, action, _args):
|
||||||
|
|
||||||
rule = rl.create_rule_from_ev(parsed_event)
|
rule = rl.create_rule_from_ev(parsed_event)
|
||||||
|
|
||||||
|
# Exec events are created with the default FileRule.ANY_EXEC. We use Pix for actual rules
|
||||||
|
if type(rule) is FileRule and rule.exec_perms == FileRule.ANY_EXEC:
|
||||||
|
rule.exec_perms = 'Pix'
|
||||||
|
|
||||||
if not rule:
|
if not rule:
|
||||||
ErrorGUI.show(_('ERROR: Could not create rule from event.'))
|
ErrorGUI(_('ERROR: Could not create rule from event.'), False).show()
|
||||||
return
|
return
|
||||||
|
|
||||||
aa.update_profiles()
|
aa.update_profiles()
|
||||||
|
|
||||||
if customized_message['userns']['cond'](parsed_event, special_profiles):
|
if customized_message['userns']['cond'](parsed_event, special_profiles):
|
||||||
if parsed_event['execpath'] is None:
|
ask_for_user_ns_denied(parsed_event['execpath'], parsed_event['comm'], False)
|
||||||
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:
|
else:
|
||||||
try:
|
add_to_profile(rule.get_clean(), parsed_event['profile'])
|
||||||
AddProfileGUI(rule.get_clean(), parsed_event['profile']).show()
|
|
||||||
except AppArmorException:
|
|
||||||
AddProfileGUI.show_error_cannot_find_profile(parsed_event['profile'])
|
|
||||||
|
|
||||||
|
|
||||||
customized_message = {
|
customized_message = {
|
||||||
|
@ -646,15 +697,6 @@ 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
|
||||||
|
@ -693,6 +735,7 @@ def main():
|
||||||
- userns_special_profiles
|
- userns_special_profiles
|
||||||
- ignore_denied_capability
|
- ignore_denied_capability
|
||||||
- interface_theme
|
- interface_theme
|
||||||
|
- prompt_filter
|
||||||
- filter.profile,
|
- filter.profile,
|
||||||
- filter.operation,
|
- filter.operation,
|
||||||
- filter.name,
|
- filter.name,
|
||||||
|
@ -709,6 +752,7 @@ def main():
|
||||||
'userns_special_profiles',
|
'userns_special_profiles',
|
||||||
'ignore_denied_capability',
|
'ignore_denied_capability',
|
||||||
'interface_theme',
|
'interface_theme',
|
||||||
|
'prompt_filter',
|
||||||
'show_notifications',
|
'show_notifications',
|
||||||
'message_body',
|
'message_body',
|
||||||
'message_footer',
|
'message_footer',
|
||||||
|
@ -798,6 +842,16 @@ def main():
|
||||||
else:
|
else:
|
||||||
set_interface_theme('ubuntu')
|
set_interface_theme('ubuntu')
|
||||||
|
|
||||||
|
# Todo: add more kinds of notifications
|
||||||
|
supported_prompt_filter = {'userns'}
|
||||||
|
if not args.prompt_filter and 'prompt_filter' in config['']:
|
||||||
|
args.prompt_filter = config['']['prompt_filter']
|
||||||
|
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)))
|
||||||
|
|
||||||
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'):
|
||||||
|
@ -831,6 +885,9 @@ def main():
|
||||||
# Required to parse_record.
|
# Required to parse_record.
|
||||||
rl = ReadLog('', '', '')
|
rl = ReadLog('', '', '')
|
||||||
|
|
||||||
|
# Initialize the list of profiles for can_allow_rule
|
||||||
|
aa.read_profiles()
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -865,8 +922,9 @@ def main():
|
||||||
'gtk-dialog-warning'
|
'gtk-dialog-warning'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if can_allow_rule(ev, userns_special_profiles):
|
||||||
n.add_action('clicked', 'Allow', cb_add_to_profile, (event, rl, userns_special_profiles))
|
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.add_action('more_clicked', 'Show More', cb_more_info, (event, rl, userns_special_profiles))
|
||||||
|
|
||||||
n.show()
|
n.show()
|
||||||
|
|
||||||
|
|
|
@ -86,11 +86,24 @@ displays a short usage statement.
|
||||||
System-wide configuration for B<aa-notify> is done via
|
System-wide configuration for B<aa-notify> is done via
|
||||||
/etc/apparmor/notify.conf:
|
/etc/apparmor/notify.conf:
|
||||||
|
|
||||||
# set to 'yes' to enable AppArmor DENIED notifications
|
# Set to 'no' to disable AppArmor notifications globally
|
||||||
show_notifications="yes"
|
show_notifications="yes"
|
||||||
|
|
||||||
# only people in use_group can use aa-notify
|
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
|
||||||
use_group="admin"
|
userns_special_profiles="unconfined,unprivileged_userns"
|
||||||
|
|
||||||
|
# Theme for aa-notify GUI. 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 - kind of operations which display a popup prompt.
|
||||||
|
prompt_filter="userns"
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
|
||||||
# OPTIONAL - custom notification message body
|
# OPTIONAL - custom notification message body
|
||||||
message_body="This is a custom notification message."
|
message_body="This is a custom notification message."
|
||||||
|
@ -98,6 +111,15 @@ System-wide configuration for B<aa-notify> is done via
|
||||||
# OPTIONAL - custom notification message footer
|
# OPTIONAL - custom notification message footer
|
||||||
message_footer="For more information visit https://foo.com"
|
message_footer="For more information visit https://foo.com"
|
||||||
|
|
||||||
|
# OPTIONAL - custom notification filtering
|
||||||
|
# Filters are used to reduce the output of information to only those entries that will match the filter. Filters use Python's regular expression syntax.
|
||||||
|
filter.profile="^(foo|bar)$" # Match the profile: Only shows notifications for profiles "foo" or "bar"
|
||||||
|
filter.operation="^open$" # Match the operation: Only shows notifications for "open" operation
|
||||||
|
filter.name="^(?!/usr/lib/)" # Match the name: Excludes notifications for names starting by "/usr/lib/"
|
||||||
|
filter.denied="^r$" # Match the denied_mask: Only shows notifications where "r", and only "r", was denied
|
||||||
|
filter.family="^inet$" # Match the network family: Only shows notifications for "inet" family
|
||||||
|
filter.socket="stream" # Match the network socket type: Only shows notifications for "stream" sockets
|
||||||
|
|
||||||
Per-user configuration is done via $XDG_CONFIG_HOME/apparmor/notify.conf (or
|
Per-user configuration is done via $XDG_CONFIG_HOME/apparmor/notify.conf (or
|
||||||
the deprecated ~/.apparmor/notify.conf if it exists):
|
the deprecated ~/.apparmor/notify.conf if it exists):
|
||||||
|
|
||||||
|
|
|
@ -1602,9 +1602,9 @@ def collapse_log(hashlog, ignore_null_profiles=True):
|
||||||
log_dict[aamode][final_name] = ProfileStorage(profile, hat, 'collapse_log()')
|
log_dict[aamode][final_name] = ProfileStorage(profile, hat, 'collapse_log()')
|
||||||
|
|
||||||
for ev_type, ev_class in ReadLog.ruletypes.items():
|
for ev_type, ev_class in ReadLog.ruletypes.items():
|
||||||
for event in ev_class.from_hashlog(hashlog[aamode][full_profile][ev_type]):
|
for rule in ev_class.from_hashlog(hashlog[aamode][full_profile][ev_type]):
|
||||||
if not hat_exists or not is_known_rule(aa[profile][hat], ev_type, event):
|
if not hat_exists or not is_known_rule(aa[profile][hat], ev_type, rule):
|
||||||
log_dict[aamode][final_name][ev_type].add(event)
|
log_dict[aamode][final_name][ev_type].add(rule)
|
||||||
|
|
||||||
return log_dict
|
return log_dict
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,11 @@ from configparser import ConfigParser
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
from apparmor.common import AppArmorException, open_file_read # , warn, msg,
|
from apparmor.common import AppArmorException, open_file_read # , warn, msg,
|
||||||
|
import apparmor.ui as aaui
|
||||||
|
|
||||||
|
from apparmor.translations import init_translation
|
||||||
|
|
||||||
|
_ = init_translation()
|
||||||
|
|
||||||
|
|
||||||
# CFG = None
|
# CFG = None
|
||||||
|
@ -103,7 +108,11 @@ class Config:
|
||||||
config = {'': dict()}
|
config = {'': dict()}
|
||||||
with open_file_read(filepath) as conf_file:
|
with open_file_read(filepath) as conf_file:
|
||||||
for line in conf_file:
|
for line in conf_file:
|
||||||
|
try:
|
||||||
result = shlex.split(line, True)
|
result = shlex.split(line, True)
|
||||||
|
except ValueError as e:
|
||||||
|
aaui.UI_Important(_('Warning! invalid line \'{line}\' in config file: {err}').format(line=line[:-1], err=e))
|
||||||
|
continue
|
||||||
# If not a comment of empty line
|
# If not a comment of empty line
|
||||||
if result:
|
if result:
|
||||||
# option="value" or option=value type
|
# option="value" or option=value type
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import os
|
import os
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import tkinter.ttk as ttk
|
import tkinter.ttk as ttk
|
||||||
import tkinter.font as font
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import ttkthemes
|
import ttkthemes
|
||||||
|
|
||||||
import apparmor.aa as aa
|
import apparmor.aa as aa
|
||||||
import apparmor.update_profile as update_profile
|
|
||||||
from apparmor.common import AppArmorException
|
|
||||||
|
|
||||||
from apparmor.translations import init_translation
|
from apparmor.translations import init_translation
|
||||||
|
|
||||||
|
@ -28,6 +25,7 @@ class GUI:
|
||||||
print(_('ERROR: Cannot initialize Tkinter. Please check that your terminal can use a graphical interface'))
|
print(_('ERROR: Cannot initialize Tkinter. Please check that your terminal can use a graphical interface'))
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
|
self.result = None
|
||||||
style = ttkthemes.ThemedStyle(self.master)
|
style = ttkthemes.ThemedStyle(self.master)
|
||||||
style.theme_use(interface_theme)
|
style.theme_use(interface_theme)
|
||||||
self.bg_color = style.lookup('TLabel', 'background')
|
self.bg_color = style.lookup('TLabel', 'background')
|
||||||
|
@ -40,73 +38,19 @@ class GUI:
|
||||||
self.button_frame = ttk.Frame(self.master, padding=(0, 10))
|
self.button_frame = ttk.Frame(self.master, padding=(0, 10))
|
||||||
self.button_frame.pack()
|
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):
|
def show(self):
|
||||||
self.master.mainloop()
|
self.master.mainloop()
|
||||||
|
return self.result
|
||||||
|
|
||||||
@staticmethod
|
def set_result(self, result):
|
||||||
def show_error_cannot_find_profile(profile_name):
|
self.result = result
|
||||||
ErrorGUI(
|
self.master.destroy()
|
||||||
_(
|
|
||||||
'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):
|
class ShowMoreGUI(GUI):
|
||||||
def __init__(self, profile_path, msg, profile_found=True):
|
def __init__(self, profile_path, msg, rule, profile_name, profile_found=True):
|
||||||
|
self.rule = rule
|
||||||
|
self.profile_name = profile_name
|
||||||
self.profile_path = profile_path
|
self.profile_path = profile_path
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.profile_found = profile_found
|
self.profile_found = profile_found
|
||||||
|
@ -121,17 +65,25 @@ class ShowMoreGUI(GUI):
|
||||||
|
|
||||||
if self.profile_found:
|
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 = ttk.Button(self.button_frame, text=_('Show Current Profile'), command=lambda: open_with_default_editor(self.profile_path))
|
||||||
self.show_profile_button.pack()
|
self.show_profile_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
def show(self):
|
self.add_to_profile_button = ttk.Button(self.button_frame, text=_('Allow'), command=lambda: self.set_result('add_rule'))
|
||||||
self.master.mainloop()
|
self.add_to_profile_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
elif rule == 'userns create,':
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class UsernsGUI(GUI):
|
class UsernsGUI(GUI):
|
||||||
def __init__(self, name, path):
|
def __init__(self, name, path):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.path = path
|
self.path = path
|
||||||
self.result = None
|
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
@ -144,10 +96,10 @@ class UsernsGUI(GUI):
|
||||||
link.pack()
|
link.pack()
|
||||||
link.bind('<Button-1>', self.more_info)
|
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 = 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.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 = 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.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 = ttk.Button(self.master, text=_('Do nothing'), command=self.master.destroy)
|
||||||
|
@ -162,17 +114,10 @@ However, this feature also introduces security risks, (e.g. privilege escalation
|
||||||
This dialog allows you to choose whether you want to enable user namespaces for this application.
|
This dialog allows you to choose whether you want to enable user namespaces for this application.
|
||||||
|
|
||||||
The application path is {}""".format(self.path))
|
The application path is {}""".format(self.path))
|
||||||
more_gui = ShowMoreGUI(self.path, more_info_text, profile_found=False)
|
# Rule=None so we don't show redundant buttons in ShowMoreGUI.
|
||||||
|
more_gui = ShowMoreGUI(self.path, more_info_text, None, self.name, profile_found=False)
|
||||||
more_gui.show()
|
more_gui.show()
|
||||||
|
|
||||||
def set_result(self, result):
|
|
||||||
self.result = result
|
|
||||||
self.master.destroy()
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
self.master.mainloop()
|
|
||||||
return self.result
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def show_error_cannot_reload_profile(profile_path, error):
|
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()
|
ErrorGUI(_('Failed to create or load profile {}\n Error code = {}').format(profile_path, error), False).show()
|
||||||
|
@ -203,7 +148,7 @@ class ErrorGUI(GUI):
|
||||||
self.label.pack()
|
self.label.pack()
|
||||||
|
|
||||||
# Create a button to close the dialog
|
# Create a button to close the dialog
|
||||||
self.button = ttk.Button(self.button_frame, text="OK", command=self.destroy)
|
self.button = ttk.Button(self.button_frame, text='OK', command=self.destroy)
|
||||||
self.button.pack()
|
self.button.pack()
|
||||||
|
|
||||||
def destroy(self):
|
def destroy(self):
|
||||||
|
|
|
@ -114,8 +114,7 @@ class ReadLog:
|
||||||
def get_event_type(self, e):
|
def get_event_type(self, e):
|
||||||
|
|
||||||
if e['operation'] == 'exec':
|
if e['operation'] == 'exec':
|
||||||
return 'exec'
|
return 'file'
|
||||||
|
|
||||||
elif e['class'] and e['class'] == 'namespace':
|
elif e['class'] and e['class'] == 'namespace':
|
||||||
if e['denied_mask'] and e['denied_mask'].startswith('userns_'):
|
if e['denied_mask'] and e['denied_mask'].startswith('userns_'):
|
||||||
return 'userns'
|
return 'userns'
|
||||||
|
@ -141,7 +140,6 @@ class ReadLog:
|
||||||
return 'change_hat'
|
return 'change_hat'
|
||||||
elif e['operation'] == 'change_profile':
|
elif e['operation'] == 'change_profile':
|
||||||
return 'change_profile'
|
return 'change_profile'
|
||||||
|
|
||||||
elif e['operation'] == 'ptrace':
|
elif e['operation'] == 'ptrace':
|
||||||
return 'ptrace'
|
return 'ptrace'
|
||||||
elif e['operation'] == 'signal':
|
elif e['operation'] == 'signal':
|
||||||
|
@ -306,17 +304,10 @@ class ReadLog:
|
||||||
|
|
||||||
if profile != 'null-complain-profile' and not self.profile_exists(profile):
|
if profile != 'null-complain-profile' and not self.profile_exists(profile):
|
||||||
return
|
return
|
||||||
if e['operation'] == 'exec':
|
|
||||||
if not e['name']:
|
|
||||||
raise AppArmorException('exec without executed binary')
|
|
||||||
|
|
||||||
if not e['name2']:
|
|
||||||
e['name2'] = '' # exec events in enforce mode don't have target=...
|
|
||||||
|
|
||||||
self.hashlog[aamode][full_profile]['exec'][e['name']][e['name2']] = True
|
|
||||||
return
|
|
||||||
|
|
||||||
# TODO: replace all the if conditions with a loop over 'ruletypes'
|
# TODO: replace all the if conditions with a loop over 'ruletypes'
|
||||||
|
if e['operation'] == 'exec':
|
||||||
|
FileRule.hashlog_from_event(self.hashlog[aamode][full_profile]['exec'], e)
|
||||||
|
return
|
||||||
|
|
||||||
elif e['class'] and e['class'] == 'namespace':
|
elif e['class'] and e['class'] == 'namespace':
|
||||||
if e['denied_mask'].startswith('userns_'):
|
if e['denied_mask'].startswith('userns_'):
|
||||||
|
|
|
@ -427,6 +427,17 @@ class FileRule(BaseRule):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def hashlog_from_event(hl, e):
|
def hashlog_from_event(hl, e):
|
||||||
|
# FileRule can be of two types, "file" or "exec"
|
||||||
|
if e['operation'] == 'exec':
|
||||||
|
if not e['name']:
|
||||||
|
raise AppArmorException('exec without executed binary')
|
||||||
|
|
||||||
|
if not e['name2']:
|
||||||
|
e['name2'] = '' # exec events in enforce mode don't have target=...
|
||||||
|
|
||||||
|
hl[e['name']][e['name2']] = True
|
||||||
|
return
|
||||||
|
|
||||||
# Map c (create) and d (delete) to w (logging is more detailed than the profile language)
|
# Map c (create) and d (delete) to w (logging is more detailed than the profile language)
|
||||||
dmask = e['denied_mask']
|
dmask = e['denied_mask']
|
||||||
dmask = dmask.replace('c', 'w')
|
dmask = dmask.replace('c', 'w')
|
||||||
|
@ -461,7 +472,14 @@ class FileRule(BaseRule):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_hashlog(cls, hl):
|
def from_hashlog(cls, hl):
|
||||||
for path, owner in BaseRule.generate_rules_from_hashlog(hl, 2):
|
for h1, h2 in BaseRule.generate_rules_from_hashlog(hl, 2):
|
||||||
|
# FileRule can be either a 'normal' or an 'exec' file rule. These rules are encoded in hashlog differently.
|
||||||
|
if hl[h1][h2] is True: # Exec Rule
|
||||||
|
name = h1
|
||||||
|
yield FileRule(name, None, FileRule.ANY_EXEC, FileRule.ALL, owner=False, log_event=True)
|
||||||
|
else:
|
||||||
|
path = h1
|
||||||
|
owner = h2
|
||||||
mode = set(hl[path][owner].keys())
|
mode = set(hl[path][owner].keys())
|
||||||
# logparser sums up multiple log events, so both 'a' and 'w' can be present
|
# logparser sums up multiple log events, so both 'a' and 'w' can be present
|
||||||
if 'a' in mode and 'w' in mode:
|
if 'a' in mode and 'w' in mode:
|
||||||
|
|
|
@ -15,7 +15,7 @@ def create_userns(template_path, name, bin_path, profile_path, decision):
|
||||||
with open(template_path, 'r') as f:
|
with open(template_path, 'r') as f:
|
||||||
profile_template = f.read()
|
profile_template = f.read()
|
||||||
|
|
||||||
rule = 'userns' if decision == 'allow' else 'audit deny userns'
|
rule = 'userns create' if decision == 'allow' else 'audit deny userns create'
|
||||||
profile = profile_template.format(rule=rule, name=name, path=bin_path)
|
profile = profile_template.format(rule=rule, name=name, path=bin_path)
|
||||||
|
|
||||||
with open(profile_path, 'w') as file:
|
with open(profile_path, 'w') as file:
|
||||||
|
|
|
@ -13,12 +13,15 @@ show_notifications="yes"
|
||||||
|
|
||||||
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
|
# Special profiles used to remove privileges for unconfined binaries using user namespaces. If unsure, leave as is.
|
||||||
userns_special_profiles="unconfined,unprivileged_userns"
|
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.
|
# Theme to use for aa-notify GUI themes. See https://ttkthemes.readthedocs.io/en/latest/themes.html for available themes.
|
||||||
interface_theme="ubuntu"
|
interface_theme="ubuntu"
|
||||||
|
|
||||||
# Binaries for which we ignore userns-related capability denials
|
# Binaries for which we ignore userns-related capability denials
|
||||||
ignore_denied_capability="sudo,su"
|
ignore_denied_capability="sudo,su"
|
||||||
|
|
||||||
|
# OPTIONAL - kind of operations which display a popup prompt.
|
||||||
|
# prompt_filter="userns"
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
|
@ -1219,8 +1219,27 @@ class FileGetExecConflictRules_1(AATest):
|
||||||
self. assertEqual(conflicts.get_clean(), expected)
|
self. assertEqual(conflicts.get_clean(), expected)
|
||||||
|
|
||||||
|
|
||||||
class FileModeTest(AATest):
|
class FileCreateFromEvent(AATest):
|
||||||
def test_write_append(self):
|
|
||||||
|
def test_exec_rule(self):
|
||||||
|
parser = ReadLog('', '', '')
|
||||||
|
raw_ev = '[258945.534540] audit: type=1400 audit(1725865139.443:401): apparmor="DENIED" operation="exec" class="file" profile="foo" name="/usr/bin/ls" pid=130888 comm="foo" requested_mask="x" denied_mask="x" fsuid=1000 ouid=0'
|
||||||
|
hl = hasher()
|
||||||
|
ev = parser.parse_event(raw_ev)
|
||||||
|
FileRule.hashlog_from_event(hl, ev)
|
||||||
|
|
||||||
|
expected = {'/usr/bin/ls': {'': True}}
|
||||||
|
self.assertEqual(hl, expected)
|
||||||
|
|
||||||
|
fr = FileRule.from_hashlog(hl)
|
||||||
|
|
||||||
|
expected = FileRule('/usr/bin/ls', None, FileRule.ANY_EXEC, FileRule.ALL, False)
|
||||||
|
|
||||||
|
self.assertTrue(expected.is_equal(next(fr)))
|
||||||
|
with self.assertRaises(StopIteration):
|
||||||
|
next(fr)
|
||||||
|
|
||||||
|
def test_filemode_write_append(self):
|
||||||
parser = ReadLog('', '', '')
|
parser = ReadLog('', '', '')
|
||||||
events = [
|
events = [
|
||||||
'[ 9614.885136] audit: type=1400 audit(1720429924.397:191): apparmor="DENIED" operation="open" class="file" profile="/home/user/test/a" name="/home/user/test/foo" pid=24460 comm="a" requested_mask="w" denied_mask="w" fsuid=1000 ouid=1000',
|
'[ 9614.885136] audit: type=1400 audit(1720429924.397:191): apparmor="DENIED" operation="open" class="file" profile="/home/user/test/a" name="/home/user/test/foo" pid=24460 comm="a" requested_mask="w" denied_mask="w" fsuid=1000 ouid=1000',
|
||||||
|
@ -1243,6 +1262,16 @@ class FileModeTest(AATest):
|
||||||
next(fr)
|
next(fr)
|
||||||
|
|
||||||
|
|
||||||
|
class FileInvalidCreateFromEvent(AATest):
|
||||||
|
def test_write_append(self):
|
||||||
|
parser = ReadLog('', '', '')
|
||||||
|
raw_ev = '[258145.094974] audit: type=1400 audit(1725864339.004:396): apparmor="DENIED" operation="exec" class="file" profile="foo" name="" pid=125456 comm="foo" requested_mask="x" denied_mask="x" fsuid=1000 ouid=1000' # Name is empty
|
||||||
|
hl = hasher()
|
||||||
|
ev = parser.parse_event(raw_ev)
|
||||||
|
with self.assertRaises(AppArmorException):
|
||||||
|
FileRule.hashlog_from_event(hl, ev)
|
||||||
|
|
||||||
|
|
||||||
setup_all_loops(__name__)
|
setup_all_loops(__name__)
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main(verbosity=1)
|
unittest.main(verbosity=1)
|
||||||
|
|
Loading…
Add table
Reference in a new issue