Merge in Kshitij Gupta <kgupta8592@gmail.com>'s rewrite of the

logprof/genprof and related utilities in python. Because the branch that
was worked on was not based on the apparmor tree, not all of the history
can be maintained for files that are not newly created or entirely
rewritten in the branch.

(This merge also includes a subsequent commit to the branch
I was merging from which includes my missed bzr add of
utils/apparmor/translations.py)
This commit is contained in:
Steve Beattie 2014-02-12 15:54:00 -08:00
commit f989dd0132
46 changed files with 9574 additions and 37 deletions

3
utils/README.md Normal file
View file

@ -0,0 +1,3 @@
Known Bugs:
Will allow multiple letters in the () due to translation/unicode issues with regexing the key.
User input will probably bug out in a different locale.

40
utils/aa-audit Normal file
View file

@ -0,0 +1,40 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import traceback
import apparmor.tools
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_('Switch the given programs to audit mode'))
parser.add_argument('-d', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('-r', '--remove', action='store_true', help=_('remove audit mode'))
parser.add_argument('program', type=str, nargs='+', help=_('name of program'))
parser.add_argument('--trace', action='store_true', help=_('Show full trace'))
args = parser.parse_args()
try:
audit = apparmor.tools.aa_tools('audit', args)
audit.act()
except Exception as e:
if not args.trace:
print(e.value + "\n")
else:
traceback.print_exc()

View file

@ -2,17 +2,30 @@
=head1 NAME =head1 NAME
aa-audit - set a AppArmor security profile to I<audit> mode. aa-audit - set an AppArmor security profile to I<audit> mode.
=head1 SYNOPSIS =head1 SYNOPSIS
B<aa-audit I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...]> B<aa-audit I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...] [I<-d /path/to/profiles>] [I<-r>]>
=head1 OPTIONS
B<-d --dir /path/to/profiles>
Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d.
B<-r --remove>
Removes the audit mode for the profile.
=head1 DESCRIPTION =head1 DESCRIPTION
B<aa-audit> is used to set the audit mode for one or more profiles to audit. B<aa-audit> is used to set one or more profiles to audit mode.
In this mode security policy is enforced and all access (successes and failures) are logged to the system log. In this mode security policy is enforced and all access (successes and failures) are logged to the system log.
The I<--remove> option can be used to remove the audit mode for the profile.
=head1 BUGS =head1 BUGS
If you find any bugs, please report them at If you find any bugs, please report them at

31
utils/aa-autodep Normal file
View file

@ -0,0 +1,31 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import apparmor.tools
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_('Generate a basic AppArmor profile by guessing requirements'))
parser.add_argument('--force', type=str, help=_('overwrite existing profile'))
parser.add_argument('-d', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('program', type=str, nargs='+', help=_('name of program'))
args = parser.parse_args()
autodep = apparmor.tools.aa_tools('autodep', args)
autodep.act()

View file

@ -26,7 +26,18 @@ aa-autodep - guess basic AppArmor profile requirements
=head1 SYNOPSIS =head1 SYNOPSIS
B<aa-autodep I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...]> B<aa-autodep I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...] [I<-d /path/to/profiles>] [I<-f>]>
=head1 OPTIONS
B<-d --dir /path/to/profiles>
Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d.
B<-f --force>
Overwrites any existing AppArmor profile for the executable with the generated minimal AppArmor profile.
=head1 DESCRIPTION =head1 DESCRIPTION
@ -37,6 +48,9 @@ a base profile containing a base include directive which includes basic
profile entries needed by most programs. The profile is generated by profile entries needed by most programs. The profile is generated by
recursively calling ldd(1) on the executables listed on the command line. recursively calling ldd(1) on the executables listed on the command line.
The I<--force> option will overwrite any existing profile for the executable with
the newly generated minimal AppArmor profile.
=head1 BUGS =head1 BUGS
This program does not perform full static analysis of executables, so This program does not perform full static analysis of executables, so

31
utils/aa-cleanprof Normal file
View file

@ -0,0 +1,31 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import apparmor.tools
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_('Cleanup the profiles for the given programs'))
parser.add_argument('-d', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('program', type=str, nargs='+', help=_('name of program'))
parser.add_argument('-s', '--silent', action='store_true', help=_('Silently overwrite with a clean profile'))
args = parser.parse_args()
clean = apparmor.tools.aa_tools('cleanprof', args)
clean.act()

39
utils/aa-cleanprof.pod Normal file
View file

@ -0,0 +1,39 @@
=pod
=head1 NAME
aa-cleanprof - clean an existing AppArmor security profile.
=head1 SYNOPSIS
B<aa-cleanprof I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...] [I<-d /path/to/profiles>] [I<-s>]>
=head1 OPTIONS
B<-d --dir /path/to/profiles>
Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d.
B<-s --silent>
Silently overwrites the profile without user prompt.
=head1 DESCRIPTION
B<aa-cleanprof> is used to perform a cleanup on one or more profiles.
The tool removes any existing superfluous rules (rules that are covered
under an include or another rule), reorders the rules to group similar rules
together and removes all comments from the file.
=head1 BUGS
If you find any bugs, please report them at
L<https://bugs.launchpad.net/apparmor/+filebug>.
=head1 SEE ALSO
apparmor(7), apparmor.d(5), aa-enforce(1), aa-complain(1), aa-disable(1),
aa_change_hat(2), and L<http://wiki.apparmor.net>.
=cut

31
utils/aa-complain Normal file
View file

@ -0,0 +1,31 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import apparmor.tools
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_('Switch the given program to complain mode'))
parser.add_argument('-d', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('-r', '--remove', action='store_true', help=_('remove complain mode'))
parser.add_argument('program', type=str, nargs='+', help=_('name of program'))
args = parser.parse_args()
complain = apparmor.tools.aa_tools('complain', args)
#print(args)
complain.act()

View file

@ -22,17 +22,31 @@
=head1 NAME =head1 NAME
aa-complain - set a AppArmor security profile to I<complain> mode. aa-complain - set an AppArmor security profile to I<complain> mode.
=head1 SYNOPSIS =head1 SYNOPSIS
B<aa-complain I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...]> B<aa-complain I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...] [I<-d /path/to/profiles>] [I<-r>]>
=head1 OPTIONS
B<-d --dir /path/to/profiles>
Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d.
B<-r --remove>
Removes the complain mode for the profile.
=head1 DESCRIPTION =head1 DESCRIPTION
B<aa-complain> is used to set the enforcement mode for one or more profiles to B<aa-complain> is used to set the enforcement mode for one or more profiles to I<complain> mode.
complain. In this mode security policy is not enforced but rather access In this mode security policy is not enforced but rather access violations
violations are logged to the system log. are logged to the system log.
The I<--remove> option can be used to remove the complain mode for the profile,
setting it to enforce mode by default.
=head1 BUGS =head1 BUGS

32
utils/aa-disable Normal file
View file

@ -0,0 +1,32 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import apparmor.tools
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_('Disable the profile for the given programs'))
parser.add_argument('-d', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('-r', '--revert', action='store_true', help=_('enable the profile for the given programs'))
parser.add_argument('program', type=str, nargs='+', help=_('name of program'))
args = parser.parse_args()
disable = apparmor.tools.aa_tools('disable', args)
disable.act()

View file

@ -26,15 +26,28 @@ aa-disable - disable an AppArmor security profile
=head1 SYNOPSIS =head1 SYNOPSIS
B<aa-disable I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...]> B<aa-disable I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...] [I<-d /path/to/profiles>] [I<-r>]>
=head1 OPTIONS
B<-d --dir /path/to/profiles>
Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d.
B<-r --revert>
Enables the profile and loads it.
=head1 DESCRIPTION =head1 DESCRIPTION
B<aa-disable> is used to disable the enforcement mode for one or more B<aa-disable> is used to I<disable> one or more profiles.
profiles. This command will unload the profile from the kernel and This command will unload the profile from the kernel and prevent the
prevent the profile from being loaded on AppArmor startup. The profile from being loaded on AppArmor startup.
I<aa-enforce> and I<aa-complain> utilities may be used to to change this The I<aa-enforce> and I<aa-complain> utilities may be used to to change
behavior. this behavior.
The I<--revert> option can be used to enable the profile.
=head1 BUGS =head1 BUGS

33
utils/aa-enforce Normal file
View file

@ -0,0 +1,33 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import apparmor.tools
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_('Switch the given program to enforce mode'))
parser.add_argument('-d', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('-r', '--remove', action='store_true', help=_('switch to complain mode'))
parser.add_argument('program', type=str, nargs='+', help=_('name of program'))
args = parser.parse_args()
# Flipping the remove flag since complain = !enforce
args.remove = not args.remove
enforce = apparmor.tools.aa_tools('complain', args)
enforce.act()

View file

@ -27,16 +27,30 @@ being disabled or I<complain> mode.
=head1 SYNOPSIS =head1 SYNOPSIS
B<aa-enforce I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...]> B<aa-enforce I<E<lt>executableE<gt>> [I<E<lt>executableE<gt>> ...] [I<-d /path/to/profiles>] [I<-r>]>
=head1 OPTIONS
B<-d --dir / path/to/profiles>
Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d.
B<-r --remove>
Removes the enforce mode for the profile.
=head1 DESCRIPTION =head1 DESCRIPTION
B<aa-enforce> is used to set the enforcement mode for one or more profiles B<aa-enforce> is used to set one or more profiles to I<enforce> mode.
to I<enforce>. This command is only relevant in conjunction with the This command is only relevant in conjunction with the I<aa-complain> utility
I<aa-complain> utility which sets a profile to complain mode and the which sets a profile to complain mode and the I<aa-disable> utility which
I<aa-disable> utility which unloads and disables a profile. The default unloads and disables a profile.
mode for a security policy is enforce and the I<aa-complain> utility must The default mode for a security policy is enforce and the I<aa-complain>
be run to change this behavior. utility must be run to change this behavior.
The I<--remove> option can be used to remove the enforce mode for the profile,
setting it to complain mode.
=head1 BUGS =head1 BUGS

164
utils/aa-genprof Normal file
View file

@ -0,0 +1,164 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import atexit
import os
import re
import subprocess
import sys
import apparmor.aa as apparmor
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
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', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('-f', '--file', 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.dir
filename = args.file
if filename:
if not os.path.isfile(filename):
raise apparmor.AppArmorException(_('The logfile %s does not exist. Please check the path') % filename)
else:
apparmor.filename = filename
aa_mountpoint = apparmor.check_for_apparmor()
if not aa_mountpoint:
raise apparmor.AppArmorException(_('It seems AppArmor was not 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(_("%s is not a directory.") %profiledir)
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\nis correct, please run 'which %s' as a user with correct PATH\nenvironment set up in order to find the fully-qualified path and\nuse the full path as parameter.") %(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, program)
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)

View file

@ -26,7 +26,7 @@ aa-genprof - profile generation utility for AppArmor
=head1 SYNOPSIS =head1 SYNOPSIS
B<aa-genprof I<E<lt>executableE<gt>> [I<-d /path/to/profiles>]> B<aa-genprof I<E<lt>executableE<gt>> [I<-d /path/to/profiles>] [I<-f /path/to/logfile>]>
=head1 OPTIONS =head1 OPTIONS
@ -35,6 +35,14 @@ B<-d --dir /path/to/profiles>
Specifies where to look for the AppArmor security profile set. Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d. Defaults to /etc/apparmor.d.
B<-f --file /path/to/logfile>
Specifies the location of logfile.
Default locations are read from F</etc/apparmor/logprof.conf>.
Typical defaults are:
/var/log/audit/audit.log
/var/log/syslog
/var/log/messages
=head1 DESCRIPTION =head1 DESCRIPTION
@ -64,7 +72,7 @@ using aa-logprof(1).
After the user finishes selecting profile entries based on violations After the user finishes selecting profile entries based on violations
that were detected during the program execution, aa-genprof will reload that were detected during the program execution, aa-genprof will reload
the updated profiles in complain mode and again prompt the user for (S)can and the updated profiles in complain mode and again prompt the user for (S)can and
(D)one. This cycle can then be repeated as necessary until all application (F)inish. This cycle can then be repeated as necessary until all application
functionality has been exercised without generating access violations. functionality has been exercised without generating access violations.
When the user eventually hits (F)inish, aa-genprof will set the main profile, When the user eventually hits (F)inish, aa-genprof will set the main profile,

53
utils/aa-logprof Normal file
View file

@ -0,0 +1,53 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import os
import apparmor.aa as apparmor
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_('Process log entries to generate profiles'))
parser.add_argument('-d', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('-f', '--file', type=str, help=_('path to logfile'))
parser.add_argument('-m', '--mark', type=str, help=_('mark in the log to start processing after'))
args = parser.parse_args()
profiledir = args.dir
filename = args.file
logmark = args.mark or ''
if filename:
if not os.path.isfile(filename):
raise apparmor.AppArmorException(_('The logfile %s does not exist. Please check the path') % filename)
else:
apparmor.filename = filename
aa_mountpoint = apparmor.check_for_apparmor()
if not aa_mountpoint:
raise apparmor.AppArmorException(_('It seems AppArmor was not 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("%s is not a directory."%profiledir)
apparmor.loadincludes()
apparmor.do_logprof_pass(logmark)

View file

@ -22,7 +22,7 @@
=head1 NAME =head1 NAME
aa-logprof - utility program for managing AppArmor security profiles aa-logprof - utility for updating AppArmor security profiles
=head1 SYNOPSIS =head1 SYNOPSIS
@ -32,12 +32,17 @@ B<aa-logprof [I<-d /path/to/profiles>] [I<-f /path/to/logfile>] [I<-m E<lt>mark
B<-d --dir /path/to/profiles> B<-d --dir /path/to/profiles>
The path to where the AppArmor security profiles are stored Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d.
B<-f --file /path/to/logfile> B<-f --file /path/to/logfile>
The path to the location of the logfile that contains AppArmor Specifies the location of logfile that contains AppArmor security events.
security events. Default locations are read from F</etc/apparmor/logprof.conf>.
Typical defaults are:
/var/log/audit/audit.log
/var/log/syslog
/var/log/messages
B< -m --logmark "mark"> B< -m --logmark "mark">
@ -47,9 +52,8 @@ B< -m --logmark "mark">
=head1 DESCRIPTION =head1 DESCRIPTION
B<aa-logprof> is an interactive tool used to review AppArmor's B<aa-logprof> is an interactive tool used to review AppArmor generated
complain mode output and generate new entries for AppArmor security messages and update AppArmor security profiles.
profiles.
Running aa-logprof will scan the log file and if there are new AppArmor Running aa-logprof will scan the log file and if there are new AppArmor
events that are not covered by the existing profile set, the user will events that are not covered by the existing profile set, the user will
@ -71,11 +75,17 @@ The user is then presented with info about the access including profile,
path, old mode if there was a previous entry in the profile for this path, path, old mode if there was a previous entry in the profile for this path,
new mode, the suggestion list, and given these options: new mode, the suggestion list, and given these options:
(A)llow, (D)eny, (N)ew, (G)lob last piece, (Q)uit (A)llow, (D)eny, (I)gnore, (N)ew, (G)lob last piece, (Q)uit
If the AppArmor profile was in complain mode when the event was generated, If the AppArmor profile was in complain mode when the event was generated,
the default for this option is (A)llow, otherwise, it's (D)eny. the default for this option is (A)llow, otherwise, it's (D)eny.
The (D)eny option adds a "deny" rule to the AppArmor profile, which
silences logging.
The (I)gnore option allows user to ignore the event, without making any
changes to the AppArmor profile.
The suggestion list is presented as a numbered list with includes The suggestion list is presented as a numbered list with includes
at the top, the literal path in the middle, and the suggested globs at the top, the literal path in the middle, and the suggested globs
at the bottom. If any globs are being suggested, the shortest glob at the bottom. If any globs are being suggested, the shortest glob
@ -109,9 +119,9 @@ Adding r access to /usr/share/themes/** would delete an entry for r
access to /usr/share/themes/foo/*.gif if it exists in the profile. access to /usr/share/themes/foo/*.gif if it exists in the profile.
If (Q)uit is selected at this point, aa-logprof will ignore all new pending If (Q)uit is selected at this point, aa-logprof will ignore all new pending
capability and path accesses. accesses.
After all of the path accesses have been handled, logrof will write all After all of the accesses have been handled, logrof will write all
updated profiles to the disk and reload them if AppArmor is running. updated profiles to the disk and reload them if AppArmor is running.
=head2 New Process (Execution) Events =head2 New Process (Execution) Events

682
utils/aa-mergeprof Normal file
View file

@ -0,0 +1,682 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import sys
import apparmor.aa
import apparmor.aamode
import apparmor.severity
import apparmor.cleanprofile as cleanprofile
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_('Perform a 3way merge on the given profiles'))
parser.add_argument('mine', type=str, help=_('your profile'))
parser.add_argument('base', type=str, help=_('base profile'))
parser.add_argument('other', type=str, help=_('other profile'))
parser.add_argument('-d', '--dir', type=str, help=_('path to profiles'))
parser.add_argument('-a', '--auto', action='store_true', help=_('Automatically merge profiles, exits incase of *x conflicts'))
args = parser.parse_args()
profiles = [args.mine, args.base, args.other]
def main():
mergeprofiles = Merge(profiles)
#Get rid of common/superfluous stuff
mergeprofiles.clear_common()
if not args.auto:
mergeprofiles.ask_the_questions('other')
mergeprofiles.clear_common()
mergeprofiles.ask_the_questions('base')
q = apparmor.aa.hasher()
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_ABORT']
q['default'] = 'CMD_VIEW_CHANGES'
q['options'] = []
q['selected'] = 0
p =None
ans = ''
arg = None
programs = list(mergeprofiles.user.aa.keys())
program = programs[0]
while ans != 'CMD_SAVE_CHANGES':
ans, arg = apparmor.aa.UI_PromptUser(q)
if ans == 'CMD_SAVE_CHANGES':
apparmor.aa.write_profile_ui_feedback(program)
apparmor.aa.reload_base(program)
elif ans == 'CMD_VIEW_CHANGES':
for program in programs:
apparmor.aa.original_aa[program] = apparmor.aa.deepcopy(apparmor.aa.aa[program])
#oldprofile = apparmor.serialize_profile(apparmor.original_aa[program], program, '')
newprofile = apparmor.aa.serialize_profile(mergeprofiles.user.aa[program], program, '')
apparmor.aa.display_changes_with_comments(mergeprofiles.user.filename, newprofile)
class Merge(object):
def __init__(self, profiles):
user, base, other = profiles
#Read and parse base profile and save profile data, include data from it and reset them
apparmor.aa.read_profile(base, True)
self.base = cleanprofile.Prof(base)
self.reset()
#Read and parse other profile and save profile data, include data from it and reset them
apparmor.aa.read_profile(other, True)
self.other = cleanprofile.Prof(other)
self.reset()
#Read and parse user profile
apparmor.aa.read_profile(profiles[0], True)
self.user = cleanprofile.Prof(user)
def reset(self):
apparmor.aa.aa = apparmor.aa.hasher()
apparmor.aa.filelist = apparmor.aa.hasher()
apparmor.aa.include = dict()
apparmor.aa.existing_profiles = apparmor.aa.hasher()
apparmor.aa.original_aa = apparmor.aa.hasher()
def clear_common(self):
deleted = 0
#Remove off the parts in other profile which are common/superfluous from user profile
user_other = cleanprofile.CleanProf(False, self.user, self.other)
deleted += user_other.compare_profiles()
#Remove off the parts in base profile which are common/superfluous from user profile
user_base = cleanprofile.CleanProf(False, self.user, self.base)
deleted += user_base.compare_profiles()
#Remove off the parts in other profile which are common/superfluous from base profile
base_other = cleanprofile.CleanProf(False, self.base, self.other)
deleted += user_base.compare_profiles()
def conflict_mode(self, profile, hat, allow, path, mode, new_mode, old_mode):
m = new_mode
o = old_mode
new_mode = apparmor.aa.flatten_mode(new_mode)
old_mode = apparmor.aa.flatten_mode(old_mode)
conflict_modes = set('uUpPcCiIxX')
conflict_x= (old_mode | new_mode) & conflict_modes
if conflict_x:
#We may have conflicting x modes
if conflict_x & set('x'):
conflict_x.remove('x')
if conflict_x & set('X'):
conflict_x.remove('X')
if len(conflict_x) > 1:
q = apparmor.aa.hasher()
q['headers'] = [_('Path'), path]
q['headers'] += [_('Select the appropriate mode'), '']
options = []
options.append('%s: %s' %(mode, apparmor.aa.mode_to_str_user(new_mode)))# - (old_mode & conflict_x))))
options.append('%s: %s' %(mode, apparmor.aa.mode_to_str_user(old_mode)))#(old_mode | new_mode) - (new_mode & conflict_x))))
q['options'] = options
q['functions'] = ['CMD_ALLOW', 'CMD_ABORT']
done = False
while not done:
ans, selected = apparmor.aa.UI_PromptUser(q)
if ans == 'CMD_ALLOW':
if selected == 0:
self.user.aa[profile][hat][allow]['path'][path][mode] = m#apparmor.aa.owner_flatten_mode(new_mode)#(old_mode | new_mode) - (old_mode & conflict_x)
return m
elif selected == 1:
return o
pass#self.user.aa[profile][hat][allow][path][mode] = (old_mode | new_mode) - (new_mode & conflict_x)
else:
raise apparmor.aa.AppArmorException(_('Unknown selection'))
done = True
def ask_the_questions(self, other):
if other == 'other':
other = self.other
else:
other = self.base
#print(other.aa)
#Add the file-wide includes from the other profile to the user profile
done = False
options = list(map(lambda inc: '#include <%s>' %inc, sorted(other.filelist[other.filename]['include'].keys())))
q = apparmor.aa.hasher()
q['options'] = options
default_option = 1
q['selected'] = default_option - 1
q['headers'] = [_('File includes'), _('Select the ones you wish to add')]
q['functions'] = ['CMD_ALLOW', 'CMD_IGNORE_ENTRY', 'CMD_ABORT', 'CMD_FINISHED']
q['default'] = 'CMD_ALLOW'
while not done and options:
ans, selected = apparmor.aa.UI_PromptUser(q)
if ans == 'CMD_IGNORE_ENTRY':
done = True
elif ans == 'CMD_ALLOW':
selection = options[selected]
inc = apparmor.aa.re_match_include(selection)
self.user.filelist[self.user.filename]['include'][inc] = True
options.pop(selected)
apparmor.aa.UI_Info(_('Adding %s to the file.') % selection)
sev_db = apparmor.aa.sev_db
if not sev_db:
sev_db = apparmor.severity.Severity(apparmor.aa.CONFDIR + '/severity.db', _('unknown'))
for profile in sorted(other.aa.keys()):
for hat in sorted(other.aa[profile].keys()):
#Add the includes from the other profile to the user profile
done = False
options = list(map(lambda inc: '#include <%s>' %inc, sorted(other.aa[profile][hat]['include'].keys())))
q = apparmor.aa.hasher()
q['options'] = options
default_option = 1
q['selected'] = default_option - 1
q['headers'] = [_('File includes'), _('Select the ones you wish to add')]
q['functions'] = ['CMD_ALLOW', 'CMD_IGNORE_ENTRY', 'CMD_ABORT', 'CMD_FINISHED']
q['default'] = 'CMD_ALLOW'
while not done and options:
ans, selected = apparmor.aa.UI_PromptUser(q)
if ans == 'CMD_IGNORE_ENTRY':
done = True
elif ans == 'CMD_ALLOW':
selection = options[selected]
inc = apparmor.aa.re_match_include(selection)
deleted = apparmor.aa.delete_duplicates(self.user.aa[profile][hat], inc)
self.user.aa[profile][hat]['include'][inc] = True
options.pop(selected)
apparmor.aa.UI_Info(_('Adding %s to the file.') % selection)
if deleted:
apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted)
#Add the capabilities
for allow in ['allow', 'deny']:
if other.aa[profile][hat].get(allow, False):
continue
for capability in sorted(other.aa[profile][hat][allow]['capability'].keys()):
severity = sev_db.rank('CAP_%s' % capability)
default_option = 1
options = []
newincludes = apparmor.aa.match_cap_includes(self.user.aa[profile][hat], capability)
q = apparmor.aa.hasher()
if newincludes:
options += list(map(lambda inc: '#include <%s>' %inc, sorted(set(newincludes))))
if options:
options.append('capability %s' % capability)
q['options'] = [options]
q['selected'] = default_option - 1
q['headers'] = [_('Profile'), apparmor.aa.combine_name(profile, hat)]
q['headers'] += [_('Capability'), capability]
q['headers'] += [_('Severity'), severity]
audit_toggle = 0
q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_ABORT', 'CMD_FINISHED']
q['default'] = 'CMD_ALLOW'
done = False
while not done:
ans, selected = apparmor.aa.UI_PromptUser(q)
# Ignore the log entry
if ans == 'CMD_IGNORE_ENTRY':
done = True
break
if ans == 'CMD_ALLOW':
selection = ''
if options:
selection = options[selected]
match = apparmor.aa.re_match_include(selection)
if match:
deleted = False
inc = match
deleted = apparmor.aa.delete_duplicates(self.user.aa[profile][hat], inc)
self.user.aa[profile][hat]['include'][inc] = True
apparmor.aa.UI_Info(_('Adding %s to profile.') % selection)
if deleted:
apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted)
self.user.aa[profile][hat]['allow']['capability'][capability]['set'] = True
self.user.aa[profile][hat]['allow']['capability'][capability]['audit'] = other.aa[profile][hat]['allow']['capability'][capability]['audit']
apparmor.aa.changed[profile] = True
apparmor.aa.UI_Info(_('Adding capability %s to profile.'), capability)
done = True
elif ans == 'CMD_DENY':
self.user.aa[profile][hat]['deny']['capability'][capability]['set'] = True
apparmor.aa.changed[profile] = True
apparmor.aa.UI_Info(_('Denying capability %s to profile.') % capability)
done = True
else:
done = False
# Process all the path entries.
for allow in ['allow', 'deny']:
for path in sorted(other.aa[profile][hat][allow]['path'].keys()):
#print(path, other.aa[profile][hat][allow]['path'][path])
mode = other.aa[profile][hat][allow]['path'][path]['mode']
if self.user.aa[profile][hat][allow]['path'].get(path, False):
mode = self.conflict_mode(profile, hat, allow, path, 'mode', other.aa[profile][hat][allow]['path'][path]['mode'], self.user.aa[profile][hat][allow]['path'][path]['mode'])
self.conflict_mode(profile, hat, allow, path, 'audit', other.aa[profile][hat][allow]['path'][path]['audit'], self.user.aa[profile][hat][allow]['path'][path]['audit'])
apparmor.aa.changed[profile] = True
continue
# Lookup modes from profile
allow_mode = set()
allow_audit = set()
deny_mode = set()
deny_audit = set()
fmode, famode, fm = apparmor.aa.rematchfrag(self.user.aa[profile][hat], 'allow', path)
if fmode:
allow_mode |= fmode
if famode:
allow_audit |= famode
cm, cam, m = apparmor.aa.rematchfrag(self.user.aa[profile][hat], 'deny', path)
if cm:
deny_mode |= cm
if cam:
deny_audit |= cam
imode, iamode, im = apparmor.aa.match_prof_incs_to_path(self.user.aa[profile][hat], 'allow', path)
if imode:
allow_mode |= imode
if iamode:
allow_audit |= iamode
cm, cam, m = apparmor.aa.match_prof_incs_to_path(self.user.aa[profile][hat], 'deny', path)
if cm:
deny_mode |= cm
if cam:
deny_audit |= cam
if deny_mode & apparmor.aa.AA_MAY_EXEC:
deny_mode |= apparmor.aamode.ALL_AA_EXEC_TYPE
# Mask off the denied modes
mode = mode - deny_mode
# If we get an exec request from some kindof event that generates 'PERMITTING X'
# check if its already in allow_mode
# if not add ix permission
if mode & apparmor.aa.AA_MAY_EXEC:
# Remove all type access permission
mode = mode - apparmor.aamode.ALL_AA_EXEC_TYPE
if not allow_mode & apparmor.aa.AA_MAY_EXEC:
mode |= apparmor.aa.str_to_mode('ix')
# m is not implied by ix
### If we get an mmap request, check if we already have it in allow_mode
##if mode & AA_EXEC_MMAP:
## # ix implies m, so we don't need to add m if ix is present
## if contains(allow_mode, 'ix'):
## mode = mode - AA_EXEC_MMAP
if not mode:
continue
matches = []
if fmode:
matches += fm
if imode:
matches += im
if not apparmor.aa.mode_contains(allow_mode, mode):
default_option = 1
options = []
newincludes = []
include_valid = False
for incname in apparmor.aa.include.keys():
include_valid = False
# If already present skip
if self.user.aa[profile][hat][incname]:
continue
if incname.startswith(apparmor.aa.profile_dir):
incname = incname.replace(apparmor.aa.profile_dir+'/', '', 1)
include_valid = apparmor.aa.valid_include('', incname)
if not include_valid:
continue
cm, am, m = apparmor.aa.match_include_to_path(incname, 'allow', path)
if cm and apparmor.aa.mode_contains(cm, mode):
dm = apparmor.aa.match_include_to_path(incname, 'deny', path)[0]
# If the mode is denied
if not mode & dm:
if not list(filter(lambda s: '/**' == s, m)):
newincludes.append(incname)
# Add new includes to the options
if newincludes:
options += list(map(lambda s: '#include <%s>' % s, sorted(set(newincludes))))
# We should have literal the path in options list too
options.append(path)
# Add any the globs matching path from logprof
globs = apparmor.aa.glob_common(path)
if globs:
matches += globs
# Add any user entered matching globs
for user_glob in apparmor.aa.user_globs:
if apparmor.aa.matchliteral(user_glob, path):
matches.append(user_glob)
matches = list(set(matches))
if path in matches:
matches.remove(path)
options += apparmor.aa.order_globs(matches, path)
default_option = len(options)
sev_db.unload_variables()
sev_db.load_variables(apparmor.aa.get_profile_filename(profile))
severity = sev_db.rank(path, apparmor.aa.mode_to_str(mode))
sev_db.unload_variables()
audit_toggle = 0
owner_toggle = 0
if apparmor.aa.cfg['settings']['default_owner_prompt']:
owner_toggle = apparmor.aa.cfg['settings']['default_owner_prompt']
done = False
while not done:
q = apparmor.aa.hasher()
q['headers'] = [_('Profile'), apparmor.aa.combine_name(profile, hat),
_('Path'), path]
if allow_mode:
mode |= allow_mode
tail = ''
s = ''
prompt_mode = None
if owner_toggle == 0:
prompt_mode = apparmor.aa.flatten_mode(mode)
tail = ' ' + _('(owner permissions off)')
elif owner_toggle == 1:
prompt_mode = mode
elif owner_toggle == 2:
prompt_mode = allow_mode | apparmor.aa.owner_flatten_mode(mode - allow_mode)
tail = ' ' + _('(force new perms to owner)')
else:
prompt_mode = apparmor.aa.owner_flatten_mode(mode)
tail = ' ' + _('(force all rule perms to owner)')
if audit_toggle == 1:
s = apparmor.aa.mode_to_str_user(allow_mode)
if allow_mode:
s += ', '
s += 'audit ' + apparmor.aa.mode_to_str_user(prompt_mode - allow_mode) + tail
elif audit_toggle == 2:
s = 'audit ' + apparmor.aa.mode_to_str_user(prompt_mode) + tail
else:
s = apparmor.aa.mode_to_str_user(prompt_mode) + tail
q['headers'] += [_('Old Mode'), apparmor.aa.mode_to_str_user(allow_mode),
_('New Mode'), s]
else:
s = ''
tail = ''
prompt_mode = None
if audit_toggle:
s = 'audit'
if owner_toggle == 0:
prompt_mode = apparmor.aa.flatten_mode(mode)
tail = ' ' + _('(owner permissions off)')
elif owner_toggle == 1:
prompt_mode = mode
else:
prompt_mode = apparmor.aa.owner_flatten_mode(mode)
tail = ' ' + _('(force perms to owner)')
s = apparmor.aa.mode_to_str_user(prompt_mode)
q['headers'] += [_('Mode'), s]
q['headers'] += [_('Severity'), severity]
q['options'] = options
q['selected'] = default_option - 1
q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_GLOB',
'CMD_GLOBEXT', 'CMD_NEW', 'CMD_ABORT',
'CMD_FINISHED', 'CMD_OTHER']
q['default'] = 'CMD_ALLOW'
ans, selected = apparmor.aa.UI_PromptUser(q)
if ans == 'CMD_IGNORE_ENTRY':
done = True
break
if ans == 'CMD_OTHER':
audit_toggle, owner_toggle = apparmor.aa.UI_ask_mode_toggles(audit_toggle, owner_toggle, allow_mode)
elif ans == 'CMD_USER_TOGGLE':
owner_toggle += 1
if not allow_mode and owner_toggle == 2:
owner_toggle += 1
if owner_toggle > 3:
owner_toggle = 0
elif ans == 'CMD_ALLOW':
path = options[selected]
done = True
match = apparmor.aa.re_match_include(path)
if match:
inc = match
deleted = 0
deleted = apparmor.aa.delete_duplicates(aa[profile][hat], inc)
self.user.aa[profile][hat]['include'][inc] = True
apparmor.aa.changed[profile] = True
apparmor.aa.UI_Info(_('Adding %s to profile.') % path)
if deleted:
apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted)
else:
if self.user.aa[profile][hat]['allow']['path'][path].get('mode', False):
mode |= self.user.aa[profile][hat]['allow']['path'][path]['mode']
deleted = []
for entry in self.user.aa[profile][hat]['allow']['path'].keys():
if path == entry:
continue
if apparmor.aa.matchregexp(path, entry):
if apparmor.aa.mode_contains(mode, self.user.aa[profile][hat]['allow']['path'][entry]['mode']):
deleted.append(entry)
for entry in deleted:
self.user.aa[profile][hat]['allow']['path'].pop(entry)
deleted = len(deleted)
if owner_toggle == 0:
mode = apparmor.aa.flatten_mode(mode)
#elif owner_toggle == 1:
# mode = mode
elif owner_toggle == 2:
mode = allow_mode | apparmor.aa.owner_flatten_mode(mode - allow_mode)
elif owner_toggle == 3:
mode = apparmor.aa.owner_flatten_mode(mode)
if not self.user.aa[profile][hat]['allow'].get(path, False):
self.user.aa[profile][hat]['allow']['path'][path]['mode'] = self.user.aa[profile][hat]['allow']['path'][path].get('mode', set()) | mode
tmpmode = set()
if audit_toggle == 1:
tmpmode = mode- allow_mode
elif audit_toggle == 2:
tmpmode = mode
self.user.aa[profile][hat]['allow']['path'][path]['audit'] = self.user.aa[profile][hat]['allow']['path'][path].get('audit', set()) | tmpmode
apparmor.aa.changed[profile] = True
apparmor.aa.UI_Info(_('Adding %s %s to profile') % (path, apparmor.aa.mode_to_str_user(mode)))
if deleted:
apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted)
elif ans == 'CMD_DENY':
path = options[selected].strip()
# Add new entry?
self.user.aa[profile][hat]['deny']['path'][path]['mode'] = self.user.aa[profile][hat]['deny']['path'][path].get('mode', set()) | (mode - allow_mode)
self.user.aa[profile][hat]['deny']['path'][path]['audit'] = self.user.aa[profile][hat]['deny']['path'][path].get('audit', set())
apparmor.aa.changed[profile] = True
done = True
elif ans == 'CMD_NEW':
arg = options[selected]
if not apparmor.aa.re_match_include(arg):
ans = apparmor.aa.UI_GetString(_('Enter new path: '), arg)
# if ans:
# if not matchliteral(ans, path):
# ynprompt = _('The specified path does not match this log entry:\n\n Log Entry: %s\n Entered Path: %s\nDo you really want to use this path?') % (path,ans)
# key = apparmor.aa.UI_YesNo(ynprompt, 'n')
# if key == 'n':
# continue
apparmor.aa.user_globs.append(ans)
options.append(ans)
default_option = len(options)
elif ans == 'CMD_GLOB':
newpath = options[selected].strip()
if not apparmor.aa.re_match_include(newpath):
newpath = apparmor.aa.glob_path(newpath)
if newpath not in options:
options.append(newpath)
default_option = len(options)
else:
default_option = options.index(newpath) + 1
elif ans == 'CMD_GLOBEXT':
newpath = options[selected].strip()
if not apparmor.aa.re_match_include(newpath):
newpath = apparmor.aa.glob_path_withext(newpath)
if newpath not in options:
options.append(newpath)
default_option = len(options)
else:
default_option = options.index(newpath) + 1
elif re.search('\d', ans):
default_option = ans
#
for allow in ['allow', 'deny']:
for family in sorted(other.aa[profile][hat][allow]['netdomain']['rule'].keys()):
# severity handling for net toggles goes here
for sock_type in sorted(other.aa[profile][hat][allow]['netdomain']['rule'][family].keys()):
if apparmor.aa.profile_known_network(self.user.aa[profile][hat], family, sock_type):
continue
default_option = 1
options = []
newincludes = apparmor.aa.match_net_includes(self.user.aa[profile][hat], family, sock_type)
q = apparmor.aa.hasher()
if newincludes:
options += list(map(lambda s: '#include <%s>'%s, sorted(set(newincludes))))
if True:#options:
options.append('network %s %s' % (family, sock_type))
q['options'] = options
q['selected'] = default_option - 1
q['headers'] = [_('Profile'), apparmor.aa.combine_name(profile, hat)]
q['headers'] += [_('Network Family'), family]
q['headers'] += [_('Socket Type'), sock_type]
audit_toggle = 0
q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_IGNORE_ENTRY', 'CMD_AUDIT_NEW',
'CMD_ABORT', 'CMD_FINISHED']
q['default'] = 'CMD_ALLOW'
done = False
while not done:
ans, selected = apparmor.aa.UI_PromptUser(q)
if ans == 'CMD_IGNORE_ENTRY':
done = True
break
if ans.startswith('CMD_AUDIT'):
audit_toggle = not audit_toggle
audit = ''
if audit_toggle:
audit = 'audit'
q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_AUDIT_OFF',
'CMD_ABORT', 'CMD_FINISHED']
else:
q['functions'] = ['CMD_ALLOW', 'CMD_DENY', 'CMD_AUDIT_NEW',
'CMD_ABORT', 'CMD_FINISHED']
q['headers'] = [_('Profile'), apparmor.aa.combine_name(profile, hat)]
q['headers'] += [_('Network Family'), audit + family]
q['headers'] += [_('Socket Type'), sock_type]
elif ans == 'CMD_ALLOW':
#print(options, selected)
selection = options[selected]
done = True
if apparmor.aa.re_match_include(selection): #re.search('#include\s+<.+>$', selection):
inc = apparmor.aa.re_match_include(selection) #re.search('#include\s+<(.+)>$', selection).groups()[0]
deleted = 0
deleted = apparmor.aa.delete_duplicates(self.user.aa[profile][hat], inc)
self.user.aa[profile][hat]['include'][inc] = True
apparmor.aa.changed[profile] = True
apparmor.aa.UI_Info(_('Adding %s to profile') % selection)
if deleted:
apparmor.aa.UI_Info(_('Deleted %s previous matching profile entries.') % deleted)
else:
self.user.aa[profile][hat]['allow']['netdomain']['audit'][family][sock_type] = audit_toggle
self.user.aa[profile][hat]['allow']['netdomain']['rule'][family][sock_type] = True
apparmor.aa.changed[profile] = True
apparmor.aa.UI_Info(_('Adding network access %s %s to profile.') % (family, sock_type))
elif ans == 'CMD_DENY':
done = True
self.user.aa[profile][hat]['deny']['netdomain']['rule'][family][sock_type] = True
apparmor.aa.changed[profile] = True
apparmor.aa.UI_Info(_('Denying network access %s %s to profile') % (family, sock_type))
else:
done = False
if __name__ == '__main__':
main()

33
utils/aa-mergeprof.pod Normal file
View file

@ -0,0 +1,33 @@
=pod
=head1 NAME
aa-mergeprof - merge AppArmor security profiles.
=head1 SYNOPSIS
B<aa-mergeprof I<E<lt>mineE<gt>> I<E<lt>userE<gt>> I<E<lt>otherE<gt>> [I<-d /path/to/profiles>]>
=head1 OPTIONS
B<-d --dir /path/to/profiles>
Specifies where to look for the AppArmor security profile set.
Defaults to /etc/apparmor.d.
=head1 DESCRIPTION
B<aa-mergeprof>
=head1 BUGS
If you find any bugs, please report them at
L<https://bugs.launchpad.net/apparmor/+filebug>.
=head1 SEE ALSO
apparmor(7), apparmor.d(5), aa_change_hat(2), aa-genprof(1),
aa-logprof(1), aa-enforce(1), aa-audit(1), aa-complain(1),
aa-disable(1), and L<http://wiki.apparmor.net>.
=cut

92
utils/aa-unconfined Normal file
View file

@ -0,0 +1,92 @@
#!/usr/bin/python
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import argparse
import os
import re
import sys
import apparmor.aa as apparmor
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
parser = argparse.ArgumentParser(description=_("Lists unconfined processes having tcp or udp ports"))
parser.add_argument("--paranoid", action="store_true", help=_("scan all processes from /proc"))
args = parser.parse_args()
paranoid = args.paranoid
aa_mountpoint = apparmor.check_for_apparmor()
if not aa_mountpoint:
raise apparmor.AppArmorException(_("It seems AppArmor was not started. Please enable AppArmor and try again."))
pids = []
if paranoid:
pids = list(filter(lambda x: re.search(r"^\d+$", x), apparmor.get_subdirectories("/proc")))
else:
regex_tcp_udp = re.compile(r"^(tcp|udp)\s+\d+\s+\d+\s+\S+\:(\d+)\s+\S+\:(\*|\d+)\s+(LISTEN|\s+)\s+(\d+)\/(\S+)")
import subprocess
if sys.version_info < (3, 0):
output = subprocess.check_output("LANG=C netstat -nlp", shell=True).split("\n")
else:
#Python3 needs to translate a stream of bytes to string with specified encoding
output = str(subprocess.check_output("LANG=C netstat -nlp", shell=True), encoding='utf8').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(int, set(pids)))
for pid in sorted(pids):
try:
prog = os.readlink("/proc/%s/exe"%pid)
except OSError:
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 = ""
regex_interpreter = re.compile(r"^(/usr)?/bin/(python|perl|bash|dash|sh)$")
if not attr:
if regex_interpreter.search(prog):
cmdline = re.sub(r"\x00", " ", cmdline)
cmdline = re.sub(r"\s+$", "", cmdline).strip()
apparmor.UI_Info(_("%s %s (%s) not confined\n")%(pid, prog, cmdline))
else:
if pname and pname[-1] == ')':
pname += ' '
apparmor.UI_Info(_("%s %s %snot confined\n")%(pid, prog, pname))
else:
if regex_interpreter.search(prog):
cmdline = re.sub(r"\0", " ", cmdline)
cmdline = re.sub(r"\s+$", "", cmdline).strip()
apparmor.UI_Info(_("%s %s (%s) confined by '%s'\n")%(pid, prog, cmdline, attr))
else:
if pname and pname[-1] == ')':
pname += ' '
apparmor.UI_Info(_("%s %s %sconfined by '%s'\n")%(pid, prog, pname, attr))

View file

@ -27,7 +27,14 @@ not have AppArmor profiles loaded
=head1 SYNOPSIS =head1 SYNOPSIS
B<aa-unconfined> B<aa-unconfined [I<--paranoid>]>
=head1 OPTIONS
B<--paranoid>
Displays all processes from F</proc> filesystem with tcp or udp ports
that do not have AppArmor profiles loaded.
=head1 DESCRIPTION =head1 DESCRIPTION
@ -39,7 +46,7 @@ network sockets and do not have AppArmor profiles loaded into the kernel.
B<aa-unconfined> must be run as root to retrieve the process executable B<aa-unconfined> must be run as root to retrieve the process executable
link from the F</proc> filesystem. This program is susceptible to race link from the F</proc> filesystem. This program is susceptible to race
conditions of several flavours: an unlinked executable will be mishandled; conditions of several flavours: an unlinked executable will be mishandled;
an executable started before a AppArmor profile is loaded will not an executable started before an AppArmor profile is loaded will not
appear in the output, despite running without confinement; a process that dies appear in the output, despite running without confinement; a process that dies
between the netstat(8) and further checks will be mishandled. This between the netstat(8) and further checks will be mishandled. This
program only lists processes using TCP and UDP. In short, this program only lists processes using TCP and UDP. In short, this

4281
utils/apparmor/aa.py Normal file

File diff suppressed because it is too large Load diff

280
utils/apparmor/aamode.py Normal file
View file

@ -0,0 +1,280 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import re
def AA_OTHER(mode):
other = set()
for i in mode:
other.add('::%s' % i)
return other
def AA_OTHER_REMOVE(mode):
other = set()
for i in mode:
if '::' in i:
other.add(i[2:])
return other
AA_MAY_EXEC = set('x')
AA_MAY_WRITE = set('w')
AA_MAY_READ = set('r')
AA_MAY_APPEND = set('a')
AA_MAY_LINK = set('l')
AA_MAY_LOCK = set('k')
AA_EXEC_MMAP = set('m')
AA_EXEC_UNSAFE = set(['execunsafe'])
AA_EXEC_INHERIT = set('i')
AA_EXEC_UNCONFINED = set('U')
AA_EXEC_PROFILE = set('P')
AA_EXEC_CHILD = set('C')
AA_EXEC_NT = set('N')
AA_LINK_SUBSET = set(['linksubset'])
#AA_OTHER_SHIFT = 14
#AA_USER_MASK = 16384 - 1
AA_EXEC_TYPE = (AA_MAY_EXEC | AA_EXEC_UNSAFE | AA_EXEC_INHERIT |
AA_EXEC_UNCONFINED | AA_EXEC_PROFILE | AA_EXEC_CHILD | AA_EXEC_NT)
ALL_AA_EXEC_TYPE = AA_EXEC_TYPE
MODE_HASH = {'x': AA_MAY_EXEC, 'X': AA_MAY_EXEC,
'w': AA_MAY_WRITE, 'W': AA_MAY_WRITE,
'r': AA_MAY_READ, 'R': AA_MAY_READ,
'a': AA_MAY_APPEND, 'A': AA_MAY_APPEND,
'l': AA_MAY_LINK, 'L': AA_MAY_LINK,
'k': AA_MAY_LOCK, 'K': AA_MAY_LOCK,
'm': AA_EXEC_MMAP, 'M': AA_EXEC_MMAP,
'i': AA_EXEC_INHERIT, 'I': AA_EXEC_INHERIT,
'u': AA_EXEC_UNCONFINED | AA_EXEC_UNSAFE, # Unconfined + Unsafe
'U': AA_EXEC_UNCONFINED,
'p': AA_EXEC_PROFILE | AA_EXEC_UNSAFE, # Profile + unsafe
'P': AA_EXEC_PROFILE,
'c': AA_EXEC_CHILD | AA_EXEC_UNSAFE, # Child + Unsafe
'C': AA_EXEC_CHILD,
'n': AA_EXEC_NT | AA_EXEC_UNSAFE,
'N': AA_EXEC_NT
}
LOG_MODE_RE = re.compile('(r|w|l|m|k|a|x|ix|ux|px|cx|nx|pix|cix|Ix|Ux|Px|PUx|Cx|Nx|Pix|Cix)')
MODE_MAP_RE = re.compile('(r|w|l|m|k|a|x|i|u|p|c|n|I|U|P|C|N)')
def str_to_mode(string):
if not string:
return set()
user, other = split_log_mode(string)
if not user:
user = other
mode = sub_str_to_mode(user)
#print(string, mode)
#print(string, 'other', sub_str_to_mode(other))
mode |= (AA_OTHER(sub_str_to_mode(other)))
#print (string, mode)
#print('str_to_mode:', mode)
return mode
def sub_str_to_mode(string):
mode = set()
if not string:
return mode
while string:
tmp = MODE_MAP_RE.search(string)
if tmp:
tmp = tmp.groups()[0]
string = MODE_MAP_RE.sub('', string, 1)
if tmp and MODE_HASH.get(tmp, False):
mode |= MODE_HASH[tmp]
else:
pass
return mode
def split_log_mode(mode):
user = ''
other = ''
match = re.search('(.*?)::(.*)', mode)
if match:
user, other = match.groups()
else:
user = mode
other = mode
#print ('split_logmode:', user, mode)
return user, other
def mode_contains(mode, subset):
# w implies a
if mode & AA_MAY_WRITE:
mode |= AA_MAY_APPEND
if mode & (AA_OTHER(AA_MAY_WRITE)):
mode |= (AA_OTHER(AA_MAY_APPEND))
return (mode & subset) == subset
def contains(mode, string):
return mode_contains(mode, str_to_mode(string))
def validate_log_mode(mode):
if LOG_MODE_RE.search(mode):
#if LOG_MODE_RE.search(mode):
return True
else:
return False
def hide_log_mode(mode):
mode = mode.replace('::', '')
return mode
def map_log_mode(mode):
return mode
def print_mode(mode):
user, other = split_mode(mode)
string = sub_mode_to_str(user) + '::' + sub_mode_to_str(other)
return string
def sub_mode_to_str(mode):
string = ''
# w(write) implies a(append)
if mode & AA_MAY_WRITE:
mode = mode - AA_MAY_APPEND
#string = ''.join(mode)
if mode & AA_EXEC_MMAP:
string += 'm'
if mode & AA_MAY_READ:
string += 'r'
if mode & AA_MAY_WRITE:
string += 'w'
if mode & AA_MAY_APPEND:
string += 'a'
if mode & AA_MAY_LINK:
string += 'l'
if mode & AA_MAY_LOCK:
string += 'k'
# modes P and C must appear before I and U else invalid syntax
if mode & (AA_EXEC_PROFILE | AA_EXEC_NT):
if mode & AA_EXEC_UNSAFE:
string += 'p'
else:
string += 'P'
if mode & AA_EXEC_CHILD:
if mode & AA_EXEC_UNSAFE:
string += 'c'
else:
string += 'C'
if mode & AA_EXEC_UNCONFINED:
if mode & AA_EXEC_UNSAFE:
string += 'u'
else:
string += 'U'
if mode & AA_EXEC_INHERIT:
string += 'i'
if mode & AA_MAY_EXEC:
string += 'x'
return string
def is_user_mode(mode):
user, other = split_mode(mode)
if user and not other:
return True
else:
return False
def profilemode(mode):
pass
def split_mode(mode):
user = set()
for i in mode:
if not '::' in i:
user.add(i)
other = mode - user
other = AA_OTHER_REMOVE(other)
return user, other
def mode_to_str(mode):
mode = flatten_mode(mode)
return sub_mode_to_str(mode)
def flatten_mode(mode):
if not mode:
return set()
user, other = split_mode(mode)
mode = user | other
mode |= (AA_OTHER(mode))
return mode
def owner_flatten_mode(mode):
mode = flatten_mode(mode)
return mode
def mode_to_str_user(mode):
user, other = split_mode(mode)
string = ''
if not user:
user = set()
if not other:
other = set()
if user - other:
if other:
string = sub_mode_to_str(other) + '+'
string += 'owner ' + sub_mode_to_str(user - other)
elif is_user_mode(mode):
string = 'owner ' + sub_mode_to_str(user)
else:
string = sub_mode_to_str(flatten_mode(mode))
return string
def log_str_to_mode(profile, string, nt_name):
mode = str_to_mode(string)
# If contains nx and nix
#print (profile, string, nt_name)
if contains(mode, 'Nx'):
# Transform to px, cx
match = re.search('(.+?)//(.+?)', nt_name)
if match:
lprofile, lhat = match.groups()
tmode = 0
if lprofile == profile:
if mode & AA_MAY_EXEC:
tmode = str_to_mode('Cx::')
if mode & AA_OTHER(AA_MAY_EXEC):
tmode |= str_to_mode('Cx')
nt_name = lhat
else:
if mode & AA_MAY_EXEC:
tmode = str_to_mode('Px::')
if mode & AA_OTHER(AA_MAY_EXEC):
tmode |= str_to_mode('Px')
nt_name = lhat
mode = mode - str_to_mode('Nx')
mode |= tmode
return mode, nt_name

View file

@ -0,0 +1,153 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import re
import copy
import apparmor
class Prof(object):
def __init__(self, filename):
self.aa = apparmor.aa.aa
self.filelist = apparmor.aa.filelist
self.include = apparmor.aa.include
self.filename = filename
class CleanProf(object):
def __init__(self, same_file, profile, other):
#If same_file we're basically comparing the file against itself to check superfluous rules
self.same_file = same_file
self.profile = profile
self.other = other
def compare_profiles(self):
deleted = 0
other_file_includes = list(self.other.filelist[self.other.filename]['include'].keys())
#Remove the duplicate file-level includes from other
for rule in self.profile.filelist[self.profile.filename]['include'].keys():
if rule in other_file_includes:
self.other.filelist[self.other.filename]['include'].pop(rule)
for profile in self.profile.aa.keys():
deleted += self.remove_duplicate_rules(profile)
return deleted
def remove_duplicate_rules(self, program):
#Process the profile of the program
#Process every hat in the profile individually
file_includes = list(self.profile.filelist[self.profile.filename]['include'].keys())
deleted = 0
for hat in self.profile.aa[program].keys():
#The combined list of includes from profile and the file
includes = list(self.profile.aa[program][hat]['include'].keys()) + file_includes
#If different files remove duplicate includes in the other profile
if not self.same_file:
for inc in includes:
if self.other.aa[program][hat]['include'].get(inc, False):
self.other.aa[program][hat]['include'].pop(inc)
deleted += 1
#Clean up superfluous rules from includes in the other profile
for inc in includes:
if not self.profile.include.get(inc, {}).get(inc, False):
apparmor.aa.load_include(inc)
deleted += apparmor.aa.delete_duplicates(self.other.aa[program][hat], inc)
#Clean the duplicates of caps in other profile
deleted += delete_cap_duplicates(self.profile.aa[program][hat]['allow']['capability'], self.other.aa[program][hat]['allow']['capability'], self.same_file)
deleted += delete_cap_duplicates(self.profile.aa[program][hat]['deny']['capability'], self.other.aa[program][hat]['deny']['capability'], self.same_file)
#Clean the duplicates of path in other profile
deleted += delete_path_duplicates(self.profile.aa[program][hat], self.other.aa[program][hat], 'allow', self.same_file)
deleted += delete_path_duplicates(self.profile.aa[program][hat], self.other.aa[program][hat], 'deny', self.same_file)
#Clean the duplicates of net rules in other profile
deleted += delete_net_duplicates(self.profile.aa[program][hat]['allow']['netdomain'], self.other.aa[program][hat]['allow']['netdomain'], self.same_file)
deleted += delete_net_duplicates(self.profile.aa[program][hat]['deny']['netdomain'], self.other.aa[program][hat]['deny']['netdomain'], self.same_file)
return deleted
def delete_path_duplicates(profile, profile_other, allow, same_profile=True):
deleted = []
# Check if any individual rule makes any rule superfluous
for rule in profile[allow]['path'].keys():
for entry in profile_other[allow]['path'].keys():
if rule == entry:
# Check the modes
cm = profile[allow]['path'][rule]['mode']
am = profile[allow]['path'][rule]['audit']
# If modes of rule are a superset of rules implied by entry we can safely remove it
if apparmor.aa.mode_contains(cm, profile_other[allow]['path'][entry]['mode']) and apparmor.aa.mode_contains(am, profile_other[allow]['path'][entry]['audit']):
if not same_profile:
deleted.append(entry)
continue
if re.search('#?\s*include', rule) or re.search('#?\s*include', entry):
continue
# Check if the rule implies entry
if apparmor.aa.matchliteral(rule, entry):
# Check the modes
cm = profile[allow]['path'][rule]['mode']
am = profile[allow]['path'][rule]['audit']
# If modes of rule are a superset of rules implied by entry we can safely remove it
if apparmor.aa.mode_contains(cm, profile_other[allow]['path'][entry]['mode']) and apparmor.aa.mode_contains(am, profile_other[allow]['path'][entry]['audit']):
deleted.append(entry)
for entry in deleted:
profile_other[allow]['path'].pop(entry)
return len(deleted)
def delete_cap_duplicates(profilecaps, profilecaps_other, same_profile=True):
deleted = []
if profilecaps and profilecaps_other and not same_profile:
for capname in profilecaps.keys():
if profilecaps_other[capname].get('set', False):
deleted.append(capname)
for capname in deleted:
profilecaps_other.pop(capname)
return len(deleted)
def delete_net_duplicates(netrules, netrules_other, same_profile=True):
deleted = 0
hasher_obj = apparmor.aa.hasher()
copy_netrules_other = copy.deepcopy(netrules_other)
if netrules_other and netrules:
netglob = False
# Delete matching rules
if netrules.get('all', False):
netglob = True
# Iterate over a copy of the rules in the other profile
for fam in copy_netrules_other['rule'].keys():
if netglob or (type(netrules['rule'][fam]) != type(hasher_obj) and netrules['rule'][fam]):
if not same_profile:
if type(netrules_other['rule'][fam]) == type(hasher_obj):
deleted += len(netrules_other['rule'][fam].keys())
else:
deleted += 1
netrules_other['rule'].pop(fam)
elif type(netrules_other['rule'][fam]) != type(hasher_obj) and netrules_other['rule'][fam]:
if type(netrules['rule'][fam]) != type(hasher_obj) and netrules['rule'][fam]:
if not same_profile:
netrules_other['rule'].pop(fam)
deleted += 1
else:
for sock_type in netrules_other['rule'][fam].keys():
if netrules['rule'].get(fam, False):
if netrules['rule'][fam].get(sock_type, False):
if not same_profile:
netrules_other['rule'][fam].pop(sock_type)
deleted += 1
return deleted

View file

@ -1,6 +1,7 @@
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# #
# Copyright (C) 2012 Canonical Ltd. # Copyright (C) 2012 Canonical Ltd.
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
# #
# This program is free software; you can redistribute it and/or # This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public # modify it under the terms of version 2 of the GNU General Public
@ -9,11 +10,20 @@
# ------------------------------------------------------------------ # ------------------------------------------------------------------
from __future__ import print_function from __future__ import print_function
import codecs
import collections
import glob
import logging
import os
import re
import subprocess import subprocess
import sys import sys
import termios
import tty
DEBUGGING = False DEBUGGING = False
# #
# Utility classes # Utility classes
# #
@ -93,3 +103,154 @@ def cmd_pipe(command1, command2):
return [sp2.returncode, out] return [sp2.returncode, out]
def valid_path(path):
'''Valid path'''
# No relative paths
m = "Invalid path: %s" % (path)
if not path.startswith('/'):
debug("%s (relative)" % (m))
return False
if '"' in path: # We double quote elsewhere
return False
try:
os.path.normpath(path)
except Exception:
debug("%s (could not normalize)" % (m))
return False
return True
def get_directory_contents(path):
'''Find contents of the given directory'''
if not valid_path(path):
return None
files = []
for f in glob.glob(path + "/*"):
files.append(f)
files.sort()
return files
def open_file_read(path, encoding='UTF-8'):
'''Open specified file read-only'''
try:
orig = codecs.open(path, 'r', encoding)
except Exception:
raise
return orig
def open_file_write(path):
'''Open specified file in write/overwrite mode'''
try:
orig = codecs.open(path, 'w', 'UTF-8')
except Exception:
raise
return orig
def readkey():
'''Returns the pressed key'''
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
def hasher():
'''A neat alternative to perl's hash reference'''
# Creates a dictionary for any depth and returns empty dictionary otherwise
return collections.defaultdict(hasher)
def convert_regexp(regexp):
regex_paren = re.compile('^(.*){([^}]*)}(.*)$')
regexp = regexp.strip()
new_reg = re.sub(r'(?<!\\)(\.|\+|\$)', r'\\\1', regexp)
while regex_paren.search(new_reg):
match = regex_paren.search(new_reg).groups()
prev = match[0]
after = match[2]
p1 = match[1].replace(',', '|')
new_reg = prev + '(' + p1 + ')' + after
new_reg = new_reg.replace('?', '[^/\000]')
multi_glob = '__KJHDKVZH_AAPROF_INTERNAL_GLOB_SVCUZDGZID__'
new_reg = new_reg.replace('**', multi_glob)
#print(new_reg)
# Match atleast one character if * or ** after /
# ?< is the negative lookback operator
new_reg = new_reg.replace('*', '(((?<=/)[^/\000]+)|((?<!/)[^/\000]*))')
new_reg = new_reg.replace(multi_glob, '(((?<=/)[^\000]+)|((?<!/)[^\000]*))')
if regexp[0] != '^':
new_reg = '^' + new_reg
if regexp[-1] != '$':
new_reg = new_reg + '$'
return new_reg
def user_perm(prof_dir):
if not os.access(prof_dir, os.W_OK):
sys.stdout.write("Cannot write to profile directory.\n" +
"Please run as a user with appropriate permissions.\n")
return False
return True
class DebugLogger(object):
def __init__(self, module_name=__name__):
self.debugging = False
self.logfile = '/var/log/apparmor/logprof.log'
self.debug_level = logging.DEBUG
if os.getenv('LOGPROF_DEBUG', False):
self.debugging = os.getenv('LOGPROF_DEBUG')
try:
self.debugging = int(self.debugging)
except Exception:
self.debugging = False
if self.debugging not in range(0, 4):
sys.stdout.write('Environment Variable: LOGPROF_DEBUG contains invalid value: %s'
% os.getenv('LOGPROF_DEBUG'))
if self.debugging == 0: # debugging disabled, don't need to setup logging
return
if self.debugging == 1:
self.debug_level = logging.ERROR
elif self.debugging == 2:
self.debug_level = logging.INFO
elif self.debugging == 3:
self.debug_level = logging.DEBUG
try:
logging.basicConfig(filename=self.logfile, level=self.debug_level,
format='%(asctime)s - %(name)s - %(message)s\n')
except OSError:
# Unable to open the default logfile, so create a temporary logfile and tell use about it
import tempfile
templog = tempfile.NamedTemporaryFile('w', prefix='apparmor', suffix='.log', delete=False)
sys.stdout.write("\nCould not open: %s\nLogging to: %s\n" % (self.logfile, templog.name))
logging.basicConfig(filename=templog.name, level=self.debug_level,
format='%(asctime)s - %(name)s - %(message)s\n')
self.logger = logging.getLogger(module_name)
def error(self, message):
if self.debugging:
self.logger.error(message)
def info(self, message):
if self.debugging:
self.logger.info(message)
def debug(self, message):
if self.debugging:
self.logger.debug(message)
def shutdown(self):
logging.shutdown()
#logging.shutdown([self.logger])

297
utils/apparmor/config.py Normal file
View file

@ -0,0 +1,297 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
from __future__ import with_statement
import os
import shlex
import shutil
import stat
import sys
import tempfile
if sys.version_info < (3, 0):
import ConfigParser as configparser
# Class to provide the object[section][option] behavior in Python2
class configparser_py2(configparser.ConfigParser):
def __getitem__(self, section):
section_val = self.items(section)
section_options = dict()
for option, value in section_val:
section_options[option] = value
return section_options
else:
import configparser
from apparmor.common import AppArmorException, open_file_read # , warn, msg,
# CFG = None
# REPO_CFG = None
# SHELL_FILES = ['easyprof.conf', 'notify.conf', 'parser.conf', 'subdomain.conf']
class Config(object):
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
self.input_file = None
else:
raise AppArmorException("Unknown configuration file type")
def new_config(self):
if self.conf_type == 'shell':
config = {'': dict()}
elif self.conf_type == 'ini':
config = configparser.ConfigParser()
return config
def read_config(self, filename):
"""Reads the file and returns a config[section][attribute]=property object"""
# LP: Bug #692406
# Explicitly disabled repository
filepath = self.CONF_DIR + '/' + filename
self.input_file = filepath
if filename == "repository.conf":
config = dict()
config['repository'] = {'enabled': 'no'}
elif self.conf_type == 'shell':
config = self.read_shell(filepath)
elif self.conf_type == 'ini':
if sys.version_info > (3, 0):
config = configparser.ConfigParser()
else:
config = configparser_py2()
# Set the option form to string -prevents forced conversion to lowercase
config.optionxform = str
if sys.version_info > (3, 0):
config.read(filepath)
else:
try:
config.read(filepath)
except configparser.ParsingError:
tmp_filepath = py2_parser(filepath)
config.read(tmp_filepath.name)
##config.__get__()
return config
def write_config(self, filename, config):
"""Writes the given config to the specified file"""
filepath = self.CONF_DIR + '/' + filename
permission_600 = stat.S_IRUSR | stat.S_IWUSR # Owner read and write
try:
# Open a temporary file in the CONF_DIR to write the config file
config_file = tempfile.NamedTemporaryFile('w', prefix='aa_temp', delete=False, dir=self.CONF_DIR)
if os.path.exists(self.input_file):
# Copy permissions from an existing file to temporary file
shutil.copymode(self.input_file, config_file.name)
else:
# If no existing permission set the file permissions as 0600
os.chmod(config_file.name, permission_600)
if self.conf_type == 'shell':
self.write_shell(filepath, config_file, config)
elif self.conf_type == 'ini':
self.write_configparser(filepath, config_file, config)
config_file.close()
except IOError:
raise AppArmorException("Unable to write to %s" % filename)
else:
# Replace the target config file with the temporary file
os.rename(config_file.name, filepath)
def find_first_file(self, file_list):
"""Returns name of first matching file None otherwise"""
filename = None
for f in file_list.split():
if os.path.isfile(f):
filename = f
break
return filename
def find_first_dir(self, dir_list):
"""Returns name of first matching directory None otherwise"""
dirname = None
if dir_list:
for direc in dir_list.split():
if os.path.isdir(direc):
dirname = direc
break
return dirname
def read_shell(self, filepath):
"""Reads the shell type conf files and returns config[''][option]=value"""
config = {'': dict()}
with open_file_read(filepath) as conf_file:
for line in conf_file:
result = shlex.split(line, True)
# If not a comment of empty line
if result:
# option="value" or option=value type
if '=' in result[0]:
option, value = result[0].split('=')
# option type
else:
option = result[0]
value = None
config[''][option] = value
return config
def write_shell(self, filepath, f_out, config):
"""Writes the config object in shell file format"""
# All the options in the file
options = [key for key in config[''].keys()]
# If a previous file exists modify it keeping the comments
if os.path.exists(self.input_file):
with open_file_read(self.input_file) as f_in:
for line in f_in:
result = shlex.split(line, True)
# If line is not empty or comment
if result:
# If option=value or option="value" type
if '=' in result[0]:
option, value = result[0].split('=')
if '#' in line:
comment = value.split('#', 1)[1]
comment = '#' + comment
else:
comment = ''
# If option exists in the new config file
if option in options:
# If value is different
if value != config[''][option]:
value_new = config[''][option]
if value_new is not None:
# Update value
if '"' in line:
value_new = '"' + value_new + '"'
line = option + '=' + value_new + comment + '\n'
else:
# If option changed to option type from option=value type
line = option + comment + '\n'
f_out.write(line)
# Remove from remaining options list
options.remove(option)
else:
# If option type
option = result[0]
value = None
# If option exists in the new config file
if option in options:
# If its no longer option type
if config[''][option] is not None:
value = config[''][option]
line = option + '=' + value + '\n'
f_out.write(line)
# Remove from remaining options list
options.remove(option)
else:
# If its empty or comment copy as it is
f_out.write(line)
# If any new options are present
if options:
for option in options:
value = config[''][option]
# option type entry
if value is None:
line = option + '\n'
# option=value type entry
else:
line = option + '=' + value + '\n'
f_out.write(line)
def write_configparser(self, filepath, f_out, config):
"""Writes/updates the given file with given config object"""
# All the sections in the file
sections = config.sections()
write = True
section = None
options = []
# If a previous file exists modify it keeping the comments
if os.path.exists(self.input_file):
with open_file_read(self.input_file) as f_in:
for line in f_in:
# If its a section
if line.lstrip().startswith('['):
# If any options from preceding section remain write them
if options:
for option in options:
line_new = ' ' + option + ' = ' + config[section][option] + '\n'
f_out.write(line_new)
options = []
if section in sections:
# Remove the written section from the list
sections.remove(section)
section = line.strip()[1:-1]
if section in sections:
# enable write for all entries in that section
write = True
options = config.options(section)
# write the section
f_out.write(line)
else:
# disable writing until next valid section
write = False
# If write enabled
elif write:
value = shlex.split(line, True)
# If the line is empty or a comment
if not value:
f_out.write(line)
else:
option, value = line.split('=', 1)
try:
# split any inline comments
value, comment = value.split('#', 1)
comment = '#' + comment
except ValueError:
comment = ''
if option.strip() in options:
if config[section][option.strip()] != value.strip():
value = value.replace(value, config[section][option.strip()])
line = option + '=' + value + comment
f_out.write(line)
options.remove(option.strip())
# If any options remain from the preceding section
if options:
for option in options:
line = ' ' + option + ' = ' + config[section][option] + '\n'
f_out.write(line)
options = []
# If any new sections are present
if section in sections:
sections.remove(section)
for section in sections:
f_out.write('\n[%s]\n' % section)
options = config.options(section)
for option in options:
line = ' ' + option + ' = ' + config[section][option] + '\n'
f_out.write(line)
def py2_parser(filename):
"""Returns the de-dented ini file from the new format ini"""
tmp = tempfile.NamedTemporaryFile('rw')
f_out = open(tmp.name, 'w')
if os.path.exists(filename):
with open_file_read(filename) as f_in:
for line in f_in:
# The ini format allows for multi-line entries, with the subsequent
# entries being indented deeper hence simple lstrip() is not appropriate
if line[:2] == ' ':
line = line[2:]
elif line[0] == '\t':
line = line[1:]
f_out.write(line)
f_out.flush()
return tmp

399
utils/apparmor/logparser.py Normal file
View file

@ -0,0 +1,399 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import gettext
import os
import re
import sys
import time
import LibAppArmor
from apparmor.common import (AppArmorException, error, debug,
open_file_read, valid_path, hasher,
open_file_write, convert_regexp,
DebugLogger)
from apparmor.aamode import validate_log_mode, log_str_to_mode, hide_log_mode, AA_MAY_EXEC
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
class ReadLog:
RE_LOG_v2_6_syslog = re.compile('kernel:\s+(\[[\d\.\s]+\]\s+)?type=\d+\s+audit\([\d\.\:]+\):\s+apparmor=')
RE_LOG_v2_6_audit = re.compile('type=AVC\s+(msg=)?audit\([\d\.\:]+\):\s+apparmor=')
MODE_MAP_RE = re.compile('r|w|l|m|k|a|x|i|u|p|c|n|I|U|P|C|N')
LOG_MODE_RE = re.compile('r|w|l|m|k|a|x|ix|ux|px|cx|nx|pix|cix|Ix|Ux|Px|PUx|Cx|Nx|Pix|Cix')
PROFILE_MODE_RE = re.compile('r|w|l|m|k|a|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix')
PROFILE_MODE_NT_RE = re.compile('r|w|l|m|k|a|x|ix|ux|px|cx|pix|cix|Ux|Px|PUx|Cx|Pix|Cix')
PROFILE_MODE_DENY_RE = re.compile('r|w|l|m|k|a|x')
# Used by netdomain to identify the operation types
# New socket names
OPERATION_TYPES = {'create': 'net',
'post_create': 'net',
'bind': 'net',
'connect': 'net',
'listen': 'net',
'accept': 'net',
'sendmsg': 'net',
'recvmsg': 'net',
'getsockname': 'net',
'getpeername': 'net',
'getsockopt': 'net',
'setsockopt': 'net',
'sock_shutdown': 'net'
}
def __init__(self, pid, filename, existing_profiles, profile_dir, log):
self.filename = filename
self.profile_dir = profile_dir
self.pid = pid
self.existing_profiles = existing_profiles
self.log = log
self.debug_logger = DebugLogger('ReadLog')
self.LOG = None
self.logmark = ''
self.seenmark = None
self.next_log_entry = None
def prefetch_next_log_entry(self):
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) 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
def get_next_log_entry(self):
# If no next log entry fetch it
if not self.next_log_entry:
self.prefetch_next_log_entry()
log_entry = self.next_log_entry
self.next_log_entry = None
return log_entry
def peek_at_next_log_entry(self):
# Take a peek at the next log entry
if not self.next_log_entry:
self.prefetch_next_log_entry()
return self.next_log_entry
def throw_away_next_log_entry(self):
self.next_log_entry = None
def parse_log_record(self, record):
self.debug_logger.debug('parse_log_record: %s' % record)
record_event = self.parse_event(record)
return record_event
def parse_event(self, msg):
"""Parse the event from log into key value pairs"""
msg = msg.strip()
self.debug_logger.info('parse_event: %s' % msg)
#print(repr(msg))
if sys.version_info < (3, 0):
# parse_record fails with u'foo' style strings hence typecasting to string
msg = str(msg)
event = LibAppArmor.parse_record(msg)
ev = dict()
ev['resource'] = event.info
ev['active_hat'] = event.active_hat
ev['aamode'] = event.event
ev['time'] = event.epoch
ev['operation'] = event.operation
ev['profile'] = event.profile
ev['name'] = event.name
ev['name2'] = event.name2
ev['attr'] = event.attribute
ev['parent'] = event.parent
ev['pid'] = event.pid
ev['task'] = event.task
ev['info'] = event.info
dmask = event.denied_mask
rmask = event.requested_mask
ev['magic_token'] = event.magic_token
if ev['operation'] and self.op_type(ev['operation']) == 'net':
ev['family'] = event.net_family
ev['protocol'] = event.net_protocol
ev['sock_type'] = event.net_sock_type
LibAppArmor.free_record(event)
# Map c (create) to a and d (delete) to w, logprof doesn't support c and d
if rmask:
rmask = rmask.replace('c', 'a')
rmask = rmask.replace('d', 'w')
if not validate_log_mode(hide_log_mode(rmask)):
raise AppArmorException(_('Log contains unknown mode %s') % rmask)
if dmask:
dmask = dmask.replace('c', 'a')
dmask = dmask.replace('d', 'w')
if not validate_log_mode(hide_log_mode(dmask)):
raise AppArmorException(_('Log contains unknown mode %s') % dmask)
#print('parse_event:', ev['profile'], dmask, ev['name2'])
mask, name = log_str_to_mode(ev['profile'], dmask, ev['name2'])
ev['denied_mask'] = mask
ev['name2'] = name
mask, name = log_str_to_mode(ev['profile'], rmask, ev['name2'])
ev['request_mask'] = mask
ev['name2'] = name
if not ev['time']:
ev['time'] = int(time.time())
# Remove None keys
#for key in ev.keys():
# if not ev[key] or not re.search('[\w]+', ev[key]):
# ev.pop(key)
if ev['aamode']:
# Convert aamode values to their counter-parts
mode_convertor = {0: 'UNKNOWN',
1: 'ERROR',
2: 'AUDITING',
3: 'PERMITTING',
4: 'REJECTING',
5: 'HINT',
6: 'STATUS'
}
try:
ev['aamode'] = mode_convertor[ev['aamode']]
except KeyError:
ev['aamode'] = None
if ev['aamode']:
#debug_logger.debug(ev)
return ev
else:
return None
def add_to_tree(self, loc_pid, parent, type, event):
self.debug_logger.info('add_to_tree: pid [%s] type [%s] event [%s]' % (loc_pid, type, event))
if not self.pid.get(loc_pid, False):
profile, hat = event[:2]
if parent and self.pid.get(parent, False):
if not hat:
hat = 'null-complain-profile'
arrayref = []
self.pid[parent].append(arrayref)
self.pid[loc_pid] = arrayref
for ia in ['fork', loc_pid, profile, hat]:
arrayref.append(ia)
# self.pid[parent].append(array_ref)
# self.pid[loc_pid] = array_ref
else:
arrayref = []
self.log.append(arrayref)
self.pid[loc_pid] = arrayref
# self.log.append(array_ref)
# self.pid[loc_pid] = array_ref
self.pid[loc_pid].append([type, loc_pid] + event)
#print("\n\npid",self.pid)
#print("log",self.log)
def add_event_to_tree(self, e):
aamode = e.get('aamode', 'UNKNOWN')
if e.get('type', False):
if re.search('(UNKNOWN\[1501\]|APPARMOR_AUDIT|1501)', e['type']):
aamode = 'AUDIT'
elif re.search('(UNKNOWN\[1502\]|APPARMOR_ALLOWED|1502)', e['type']):
aamode = 'PERMITTING'
elif re.search('(UNKNOWN\[1503\]|APPARMOR_DENIED|1503)', e['type']):
aamode = 'REJECTING'
elif re.search('(UNKNOWN\[1504\]|APPARMOR_HINT|1504)', e['type']):
aamode = 'HINT'
elif re.search('(UNKNOWN\[1505\]|APPARMOR_STATUS|1505)', e['type']):
aamode = 'STATUS'
elif re.search('(UNKNOWN\[1506\]|APPARMOR_ERROR|1506)', e['type']):
aamode = 'ERROR'
else:
aamode = 'UNKNOWN'
if aamode in ['UNKNOWN', 'AUDIT', 'STATUS', 'ERROR']:
return None
if 'profile_set' in e['operation']:
return None
# Skip if AUDIT event was issued due to a change_hat in unconfined mode
if not e.get('profile', False):
return None
# Convert new null profiles to old single level null profile
if '//null-' in e['profile']:
e['profile'] = 'null-complain-profile'
profile = e['profile']
hat = None
if '//' in e['profile']:
profile, hat = e['profile'].split('//')[:2]
# Filter out change_hat events that aren't from learning
if e['operation'] == 'change_hat':
if aamode != 'HINT' and aamode != 'PERMITTING':
return None
profile = e['name']
#hat = None
if '//' in e['name']:
profile, hat = e['name'].split('//')[:2]
if not hat:
hat = profile
# prog is no longer passed around consistently
prog = 'HINT'
if profile != 'null-complain-profile' and not self.profile_exists(profile):
return None
if e['operation'] == 'exec':
if e.get('info', False) and e['info'] == 'mandatory profile missing':
self.add_to_tree(e['pid'], e['parent'], 'exec',
[profile, hat, aamode, 'PERMITTING', e['denied_mask'], e['name'], e['name2']])
elif e.get('name2', False) and '\\null-/' in e['name2']:
self.add_to_tree(e['pid'], e['parent'], 'exec',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e.get('name', False):
self.add_to_tree(e['pid'], e['parent'], 'exec',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
else:
self.debug_logger.debug('add_event_to_tree: dropped exec event in %s' % e['profile'])
elif 'file_' in e['operation']:
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e['operation'] in ['open', 'truncate', 'mkdir', 'mknod', 'rename_src',
'rename_dest', 'unlink', 'rmdir', 'symlink_create', 'link']:
#print(e['operation'], e['name'])
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e['operation'] == 'capable':
self.add_to_tree(e['pid'], e['parent'], 'capability',
[profile, hat, prog, aamode, e['name'], ''])
elif e['operation'] == 'setattr' or 'xattr' in e['operation']:
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif 'inode_' in e['operation']:
is_domain_change = False
if e['operation'] == 'inode_permission' and (e['denied_mask'] & AA_MAY_EXEC) and aamode == 'PERMITTING':
following = self.peek_at_next_log_entry()
if following:
entry = self.parse_log_record(following)
if entry and entry.get('info', False) == 'set profile':
is_domain_change = True
self.throw_away_next_log_entry()
if is_domain_change:
self.add_to_tree(e['pid'], e['parent'], 'exec',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], e['name2']])
else:
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e['operation'] == 'sysctl':
self.add_to_tree(e['pid'], e['parent'], 'path',
[profile, hat, prog, aamode, e['denied_mask'], e['name'], ''])
elif e['operation'] == 'clone':
parent, child = e['pid'], e['task']
if not parent:
parent = 'null-complain-profile'
if not hat:
hat = 'null-complain-profile'
arrayref = []
if self.pid.get(parent, False):
self.pid[parent].append(arrayref)
else:
self.log.append(arrayref)
self.pid[child].append(arrayref)
for ia in ['fork', child, profile, hat]:
arrayref.append(ia)
# if self.pid.get(parent, False):
# self.pid[parent] += [arrayref]
# else:
# self.log += [arrayref]
# self.pid[child] = arrayref
elif self.op_type(e['operation']) == 'net':
self.add_to_tree(e['pid'], e['parent'], 'netdomain',
[profile, hat, prog, aamode, e['family'], e['sock_type'], e['protocol']])
elif e['operation'] == 'change_hat':
self.add_to_tree(e['pid'], e['parent'], 'unknown_hat',
[profile, hat, aamode, hat])
else:
self.debug_logger.debug('UNHANDLED: %s' % e)
def read_log(self, logmark):
self.logmark = logmark
seenmark = True
if self.logmark:
seenmark = False
#last = None
#event_type = None
try:
#print(self.filename)
self.LOG = open_file_read(self.filename)
except IOError:
raise AppArmorException('Can not read AppArmor logfile: ' + self.filename)
#LOG = open_file_read(log_open)
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:
seenmark = True
self.debug_logger.debug('read_log: seenmark = %s' % seenmark)
if not seenmark:
continue
event = self.parse_log_record(line)
#print(event)
if event:
self.add_event_to_tree(event)
self.LOG.close()
self.logmark = ''
return self.log
def op_type(self, operation):
"""Returns the operation type if known, unkown otherwise"""
operation_type = self.OPERATION_TYPES.get(operation, 'unknown')
return operation_type
def profile_exists(self, program):
"""Returns True if profile exists, False otherwise"""
# Check cache of profiles
if self.existing_profiles.get(program, False):
return True
# Check the disk for profile
prof_path = self.get_profile_filename(program)
#print(prof_path)
if os.path.isfile(prof_path):
# Add to cache of profile
self.existing_profiles[program] = prof_path
return True
return False
def get_profile_filename(self, profile):
"""Returns the full profile name"""
if profile.startswith('/'):
# Remove leading /
profile = profile[1:]
else:
profile = "profile_" + profile
profile = profile.replace('/', '.')
full_profilename = self.profile_dir + '/' + profile
return full_profilename

208
utils/apparmor/severity.py Normal file
View file

@ -0,0 +1,208 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
from __future__ import with_statement
import os
import re
from apparmor.common import AppArmorException, open_file_read, warn, convert_regexp # , msg, error, debug
class Severity(object):
def __init__(self, dbname=None, default_rank=10):
"""Initialises the class object"""
self.PROF_DIR = '/etc/apparmor.d' # The profile directory
self.severity = dict()
self.severity['DATABASENAME'] = dbname
self.severity['CAPABILITIES'] = {}
self.severity['FILES'] = {}
self.severity['REGEXPS'] = {}
self.severity['DEFAULT_RANK'] = default_rank
# For variable expansions for the profile
self.severity['VARIABLES'] = dict()
if not dbname:
return None
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:
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:
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"""
if resource in self.severity['CAPABILITIES'].keys():
return self.severity['CAPABILITIES'][resource]
# raise ValueError("unexpected capability rank input: %s"%resource)
warn("unknown capability: %s" % resource)
return self.severity['DEFAULT_RANK']
def check_subtree(self, tree, mode, sev, segments):
"""Returns the max severity from the regex tree"""
if len(segments) == 0:
first = ''
else:
first = segments[0]
rest = segments[1:]
path = '/'.join([first] + rest)
# Check if we have a matching directory tree to descend into
if tree.get(first, False):
sev = self.check_subtree(tree[first], mode, sev, rest)
# If severity still not found, match against globs
if sev is None:
# Match against all globs at this directory level
for chunk in tree.keys():
if '*' in chunk:
# Match rest of the path
if re.search("^" + chunk, path):
# Find max rank
if "AA_RANK" in tree[chunk].keys():
for m in mode:
if sev is None or tree[chunk]["AA_RANK"].get(m, -1) > sev:
sev = tree[chunk]["AA_RANK"].get(m, None)
return sev
def handle_file(self, resource, mode):
"""Returns the severity for the file, default value if no match found"""
resource = resource[1:] # remove initial / from path
pieces = resource.split('/') # break path into directory level chunks
sev = None
# Check for an exact match in the db
if resource in self.severity['FILES'].keys():
# Find max value among the given modes
for m in mode:
if sev is None or self.severity['FILES'][resource].get(m, -1) > sev:
sev = self.severity['FILES'][resource].get(m, None)
else:
# Search regex tree for matching glob
sev = self.check_subtree(self.severity['REGEXPS'], mode, sev, pieces)
if sev is None:
# Return default rank if severity cannot be found
return self.severity['DEFAULT_RANK']
else:
return sev
def rank(self, resource, mode=None):
"""Returns the rank for the resource file/capability"""
if '@' in resource: # path contains variable
return self.handle_variable_rank(resource, mode)
elif resource[0] == '/': # file resource
return self.handle_file(resource, mode)
elif resource[0:4] == 'CAP_': # capability resource
return self.handle_capability(resource)
else:
raise AppArmorException("Unexpected rank input: %s" % resource)
def handle_variable_rank(self, resource, mode):
"""Returns the max possible rank for file resources containing variables"""
regex_variable = re.compile('@{([^{.]*)}')
rank = None
if '@' in resource:
variable = regex_variable.search(resource).groups()[0]
variable = '@{%s}' % variable
#variables = regex_variable.findall(resource)
for replacement in self.severity['VARIABLES'][variable]:
resource_replaced = self.variable_replace(variable, replacement, resource)
rank_new = self.handle_variable_rank(resource_replaced, mode)
#rank_new = self.handle_variable_rank(resource.replace('@{'+variable+'}', replacement), mode)
if rank is None or rank_new > rank:
rank = rank_new
return rank
else:
return self.handle_file(resource, mode)
def variable_replace(self, variable, replacement, resource):
"""Returns the expanded path for the passed variable"""
leading = False
trailing = False
# Check for leading or trailing / that may need to be collapsed
if resource.find("/" + variable) != -1 and resource.find("//" + variable) == -1: # find that a single / exists before variable or not
leading = True
if resource.find(variable + "/") != -1 and resource.find(variable + "//") == -1:
trailing = True
if replacement[0] == '/' and replacement[:2] != '//' and leading: # finds if the replacement has leading / or not
replacement = replacement[1:]
if replacement[-1] == '/' and replacement[-2:] != '//' and trailing:
replacement = replacement[:-1]
return resource.replace(variable, replacement)
def load_variables(self, prof_path):
"""Loads the variables for the given profile"""
regex_include = re.compile('^#?include\s*<(\S*)>')
if os.path.isfile(prof_path):
with open_file_read(prof_path) as f_in:
for line in f_in:
line = line.strip()
# If any includes, load variables from them first
match = regex_include.search(line)
if match:
new_path = match.groups()[0]
new_path = self.PROF_DIR + '/' + new_path
self.load_variables(new_path)
else:
# Remove any comments
if '#' in line:
line = line.split('#')[0].rstrip()
# Expected format is @{Variable} = value1 value2 ..
if line.startswith('@') and '=' in line:
if '+=' in line:
line = line.split('+=')
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 value 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 in file: %s" % (line[0], prof_path))
self.severity['VARIABLES'][line[0]] = [i.strip('"') for i in line[1].split()]
def unload_variables(self):
"""Clears all loaded variables"""
self.severity['VARIABLES'] = dict()

185
utils/apparmor/tools.py Normal file
View file

@ -0,0 +1,185 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import gettext
import os
import sys
import apparmor.aa as apparmor
from apparmor.common import user_perm
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
class aa_tools:
def __init__(self, tool_name, args):
self.name = tool_name
self.profiledir = args.dir
self.profiling = args.program
self.check_profile_dir()
self.silent = None
if tool_name in ['audit', 'complain']:
self.remove = args.remove
elif tool_name == 'disable':
self.revert = args.revert
self.disabledir = apparmor.profile_dir + '/disable'
self.check_disable_dir()
elif tool_name == 'autodep':
self.force = args.force
self.aa_mountpoint = apparmor.check_for_apparmor()
elif tool_name == 'cleanprof':
self.silent = args.silent
def check_profile_dir(self):
if self.profiledir:
apparmor.profile_dir = apparmor.get_full_path(self.profiledir)
if not os.path.isdir(apparmor.profile_dir):
raise apparmor.AppArmorException("%s is not a directory." % self.profiledir)
if not user_perm(apparmor.profile_dir):
raise apparmor.AppArmorException("Cannot write to profile directory: %s" % (apparmor.profile_dir))
def check_disable_dir(self):
if not os.path.isdir(self.disabledir):
raise apparmor.AppArmorException("Can't find AppArmor disable directory %s" % self.disabledir)
def act(self):
for p in self.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.read_profiles()
#If program does not exists on the system but its profile does
if not program and apparmor.profile_exists(p):
program = p
if not program or not(os.path.exists(program) or apparmor.profile_exists(program)):
if program and not program.startswith('/'):
program = apparmor.UI_GetString(_('The given program cannot be found, please try with the fully qualified path name of the program: '), '')
else:
apparmor.UI_Info(_("%s does not exist, please double-check the path.") % p)
sys.exit(1)
if self.name == 'autodep' and program and os.path.exists(program):
self.use_autodep(program)
elif program and apparmor.profile_exists(program):
if self.name == 'cleanprof':
self.clean_profile(program, p)
else:
filename = apparmor.get_profile_filename(program)
if not os.path.isfile(filename) or apparmor.is_skippable_file(filename):
apparmor.UI_Info(_('Profile for %s not found, skipping') % p)
elif self.name == 'disable':
if not self.revert:
apparmor.UI_Info(_('Disabling %s.') % program)
self.disable_profile(filename)
else:
apparmor.UI_Info(_('Enabling %s.') % program)
self.enable_profile(filename)
elif self.name == 'audit':
if not self.remove:
apparmor.UI_Info(_('Setting %s to audit mode.') % program)
else:
apparmor.UI_Info(_('Removing audit mode from %s.') % program)
apparmor.change_profile_flags(filename, program, 'audit', not self.remove)
elif self.name == 'complain':
if not self.remove:
apparmor.set_complain(filename, program)
else:
apparmor.set_enforce(filename, program)
#apparmor.set_profile_flags(filename, self.name)
else:
# One simply does not walk in here!
raise apparmor.AppArmorException('Unknown tool: %s' % self.name)
cmd_info = apparmor.cmd([apparmor.parser, filename, '-I%s' % apparmor.profile_dir, '-R 2>&1', '1>/dev/null'])
#cmd_info = apparmor.cmd(['cat', filename, '|', apparmor.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\nis correct, please run 'which %s' as a user with correct PATH\nenvironment set up in order to find the fully-qualified path and\nuse the full path as parameter.") % (p, p))
else:
apparmor.UI_Info(_("%s does not exist, please double-check the path.") % p)
sys.exit(1)
def clean_profile(self, program, p):
filename = apparmor.get_profile_filename(program)
import apparmor.cleanprofile as cleanprofile
prof = cleanprofile.Prof(filename)
cleanprof = cleanprofile.CleanProf(True, prof, prof)
deleted = cleanprof.remove_duplicate_rules(program)
apparmor.UI_Info(_("\nDeleted %s rules.") % deleted)
apparmor.changed[program] = True
if filename:
if not self.silent:
q = apparmor.hasher()
q['title'] = 'Changed Local Profiles'
q['headers'] = []
q['explanation'] = _('The local profile for %s in file %s was changed. Would you like to save it?') % (program, filename)
q['functions'] = ['CMD_SAVE_CHANGES', 'CMD_VIEW_CHANGES', 'CMD_ABORT']
q['default'] = 'CMD_VIEW_CHANGES'
q['options'] = []
q['selected'] = 0
p = None
ans = ''
arg = None
while ans != 'CMD_SAVE_CHANGES':
ans, arg = apparmor.UI_PromptUser(q)
if ans == 'CMD_SAVE_CHANGES':
apparmor.write_profile_ui_feedback(program)
apparmor.reload_base(program)
elif ans == 'CMD_VIEW_CHANGES':
#oldprofile = apparmor.serialize_profile(apparmor.original_aa[program], program, '')
newprofile = apparmor.serialize_profile(apparmor.aa[program], program, '')
apparmor.display_changes_with_comments(filename, newprofile)
else:
apparmor.write_profile_ui_feedback(program)
apparmor.reload_base(program)
else:
raise apparmor.AppArmorException(_('The profile for %s does not exists. Nothing to clean.') % p)
def use_autodep(self, program):
apparmor.check_qualifiers(program)
if os.path.exists(apparmor.get_profile_filename(program) and not self.force):
apparmor.UI_Info('Profile for %s already exists - skipping.' % program)
else:
apparmor.autodep(program)
if self.aa_mountpoint:
apparmor.reload(program)
def enable_profile(self, filename):
apparmor.delete_symlink('disable', filename)
def disable_profile(self, filename):
apparmor.create_symlink('disable', filename)

View file

@ -0,0 +1,21 @@
# ------------------------------------------------------------------
#
# Copyright (C) 2014 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#
# ------------------------------------------------------------------
import gettext
TRANSLATION_DOMAIN = 'apparmor-utils'
__apparmor_gettext__ = None
def init_translation():
global __apparmor_gettext__
if __apparmor_gettext__ is None:
t = gettext.translation(TRANSLATION_DOMAIN, fallback=True)
__apparmor_gettext__ = t.gettext
return __apparmor_gettext__

457
utils/apparmor/ui.py Normal file
View file

@ -0,0 +1,457 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import gettext
import sys
import re
from apparmor.yasti import yastLog, SendDataToYast, GetDataFromYast
from apparmor.common import readkey, AppArmorException, DebugLogger
# setup module translations
from apparmor.translations import init_translation
_ = init_translation()
# Set up UI logger for separate messages from UI module
debug_logger = DebugLogger('UI')
# The operating mode: yast or text, text by default
UI_mode = 'text'
ARROWS = {'A': 'UP', 'B': 'DOWN', 'C': 'RIGHT', 'D': 'LEFT'}
def getkey():
key = readkey()
if key == '\x1B':
key = readkey()
if key == '[':
key = readkey()
if(ARROWS.get(key, False)):
key = ARROWS[key]
return key.strip()
def UI_Info(text):
debug_logger.info(text)
if UI_mode == 'text':
sys.stdout.write(text + '\n')
else:
yastLog(text)
def UI_Important(text):
debug_logger.debug(text)
if UI_mode == 'text':
sys.stdout.write('\n' + text + '\n')
else:
SendDataToYast({'type': 'dialog-error',
'message': text
})
path, yarg = GetDataFromYast()
def get_translated_hotkey(translated, cmsg=''):
msg = 'PromptUser: ' + _('Invalid hotkey for')
# Originally (\S) was used but with translations it would not work :(
if re.search('\((\S+)\)', translated, re.LOCALE):
return re.search('\((\S+)\)', translated, re.LOCALE).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))
default = default.lower()
ans = None
if UI_mode == 'text':
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'
elif ans == nokey:
ans = 'n'
elif ans == 'left':
default = 'y'
elif ans == 'right':
default = 'n'
else:
ans = 'XXXINVALIDXXX'
continue # If user presses any other button ask again
else:
ans = default
else:
SendDataToYast({'type': 'dialog-yesno',
'question': text
})
ypath, yarg = GetDataFromYast()
ans = yarg['answer']
if not ans:
ans = default
return ans
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 = get_translated_hotkey(yes).lower()
nokey = get_translated_hotkey(no).lower()
cancelkey = get_translated_hotkey(cancel).lower()
ans = 'XXXINVALIDXXX'
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))
elif default == 'n':
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()
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'
elif ans == 'left':
if default == 'n':
default = 'y'
elif default == 'c':
default = 'n'
elif ans == 'right':
if default == 'y':
default = 'n'
elif default == 'n':
default = 'c'
else:
ans = default
else:
SendDataToYast({'type': 'dialog-yesnocancel',
'question': text
})
ypath, yarg = GetDataFromYast()
ans = yarg['answer']
if not ans:
ans = default
return ans
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)
string = sys.stdin.readline()
else:
SendDataToYast({'type': 'dialog-getstring',
'label': text,
'default': default
})
ypath, yarg = GetDataFromYast()
string = yarg['string']
return string.strip()
def UI_GetFile(file):
debug_logger.debug('UI_GetFile: %s' % UI_mode)
filename = None
if UI_mode == 'text':
sys.stdout.write(file['description'] + '\n')
filename = sys.stdin.read()
else:
file['type'] = 'dialog-getfile'
SendDataToYast(file)
ypath, yarg = GetDataFromYast()
if yarg['answer'] == 'okay':
filename = yarg['filename']
return filename
def UI_BusyStart(message):
debug_logger.debug('UI_BusyStart: %s' % UI_mode)
if UI_mode == 'text':
UI_Info(message)
else:
SendDataToYast({'type': 'dialog-busy-start',
'message': message
})
ypath, yarg = GetDataFromYast()
def UI_BusyStop():
debug_logger.debug('UI_BusyStop: %s' % UI_mode)
if UI_mode != 'text':
SendDataToYast({'type': 'dialog-busy-stop'})
ypath, yarg = GetDataFromYast()
CMDS = {'CMD_ALLOW': _('(A)llow'),
'CMD_OTHER': _('(M)ore'),
'CMD_AUDIT_NEW': _('Audi(t)'),
'CMD_AUDIT_OFF': _('Audi(t) off'),
'CMD_AUDIT_FULL': _('Audit (A)ll'),
#'CMD_OTHER': '(O)pts',
'CMD_USER_ON': _('(O)wner permissions on'),
'CMD_USER_OFF': _('(O)wner permissions off'),
'CMD_DENY': _('(D)eny'),
'CMD_ABORT': _('Abo(r)t'),
'CMD_FINISHED': _('(F)inish'),
'CMD_ix': _('(I)nherit'),
'CMD_px': _('(P)rofile'),
'CMD_px_safe': _('(P)rofile Clean Exec'),
'CMD_cx': _('(C)hild'),
'CMD_cx_safe': _('(C)hild Clean Exec'),
'CMD_nx': _('(N)amed'),
'CMD_nx_safe': _('(N)amed Clean Exec'),
'CMD_ux': _('(U)nconfined'),
'CMD_ux_safe': _('(U)nconfined Clean Exec'),
'CMD_pix': _('(P)rofile Inherit'),
'CMD_pix_safe': _('(P)rofile Inherit Clean Exec'),
'CMD_cix': _('(C)hild Inherit'),
'CMD_cix_safe': _('(C)hild Inherit Clean Exec'),
'CMD_nix': _('(N)amed Inherit'),
'CMD_nix_safe': _('(N)amed Inherit Clean Exec'),
'CMD_EXEC_IX_ON': _('(X) ix On'),
'CMD_EXEC_IX_OFF': _('(X) ix Off'),
'CMD_SAVE': _('(S)ave Changes'),
'CMD_CONTINUE': _('(C)ontinue Profiling'),
'CMD_NEW': _('(N)ew'),
'CMD_GLOB': _('(G)lob'),
'CMD_GLOBEXT': _('Glob with (E)xtension'),
'CMD_ADDHAT': _('(A)dd Requested Hat'),
'CMD_USEDEFAULT': _('(U)se Default Hat'),
'CMD_SCAN': _('(S)can system log for AppArmor events'),
'CMD_HELP': _('(H)elp'),
'CMD_VIEW_PROFILE': _('(V)iew Profile'),
'CMD_USE_PROFILE': _('(U)se Profile'),
'CMD_CREATE_PROFILE': _('(C)reate New Profile'),
'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'),
'CMD_VIEW': _('(V)iew'),
'CMD_ENABLE_REPO': _('(E)nable Repository'),
'CMD_DISABLE_REPO': _('(D)isable Repository'),
'CMD_ASK_NEVER': _('(N)ever Ask Again'),
'CMD_ASK_LATER': _('Ask Me (L)ater'),
'CMD_YES': _('(Y)es'),
'CMD_NO': _('(N)o'),
'CMD_ALL_NET': _('Allow All (N)etwork'),
'CMD_NET_FAMILY': _('Allow Network Fa(m)ily'),
'CMD_OVERWRITE': _('(O)verwrite Profile'),
'CMD_KEEP': _('(K)eep Profile'),
'CMD_CONTINUE': _('(C)ontinue'),
'CMD_IGNORE_ENTRY': _('(I)gnore')
}
def UI_PromptUser(q, params=''):
cmd = None
arg = None
if UI_mode == 'text':
cmd, arg = Text_PromptUser(q)
else:
q['type'] = 'wizard'
SendDataToYast(q)
ypath, yarg = GetDataFromYast()
if not cmd:
cmd = 'CMD_ABORT'
arg = yarg['selected']
if cmd == 'CMD_ABORT':
confirm_and_abort()
cmd = 'XXXINVALIDXXX'
elif cmd == 'CMD_FINISHED':
if not params:
confirm_and_finish()
cmd = 'XXXINVALIDXXX'
return (cmd, arg)
def confirm_and_abort():
ans = UI_YesNo(_('Are you sure you want to abandon this set of profile changes and exit?'), 'n')
if ans == 'y':
UI_Info(_('Abandoning all changes.'))
#shutdown_yast()
#for prof in created:
# delete_profile(prof)
sys.exit(0)
def UI_ShortMessage(title, message):
SendDataToYast({'type': 'short-dialog-message',
'headline': title,
'message': message
})
ypath, yarg = GetDataFromYast()
def UI_LongMessage(title, message):
SendDataToYast({'type': 'long-dialog-message',
'headline': title,
'message': message
})
ypath, yarg = GetDataFromYast()
def confirm_and_finish():
sys.stdout.write(_('FINISHING...\n'))
sys.exit(0)
def Text_PromptUser(question):
title = question['title']
explanation = question['explanation']
headers = question['headers']
functions = question['functions']
default = question['default']
options = question['options']
selected = question.get('selected', 0)
helptext = question['helptext']
if helptext:
functions.append('CMD_HELP')
menu_items = []
keys = dict()
for cmd in functions:
if not CMDS.get(cmd, False):
raise AppArmorException(_('PromptUser: Unknown command %s') % cmd)
menutext = CMDS[cmd]
key = get_translated_hotkey(menutext).lower()
# Duplicate hotkey
if keys.get(key, False):
raise AppArmorException(_('PromptUser: Duplicate hotkey for %s: %s ') % (cmd, menutext))
keys[key] = cmd
if default and default == cmd:
menutext = '[%s]' % menutext
menu_items.append(menutext)
default_key = 0
if default and CMDS[default]:
defaulttext = CMDS[default]
defmsg = _('PromptUser: Invalid hotkey in default item')
default_key = get_translated_hotkey(defaulttext, defmsg).lower()
if not keys.get(default_key, False):
raise AppArmorException(_('PromptUser: Invalid default %s') % default)
widest = 0
header_copy = headers[:]
while header_copy:
header = header_copy.pop(0)
header_copy.pop(0)
if len(header) > widest:
widest = len(header)
widest += 1
formatstr = '%-' + str(widest) + 's %s\n'
function_regexp = '^('
function_regexp += '|'.join(keys.keys())
if options:
function_regexp += '|\d'
function_regexp += ')$'
ans = 'XXXINVALIDXXX'
while not re.search(function_regexp, ans, flags=re.IGNORECASE):
prompt = '\n'
if title:
prompt += '= %s =\n\n' % title
if headers:
header_copy = headers[:]
while header_copy:
header = header_copy.pop(0)
value = header_copy.pop(0)
prompt += formatstr % (header + ':', value)
prompt += '\n'
if explanation:
prompt += explanation + '\n\n'
if options:
for index, option in enumerate(options):
if selected == index:
format_option = ' [%s - %s]'
else:
format_option = ' %s - %s '
prompt += format_option % (index + 1, option)
prompt += '\n'
prompt += ' / '.join(menu_items)
sys.stdout.write(prompt + '\n')
ans = getkey().lower()
if ans:
if ans == 'up':
if options and selected > 0:
selected -= 1
ans = 'XXXINVALIDXXX'
elif ans == 'down':
if options and selected < len(options) - 1:
selected += 1
ans = 'XXXINVALIDXXX'
# elif keys.get(ans, False) == 'CMD_HELP':
# sys.stdout.write('\n%s\n' %helptext)
# ans = 'XXXINVALIDXXX'
elif is_number(ans) == 10:
# If they hit return choose default option
ans = default_key
elif options and re.search('^\d$', ans):
ans = int(ans)
if ans > 0 and ans <= len(options):
selected = ans - 1
ans = 'XXXINVALIDXXX'
if keys.get(ans, False) == 'CMD_HELP':
sys.stdout.write('\n%s\n' % helptext)
ans = 'again'
if keys.get(ans, False):
ans = keys[ans]
return ans, selected
def is_number(number):
try:
return int(number)
except:
return False

103
utils/apparmor/yasti.py Normal file
View file

@ -0,0 +1,103 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import re
#import ycp
import os
import sys
from apparmor.common import error, DebugLogger
# Set up UI logger for separate messages from YaST module
debug_logger = DebugLogger('YaST')
def setup_yast():
# To-Do
pass
def shutdown_yast():
# To-Do
pass
def yastLog(text):
ycp.y2milestone(text)
def SendDataToYast(data):
debug_logger.info('SendDataToYast: Waiting for YCP command')
for line in sys.stdin:
ycommand, ypath, yargument = ParseCommand(line)
if ycommand and ycommand == 'Read':
debug_logger.info('SendDataToYast: Sending--%s' % data)
Return(data)
return True
else:
debug_logger.info('SendDataToYast: Expected \'Read\' but got-- %s' % line)
error('SendDataToYast: didn\'t receive YCP command before connection died')
def GetDataFromYast():
debug_logger.inf('GetDataFromYast: Waiting for YCP command')
for line in sys.stdin:
debug_logger.info('GetDataFromYast: YCP: %s' % line)
ycommand, ypath, yarg = ParseCommand(line)
debug_logger.info('GetDataFromYast: Recieved--\n%s' % yarg)
if ycommand and ycommand == 'Write':
Return('true')
return ypath, yarg
else:
debug_logger.info('GetDataFromYast: Expected Write but got-- %s' % line)
error('GetDataFromYast: didn\'t receive YCP command before connection died')
def ParseCommand(commands):
term = ParseTerm(commands)
if term:
command = term[0]
term = term[1:]
else:
command = ''
path = ''
pathref = None
if term:
pathref = term[0]
term = term[1:]
if pathref:
if pathref.strip():
path = pathref.strip()
elif command != 'result':
ycp.y2error('The first arguement is not a path. (%s)' % pathref)
argument = None
if term:
argument = term[0]
if len(term) > 1:
ycp.y2warning('Superfluous command arguments ignored')
return (command, path, argument)
def ParseTerm(inp):
regex_term = re.compile('^\s*`?(\w*)\s*')
term = regex_term.search(inp)
ret = []
symbol = None
if term:
symbol = term.groups()[0]
else:
ycp.y2error('No term symbol')
ret.append(symbol)
inp = regex_term.sub('', inp)
if not inp.startswith('('):
ycp.y2error('No term parantheses')
argref, err, rest = ParseYcpTermBody(inp)
if err:
ycp.y2error('%s (%s)' % (err, rest))
else:
ret += argref
return ret

10
utils/po/README Normal file
View file

@ -0,0 +1,10 @@
GENERATING TRANSLATION MESSAGES
To generate the messages.pot file:
Run the following command in Translate.
python pygettext.py ../apparmor/*.py ../Tools/aa*
It will generate the messages.pot file in the Translate directory.
You might need to provide the full path to pygettext.py from your python installation. It will typically be in the /path/to/python/libs/Tools/i18n/pygettext.py

149
utils/test/aa_test.py Normal file
View file

@ -0,0 +1,149 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import unittest
import sys
sys.path.append('../')
import apparmor.aa
import apparmor.logparser
class Test(unittest.TestCase):
def setUp(self):
self.MODE_TEST = {'x': apparmor.aamode.AA_MAY_EXEC,
'w': apparmor.aamode.AA_MAY_WRITE,
'r': apparmor.aamode.AA_MAY_READ,
'a': apparmor.aamode.AA_MAY_APPEND,
'l': apparmor.aamode.AA_MAY_LINK,
'k': apparmor.aamode.AA_MAY_LOCK,
'm': apparmor.aamode.AA_EXEC_MMAP,
'i': apparmor.aamode.AA_EXEC_INHERIT,
'u': apparmor.aamode.AA_EXEC_UNCONFINED | apparmor.aamode.AA_EXEC_UNSAFE,
'U': apparmor.aamode.AA_EXEC_UNCONFINED,
'p': apparmor.aamode.AA_EXEC_PROFILE | apparmor.aamode.AA_EXEC_UNSAFE,
'P': apparmor.aamode.AA_EXEC_PROFILE,
'c': apparmor.aamode.AA_EXEC_CHILD | apparmor.aamode.AA_EXEC_UNSAFE,
'C': apparmor.aamode.AA_EXEC_CHILD,
}
def test_loadinclude(self):
apparmor.aa.loadincludes()
def test_path_globs(self):
globs = {
'/foo/': '/*/',
'/foo': '/*',
'/b*': '/*',
'/*b': '/*',
'/*': '/*',
'/*/': '/*/',
'/*.foo/': '/*/',
'/**.foo/': '/**/',
'/foo/*/': '/**/',
'/usr/foo/*': '/usr/**',
'/usr/foo/**': '/usr/**',
'/usr/foo/bar**': '/usr/foo/**',
'/usr/foo/**bar': '/usr/foo/**',
'/usr/bin/foo**bar': '/usr/bin/**',
'/usr/foo/**/bar': '/usr/foo/**/*',
'/usr/foo/**/*': '/usr/foo/**',
'/usr/foo/*/bar': '/usr/foo/*/*',
'/usr/bin/foo*bar': '/usr/bin/*',
'/usr/bin/*foo*': '/usr/bin/*',
'/usr/foo/*/*': '/usr/foo/**',
'/usr/foo/*/**': '/usr/foo/**',
'/**': '/**',
'/**/': '/**/'
}
for path in globs.keys():
self.assertEqual(apparmor.aa.glob_path(path), globs[path], 'Unexpected glob generated for path: %s'%path)
def test_path_withext_globs(self):
globs = {
'/foo/bar': '/foo/bar',
'/foo/**/bar': '/foo/**/bar',
'/foo.bar': '/*.bar',
'/*.foo': '/*.foo' ,
'/usr/*.bar': '/**.bar',
'/usr/**.bar': '/**.bar',
'/usr/foo**.bar': '/usr/**.bar',
'/usr/foo*.bar': '/usr/*.bar',
'/usr/fo*oo.bar': '/usr/*.bar',
'/usr/*foo*.bar': '/usr/*.bar',
'/usr/**foo.bar': '/usr/**.bar',
'/usr/*foo.bar': '/usr/*.bar',
'/usr/foo.b*': '/usr/*.b*'
}
for path in globs.keys():
self.assertEqual(apparmor.aa.glob_path_withext(path), globs[path], 'Unexpected glob generated for path: %s'%path)
def test_parse_event(self):
parser = apparmor.logparser.ReadLog('', '', '', '', '')
event = 'type=AVC msg=audit(1345027352.096:499): apparmor="ALLOWED" operation="rename_dest" parent=6974 profile="/usr/sbin/httpd2-prefork//vhost_foo" name=2F686F6D652F7777772F666F6F2E6261722E696E2F68747470646F63732F61707061726D6F722F696D616765732F746573742F696D61676520312E6A7067 pid=20143 comm="httpd2-prefork" requested_mask="wc" denied_mask="wc" fsuid=30 ouid=30'
parsed_event = parser.parse_event(event)
self.assertEqual(parsed_event['name'], '/home/www/foo.bar.in/httpdocs/apparmor/images/test/image 1.jpg', 'Incorrectly parsed/decoded name')
self.assertEqual(parsed_event['profile'], '/usr/sbin/httpd2-prefork//vhost_foo', 'Incorrectly parsed/decode profile name')
self.assertEqual(parsed_event['aamode'], 'PERMITTING')
self.assertEqual(parsed_event['request_mask'], set(['w', 'a', '::w', '::a']))
#print(parsed_event)
#event = 'type=AVC msg=audit(1322614912.304:857): apparmor="ALLOWED" operation="getattr" parent=16001 profile=74657374207370616365 name=74657374207370616365 pid=17011 comm="bash" requested_mask="r" denied_mask="r" fsuid=0 ouid=0'
#parsed_event = apparmor.aa.parse_event(event)
#print(parsed_event)
event = 'type=AVC msg=audit(1322614918.292:4376): apparmor="ALLOWED" operation="file_perm" parent=16001 profile=666F6F20626172 name="/home/foo/.bash_history" pid=17011 comm="bash" requested_mask="rw" denied_mask="rw" fsuid=0 ouid=1000'
parsed_event = parser.parse_event(event)
self.assertEqual(parsed_event['name'], '/home/foo/.bash_history', 'Incorrectly parsed/decoded name')
self.assertEqual(parsed_event['profile'], 'foo bar', 'Incorrectly parsed/decode profile name')
self.assertEqual(parsed_event['aamode'], 'PERMITTING')
self.assertEqual(parsed_event['request_mask'], set(['r', 'w', 'a','::r' , '::w', '::a']))
#print(parsed_event)
def test_modes_to_string(self):
for string in self.MODE_TEST.keys():
mode = self.MODE_TEST[string]
self.assertEqual(apparmor.aamode.mode_to_str(mode), string, 'mode is %s and string is %s'%(mode, string))
def test_string_to_modes(self):
#self.assertEqual(apparmor.aa.str_to_mode('wc'), 32270)
MODE_TEST = {'x': apparmor.aamode.AA_MAY_EXEC,
'w': apparmor.aamode.AA_MAY_WRITE,
'r': apparmor.aamode.AA_MAY_READ,
'a': apparmor.aamode.AA_MAY_APPEND,
'l': apparmor.aamode.AA_MAY_LINK,
'k': apparmor.aamode.AA_MAY_LOCK,
'm': apparmor.aamode.AA_EXEC_MMAP,
'i': apparmor.aamode.AA_EXEC_INHERIT,
'u': apparmor.aamode.AA_EXEC_UNCONFINED | apparmor.aamode.AA_EXEC_UNSAFE, # Unconfined + Unsafe
'U': apparmor.aamode.AA_EXEC_UNCONFINED,
'p': apparmor.aamode.AA_EXEC_PROFILE | apparmor.aamode.AA_EXEC_UNSAFE, # Profile + unsafe
'P': apparmor.aamode.AA_EXEC_PROFILE,
'c': apparmor.aamode.AA_EXEC_CHILD | apparmor.aamode.AA_EXEC_UNSAFE, # Child + Unsafe
'C': apparmor.aamode.AA_EXEC_CHILD,
}
#while MODE_TEST:
# string,mode = MODE_TEST.popitem()
# self.assertEqual(apparmor.aamode.str_to_mode(string), mode)
#self.assertEqual(apparmor.aa.str_to_mode('C'), 2048)
if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.testName']
unittest.main()

View file

@ -0,0 +1,19 @@
# A simple test comment which will persist
#include <tunables/global>
/usr/bin/a/simple/cleanprof/test/profile {
# Just for the heck of it, this comment wont see the day of light
#include <abstractions/base>
#Below rule comes from abstractions/base
allow /usr/share/X11/locale/** r,
allow /home/*/** r,
allow /home/foo/bar r,
allow /home/foo/** w,
}
/usr/bin/other/cleanprof/test/profile {
# This one shouldn't be affected by the processing
# However this comment will be wiped, need to change that
allow /home/*/** rw,
allow /home/foo/bar r,
}

View file

@ -0,0 +1,17 @@
#include <tunables/global>
# A simple test comment which will persist
/usr/bin/a/simple/cleanprof/test/profile {
#include <abstractions/base>
/home/*/** r,
/home/foo/** w,
}
/usr/bin/other/cleanprof/test/profile {
/home/*/** rw,
/home/foo/bar r,
}

41
utils/test/common_test.py Normal file
View file

@ -0,0 +1,41 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import unittest
import re
import sys
sys.path.append('../')
import apparmor.common
import apparmor.config
class Test(unittest.TestCase):
def test_RegexParser(self):
tests = apparmor.config.Config('ini')
tests.CONF_DIR = '.'
regex_tests = tests.read_config('regex_tests.ini')
for regex in regex_tests.sections():
parsed_regex = re.compile(apparmor.common.convert_regexp(regex))
for regex_testcase in regex_tests.options(regex):
self.assertEqual(bool(parsed_regex.search(regex_testcase)), eval(regex_tests[regex][regex_testcase]), 'Incorrectly Parsed regex: %s' %regex)
#def test_readkey(self):
# print("Please press the Y button on the keyboard.")
# self.assertEqual(apparmor.common.readkey().lower(), 'y', 'Error reading key from shell!')
if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.test_RegexParser']
unittest.main()

52
utils/test/config_test.py Normal file
View file

@ -0,0 +1,52 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import unittest
import sys
sys.path.append('../')
import apparmor.config as config
class Test(unittest.TestCase):
def test_IniConfig(self):
ini_config = config.Config('ini')
ini_config.CONF_DIR = '.'
conf = ini_config.read_config('logprof.conf')
logprof_sections = ['settings', 'repository', 'qualifiers', 'required_hats', 'defaulthat', 'globs']
logprof_sections_options = ['profiledir', 'inactive_profiledir', 'logfiles', 'parser', 'ldd', 'logger', 'default_owner_prompt', 'custom_includes']
logprof_settings_parser = '/sbin/apparmor_parser /sbin/subdomain_parser'
self.assertEqual(conf.sections(), logprof_sections)
self.assertEqual(conf.options('settings'), logprof_sections_options)
self.assertEqual(conf['settings']['parser'], logprof_settings_parser)
def test_ShellConfig(self):
shell_config = config.Config('shell')
shell_config.CONF_DIR = '.'
conf = shell_config.read_config('easyprof.conf')
easyprof_sections = ['POLICYGROUPS_DIR', 'TEMPLATES_DIR']
easyprof_Policygroup = '/usr/share/apparmor/easyprof/policygroups'
easyprof_Templates = '/usr/share/apparmor/easyprof/templates'
self.assertEqual(sorted(list(conf[''].keys())), sorted(easyprof_sections))
self.assertEqual(conf['']['POLICYGROUPS_DIR'], easyprof_Policygroup)
self.assertEqual(conf['']['TEMPLATES_DIR'], easyprof_Templates)
if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.testConfig']
unittest.main()

131
utils/test/logprof.conf Normal file
View file

@ -0,0 +1,131 @@
# ------------------------------------------------------------------
#
# Copyright (C) 2004-2006 Novell/SUSE
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#
# ------------------------------------------------------------------
[settings]
profiledir = /etc/apparmor.d /etc/subdomain.d
inactive_profiledir = /usr/share/doc/apparmor-profiles/extras
logfiles = /var/log/audit/audit.log /var/log/syslog /var/log/messages
parser = /sbin/apparmor_parser /sbin/subdomain_parser
ldd = /usr/bin/ldd
logger = /bin/logger /usr/bin/logger
# customize how file ownership permissions are presented
# 0 - off
# 1 - default of what ever mode the log reported
# 2 - force the new permissions to be user
# 3 - force all perms on the rule to be user
default_owner_prompt = 1
# custom directory locations to look for #includes
#
# each name should be a valid directory containing possible #include
# candidate files under the profile dir which by default is /etc/apparmor.d.
#
# So an entry of my-includes will allow /etc/apparmor.d/my-includes to
# be used by the yast UI and profiling tools as a source of #include
# files.
custom_includes =
[repository]
distro = ubuntu-intrepid
url = http://apparmor.test.opensuse.org/backend/api
preferred_user = ubuntu
[qualifiers]
# things will be painfully broken if bash has a profile
/bin/bash = icnu
/bin/ksh = icnu
/bin/dash = icnu
# these programs can't function if they're confined
/bin/mount = u
/etc/init.d/subdomain = u
/sbin/cardmgr = u
/sbin/subdomain_parser = u
/usr/sbin/genprof = u
/usr/sbin/logprof = u
/usr/lib/YaST2/servers_non_y2/ag_genprof = u
/usr/lib/YaST2/servers_non_y2/ag_logprof = u
# these ones shouln't have their own profiles
/bin/awk = icn
/bin/cat = icn
/bin/chmod = icn
/bin/chown = icn
/bin/cp = icn
/bin/gawk = icn
/bin/grep = icn
/bin/gunzip = icn
/bin/gzip = icn
/bin/kill = icn
/bin/ln = icn
/bin/ls = icn
/bin/mkdir = icn
/bin/mv = icn
/bin/readlink = icn
/bin/rm = icn
/bin/sed = icn
/bin/touch = icn
/sbin/killall5 = icn
/usr/bin/find = icn
/usr/bin/killall = icn
/usr/bin/nice = icn
/usr/bin/perl = icn
/usr/bin/tr = icn
[required_hats]
^.+/apache(|2|2-prefork)$ = DEFAULT_URI HANDLING_UNTRUSTED_INPUT
^.+/httpd(|2|2-prefork)$ = DEFAULT_URI HANDLING_UNTRUSTED_INPUT
[defaulthat]
^.+/apache(|2|2-prefork)$ = DEFAULT_URI
^.+/httpd(|2|2-prefork)$ = DEFAULT_URI
[globs]
# /foo/bar/lib/libbaz.so -> /foo/bar/lib/lib*
/lib/lib[^\/]+so[^\/]*$ = /lib/lib*so*
# strip kernel version numbers from kernel module accesses
^/lib/modules/[^\/]+\/ = /lib/modules/*/
# strip pid numbers from /proc accesses
^/proc/\d+/ = /proc/*/
# if it looks like a home directory, glob out the username
^/home/[^\/]+ = /home/*
# if they use any perl modules, grant access to all
^/usr/lib/perl5/.+$ = /usr/lib/perl5/**
# locale foo
^/usr/lib/locale/.+$ = /usr/lib/locale/**
^/usr/share/locale/.+$ = /usr/share/locale/**
# timezone fun
^/usr/share/zoneinfo/.+$ = /usr/share/zoneinfo/**
# /foobar/fonts/baz -> /foobar/fonts/**
/fonts/.+$ = /fonts/**
# turn /foo/bar/baz.8907234 into /foo/bar/baz.*
# BUGBUG - this one looked weird because it would suggest a glob for
# BUGBUG - libfoo.so.5.6.0 that looks like libfoo.so.5.6.*
# \.\d+$ = .*
# some various /etc/security poo -- dunno about these ones...
^/etc/security/_[^\/]+$ = /etc/security/*
^/lib/security/pam_filter/[^\/]+$ = /lib/security/pam_filter/*
^/lib/security/pam_[^\/]+\.so$ = /lib/security/pam_*.so
^/etc/pam.d/[^\/]+$ = /etc/pam.d/*
^/etc/profile.d/[^\/]+\.sh$ = /etc/profile.d/*.sh

View file

@ -0,0 +1,153 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import atexit
import os
import shutil
import subprocess
import sys
import unittest
import filecmp
import apparmor.aa as apparmor
# Path for the program
test_path = '/usr/sbin/ntpd'
# Path for the target file containing profile
local_profilename = './profiles/usr.sbin.ntpd'
python_interpreter = 'python'
if sys.version_info >= (3, 0):
python_interpreter = 'python3'
class Test(unittest.TestCase):
def test_audit(self):
#Set ntpd profile to audit mode and check if it was correctly set
str(subprocess.check_output('%s ./../Tools/aa-audit -d ./profiles %s'%(python_interpreter, test_path), shell=True))
self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'audit', 'Audit flag could not be set in profile %s'%local_profilename)
#Remove audit mode from ntpd profile and check if it was correctly removed
subprocess.check_output('%s ./../Tools/aa-audit -d ./profiles -r %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), None, 'Complain flag could not be removed in profile %s'%local_profilename)
def test_complain(self):
#Set ntpd profile to complain mode and check if it was correctly set
subprocess.check_output('%s ./../Tools/aa-complain -d ./profiles %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), True, 'Failed to create a symlink for %s in force-complain'%local_profilename)
self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'complain', 'Complain flag could not be set in profile %s'%local_profilename)
#Set ntpd profile to enforce mode and check if it was correctly set
subprocess.check_output('%s ./../Tools/aa-complain -d ./profiles -r %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from force-complain'%local_profilename)
self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from disable'%local_profilename)
self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), None, 'Complain flag could not be removed in profile %s'%local_profilename)
# Set audit flag and then complain flag in a profile
subprocess.check_output('%s ./../Tools/aa-audit -d ./profiles %s'%(python_interpreter, test_path), shell=True)
subprocess.check_output('%s ./../Tools/aa-complain -d ./profiles %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), True, 'Failed to create a symlink for %s in force-complain'%local_profilename)
self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'audit,complain', 'Complain flag could not be set in profile %s'%local_profilename)
#Remove complain flag first i.e. set to enforce mode
subprocess.check_output('%s ./../Tools/aa-complain -d ./profiles -r %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from force-complain'%local_profilename)
self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from disable'%local_profilename)
self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'audit', 'Complain flag could not be removed in profile %s'%local_profilename)
#Remove audit flag
subprocess.check_output('%s ./../Tools/aa-audit -d ./profiles -r %s'%(python_interpreter, test_path), shell=True)
def test_enforce(self):
#Set ntpd profile to complain mode and check if it was correctly set
subprocess.check_output('%s ./../Tools/aa-enforce -d ./profiles -r %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), True, 'Failed to create a symlink for %s in force-complain'%local_profilename)
self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), 'complain', 'Complain flag could not be set in profile %s'%local_profilename)
#Set ntpd profile to enforce mode and check if it was correctly set
subprocess.check_output('%s ./../Tools/aa-enforce -d ./profiles %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(os.path.islink('./profiles/force-complain/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from force-complain'%local_profilename)
self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), False, 'Failed to remove symlink for %s from disable'%local_profilename)
self.assertEqual(apparmor.get_profile_flags(local_profilename, test_path), None, 'Complain flag could not be removed in profile %s'%local_profilename)
def test_disable(self):
#Disable the ntpd profile and check if it was correctly disabled
subprocess.check_output('%s ./../Tools/aa-disable -d ./profiles %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), True, 'Failed to create a symlink for %s in disable'%local_profilename)
#Enable the ntpd profile and check if it was correctly re-enabled
subprocess.check_output('%s ./../Tools/aa-disable -d ./profiles -r %s'%(python_interpreter, test_path), shell=True)
self.assertEqual(os.path.islink('./profiles/disable/%s'%os.path.basename(local_profilename)), False, 'Failed to remove a symlink for %s from disable'%local_profilename)
def test_autodep(self):
pass
def test_unconfined(self):
output = subprocess.check_output('%s ./../Tools/aa-unconfined'%python_interpreter, shell=True)
output_force = subprocess.check_output('%s ./../Tools/aa-unconfined --paranoid'%python_interpreter, shell=True)
self.assertIsNot(output, '', 'Failed to run aa-unconfined')
self.assertIsNot(output_force, '', 'Failed to run aa-unconfined in paranoid mode')
def test_cleanprof(self):
input_file = 'cleanprof_test.in'
output_file = 'cleanprof_test.out'
#We position the local testfile
shutil.copy('./%s'%input_file, './profiles')
#Our silly test program whose profile we wish to clean
cleanprof_test = '/usr/bin/a/simple/cleanprof/test/profile'
subprocess.check_output('%s ./../Tools/aa-cleanprof -d ./profiles -s %s' % (python_interpreter, cleanprof_test), shell=True)
#Strip off the first line (#modified line)
subprocess.check_output('sed -i 1d ./profiles/%s'%(input_file), shell=True)
self.assertEqual(filecmp.cmp('./profiles/%s'%input_file, './%s'%output_file, False), True, 'Failed to cleanup profile properly')
def clean_profile_dir():
#Wipe the local profiles from the test directory
shutil.rmtree('./profiles')
if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.testName']
if os.path.exists('./profiles'):
shutil.rmtree('./profiles')
#copy the local profiles to the test directory
#Should be the set of cleanprofile
shutil.copytree('/etc/apparmor.d', './profiles', symlinks=True)
apparmor.profile_dir = './profiles'
atexit.register(clean_profile_dir)
unittest.main()

View file

@ -0,0 +1,51 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
[/foo/**/bar/]
/foo/user/tools/bar/ = True
/foo/apparmor/bar/ = True
/foo/apparmor/bar = False
[/foo/*/bar/]
/foo/apparmor/bar/ = True
/foo/apparmor/tools/bar/ = False
/foo/apparmor/bar = False
[/foo/{foo,bar,user,other}/bar/]
/foo/user/bar/ = True
/foo/bar/bar/ = True
/foo/wrong/bar/ = False
[/foo/{foo,bar,user,other}/test,ca}se/{aa,sd,nd}/bar/]
/foo/user/test,ca}se/aa/bar/ = True
/foo/bar/test,ca}se/sd/bar/ = True
/foo/wrong/user/bar/ = False
/foo/user/wrong/bar/ = False
/foo/wrong/aa/bar/ = False
[/foo/user/ba?/]
/foo/user/bar/ = True
/foo/user/bar/apparmor/ = False
/foo/user/ba/ = False
/foo/user/ba// = False
[/foo/user/bar/**]
/foo/user/bar/apparmor = True
/foo/user/bar/apparmor/tools = True
/foo/user/bar/ = False
[/foo/user/bar/*]
/foo/user/bar/apparmor = True
/foo/user/bar/apparmor/tools = False
/foo/user/bar/ = False
/foo/user/bar/apparmor/ = False

View file

@ -0,0 +1 @@
for file in *.py ; do echo "running $file..." ; python $file; echo; done

View file

@ -0,0 +1 @@
for file in *.py ; do echo "running $file..." ; python3 $file; echo; done

460
utils/test/severity.db Normal file
View file

@ -0,0 +1,460 @@
# ------------------------------------------------------------------
#
# Copyright (C) 2002-2005 Novell/SUSE
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#
# ------------------------------------------------------------------
# Allow this process to 0wn the machine:
CAP_SYS_ADMIN 10
CAP_SYS_CHROOT 10
CAP_SYS_MODULE 10
CAP_SYS_PTRACE 10
CAP_SYS_RAWIO 10
CAP_MAC_ADMIN 10
CAP_MAC_OVERRIDE 10
# Allow other processes to 0wn the machine:
CAP_SETPCAP 9
CAP_SETFCAP 9
CAP_CHOWN 9
CAP_FSETID 9
CAP_MKNOD 9
CAP_LINUX_IMMUTABLE 9
CAP_DAC_OVERRIDE 9
CAP_SETGID 9
CAP_SETUID 9
CAP_FOWNER 9
# Denial of service, bypass audit controls, information leak
CAP_SYS_TIME 8
CAP_NET_ADMIN 8
CAP_SYS_RESOURCE 8
CAP_KILL 8
CAP_IPC_OWNER 8
CAP_SYS_PACCT 8
CAP_SYS_BOOT 8
CAP_NET_BIND_SERVICE 8
CAP_NET_RAW 8
CAP_SYS_NICE 8
CAP_LEASE 8
CAP_IPC_LOCK 8
CAP_SYS_TTY_CONFIG 8
CAP_AUDIT_CONTROL 8
CAP_AUDIT_WRITE 8
CAP_SYSLOG 8
CAP_WAKE_ALARM 8
CAP_BLOCK_SUSPEND 8
CAP_DAC_READ_SEARCH 7
# unused
CAP_NET_BROADCAST 0
# filename r w x
# 'hard drives' are generally 4 10 0
/**/lost+found/** 5 5 0
/boot/** 7 10 0
/etc/passwd* 4 8 0
/etc/group* 4 8 0
/etc/shadow* 7 9 0
/etc/shadow* 7 9 0
/home/*/.ssh/** 7 9 0
/home/*/.gnupg/** 5 7 0
/home/** 4 6 0
/srv/** 4 6 0
/proc/** 6 9 0
/proc/sys/kernel/hotplug 2 10 0
/proc/sys/kernel/modprobe 2 10 0
/proc/kallsyms 7 0 0
/sys/** 4 8 0
/sys/power/state 2 8 0
/sys/firmware/** 2 10 0
/dev/pts/* 8 9 0
/dev/ptmx 8 9 0
/dev/pty* 8 9 0
/dev/null 0 0 0
/dev/adbmouse 3 8 0
/dev/ataraid 9 10 0
/dev/zero 0 0 0
/dev/agpgart* 8 10 0
/dev/aio 3 3 0
/dev/cbd/* 5 5 0
/dev/cciss/* 4 10 0
/dev/capi* 4 6 0
/dev/cfs0 4 10 0
/dev/compaq/* 4 10 0
/dev/cdouble* 4 8 0
/dev/cpu** 5 5 0
/dev/cpu**microcode 1 10 0
/dev/double* 4 8 0
/dev/hd* 4 10 0
/dev/sd* 4 10 0
/dev/ida/* 4 10 0
/dev/input/* 4 8 0
/dev/mapper/control 4 10 0
/dev/*mem 8 10 0
/dev/loop* 4 10 0
/dev/lp* 0 4 0
/dev/md* 4 10 0
/dev/msr 4 10 0
/dev/nb* 4 10 0
/dev/ram* 8 10 0
/dev/rd/* 4 10 0
/dev/*random 3 1 0
/dev/sbpcd* 4 0 0
/dev/rtc 6 0 0
/dev/sd* 4 10 0
/dev/sc* 4 10 0
/dev/sg* 4 10 0
/dev/st* 4 10 0
/dev/snd/* 3 8 0
/dev/usb/mouse* 4 6 0
/dev/usb/hid* 4 6 0
/dev/usb/tty* 4 6 0
/dev/tty* 8 9 0
/dev/stderr 0 0 0
/dev/stdin 0 0 0
/dev/stdout 0 0 0
/dev/ubd* 4 10 0
/dev/usbmouse* 4 6 0
/dev/userdma 8 10 0
/dev/vcs* 8 9 0
/dev/xta* 4 10 0
/dev/zero 0 0 0
/dev/inittcl 8 10 0
/dev/log 5 7 0
/etc/fstab 3 8 0
/etc/mtab 3 5 0
/etc/SuSEconfig/* 1 8 0
/etc/X11/* 2 7 0
/etc/X11/xinit/* 2 8 0
/etc/SuSE-release 1 5 0
/etc/issue* 1 3 0
/etc/motd 1 3 0
/etc/aliases.d/* 1 7 0
/etc/cron* 1 9 0
/etc/cups/* 2 7 0
/etc/default/* 3 8 0
/etc/init.d/* 1 10 0
/etc/permissions.d/* 1 8 0
/etc/ppp/* 2 6 0
/etc/ppp/*secrets 8 6 0
/etc/profile.d/* 1 8 0
/etc/skel/* 0 7 0
/etc/sysconfig/* 4 10 0
/etc/xinetd.d/* 1 9 0
/etc/termcap/* 1 4 0
/etc/ld.so.* 1 9 0
/etc/pam.d/* 3 9 0
/etc/udev/* 3 9 0
/etc/insserv.conf 3 6 0
/etc/security/* 1 9 0
/etc/securetty 0 7 0
/etc/sudoers 4 9 0
/etc/hotplug/* 2 10 0
/etc/xinitd.conf 1 9 0
/etc/gpm/* 2 10 0
/etc/ssl/** 2 7 0
/etc/shadow* 5 9 0
/etc/bash.bashrc 1 9 0
/etc/csh.cshrc 1 9 0
/etc/csh.login 1 9 0
/etc/inittab 1 10 0
/etc/profile* 1 9 0
/etc/shells 1 5 0
/etc/alternatives 1 6 0
/etc/sysctl.conf 3 7 0
/etc/dev.d/* 1 8 0
/etc/manpath.config 1 6 0
/etc/permissions* 1 8 0
/etc/evms.conf 3 8 0
/etc/exports 3 8 0
/etc/samba/* 5 8 0
/etc/ssh/* 3 8 0
/etc/ssh/ssh_host_*key 8 8 0
/etc/krb5.conf 4 8 0
/etc/ntp.conf 3 8 0
/etc/auto.* 3 8 0
/etc/postfix/* 3 7 0
/etc/postfix/*passwd* 6 7 0
/etc/postfix/*cert* 6 7 0
/etc/foomatic/* 3 5 0
/etc/printcap 3 5 0
/etc/youservers 4 9 0
/etc/grub.conf 7 10 0
/etc/modules.conf 4 10 0
/etc/resolv.conf 2 7 0
/etc/apache2/** 3 7 0
/etc/apache2/**ssl** 7 7 0
/etc/subdomain.d/** 6 10 0
/etc/apparmor.d/** 6 10 0
/etc/apparmor/** 6 10 0
/var/log/** 3 8 0
/var/adm/SuSEconfig/** 3 8 0
/var/adm/** 3 7 0
/var/lib/rpm/** 4 8 0
/var/run/nscd/* 3 3 0
/var/run/.nscd_socket 3 3 0
/usr/share/doc/** 1 1 0
/usr/share/man/** 3 5 0
/usr/X11/man/** 3 5 0
/usr/share/info/** 2 4 0
/usr/share/java/** 2 5 0
/usr/share/locale/** 2 4 0
/usr/share/sgml/** 2 4 0
/usr/share/YaST2/** 3 9 0
/usr/share/ghostscript/** 3 5 0
/usr/share/terminfo/** 1 8 0
/usr/share/latex2html/** 2 4 0
/usr/share/cups/** 5 6 0
/usr/share/susehelp/** 2 6 0
/usr/share/susehelp/cgi-bin/** 3 7 7
/usr/share/zoneinfo/** 2 7 0
/usr/share/zsh/** 3 6 0
/usr/share/vim/** 3 8 0
/usr/share/groff/** 3 7 0
/usr/share/vnc/** 3 8 0
/usr/share/wallpapers/** 2 4 0
/usr/X11** 3 8 5
/usr/X11*/bin/XFree86 3 8 8
/usr/X11*/bin/Xorg 3 8 8
/usr/X11*/bin/sux 3 8 8
/usr/X11*/bin/xconsole 3 7 7
/usr/X11*/bin/xhost 3 7 7
/usr/X11*/bin/xauth 3 7 7
/usr/X11*/bin/ethereal 3 6 8
/usr/lib/ooo-** 3 6 5
/usr/lib/lsb/** 2 8 8
/usr/lib/pt_chwon 2 8 5
/usr/lib/tcl** 2 5 3
/usr/lib/lib*so* 3 8 4
/usr/lib/iptables/* 2 8 2
/usr/lib/perl5/** 4 10 6
/usr/lib/gconv/* 4 7 4
/usr/lib/locale/** 4 8 0
/usr/lib/jvm/** 5 7 5
/usr/lib/sasl*/** 5 8 4
/usr/lib/jvm-exports/** 5 7 5
/usr/lib/jvm-private/** 5 7 5
/usr/lib/python*/** 5 7 5
/usr/lib/libkrb5* 4 8 4
/usr/lib/postfix/* 4 7 4
/usr/lib/rpm/** 4 8 6
/usr/lib/rpm/gnupg/** 4 9 0
/usr/lib/apache2** 4 7 4
/usr/lib/mailman/** 4 6 4
/usr/bin/ldd 1 7 4
/usr/bin/netcat 5 7 8
/usr/bin/clear 2 6 3
/usr/bin/reset 2 6 3
/usr/bin/tput 2 6 3
/usr/bin/tset 2 6 3
/usr/bin/file 2 6 3
/usr/bin/ftp 3 7 5
/usr/bin/busybox 4 8 6
/usr/bin/rbash 4 8 5
/usr/bin/screen 3 6 5
/usr/bin/getfacl 3 7 4
/usr/bin/setfacl 3 7 9
/usr/bin/*awk* 3 7 7
/usr/bin/sudo 2 9 10
/usr/bin/lsattr 2 6 5
/usr/bin/chattr 2 7 8
/usr/bin/sed 3 7 6
/usr/bin/grep 2 7 2
/usr/bin/chroot 2 6 10
/usr/bin/dircolors 2 9 3
/usr/bin/cut 2 7 2
/usr/bin/du 2 7 3
/usr/bin/env 2 7 2
/usr/bin/head 2 7 2
/usr/bin/tail 2 7 2
/usr/bin/install 2 8 4
/usr/bin/link 2 6 4
/usr/bin/logname 2 6 2
/usr/bin/md5sum 2 8 3
/usr/bin/mkfifo 2 6 10
/usr/bin/nice 2 7 7
/usr/bin/nohup 2 7 7
/usr/bin/printf 2 7 1
/usr/bin/readlink 2 7 3
/usr/bin/seq 2 7 1
/usr/bin/sha1sum 2 8 3
/usr/bin/shred 2 7 3
/usr/bin/sort 2 7 3
/usr/bin/split 2 7 3
/usr/bin/stat 2 7 4
/usr/bin/sum 2 8 3
/usr/bin/tac 2 7 3
/usr/bin/tail 3 8 4
/usr/bin/tee 2 7 3
/usr/bin/test 2 8 4
/usr/bin/touch 2 7 3
/usr/bin/tr 2 8 3
/usr/bin/tsort 2 7 3
/usr/bin/tty 2 7 3
/usr/bin/unexpand 2 7 3
/usr/bin/uniq 2 7 3
/usr/bin/unlink 2 8 4
/usr/bin/uptime 2 7 3
/usr/bin/users 2 8 4
/usr/bin/vdir 2 8 4
/usr/bin/wc 2 7 3
/usr/bin/who 2 8 4
/usr/bin/whoami 2 8 4
/usr/bin/yes 1 6 1
/usr/bin/ed 2 7 5
/usr/bin/red 2 7 4
/usr/bin/find 2 8 5
/usr/bin/xargs 2 7 5
/usr/bin/ispell 2 7 4
/usr/bin/a2p 2 7 5
/usr/bin/perlcc 2 7 5
/usr/bin/perldoc 2 7 5
/usr/bin/pod2* 2 7 5
/usr/bin/prove 2 7 5
/usr/bin/perl 2 10 7
/usr/bin/perl* 2 10 7
/usr/bin/suidperl 2 8 8
/usr/bin/csh 2 8 8
/usr/bin/tcsh 2 8 8
/usr/bin/tree 2 6 5
/usr/bin/last 2 7 5
/usr/bin/lastb 2 7 5
/usr/bin/utmpdump 2 6 5
/usr/bin/alsamixer 2 6 8
/usr/bin/amixer 2 6 8
/usr/bin/amidi 2 6 8
/usr/bin/aoss 2 6 8
/usr/bin/aplay 2 6 8
/usr/bin/aplaymidi 2 6 8
/usr/bin/arecord 2 6 8
/usr/bin/arecordmidi 2 6 8
/usr/bin/aseqnet 2 6 8
/usr/bin/aserver 2 6 8
/usr/bin/iecset 2 6 8
/usr/bin/rview 2 6 5
/usr/bin/ex 2 7 5
/usr/bin/enscript 2 6 5
/usr/bin/genscript 2 6 5
/usr/bin/xdelta 2 6 5
/usr/bin/edit 2 6 5
/usr/bin/vimtutor 2 6 5
/usr/bin/rvim 2 6 5
/usr/bin/vim 2 8 7
/usr/bin/vimdiff 2 8 7
/usr/bin/aspell 2 6 5
/usr/bin/xxd 2 6 5
/usr/bin/spell 2 6 5
/usr/bin/eqn 2 6 5
/usr/bin/eqn2graph 2 6 5
/usr/bin/word-list-compress 2 6 4
/usr/bin/afmtodit 2 6 4
/usr/bin/hpf2dit 2 6 4
/usr/bin/geqn 2 6 4
/usr/bin/grn 2 6 4
/usr/bin/grodvi 2 6 4
/usr/bin/groff 2 6 5
/usr/bin/groffer 2 6 4
/usr/bin/grolj4 2 6 4
/usr/bin/grotty 2 6 4
/usr/bin/gtbl 2 6 4
/usr/bin/pic2graph 2 6 4
/usr/bin/indxbib 2 6 4
/usr/bin/lkbib 2 6 4
/usr/bin/lookbib 2 6 4
/usr/bin/mmroff 2 6 4
/usr/bin/neqn 2 6 4
/usr/bin/pfbtops 2 6 4
/usr/bin/pic 2 6 4
/usr/bin/tfmtodit 2 6 4
/usr/bin/tbl 2 6 4
/usr/bin/post-grohtml 2 6 4
/usr/bin/pre-grohtml 2 6 4
/usr/bin/refer 2 6 4
/usr/bin/soelim 2 6 4
/usr/bin/disable-paste 2 6 6
/usr/bin/troff 2 6 4
/usr/bin/strace-graph 2 6 4
/usr/bin/gpm-root 2 6 7
/usr/bin/hltest 2 6 7
/usr/bin/mev 2 6 6
/usr/bin/mouse-test 2 6 6
/usr/bin/strace 2 8 9
/usr/bin/scsiformat 2 7 10
/usr/bin/lsscsi 2 7 7
/usr/bin/scsiinfo 2 7 7
/usr/bin/sg_* 2 7 7
/usr/bin/build-classpath 2 6 6
/usr/bin/build-classpath-directory 2 6 6
/usr/bin/build-jar-repository 2 6 6
/usr/bin/diff-jars 2 6 6
/usr/bin/jvmjar 2 6 6
/usr/bin/rebuild-jar-repository 2 6 6
/usr/bin/scriptreplay 2 6 5
/usr/bin/cal 2 6 3
/usr/bin/chkdupexe 2 6 5
/usr/bin/col 2 6 4
/usr/bin/colcrt 2 6 4
/usr/bin/colrm 2 6 3
/usr/bin/column 2 6 4
/usr/bin/cytune 2 6 6
/usr/bin/ddate 2 6 3
/usr/bin/fdformat 2 6 6
/usr/bin/getopt 2 8 6
/usr/bin/hexdump 2 6 4
/usr/bin/hostid 2 6 4
/usr/bin/ipcrm 2 7 7
/usr/bin/ipcs 2 7 6
/usr/bin/isosize 2 6 4
/usr/bin/line 2 6 4
/usr/bin/look 2 6 5
/usr/bin/mcookie 2 7 5
/usr/bin/mesg 2 6 4
/usr/bin/namei 2 6 5
/usr/bin/rename 2 6 5
/usr/bin/renice 2 6 7
/usr/bin/rev 2 6 5
/usr/bin/script 2 6 6
/usr/bin/ChangeSymlinks 2 8 8
/usr/bin/setfdprm 2 6 7
/usr/bin/setsid 2 6 3
/usr/bin/setterm 2 6 5
/usr/bin/tailf 2 6 4
/usr/bin/time 2 6 4
/usr/bin/ul 2 6 4
/usr/bin/wall 2 6 5
/usr/bin/whereis 2 6 4
/usr/bin/which 2 6 3
/usr/bin/c_rehash 2 7 6
/usr/bin/openssl 2 8 6
/usr/bin/lsdev 2 6 5
/usr/bin/procinfo 2 6 5
/usr/bin/socklist 2 6 5
/usr/bin/filesize 2 6 3
/usr/bin/linkto 2 6 3
/usr/bin/mkinfodir 2 6 5
/usr/bin/old 2 6 4
/usr/bin/rpmlocate 2 6 5
/usr/bin/safe-rm 2 8 6
/usr/bin/safe-rmdir 2 8 6
/usr/bin/setJava 2 6 1
/usr/bin/vmstat 2 6 4
/usr/bin/top 2 6 6
/usr/bin/pinentry* 2 7 6
/usr/bin/free 2 8 4
/usr/bin/pmap 2 6 5
/usr/bin/slabtop 2 6 4
/usr/bin/tload 2 6 4
/usr/bin/watch 2 6 3
/usr/bin/w 2 6 4
/usr/bin/pstree.x11 2 6 4
/usr/bin/pstree 2 6 4
/usr/bin/snice 2 6 6
/usr/bin/skill 2 6 7
/usr/bin/pgrep 2 6 4
/usr/bin/killall 2 6 7
/usr/bin/curl 2 7 7
/usr/bin/slptool 2 7 8
/usr/bin/ldap* 2 7 7
/usr/bin/whatis 2 7 5

View file

@ -0,0 +1,460 @@
# ------------------------------------------------------------------
#
# Copyright (C) 2002-2005 Novell/SUSE
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#
# ------------------------------------------------------------------
# Allow this process to 0wn the machine:
CAP_SYS_ADMIN 10
CAP_SYS_CHROOT 10
CAP_SYS_MODULE
CAP_SYS_PTRACE 10
CAP_SYS_RAWIO 10
CAP_MAC_ADMIN 10
CAP_MAC_OVERRIDE 10
# Allow other processes to 0wn the machine:
CAP_SETPCAP 9
CAP_SETFCAP 9
CAP_CHOWN 9
CAP_FSETID 9
CAP_MKNOD 9
CAP_LINUX_IMMUTABLE 9
CAP_DAC_OVERRIDE 9
CAP_SETGID 9
CAP_SETUID 9
CAP_FOWNER 9
# Denial of service, bypass audit controls, information leak
CAP_SYS_TIME 8
CAP_NET_ADMIN 8
CAP_SYS_RESOURCE 8
CAP_KILL 8
CAP_IPC_OWNER 8
CAP_SYS_PACCT 8
CAP_SYS_BOOT 8
CAP_NET_BIND_SERVICE 8
CAP_NET_RAW 8
CAP_SYS_NICE 8
CAP_LEASE 8
CAP_IPC_LOCK 8
CAP_SYS_TTY_CONFIG 8
CAP_AUDIT_CONTROL 8
CAP_AUDIT_WRITE 8
CAP_SYSLOG 8
CAP_WAKE_ALARM 8
CAP_BLOCK_SUSPEND 8
CAP_DAC_READ_SEARCH 7
# unused
CAP_NET_BROADCAST 0
# filename r w x
# 'hard drives' are generally 4 10 0
/**/lost+found/** 5 5 0
/boot/** 7 10 0
/etc/passwd* 4 8 0
/etc/group* 4 8 0
/etc/shadow* 7 9 0
/etc/shadow* 7 9 0
/home/*/.ssh/** 7 9 0
/home/*/.gnupg/** 5 7 0
/home/** 4 6 0
/srv/** 4 6 0
/proc/** 6 9 0
/proc/sys/kernel/hotplug 2 10 0
/proc/sys/kernel/modprobe 2 10 0
/proc/kallsyms 7 0 0
/sys/** 4 8 0
/sys/power/state 2 8 0
/sys/firmware/** 2 10 0
/dev/pts/* 8 9 0
/dev/ptmx 8 9 0
/dev/pty* 8 9 0
/dev/null 0 0 0
/dev/adbmouse 3 8 0
/dev/ataraid 9 10 0
/dev/zero 0 0 0
/dev/agpgart* 8 10 0
/dev/aio 3 3 0
/dev/cbd/* 5 5 0
/dev/cciss/* 4 10 0
/dev/capi* 4 6 0
/dev/cfs0 4 10 0
/dev/compaq/* 4 10 0
/dev/cdouble* 4 8 0
/dev/cpu** 5 5 0
/dev/cpu**microcode 1 10 0
/dev/double* 4 8 0
/dev/hd* 4 10 0
/dev/sd* 4 10 0
/dev/ida/* 4 10 0
/dev/input/* 4 8 0
/dev/mapper/control 4 10 0
/dev/*mem 8 10 0
/dev/loop* 4 10 0
/dev/lp* 0 4 0
/dev/md* 4 10 0
/dev/msr 4 10 0
/dev/nb* 4 10 0
/dev/ram* 8 10 0
/dev/rd/* 4 10 0
/dev/*random 3 1 0
/dev/sbpcd* 4 0 0
/dev/rtc 6 0 0
/dev/sd* 4 10 0
/dev/sc* 4 10 0
/dev/sg* 4 10 0
/dev/st* 4 10 0
/dev/snd/* 3 8 0
/dev/usb/mouse* 4 6 0
/dev/usb/hid* 4 6 0
/dev/usb/tty* 4 6 0
/dev/tty* 8 9 0
/dev/stderr 0 0 0
/dev/stdin 0 0 0
/dev/stdout 0 0 0
/dev/ubd* 4 10 0
/dev/usbmouse* 4 6 0
/dev/userdma 8 10 0
/dev/vcs* 8 9 0
/dev/xta* 4 10 0
/dev/zero 0 0 0
/dev/inittcl 8 10 0
/dev/log 5 7 0
/etc/fstab 3 8 0
/etc/mtab 3 5 0
/etc/SuSEconfig/* 1 8 0
/etc/X11/* 2 7 0
/etc/X11/xinit/* 2 8 0
/etc/SuSE-release 1 5 0
/etc/issue* 1 3 0
/etc/motd 1 3 0
/etc/aliases.d/* 1 7 0
/etc/cron* 1 9 0
/etc/cups/* 2 7 0
/etc/default/* 3 8 0
/etc/init.d/* 1 10 0
/etc/permissions.d/* 1 8 0
/etc/ppp/* 2 6 0
/etc/ppp/*secrets 8 6 0
/etc/profile.d/* 1 8 0
/etc/skel/* 0 7 0
/etc/sysconfig/* 4 10 0
/etc/xinetd.d/* 1 9 0
/etc/termcap/* 1 4 0
/etc/ld.so.* 1 9 0
/etc/pam.d/* 3 9 0
/etc/udev/* 3 9 0
/etc/insserv.conf 3 6 0
/etc/security/* 1 9 0
/etc/securetty 0 7 0
/etc/sudoers 4 9 0
/etc/hotplug/* 2 10 0
/etc/xinitd.conf 1 9 0
/etc/gpm/* 2 10 0
/etc/ssl/** 2 7 0
/etc/shadow* 5 9 0
/etc/bash.bashrc 1 9 0
/etc/csh.cshrc 1 9 0
/etc/csh.login 1 9 0
/etc/inittab 1 10 0
/etc/profile* 1 9 0
/etc/shells 1 5 0
/etc/alternatives 1 6 0
/etc/sysctl.conf 3 7 0
/etc/dev.d/* 1 8 0
/etc/manpath.config 1 6 0
/etc/permissions* 1 8 0
/etc/evms.conf 3 8 0
/etc/exports 3 8 0
/etc/samba/* 5 8 0
/etc/ssh/* 3 8 0
/etc/ssh/ssh_host_*key 8 8 0
/etc/krb5.conf 4 8 0
/etc/ntp.conf 3 8 0
/etc/auto.* 3 8 0
/etc/postfix/* 3 7 0
/etc/postfix/*passwd* 6 7 0
/etc/postfix/*cert* 6 7 0
/etc/foomatic/* 3 5 0
/etc/printcap 3 5 0
/etc/youservers 4 9 0
/etc/grub.conf 7 10 0
/etc/modules.conf 4 10 0
/etc/resolv.conf 2 7 0
/etc/apache2/** 3 7 0
/etc/apache2/**ssl** 7 7 0
/etc/subdomain.d/** 6 10 0
/etc/apparmor.d/** 6 10 0
/etc/apparmor/** 6 10 0
/var/log/** 3 8 0
/var/adm/SuSEconfig/** 3 8 0
/var/adm/** 3 7 0
/var/lib/rpm/** 4 8 0
/var/run/nscd/* 3 3 0
/var/run/.nscd_socket 3 3 0
/usr/share/doc/** 1 1 0
/usr/share/man/** 3 5 0
/usr/X11/man/** 3 5 0
/usr/share/info/** 2 4 0
/usr/share/java/** 2 5 0
/usr/share/locale/** 2 4 0
/usr/share/sgml/** 2 4 0
/usr/share/YaST2/** 3 9 0
/usr/share/ghostscript/** 3 5 0
/usr/share/terminfo/** 1 8 0
/usr/share/latex2html/** 2 4 0
/usr/share/cups/** 5 6 0
/usr/share/susehelp/** 2 6 0
/usr/share/susehelp/cgi-bin/** 3 7 7
/usr/share/zoneinfo/** 2 7 0
/usr/share/zsh/** 3 6 0
/usr/share/vim/** 3 8 0
/usr/share/groff/** 3 7 0
/usr/share/vnc/** 3 8 0
/usr/share/wallpapers/** 2 4 0
/usr/X11** 3 8 5
/usr/X11*/bin/XFree86 3 8 8
/usr/X11*/bin/Xorg 3 8 8
/usr/X11*/bin/sux 3 8 8
/usr/X11*/bin/xconsole 3 7 7
/usr/X11*/bin/xhost 3 7 7
/usr/X11*/bin/xauth 3 7 7
/usr/X11*/bin/ethereal 3 6 8
/usr/lib/ooo-** 3 6 5
/usr/lib/lsb/** 2 8 8
/usr/lib/pt_chwon 2 8 5
/usr/lib/tcl** 2 5 3
/usr/lib/lib*so* 3 8 4
/usr/lib/iptables/* 2 8 2
/usr/lib/perl5/** 4 10 6
/usr/lib/gconv/* 4 7 4
/usr/lib/locale/** 4 8 0
/usr/lib/jvm/** 5 7 5
/usr/lib/sasl*/** 5 8 4
/usr/lib/jvm-exports/** 5 7 5
/usr/lib/jvm-private/** 5 7 5
/usr/lib/python*/** 5 7 5
/usr/lib/libkrb5* 4 8 4
/usr/lib/postfix/* 4 7 4
/usr/lib/rpm/** 4 8 6
/usr/lib/rpm/gnupg/** 4 9 0
/usr/lib/apache2** 4 7 4
/usr/lib/mailman/** 4 6 4
/usr/bin/ldd 1 7 4
/usr/bin/netcat 5 7 8
/usr/bin/clear 2 6 3
/usr/bin/reset 2 6 3
/usr/bin/tput 2 6 3
/usr/bin/tset 2 6 3
/usr/bin/file 2 6 3
/usr/bin/ftp 3 7 5
/usr/bin/busybox 4 8 6
/usr/bin/rbash 4 8 5
/usr/bin/screen 3 6 5
/usr/bin/getfacl 3 7 4
/usr/bin/setfacl 3 7 9
/usr/bin/*awk* 3 7 7
/usr/bin/sudo 2 9 10
/usr/bin/lsattr 2 6 5
/usr/bin/chattr 2 7 8
/usr/bin/sed 3 7 6
/usr/bin/grep 2 7 2
/usr/bin/chroot 2 6 10
/usr/bin/dircolors 2 9 3
/usr/bin/cut 2 7 2
/usr/bin/du 2 7 3
/usr/bin/env 2 7 2
/usr/bin/head 2 7 2
/usr/bin/tail 2 7 2
/usr/bin/install 2 8 4
/usr/bin/link 2 6 4
/usr/bin/logname 2 6 2
/usr/bin/md5sum 2 8 3
/usr/bin/mkfifo 2 6 10
/usr/bin/nice 2 7 7
/usr/bin/nohup 2 7 7
/usr/bin/printf 2 7 1
/usr/bin/readlink 2 7 3
/usr/bin/seq 2 7 1
/usr/bin/sha1sum 2 8 3
/usr/bin/shred 2 7 3
/usr/bin/sort 2 7 3
/usr/bin/split 2 7 3
/usr/bin/stat 2 7 4
/usr/bin/sum 2 8 3
/usr/bin/tac 2 7 3
/usr/bin/tail 3 8 4
/usr/bin/tee 2 7 3
/usr/bin/test 2 8 4
/usr/bin/touch 2 7 3
/usr/bin/tr 2 8 3
/usr/bin/tsort 2 7 3
/usr/bin/tty 2 7 3
/usr/bin/unexpand 2 7 3
/usr/bin/uniq 2 7 3
/usr/bin/unlink 2 8 4
/usr/bin/uptime 2 7 3
/usr/bin/users 2 8 4
/usr/bin/vdir 2 8 4
/usr/bin/wc 2 7 3
/usr/bin/who 2 8 4
/usr/bin/whoami 2 8 4
/usr/bin/yes 1 6 1
/usr/bin/ed 2 7 5
/usr/bin/red 2 7 4
/usr/bin/find 2 8 5
/usr/bin/xargs 2 7 5
/usr/bin/ispell 2 7 4
/usr/bin/a2p 2 7 5
/usr/bin/perlcc 2 7 5
/usr/bin/perldoc 2 7 5
/usr/bin/pod2* 2 7 5
/usr/bin/prove 2 7 5
/usr/bin/perl 2 10 7
/usr/bin/perl* 2 10 7
/usr/bin/suidperl 2 8 8
/usr/bin/csh 2 8 8
/usr/bin/tcsh 2 8 8
/usr/bin/tree 2 6 5
/usr/bin/last 2 7 5
/usr/bin/lastb 2 7 5
/usr/bin/utmpdump 2 6 5
/usr/bin/alsamixer 2 6 8
/usr/bin/amixer 2 6 8
/usr/bin/amidi 2 6 8
/usr/bin/aoss 2 6 8
/usr/bin/aplay 2 6 8
/usr/bin/aplaymidi 2 6 8
/usr/bin/arecord 2 6 8
/usr/bin/arecordmidi 2 6 8
/usr/bin/aseqnet 2 6 8
/usr/bin/aserver 2 6 8
/usr/bin/iecset 2 6 8
/usr/bin/rview 2 6 5
/usr/bin/ex 2 7 5
/usr/bin/enscript 2 6 5
/usr/bin/genscript 2 6 5
/usr/bin/xdelta 2 6 5
/usr/bin/edit 2 6 5
/usr/bin/vimtutor 2 6 5
/usr/bin/rvim 2 6 5
/usr/bin/vim 2 8 7
/usr/bin/vimdiff 2 8 7
/usr/bin/aspell 2 6 5
/usr/bin/xxd 2 6 5
/usr/bin/spell 2 6 5
/usr/bin/eqn 2 6 5
/usr/bin/eqn2graph 2 6 5
/usr/bin/word-list-compress 2 6 4
/usr/bin/afmtodit 2 6 4
/usr/bin/hpf2dit 2 6 4
/usr/bin/geqn 2 6 4
/usr/bin/grn 2 6 4
/usr/bin/grodvi 2 6 4
/usr/bin/groff 2 6 5
/usr/bin/groffer 2 6 4
/usr/bin/grolj4 2 6 4
/usr/bin/grotty 2 6 4
/usr/bin/gtbl 2 6 4
/usr/bin/pic2graph 2 6 4
/usr/bin/indxbib 2 6 4
/usr/bin/lkbib 2 6 4
/usr/bin/lookbib 2 6 4
/usr/bin/mmroff 2 6 4
/usr/bin/neqn 2 6 4
/usr/bin/pfbtops 2 6 4
/usr/bin/pic 2 6 4
/usr/bin/tfmtodit 2 6 4
/usr/bin/tbl 2 6 4
/usr/bin/post-grohtml 2 6 4
/usr/bin/pre-grohtml 2 6 4
/usr/bin/refer 2 6 4
/usr/bin/soelim 2 6 4
/usr/bin/disable-paste 2 6 6
/usr/bin/troff 2 6 4
/usr/bin/strace-graph 2 6 4
/usr/bin/gpm-root 2 6 7
/usr/bin/hltest 2 6 7
/usr/bin/mev 2 6 6
/usr/bin/mouse-test 2 6 6
/usr/bin/strace 2 8 9
/usr/bin/scsiformat 2 7 10
/usr/bin/lsscsi 2 7 7
/usr/bin/scsiinfo 2 7 7
/usr/bin/sg_* 2 7 7
/usr/bin/build-classpath 2 6 6
/usr/bin/build-classpath-directory 2 6 6
/usr/bin/build-jar-repository 2 6 6
/usr/bin/diff-jars 2 6 6
/usr/bin/jvmjar 2 6 6
/usr/bin/rebuild-jar-repository 2 6 6
/usr/bin/scriptreplay 2 6 5
/usr/bin/cal 2 6 3
/usr/bin/chkdupexe 2 6 5
/usr/bin/col 2 6 4
/usr/bin/colcrt 2 6 4
/usr/bin/colrm 2 6 3
/usr/bin/column 2 6 4
/usr/bin/cytune 2 6 6
/usr/bin/ddate 2 6 3
/usr/bin/fdformat 2 6 6
/usr/bin/getopt 2 8 6
/usr/bin/hexdump 2 6 4
/usr/bin/hostid 2 6 4
/usr/bin/ipcrm 2 7 7
/usr/bin/ipcs 2 7 6
/usr/bin/isosize 2 6 4
/usr/bin/line 2 6 4
/usr/bin/look 2 6 5
/usr/bin/mcookie 2 7 5
/usr/bin/mesg 2 6 4
/usr/bin/namei 2 6 5
/usr/bin/rename 2 6 5
/usr/bin/renice 2 6 7
/usr/bin/rev 2 6 5
/usr/bin/script 2 6 6
/usr/bin/ChangeSymlinks 2 8 8
/usr/bin/setfdprm 2 6 7
/usr/bin/setsid 2 6 3
/usr/bin/setterm 2 6 5
/usr/bin/tailf 2 6 4
/usr/bin/time 2 6 4
/usr/bin/ul 2 6 4
/usr/bin/wall 2 6 5
/usr/bin/whereis 2 6 4
/usr/bin/which 2 6 3
/usr/bin/c_rehash 2 7 6
/usr/bin/openssl 2 8 6
/usr/bin/lsdev 2 6 5
/usr/bin/procinfo 2 6 5
/usr/bin/socklist 2 6 5
/usr/bin/filesize 2 6 3
/usr/bin/linkto 2 6 3
/usr/bin/mkinfodir 2 6 5
/usr/bin/old 2 6 4
/usr/bin/rpmlocate 2 6 5
/usr/bin/safe-rm 2 8 6
/usr/bin/safe-rmdir 2 8 6
/usr/bin/setJava 2 6 1
/usr/bin/vmstat 2 6 4
/usr/bin/top 2 6 6
/usr/bin/pinentry* 2 7 6
/usr/bin/free 2 8 4
/usr/bin/pmap 2 6 5
/usr/bin/slabtop 2 6 4
/usr/bin/tload 2 6 4
/usr/bin/watch 2 6 3
/usr/bin/w 2 6 4
/usr/bin/pstree.x11 2 6 4
/usr/bin/pstree 2 6 4
/usr/bin/snice 2 6 6
/usr/bin/skill 2 6 7
/usr/bin/pgrep 2 6 4
/usr/bin/killall 2 6 7
/usr/bin/curl 2 7 7
/usr/bin/slptool 2 7 8
/usr/bin/ldap* 2 7 7
/usr/bin/whatis 2 7 5

View file

@ -0,0 +1,90 @@
# ----------------------------------------------------------------------
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# ----------------------------------------------------------------------
import os
import shutil
import sys
import unittest
sys.path.append('../')
import apparmor.severity as severity
from apparmor.common import AppArmorException
class Test(unittest.TestCase):
def setUp(self):
#copy the local profiles to the test directory
if os.path.exists('./profiles'):
shutil.rmtree('./profiles')
shutil.copytree('/etc/apparmor.d/', './profiles/', symlinks=True)
def tearDown(self):
#Wipe the local profiles from the test directory
shutil.rmtree('./profiles')
def testRank_Test(self):
sev_db = severity.Severity('severity.db')
rank = sev_db.rank('/usr/bin/whatis', 'x')
self.assertEqual(rank, 5, 'Wrong rank')
rank = sev_db.rank('/etc', 'x')
self.assertEqual(rank, 10, 'Wrong rank')
rank = sev_db.rank('/dev/doublehit', 'x')
self.assertEqual(rank, 0, 'Wrong rank')
rank = sev_db.rank('/dev/doublehit', 'rx')
self.assertEqual(rank, 4, 'Wrong rank')
rank = sev_db.rank('/dev/doublehit', 'rwx')
self.assertEqual(rank, 8, 'Wrong rank')
rank = sev_db.rank('/dev/tty10', 'rwx')
self.assertEqual(rank, 9, 'Wrong rank')
rank = sev_db.rank('/var/adm/foo/**', 'rx')
self.assertEqual(rank, 3, 'Wrong rank')
rank = sev_db.rank('CAP_KILL')
self.assertEqual(rank, 8, 'Wrong rank')
rank = sev_db.rank('CAP_SETPCAP')
self.assertEqual(rank, 9, 'Wrong rank')
self.assertEqual(sev_db.rank('/etc/apparmor/**', 'r') , 6, 'Invalid Rank')
self.assertEqual(sev_db.rank('/etc/**', 'r') , 10, 'Invalid Rank')
# Load all variables for /sbin/klogd and test them
sev_db.load_variables('profiles/sbin.klogd')
self.assertEqual(sev_db.rank('@{PROC}/sys/vm/overcommit_memory', 'r'), 6, 'Invalid Rank')
self.assertEqual(sev_db.rank('@{HOME}/sys/@{PROC}/overcommit_memory', 'r'), 10, 'Invalid Rank')
self.assertEqual(sev_db.rank('/overco@{multiarch}mmit_memory', 'r'), 10, 'Invalid Rank')
sev_db.unload_variables()
sev_db.load_variables('profiles/usr.sbin.dnsmasq')
self.assertEqual(sev_db.rank('@{PROC}/sys/@{TFTP_DIR}/overcommit_memory', 'r'), 6, 'Invalid Rank')
self.assertEqual(sev_db.rank('@{PROC}/sys/vm/overcommit_memory', 'r'), 6, 'Invalid Rank')
self.assertEqual(sev_db.rank('@{HOME}/sys/@{PROC}/overcommit_memory', 'r'), 10, 'Invalid Rank')
self.assertEqual(sev_db.rank('/overco@{multiarch}mmit_memory', 'r'), 10, 'Invalid Rank')
#self.assertEqual(sev_db.rank('/proc/@{PID}/maps', 'rw'), 9, 'Invalid Rank')
def testInvalid(self):
sev_db = severity.Severity('severity.db')
rank = sev_db.rank('/dev/doublehit', 'i')
self.assertEqual(rank, 10, 'Wrong')
try:
severity.Severity('severity_broken.db')
except AppArmorException:
pass
rank = sev_db.rank('CAP_UNKOWN')
rank = sev_db.rank('CAP_K*')
if __name__ == "__main__":
#import sys;sys.argv = ['', 'Test.testName']
unittest.main()