From 5490dddbdadf525237b59aefe7cac7f9bf959746 Mon Sep 17 00:00:00 2001 From: Kshitij Gupta Date: Wed, 21 Aug 2013 11:26:09 +0530 Subject: [PATCH] First set of tools in their alpha release, logprof and genprof are pre-bleeding edge so dont hurt yourself or worse your distro. --- Tools/aa-audit.py | 54 ++++++++++++++ Tools/aa-autodep.py | 53 ++++++++++++++ Tools/aa-complain.py | 55 +++++++++++++++ Tools/aa-disable.py | 67 ++++++++++++++++++ Tools/aa-enforce.py | 67 ++++++++++++++++++ Tools/aa-genprof.py | 148 +++++++++++++++++++++++++++++++++++++++ Tools/aa-logprof.py | 27 +++++-- Tools/aa-unconfined.py | 69 ++++++++++++++++++ apparmor/aa.py | 75 ++++++++++++++------ apparmor/config.py | 4 +- apparmor/logparser.py | 12 ++-- apparmor/severity.py | 91 ++++++++++++------------ apparmor/ui.py | 103 ++++++++++++++++----------- apparmor/writeprofile.py | 114 ------------------------------ 14 files changed, 703 insertions(+), 236 deletions(-) create mode 100644 Tools/aa-audit.py create mode 100644 Tools/aa-autodep.py create mode 100644 Tools/aa-complain.py create mode 100644 Tools/aa-disable.py create mode 100644 Tools/aa-enforce.py create mode 100644 Tools/aa-genprof.py create mode 100644 Tools/aa-unconfined.py diff --git a/Tools/aa-audit.py b/Tools/aa-audit.py new file mode 100644 index 000000000..c5e9b2380 --- /dev/null +++ b/Tools/aa-audit.py @@ -0,0 +1,54 @@ +#!/usr/bin/python +import sys +import os +import argparse + +import apparmor.aa as apparmor + +parser = argparse.ArgumentParser(description='Switch the given programs to audit mode') +parser.add_argument('-d', type=str, help='path to profiles') +parser.add_argument('program', type=str, nargs='+', help='name of program to hswitch to audit mode') +args = parser.parse_args() + +profiling = args.program +profiledir = args.d + +if profiledir: + apparmor.profile_dir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException("Can't find AppArmor profiles in %s." %profiledir) + +for p in profiling: + if not p: + continue + + program = None + if os.path.exists(p): + program = apparmor.get_full_path(p).strip() + else: + which = apparmor.which(p) + if which: + program = apparmor.get_full_path(which) + + if os.path.exists(program): + apparmor.read_profiles() + filename = apparmor.get_profile_filename(program) + + if not os.path.isfile(filename) or apparmor.is_skippable_file(filename): + continue + + sys.stdout.write(_('Setting %s to audit mode.\n')%program) + + apparmor.set_profile_flags(filename, 'audit') + + cmd_info = apparmor.cmd(['cat', filename, '|', parser, '-I%s'%apparmor.profile_dir, '-R 2>&1', '1>/dev/null']) + if cmd_info[0] != 0: + raise apparmor.AppArmorException(cmd_info[1]) + else: + if '/' not in p: + apparmor.UI_Info(_("Can't find %s in the system path list. If the name of the application is correct, please run 'which %s' as a user with correct PATH environment set up in order to find the fully-qualified path.")%(p, p)) + else: + apparmor.UI_Info(_("%s does not exist, please double-check the path.")%p) + sys.exit(1) + +sys.exit(0) \ No newline at end of file diff --git a/Tools/aa-autodep.py b/Tools/aa-autodep.py new file mode 100644 index 000000000..9adbc429b --- /dev/null +++ b/Tools/aa-autodep.py @@ -0,0 +1,53 @@ +#!/usr/bin/python +import sys +import os +import argparse + +import apparmor.aa as apparmor + +parser = argparse.ArgumentParser(description='Disable the profile for the given programs') +parser.add_argument('--force', type=str, help='path to profiles') +parser.add_argument('-d', type=str, help='path to profiles') +parser.add_argument('program', type=str, nargs='+', help='name of program to have profile disabled') +args = parser.parse_args() + +force = args.force +profiledir = args.d +profiling = args.program + +aa_mountpoint = apparmor.check_for_apparmor() + +if profiledir: + apparmor.profile_dir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException("Can't find AppArmor profiles in %s." %profiledir) + +for p in profiling: + if not p: + continue + + program = None + if os.path.exists(p): + program = apparmor.get_full_path(p).strip() + else: + which = apparmor.which(p) + if which: + program = apparmor.get_full_path(which) + + apparmor.check_qualifiers(program) + + if os.path.exists(program): + if os.path.exists(apparmor.get_profile_filename(program) and not force): + apparmor.UI_Info('Profile for %s already exists - skipping.'%program) + else: + apparmor.autodep(program) + if aa_mountpoint: + apparmor.reload(program) + else: + if '/' not in p: + apparmor.UI_Info(_("Can't find %s in the system path list. If the name of the application is correct, please run 'which %s' as a user with correct PATH environment set up in order to find the fully-qualified path.")%(p, p)) + else: + apparmor.UI_Info(_("%s does not exist, please double-check the path.")%p) + sys.exit(1) + +sys.exit(0) diff --git a/Tools/aa-complain.py b/Tools/aa-complain.py new file mode 100644 index 000000000..b7ee90839 --- /dev/null +++ b/Tools/aa-complain.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +import sys +import os +import argparse + +import apparmor.aa as apparmor + +parser = argparse.ArgumentParser(description='Switch the given program to complain mode') +parser.add_argument('-d', type=str, help='path to profiles') +parser.add_argument('program', type=str, nargs='+', help='name of program to switch to complain mode') +args = parser.parse_args() + +profiling = args.program +profiledir = args.d + +if profiledir: + apparmor.profile_dir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException("Can't find AppArmor profiles in %s." %profiledir) + +for p in profiling: + if not p: + continue + + program = None + if os.path.exists(p): + program = apparmor.get_full_path(p).strip() + else: + which = apparmor.which(p) + if which: + program = apparmor.get_full_path(which) + + if os.path.exists(program): + apparmor.read_profiles() + filename = apparmor.get_profile_filename(program) + + if not os.path.isfile(filename) or apparmor.is_skippable_file(filename): + continue + + sys.stdout.write(_('Setting %s to complain mode.\n')%program) + + apparmor.set_profile_flags(filename, 'complain') + + cmd_info = apparmor.cmd(['cat', filename, '|', parser, '-I%s'%apparmor.profile_dir, '-R 2>&1', '1>/dev/null']) + if cmd_info[0] != 0: + raise apparmor.AppArmorException(cmd_info[1]) + else: + if '/' not in p: + apparmor.UI_Info(_("Can't find %s in the system path list. If the name of the application is correct, please run 'which %s' as a user with correct PATH environment set up in order to find the fully-qualified path.")%(p, p)) + else: + apparmor.UI_Info(_("%s does not exist, please double-check the path.")%p) + sys.exit(1) + +sys.exit(0) + diff --git a/Tools/aa-disable.py b/Tools/aa-disable.py new file mode 100644 index 000000000..1eaac4ad0 --- /dev/null +++ b/Tools/aa-disable.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +import sys +import os +import argparse + +import apparmor.aa as apparmor + +parser = argparse.ArgumentParser(description='Disable the profile for the given programs') +parser.add_argument('-d', type=str, help='path to profiles') +parser.add_argument('program', type=str, nargs='+', help='name of program to have profile disabled') +args = parser.parse_args() + +profiledir = args.d +profiling = args.program + +if profiledir: + apparmor.profile_dir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException("Can't find AppArmor profiles in %s." %profiledir) + +disabledir = apparmor.profile_dir+'/disable' +if not os.path.isdir(disabledir): + raise apparmor.AppArmorException("Can't find AppArmor disable directorys %s." %disabledir) + +for p in profiling: + if not p: + continue + + program = None + if os.path.exists(p): + program = apparmor.get_full_path(p).strip() + else: + which = apparmor.which(p) + if which: + program = apparmor.get_full_path(which) + + if os.path.exists(program): + apparmor.read_profiles() + filename = apparmor.get_profile_filename(program) + + if not os.path.isfile(filename) or apparmor.is_skippable_file(filename): + continue + + bname = os.path.basename(filename) + if not bname: + apparmor.AppArmorException(_('Unable to find basename for %s.')%filename) + + sys.stdout.write(_('Disabling %s.\n')%program) + + link = '%s/%s'%(disabledir, bname) + if not os.path.exists(link): + try: + os.symlink(filename, link) + except: + raise apparmor.AppArmorException('Could not create %s symlink to %s.'%(link, filename)) + + cmd_info = apparmor.cmd(['cat', filename, '|', parser, '-I%s'%apparmor.profile_dir, '-R 2>&1', '1>/dev/null']) + if cmd_info[0] != 0: + raise apparmor.AppArmorException(cmd_info[1]) + else: + if '/' not in p: + apparmor.UI_Info(_("Can't find %s in the system path list. If the name of the application is correct, please run 'which %s' as a user with correct PATH environment set up in order to find the fully-qualified path.")%(p, p)) + else: + apparmor.UI_Info(_("%s does not exist, please double-check the path.")%p) + sys.exit(1) + +sys.exit(0) diff --git a/Tools/aa-enforce.py b/Tools/aa-enforce.py new file mode 100644 index 000000000..be0611659 --- /dev/null +++ b/Tools/aa-enforce.py @@ -0,0 +1,67 @@ +#!/usr/bin/python +import sys +import os +import argparse + +import apparmor.aa as apparmor + +parser = argparse.ArgumentParser(description='Switch the given program to enforce mode') +parser.add_argument('-d', type=str, help='path to profiles') +parser.add_argument('program', type=str, nargs='+', help='name of program to switch to enforce mode') +args = parser.parse_args() + +profiledir = args.d +profiling = args.program + +if profiledir: + apparmor.profile_dir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException("Can't find AppArmor profiles in %s." %profiledir) + +for p in profiling: + if not p: + continue + + program = None + if os.path.exists(p): + program = apparmor.get_full_path(p).strip() + else: + which = apparmor.which(p) + if which: + program = apparmor.get_full_path(which) + + if os.path.exists(program): + apparmor.read_profiles() + filename = apparmor.get_profile_filename(program) + + if not os.path.isfile(filename) or apparmor.is_skippable_file(filename): + continue + + sys.stdout.write(_('Setting %s to enforce mode.\n')%program) + + apparmor.set_profile_flags(filename, '') + + # Remove symlink from profile_dir/force-complain + complainlink = filename + complainlink = re.sub('^%s'%apparmor.profile_dir, '%s/force-complain'%apparmor.profile_dir, complainlink) + if os.path.exists(complainlink): + os.remove(complainlink) + + # remove symlink in profile_dir/disable + disablelink = filename + disablelink = re.sub('^%s'%apparmor.profile_dir, '%s/disable'%apparmor.profile_dir, disablelink) + if os.path.exists(disablelink): + os.remove(disablelink) + + cmd_info = apparmor.cmd(['cat', filename, '|', parser, '-I%s'%apparmor.profile_dir, '-R 2>&1', '1>/dev/null']) + if cmd_info[0] != 0: + raise apparmor.AppArmorException(cmd_info[1]) + else: + if '/' not in p: + apparmor.UI_Info(_("Can't find %s in the system path list. If the name of the application is correct, please run 'which %s' as a user with correct PATH environment set up in order to find the fully-qualified path.")%(p, p)) + else: + apparmor.UI_Info(_("%s does not exist, please double-check the path.")%p) + sys.exit(1) + +sys.exit(0) + diff --git a/Tools/aa-genprof.py b/Tools/aa-genprof.py new file mode 100644 index 000000000..db9733d82 --- /dev/null +++ b/Tools/aa-genprof.py @@ -0,0 +1,148 @@ +#!/usr/bin/python +import sys +import subprocess +import os +import re +import atexit +import argparse + +import apparmor.aa as apparmor + +def sysctl_read(path): + value = None + with open(path, 'r') as f_in: + value = int(f_in.readline()) + return value + +def sysctl_write(path, value): + if not value: + return + with open(path, 'w') as f_out: + f_out.write(str(value)) + +def last_audit_entry_time(): + out = subprocess.check_output(['tail', '-1', '/var/log/audit/audit.log'], shell=True) + logmark = None + if re.search('^*msg\=audit\((\d+\.\d+\:\d+).*\).*$', out): + logmark = re.search('^*msg\=audit\((\d+\.\d+\:\d+).*\).*$', out).groups()[0] + else: + logmark = '' + return logmark + +def restore_ratelimit(): + sysctl_write(ratelimit_sysctl, ratelimit_saved) + +parser = argparse.ArgumentParser(description='Generate profile for the given program') +parser.add_argument('-d', type=str, help='path to profiles') +parser.add_argument('-f', type=str, help='path to logfile') +parser.add_argument('program', type=str, help='name of program to profile') +args = parser.parse_args() + +profiling = args.program +profiledir = args.d +filename = args.f + +aa_mountpoint = apparmor.check_for_apparmor() +if not aa_mountpoint: + raise apparmor.AppArmorException(_('AppArmor seems to have not been started. Please enable AppArmor and try again.')) + +if profiledir: + apparmor.profile_dir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profile_dir): + raise apparmor.AppArmorException("Can't find AppArmor profiles in %s." %profiledir) + + +# if not profiling: +# profiling = apparmor.UI_GetString(_('Please enter the program to profile: '), '') +# if profiling: +# profiling = profiling.strip() +# else: +# sys.exit(0) + +program = None +#if os.path.exists(apparmor.which(profiling.strip())): +if os.path.exists(profiling): + program = apparmor.get_full_path(profiling) +else: + if '/' not in profiling: + which = apparmor.which(profiling) + if which: + program = apparmor.get_full_path(which) + +if not program or not os.path.exists(program): + if '/' not in profiling: + raise apparmor.AppArmorException(_("Can't find %s in the system path list. If the name of the application is correct, please run 'which %s' in another window in order to find the fully-qualified path.") %(profiling, profiling)) + else: + raise apparmor.AppArmorException(_('%s does not exists, please double-check the path.') %profiling) + +# Check if the program has been marked as not allowed to have a profile +apparmor.check_qualifiers(program) + +apparmor.loadincludes() + +profile_filename = apparmor.get_profile_filename(program) +if os.path.exists(profile_filename): + apparmor.helpers[program] = apparmor.get_profile_flags(profile_filename) +else: + apparmor.autodep(program) + apparmor.helpers[program] = 'enforce' + +if apparmor.helpers[program] == 'enforce': + apparmor.complain(program) + apparmor.reload(program) + +# When reading from syslog, it is possible to hit the default kernel +# printk ratelimit. This will result in audit entries getting skipped, +# making profile generation inaccurate. When using genprof, disable +# the printk ratelimit, and restore it on exit. +ratelimit_sysctl = '/proc/sys/kernel/printk_ratelimit' +ratelimit_saved = sysctl_read(ratelimit_sysctl) +sysctl_write(ratelimit_sysctl, 0) + +atexit.register(restore_ratelimit) + +apparmor.UI_Info(_('\nBefore you begin, you may wish to check if a\nprofile already exists for the application you\nwish to confine. See the following wiki page for\nmore information:\nhttp://wiki.apparmor.net/index.php/Profiles')) + +apparmor.UI_Important(_('Please start the application to be profiled in\nanother window and exercise its functionality now.\n\nOnce completed, select the "Scan" option below in \norder to scan the system logs for AppArmor events. \n\nFor each AppArmor event, you will be given the \nopportunity to choose whether the access should be \nallowed or denied.')) + +syslog = True +logmark = '' +done_profiling = False + +if os.path.exists('/var/log/audit/audit.log'): + syslog = False + +passno = 0 +while not done_profiling: + if syslog: + logmark = subprocess.check_output(['date | md5sum'], shell=True) + logmark = logmark.decode('ascii').strip() + logmark = re.search('^([0-9a-f]+)', logmark).groups()[0] + t=subprocess.call("%s -p kern.warn 'GenProf: %s'"%(apparmor.logger, logmark), shell=True) + + else: + logmark = last_audit_entry_time() + + q=apparmor.hasher() + q['headers'] = [_('Profiling'), program] + q['functions'] = ['CMD_SCAN', 'CMD_FINISHED'] + q['default'] = 'CMD_SCAN' + ans, arg = apparmor.UI_PromptUser(q, 'noexit') + + if ans == 'CMD_SCAN': + lp_ret = apparmor.do_logprof_pass(logmark, passno) + passno += 1 + if lp_ret == 'FINISHED': + done_profiling = True + else: + done_profiling = True + +for p in sorted(apparmor.helpers.keys()): + if apparmor.helpers[p] == 'enforce': + enforce(p) + reload(p) + +apparmor.UI_Info(_('\nReloaded AppArmor profiles in enforce mode.')) +apparmor.UI_Info(_('\nPlease consider contributing your new profile!\nSee the following wiki page for more information:\nhttp://wiki.apparmor.net/index.php/Profiles\n')) +apparmor.UI_Info(_('Finished generating profile for %s.')%program) +sys.exit(0) diff --git a/Tools/aa-logprof.py b/Tools/aa-logprof.py index ebc3402aa..d8b73cd46 100644 --- a/Tools/aa-logprof.py +++ b/Tools/aa-logprof.py @@ -1,15 +1,30 @@ #!/usr/bin/python import sys - -sys.path.append('../') -import apparmor.aa +import apparmor.aa as apparmor import os import argparse -logmark = '' +parser = argparse.ArgumentParser(description='Process log entries to generate profiles') +parser.add_argument('-d', type=str, help='path to profiles') +parser.add_argument('-f', type=str, help='path to logfile') +parser.add_argument('-m', type=str, help='mark in the log to start processing after') +args = parser.parse_args() -apparmor.aa.loadincludes() +profiledir = args.d +filename = args.f +logmark = args.m or '' -apparmor.aa.do_logprof_pass(logmark) +aa_mountpoint = apparmor.check_for_apparmor() +if not aa_mountpoint: + raise apparmor.AppArmorException(_('AppArmor seems to have not been started. Please enable AppArmor and try again.')) +if profiledir: + apparmor.profiledir = apparmor.get_full_path(profiledir) + if not os.path.isdir(apparmor.profiledir): + raise apparmor.AppArmorException("Can't find AppArmor profiles in %s." %profiledir) +apparmor.loadincludes() + +apparmor.do_logprof_pass(logmark) + +sys.exit(0) diff --git a/Tools/aa-unconfined.py b/Tools/aa-unconfined.py new file mode 100644 index 000000000..be39f711a --- /dev/null +++ b/Tools/aa-unconfined.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +import sys +import os +import re +import argparse + +import apparmor.aa as apparmor + +parser = argparse.ArgumentParser(description='') +parser.add_argument('--paranoid', type=str) +args = parser.parse_args() + +paranoid = args.paranoid + +aa_mountpoint = apparmor.check_for_apparmor() +if not aa_mountpoint: + raise apparmor.AppArmorException(_('AppArmor seems to have not been started. Please enable AppArmor and try again.')) + +pids = [] +if paranoid: + pids = list(filter(lambda x: re.search('^\d+$', x), apparmor.get_subdirectories('/proc'))) +else: + regex_tcp_udp = re.compile('^(tcp|udp)\s+\d+\s+\d+\s+\S+\:(\d+)\s+\S+\:(\*|\d+)\s+(LISTEN|\s+)\s+(\d+)\/(\S+)') + output = apparmor.cmd(['netstat','-nlp'])[1].split('\n') + for line in output: + match = regex_tcp_udp.search(line) + if match: + pids.append(match.groups()[4]) +# We can safely remove duplicate pid's? +pids = list(map(lambda x: int(x), set(pids))) + +for pid in sorted(pids): + try: + prog = os.readlink('/proc/%s/exe'%pid) + except: + continue + attr = None + if os.path.exists('/proc/%s/attr/current'%pid): + with apparmor.open_file_read('/proc/%s/attr/current'%pid) as current: + for line in current: + if line.startswith('/') or line.startswith('null'): + attr = line.strip() + + cmdline = apparmor.cmd(['cat', '/proc/%s/cmdline'%pid])[1] + pname = cmdline.split('\0')[0] + if '/' in pname and pname != prog: + pname = '(%s)'%pname + else: + pname = '' + if not attr: + if re.search('^(/usr)?/bin/(python|perl|bash)', prog): + cmdline = re.sub('\0', ' ', cmdline) + cmdline = re.sub('\s+$', '', cmdline).strip() + sys.stdout.write(_('%s %s (%s) not confined\n')%(pid, prog, cmdline)) + else: + if pname and pname[-1] == ')': + pname += ' ' + sys.stdout.write(_('%s %s %snot confined\n')%(pid, prog, pname)) + else: + if re.search('^(/usr)?/bin/(python|perl|bash)', prog): + cmdline = re.sub('\0', ' ', cmdline) + cmdline = re.sub('\s+$', '', cmdline).strip() + sys.stdout.write(_("%s %s (%s) confined by '%s'\n")%(pid, prog, cmdline, attr)) + else: + if pname and pname[-1] == ')': + pname += ' ' + sys.stdout.write(_("%s %s %sconfined by '%s'\n")%(pid, prog, pname, attr)) + +sys.exit(0) \ No newline at end of file diff --git a/apparmor/aa.py b/apparmor/aa.py index dcfbef794..92fab724d 100644 --- a/apparmor/aa.py +++ b/apparmor/aa.py @@ -18,7 +18,7 @@ import apparmor.logparser import apparmor.severity import LibAppArmor -from apparmor.common import (AppArmorException, error, debug, msg, +from apparmor.common import (AppArmorException, error, debug, msg, cmd, open_file_read, valid_path, hasher, open_file_write, convert_regexp, DebugLogger) @@ -228,7 +228,7 @@ def complain(path): set_profile_flags(prof_filename, 'complain') def enforce(path): - """Sets the profile to complain mode if it exists""" + """Sets the profile to enforce mode if it exists""" prof_filename, name = name_to_prof_filename(path) if not prof_filename : fatal_error("Can't find %s" % path) @@ -241,7 +241,9 @@ def head(file): if os.path.isfile(file): with open_file_read(file) as f_in: first = f_in.readline().rstrip() - return first + return first + else: + raise AppArmorException('Unable to read first line from: %s : File Not Found' %file) def get_output(params): """Returns the return code output by running the program with the args given in the list""" @@ -484,7 +486,7 @@ def autodep(bin_name, pname=''): if not bin_name and pname.startswith('/'): bin_name = pname if not repo_cfg and not cfg['repository'].get('url', False): - repo_conf = apparmor.config.Config('shell') + repo_conf = apparmor.config.Config('shell', CONFDIR) repo_cfg = repo_conf.read_config('repository.conf') if not repo_cfg.get('repository', False) or repo_cfg['repository']['enabled'] == 'later': UI_ask_to_enable_repo() @@ -511,6 +513,16 @@ def autodep(bin_name, pname=''): filelist.file = hasher() filelist[file][include]['tunables/global'] = True write_profile_ui_feedback(pname) + +def get_profile_flags(filename): + flags = 'enforce' + with open_file_read(filename) as f_in: + for line in f_in: + if RE_PROFILE_START.search(line): + flags = RE_PROFILE_START.search(line).groups()[6] + return flags + return flags + def set_profile_flags(prof_filename, newflags): """Reads the old profile file and updates the flags accordingly""" @@ -821,6 +833,7 @@ def handle_children(profile, hat, root): else: typ = entry.pop(0) if typ == 'fork': + # If type is fork then we (should) have pid, profile and hat pid, p, h = entry[:3] if not regex_nullcomplain.search(p) and not regex_nullcomplain.search(h): profile = p @@ -830,6 +843,7 @@ def handle_children(profile, hat, root): else: profile_changes[pid] = profile elif typ == 'unknown_hat': + # If hat is not known then we (should) have pid, profile, hat, mode and unknown hat in entry pid, p, h, aamode, uhat = entry[:5] if not regex_nullcomplain.search(p): profile = p @@ -882,9 +896,11 @@ def handle_children(profile, hat, root): elif ans == 'CMD_USEDEFAULT': hat = default_hat elif ans == 'CMD_DENY': + # As unknown hat is denied no entry for it should be made return None elif typ == 'capability': + # If capability then we (should) have pid, profile, hat, program, mode, capability pid, p, h, prog, aamode, capability = entry[:6] if not regex_nullcomplain.search(p) and not regex_nullcomplain.search(h): profile = p @@ -894,6 +910,7 @@ def handle_children(profile, hat, root): prelog[aamode][profile][hat]['capability'][capability] = True elif typ == 'path' or typ == 'exec': + # If path or exec then we (should) have pid, profile, hat, program, mode, details and to_name pid, p, h, prog, aamode, mode, detail, to_name = entry[:8] if not mode: mode = set() @@ -1086,6 +1103,7 @@ def handle_children(profile, hat, root): default = None if 'p' in options and os.path.exists(get_profile_filename(exec_target)): default = 'CMD_px' + sys.stdout.write('Target profile exists: %s\n' %get_profile_filename(exec_target)) elif 'i' in options: default = 'CMD_ix' elif 'c' in options: @@ -1309,6 +1327,7 @@ def handle_children(profile, hat, root): return None elif typ == 'netdomain': + # If netdomain we (should) have pid, profile, hat, program, mode, network family, socket type and protocol pid, p, h, prog, aamode, family, sock_type, protocol = entry[:8] if not regex_nullcomplain.search(p) and not regex_nullcomplain.search(h): @@ -1717,15 +1736,17 @@ def ask_the_questions(): else: if aa[profile][hat]['allow']['path'][path].get('mode', False): mode |= aa[profile][hat]['allow']['path'][path]['mode'] - deleted = 0 + deleted = [] for entry in aa[profile][hat]['allow']['path'].keys(): if path == entry: continue if matchregexp(path, entry): if mode_contains(mode, aa[profile][hat]['allow']['path'][entry]['mode']): - aa[profile][hat]['allow']['path'].pop(entry) - deleted += 1 + deleted.append(entry) + for entry in deleted: + aa[profile][hat]['allow']['path'].pop(entry) + deleted = len(deleted) if owner_toggle == 0: mode = flatten_mode(mode) @@ -1948,13 +1969,15 @@ def delete_net_duplicates(netrules, incnetrules): return deleted def delete_cap_duplicates(profilecaps, inccaps): - deleted = 0 + deleted = [] if profilecaps and inccaps: for capname in profilecaps.keys(): if inccaps[capname].get('set', False) == 1: - profilecaps.pop(capname) - deleted += 1 - return deleted + deleted.append(capname) + for capname in deleted: + profilecaps.pop(capname) + + return len(deleted) def delete_path_duplicates(profile, incname, allow): deleted = [] @@ -2047,7 +2070,7 @@ def match_net_includes(profile, family, nettype): return newincludes -def do_logprof_pass(logmark='', pid=pid): +def do_logprof_pass(logmark='', passno=0, pid=pid): # set up variables for this pass t = hasher() # transitions = hasher() @@ -2066,9 +2089,10 @@ def do_logprof_pass(logmark='', pid=pid): # filelist = hasher() UI_Info(_('Reading log entries from %s.') %filename) - UI_Info(_('Updating AppArmor profiles in %s.') %profile_dir) - read_profiles() + if not passno: + UI_Info(_('Updating AppArmor profiles in %s.') %profile_dir) + read_profiles() if not sev_db: sev_db = apparmor.severity.Severity(CONFDIR + '/severity.db', _('unknown')) @@ -2158,7 +2182,7 @@ def save_profiles(): q['title'] = 'Changed Local Profiles' q['headers'] = [] q['explanation'] = _('The following local profiles were changed. Would you like to save them?') - q['functions'] = ['CMD_SAVE_CHANGES', 'CMD_VIEW_CHANGES', 'CMD_VIEW_CHANGES_CLEAN', 'CMD_ABORT'] + q['functions'] = ['CMD_SAVE_CHANGES', 'CMD_SAVE_SELECTED', 'CMD_VIEW_CHANGES', 'CMD_VIEW_CHANGES_CLEAN', 'CMD_ABORT'] q['default'] = 'CMD_VIEW_CHANGES' q['options'] = changed q['selected'] = 0 @@ -2167,7 +2191,14 @@ def save_profiles(): arg = None while ans != 'CMD_SAVE_CHANGES': ans, arg = UI_PromptUser(q) - if ans == 'CMD_VIEW_CHANGES': + if ans == 'CMD_SAVE_SELECTED': + profile_name = list(changed.keys())[arg] + write_profile_ui_feedback(profile_name) + reload_base(profile_name) + changed.pop(profile_name) + #q['options'] = changed + + elif ans == 'CMD_VIEW_CHANGES': which = list(changed.keys())[arg] oldprofile = None if aa[which][which].get('filename', False): @@ -2485,9 +2516,8 @@ def parse_profile_data(data, file, do_include): profile_data[profile][hat]['external'] = True else: hat = profile - # Profile stored - existing_profiles[profile] = file - + # Profile stored + existing_profiles[profile] = file flags = matches[6] @@ -2959,7 +2989,7 @@ def write_cap_rules(prof_data, depth, allow): for cap in sorted(prof_data[allow]['capability'].keys()): audit = '' if prof_data[allow]['capability'][cap].get('audit', False): - audit = 'audit' + audit = 'audit ' if prof_data[allow]['capability'][cap].get('set', False): data.append('%s%s%scapability %s,' %(pre, audit, allowstr, cap)) data.append('') @@ -3837,7 +3867,7 @@ def reload_base(bin_path): def reload(bin_path): bin_path = find_executable(bin_path) - if not bin: + if not bin_path: return None return reload_base(bin_path) @@ -3955,6 +3985,7 @@ def check_qualifiers(program): 'them is likely to break the rest of the system. If you know what you\'re\n' + 'doing and are certain you want to create a profile for this program, edit\n' + 'the corresponding entry in the [qualifiers] section in /etc/apparmor/logprof.conf.') %program) + return False def get_subdirectories(current_dir): """Returns a list of all directories directly inside given directory""" @@ -4048,7 +4079,7 @@ def matchregexp(new, old): ######Initialisations###### -conf = apparmor.config.Config('ini') +conf = apparmor.config.Config('ini', CONFDIR) cfg = conf.read_config('logprof.conf') #print(cfg['settings']) diff --git a/apparmor/config.py b/apparmor/config.py index e6ea8a5f6..ae8f70291 100644 --- a/apparmor/config.py +++ b/apparmor/config.py @@ -28,8 +28,8 @@ from apparmor.common import AppArmorException, warn, msg, open_file_read # REPO_CFG = None # SHELL_FILES = ['easyprof.conf', 'notify.conf', 'parser.conf', 'subdomain.conf'] class Config: - def __init__(self, conf_type): - self.CONF_DIR = '/etc/apparmor' + def __init__(self, conf_type, conf_dir='/etc/apparmor'): + self.CONF_DIR = conf_dir # The type of config file that'll be read and/or written if conf_type == 'shell' or conf_type == 'ini': self.conf_type = conf_type diff --git a/apparmor/logparser.py b/apparmor/logparser.py index 1b6ac3544..55a2757e8 100644 --- a/apparmor/logparser.py +++ b/apparmor/logparser.py @@ -50,7 +50,7 @@ class ReadLog: if self.next_log_entry: sys.stderr.out('A log entry already present: %s' % self.next_log_entry) self.next_log_entry = self.LOG.readline() - while not (self.RE_LOG_v2_6_syslog.search(self.next_log_entry) or self.RE_LOG_v2_6_audit.search(self.next_log_entry)) or (self.logmark and re.search(self.logmark, self.next_log_entry)): + while not self.RE_LOG_v2_6_syslog.search(self.next_log_entry) and not self.RE_LOG_v2_6_audit.search(self.next_log_entry) and not (self.logmark and self.logmark in self.next_log_entry): self.next_log_entry = self.LOG.readline() if not self.next_log_entry: break @@ -328,8 +328,11 @@ class ReadLog: except IOError: raise AppArmorException('Can not read AppArmor logfile: ' + self.filename) #LOG = open_file_read(log_open) - line = self.get_next_log_entry() + line = True while line: + line = self.get_next_log_entry() + if not line: + break line = line.strip() self.debug_logger.debug('read_log: %s' % line) if self.logmark in line: @@ -337,13 +340,12 @@ class ReadLog: self.debug_logger.debug('read_log: seenmark = %s' %seenmark) if not seenmark: - line = self.get_next_log_entry() - continue + continue + event = self.parse_log_record(line) #print(event) if event: self.add_event_to_tree(event) - line = self.get_next_log_entry() self.LOG.close() self.logmark = '' return self.log diff --git a/apparmor/severity.py b/apparmor/severity.py index 3783b9732..7294411a2 100644 --- a/apparmor/severity.py +++ b/apparmor/severity.py @@ -17,53 +17,50 @@ class Severity: self.severity['VARIABLES'] = dict() if not dbname: return None - try: - database = open_file_read(dbname)#open(dbname, 'r') - except IOError: - raise AppArmorException("Could not open severity database: %s" % dbname) - for lineno, line in enumerate(database, start=1): - line = line.strip() # or only rstrip and lstrip? - if line == '' or line.startswith('#') : - continue - if line.startswith('/'): - try: - path, read, write, execute = line.split() - read, write, execute = int(read), int(write), int(execute) - except ValueError: - raise AppArmorException("Insufficient values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) - else: - if read not in range(0,11) or write not in range(0,11) or execute not in range(0,11): - raise AppArmorException("Inappropriate values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) - path = path.lstrip('/') - if '*' not in path: - self.severity['FILES'][path] = {'r': read, 'w': write, 'x': execute} + + with open_file_read(dbname) as database:#open(dbname, 'r') + for lineno, line in enumerate(database, start=1): + line = line.strip() # or only rstrip and lstrip? + if line == '' or line.startswith('#') : + continue + if line.startswith('/'): + try: + path, read, write, execute = line.split() + read, write, execute = int(read), int(write), int(execute) + except ValueError: + raise AppArmorException("Insufficient values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) else: - ptr = self.severity['REGEXPS'] - pieces = path.split('/') - for index, piece in enumerate(pieces): - if '*' in piece: - path = '/'.join(pieces[index:]) - regexp = convert_regexp(path) - ptr[regexp] = {'AA_RANK': {'r': read, 'w': write, 'x': execute}} - break - else: - ptr[piece] = ptr.get(piece, {}) - ptr = ptr[piece] - elif line.startswith('CAP_'): - try: - resource, severity = line.split() - severity = int(severity) - except ValueError as e: - error_message = 'No severity value present in file: %s\n\t[Line %s]: %s' % (dbname, lineno, line) - #error(error_message) - raise AppArmorException(error_message) # from None + if read not in range(0,11) or write not in range(0,11) or execute not in range(0,11): + raise AppArmorException("Inappropriate values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) + path = path.lstrip('/') + if '*' not in path: + self.severity['FILES'][path] = {'r': read, 'w': write, 'x': execute} + else: + ptr = self.severity['REGEXPS'] + pieces = path.split('/') + for index, piece in enumerate(pieces): + if '*' in piece: + path = '/'.join(pieces[index:]) + regexp = convert_regexp(path) + ptr[regexp] = {'AA_RANK': {'r': read, 'w': write, 'x': execute}} + break + else: + ptr[piece] = ptr.get(piece, {}) + ptr = ptr[piece] + elif line.startswith('CAP_'): + try: + resource, severity = line.split() + severity = int(severity) + except ValueError as e: + error_message = 'No severity value present in file: %s\n\t[Line %s]: %s' % (dbname, lineno, line) + #error(error_message) + raise AppArmorException(error_message) # from None + else: + if severity not in range(0,11): + raise AppArmorException("Inappropriate severity value present in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) + self.severity['CAPABILITIES'][resource] = severity else: - if severity not in range(0,11): - raise AppArmorException("Inappropriate severity value present in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) - self.severity['CAPABILITIES'][resource] = severity - else: - raise AppArmorException("Unexpected line in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) - database.close() + raise AppArmorException("Unexpected line in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line)) def handle_capability(self, resource): """Returns the severity of for the capability resource, default value if no match""" @@ -187,11 +184,11 @@ class Severity: try: self.severity['VARIABLES'][line[0]] += [i.strip('"') for i in line[1].split()] except KeyError: - raise AppArmorException("Variable %s was not previously declared, but is being assigned additional values" % line[0]) + raise AppArmorException("Variable %s was not previously declared, but is being assigned additional values in file: %s" % (line[0], prof_path)) else: line = line.split('=') if line[0] in self.severity['VARIABLES'].keys(): - raise AppArmorException("Variable %s was previously declared" % line[0]) + raise AppArmorException("Variable %s was previously declared in file: %s" % (line[0], prof_path)) self.severity['VARIABLES'][line[0]] = [i.strip('"') for i in line[1].split()] def unload_variables(self): diff --git a/apparmor/ui.py b/apparmor/ui.py index 3c7d314d7..cf925e6e2 100644 --- a/apparmor/ui.py +++ b/apparmor/ui.py @@ -42,25 +42,43 @@ def UI_Important(text): }) path, yarg = GetDataFromYast() +def get_translated_hotkey(translated, cmsg=''): + msg = 'PromptUser: '+_('Invalid hotkey for') + if re.search('\((\S)\)', translated): + return re.search('\((\S)\)', translated).groups()[0] + else: + if cmsg: + raise AppArmorException(cmsg) + else: + raise AppArmorException('%s %s' %(msg, translated)) + def UI_YesNo(text, default): debug_logger.debug('UI_YesNo: %s: %s %s' %(UI_mode, text, default)) - ans = default + default = default.lower() + ans = None if UI_mode == 'text': - yes = '(Y)es' - no = '(N)o' - usrmsg = 'PromptUser: Invalid hotkey for' - yeskey = 'y' - nokey = 'n' - sys.stdout.write('\n' + text + '\n') - if default == 'y': - sys.stdout.write('\n[%s] / %s\n' % (yes, no)) - else: - sys.stdout.write('\n%s / [%s]\n' % (yes, no)) - ans = getkey()#readkey() - if ans: - ans = ans.lower() - else: - ans = default + yes = _('(Y)es') + no = _('(N)o') + yeskey = get_translated_hotkey(yes).lower() + nokey = get_translated_hotkey(no).lower() + ans = 'XXXINVALIDXXX' + while ans not in ['y', 'n']: + sys.stdout.write('\n' + text + '\n') + if default == 'y': + sys.stdout.write('\n[%s] / %s\n' % (yes, no)) + else: + sys.stdout.write('\n%s / [%s]\n' % (yes, no)) + ans = getkey() + if ans: + # Get back to english from localised answer + ans = ans.lower() + if ans == yeskey: + ans = 'y' + else: + ans = 'n' + else: + ans = default + else: SendDataToYast({ 'type': 'dialog-yesno', @@ -74,16 +92,19 @@ def UI_YesNo(text, default): def UI_YesNoCancel(text, default): debug_logger.debug('UI_YesNoCancel: %s: %s %s' % (UI_mode, text, default)) - + default = default.lower() + ans = None if UI_mode == 'text': - yes = '(Y)es' - no = '(N)o' - cancel = '(C)ancel' - yeskey = 'y' - nokey = 'n' - cancelkey = 'c' + yes = _('(Y)es') + no = _('(N)o') + cancel = _('(C)ancel') + + yeskey = get_translated_hotkey(yes).lower() + nokey = get_translated_hotkey(no).lower() + cancelkey = get_translated_hotkey(cancel).lower() + ans = 'XXXINVALIDXXX' - while ans != 'c' and ans != 'n' and ans != 'y': + while ans not in ['c', 'n', 'y']: sys.stdout.write('\n' + text + '\n') if default == 'y': sys.stdout.write('\n[%s] / %s / %s\n' % (yes, no, cancel)) @@ -91,9 +112,16 @@ def UI_YesNoCancel(text, default): sys.stdout.write('\n%s / [%s] / %s\n' % (yes, no, cancel)) else: sys.stdout.write('\n%s / %s / [%s]\n' % (yes, no, cancel)) - ans = getkey()#readkey() + ans = getkey() if ans: + # Get back to english from localised answer ans = ans.lower() + if ans == yeskey: + ans = 'y' + elif ans == nokey: + ans = 'n' + elif ans== cancelkey: + ans= 'c' else: ans = default else: @@ -111,7 +139,7 @@ def UI_GetString(text, default): debug_logger.debug('UI_GetString: %s: %s %s' % (UI_mode, text, default)) string = default if UI_mode == 'text': - sys.stdout.write('\n' + text + '\n') + sys.stdout.write('\n' + text) string = sys.stdin.readline() else: SendDataToYast({ @@ -198,6 +226,7 @@ CMDS = { 'CMD_UPDATE_PROFILE': '(U)pdate Profile', 'CMD_IGNORE_UPDATE': '(I)gnore Update', 'CMD_SAVE_CHANGES': '(S)ave Changes', + 'CMD_SAVE_SELECTED': 'Save Selec(t)ed Profile', 'CMD_UPLOAD_CHANGES': '(U)pload Changes', 'CMD_VIEW_CHANGES': '(V)iew Changes', 'CMD_VIEW_CHANGES_CLEAN': 'View Changes b/w (C)lean profiles', @@ -216,7 +245,7 @@ CMDS = { 'CMD_IGNORE_ENTRY': '(I)gnore' } -def UI_PromptUser(q): +def UI_PromptUser(q, params=''): cmd = None arg = None if UI_mode == 'text': @@ -230,10 +259,11 @@ def UI_PromptUser(q): arg = yarg['selected'] if cmd == 'CMD_ABORT': confirm_and_abort() - cmd == 'XXXINVALIDXXX' + cmd = 'XXXINVALIDXXX' elif cmd == 'CMD_FINISHED': - confirm_and_finish() - cmd == 'XXXINVALIDXXX' + if not params: + confirm_and_finish() + cmd = 'XXXINVALIDXXX' return (cmd, arg) def confirm_and_abort(): @@ -287,11 +317,7 @@ def Text_PromptUser(question): menutext = _(CMDS[cmd]) - menuhotkey = re.search('\((\S)\)', menutext) - if not menuhotkey: - raise AppArmorException('PromptUser: %s \'%s\'' %(_('Invalid hotkey in'), menutext)) - - key = menuhotkey.groups()[0].lower() + key = get_translated_hotkey(menutext).lower() # Duplicate hotkey if keys.get(key, False): raise AppArmorException('PromptUser: %s %s: %s' %(_('Duplicate hotkey for'), cmd, menutext)) @@ -306,12 +332,9 @@ def Text_PromptUser(question): default_key = 0 if default and CMDS[default]: defaulttext = _(CMDS[default]) + defmsg = 'PromptUser: ' + _('Invalid hotkey in default item') - defaulthotkey = re.search('\((\S)\)', defaulttext) - if not menuhotkey: - raise AppArmorException('PromptUser: %s \'%s\'' %(_('Invalid hotkey in default item'), defaulttext)) - - default_key = defaulthotkey.groups()[0].lower() + default_key = get_translated_hotkey(defaulttext, defmsg).lower() if not keys.get(default_key, False): raise AppArmorException('PromptUser: %s %s' %(_('Invalid default'), default)) diff --git a/apparmor/writeprofile.py b/apparmor/writeprofile.py index 0e1a2dd94..e69de29bb 100644 --- a/apparmor/writeprofile.py +++ b/apparmor/writeprofile.py @@ -1,114 +0,0 @@ -def write_header(prof_data, depth, name, embedded_hat, write_flags): - pre = ' ' * depth - data = [] - name = quote_if_needed(name) - - if (not embedded_hat and re.search('^[^/]|^"[^/]', name)) or (embedded_hat and re.search('^[^^]' ,name)): - name = 'profile %s' % name - - if write_flags and prof_data['flags']: - data.append('%s%s flags=(%s) {' % (pre, name, prof_data['flags'])) - else: - data.append('%s%s {' % (pre, name)) - - return data - -def write_rules(prof_data, depth): - data = write_alias(prof_data, depth) - data += write_list_vars(prof_data, depth) - data += write_includes(prof_data, depth) - data += write_rlimits(prof_data, depth) - data += write_capabilities(prof_data, depth) - data += write_netdomain(prof_data, depth) - data += write_links(prof_data, depth) - data += write_paths(prof_data, depth) - data += write_change_profile(prof_data, depth) - - return data - -def write_piece(profile_data, depth, name, nhat, write_flags): - pre = ' ' * depth - data = [] - wname = None - inhat = False - if name == nhat: - wname = name - else: - wname = name + '//' + nhat - name = nhat - inhat = True - data += ['begin header'] - data += write_header(profile_data[name], depth, wname, False, write_flags) - data +=['end header'] - data += write_rules(profile_data[name], depth+1) - - pre2 = ' ' * (depth+1) - # External hat declarations - for hat in list(filter(lambda x: x != name, sorted(profile_data.keys()))): - if profile_data[hat].get('declared', False): - data.append('%s^%s,' %(pre2, hat)) - - if not inhat: - # Embedded hats - for hat in list(filter(lambda x: x != name, sorted(profile_data.keys()))): - if not profile_data[hat]['external'] and not profile_data[hat]['declared']: - data.append('') - if profile_data[hat]['profile']: - data += list(map(str, write_header(profile_data[hat], depth+1, hat, True, write_flags))) - else: - data += list(map(str, write_header(profile_data[hat], depth+1, '^'+hat, True, write_flags))) - - data += list(map(str, write_rules(profile_data[hat], depth+2))) - - data.append('%s}' %pre2) - - data.append('%s}' %pre) - - # External hats - for hat in list(filter(lambda x: x != name, sorted(profile_data.keys()))): - if name == nhat and profile_data[hat].get('external', False): - data.append('') - data += list(map(lambda x: ' %s' %x, write_piece(profile_data, depth-1, name, nhat, write_flags))) - data.append(' }') - - return data - - -def serialize_profile(profile_data, name, options): - string = '' - include_metadata = False - include_flags = True - data= [] - - if options:# and type(options) == dict: - if options.get('METADATA', False): - include_metadata = True - if options.get('NO_FLAGS', False): - include_flags = False - - if include_metadata: - string = '# Last Modified: %s\n' %time.time() - - if (profile_data[name].get('repo', False) and profile_data[name]['repo']['url'] - and profile_data[name]['repo']['user'] and profile_data[name]['repo']['id']): - repo = profile_data[name]['repo'] - string += '# REPOSITORY: %s %s %s\n' %(repo['url'], repo['user'], repo['id']) - elif profile_data[name]['repo']['neversubmit']: - string += '# REPOSITORY: NEVERSUBMIT\n' - - if profile_data[name].get('initial_comment', False): - comment = profile_data[name]['initial_comment'] - comment.replace('\\n', '\n') - string += comment + '\n' - - prof_filename = get_profile_filename(name) - if filelist.get(prof_filename, False): - data += write_alias(filelist[prof_filename], 0) - data += write_list_vars(filelist[prof_filename], 0) - data += write_includes(filelist[prof_filename], 0) - - data += write_piece(profile_data, 0, name, name, include_flags) - - string += '\n'.join(data) - - return string+'\n'