diff --git a/utils/Makefile b/utils/Makefile index 504285945..0618a18d9 100644 --- a/utils/Makefile +++ b/utils/Makefile @@ -32,8 +32,10 @@ PERLTOOLS = aa-genprof aa-logprof aa-autodep aa-audit aa-complain aa-enforce \ TOOLS = ${PERLTOOLS} aa-decode aa-status MODULES = ${MODDIR}/AppArmor.pm ${MODDIR}/Repository.pm \ ${MODDIR}/Config.pm ${MODDIR}/Severity.pm +PYTOOLS = aa-easyprof +PYSETUP = python-tools-setup.py -MANPAGES = ${TOOLS:=.8} logprof.conf.5 +MANPAGES = ${TOOLS:=.8} logprof.conf.5 ${PYTOOLS:=.8} all: ${MANPAGES} ${HTMLMANPAGES} $(MAKE) -C po all @@ -45,9 +47,10 @@ BINDIR=${DESTDIR}/usr/sbin CONFDIR=${DESTDIR}/etc/apparmor VENDOR_PERL=$(shell perl -e 'use Config; print $$Config{"vendorlib"};') PERLDIR=${DESTDIR}${VENDOR_PERL}/${MODDIR} +PYPREFIX=/usr -po/${NAME}.pot: ${TOOLS} - $(MAKE) -C po ${NAME}.pot NAME=${NAME} SOURCES="${TOOLS} ${MODULES}" +po/${NAME}.pot: ${TOOLS} ${PYTOOLS} + $(MAKE) -C po ${NAME}.pot NAME=${NAME} SOURCES="${TOOLS} ${MODULES} ${PYTOOLS}" .PHONY: install install: ${MANPAGES} ${HTMLMANPAGES} @@ -62,6 +65,7 @@ install: ${MANPAGES} ${HTMLMANPAGES} $(MAKE) install_manpages DESTDIR=${DESTDIR} $(MAKE) -C vim install DESTDIR=${DESTDIR} ln -sf aa-status.8 ${DESTDIR}/${MANDIR}/man8/apparmor_status.8 + python ${PYSETUP} install --prefix=${PYPREFIX} --root=${DESTDIR} --version=${VERSION} .PHONY: clean ifndef VERBOSE @@ -72,6 +76,8 @@ clean: _clean rm -f Make.rules $(MAKE) -C po clean $(MAKE) -C vim clean + rm -rf staging/ build/ + rm -f apparmor/*.pyc # ${CAPABILITIES} is defined in common/Make.rules .PHONY: check_severity_db @@ -92,3 +98,13 @@ check: check_severity_db for i in ${MODULES} ${PERLTOOLS} ; do \ perl -c $$i || exit 1; \ done + tmpfile=$$(mktemp --tmpdir aa-pyflakes-XXXXXX); \ + for i in ${PYTOOLS} apparmor aa-status test/*.py; do \ + echo Checking $$i; \ + pyflakes $$i 2>&1 | grep -v "undefined name '_'" > $$tmpfile; \ + test -s $$tmpfile && cat $$tmpfile && rm -f $$tmpfile && exit 1; \ + done || true; \ + rm -f $$tmpfile + for i in test/* ; do \ + python $$i || exit 1; \ + done diff --git a/utils/aa-easyprof b/utils/aa-easyprof new file mode 100644 index 000000000..44e9aaa69 --- /dev/null +++ b/utils/aa-easyprof @@ -0,0 +1,65 @@ +#! /usr/bin/env python +# ------------------------------------------------------------------ +# +# Copyright (C) 2011-2012 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 apparmor.easyprof +from apparmor.easyprof import AppArmorException, error +import os +import sys + +if __name__ == "__main__": + def usage(): + '''Return usage information''' + return 'USAGE: %s [options] ' % \ + os.path.basename(sys.argv[0]) + + (opt, args) = apparmor.easyprof.parse_args() + binary = None + + m = usage() + if opt.show_policy_group and not opt.policy_groups: + error("Must specify -p with --show-policy-group") + elif not opt.template and not opt.policy_groups and len(args) < 1: + error("Must specify full path to binary\n%s" % m) + + binary = None + if len(args) >= 1: + binary = args[0] + + try: + easyp = apparmor.easyprof.AppArmorEasyProfile(binary, opt) + except AppArmorException, e: + error(e.value) + except Exception: + raise + + if opt.list_templates: + apparmor.easyprof.print_basefilenames(easyp.get_templates()) + sys.exit(0) + elif opt.template and opt.show_template: + files = [os.path.join(easyp.dirs['templates'], opt.template)] + apparmor.easyprof.print_files(files) + sys.exit(0) + elif opt.list_policy_groups: + apparmor.easyprof.print_basefilenames(easyp.get_policy_groups()) + sys.exit(0) + elif opt.policy_groups and opt.show_policy_group: + for g in opt.policy_groups.split(','): + files = [os.path.join(easyp.dirs['policygroups'], g)] + apparmor.easyprof.print_files(files) + sys.exit(0) + elif binary == None: + error("Must specify full path to binary\n%s" % m) + + # if we made it here, generate a profile + params = apparmor.easyprof.gen_policy_params(binary, opt) + p = easyp.gen_policy(**params) + print p, + diff --git a/utils/aa-easyprof.pod b/utils/aa-easyprof.pod new file mode 100644 index 000000000..e47b65237 --- /dev/null +++ b/utils/aa-easyprof.pod @@ -0,0 +1,146 @@ +# This publication is intellectual property of Canonical Ltd. Its contents +# can be duplicated, either in part or in whole, provided that a copyright +# label is visibly located on each copy. +# +# All information found in this book has been compiled with utmost +# attention to detail. However, this does not guarantee complete accuracy. +# Neither Canonical Ltd, the authors, nor the translators shall be held +# liable for possible errors or the consequences thereof. +# +# Many of the software and hardware descriptions cited in this book +# are registered trademarks. All trade names are subject to copyright +# restrictions and may be registered trade marks. Canonical Ltd +# essentially adheres to the manufacturer's spelling. +# +# Names of products and trademarks appearing in this book (with or without +# specific notation) are likewise subject to trademark and trade protection +# laws and may thus fall under copyright restrictions. +# + +=pod + +=head1 NAME + +aa-easyprof - AppArmor profile generation made easy. + +=head1 SYNOPSIS + +B [option] + +=head1 DESCRIPTION + +B provides an easy to use interface for AppArmor policy +generation. B supports the use of templates and policy groups to +quickly profile an application. Please note that while this tool can help +with policy generation, its utility is dependent on the quality of the +templates, policy groups and abstractions used. Also, this tool may create +policy which is less restricted than creating policy by hand or with +B and B. + +=head1 OPTIONS + +B accepts the following arguments: + +=over 4 + +=item -t TEMPLATE, --template=TEMPLATE + +Specify which template to use. May specify either a system template from +/usr/share/apparmor/easyprof/templates or a filename for the template to +use. If not specified, use /usr/share/apparmor/easyprof/templates/default. + +=item -p POLICYGROUPS, --policy-groups=POLICYGROUPS + +Specify POLICY as a comma-separated list of policy groups. See --list-templates +for supported policy groups. The available policy groups are in +/usr/share/apparmor/easyprof/policy. Policy groups are simply groupings of +AppArmor rules or policies. They are similar to AppArmor abstractions, but +usually encompass more policy rules. + +=item -a ABSTRACTIONS, --abstractions=ABSTRACTIONS + +Specify ABSTRACTIONS as a comma-separated list of AppArmor abstractions. It is +usually recommended you use policy groups instead, but this is provided as a +convenience. AppArmor abstractions are located in /etc/apparmor.d/abstractions. +See apparmor.d(5) for details. + +=item -r PATH, --read-path=PATH + +Specify a PATH to allow owner reads. May be specified multiple times. If the +PATH ends in a '/', then PATH is treated as a directory and reads are allowed +to all files under this directory. Can optionally use '/*' at the end of the +PATH to only allow reads to files directly in PATH. + +=item -w PATH, --write-dir=PATH + +Like --read-path but also allow owner writes in additions to reads. + +=item -n NAME, --name=NAME + +Specify NAME of policy. If not specified, NAME is set to the name of the +binary. The NAME of the policy is often used as part of the path in the +various templates. + +=item --template-var="@{VAR}=VALUE" + +Set VAR to VALUE in the resulting policy. This typically only makes sense if +the specified template uses this value. May be specified multiple times. + +=item --list-templates + +List available templates. + +=item --show-template=TEMPLATE + +Display template specified with --template. + +=item --templates-dir=PATH + +Use PATH instead of system templates directory. + +=item --list-policy-groups + +List available policy groups. + +=item --show-policy-group + +Display policy groups specified with --policy. + +=item --policy-groups-dir=PATH + +Use PATH instead of system policy-groups directory. + +=item --author + +Specify author of the policy. + +=item --copyright + +Specify copyright of the policy. + +=item --comment + +Specify comment for the policy. + +=back + +=head1 EXAMPLE + +Example usage for a program named 'foo' which is installed in /opt/foo: + +=over + +$ aa-easyprof --template=user-application --template-var="@{APPNAME}=foo" --policy-groups=opt-application,user-application /opt/foo/bin/FooApp + +=back + +=head1 BUGS + +If you find any additional bugs, please report them to Launchpad at +L. + +=head1 SEE ALSO + +apparmor(7) apparmor.d(5) + +=cut diff --git a/utils/apparmor/__init__.py b/utils/apparmor/__init__.py new file mode 100644 index 000000000..94f439406 --- /dev/null +++ b/utils/apparmor/__init__.py @@ -0,0 +1,9 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2011-2012 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. +# +# ------------------------------------------------------------------ diff --git a/utils/apparmor/easyprof.py b/utils/apparmor/easyprof.py new file mode 100644 index 000000000..92074d375 --- /dev/null +++ b/utils/apparmor/easyprof.py @@ -0,0 +1,567 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2011-2012 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 codecs +import glob +import optparse +import os +import re +import subprocess +import sys +import tempfile + +# +# TODO: move this out to the common library +# +#from apparmor import AppArmorException +class AppArmorException(Exception): + '''This class represents AppArmor exceptions''' + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) +# +# End common +# + +DEBUGGING = False + +# +# TODO: move this out to a utilities library +# +def error(out, exit_code=1, do_exit=True): + '''Print error message and exit''' + try: + print >> sys.stderr, "ERROR: %s" % (out) + except IOError: + pass + + if do_exit: + sys.exit(exit_code) + + +def warn(out): + '''Print warning message''' + try: + print >> sys.stderr, "WARN: %s" % (out) + except IOError: + pass + + +def msg(out, output=sys.stdout): + '''Print message''' + try: + print >> output, "%s" % (out) + except IOError: + pass + + +def cmd(command): + '''Try to execute the given command.''' + debug(command) + try: + sp = subprocess.Popen(command, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + except OSError, ex: + return [127, str(ex)] + + out = sp.communicate()[0] + return [sp.returncode, out] + + +def cmd_pipe(command1, command2): + '''Try to pipe command1 into command2.''' + try: + sp1 = subprocess.Popen(command1, stdout=subprocess.PIPE) + sp2 = subprocess.Popen(command2, stdin=sp1.stdout) + except OSError, ex: + return [127, str(ex)] + + out = sp2.communicate()[0] + return [sp2.returncode, out] + + +def debug(out): + '''Print debug message''' + if DEBUGGING: + try: + print >> sys.stderr, "DEBUG: %s" % (out) + except IOError: + pass + + +def valid_binary_path(path): + '''Validate name''' + try: + a_path = os.path.abspath(path) + except Exception: + debug("Could not find absolute path for binary") + return False + + if path != a_path: + debug("Binary should use a normalized absolute path") + return False + + if not os.path.exists(a_path): + return True + + r_path = os.path.realpath(path) + if r_path != a_path: + debug("Binary should not be a symlink") + return False + + return True + + +def valid_variable_name(var): + '''Validate variable name''' + if re.search(r'[a-zA-Z0-9_]+$', var): + return True + return False + + +def valid_path(path): + '''Valid path''' + # No relative paths + m = "Invalid path: %s" % (path) + if not path.startswith('/'): + debug("%s (relative)" % (m)) + 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): + '''Open specified file read-only''' + try: + orig = codecs.open(path, 'r', "UTF-8") + except Exception: + raise + + return orig + + +def verify_policy(policy): + '''Verify policy compiles''' + exe = "/sbin/apparmor_parser" + if not os.path.exists(exe): + rc, exe = cmd(['which', 'apparmor_parser']) + if rc != 0: + warn("Could not find apparmor_parser. Skipping verify") + return True + + fn = "" + # if policy starts with '/' and is one line, assume it is a path + if len(policy.splitlines()) == 1 and valid_path(policy): + fn = policy + else: + f, fn = tempfile.mkstemp(prefix='aa-easyprof') + os.write(f, policy) + os.close(f) + + rc, out = cmd([exe, '-p', fn]) + os.unlink(fn) + if rc == 0: + return True + return False + +# +# End utility functions +# + + +class AppArmorEasyProfile: + '''Easy profile class''' + def __init__(self, binary, opt): + self.conffile = "/etc/apparmor/easyprof.conf" + if opt.conffile: + self.conffile = os.path.abspath(opt.conffile) + + self.dirs = dict() + if os.path.isfile(self.conffile): + self._get_defaults() + + if opt.templates_dir and os.path.isdir(opt.templates_dir): + self.dirs['templates'] = os.path.abspath(opt.templates_dir) + elif not opt.templates_dir and \ + opt.template and \ + os.path.isfile(opt.template) and \ + valid_path(opt.template): + # If we specified the template and it is an absolute path, just set + # the templates directory to the parent of the template so we don't + # have to require --template-dir with absolute paths. + self.dirs['templates'] = os.path.abspath(os.path.dirname(opt.template)) + if opt.policy_groups_dir and os.path.isdir(opt.policy_groups_dir): + self.dirs['policygroups'] = os.path.abspath(opt.policy_groups_dir) + + if not self.dirs.has_key('templates'): + raise AppArmorException("Could not find templates directory") + if not self.dirs.has_key('policygroups'): + raise AppArmorException("Could not find policygroups directory") + + self.aa_topdir = "/etc/apparmor.d" + + self.binary = binary + if binary != None: + if not valid_binary_path(binary): + raise AppArmorException("Invalid path for binary: '%s'" % binary) + + self.set_template(opt.template) + self.set_policygroup(opt.policy_groups) + if opt.name: + self.set_name(opt.name) + elif self.binary != None: + self.set_name(self.binary) + + self.templates = get_directory_contents(self.dirs['templates']) + self.policy_groups = get_directory_contents(self.dirs['policygroups']) + + def _get_defaults(self): + '''Read in defaults from configuration''' + if not os.path.exists(self.conffile): + raise AppArmorException("Could not find '%s'" % self.conffile) + + # Read in the configuration + f = open_file_read(self.conffile) + + pat = re.compile(r'^\w+=".*"?') + for line in f: + if not pat.search(line): + continue + if line.startswith("POLICYGROUPS_DIR="): + d = re.split(r'=', line.strip())[1].strip('["\']') + self.dirs['policygroups'] = d + elif line.startswith("TEMPLATES_DIR="): + d = re.split(r'=', line.strip())[1].strip('["\']') + self.dirs['templates'] = d + f.close() + + keys = self.dirs.keys() + if 'templates' not in keys: + raise AppArmorException("Could not find TEMPLATES_DIR in '%s'" % self.conffile) + if 'policygroups' not in keys: + raise AppArmorException("Could not find POLICYGROUPS_DIR in '%s'" % self.conffile) + + for k in self.dirs.keys(): + if not os.path.isdir(self.dirs[k]): + raise AppArmorException("Could not find '%s'" % self.dirs[k]) + + def set_name(self, name): + '''Set name of policy''' + self.name = name + + def get_template(self): + '''Get contents of current template''' + return open(self.template).read() + + def set_template(self, template): + '''Set current template''' + self.template = template + if not template.startswith('/'): + self.template = os.path.join(self.dirs['templates'], template) + if not os.path.exists(self.template): + raise AppArmorException('%s does not exist' % (self.template)) + + def get_templates(self): + '''Get list of all available templates by filename''' + return self.templates + + def get_policygroup(self, policygroup): + '''Get contents of specific policygroup''' + p = policygroup + if not p.startswith('/'): + p = os.path.join(self.dirs['policygroups'], p) + if self.policy_groups == None or not p in self.policy_groups: + raise AppArmorException("Policy group '%s' does not exist" % p) + return open(p).read() + + def set_policygroup(self, policygroups): + '''Set policygroups''' + self.policy_groups = [] + if policygroups != None: + for p in policygroups.split(','): + if not p.startswith('/'): + p = os.path.join(self.dirs['policygroups'], p) + if not os.path.exists(p): + raise AppArmorException('%s does not exist' % (p)) + self.policy_groups.append(p) + + def get_policy_groups(self): + '''Get list of all policy groups by filename''' + return self.policy_groups + + def gen_abstraction_rule(self, abstraction): + '''Generate an abstraction rule''' + p = os.path.join(self.aa_topdir, "abstractions", abstraction) + if not os.path.exists(p): + raise AppArmorException("%s does not exist" % p) + return "#include " % abstraction + + def gen_variable_declaration(self, dec): + '''Generate a variable declaration''' + if not re.search(r'^@\{[a-zA-Z_]+\}=.+', dec): + raise AppArmorException("Invalid variable declaration '%s'" % dec) + return dec + + def gen_path_rule(self, path, access): + rule = [] + if not path.startswith('/') and not path.startswith('@'): + raise AppArmorException("'%s' should not be relative path" % path) + + owner = "" + if path.startswith('/home/') or path.startswith("@{HOME"): + owner = "owner " + + if path.endswith('/'): + rule.append("%s %s," % (path, access)) + rule.append("%s%s** %s," % (owner, path, access)) + elif path.endswith('/**') or path.endswith('/*'): + rule.append("%s %s," % (os.path.dirname(path), access)) + rule.append("%s%s %s," % (owner, path, access)) + else: + rule.append("%s%s %s," % (owner, path, access)) + + return rule + + + def gen_policy(self, name, binary, template_var=[], abstractions=None, policy_groups=None, read_path=[], write_path=[], author=None, comment=None, copyright=None): + def find_prefix(t, s): + '''Calculate whitespace prefix based on occurrence of s in t''' + pat = re.compile(r'^ *%s' % s) + p = "" + for line in t.splitlines(): + if pat.match(line): + p = " " * (len(line) - len(line.lstrip())) + break + return p + + policy = self.get_template() + if '###ENDUSAGE###' in policy: + found = False + tmp = "" + for line in policy.splitlines(): + if not found: + if line.startswith('###ENDUSAGE###'): + found = True + continue + tmp += line + "\n" + policy = tmp + + # Fill-in profile name and binary + policy = re.sub(r'###NAME###', name, policy) + policy = re.sub(r'###BINARY###', binary, policy) + + # Fill-in various comment fields + if comment != None: + policy = re.sub(r'###COMMENT###', "Comment: %s" % comment, policy) + + if author != None: + policy = re.sub(r'###AUTHOR###', "Author: %s" % author, policy) + + if copyright != None: + policy = re.sub(r'###COPYRIGHT###', "Copyright: %s" % copyright, policy) + + # Fill-in rules and variables with proper indenting + search = '###ABSTRACTIONS###' + prefix = find_prefix(policy, search) + s = "%s# No abstractions specified" % prefix + if abstractions != None: + s = "%s# Specified abstractions" % (prefix) + for i in abstractions.split(','): + s += "\n%s%s" % (prefix, self.gen_abstraction_rule(i)) + policy = re.sub(r' *%s' % search, s, policy) + + search = '###POLICYGROUPS###' + prefix = find_prefix(policy, search) + s = "%s# No policy groups specified" % prefix + if policy_groups != None: + s = "%s# Rules specified via policy groups" % (prefix) + for i in policy_groups.split(','): + for line in self.get_policygroup(i).splitlines(): + s += "\n%s%s" % (prefix, line) + if i != policy_groups.split(',')[-1]: + s += "\n" + policy = re.sub(r' *%s' % search, s, policy) + + search = '###VAR###' + prefix = find_prefix(policy, search) + s = "%s# No template variables specified" % prefix + if len(template_var) > 0: + s = "%s# Specified profile variables" % (prefix) + for i in template_var: + s += "\n%s%s" % (prefix, self.gen_variable_declaration(i)) + policy = re.sub(r' *%s' % search, s, policy) + + search = '###READS###' + prefix = find_prefix(policy, search) + s = "%s# No read paths specified" % prefix + if len(read_path) > 0: + s = "%s# Specified read permissions" % (prefix) + for i in read_path: + for r in self.gen_path_rule(i, 'r'): + s += "\n%s%s" % (prefix, r) + policy = re.sub(r' *%s' % search, s, policy) + + search = '###WRITES###' + prefix = find_prefix(policy, search) + s = "%s# No write paths specified" % prefix + if len(write_path) > 0: + s = "%s# Specified write permissions" % (prefix) + for i in write_path: + for r in self.gen_path_rule(i, 'rwk'): + s += "\n%s%s" % (prefix, r) + policy = re.sub(r' *%s' % search, s, policy) + + if not verify_policy(policy): + debug("\n" + policy) + raise AppArmorException("Invalid policy") + + return policy + +def print_basefilenames(files): + for i in files: + print "%s" % (os.path.basename(i)) + +def print_files(files): + for i in files: + print open(i).read() + +def parse_args(args=None): + '''Parse arguments''' + global DEBUGGING + + parser = optparse.OptionParser() + parser.add_option("-c", "--config-file", + dest="conffile", + help="Use alternate configuration file", + metavar="FILE") + parser.add_option("-d", "--debug", + help="Show debugging output", + action='store_true', + default=False) + parser.add_option("-t", "--template", + dest="template", + help="Use non-default policy template", + metavar="TEMPLATE", + default='default') + parser.add_option("--list-templates", + help="List available templates", + action='store_true', + default=False) + parser.add_option("--templates-dir", + dest="templates_dir", + help="Use non-default templates directory", + metavar="DIR") + parser.add_option("--show-template", + help="Show specified template", + action='store_true', + default=False) + parser.add_option("-p", "--policy-groups", + help="Comma-separated list of policy groups", + metavar="POLICYGROUPS") + parser.add_option("--list-policy-groups", + help="List available policy groups", + action='store_true', + default=False) + parser.add_option("--policy-groups-dir", + dest="policy_groups_dir", + help="Use non-default policy-groups directory", + metavar="DIR") + parser.add_option("--show-policy-group", + help="Show specified policy groups", + action='store_true', + default=False) + parser.add_option("-a", "--abstractions", + dest="abstractions", + help="Comma-separated list of abstractions", + metavar="ABSTRACTIONS") + parser.add_option("--read-path", + dest="read_path", + help="Path allowing owner reads", + metavar="PATH", + action="append") + parser.add_option("--write-path", + dest="write_path", + help="Path allowing owner writes", + metavar="PATH", + action="append") + parser.add_option("-n", "--name", + dest="name", + help="Name of policy", + metavar="NAME") + parser.add_option("--comment", + dest="comment", + help="Comment for policy", + metavar="COMMENT") + parser.add_option("--author", + dest="author", + help="Author of policy", + metavar="COMMENT") + parser.add_option("--copyright", + dest="copyright", + help="Copyright for policy", + metavar="COMMENT") + parser.add_option("--template-var", + dest="template_var", + help="Declare AppArmor variable", + metavar="@{VARIABLE}=VALUE", + action="append") + + (my_opt, my_args) = parser.parse_args(args) + if my_opt.debug: + DEBUGGING = True + return (my_opt, my_args) + +def gen_policy_params(binary, opt): + '''Generate parameters for gen_policy''' + params = dict(binary=binary) + if opt.name: + params['name'] = opt.name + else: + params['name'] = os.path.basename(binary) + if opt.template_var: # What about specified multiple times? + params['template_var'] = opt.template_var + if opt.abstractions: + params['abstractions'] = opt.abstractions + if opt.policy_groups: + params['policy_groups'] = opt.policy_groups + if opt.read_path: + params['read_path'] = opt.read_path + if opt.write_path: + params['write_path'] = opt.write_path + if opt.abstractions: + params['abstractions'] = opt.abstractions + if opt.comment: + params['comment'] = opt.comment + if opt.author: + params['author'] = opt.author + if opt.copyright: + params['copyright'] = opt.copyright + + return params + diff --git a/utils/easyprof/README b/utils/easyprof/README new file mode 100644 index 000000000..38354c495 --- /dev/null +++ b/utils/easyprof/README @@ -0,0 +1,44 @@ +AppArmor Easy Profiler +---------------------- +aa-easyprof is a standalone CLI application which can also be imported into +developer SDKs. See test/test-aa-easyprof.py for an example of how to import +this into your SDK. + + +Templates +--------- +Any number of templates can be used. The user may specify one on the command +line or use a system-wide template from /usr/share/apparmor/easyprof/templates. + +Currently the combination of the user-application and the opt-application and +user-application policygroups should achieve a working policy for Ubuntu's +Application Review Board: +- http://developer.ubuntu.com/publish/my-apps-packages/ + +Eg: +$ aa-easyprof --template=user-application \ + --template-var="@{APPNAME}=foo" \ + --policy-groups=opt-application,user-application \ + /opt/foo/bin/foo + +Testing +------- +Unit tests: +$ ./test/test-aa-easyprof.py + +In source manual testing: +$ ./aa-easyprof --templates-dir=./easyprof/templates \ + --policy-groups-dir=./easyprof/policygroups \ + ... \ + /opt/foo/bin/foo + +Post-install manual testing: +$ make DESTDIR=/tmp/test PERLDIR=/tmp/test/usr/share/perl5/Immunix install +$ cd /tmp/test +$ PYTHONPATH=/tmp/test/usr/local/.../dist-packages ./usr/bin/aa-easyprof \ + --templates-dir=/tmp/test/usr/share/apparmor/easyprof/templates \ + --policy-groups-dir=/tmp/test/usr/share/apparmor/easyprof/policygroups \ + /opt/bin/foo + +(you may also adjust /tmp/test/etc/apparmor/easyprof.conf to avoid specifying +--templates-dir and --policy-groups-dir). diff --git a/utils/easyprof/easyprof.conf b/utils/easyprof/easyprof.conf new file mode 100644 index 000000000..fdd9cc837 --- /dev/null +++ b/utils/easyprof/easyprof.conf @@ -0,0 +1,5 @@ +# Location of system policygroups +POLICYGROUPS_DIR="/usr/share/apparmor/easyprof/policygroups" + +# Location of system templates +TEMPLATES_DIR="/usr/share/apparmor/easyprof/templates" diff --git a/utils/easyprof/policygroups/networking b/utils/easyprof/policygroups/networking new file mode 100644 index 000000000..c60a4ede2 --- /dev/null +++ b/utils/easyprof/policygroups/networking @@ -0,0 +1,2 @@ +# Policygroup to allow networking +#include diff --git a/utils/easyprof/policygroups/opt-application b/utils/easyprof/policygroups/opt-application new file mode 100644 index 000000000..f586576b6 --- /dev/null +++ b/utils/easyprof/policygroups/opt-application @@ -0,0 +1,3 @@ +# Policy group for applications installed in /opt +/opt/@{APPNAME}/ r, +/opt/@{APPNAME}/** mrk, diff --git a/utils/easyprof/policygroups/user-application b/utils/easyprof/policygroups/user-application new file mode 100644 index 000000000..f13515d46 --- /dev/null +++ b/utils/easyprof/policygroups/user-application @@ -0,0 +1,8 @@ +# Policy group allowing various writes to standard directories in @{HOMEDIRS} +#include +owner @{HOMEDIRS}/.cache/@{APPNAME}/ rw, +owner @{HOMEDIRS}/.cache/@{APPNAME}/** rwkl, +owner @{HOMEDIRS}/.config/@{APPNAME}/ rw, +owner @{HOMEDIRS}/.config/@{APPNAME}/** rwkl, +owner @{HOMEDIRS}/.local/share/@{APPNAME}/ rw, +owner @{HOMEDIRS}/.local/share/@{APPNAME}/** rwkl, diff --git a/utils/easyprof/templates/default b/utils/easyprof/templates/default new file mode 100644 index 000000000..c6d0be497 --- /dev/null +++ b/utils/easyprof/templates/default @@ -0,0 +1,26 @@ +# +# Example usage: +# $ aa-easyprof --policy-groups=user-application /usr/bin/foo +# +###ENDUSAGE### +# vim:syntax=apparmor +# AppArmor policy for ###NAME### +# ###AUTHOR### +# ###COPYRIGHT### +# ###COMMENT### + +#include + +###VAR### + +###BINARY### { + #include + + ###ABSTRACTIONS### + + ###POLICYGROUPS### + + ###READS### + + ###WRITES### +} diff --git a/utils/easyprof/templates/user-application b/utils/easyprof/templates/user-application new file mode 100644 index 000000000..766da425e --- /dev/null +++ b/utils/easyprof/templates/user-application @@ -0,0 +1,29 @@ +# +# Example usage for a program named 'foo' which is installed in /opt/foo +# $ aa-easyprof --template=user-application \ +# --template-var="@{APPNAME}=foo" \ +# --policy-groups=opt-application,user-application \ +# /opt/foo/bin/foo +# +###ENDUSAGE### +# vim:syntax=apparmor +# AppArmor policy for ###NAME### +# ###AUTHOR### +# ###COPYRIGHT### +# ###COMMENT### + +#include + +###VAR### + +###BINARY### { + #include + + ###ABSTRACTIONS### + + ###POLICYGROUPS### + + ###READS### + + ###WRITES### +} diff --git a/utils/python-tools-setup.py b/utils/python-tools-setup.py new file mode 100644 index 000000000..8d905de07 --- /dev/null +++ b/utils/python-tools-setup.py @@ -0,0 +1,79 @@ +# ---------------------------------------------------------------------- +# Copyright (c) 2012 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. +# +# 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. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, contact Canonical, Ltd. +# ---------------------------------------------------------------------- +# +# Usage: +# $ python ./python-tools-setup.py install --root=... --version=... +# +# Note: --version=... must be the last argument to this script +# + +from distutils.command.install import install as _install +from distutils.core import setup +import os +import shutil +import sys + +class Install(_install, object): + '''Override distutils to install the files where we want them.''' + def run(self): + # Now byte-compile everything + super(Install, self).run() + + prefix = self.prefix + if self.root != None: + prefix = self.root + + # Install scripts, configuration files and data + scripts = ['/usr/bin/aa-easyprof'] + self.mkpath(prefix + os.path.dirname(scripts[0])) + for s in scripts: + self.copy_file(os.path.basename(s), prefix + s) + + configs = ['easyprof/easyprof.conf'] + self.mkpath(prefix + "/etc/apparmor") + for c in configs: + self.copy_file(c, os.path.join(prefix + "/etc/apparmor", os.path.basename(c))) + + data = ['easyprof/templates', 'easyprof/policygroups'] + self.mkpath(prefix + "/usr/share/apparmor/easyprof") + for d in data: + self.copy_tree(d, os.path.join(prefix + "/usr/share/apparmor/easyprof", os.path.basename(d))) + + +if os.path.exists('staging'): + shutil.rmtree('staging') +shutil.copytree('apparmor', 'staging') + +# Support the --version=... since this will be part of a Makefile +version = "unknown-version" +if "--version=" in sys.argv[-1]: + version=sys.argv[-1].split('=')[1] + sys.argv = sys.argv[0:-1] + +setup (name='apparmor', + version=version, + description='Python libraries for AppArmor utilities', + long_description='Python libraries for AppArmor utilities', + author='AppArmor Developers', + author_email='apparmor@lists.ubuntu.com', + url='https://launchpad.net/apparmor', + license='GPL-2', + cmdclass={'install': Install}, + package_dir={'apparmor': 'staging'}, + py_modules=['apparmor.easyprof'] +) + +shutil.rmtree('staging') diff --git a/utils/test/easyprof.conf b/utils/test/easyprof.conf new file mode 100644 index 000000000..9b1b61914 --- /dev/null +++ b/utils/test/easyprof.conf @@ -0,0 +1,5 @@ +# Location of system policygroups +POLICYGROUPS_DIR="./policygroups" + +# Location of system templates +TEMPLATES_DIR="./templates" diff --git a/utils/test/test-aa-easyprof.py b/utils/test/test-aa-easyprof.py new file mode 100644 index 000000000..9ced781ef --- /dev/null +++ b/utils/test/test-aa-easyprof.py @@ -0,0 +1,882 @@ +#! /usr/bin/env python +# ------------------------------------------------------------------ +# +# Copyright (C) 2011-2012 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 glob +import os +import shutil +import sys +import tempfile +import unittest + +topdir = None +debugging = False + +def recursive_rm(dirPath, contents_only=False): + '''recursively remove directory''' + names = os.listdir(dirPath) + for name in names: + path = os.path.join(dirPath, name) + if os.path.islink(path) or not os.path.isdir(path): + os.unlink(path) + else: + recursive_rm(path) + if contents_only == False: + os.rmdir(dirPath) + +# +# Our test class +# +class T(unittest.TestCase): + def setUp(self): + '''Setup for tests''' + global topdir + + self.tmpdir = tempfile.mkdtemp(prefix='test-aa-easyprof') + + # Copy everything into place + for d in ['easyprof/policygroups', 'easyprof/templates']: + shutil.copytree(os.path.join(topdir, d), os.path.join(self.tmpdir, os.path.basename(d))) + + # Create a test template + self.test_template = "test-template" + contents = '''# vim:syntax=apparmor +# %s +# AppArmor policy for ###NAME### +# ###AUTHOR### +# ###COPYRIGHT### +# ###COMMENT### + +#include + +###VAR### + +###BINARY### { + #include + + ###ABSTRACTIONS### + + ###POLICYGROUPS### + + ###READS### + + ###WRITES### +} + +''' % (self.test_template) + open(os.path.join(self.tmpdir, 'templates', self.test_template), 'w').write(contents) + + # Create a test policygroup + self.test_policygroup = "test-policygroup" + contents = ''' + # %s + #include + #include +''' % (self.test_policygroup) + open(os.path.join(self.tmpdir, 'policygroups', self.test_policygroup), 'w').write(contents) + + # setup our conffile + self.conffile = os.path.join(self.tmpdir, 'easyprof.conf') + contents = ''' +POLICYGROUPS_DIR="%s/policygroups" +TEMPLATES_DIR="%s/templates" +''' % (self.tmpdir, self.tmpdir) + open(self.conffile, 'w').write(contents) + + self.binary = "/opt/bin/foo" + self.full_args = ['-c', self.conffile, self.binary] + + if debugging: + self.full_args.append('-d') + + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + + def tearDown(self): + '''Teardown for tests''' + if os.path.exists(self.tmpdir): + recursive_rm(self.tmpdir) + +# +# config file tests +# + def test_configuration_file_p_invalid(self): + '''Test config parsing (invalid POLICYGROUPS_DIR)''' + contents = ''' +POLICYGROUPS_DIR= +TEMPLATES_DIR="%s/templates" +''' % (self.tmpdir) + + open(self.conffile, 'w').write(contents) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + + raise Exception ("File should have been invalid") + + def test_configuration_file_p_empty(self): + '''Test config parsing (empty POLICYGROUPS_DIR)''' + contents = ''' +POLICYGROUPS_DIR="%s" +TEMPLATES_DIR="%s/templates" +''' % ('', self.tmpdir) + + open(self.conffile, 'w').write(contents) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + + raise Exception ("File should have been invalid") + + def test_configuration_file_p_nonexistent(self): + '''Test config parsing (nonexistent POLICYGROUPS_DIR)''' + contents = ''' +POLICYGROUPS_DIR="%s/policygroups" +TEMPLATES_DIR="%s/templates" +''' % ('/nonexistent', self.tmpdir) + + open(self.conffile, 'w').write(contents) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + + raise Exception ("File should have been invalid") + + def test_policygroups_dir_relative(self): + '''Test --policy-groups-dir (relative DIR)''' + os.chdir(self.tmpdir) + rel = os.path.join(self.tmpdir, 'relative') + os.mkdir(rel) + shutil.copy(os.path.join(self.tmpdir, 'policygroups', self.test_policygroup), os.path.join(rel, self.test_policygroup)) + + args = self.full_args + args += ['--policy-groups-dir', './relative', '--show-policy-group', '--policy-groups=%s' % self.test_policygroup] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + # no fallback + self.assertTrue(easyp.dirs['policygroups'] == rel, "Not using specified --policy-groups-dir") + self.assertFalse(easyp.get_policy_groups() == None, "Could not find policy-groups") + + def test_policygroups_dir_nonexistent(self): + '''Test --policy-groups-dir (nonexistent DIR)''' + os.chdir(self.tmpdir) + rel = os.path.join(self.tmpdir, 'nonexistent') + + args = self.full_args + args += ['--policy-groups-dir', rel, '--show-policy-group', '--policy-groups=%s' % self.test_policygroup] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + # test if using fallback + self.assertFalse(easyp.dirs['policygroups'] == rel, "Using nonexistent --policy-groups-dir") + + # test fallback + self.assertTrue(easyp.get_policy_groups() != None, "Found policy-groups when shouldn't have") + + def test_policygroups_dir_valid(self): + '''Test --policy-groups-dir (valid DIR)''' + os.chdir(self.tmpdir) + valid = os.path.join(self.tmpdir, 'valid') + os.mkdir(valid) + shutil.copy(os.path.join(self.tmpdir, 'policygroups', self.test_policygroup), os.path.join(valid, self.test_policygroup)) + + args = self.full_args + args += ['--policy-groups-dir', valid, '--show-policy-group', '--policy-groups=%s' % self.test_policygroup] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + # no fallback + self.assertTrue(easyp.dirs['policygroups'] == valid, "Not using specified --policy-groups-dir") + self.assertFalse(easyp.get_policy_groups() == None, "Could not find policy-groups") + + def test_configuration_file_t_invalid(self): + '''Test config parsing (invalid TEMPLATES_DIR)''' + contents = ''' +TEMPLATES_DIR= +POLICYGROUPS_DIR="%s/templates" +''' % (self.tmpdir) + + open(self.conffile, 'w').write(contents) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + + raise Exception ("File should have been invalid") + + def test_configuration_file_t_empty(self): + '''Test config parsing (empty TEMPLATES_DIR)''' + contents = ''' +TEMPLATES_DIR="%s" +POLICYGROUPS_DIR="%s/templates" +''' % ('', self.tmpdir) + + open(self.conffile, 'w').write(contents) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + + raise Exception ("File should have been invalid") + + def test_configuration_file_t_nonexistent(self): + '''Test config parsing (nonexistent TEMPLATES_DIR)''' + contents = ''' +TEMPLATES_DIR="%s/policygroups" +POLICYGROUPS_DIR="%s/templates" +''' % ('/nonexistent', self.tmpdir) + + open(self.conffile, 'w').write(contents) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + + raise Exception ("File should have been invalid") + + def test_templates_dir_relative(self): + '''Test --templates-dir (relative DIR)''' + os.chdir(self.tmpdir) + rel = os.path.join(self.tmpdir, 'relative') + os.mkdir(rel) + shutil.copy(os.path.join(self.tmpdir, 'templates', self.test_template), os.path.join(rel, self.test_template)) + + args = self.full_args + args += ['--templates-dir', './relative', '--show-template', '--template=%s' % self.test_template] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + # no fallback + self.assertTrue(easyp.dirs['templates'] == rel, "Not using specified --template-dir") + self.assertFalse(easyp.get_templates() == None, "Could not find templates") + + def test_templates_dir_nonexistent(self): + '''Test --templates-dir (nonexistent DIR)''' + os.chdir(self.tmpdir) + rel = os.path.join(self.tmpdir, 'nonexistent') + + args = self.full_args + args += ['--templates-dir', rel, '--show-template', '--template=%s' % self.test_template] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + # test if using fallback + self.assertFalse(easyp.dirs['templates'] == rel, "Using nonexistent --template-dir") + + # test fallback + self.assertTrue(easyp.get_templates() != None, "Found templates when shouldn't have") + + def test_templates_dir_valid(self): + '''Test --templates-dir (valid DIR)''' + os.chdir(self.tmpdir) + valid = os.path.join(self.tmpdir, 'valid') + os.mkdir(valid) + shutil.copy(os.path.join(self.tmpdir, 'templates', self.test_template), os.path.join(valid, self.test_template)) + + args = self.full_args + args += ['--templates-dir', valid, '--show-template', '--template=%s' % self.test_template] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + # no fallback + self.assertTrue(easyp.dirs['templates'] == valid, "Not using specified --template-dir") + self.assertFalse(easyp.get_templates() == None, "Could not find templates") + +# +# Binary file tests +# + def test_binary(self): + '''Test binary''' + easyprof.AppArmorEasyProfile('/bin/ls', self.options) + + def test_binary_nonexistent(self): + '''Test binary (nonexistent)''' + easyprof.AppArmorEasyProfile(os.path.join(self.tmpdir, 'nonexistent'), self.options) + + def test_binary_relative(self): + '''Test binary (relative)''' + try: + easyprof.AppArmorEasyProfile('./foo', self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("Binary should have been invalid") + + def test_binary_symlink(self): + '''Test binary (symlink)''' + exe = os.path.join(self.tmpdir, 'exe') + open(exe, 'wa').close() + symlink = exe + ".lnk" + os.symlink(exe, symlink) + + try: + easyprof.AppArmorEasyProfile(symlink, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("Binary should have been invalid") + +# +# Templates tests +# + def test_templates_list(self): + '''Test templates (list)''' + args = self.full_args + args.append('--list-templates') + (self.options, self.args) = easyprof.parse_args(args) + + easyp = easyprof.AppArmorEasyProfile(None, self.options) + for i in easyp.get_templates(): + self.assertTrue(os.path.exists(i), "Could not find '%s'" % i) + + def test_templates_show(self): + '''Test templates (show)''' + files = [] + for f in glob.glob("%s/templates/*" % self.tmpdir): + files.append(f) + + for f in files: + args = self.full_args + args += ['--show-template', '--template', f] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(None, self.options) + + path = os.path.join(easyp.dirs['templates'], f) + self.assertTrue(os.path.exists(path), "Could not find '%s'" % path) + open(path).read() + +# +# Policygroups tests +# + def test_policygroups_list(self): + '''Test policygroups (list)''' + args = self.full_args + args.append('--list-policy-groups') + (self.options, self.args) = easyprof.parse_args(args) + + easyp = easyprof.AppArmorEasyProfile(None, self.options) + for i in easyp.get_templates(): + self.assertTrue(os.path.exists(i), "Could not find '%s'" % i) + + def test_policygroups_show(self): + '''Test policygroups (show)''' + files = [] + for f in glob.glob("%s/policygroups/*" % self.tmpdir): + files.append(f) + + for f in files: + args = self.full_args + args += ['--show-template', '--template', f] + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(None, self.options) + + path = os.path.join(easyp.dirs['policygroups'], f) + self.assertTrue(os.path.exists(path), "Could not find '%s'" % path) + open(path).read() + +# +# Test genpolicy +# + def _gen_policy(self, name=None, template=None, extra_args=[]): + '''Generate a policy''' + # Build up our args + args = self.full_args + + if template == None: + args.append('--template=%s' % self.test_template) + else: + args.append('--template=%s' % template) + + if name != None: + args.append('--name=%s' % name) + + if len(extra_args) > 0: + args += extra_args + + args.append(self.binary) + + # Now parse our args + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + params = easyprof.gen_policy_params(self.binary, self.options) + p = easyp.gen_policy(**params) + + # We always need to check for these + search_terms = [self.binary] + if name != None: + search_terms.append(name) + + if template == None: + search_terms.append(self.test_template) + + for s in search_terms: + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + + # ###NAME### should be replaced with self.binary or 'name'. Check for that + inv_s = '###NAME###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + if debugging: + print p + + return p + + def test_genpolicy_templates_abspath(self): + '''Test genpolicy (abspath to template)''' + # create a new template + template = os.path.join(self.tmpdir, "test-abspath-template") + shutil.copy(os.path.join(self.tmpdir, 'templates', self.test_template), template) + contents = open(template).read() + test_string = "#teststring" + open(template, 'w').write(contents + "\n%s\n" % test_string) + + p = self._gen_policy(template=template) + + for s in [self.test_template, test_string]: + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + + def test_genpolicy_templates_system(self): + '''Test genpolicy (system template)''' + self._gen_policy() + + def test_genpolicy_templates_nonexistent(self): + '''Test genpolicy (nonexistent template)''' + try: + self._gen_policy(template=os.path.join(self.tmpdir, "/nonexistent")) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("template should be invalid") + + def test_genpolicy_name(self): + '''Test genpolicy (name)''' + self._gen_policy(name='test-foo') + + def test_genpolicy_comment(self): + '''Test genpolicy (comment)''' + s = "test comment" + p = self._gen_policy(extra_args=['--comment=%s' % s]) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###COMMENT###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_author(self): + '''Test genpolicy (author)''' + s = "Archibald Poindexter" + p = self._gen_policy(extra_args=['--author=%s' % s]) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###AUTHOR###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_copyright(self): + '''Test genpolicy (copyright)''' + s = "2112/01/01" + p = self._gen_policy(extra_args=['--copyright=%s' % s]) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###COPYRIGHT###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_abstractions(self): + '''Test genpolicy (single abstraction)''' + s = "nameservice" + p = self._gen_policy(extra_args=['--abstractions=%s' % s]) + search = "#include " % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###ABSTRACTIONS###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_abstractions_multiple(self): + '''Test genpolicy (multiple abstractions)''' + abstractions = "authentication,X,user-tmp" + p = self._gen_policy(extra_args=['--abstractions=%s' % abstractions]) + for s in abstractions.split(','): + search = "#include " % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###ABSTRACTIONS###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_policygroups(self): + '''Test genpolicy (single policygroup)''' + groups = self.test_policygroup + p = self._gen_policy(extra_args=['--policy-groups=%s' % groups]) + + for s in ['#include ', '#include ']: + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###POLICYGROUPS###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_policygroups_multiple(self): + '''Test genpolicy (multiple policygroups)''' + test_policygroup2 = "test-policygroup2" + contents = ''' + # %s + #include + #include +''' % (self.test_policygroup) + open(os.path.join(self.tmpdir, 'policygroups', test_policygroup2), 'w').write(contents) + + groups = "%s,%s" % (self.test_policygroup, test_policygroup2) + p = self._gen_policy(extra_args=['--policy-groups=%s' % groups]) + + for s in ['#include ', + '#include ', + '#include ', + '#include ']: + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###POLICYGROUPS###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_policygroups_nonexistent(self): + '''Test genpolicy (nonexistent policygroup)''' + try: + self._gen_policy(extra_args=['--policy-groups=nonexistent']) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("policygroup should be invalid") + + def test_genpolicy_readpath_file(self): + '''Test genpolicy (read-path file)''' + s = "/opt/test-foo" + p = self._gen_policy(extra_args=['--read-path=%s' % s]) + search = "%s r," % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_readpath_home_file(self): + '''Test genpolicy (read-path file in /home)''' + s = "/home/*/test-foo" + p = self._gen_policy(extra_args=['--read-path=%s' % s]) + search = "owner %s r," % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_readpath_homevar_file(self): + '''Test genpolicy (read-path file in @{HOME})''' + s = "@{HOME}/test-foo" + p = self._gen_policy(extra_args=['--read-path=%s' % s]) + search = "owner %s r," % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_readpath_homedirs_file(self): + '''Test genpolicy (read-path file in @{HOMEDIRS})''' + s = "@{HOMEDIRS}/test-foo" + p = self._gen_policy(extra_args=['--read-path=%s' % s]) + search = "owner %s r," % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_readpath_dir(self): + '''Test genpolicy (read-path directory/)''' + s = "/opt/test-foo-dir/" + p = self._gen_policy(extra_args=['--read-path=%s' % s]) + search_terms = ["%s r," % s, "%s** r," % s] + for search in search_terms: + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_readpath_dir_glob(self): + '''Test genpolicy (read-path directory/*)''' + s = "/opt/test-foo-dir/*" + p = self._gen_policy(extra_args=['--read-path=%s' % s]) + search_terms = ["%s r," % os.path.dirname(s), "%s r," % s] + for search in search_terms: + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_readpath_dir_glob_all(self): + '''Test genpolicy (read-path directory/**)''' + s = "/opt/test-foo-dir/**" + p = self._gen_policy(extra_args=['--read-path=%s' % s]) + search_terms = ["%s r," % os.path.dirname(s), "%s r," % s] + for search in search_terms: + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_readpath_multiple(self): + '''Test genpolicy (read-path multiple)''' + paths = ["/opt/test-foo", + "/home/*/test-foo", + "@{HOME}/test-foo", + "@{HOMEDIRS}/test-foo", + "/opt/test-foo-dir/", + "/opt/test-foo-dir/*", + "/opt/test-foo-dir/**"] + args = [] + search_terms = [] + for s in paths: + args.append('--read-path=%s' % s) + # This mimics easyprof.gen_path_rule() + owner = "" + if s.startswith('/home/') or s.startswith("@{HOME"): + owner = "owner " + if s.endswith('/'): + search_terms.append("%s r," % (s)) + search_terms.append("%s%s** r," % (owner, s)) + elif s.endswith('/**') or s.endswith('/*'): + search_terms.append("%s r," % (os.path.dirname(s))) + search_terms.append("%s%s r," % (owner, s)) + else: + search_terms.append("%s%s r," % (owner, s)) + + p = self._gen_policy(extra_args=args) + for search in search_terms: + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_readpath_bad(self): + '''Test genpolicy (read-path bad)''' + s = "bar" + try: + self._gen_policy(extra_args=['--read-path=%s' % s]) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("read-path should be invalid") + + def test_genpolicy_writepath_file(self): + '''Test genpolicy (write-path file)''' + s = "/opt/test-foo" + p = self._gen_policy(extra_args=['--write-path=%s' % s]) + search = "%s rwk," % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_writepath_home_file(self): + '''Test genpolicy (write-path file in /home)''' + s = "/home/*/test-foo" + p = self._gen_policy(extra_args=['--write-path=%s' % s]) + search = "owner %s rwk," % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_writepath_homevar_file(self): + '''Test genpolicy (write-path file in @{HOME})''' + s = "@{HOME}/test-foo" + p = self._gen_policy(extra_args=['--write-path=%s' % s]) + search = "owner %s rwk," % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_writepath_homedirs_file(self): + '''Test genpolicy (write-path file in @{HOMEDIRS})''' + s = "@{HOMEDIRS}/test-foo" + p = self._gen_policy(extra_args=['--write-path=%s' % s]) + search = "owner %s rwk," % s + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_writepath_dir(self): + '''Test genpolicy (write-path directory/)''' + s = "/opt/test-foo-dir/" + p = self._gen_policy(extra_args=['--write-path=%s' % s]) + search_terms = ["%s rwk," % s, "%s** rwk," % s] + for search in search_terms: + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_writepath_dir_glob(self): + '''Test genpolicy (write-path directory/*)''' + s = "/opt/test-foo-dir/*" + p = self._gen_policy(extra_args=['--write-path=%s' % s]) + search_terms = ["%s rwk," % os.path.dirname(s), "%s rwk," % s] + for search in search_terms: + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_writepath_dir_glob_all(self): + '''Test genpolicy (write-path directory/**)''' + s = "/opt/test-foo-dir/**" + p = self._gen_policy(extra_args=['--write-path=%s' % s]) + search_terms = ["%s rwk," % os.path.dirname(s), "%s rwk," % s] + for search in search_terms: + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_writepath_multiple(self): + '''Test genpolicy (write-path multiple)''' + paths = ["/opt/test-foo", + "/home/*/test-foo", + "@{HOME}/test-foo", + "@{HOMEDIRS}/test-foo", + "/opt/test-foo-dir/", + "/opt/test-foo-dir/*", + "/opt/test-foo-dir/**"] + args = [] + search_terms = [] + for s in paths: + args.append('--write-path=%s' % s) + # This mimics easyprof.gen_path_rule() + owner = "" + if s.startswith('/home/') or s.startswith("@{HOME"): + owner = "owner " + if s.endswith('/'): + search_terms.append("%s rwk," % (s)) + search_terms.append("%s%s** rwk," % (owner, s)) + elif s.endswith('/**') or s.endswith('/*'): + search_terms.append("%s rwk," % (os.path.dirname(s))) + search_terms.append("%s%s rwk," % (owner, s)) + else: + search_terms.append("%s%s rwk," % (owner, s)) + + p = self._gen_policy(extra_args=args) + for search in search_terms: + self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) + inv_s = '###READPATH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_writepath_bad(self): + '''Test genpolicy (write-path bad)''' + s = "bar" + try: + self._gen_policy(extra_args=['--write-path=%s' % s]) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("write-path should be invalid") + + def test_genpolicy_templatevar(self): + '''Test genpolicy (template-var single)''' + s = "@{FOO}=bar" + p = self._gen_policy(extra_args=['--template-var=%s' % s]) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###TEMPLATEVAR###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_templatevar_multiple(self): + '''Test genpolicy (template-var multiple)''' + variables = ["@{FOO}=bar", "@{BAR}=baz"] + args = [] + for s in variables: + args.append('--template-var=%s' % s) + + p = self._gen_policy(extra_args=args) + for s in variables: + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###TEMPLATEVAR###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_templatevar_bad(self): + '''Test genpolicy (template-var bad)''' + s = "{FOO}=bar" + try: + self._gen_policy(extra_args=['--template-var=%s' % s]) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("template-var should be invalid") + + def test_genpolicy_invalid_template_policy(self): + '''Test genpolicy (invalid template policy)''' + # create a new template + template = os.path.join(self.tmpdir, "test-invalid-template") + shutil.copy(os.path.join(self.tmpdir, 'templates', self.test_template), template) + contents = open(template).read() + bad_pol = "" + bad_string = "bzzzt" + for line in contents.splitlines(): + if '}' in line: + bad_pol += bad_string + else: + bad_pol += line + bad_pol += "\n" + open(template, 'w').write(bad_pol) + try: + self._gen_policy(template=template) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("policy should be invalid") + + +# +# End test class +# + +# +# Main +# +if __name__ == '__main__': + def cleanup(files): + for f in files: + if os.path.exists(f): + os.unlink(f) + + absfn = os.path.abspath(sys.argv[0]) + topdir = os.path.dirname(os.path.dirname(absfn)) + + if len(sys.argv) > 1 and (sys.argv[1] == '-d' or sys.argv[1] == '--debug'): + debugging = True + + created = [] + + # Create the necessary files to import aa-easyprof + init = os.path.join(os.path.dirname(absfn), '__init__.py') + if not os.path.exists(init): + open(init, 'wa').close() + created.append(init) + + symlink = os.path.join(os.path.dirname(absfn), 'easyprof.py') + if not os.path.exists(symlink): + os.symlink(os.path.join(topdir, 'apparmor', 'easyprof.py'), symlink) + created.append(symlink) + created.append(symlink + 'c') + + # Now that we have everything we need, import aa-easyprof + import easyprof + + # run the tests + suite = unittest.TestSuite() + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(T)) + rc = unittest.TextTestRunner(verbosity=2).run(suite) + + cleanup(created) + + if not rc.wasSuccessful(): + sys.exit(1)