mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 08:24:42 +01:00
aa-notify: Adding support for merging notification.
This commit is contained in:
parent
e9d6e0ba14
commit
f63fcdc8d2
6 changed files with 461 additions and 145 deletions
365
utils/aa-notify
365
utils/aa-notify
|
@ -37,9 +37,11 @@ import os
|
||||||
import pwd
|
import pwd
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import notify2
|
import notify2
|
||||||
import psutil
|
import psutil
|
||||||
|
@ -54,7 +56,7 @@ 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, ErrorGUI, ShowMoreGUI, set_interface_theme
|
from apparmor.gui import UsernsGUI, ErrorGUI, ShowMoreGUI, ShowMoreGUIAggregated, set_interface_theme
|
||||||
from apparmor.rule.file import FileRule
|
from apparmor.rule.file import FileRule
|
||||||
|
|
||||||
from dbus import DBusException
|
from dbus import DBusException
|
||||||
|
@ -121,7 +123,7 @@ def is_event_in_filter(event, filters):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def notify_about_new_entries(logfile, filters, wait=0):
|
def daemonize():
|
||||||
"""Run the notification daemon in the background."""
|
"""Run the notification daemon in the background."""
|
||||||
# Kill other instances of aa-notify if already running
|
# Kill other instances of aa-notify if already running
|
||||||
for process in psutil.process_iter():
|
for process in psutil.process_iter():
|
||||||
|
@ -144,11 +146,18 @@ def notify_about_new_entries(logfile, filters, wait=0):
|
||||||
except DBusException:
|
except DBusException:
|
||||||
sys.exit(_('Cannot initialize notify2. Please check that your terminal can use a graphical interface'))
|
sys.exit(_('Cannot initialize notify2. Please check that your terminal can use a graphical interface'))
|
||||||
|
|
||||||
try:
|
|
||||||
thread = threading.Thread(target=start_glib_loop)
|
thread = threading.Thread(target=start_glib_loop)
|
||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
|
else:
|
||||||
|
print(_('Notification emitter started in the background'))
|
||||||
|
# pids = (os.getpid(), newpid)
|
||||||
|
# print("parent: %d, child: %d\n" % pids)
|
||||||
|
os._exit(0) # Exit child without calling exit handlers etc
|
||||||
|
|
||||||
|
|
||||||
|
def notify_about_new_entries(logfile, filters, wait=0):
|
||||||
|
try:
|
||||||
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
|
||||||
|
@ -157,12 +166,6 @@ def notify_about_new_entries(logfile, filters, wait=0):
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
sys.exit(_("ERROR: Cannot read {}. Please check permissions.").format(logfile))
|
sys.exit(_("ERROR: Cannot read {}. Please check permissions.").format(logfile))
|
||||||
|
|
||||||
else:
|
|
||||||
print(_('Notification emitter started in the background'))
|
|
||||||
# pids = (os.getpid(), newpid)
|
|
||||||
# print("parent: %d, child: %d\n" % pids)
|
|
||||||
os._exit(0) # Exit child without calling exit handlers etc
|
|
||||||
|
|
||||||
|
|
||||||
def show_entries_since_epoch(logfile, epoch_since, filters):
|
def show_entries_since_epoch(logfile, epoch_since, filters):
|
||||||
"""Show AppArmor notifications since given timestamp."""
|
"""Show AppArmor notifications since given timestamp."""
|
||||||
|
@ -292,6 +295,22 @@ def reopen_logfile_if_needed(logfile, logdata, log_inode, log_size):
|
||||||
return (logdata, log_inode, log_size)
|
return (logdata, log_inode, log_size)
|
||||||
|
|
||||||
|
|
||||||
|
def get_apparmor_events_return(logfile, since=0):
|
||||||
|
"""Read audit events from log source and return all relevant events."""
|
||||||
|
out = []
|
||||||
|
# Get logdata from file
|
||||||
|
# @TODO Implement more log sources in addition to just the logfile
|
||||||
|
try:
|
||||||
|
with open_file_read(logfile) as logdata:
|
||||||
|
for event in parse_logdata(logdata):
|
||||||
|
if event.epoch > since:
|
||||||
|
out.append(event)
|
||||||
|
|
||||||
|
return out
|
||||||
|
except PermissionError:
|
||||||
|
sys.exit(_("ERROR: Cannot read {}. Please check permissions.".format(logfile)))
|
||||||
|
|
||||||
|
|
||||||
def get_apparmor_events(logfile, since=0):
|
def get_apparmor_events(logfile, since=0):
|
||||||
"""Read audit events from log source and yield all relevant events."""
|
"""Read audit events from log source and yield all relevant events."""
|
||||||
|
|
||||||
|
@ -378,6 +397,10 @@ def drop_privileges():
|
||||||
os.setegid(int(next_gid))
|
os.setegid(int(next_gid))
|
||||||
os.seteuid(int(next_uid))
|
os.seteuid(int(next_uid))
|
||||||
|
|
||||||
|
# sudo does not preserve DBUS address, so we need to guess it based on UID
|
||||||
|
if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
|
||||||
|
os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/run/user/{}/bus'.format(os.geteuid())
|
||||||
|
|
||||||
|
|
||||||
def raise_privileges():
|
def raise_privileges():
|
||||||
"""Raise privileges of process.
|
"""Raise privileges of process.
|
||||||
|
@ -477,20 +500,25 @@ def ask_for_user_ns_denied(path, name, interactive=True):
|
||||||
debug_logger.debug('No action from the user for {}'.format(path))
|
debug_logger.debug('No action from the user for {}'.format(path))
|
||||||
|
|
||||||
|
|
||||||
def prompt_userns(ev, special_profiles):
|
def can_leverage_userns_event(ev):
|
||||||
"""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:
|
if ev['execpath'] is None:
|
||||||
UsernsGUI.show_error_cannot_find_execpath(ev['comm'], os.path.dirname(os.path.abspath(__file__)) + '/default_unconfined.template')
|
return 'error_cannot_find_path'
|
||||||
return True
|
|
||||||
|
|
||||||
aa.update_profiles()
|
aa.update_profiles()
|
||||||
|
|
||||||
if aa.get_profile_filename_from_profile_name(ev['comm']):
|
if aa.get_profile_filename_from_profile_name(ev['comm']):
|
||||||
|
return 'error_userns_profile_exists'
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_userns(ev):
|
||||||
|
"""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
|
||||||
|
"""
|
||||||
|
match can_leverage_userns_event(ev):
|
||||||
|
case 'error_cannot_find_path':
|
||||||
|
UsernsGUI.show_error_cannot_find_execpath(ev['comm'], os.path.dirname(os.path.abspath(__file__)) + '/default_unconfined.template')
|
||||||
|
case 'error_userns_profile_exists':
|
||||||
# There is already a profile with this name: we show an error to the user.
|
# 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
|
# 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
|
# but if execpath is not supported by the kernel it could also mean that we inferred a bad path
|
||||||
|
@ -500,54 +528,66 @@ def prompt_userns(ev, special_profiles):
|
||||||
'This is likely because there is several binaries named {profile} thus the path inferred by AppArmor ({inferred_path}) 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 {profile_dir}).').format(profile=ev['comm'], inferred_path=ev['execpath'], profile_dir=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
|
case 'ok':
|
||||||
|
|
||||||
ask_for_user_ns_denied(ev['execpath'], ev['comm'])
|
ask_for_user_ns_denied(ev['execpath'], ev['comm'])
|
||||||
|
|
||||||
return True
|
|
||||||
|
def get_more_info_about_event(rl, ev, special_profiles, header='', get_clean_rule=False):
|
||||||
|
out = header
|
||||||
|
clean_rule = None
|
||||||
|
|
||||||
|
for key, value in ev.items():
|
||||||
|
if value:
|
||||||
|
out += '\t{} = {}\n'.format(_(key), value)
|
||||||
|
|
||||||
|
out += _('\nThe software that declined this operation is {}\n').format(ev['profile'])
|
||||||
|
|
||||||
|
rule = rl.create_rule_from_ev(ev)
|
||||||
|
|
||||||
|
if rule:
|
||||||
|
if type(rule) is FileRule and rule.exec_perms == FileRule.ANY_EXEC:
|
||||||
|
rule.exec_perms = 'Pix'
|
||||||
|
aa.update_profiles()
|
||||||
|
if customized_message['userns']['cond'](ev, special_profiles):
|
||||||
|
profile_path = None
|
||||||
|
out += _('You may allow it through a dedicated unconfined profile for {}.').format(ev['comm'])
|
||||||
|
match can_leverage_userns_event(ev):
|
||||||
|
case 'error_cannot_find_path':
|
||||||
|
clean_rule = _('# You may allow it through a dedicated unconfined profile for {0}. However, apparmor cannot find {0}. If you want to allow it, please create a profile for it manually.').format(ev['comm'])
|
||||||
|
case 'error_userns_profile_exists':
|
||||||
|
clean_rule = _('# You may allow it through a dedicated unconfined profile for {} ({}). However, a profile already exists with this name. If you want to allow it, please create a profile for it manually.').format(ev['comm'], ev['execpath'])
|
||||||
|
case 'ok':
|
||||||
|
clean_rule = _('# You may allow it through a dedicated unconfined profile for {} ({})').format(ev['comm'], ev['execpath'])
|
||||||
|
else:
|
||||||
|
profile_path = aa.get_profile_filename_from_profile_name(ev['profile'])
|
||||||
|
clean_rule = rule.get_clean()
|
||||||
|
if profile_path:
|
||||||
|
out += _('If you want to allow this operation you can add the line below in profile {}\n').format(profile_path)
|
||||||
|
out += clean_rule
|
||||||
|
else:
|
||||||
|
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=ev['profile'], profile_dir=aa.profile_dir)
|
||||||
|
else: # Should not happen
|
||||||
|
out += _('ERROR: Could not create rule from event.')
|
||||||
|
profile_path = None
|
||||||
|
|
||||||
|
if get_clean_rule:
|
||||||
|
return out, profile_path, clean_rule
|
||||||
|
else:
|
||||||
|
return out, profile_path
|
||||||
|
|
||||||
|
|
||||||
# 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, special_profiles) = _args
|
(ev, rl, special_profiles) = _args
|
||||||
notification.close()
|
notification.close()
|
||||||
|
|
||||||
parsed_event = rl.parse_record(raw_ev)
|
out, profile_path, clean_rule = get_more_info_about_event(rl, ev, special_profiles, _('Operation denied by AppArmor\n\n'), get_clean_rule=True)
|
||||||
out = _('Operation denied by AppArmor\n\n')
|
|
||||||
|
|
||||||
for key, value in parsed_event.items():
|
ans = ShowMoreGUI(profile_path, out, clean_rule, ev['profile'], profile_path is not None).show()
|
||||||
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)
|
|
||||||
|
|
||||||
# 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:
|
|
||||||
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'])
|
|
||||||
if 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()
|
|
||||||
else:
|
|
||||||
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
|
|
||||||
out += _('ERROR: Could not create rule from event.')
|
|
||||||
return
|
|
||||||
|
|
||||||
ans = ShowMoreGUI(profile_path, out, rule.get_clean(), parsed_event['profile'], profile_path is not None).show()
|
|
||||||
if ans == 'add_rule':
|
if ans == 'add_rule':
|
||||||
add_to_profile(rule.get_clean(), parsed_event['profile'])
|
add_to_profile(clean_rule, ev['profile'])
|
||||||
elif ans in {'allow', 'deny'}:
|
elif ans in {'allow', 'deny'}:
|
||||||
create_userns_profile(parsed_event['comm'], parsed_event['execpath'], ans)
|
create_userns_profile(ev['comm'], ev['execpath'], ans)
|
||||||
|
|
||||||
|
|
||||||
def add_to_profile(rule, profile_name):
|
def add_to_profile(rule, profile_name):
|
||||||
|
@ -573,12 +613,67 @@ def add_to_profile(rule, profile_name):
|
||||||
ErrorGUI(_('Failed to add rule {rule} to {profile}\nError code = {retcode}').format(rule=rule, profile=profile_name, retcode=e.returncode), False).show()
|
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 create_from_file(file_path):
|
||||||
(raw_ev, rl, special_profiles) = _args
|
update_profile_path = update_profile.__file__
|
||||||
notification.close()
|
command = ['pkexec', '--keep-cwd', update_profile_path, 'from_file', file_path]
|
||||||
parsed_event = rl.parse_record(raw_ev)
|
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 some rules'), False).show()
|
||||||
|
|
||||||
rule = rl.create_rule_from_ev(parsed_event)
|
|
||||||
|
def allow_all(clean_rules):
|
||||||
|
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'
|
||||||
|
|
||||||
|
tmp = tempfile.NamedTemporaryFile()
|
||||||
|
with open(tmp.name, mode='w') as f:
|
||||||
|
profile = None
|
||||||
|
for line in clean_rules.splitlines():
|
||||||
|
if line == '':
|
||||||
|
continue
|
||||||
|
elif line[0] == '#':
|
||||||
|
profile = None
|
||||||
|
pass
|
||||||
|
elif line[0] != '\t':
|
||||||
|
profile = line[8:-1] # 8:-1 is to remove 'profile ' and ':'
|
||||||
|
else:
|
||||||
|
if line[1] == '#': # Add to userns
|
||||||
|
if line[-1] == '.': # '.' <==> There is an error: we cannot add the profile automatically
|
||||||
|
continue
|
||||||
|
profile_name = line.split()[-2] # line always finishes by <profile_name>
|
||||||
|
bin_path = line.split()[-1][1:-1] # 1:-1 to remove the parenthesis
|
||||||
|
profile_path = aa.get_profile_filename_from_profile_name(profile_name, True)
|
||||||
|
if not profile_path:
|
||||||
|
ErrorGUI(_('Cannot get profile path for {}.').format(profile_name), False).show()
|
||||||
|
continue
|
||||||
|
f.write('create_userns\t{}\t{}\t{}\t{}\t{}\n'.format(template_path, profile_name, bin_path, profile_path, 'allow'))
|
||||||
|
else:
|
||||||
|
if profile is not None:
|
||||||
|
f.write('add_rule\t{}\t{}\n'.format(line[1:], profile))
|
||||||
|
else:
|
||||||
|
print(_("Rule {} cannot be added automatically").format(line[1:]), file=sys.stdout)
|
||||||
|
|
||||||
|
create_from_file(tmp.name)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO reuse more code from aa-logprof in callbacks
|
||||||
|
def cb_more_info_aggregated(notification, action, _args):
|
||||||
|
(to_display, aggregated, clean_rules) = _args
|
||||||
|
res = ShowMoreGUIAggregated(to_display, aggregated, clean_rules).show()
|
||||||
|
if res == 'allow_all':
|
||||||
|
allow_all(clean_rules)
|
||||||
|
|
||||||
|
|
||||||
|
def cb_add_to_profile(notification, action, _args):
|
||||||
|
(ev, rl, special_profiles) = _args
|
||||||
|
notification.close()
|
||||||
|
|
||||||
|
rule = rl.create_rule_from_ev(ev)
|
||||||
|
|
||||||
# Exec events are created with the default FileRule.ANY_EXEC. We use Pix for actual rules
|
# 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:
|
if type(rule) is FileRule and rule.exec_perms == FileRule.ANY_EXEC:
|
||||||
|
@ -590,10 +685,10 @@ def cb_add_to_profile(notification, action, _args):
|
||||||
|
|
||||||
aa.update_profiles()
|
aa.update_profiles()
|
||||||
|
|
||||||
if customized_message['userns']['cond'](parsed_event, special_profiles):
|
if customized_message['userns']['cond'](ev, special_profiles):
|
||||||
ask_for_user_ns_denied(parsed_event['execpath'], parsed_event['comm'], False)
|
ask_for_user_ns_denied(ev['execpath'], ev['comm'], False)
|
||||||
else:
|
else:
|
||||||
add_to_profile(rule.get_clean(), parsed_event['profile'])
|
add_to_profile(rule.get_clean(), ev['profile'])
|
||||||
|
|
||||||
|
|
||||||
customized_message = {
|
customized_message = {
|
||||||
|
@ -616,6 +711,83 @@ def start_glib_loop():
|
||||||
loop.run()
|
loop.run()
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_event(agg, ev, keys_to_aggregate):
|
||||||
|
profile = ev['profile']
|
||||||
|
agg[profile]['count'] += 1
|
||||||
|
agg[profile]['events'].append(ev)
|
||||||
|
|
||||||
|
for key in keys_to_aggregate:
|
||||||
|
if key in ev:
|
||||||
|
value = ev[key]
|
||||||
|
agg[profile]['values'][key][value] += 1
|
||||||
|
|
||||||
|
return agg
|
||||||
|
|
||||||
|
|
||||||
|
def get_aggregated(rl, agg, max_nb_profiles, keys_to_aggregate, special_profiles):
|
||||||
|
notification = ''
|
||||||
|
summary = ''
|
||||||
|
more_info = ''
|
||||||
|
clean_rules = ''
|
||||||
|
summary = _('Notifications were raised for profiles: {}\n').format(', '.join(list(agg.keys())))
|
||||||
|
|
||||||
|
sorted_profiles = sorted(agg.items(), key=lambda item: item[1]['count'], reverse=True)
|
||||||
|
for profile, data in sorted_profiles:
|
||||||
|
profile_notif = _('profile: {} — {} events\n').format(profile, data['count'])
|
||||||
|
notification += profile_notif
|
||||||
|
summary += profile_notif
|
||||||
|
if len(agg) <= max_nb_profiles:
|
||||||
|
for key in keys_to_aggregate:
|
||||||
|
if key in data['values']:
|
||||||
|
total_key_events = sum(data['values'][key].values())
|
||||||
|
sorted_values = sorted(data['values'][key].items(), key=lambda item: item[1], reverse=True)
|
||||||
|
for value, count in sorted_values:
|
||||||
|
percent = (count / total_key_events) * 100
|
||||||
|
if percent >= 20: # We exclude rare cases for clarity. 20% is arbitrary
|
||||||
|
summary += _('\t{} was {} {:.1f}% of the time\n').format(key, value, percent)
|
||||||
|
summary += '\n'
|
||||||
|
|
||||||
|
more_info += _('profile {}, {} events\n').format(profile, data['count'])
|
||||||
|
rules_for_profiles = set()
|
||||||
|
found_profile = True
|
||||||
|
for i, ev in enumerate(data['events']):
|
||||||
|
more_info_rule, profile_path, clean_rule = get_more_info_about_event(rl, ev, special_profiles, _(' - Event {} -\n').format(i + 1), get_clean_rule=True)
|
||||||
|
if i != 0:
|
||||||
|
more_info += '\n\n'
|
||||||
|
if not profile_path:
|
||||||
|
found_profile = False
|
||||||
|
more_info += more_info_rule
|
||||||
|
if clean_rule:
|
||||||
|
rules_for_profiles.add(clean_rule)
|
||||||
|
if rules_for_profiles != set():
|
||||||
|
if profile not in special_profiles:
|
||||||
|
if found_profile:
|
||||||
|
clean_rules += _('profile {}:').format(profile)
|
||||||
|
else:
|
||||||
|
clean_rules += _('# Unknown profile {}').format(profile)
|
||||||
|
else:
|
||||||
|
clean_rules += _('# unprivileged userns denials ({}):').format(profile)
|
||||||
|
clean_rules += '\n\t' + '\n\t'.join(rules_for_profiles) + '\n'
|
||||||
|
return notification, summary, more_info, clean_rules
|
||||||
|
|
||||||
|
|
||||||
|
def display_notification(ev, rl, message, userns_special_profiles):
|
||||||
|
message = customize_notification_message(ev, message, userns_special_profiles)
|
||||||
|
n = notify2.Notification(_('AppArmor security notice'), message, 'gtk-dialog-warning')
|
||||||
|
if can_allow_rule(ev, userns_special_profiles):
|
||||||
|
n.add_action('clicked', 'Allow', cb_add_to_profile, (ev, rl, userns_special_profiles))
|
||||||
|
n.add_action('more_clicked', 'Show More', cb_more_info, (ev, rl, userns_special_profiles))
|
||||||
|
|
||||||
|
n.show()
|
||||||
|
|
||||||
|
|
||||||
|
def display_aggregated_notification(rl, aggregated, maximum_number_notification_profiles, keys_to_aggregate, special_profiles):
|
||||||
|
notification, summary, more_info, clean_rules = get_aggregated(rl, aggregated, maximum_number_notification_profiles, keys_to_aggregate, special_profiles)
|
||||||
|
n = notify2.Notification(_('AppArmor security notice'), notification, 'gtk-dialog-warning')
|
||||||
|
n.add_action('more_aggregated_clicked', 'Show More Info', cb_more_info_aggregated, (summary, more_info, clean_rules))
|
||||||
|
n.show()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run aa-notify.
|
"""Run aa-notify.
|
||||||
|
|
||||||
|
@ -652,6 +824,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('-m', '--merge-notifications', action='store_true', help=_('Merge notification for improved readability (with -p)'))
|
||||||
parser.add_argument('--prompt-filter', type=str, metavar=('PF'), help=_('kind of operations which display a popup prompt'))
|
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)
|
||||||
|
@ -736,6 +909,8 @@ def main():
|
||||||
- ignore_denied_capability
|
- ignore_denied_capability
|
||||||
- interface_theme
|
- interface_theme
|
||||||
- prompt_filter
|
- prompt_filter
|
||||||
|
- maximum_number_notification_profiles
|
||||||
|
- keys_to_aggregate
|
||||||
- filter.profile,
|
- filter.profile,
|
||||||
- filter.operation,
|
- filter.operation,
|
||||||
- filter.name,
|
- filter.name,
|
||||||
|
@ -756,6 +931,8 @@ def main():
|
||||||
'show_notifications',
|
'show_notifications',
|
||||||
'message_body',
|
'message_body',
|
||||||
'message_footer',
|
'message_footer',
|
||||||
|
'maximum_number_notification_profiles',
|
||||||
|
'keys_to_aggregate',
|
||||||
'filter.profile',
|
'filter.profile',
|
||||||
'filter.operation',
|
'filter.operation',
|
||||||
'filter.name',
|
'filter.name',
|
||||||
|
@ -852,6 +1029,16 @@ def main():
|
||||||
if unsupported:
|
if unsupported:
|
||||||
sys.exit(_('ERROR: using an unsupported prompt filter: {}\nSupported values: {}').format(', '.join(unsupported), ', '.join(supported_prompt_filter)))
|
sys.exit(_('ERROR: using an unsupported prompt filter: {}\nSupported values: {}').format(', '.join(unsupported), ', '.join(supported_prompt_filter)))
|
||||||
|
|
||||||
|
if 'maximum_number_notification_profiles' in config['']:
|
||||||
|
maximum_number_notification_profiles = int(config['']['maximum_number_notification_profiles'].strip())
|
||||||
|
else:
|
||||||
|
maximum_number_notification_profiles = 2
|
||||||
|
|
||||||
|
if 'keys_to_aggregate' in config['']:
|
||||||
|
keys_to_aggregate = config['']['keys_to_aggregate'].strip().split(',')
|
||||||
|
else:
|
||||||
|
keys_to_aggregate = {'operation', 'class', 'name', 'denied', 'target'}
|
||||||
|
|
||||||
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'):
|
||||||
|
@ -888,6 +1075,38 @@ def main():
|
||||||
# Initialize the list of profiles for can_allow_rule
|
# Initialize the list of profiles for can_allow_rule
|
||||||
aa.read_profiles()
|
aa.read_profiles()
|
||||||
|
|
||||||
|
drop_privileges()
|
||||||
|
daemonize()
|
||||||
|
raise_privileges()
|
||||||
|
|
||||||
|
if args.merge_notifications:
|
||||||
|
if not args.wait or args.wait == 0:
|
||||||
|
args.wait = 5
|
||||||
|
|
||||||
|
old_time = int(time.time())
|
||||||
|
while True:
|
||||||
|
|
||||||
|
raw_evs = get_apparmor_events_return(logfile, old_time)
|
||||||
|
drop_privileges()
|
||||||
|
|
||||||
|
if len(raw_evs) == 1: # Single event: we handle it without aggregation
|
||||||
|
raw_ev = raw_evs[0]
|
||||||
|
ev = rl.parse_record(raw_ev)
|
||||||
|
display_notification(ev, rl, format_event(raw_ev, logfile), userns_special_profiles)
|
||||||
|
elif len(raw_evs) > 1:
|
||||||
|
aggregated = defaultdict(lambda: {'count': 0, 'values': defaultdict(lambda: defaultdict(int)), 'events': []})
|
||||||
|
for raw_ev in raw_evs:
|
||||||
|
ev = rl.parse_record(raw_ev)
|
||||||
|
aggregate_event(aggregated, ev, keys_to_aggregate)
|
||||||
|
display_aggregated_notification(rl, aggregated, maximum_number_notification_profiles, keys_to_aggregate, userns_special_profiles)
|
||||||
|
|
||||||
|
old_time = int(time.time())
|
||||||
|
|
||||||
|
# When notification is sent, raise privileged back to root if the
|
||||||
|
# original effective user id was zero (to be able to read AppArmor logs)
|
||||||
|
raise_privileges()
|
||||||
|
time.sleep(args.wait)
|
||||||
|
else:
|
||||||
# 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
|
||||||
|
@ -903,30 +1122,14 @@ def main():
|
||||||
|
|
||||||
# Special behaivor for userns:
|
# Special behaivor for userns:
|
||||||
if args.prompt_filter and 'userns' in args.prompt_filter and customized_message['userns']['cond'](ev, userns_special_profiles):
|
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):
|
prompt_userns(ev)
|
||||||
continue # Notification already displayed for this event, we go to the next one.
|
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.
|
||||||
drop_privileges()
|
drop_privileges()
|
||||||
|
|
||||||
# sudo does not preserve DBUS address, so we need to guess it based on UID
|
display_notification(ev, rl, message, userns_special_profiles)
|
||||||
if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
|
|
||||||
os.environ['DBUS_SESSION_BUS_ADDRESS'] = 'unix:path=/run/user/{}/bus'.format(os.geteuid())
|
|
||||||
|
|
||||||
message = customize_notification_message(ev, message, userns_special_profiles)
|
|
||||||
|
|
||||||
n = notify2.Notification(
|
|
||||||
_('AppArmor security notice'),
|
|
||||||
message,
|
|
||||||
'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('more_clicked', 'Show More', cb_more_info, (event, rl, userns_special_profiles))
|
|
||||||
|
|
||||||
n.show()
|
|
||||||
|
|
||||||
# When notification is sent, raise privileged back to root if the
|
# When notification is sent, raise privileged back to root if the
|
||||||
# original effective user id was zero (to be able to read AppArmor logs)
|
# original effective user id was zero (to be able to read AppArmor logs)
|
||||||
|
|
|
@ -2,7 +2,12 @@ import os
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import tkinter.ttk as ttk
|
import tkinter.ttk as ttk
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
try: # We use tk without themes as a fallback which makes the GUI uglier but functional.
|
||||||
import ttkthemes
|
import ttkthemes
|
||||||
|
except ImportError:
|
||||||
|
ttkthemes = None
|
||||||
|
|
||||||
|
|
||||||
import apparmor.aa as aa
|
import apparmor.aa as aa
|
||||||
|
|
||||||
|
@ -26,17 +31,17 @@ class GUI:
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
||||||
self.result = None
|
self.result = None
|
||||||
|
if ttkthemes:
|
||||||
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')
|
||||||
self.fg_color = style.lookup('TLabel', 'foreground')
|
self.fg_color = style.lookup('TLabel', 'foreground')
|
||||||
self.master.configure(background=self.bg_color)
|
self.master.configure(background=self.bg_color)
|
||||||
|
|
||||||
self.label_frame = ttk.Frame(self.master, padding=(20, 10))
|
self.label_frame = ttk.Frame(self.master, padding=(20, 10))
|
||||||
self.label_frame.pack()
|
self.label_frame.pack()
|
||||||
|
|
||||||
self.button_frame = ttk.Frame(self.master, padding=(0, 10))
|
self.button_frame = ttk.Frame(self.master, padding=(10, 10))
|
||||||
self.button_frame.pack()
|
self.button_frame.pack(fill='x', expand=True)
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
self.master.mainloop()
|
self.master.mainloop()
|
||||||
|
@ -59,8 +64,9 @@ class ShowMoreGUI(GUI):
|
||||||
|
|
||||||
self.master.title(_('AppArmor - More info'))
|
self.master.title(_('AppArmor - More info'))
|
||||||
|
|
||||||
self.label = tk.Label(self.label_frame, background=self.bg_color, foreground=self.fg_color,
|
self.label = tk.Label(self.label_frame, text=self.msg, anchor='w', justify=tk.LEFT, wraplength=460)
|
||||||
text=self.msg, anchor='w', justify=tk.LEFT, wraplength=460)
|
if ttkthemes:
|
||||||
|
self.label.configure(background=self.bg_color, foreground=self.fg_color)
|
||||||
self.label.pack(pady=(0, 10) if not self.profile_found else (0, 0))
|
self.label.pack(pady=(0, 10) if not self.profile_found else (0, 0))
|
||||||
|
|
||||||
if self.profile_found:
|
if self.profile_found:
|
||||||
|
@ -80,6 +86,75 @@ class ShowMoreGUI(GUI):
|
||||||
self.do_nothing_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
self.do_nothing_button.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
|
||||||
|
class ShowMoreGUIAggregated(GUI):
|
||||||
|
def __init__(self, summary, detailed_text, clean_rules):
|
||||||
|
self.summary = summary
|
||||||
|
self.detailed_text = detailed_text
|
||||||
|
self.clean_rules = clean_rules
|
||||||
|
|
||||||
|
self.states = {
|
||||||
|
'summary': {
|
||||||
|
'msg': self.summary,
|
||||||
|
'btn_left': _('Show more details'),
|
||||||
|
'btn_right': _('Show rules only')
|
||||||
|
},
|
||||||
|
'detailed': {
|
||||||
|
'msg': self.detailed_text,
|
||||||
|
'btn_left': _('Show summary'),
|
||||||
|
'btn_right': _('Show rules only')
|
||||||
|
},
|
||||||
|
'rules_only': {
|
||||||
|
'msg': self.clean_rules,
|
||||||
|
'btn_left': _('Show more details'),
|
||||||
|
'btn_right': _('Show summary')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = 'rules_only'
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.master.title(_('AppArmor - More info'))
|
||||||
|
|
||||||
|
self.text_display = tk.Text(self.label_frame, height=40, width=100, wrap='word')
|
||||||
|
if ttkthemes:
|
||||||
|
self.text_display.configure(background=self.bg_color, foreground=self.fg_color)
|
||||||
|
self.text_display.insert('1.0', self.states[self.state]['msg'])
|
||||||
|
self.text_display['state'] = 'disabled'
|
||||||
|
|
||||||
|
self.scrollbar = ttk.Scrollbar(self.label_frame, command=self.text_display.yview)
|
||||||
|
self.text_display['yscrollcommand'] = self.scrollbar.set
|
||||||
|
|
||||||
|
self.scrollbar.pack(side='right', fill='y')
|
||||||
|
self.text_display.pack(side='left', fill='both', expand=True)
|
||||||
|
|
||||||
|
self.btn_left = ttk.Button(self.button_frame, text=self.states[self.state]['btn_left'], width=1, command=lambda: self.change_view('btn_left'))
|
||||||
|
self.btn_left.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
self.btn_right = ttk.Button(self.button_frame, text=self.states[self.state]['btn_right'], width=1, command=lambda: self.change_view('btn_right'))
|
||||||
|
self.btn_right.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
self.btn_allow_all = ttk.Button(self.button_frame, text="Allow All", width=1, command=lambda: self.set_result('allow_all'))
|
||||||
|
self.btn_allow_all.grid(row=0, column=2, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
self.button_frame.grid_columnconfigure(i, weight=1)
|
||||||
|
|
||||||
|
def change_view(self, action):
|
||||||
|
|
||||||
|
if action == 'btn_left':
|
||||||
|
self.state = 'detailed' if self.state != 'detailed' else 'summary'
|
||||||
|
elif action == 'btn_right':
|
||||||
|
self.state = 'rules_only' if self.state != 'rules_only' else 'summary'
|
||||||
|
|
||||||
|
self.btn_left['text'] = self.states[self.state]['btn_left']
|
||||||
|
self.btn_right['text'] = self.states[self.state]['btn_right']
|
||||||
|
self.text_display['state'] = 'normal'
|
||||||
|
self.text_display.delete('1.0', 'end')
|
||||||
|
self.text_display.insert('1.0', self.states[self.state]['msg'])
|
||||||
|
self.text_display['state'] = 'disabled'
|
||||||
|
|
||||||
|
|
||||||
class UsernsGUI(GUI):
|
class UsernsGUI(GUI):
|
||||||
def __init__(self, name, path):
|
def __init__(self, name, path):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -143,11 +218,11 @@ class ErrorGUI(GUI):
|
||||||
|
|
||||||
self.master.title('AppArmor Error')
|
self.master.title('AppArmor Error')
|
||||||
|
|
||||||
# Create label to display the error message
|
self.label = ttk.Label(self.label_frame, text=self.msg, wraplength=460)
|
||||||
self.label = ttk.Label(self.label_frame, background=self.bg_color, text=self.msg, wraplength=460)
|
if ttkthemes:
|
||||||
|
self.label.configure(background=self.bg_color)
|
||||||
self.label.pack()
|
self.label.pack()
|
||||||
|
|
||||||
# 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()
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@ def create_userns(template_path, name, bin_path, profile_path, decision):
|
||||||
|
|
||||||
def add_to_profile(rule, profile_name):
|
def add_to_profile(rule, profile_name):
|
||||||
aa.init_aa()
|
aa.init_aa()
|
||||||
aa.read_profiles()
|
aa.update_profiles()
|
||||||
|
|
||||||
rule_type, rule_class = ReadLog('', '', '').get_rule_type(rule)
|
rule_type, rule_class = ReadLog('', '', '').get_rule_type(rule)
|
||||||
|
|
||||||
|
@ -48,32 +48,51 @@ def usage(is_help):
|
||||||
print('This tool is a low level tool - do not use it directly')
|
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('{} create_userns <template_path> <name> <bin_path> <profile_path> <decision>'.format(sys.argv[0]))
|
||||||
print('{} add_rule <rule> <profile_name>'.format(sys.argv[0]))
|
print('{} add_rule <rule> <profile_name>'.format(sys.argv[0]))
|
||||||
|
print('{} from_file <file>'.format(sys.argv[0]))
|
||||||
if is_help:
|
if is_help:
|
||||||
exit(0)
|
exit(0)
|
||||||
else:
|
else:
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def create_from_file(file_path):
|
||||||
|
with open(file_path) as file:
|
||||||
|
for line in file:
|
||||||
|
args = line[:-1].split('\t')
|
||||||
|
if len(args) > 1:
|
||||||
|
command = args[0]
|
||||||
|
else:
|
||||||
|
command = None # Handle the case where no command is provided
|
||||||
|
do_command(command, args)
|
||||||
|
|
||||||
|
|
||||||
|
def do_command(command, args):
|
||||||
|
match command:
|
||||||
|
case 'from_file':
|
||||||
|
if not len(args) == 2:
|
||||||
|
usage(False)
|
||||||
|
create_from_file(args[1])
|
||||||
|
case 'create_userns':
|
||||||
|
if not len(args) == 6:
|
||||||
|
usage(False)
|
||||||
|
create_userns(args[1], args[2], args[3], args[4], args[5])
|
||||||
|
case 'add_rule':
|
||||||
|
if not len(args) == 3:
|
||||||
|
usage(False)
|
||||||
|
add_to_profile(args[1], args[2])
|
||||||
|
case 'help':
|
||||||
|
usage(True)
|
||||||
|
case _:
|
||||||
|
usage(False)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
command = sys.argv[1]
|
command = sys.argv[1]
|
||||||
else:
|
else:
|
||||||
command = None # Handle the case where no command is provided
|
command = None # Handle the case where no command is provided
|
||||||
|
|
||||||
match command:
|
do_command(command, sys.argv[1:])
|
||||||
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__':
|
if __name__ == '__main__':
|
||||||
|
|
|
@ -26,5 +26,16 @@
|
||||||
<annotate key="org.freedesktop.policykit.exec.path">{LIB_PATH}apparmor/update_profile.py</annotate>
|
<annotate key="org.freedesktop.policykit.exec.path">{LIB_PATH}apparmor/update_profile.py</annotate>
|
||||||
<annotate key="org.freedesktop.policykit.exec.argv1">create_userns</annotate>
|
<annotate key="org.freedesktop.policykit.exec.argv1">create_userns</annotate>
|
||||||
</action>
|
</action>
|
||||||
|
<action id="com.ubuntu.pkexec.aa-notify.from_file">
|
||||||
|
<description>AppArmor: Modifying profile from file</description>
|
||||||
|
<message>To modify an AppArmor security profile from file, 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">from_file</annotate>
|
||||||
|
</action>
|
||||||
|
|
||||||
</policyconfig>
|
</policyconfig>
|
||||||
|
|
|
@ -23,6 +23,12 @@ ignore_denied_capability="sudo,su"
|
||||||
# OPTIONAL - kind of operations which display a popup prompt.
|
# OPTIONAL - kind of operations which display a popup prompt.
|
||||||
# prompt_filter="userns"
|
# prompt_filter="userns"
|
||||||
|
|
||||||
|
# OPTIONAL - Maximum number of profile that can send notification before they are merged
|
||||||
|
# maximum_number_notification_profiles=2
|
||||||
|
|
||||||
|
# OPTIONAL - Keys to aggregate when merging events
|
||||||
|
# keys_to_aggregate="operation,class,name,denied,target"
|
||||||
|
|
||||||
# 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"
|
||||||
|
|
|
@ -184,7 +184,7 @@ 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] [--prompt-filter PF] [--debug]
|
[-u USER] [-w NUM] [-m] [--prompt-filter PF] [--debug]
|
||||||
[--filter.profile PROFILE] [--filter.operation OPERATION]
|
[--filter.profile PROFILE] [--filter.operation OPERATION]
|
||||||
[--filter.name NAME] [--filter.denied DENIED]
|
[--filter.name NAME] [--filter.denied DENIED]
|
||||||
[--filter.family FAMILY] [--filter.socket SOCKET]
|
[--filter.family FAMILY] [--filter.socket SOCKET]
|
||||||
|
@ -207,6 +207,8 @@ 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)
|
||||||
|
-m, --merge-notifications
|
||||||
|
Merge notification for improved readability (with -p)
|
||||||
--prompt-filter PF kind of operations which display a popup prompt
|
--prompt-filter PF kind of operations which display a popup prompt
|
||||||
--debug debug mode
|
--debug debug mode
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue