diff --git a/utils/aa-easyprof b/utils/aa-easyprof index da0d1b869..ac69ca706 100755 --- a/utils/aa-easyprof +++ b/utils/aa-easyprof @@ -1,7 +1,7 @@ #! /usr/bin/env python # ------------------------------------------------------------------ # -# Copyright (C) 2011-2012 Canonical Ltd. +# Copyright (C) 2011-2013 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 @@ -22,6 +22,7 @@ if __name__ == "__main__": (opt, args) = apparmor.easyprof.parse_args() binary = None + manifest = None m = usage() if opt.show_policy_group and not opt.policy_groups: @@ -33,32 +34,65 @@ if __name__ == "__main__": if len(args) >= 1: binary = args[0] - try: - easyp = apparmor.easyprof.AppArmorEasyProfile(binary, opt) - except AppArmorException as e: - error(e.value) - except Exception: - raise + # parse_manifest() returns a list of tuples (binary, options). Create a + # list of these profile tuples to support multiple profiles in one manifest + profiles = [] + if opt.manifest: + try: + # should hide this in a common function + if sys.version_info[0] >= 3: + f = open(opt.manifest, "r", encoding="utf-8") + else: + f = open(opt.manifest, "r") + manifest = f.read() + except EnvironmentError as e: + error("Could not read '%s': %s (%d)\n" % (opt.manifest, + os.strerror(e.errno), + e.errno)) + profiles = apparmor.easyprof.parse_manifest(manifest, opt) + else: # fake up a tuple list when processing command line args + profiles.append( (binary, opt) ) - 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)] + count = 0 + for (binary, options) in profiles: + if len(profiles) > 1: + count += 1 + try: + easyp = apparmor.easyprof.AppArmorEasyProfile(binary, options) + except AppArmorException as e: + error(e.value) + except Exception: + raise + + if options.list_templates: + apparmor.easyprof.print_basefilenames(easyp.get_templates()) + sys.exit(0) + elif options.template and options.show_template: + files = [os.path.join(easyp.dirs['templates'], options.template)] apparmor.easyprof.print_files(files) - sys.exit(0) - elif binary is None: - error("Must specify full path to binary\n%s" % m) + sys.exit(0) + elif options.list_policy_groups: + apparmor.easyprof.print_basefilenames(easyp.get_policy_groups()) + sys.exit(0) + elif options.policy_groups and options.show_policy_group: + for g in options.policy_groups.split(','): + files = [os.path.join(easyp.dirs['policygroups'], g)] + apparmor.easyprof.print_files(files) + sys.exit(0) + elif binary == None and not options.profile_name and \ + not options.manifest: + error("Must specify binary and/or profile name\n%s" % m) - # if we made it here, generate a profile - params = apparmor.easyprof.gen_policy_params(binary, opt) - p = easyp.gen_policy(**params) - sys.stdout.write('%s\n' % p) + params = apparmor.easyprof.gen_policy_params(binary, options) + if options.manifest and options.verify_manifest and \ + not apparmor.easyprof.verify_manifest(params): + error("Manifest file requires review") + + if options.output_format == "json": + sys.stdout.write('%s\n' % easyp.gen_manifest(params)) + else: + params['no_verify'] = options.no_verify + try: + easyp.output_policy(params, count, opt.output_directory) + except AppArmorException as e: + error(e) diff --git a/utils/aa-easyprof.pod b/utils/aa-easyprof.pod index e47b65237..486edead2 100644 --- a/utils/aa-easyprof.pod +++ b/utils/aa-easyprof.pod @@ -78,8 +78,15 @@ 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. +binary. The NAME of the policy is typically only used for profile meta +data and does not specify the AppArmor profile name. + +=item --profile-name=PROFILENAME + +Specify the AppArmor profile name. When set, uses 'profile PROFILENAME' in the +profile. When set and specifying a binary, uses 'profile PROFILENAME BINARY' +in the profile. If not set, the binary will be used as the profile name and +profile attachment. =item --template-var="@{VAR}=VALUE" @@ -110,6 +117,32 @@ Display policy groups specified with --policy. Use PATH instead of system policy-groups directory. +=item --policy-version=VERSION + +Must be used with --policy-vendor and is used to specify the version of policy +groups and templates. When specified, B looks for the subdirectory +VENDOR/VERSION within the policy-groups and templates directory. The specified +version must be a positive decimal number compatible with the JSON Number type. +Eg, when using: + +=over + + $ aa-easyprof --templates-dir=/usr/share/apparmor/easyprof/templates \ + --policy-groups-dir=/usr/share/apparmor/easyprof/policygroups \ + --policy-vendor="foo" \ + --policy-version=1.0 + +=back + +Then /usr/share/apparmor/easyprof/templates/foo/1.0 will be searched for +templates and /usr/share/apparmor/easyprof/policygroups/foo/1.0 for policy +groups. + +=item --policy-vendor=VENDOR + +Must be used with --policy-version and is used to specify the vendor for policy +groups and templates. See --policy-version for more information. + =item --author Specify author of the policy. @@ -122,6 +155,104 @@ Specify copyright of the policy. Specify comment for the policy. +=item -m MANIFEST, --manifest=MANIFEST + +B also supports using a JSON manifest file for specifying options +related to policy. Unlike command line arguments, the JSON file may specify +multiple profiles. The structure of the JSON is: + + { + "security": { + "profiles": { + "": { + ... attributes specific to this profile ... + }, + "": { + ... + } + } + } + } + +Each profile JSON object (ie, everything under a profile name) may specify any +fields related to policy. The "security" JSON container object is optional and +may be omitted. An example manifest file demonstrating all fields is: + + { + "security": { + "profiles": { + "com.example.foo": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "networking", + "user-application" + ], + "policy_vendor": "somevendor", + "policy_version": 1.0, + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + } + } + } + } + +A manifest file does not have to include all the fields. Eg, a manifest file +for an Ubuntu SDK application might be: + + { + "security": { + "profiles": { + "com.ubuntu.developer.myusername.MyCoolApp": { + "policy_groups": [ + "networking", + "online-accounts" + ], + "policy_vendor": "ubuntu", + "policy_version": 1.0, + "template": "ubuntu-sdk", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } + } + +=item --verify-manifest + +When used with --manifest, warn about potentially unsafe definitions in the +manifest file. + +=item --output-format=FORMAT + +Specify either B (default if unspecified) for AppArmor policy output or +B for JSON manifest format. + +=item --output-directory=DIR + +Specify output directory for profile. If unspecified, policy is sent to stdout. + =back =head1 EXAMPLE @@ -130,7 +261,41 @@ 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 + $ aa-easyprof --template=user-application --template-var="@{APPNAME}=foo" \ + --policy-groups=opt-application,user-application \ + /opt/foo/bin/FooApp + +=back + +When using a manifest file: + +=over + + $ aa-easyprof --manifest=manifest.json + +=back + +To output a manifest file based on aa-easyprof arguments: + +=over + + $ aa-easyprof --output-format=json \ + --author="Your Name" \ + --comment="Unstructured single-line comment" \ + --copyright="Unstructured single-line copyright statement" \ + --name="My Foo App" \ + --profile-name="com.example.foo" \ + --template="user-application" \ + --policy-groups="user-application,networking" \ + --abstractions="audio,gnome" \ + --read-path="/tmp/foo_r" \ + --read-path="/tmp/bar_r/" \ + --write-path="/tmp/foo_w" \ + --write-path=/tmp/bar_w/ \ + --template-var="@{APPNAME}=foo" \ + --template-var="@{VAR1}=bar" \ + --template-var="@{VAR2}=baz" \ + "/opt/foo/**" =back diff --git a/utils/apparmor/easyprof.py b/utils/apparmor/easyprof.py index 035edf502..38b9bb0d5 100644 --- a/utils/apparmor/easyprof.py +++ b/utils/apparmor/easyprof.py @@ -1,6 +1,6 @@ # ------------------------------------------------------------------ # -# Copyright (C) 2011-2012 Canonical Ltd. +# Copyright (C) 2011-2013 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 @@ -11,10 +11,13 @@ from __future__ import with_statement import codecs +import copy import glob +import json import optparse import os import re +import shutil import subprocess import sys import tempfile @@ -123,29 +126,117 @@ def valid_binary_path(path): return True -def valid_variable_name(var): +def valid_variable(v): '''Validate variable name''' - if re.search(r'[a-zA-Z0-9_]+$', var): + debug("Checking '%s'" % v) + try: + (key, value) = v.split('=') + except Exception: + return False + + if not re.search(r'^@\{[a-zA-Z0-9_]+\}$', key): + return False + + if '/' in value: + rel_ok = False + if not value.startswith('/'): + rel_ok = True + if not valid_path(value, relative_ok=rel_ok): + return False + + if '"' in value: + return False + + # If we made it here, we are safe + return True + + +def valid_path(path, relative_ok=False): + '''Valid path''' + m = "Invalid path: %s" % (path) + if not relative_ok and not path.startswith('/'): + debug("%s (relative)" % (m)) + return False + + if '"' in path: # We double quote elsewhere + debug("%s (quote)" % (m)) + return False + + if '../' in path: + debug("%s (../ path escape)" % (m)) + return False + + try: + p = os.path.normpath(path) + except Exception: + debug("%s (could not normalize)" % (m)) + return False + + if p != path: + debug("%s (normalized path != path (%s != %s))" % (m, p, path)) + return False + + # If we made it here, we are safe + return True + + +def _is_safe(s): + '''Known safe regex''' + if re.search(r'^[a-zA-Z_0-9\-\.]+$', s): 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 +def valid_policy_vendor(s): + '''Verify the policy vendor''' + return _is_safe(s) + +def valid_policy_version(v): + '''Verify the policy version''' try: - os.path.normpath(path) - except Exception: - debug("%s (could not normalize)" % (m)) + float(v) + except ValueError: + return False + if float(v) < 0: return False return True +def valid_template_name(s, strict=False): + '''Verify the template name''' + if not strict and s.startswith('/'): + if not valid_path(s): + return False + return True + return _is_safe(s) + + +def valid_abstraction_name(s): + '''Verify the template name''' + return _is_safe(s) + + +def valid_profile_name(s): + '''Verify the profile name''' + # profile name specifies path + if s.startswith('/'): + if not valid_path(s): + return False + return True + + # profile name does not specify path + # alpha-numeric and Debian version, plus '_' + if re.search(r'^[a-zA-Z0-9][a-zA-Z0-9_\+\-\.:~]+$', s): + return True + return False + + +def valid_policy_group_name(s): + '''Verify policy group name''' + return _is_safe(s) + + def get_directory_contents(path): '''Find contents of the given directory''' if not valid_path(path): @@ -202,6 +293,7 @@ def verify_policy(policy): class AppArmorEasyProfile: '''Easy profile class''' def __init__(self, binary, opt): + verify_options(opt) opt.ensure_value("conffile", "/etc/apparmor/easyprof.conf") self.conffile = os.path.abspath(opt.conffile) @@ -222,6 +314,25 @@ class AppArmorEasyProfile: if opt.policy_groups_dir and os.path.isdir(opt.policy_groups_dir): self.dirs['policygroups'] = os.path.abspath(opt.policy_groups_dir) + + self.policy_version = None + self.policy_vendor = None + if (opt.policy_version and not opt.policy_vendor) or \ + (opt.policy_vendor and not opt.policy_version): + raise AppArmorException("Must specify both policy version and vendor") + if opt.policy_version and opt.policy_vendor: + self.policy_vendor = opt.policy_vendor + self.policy_version = str(opt.policy_version) + + for i in ['templates', 'policygroups']: + d = os.path.join(self.dirs[i], \ + self.policy_vendor, \ + self.policy_version) + if not os.path.isdir(d): + raise AppArmorException( + "Could not find %s directory '%s'" % (i, d)) + self.dirs[i] = d + if not 'templates' in self.dirs: raise AppArmorException("Could not find templates directory") if not 'policygroups' in self.dirs: @@ -230,19 +341,29 @@ class AppArmorEasyProfile: self.aa_topdir = "/etc/apparmor.d" self.binary = binary - if binary != None: + if binary: if not valid_binary_path(binary): raise AppArmorException("Invalid path for binary: '%s'" % binary) - self.set_template(opt.template) + if opt.manifest: + self.set_template(opt.template, allow_abs_path=False) + else: + 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']) + self.templates = [] + for f in get_directory_contents(self.dirs['templates']): + if os.path.isfile(f): + self.templates.append(f) + self.policy_groups = [] + for f in get_directory_contents(self.dirs['policygroups']): + if os.path.isfile(f): + self.policy_groups.append(f) def _get_defaults(self): '''Read in defaults from configuration''' @@ -282,11 +403,18 @@ class AppArmorEasyProfile: '''Get contents of current template''' return open(self.template).read() - def set_template(self, template): + def set_template(self, template, allow_abs_path=True): '''Set current template''' - self.template = template - if not template.startswith('/'): + if "../" in template: + raise AppArmorException('template "%s" contains "../" escape path' % (template)) + elif template.startswith('/') and not allow_abs_path: + raise AppArmorException("Cannot use an absolute path template '%s'" % template) + + if template.startswith('/'): + self.template = template + else: self.template = os.path.join(self.dirs['templates'], template) + if not os.path.exists(self.template): raise AppArmorException('%s does not exist' % (self.template)) @@ -327,9 +455,11 @@ class AppArmorEasyProfile: def gen_variable_declaration(self, dec): '''Generate a variable declaration''' - if not re.search(r'^@\{[a-zA-Z_]+\}=.+', dec): + if not valid_variable(dec): raise AppArmorException("Invalid variable declaration '%s'" % dec) - return dec + # Make sure we always quote + k, v = dec.split('=') + return '%s="%s"' % (k, v) def gen_path_rule(self, path, access): rule = [] @@ -352,7 +482,18 @@ class AppArmorEasyProfile: 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 gen_policy(self, name, + binary=None, + profile_name=None, + template_var=[], + abstractions=None, + policy_groups=None, + read_path=[], + write_path=[], + author=None, + comment=None, + copyright=None, + no_verify=False): def find_prefix(t, s): '''Calculate whitespace prefix based on occurrence of s in t''' pat = re.compile(r'^ *%s' % s) @@ -375,12 +516,22 @@ class AppArmorEasyProfile: tmp += line + "\n" policy = tmp - # Fill-in profile name and binary - policy = re.sub(r'###NAME###', name, policy) - if binary.startswith('/'): - policy = re.sub(r'###BINARY###', binary, policy) + attachment = "" + if binary: + if not valid_binary_path(binary): + raise AppArmorException("Invalid path for binary: '%s'" % \ + binary) + if profile_name: + attachment = 'profile "%s" "%s"' % (profile_name, binary) + else: + attachment = '"%s"' % binary + elif profile_name: + attachment = 'profile "%s"' % profile_name else: - policy = re.sub(r'###BINARY###', "profile %s" % binary, policy) + raise AppArmorException("Must specify binary and/or profile name") + policy = re.sub(r'###PROFILEATTACH###', attachment, policy) + + policy = re.sub(r'###NAME###', name, policy) # Fill-in various comment fields if comment != None: @@ -398,7 +549,9 @@ class AppArmorEasyProfile: s = "%s# No abstractions specified" % prefix if abstractions != None: s = "%s# Specified abstractions" % (prefix) - for i in abstractions.split(','): + t = abstractions.split(',') + t.sort() + for i in t: s += "\n%s%s" % (prefix, self.gen_abstraction_rule(i)) policy = re.sub(r' *%s' % search, s, policy) @@ -407,7 +560,9 @@ class AppArmorEasyProfile: 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(','): + t = policy_groups.split(',') + t.sort() + for i in t: for line in self.get_policygroup(i).splitlines(): s += "\n%s%s" % (prefix, line) if i != policy_groups.split(',')[-1]: @@ -419,6 +574,7 @@ class AppArmorEasyProfile: s = "%s# No template variables specified" % prefix if len(template_var) > 0: s = "%s# Specified profile variables" % (prefix) + template_var.sort() for i in template_var: s += "\n%s%s" % (prefix, self.gen_variable_declaration(i)) policy = re.sub(r' *%s' % search, s, policy) @@ -428,8 +584,9 @@ class AppArmorEasyProfile: s = "%s# No read paths specified" % prefix if len(read_path) > 0: s = "%s# Specified read permissions" % (prefix) + read_path.sort() for i in read_path: - for r in self.gen_path_rule(i, 'r'): + for r in self.gen_path_rule(i, 'rk'): s += "\n%s%s" % (prefix, r) policy = re.sub(r' *%s' % search, s, policy) @@ -438,17 +595,110 @@ class AppArmorEasyProfile: s = "%s# No write paths specified" % prefix if len(write_path) > 0: s = "%s# Specified write permissions" % (prefix) + write_path.sort() 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) + if no_verify: + debug("Skipping policy verification") + elif not verify_policy(policy): + msg("\n" + policy) raise AppArmorException("Invalid policy") return policy + def output_policy(self, params, count=0, dir=None): + '''Output policy''' + policy = self.gen_policy(**params) + if not dir: + if count: + sys.stdout.write('### aa-easyprof profile #%d ###\n' % count) + sys.stdout.write('%s\n' % policy) + else: + out_fn = "" + if 'profile_name' in params: + out_fn = params['profile_name'] + elif 'binary' in params: + out_fn = params['binary'] + else: # should not ever reach this + raise AppArmorException("Could not determine output filename") + + # Generate an absolute path, convertng any path delimiters to '.' + out_fn = os.path.join(dir, re.sub(r'/', '.', out_fn.lstrip('/'))) + if os.path.exists(out_fn): + raise AppArmorException("'%s' already exists" % out_fn) + + if not os.path.exists(dir): + os.mkdir(dir) + + if not os.path.isdir(dir): + raise AppArmorException("'%s' is not a directory" % dir) + + f, fn = tempfile.mkstemp(prefix='aa-easyprof') + if not isinstance(policy, bytes): + policy = policy.encode('utf-8') + os.write(f, policy) + os.close(f) + + shutil.move(fn, out_fn) + + def gen_manifest(self, params): + '''Take params list and output a JSON file''' + d = dict() + d['security'] = dict() + d['security']['profiles'] = dict() + + pkey = "" + if 'profile_name' in params: + pkey = params['profile_name'] + elif 'binary' in params: + # when profile_name is not specified, the binary (path attachment) + # also functions as the profile name + pkey = params['binary'] + else: + raise AppArmorException("Must supply binary or profile name") + + d['security']['profiles'][pkey] = dict() + + # Add the template since it isn't part of 'params' + template = os.path.basename(self.template) + if template != 'default': + d['security']['profiles'][pkey]['template'] = template + + # Add the policy_version since it isn't part of 'params' + if self.policy_version: + d['security']['profiles'][pkey]['policy_version'] = float(self.policy_version) + if self.policy_vendor: + d['security']['profiles'][pkey]['policy_vendor'] = self.policy_vendor + + for key in params: + if key == 'profile_name' or \ + (key == 'binary' and not 'profile_name' in params): + continue # don't re-add the pkey + elif key == 'binary' and not params[key]: + continue # binary can by None when specifying --profile-name + elif key == 'template_var': + d['security']['profiles'][pkey]['template_variables'] = dict() + for tvar in params[key]: + if not self.gen_variable_declaration(tvar): + raise AppArmorException("Malformed template_var '%s'" % tvar) + (k, v) = tvar.split('=') + k = k.lstrip('@').lstrip('{').rstrip('}') + d['security']['profiles'][pkey]['template_variables'][k] = v + elif key == 'abstractions' or key == 'policy_groups': + d['security']['profiles'][pkey][key] = params[key].split(",") + d['security']['profiles'][pkey][key].sort() + else: + d['security']['profiles'][pkey][key] = params[key] + json_str = json.dumps(d, + sort_keys=True, + indent=2, + separators=(',', ': ') + ) + return json_str + def print_basefilenames(files): for i in files: sys.stdout.write("%s\n" % (os.path.basename(i))) @@ -458,22 +708,65 @@ def print_files(files): with open(i) as f: sys.stdout.write(f.read()+"\n") +def check_manifest_conflict_args(option, opt_str, value, parser): + '''Check for -m/--manifest with conflicting args''' + conflict_args = ['abstractions', + 'read_path', + 'write_path', + # template always get set to 'default', can't conflict + # 'template', + 'policy_groups', + 'policy_version', + 'policy_vendor', + 'name', + 'profile_name', + 'comment', + 'copyright', + 'author', + 'template_var'] + for conflict in conflict_args: + if getattr(parser.values, conflict, False): + raise optparse.OptionValueError("can't use --%s with --manifest " \ + "argument" % conflict) + setattr(parser.values, option.dest, value) + +def check_for_manifest_arg(option, opt_str, value, parser): + '''Check for -m/--manifest with conflicting args''' + if parser.values.manifest: + raise optparse.OptionValueError("can't use --%s with --manifest " \ + "argument" % opt_str.lstrip('-')) + setattr(parser.values, option.dest, value) + +def check_for_manifest_arg_append(option, opt_str, value, parser): + '''Check for -m/--manifest with conflicting args (with append)''' + if parser.values.manifest: + raise optparse.OptionValueError("can't use --%s with --manifest " \ + "argument" % opt_str.lstrip('-')) + parser.values.ensure_value(option.dest, []).append(value) + def add_parser_policy_args(parser): '''Add parser arguments''' parser.add_option("-a", "--abstractions", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="abstractions", help="Comma-separated list of abstractions", metavar="ABSTRACTIONS") parser.add_option("--read-path", + action="callback", + callback=check_for_manifest_arg_append, + type=str, dest="read_path", help="Path allowing owner reads", - metavar="PATH", - action="append") + metavar="PATH") parser.add_option("--write-path", + action="callback", + callback=check_for_manifest_arg_append, + type=str, dest="write_path", help="Path allowing owner writes", - metavar="PATH", - action="append") + metavar="PATH") parser.add_option("-t", "--template", dest="template", help="Use non-default policy template", @@ -484,12 +777,36 @@ def add_parser_policy_args(parser): help="Use non-default templates directory", metavar="DIR") parser.add_option("-p", "--policy-groups", + action="callback", + callback=check_for_manifest_arg, + type=str, help="Comma-separated list of policy groups", metavar="POLICYGROUPS") parser.add_option("--policy-groups-dir", dest="policy_groups_dir", help="Use non-default policy-groups directory", metavar="DIR") + parser.add_option("--policy-version", + action="callback", + callback=check_for_manifest_arg, + type=str, + dest="policy_version", + help="Specify version for templates and policy groups", + metavar="VERSION") + parser.add_option("--policy-vendor", + action="callback", + callback=check_for_manifest_arg, + type=str, + dest="policy_vendor", + help="Specify vendor for templates and policy groups", + metavar="VENDOR") + parser.add_option("--profile-name", + action="callback", + callback=check_for_manifest_arg, + type=str, + dest="profile_name", + help="AppArmor profile name", + metavar="PROFILENAME") def parse_args(args=None, parser=None): '''Parse arguments''' @@ -506,6 +823,10 @@ def parse_args(args=None, parser=None): help="Show debugging output", action='store_true', default=False) + parser.add_option("--no-verify", + help="Don't verify policy using 'apparmor_parser -p'", + action='store_true', + default=False) parser.add_option("--list-templates", help="List available templates", action='store_true', @@ -523,31 +844,72 @@ def parse_args(args=None, parser=None): action='store_true', default=False) parser.add_option("-n", "--name", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="name", - help="Name of policy", - metavar="NAME") + help="Name of policy (not AppArmor profile name)", + metavar="COMMENT") parser.add_option("--comment", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="comment", help="Comment for policy", metavar="COMMENT") parser.add_option("--author", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="author", help="Author of policy", metavar="COMMENT") parser.add_option("--copyright", + action="callback", + callback=check_for_manifest_arg, + type=str, dest="copyright", help="Copyright for policy", metavar="COMMENT") parser.add_option("--template-var", + action="callback", + callback=check_for_manifest_arg_append, + type=str, dest="template_var", help="Declare AppArmor variable", - metavar="@{VARIABLE}=VALUE", - action="append") + metavar="@{VARIABLE}=VALUE") + parser.add_option("--output-format", + action="store", + dest="output_format", + help="Specify output format as text (default) or json", + metavar="FORMAT", + default="text") + parser.add_option("--output-directory", + action="store", + dest="output_directory", + help="Output policy to this directory", + metavar="DIR") + # This option conflicts with any of the value arguments, e.g. name, + # author, template-var, etc. + parser.add_option("-m", "--manifest", + action="callback", + callback=check_manifest_conflict_args, + type=str, + dest="manifest", + help="JSON manifest file", + metavar="FILE") + parser.add_option("--verify-manifest", + action="store_true", + default=False, + dest="verify_manifest", + help="Verify JSON manifest file") + # add policy args now add_parser_policy_args(parser) (my_opt, my_args) = parser.parse_args(args) + if my_opt.debug: DEBUGGING = True return (my_opt, my_args) @@ -555,10 +917,21 @@ def parse_args(args=None, parser=None): def gen_policy_params(binary, opt): '''Generate parameters for gen_policy''' params = dict(binary=binary) + + if not binary and not opt.profile_name: + raise AppArmorException("Must specify binary and/or profile name") + + if opt.profile_name: + params['profile_name'] = opt.profile_name + if opt.name: params['name'] = opt.name else: - params['name'] = os.path.basename(binary) + if opt.profile_name: + params['name'] = opt.profile_name + elif binary: + params['name'] = os.path.basename(binary) + if opt.template_var: # What about specified multiple times? params['template_var'] = opt.template_var if opt.abstractions: @@ -569,14 +942,183 @@ def gen_policy_params(binary, opt): 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 + if opt.policy_version and opt.output_format == "json": + params['policy_version'] = opt.policy_version + if opt.policy_vendor and opt.output_format == "json": + params['policy_vendor'] = opt.policy_vendor return params +def parse_manifest(manifest, opt_orig): + '''Take a JSON manifest as a string and updates options, returning an + updated binary. Note that a JSON file may contain multiple profiles.''' + + try: + m = json.loads(manifest) + except ValueError: + raise AppArmorException("Could not parse manifest") + + if 'security' in m: + top_table = m['security'] + else: + top_table = m + + if 'profiles' not in top_table: + raise AppArmorException("Could not parse manifest (could not find 'profiles')") + table = top_table['profiles'] + + # generally mirrors what is settable in gen_policy_params() + valid_keys = ['abstractions', + 'author', + 'binary', + 'comment', + 'copyright', + 'name', + 'policy_groups', + 'policy_version', + 'policy_vendor', + 'profile_name', + 'read_path', + 'template', + 'template_variables', + 'write_path', + ] + + profiles = [] + + for profile_name in table: + if not isinstance(table[profile_name], dict): + raise AppArmorException("Wrong JSON structure") + opt = copy.deepcopy(opt_orig) + + # The JSON structure is: + # { + # "security": { + # : { + # "binary": ... + # ... + # but because binary can be the profile name, we need to handle + # 'profile_name' and 'binary' special. If a profile_name starts with + # '/', then it is considered the binary. Otherwise, set the + # profile_name and set the binary if it is in the JSON. + binary = None + if profile_name.startswith('/'): + if 'binary' in table[profile_name]: + raise AppArmorException("Profile name should not specify path with binary") + binary = profile_name + else: + setattr(opt, 'profile_name', profile_name) + if 'binary' in table[profile_name]: + binary = table[profile_name]['binary'] + setattr(opt, 'binary', binary) + + for key in table[profile_name]: + if key not in valid_keys: + raise AppArmorException("Invalid key '%s'" % key) + + if key == 'binary': + continue # handled above + elif key == 'abstractions' or key == 'policy_groups': + setattr(opt, key, ",".join(table[profile_name][key])) + elif key == "template_variables": + t = table[profile_name]['template_variables'] + vlist = [] + for v in t.keys(): + vlist.append("@{%s}=%s" % (v, t[v])) + setattr(opt, 'template_var', vlist) + else: + if hasattr(opt, key): + setattr(opt, key, table[profile_name][key]) + + profiles.append( (binary, opt) ) + + return profiles + + +def verify_options(opt, strict=False): + '''Make sure our options are valid''' + if hasattr(opt, 'binary') and opt.binary and not valid_path(opt.binary): + raise AppArmorException("Invalid binary '%s'" % opt.binary) + if hasattr(opt, 'profile_name') and opt.profile_name != None and \ + not valid_profile_name(opt.profile_name): + raise AppArmorException("Invalid profile name '%s'" % opt.profile_name) + if hasattr(opt, 'binary') and opt.binary and \ + hasattr(opt, 'profile_name') and opt.profile_name != None and \ + opt.profile_name.startswith('/'): + raise AppArmorException("Profile name should not specify path with binary") + if hasattr(opt, 'policy_vendor') and opt.policy_vendor and \ + not valid_policy_vendor(opt.policy_vendor): + raise AppArmorException("Invalid policy vendor '%s'" % \ + opt.policy_vendor) + if hasattr(opt, 'policy_version') and opt.policy_version and \ + not valid_policy_version(opt.policy_version): + raise AppArmorException("Invalid policy version '%s'" % \ + opt.policy_version) + if hasattr(opt, 'template') and opt.template and \ + not valid_template_name(opt.template, strict): + raise AppArmorException("Invalid template '%s'" % opt.template) + if hasattr(opt, 'template_var') and opt.template_var: + for i in opt.template_var: + if not valid_variable(i): + raise AppArmorException("Invalid variable '%s'" % i) + if hasattr(opt, 'policy_groups') and opt.policy_groups: + for i in opt.policy_groups.split(','): + if not valid_policy_group_name(i): + raise AppArmorException("Invalid policy group '%s'" % i) + if hasattr(opt, 'abstractions') and opt.abstractions: + for i in opt.abstractions.split(','): + if not valid_abstraction_name(i): + raise AppArmorException("Invalid abstraction '%s'" % i) + if hasattr(opt, 'read_paths') and opt.read_paths: + for i in opt.read_paths: + if not valid_path(i): + raise AppArmorException("Invalid read path '%s'" % i) + if hasattr(opt, 'write_paths') and opt.write_paths: + for i in opt.write_paths: + if not valid_path(i): + raise AppArmorException("Invalid write path '%s'" % i) + + +def verify_manifest(params): + '''Verify manifest for safe and unsafe options''' + err_str = "" + (opt, args) = parse_args() + fake_easyp = AppArmorEasyProfile(None, opt) + + unsafe_keys = ['read_path', 'write_path'] + safe_abstractions = ['base'] + for k in params: + debug("Examining %s=%s" % (k, params[k])) + if k in unsafe_keys: + err_str += "\nfound %s key" % k + elif k == 'profile_name': + if params['profile_name'].startswith('/') or \ + '*' in params['profile_name']: + err_str += "\nprofile_name '%s'" % params['profile_name'] + elif k == 'abstractions': + for a in params['abstractions'].split(','): + if not a in safe_abstractions: + err_str += "\nfound '%s' abstraction" % a + elif k == "template_var": + pat = re.compile(r'[*/\{\}\[\]]') + for tv in params['template_var']: + if not fake_easyp.gen_variable_declaration(tv): + err_str += "\n%s" % tv + continue + tv_val = tv.split('=')[1] + debug("Examining %s" % tv_val) + if '..' in tv_val or pat.search(tv_val): + err_str += "\n%s" % tv + + if err_str: + warn("Manifest definition is potentially unsafe%s" % err_str) + return False + + return True + diff --git a/utils/easyprof/policygroups/networking b/utils/easyprof/policygroups/networking deleted file mode 100644 index c60a4ede2..000000000 --- a/utils/easyprof/policygroups/networking +++ /dev/null @@ -1,2 +0,0 @@ -# Policygroup to allow networking -#include diff --git a/utils/easyprof/templates/default b/utils/easyprof/templates/default index c6d0be497..d19483f21 100644 --- a/utils/easyprof/templates/default +++ b/utils/easyprof/templates/default @@ -13,7 +13,7 @@ ###VAR### -###BINARY### { +###PROFILEATTACH### { #include ###ABSTRACTIONS### diff --git a/utils/easyprof/templates/sandbox b/utils/easyprof/templates/sandbox index acc81f97c..80dfbfc13 100644 --- a/utils/easyprof/templates/sandbox +++ b/utils/easyprof/templates/sandbox @@ -13,7 +13,7 @@ ###VAR### -###BINARY### { +###PROFILEATTACH### { #include / r, /**/ r, diff --git a/utils/easyprof/templates/sandbox-x b/utils/easyprof/templates/sandbox-x index 077cb6049..45fe09830 100644 --- a/utils/easyprof/templates/sandbox-x +++ b/utils/easyprof/templates/sandbox-x @@ -13,7 +13,7 @@ ###VAR### -###BINARY### { +###PROFILEATTACH### { #include #include #include diff --git a/utils/easyprof/templates/user-application b/utils/easyprof/templates/user-application index 766da425e..dd5f3266f 100644 --- a/utils/easyprof/templates/user-application +++ b/utils/easyprof/templates/user-application @@ -16,7 +16,7 @@ ###VAR### -###BINARY### { +###PROFILEATTACH### { #include ###ABSTRACTIONS### diff --git a/utils/test/test-aa-easyprof.py b/utils/test/test-aa-easyprof.py index e59c13860..5607ab7fd 100755 --- a/utils/test/test-aa-easyprof.py +++ b/utils/test/test-aa-easyprof.py @@ -1,7 +1,7 @@ #! /usr/bin/env python # ------------------------------------------------------------------ # -# Copyright (C) 2011-2012 Canonical Ltd. +# Copyright (C) 2011-2013 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 @@ -10,6 +10,8 @@ # ------------------------------------------------------------------ import glob +import json +import optparse import os import shutil import sys @@ -31,6 +33,69 @@ def recursive_rm(dirPath, contents_only=False): if contents_only == False: os.rmdir(dirPath) +# From Lib/test/test_optparse.py from python 2.7.4 +class InterceptedError(Exception): + def __init__(self, + error_message=None, + exit_status=None, + exit_message=None): + self.error_message = error_message + self.exit_status = exit_status + self.exit_message = exit_message + + def __str__(self): + return self.error_message or self.exit_message or "intercepted error" + + +class InterceptingOptionParser(optparse.OptionParser): + def exit(self, status=0, msg=None): + raise InterceptedError(exit_status=status, exit_message=msg) + + def error(self, msg): + raise InterceptedError(error_message=msg) + + +class Manifest(object): + def __init__(self, profile_name): + self.security = dict() + self.security['profiles'] = dict() + self.profile_name = profile_name + self.security['profiles'][self.profile_name] = dict() + + def add_policygroups(self, policy_list): + self.security['profiles'][self.profile_name]['policy_groups'] = policy_list.split(",") + + def add_author(self, author): + self.security['profiles'][self.profile_name]['author'] = author + + def add_copyright(self, copyright): + self.security['profiles'][self.profile_name]['copyright'] = copyright + + def add_comment(self, comment): + self.security['profiles'][self.profile_name]['comment'] = comment + + def add_binary(self, binary): + self.security['profiles'][self.profile_name]['binary'] = binary + + def add_template(self, template): + self.security['profiles'][self.profile_name]['template'] = template + + def add_template_variable(self, name, value): + if not 'template_variables' in self.security['profiles'][self.profile_name]: + self.security['profiles'][self.profile_name]['template_variables'] = dict() + + self.security['profiles'][self.profile_name]['template_variables'][name] = value + + def emit_json(self, use_security_prefix=True): + manifest = dict() + manifest['security'] = self.security + if use_security_prefix: + dumpee = manifest + else: + dumpee = self.security + + return json.dumps(dumpee, indent=2) + # # Our test class # @@ -58,7 +123,7 @@ class T(unittest.TestCase): ###VAR### -###BINARY### { +###PROFILEATTACH### { #include ###ABSTRACTIONS### @@ -101,7 +166,8 @@ TEMPLATES_DIR="%s/templates" def tearDown(self): '''Teardown for tests''' if os.path.exists(self.tmpdir): - sys.stdout.write("%s\n" % self.tmpdir) + if debugging: + sys.stdout.write("%s\n" % self.tmpdir) recursive_rm(self.tmpdir) # @@ -206,6 +272,30 @@ TEMPLATES_DIR="%s/templates" 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_policygroups_dir_valid_with_vendor(self): + '''Test --policy-groups-dir (valid DIR with vendor)''' + 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)) + + vendor = "ubuntu" + version = "1.0" + valid_distro = os.path.join(valid, vendor, version) + os.mkdir(os.path.join(valid, vendor)) + os.mkdir(valid_distro) + shutil.copy(os.path.join(self.tmpdir, 'policygroups', self.test_policygroup), valid_distro) + + 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) + + self.assertTrue(easyp.dirs['policygroups'] == valid, "Not using specified --policy-groups-dir") + self.assertFalse(easyp.get_policy_groups() == None, "Could not find policy-groups") + for f in easyp.get_policy_groups(): + self.assertFalse(os.path.basename(f) == vendor, "Found '%s' in %s" % (vendor, f)) + def test_configuration_file_t_invalid(self): '''Test config parsing (invalid TEMPLATES_DIR)''' contents = ''' @@ -305,13 +395,51 @@ POLICYGROUPS_DIR="%s/templates" self.assertTrue(easyp.dirs['templates'] == valid, "Not using specified --template-dir") self.assertFalse(easyp.get_templates() == None, "Could not find templates") + def test_templates_dir_valid_with_vendor(self): + '''Test --templates-dir (valid DIR with vendor)''' + 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)) + + vendor = "ubuntu" + version = "1.0" + valid_distro = os.path.join(valid, vendor, version) + os.mkdir(os.path.join(valid, vendor)) + os.mkdir(valid_distro) + shutil.copy(os.path.join(self.tmpdir, 'templates', self.test_template), valid_distro) + + 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) + + self.assertTrue(easyp.dirs['templates'] == valid, "Not using specified --template-dir") + self.assertFalse(easyp.get_templates() == None, "Could not find templates") + for f in easyp.get_templates(): + self.assertFalse(os.path.basename(f) == vendor, "Found '%s' in %s" % (vendor, f)) + # # Binary file tests # - def test_binary(self): - '''Test binary''' + def test_binary_without_profile_name(self): + '''Test binary ( { })''' easyprof.AppArmorEasyProfile('/bin/ls', self.options) + def test_binary_with_profile_name(self): + '''Test binary (profile { })''' + args = self.full_args + args += ['--profile-name=some-profile-name'] + (self.options, self.args) = easyprof.parse_args(args) + easyprof.AppArmorEasyProfile('/bin/ls', self.options) + + def test_binary_omitted_with_profile_name(self): + '''Test binary (profile { })''' + args = self.full_args + args += ['--profile-name=some-profile-name'] + (self.options, self.args) = easyprof.parse_args(args) + easyprof.AppArmorEasyProfile(None, self.options) + def test_binary_nonexistent(self): '''Test binary (nonexistent)''' easyprof.AppArmorEasyProfile(os.path.join(self.tmpdir, 'nonexistent'), self.options) @@ -399,9 +527,109 @@ POLICYGROUPS_DIR="%s/templates" self.assertTrue(os.path.exists(path), "Could not find '%s'" % path) open(path).read() +# +# Manifest file argument tests +# + def test_manifest_argument(self): + '''Test manifest argument''' + + # setup our manifest + self.manifest = os.path.join(self.tmpdir, 'manifest.json') + contents = ''' +{"security": {"domain.reverse.appname": {"name": "simple-app"}}} +''' + open(self.manifest, 'w').write(contents) + + args = self.full_args + args.extend(['--manifest', self.manifest]) + easyprof.parse_args(args) + + def _manifest_conflicts(self, opt, value): + '''Helper for conflicts tests''' + # setup our manifest + self.manifest = os.path.join(self.tmpdir, 'manifest.json') + contents = ''' +{"security": {"domain.reverse.appname": {"binary": /nonexistent"}}} +''' + open(self.manifest, 'w').write(contents) + + # opt first + args = self.full_args + args.extend([opt, value, '--manifest', self.manifest]) + raised = False + try: + easyprof.parse_args(args, InterceptingOptionParser()) + except InterceptedError: + raised = True + + self.assertTrue(raised, msg="%s and manifest arguments did not " \ + "raise a parse error" % opt) + + # manifest first + args = self.full_args + args.extend(['--manifest', self.manifest, opt, value]) + raised = False + try: + easyprof.parse_args(args, InterceptingOptionParser()) + except InterceptedError: + raised = True + + self.assertTrue(raised, msg="%s and manifest arguments did not " \ + "raise a parse error" % opt) + + def test_manifest_conflicts_profilename(self): + '''Test manifest arg conflicts with profile_name arg''' + self._manifest_conflicts("--profile-name", "simple-app") + + def test_manifest_conflicts_copyright(self): + '''Test manifest arg conflicts with copyright arg''' + self._manifest_conflicts("--copyright", "2013-01-01") + + def test_manifest_conflicts_author(self): + '''Test manifest arg conflicts with author arg''' + self._manifest_conflicts("--author", "Foo Bar") + + def test_manifest_conflicts_comment(self): + '''Test manifest arg conflicts with comment arg''' + self._manifest_conflicts("--comment", "some comment") + + def test_manifest_conflicts_abstractions(self): + '''Test manifest arg conflicts with abstractions arg''' + self._manifest_conflicts("--abstractions", "base") + + def test_manifest_conflicts_read_path(self): + '''Test manifest arg conflicts with read-path arg''' + self._manifest_conflicts("--read-path", "/etc/passwd") + + def test_manifest_conflicts_write_path(self): + '''Test manifest arg conflicts with write-path arg''' + self._manifest_conflicts("--write-path", "/tmp/foo") + + def test_manifest_conflicts_policy_groups(self): + '''Test manifest arg conflicts with policy-groups arg''' + self._manifest_conflicts("--policy-groups", "opt-application") + + def test_manifest_conflicts_name(self): + '''Test manifest arg conflicts with name arg''' + self._manifest_conflicts("--name", "foo") + + def test_manifest_conflicts_template_var(self): + '''Test manifest arg conflicts with template-var arg''' + self._manifest_conflicts("--template-var", "foo") + + def test_manifest_conflicts_policy_version(self): + '''Test manifest arg conflicts with policy-version arg''' + self._manifest_conflicts("--policy-version", "1.0") + + def test_manifest_conflicts_policy_vendor(self): + '''Test manifest arg conflicts with policy-vendor arg''' + self._manifest_conflicts("--policy-vendor", "somevendor") + + # # Test genpolicy # + def _gen_policy(self, name=None, template=None, extra_args=[]): '''Generate a policy''' # Build up our args @@ -446,6 +674,40 @@ POLICYGROUPS_DIR="%s/templates" return p + def _gen_manifest_policy(self, manifest, use_security_prefix=True): + # Build up our args + args = self.full_args + args.append("--manifest=/dev/null") + + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(manifest.emit_json(use_security_prefix), self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, self.options) + params = easyprof.gen_policy_params(binary, self.options) + p = easyp.gen_policy(**params) + + # ###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: + sys.stdout.write("%s\n" % p) + + return p + + def test__is_safe(self): + '''Test _is_safe()''' + bad = [ + "/../../../../etc/passwd", + "abstraction with spaces", + "semicolon;bad", + "bad\x00baz", + "foo/bar", + "foo'bar", + 'foo"bar', + ] + for s in bad: + self.assertFalse(easyprof._is_safe(s), "'%s' should be bad" %s) + def test_genpolicy_templates_abspath(self): '''Test genpolicy (abspath to template)''' # create a new template @@ -521,6 +783,54 @@ POLICYGROUPS_DIR="%s/templates" inv_s = '###ABSTRACTIONS###' self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + def test_genpolicy_abstractions_bad(self): + '''Test genpolicy (abstractions - bad values)''' + bad = [ + "nonexistent", + "/../../../../etc/passwd", + "abstraction with spaces", + ] + for s in bad: + try: + self._gen_policy(extra_args=['--abstractions=%s' % s]) + except easyprof.AppArmorException: + continue + except Exception: + raise + raise Exception ("abstraction '%s' should be invalid" % s) + + def test_genpolicy_profile_name_bad(self): + '''Test genpolicy (profile name - bad values)''' + bad = [ + "/../../../../etc/passwd", + "../../../../etc/passwd", + "profile name with spaces", + ] + for s in bad: + try: + self._gen_policy(extra_args=['--profile-name=%s' % s]) + except easyprof.AppArmorException: + continue + except Exception: + raise + raise Exception ("profile_name '%s' should be invalid" % s) + + def test_genpolicy_policy_group_bad(self): + '''Test genpolicy (policy group - bad values)''' + bad = [ + "/../../../../etc/passwd", + "../../../../etc/passwd", + "profile name with spaces", + ] + for s in bad: + try: + self._gen_policy(extra_args=['--policy-groups=%s' % s]) + except easyprof.AppArmorException: + continue + except Exception: + raise + raise Exception ("policy group '%s' should be invalid" % s) + def test_genpolicy_policygroups(self): '''Test genpolicy (single policygroup)''' groups = self.test_policygroup @@ -566,7 +876,7 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (read-path file)''' s = "/opt/test-foo" p = self._gen_policy(extra_args=['--read-path=%s' % s]) - search = "%s r," % s + search = "%s rk," % 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)) @@ -575,7 +885,7 @@ POLICYGROUPS_DIR="%s/templates" '''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 + search = "owner %s rk," % 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)) @@ -584,7 +894,7 @@ POLICYGROUPS_DIR="%s/templates" '''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 + search = "owner %s rk," % 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)) @@ -593,7 +903,7 @@ POLICYGROUPS_DIR="%s/templates" '''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 + search = "owner %s rk," % 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)) @@ -602,7 +912,7 @@ POLICYGROUPS_DIR="%s/templates" '''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] + search_terms = ["%s rk," % s, "%s** rk," % s] for search in search_terms: self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' @@ -612,7 +922,7 @@ POLICYGROUPS_DIR="%s/templates" '''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] + search_terms = ["%s rk," % os.path.dirname(s), "%s rk," % s] for search in search_terms: self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' @@ -622,7 +932,7 @@ POLICYGROUPS_DIR="%s/templates" '''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] + search_terms = ["%s rk," % os.path.dirname(s), "%s rk," % s] for search in search_terms: self.assertTrue(search in p, "Could not find '%s' in:\n%s" % (search, p)) inv_s = '###READPATH###' @@ -646,13 +956,13 @@ POLICYGROUPS_DIR="%s/templates" 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)) + search_terms.append("%s rk," % (s)) + search_terms.append("%s%s** rk," % (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)) + search_terms.append("%s rk," % (os.path.dirname(s))) + search_terms.append("%s%s rk," % (owner, s)) else: - search_terms.append("%s%s r," % (owner, s)) + search_terms.append("%s%s rk," % (owner, s)) p = self._gen_policy(extra_args=args) for search in search_terms: @@ -784,33 +1094,48 @@ POLICYGROUPS_DIR="%s/templates" '''Test genpolicy (template-var single)''' s = "@{FOO}=bar" p = self._gen_policy(extra_args=['--template-var=%s' % s]) + k, v = s.split('=') + s = '%s="%s"' % (k, v) 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"] + 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: + k, v = s.split('=') + s = '%s="%s"' % (k, v) 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") + '''Test genpolicy (template-var - bad values)''' + bad = [ + "{FOO}=bar", + "@FOO}=bar", + "@{FOO=bar", + "FOO=bar", + "@FOO=bar", + "@{FOO}=/../../../etc/passwd", + "@{FOO}=bar=foo", + "@{FOO;BAZ}=bar", + '@{FOO}=bar"baz', + ] + for s in bad: + try: + self._gen_policy(extra_args=['--template-var=%s' % s]) + except easyprof.AppArmorException: + continue + except Exception: + raise + raise Exception ("template-var should be invalid") def test_genpolicy_invalid_template_policy(self): '''Test genpolicy (invalid template policy)''' @@ -835,6 +1160,1291 @@ POLICYGROUPS_DIR="%s/templates" raise raise Exception ("policy should be invalid") + def test_genpolicy_no_binary_without_profile_name(self): + '''Test genpolicy (no binary with no profile name)''' + try: + easyprof.gen_policy_params(None, self.options) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("No binary or profile name should have been invalid") + + def test_genpolicy_with_binary_with_profile_name(self): + '''Test genpolicy (binary with profile name)''' + profile_name = "some-profile-name" + p = self._gen_policy(extra_args=['--profile-name=%s' % profile_name]) + s = 'profile "%s" "%s" {' % (profile_name, self.binary) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###PROFILEATTACH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_with_binary_without_profile_name(self): + '''Test genpolicy (binary without profile name)''' + p = self._gen_policy() + s = '"%s" {' % (self.binary) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###PROFILEATTACH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_genpolicy_without_binary_with_profile_name(self): + '''Test genpolicy (no binary with profile name)''' + profile_name = "some-profile-name" + args = self.full_args + args.append('--profile-name=%s' % profile_name) + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(None, self.options) + params = easyprof.gen_policy_params(None, self.options) + p = easyp.gen_policy(**params) + s = 'profile "%s" {' % (profile_name) + self.assertTrue(s in p, "Could not find '%s' in:\n%s" % (s, p)) + inv_s = '###PROFILEATTACH###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + +# manifest tests + + def test_gen_manifest_policy_with_binary_with_profile_name(self): + '''Test gen_manifest_policy (binary with profile name)''' + m = Manifest("test_gen_manifest_policy") + m.add_binary('/bin/ls') + self._gen_manifest_policy(m) + + def test_gen_manifest_policy_without_binary_with_profile_name(self): + '''Test gen_manifest_policy (no binary with profile name)''' + m = Manifest("test_gen_manifest_policy") + self._gen_manifest_policy(m) + + def test_gen_manifest_policy_templates_system(self): + '''Test gen_manifest_policy (system template)''' + m = Manifest("test_gen_manifest_policy") + m.add_template(self.test_template) + self._gen_manifest_policy(m) + + def test_gen_manifest_policy_templates_system_noprefix(self): + '''Test gen_manifest_policy (system template, no security prefix)''' + m = Manifest("test_gen_manifest_policy") + m.add_template(self.test_template) + self._gen_manifest_policy(m, use_security_prefix=False) + + def test_gen_manifest_abs_path_template(self): + '''Test gen_manifest_policy (abs path template)''' + m = Manifest("test_gen_manifest_policy") + m.add_template("/etc/shadow") + try: + self._gen_manifest_policy(m) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("abs path template name should be invalid") + + def test_gen_manifest_escape_path_templates(self): + '''Test gen_manifest_policy (esc path template)''' + m = Manifest("test_gen_manifest_policy") + m.add_template("../../../../../../../../etc/shadow") + try: + self._gen_manifest_policy(m) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("../ template name should be invalid") + + def test_gen_manifest_policy_templates_nonexistent(self): + '''Test gen manifest policy (nonexistent template)''' + m = Manifest("test_gen_manifest_policy") + m.add_template("nonexistent") + try: + self._gen_manifest_policy(m) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("template should be invalid") + + def test_gen_manifest_policy_comment(self): + '''Test gen manifest policy (comment)''' + s = "test comment" + m = Manifest("test_gen_manifest_policy") + m.add_comment(s) + p = self._gen_manifest_policy(m) + 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_gen_manifest_policy_author(self): + '''Test gen manifest policy (author)''' + s = "Archibald Poindexter" + m = Manifest("test_gen_manifest_policy") + m.add_author(s) + p = self._gen_manifest_policy(m) + 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_gen_manifest_policy_copyright(self): + '''Test genpolicy (copyright)''' + s = "2112/01/01" + m = Manifest("test_gen_manifest_policy") + m.add_copyright(s) + p = self._gen_manifest_policy(m) + 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_gen_manifest_policy_policygroups(self): + '''Test gen manifest policy (single policygroup)''' + groups = self.test_policygroup + m = Manifest("test_gen_manifest_policy") + m.add_policygroups(groups) + p = self._gen_manifest_policy(m) + + 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_gen_manifest_policy_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) + m = Manifest("test_gen_manifest_policy") + m.add_policygroups(groups) + p = self._gen_manifest_policy(m) + + 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_gen_manifest_policy_policygroups_nonexistent(self): + '''Test gen manifest policy (nonexistent policygroup)''' + groups = "nonexistent" + m = Manifest("test_gen_manifest_policy") + m.add_policygroups(groups) + try: + self._gen_manifest_policy(m) + except easyprof.AppArmorException: + return + except Exception: + raise + raise Exception ("policygroup should be invalid") + + def test_gen_manifest_policy_templatevar(self): + '''Test gen manifest policy (template-var single)''' + m = Manifest("test_gen_manifest_policy") + m.add_template_variable("FOO", "bar") + p = self._gen_manifest_policy(m) + s = '@{FOO}="bar"' + 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_gen_manifest_policy_templatevar_multiple(self): + '''Test gen manifest policy (template-var multiple)''' + variables = [["FOO", "bar"], ["BAR", "baz"]] + m = Manifest("test_gen_manifest_policy") + for s in variables: + m.add_template_variable(s[0], s[1]) + + p = self._gen_manifest_policy(m) + for s in variables: + str_s = '@{%s}="%s"' % (s[0], s[1]) + self.assertTrue(str_s in p, "Could not find '%s' in:\n%s" % (str_s, p)) + inv_s = '###TEMPLATEVAR###' + self.assertFalse(inv_s in p, "Found '%s' in :\n%s" % (inv_s, p)) + + def test_gen_manifest_policy_invalid_keys(self): + '''Test gen manifest policy (invalid keys)''' + keys = ['config_file', + 'debug', + 'help', + 'list-templates', + 'list_templates', + 'show-template', + 'show_template', + 'list-policy-groups', + 'list_policy_groups', + 'show-policy-group', + 'show_policy_group', + 'templates-dir', + 'templates_dir', + 'policy-groups-dir', + 'policy_groups_dir', + 'nonexistent', + 'no_verify', + ] + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + for k in keys: + security = dict() + security["profile_name"] = "test-app" + security[k] = "bad" + j = json.dumps(security, indent=2) + try: + easyprof.parse_manifest(j, self.options) + except easyprof.AppArmorException: + continue + raise Exception ("'%s' should be invalid" % k) + + def test_gen_manifest(self): + '''Test gen_manifest''' + # this should come from manpage + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "opt-application", + "user-application" + ], + "policy_vendor": "somevendor", + "policy_version": 1.0, + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + } + } + } +}''' + + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, "somevendor/1.0")) + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, self.options) + params = easyprof.gen_policy_params(binary, self.options) + + # verify we get the same manifest back + man_new = easyp.gen_manifest(params) + self.assertEquals(m, man_new) + + def test_gen_manifest_ubuntu(self): + '''Test gen_manifest (ubuntu)''' + # this should be based on the manpage (but use existing policy_groups + # and template + m = '''{ + "security": { + "profiles": { + "com.ubuntu.developer.myusername.MyCoolApp": { + "name": "MyCoolApp", + "policy_groups": [ + "opt-application", + "user-application" + ], + "policy_vendor": "ubuntu", + "policy_version": 1.0, + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } +}''' + + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, "ubuntu/1.0")) + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, self.options) + params = easyprof.gen_policy_params(binary, self.options) + + # verify we get the same manifest back + man_new = easyp.gen_manifest(params) + self.assertEquals(m, man_new) + + def test_parse_manifest_no_version(self): + '''Test parse_manifest (vendor with no version)''' + # this should come from manpage + m = '''{ + "security": { + "profiles": { + "com.ubuntu.developer.myusername.MyCoolApp": { + "policy_groups": [ + "opt-application", + "user-application" + ], + "policy_vendor": "ubuntu", + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } +}''' + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + try: + easyprof.AppArmorEasyProfile(binary, self.options) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed on missing version") + + def test_parse_manifest_no_vendor(self): + '''Test parse_manifest (version with no vendor)''' + # this should come from manpage + m = '''{ + "security": { + "profiles": { + "com.ubuntu.developer.myusername.MyCoolApp": { + "policy_groups": [ + "opt-application", + "user-application" + ], + "policy_version": 1.0, + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } +}''' + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + try: + easyprof.AppArmorEasyProfile(binary, self.options) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed on missing vendor") + + def test_parse_manifest_multiple(self): + '''Test parse_manifest_multiple''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "opt-application", + "user-application" + ], + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + }, + "com.ubuntu.developer.myusername.MyCoolApp": { + "policy_groups": [ + "opt-application" + ], + "policy_vendor": "ubuntu", + "policy_version": 1.0, + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + } + } + } +}''' + + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, "ubuntu/1.0")) + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + profiles = easyprof.parse_manifest(m, self.options) + for (binary, options) in profiles: + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + easyp.gen_manifest(params) + easyp.gen_policy(**params) + + +# verify manifest tests + def _verify_manifest(self, m, expected, invalid=False): + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + try: + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + except easyprof.AppArmorException: + if invalid: + return + raise + params = easyprof.gen_policy_params(binary, options) + if expected: + self.assertTrue(easyprof.verify_manifest(params), "params=%s\nmanifest=%s" % (params,m)) + else: + self.assertFalse(easyprof.verify_manifest(params), "params=%s\nmanifest=%s" % (params,m)) + + def test_verify_manifest_full(self): + '''Test verify_manifest (full)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "abstractions": [ + "base" + ], + "author": "Your Name", + "binary": "/opt/com.example/foo/**", + "comment": "some free-form single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "foo", + "policy_groups": [ + "user-application", + "opt-application" + ], + "template": "user-application", + "template_variables": { + "OK1": "foo", + "OK2": "com.example.foo" + } + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_full_bad(self): + '''Test verify_manifest (full bad)''' + m = '''{ + "security": { + "profiles": { + "/com.example.foo": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/usr/foo/**", + "comment": "some free-form single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "foo", + "policy_groups": [ + "user-application", + "opt-application" + ], + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "VAR1": "f*o", + "VAR2": "*foo", + "VAR3": "fo*", + "VAR4": "b{ar", + "VAR5": "b{a,r}", + "VAR6": "b}ar", + "VAR7": "bar[0-9]", + "VAR8": "b{ar", + "VAR9": "/tmp/../etc/passwd" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + } + } + } +}''' + + self._verify_manifest(m, expected=False, invalid=True) + + def test_verify_manifest_binary(self): + '''Test verify_manifest (binary in /usr)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/usr/foo/**", + "template": "user-application" + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_profile_profile_name_bad(self): + '''Test verify_manifest (bad profile_name)''' + m = '''{ + "security": { + "profiles": { + "/foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' + self._verify_manifest(m, expected=False, invalid=True) + + m = '''{ + "security": { + "profiles": { + "bin/*": { + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' + self._verify_manifest(m, expected=False) + + def test_verify_manifest_profile_profile_name(self): + '''Test verify_manifest (profile_name)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_profile_abstractions(self): + '''Test verify_manifest (abstractions)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "base" + ] + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_profile_abstractions_bad(self): + '''Test verify_manifest (bad abstractions)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "user-tmp" + ] + } + } + } +}''' + self._verify_manifest(m, expected=False) + + def test_verify_manifest_profile_template_var(self): + '''Test verify_manifest (good template_var)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/something with spaces/**", + "template": "user-application", + "template_variables": { + "OK1": "foo", + "OK2": "com.example.foo", + "OK3": "something with spaces" + } + } + } + } +}''' + self._verify_manifest(m, expected=True) + + def test_verify_manifest_profile_template_var_bad(self): + '''Test verify_manifest (bad template_var)''' + for v in ['"VAR1": "f*o"', + '"VAR2": "*foo"', + '"VAR3": "fo*"', + '"VAR4": "b{ar"', + '"VAR5": "b{a,r}"', + '"VAR6": "b}ar"', + '"VAR7": "bar[0-9]"', + '"VAR8": "b{ar"', + '"VAR9": "foo/bar"' # this is valid, but potentially unsafe + ]: + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "template_variables": { + %s + } + } + } + } +}''' % v + self._verify_manifest(m, expected=False) + + def test_manifest_invalid(self): + '''Test invalid manifest (parse error)''' + m = '''{ + "security": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "base" + ] +}''' + self._verify_manifest(m, expected=False, invalid=True) + + def test_manifest_invalid2(self): + '''Test invalid manifest (profile_name is not key)''' + m = '''{ + "security": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "base" + ] + } +}''' + self._verify_manifest(m, expected=False, invalid=True) + + def test_manifest_invalid3(self): + '''Test invalid manifest (profile_name in dict)''' + m = '''{ + "security": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "abstractions": [ + "base" + ], + "profile_name": "com.example.foo" + } +}''' + self._verify_manifest(m, expected=False, invalid=True) + + def test_manifest_invalid4(self): + '''Test invalid manifest (bad path in template var)''' + for v in ['"VAR1": "/tmp/../etc/passwd"', + '"VAR2": "./"', + '"VAR3": "foo\"bar"', + '"VAR4": "foo//bar"', + ]: + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "binary": "/opt/com.example/foo/**", + "template": "user-application", + "template_variables": { + %s + } + } + } + } +}''' % v + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + params = easyprof.gen_policy_params(binary, options) + try: + easyprof.verify_manifest(params) + except easyprof.AppArmorException: + return + + raise Exception ("Should have failed with invalid variable declaration") + + +# policy version tests + def test_policy_vendor_manifest_nonexistent(self): + '''Test policy vendor via manifest (nonexistent)''' + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "policy_vendor": "nonexistent", + "policy_version": 1.0, + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' + # Build up our args + args = self.full_args + args.append("--manifest=/dev/null") + + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + try: + easyprof.AppArmorEasyProfile(binary, self.options) + except easyprof.AppArmorException: + return + + raise Exception ("Should have failed with non-existent directory") + + def test_policy_version_manifest(self): + '''Test policy version via manifest (good)''' + policy_vendor = "somevendor" + policy_version = "1.0" + policy_subdir = "%s/%s" % (policy_vendor, policy_version) + m = '''{ + "security": { + "profiles": { + "com.example.foo": { + "policy_vendor": "%s", + "policy_version": %s, + "binary": "/opt/com.example/foo/**", + "template": "user-application" + } + } + } +}''' % (policy_vendor, policy_version) + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, policy_subdir)) + + # Build up our args + args = self.full_args + args.append("--manifest=/dev/null") + + (self.options, self.args) = easyprof.parse_args(args) + (binary, self.options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, self.options) + + tdir = os.path.join(self.tmpdir, 'templates', policy_subdir) + for t in easyp.get_templates(): + self.assertTrue(t.startswith(tdir)) + + pdir = os.path.join(self.tmpdir, 'policygroups', policy_subdir) + for p in easyp.get_policy_groups(): + self.assertTrue(p.startswith(pdir)) + + params = easyprof.gen_policy_params(binary, self.options) + easyp.gen_policy(**params) + + def test_policy_vendor_version_args(self): + '''Test policy vendor and version via command line args (good)''' + policy_version = "1.0" + policy_vendor = "somevendor" + policy_subdir = "%s/%s" % (policy_vendor, policy_version) + + # Create the directories + for d in ['policygroups', 'templates']: + shutil.copytree(os.path.join(self.tmpdir, d), + os.path.join(self.tmpdir, d, policy_subdir)) + + # Build up our args + args = self.full_args + args.append("--policy-version=%s" % policy_version) + args.append("--policy-vendor=%s" % policy_vendor) + + (self.options, self.args) = easyprof.parse_args(args) + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + easyp = easyprof.AppArmorEasyProfile(self.binary, self.options) + + tdir = os.path.join(self.tmpdir, 'templates', policy_subdir) + for t in easyp.get_templates(): + self.assertTrue(t.startswith(tdir), \ + "'%s' does not start with '%s'" % (t, tdir)) + + pdir = os.path.join(self.tmpdir, 'policygroups', policy_subdir) + for p in easyp.get_policy_groups(): + self.assertTrue(p.startswith(pdir), \ + "'%s' does not start with '%s'" % (p, pdir)) + + params = easyprof.gen_policy_params(self.binary, self.options) + easyp.gen_policy(**params) + + def test_policy_vendor_args_nonexistent(self): + '''Test policy vendor via command line args (nonexistent)''' + policy_vendor = "nonexistent" + policy_version = "1.0" + args = self.full_args + args.append("--policy-version=%s" % policy_version) + args.append("--policy-vendor=%s" % policy_vendor) + + (self.options, self.args) = easyprof.parse_args(args) + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + return + + raise Exception ("Should have failed with non-existent directory") + + def test_policy_version_args_bad(self): + '''Test policy version via command line args (bad)''' + bad = [ + "../../../../../../etc", + "notanumber", + "v1.0a", + "-1", + ] + for policy_version in bad: + args = self.full_args + args.append("--policy-version=%s" % policy_version) + args.append("--policy-vendor=somevendor") + + (self.options, self.args) = easyprof.parse_args(args) + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + continue + + raise Exception ("Should have failed with bad version") + + def test_policy_vendor_args_bad(self): + '''Test policy vendor via command line args (bad)''' + bad = [ + "../../../../../../etc", + "vendor with space", + "semicolon;isbad", + ] + for policy_vendor in bad: + args = self.full_args + args.append("--policy-vendor=%s" % policy_vendor) + args.append("--policy-version=1.0") + + (self.options, self.args) = easyprof.parse_args(args) + (self.options, self.args) = easyprof.parse_args(self.full_args + [self.binary]) + try: + easyprof.AppArmorEasyProfile(self.binary, self.options) + except easyprof.AppArmorException: + continue + + raise Exception ("Should have failed with bad vendor") + +# output_directory tests + def test_output_directory_multiple(self): + '''Test output_directory (multiple)''' + files = dict() + files["com.example.foo"] = "com.example.foo" + files["com.ubuntu.developer.myusername.MyCoolApp"] = "com.ubuntu.developer.myusername.MyCoolApp" + files["usr.bin.baz"] = "/usr/bin/baz" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "opt-application", + "user-application" + ], + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + }, + "%s": { + "policy_groups": [ + "opt-application", + "user-application" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "MyCoolApp", + "APPVERSION": "0.1.2" + } + }, + "%s": { + "abstractions": [ + "gnome" + ], + "policy_groups": [ + "user-application" + ], + "template_variables": { + "APPNAME": "baz" + } + } + } + } +}''' % (files["com.example.foo"], + files["com.ubuntu.developer.myusername.MyCoolApp"], + files["usr.bin.baz"]) + + out_dir = os.path.join(self.tmpdir, "output") + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + profiles = easyprof.parse_manifest(m, self.options) + for (binary, options) in profiles: + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + easyp.output_policy(params, dir=out_dir) + + for fn in files: + f = os.path.join(out_dir, fn) + self.assertTrue(os.path.exists(f), "Could not find '%s'" % f) + + def test_output_directory_single(self): + '''Test output_directory (single)''' + files = dict() + files["com.example.foo"] = "com.example.foo" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "audio", + "gnome" + ], + "author": "Your Name", + "binary": "/opt/foo/**", + "comment": "Unstructured single-line comment", + "copyright": "Unstructured single-line copyright statement", + "name": "My Foo App", + "policy_groups": [ + "opt-application", + "user-application" + ], + "read_path": [ + "/tmp/foo_r", + "/tmp/bar_r/" + ], + "template": "user-application", + "template_variables": { + "APPNAME": "foo", + "VAR1": "bar", + "VAR2": "baz" + }, + "write_path": [ + "/tmp/foo_w", + "/tmp/bar_w/" + ] + } + } + } +}''' % (files["com.example.foo"]) + + out_dir = os.path.join(self.tmpdir, "output") + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + profiles = easyprof.parse_manifest(m, self.options) + for (binary, options) in profiles: + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + easyp.output_policy(params, dir=out_dir) + + for fn in files: + f = os.path.join(out_dir, fn) + self.assertTrue(os.path.exists(f), "Could not find '%s'" % f) + + + + + def test_output_directory_invalid(self): + '''Test output_directory (output directory exists as file)''' + files = dict() + files["usr.bin.baz"] = "/usr/bin/baz" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "gnome" + ], + "policy_groups": [ + "user-application" + ], + "template_variables": { + "APPNAME": "baz" + } + } + } + } +}''' % files["usr.bin.baz"] + + + out_dir = os.path.join(self.tmpdir, "output") + open(out_dir, 'w').close() + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + try: + easyp.output_policy(params, dir=out_dir) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed with 'is not a directory'") + + def test_output_directory_invalid_params(self): + '''Test output_directory (no binary or profile_name)''' + files = dict() + files["usr.bin.baz"] = "/usr/bin/baz" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "gnome" + ], + "policy_groups": [ + "user-application" + ], + "template_variables": { + "APPNAME": "baz" + } + } + } + } +}''' % files["usr.bin.baz"] + + out_dir = os.path.join(self.tmpdir, "output") + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + del params['binary'] + try: + easyp.output_policy(params, dir=out_dir) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed with 'Must specify binary and/or profile name'") + + def test_output_directory_invalid2(self): + '''Test output_directory (profile exists)''' + files = dict() + files["usr.bin.baz"] = "/usr/bin/baz" + m = '''{ + "security": { + "profiles": { + "%s": { + "abstractions": [ + "gnome" + ], + "policy_groups": [ + "user-application" + ], + "template_variables": { + "APPNAME": "baz" + } + } + } + } +}''' % files["usr.bin.baz"] + + out_dir = os.path.join(self.tmpdir, "output") + os.mkdir(out_dir) + open(os.path.join(out_dir, "usr.bin.baz"), 'w').close() + + args = self.full_args + args.append("--manifest=/dev/null") + (self.options, self.args) = easyprof.parse_args(args) + (binary, options) = easyprof.parse_manifest(m, self.options)[0] + easyp = easyprof.AppArmorEasyProfile(binary, options) + params = easyprof.gen_policy_params(binary, options) + try: + easyp.output_policy(params, dir=out_dir) + except easyprof.AppArmorException: + return + raise Exception ("Should have failed with 'already exists'") + + def test_output_directory_args(self): + '''Test output_directory (args)''' + files = dict() + files["usr.bin.baz"] = "/usr/bin/baz" + + # Build up our args + args = self.full_args + args.append('--template=%s' % self.test_template) + args.append('--name=%s' % 'foo') + args.append(files["usr.bin.baz"]) + + out_dir = os.path.join(self.tmpdir, "output") + + # Now parse our args + (self.options, self.args) = easyprof.parse_args(args) + easyp = easyprof.AppArmorEasyProfile(files["usr.bin.baz"], self.options) + params = easyprof.gen_policy_params(files["usr.bin.baz"], self.options) + easyp.output_policy(params, dir=out_dir) + + for fn in files: + f = os.path.join(out_dir, fn) + self.assertTrue(os.path.exists(f), "Could not find '%s'" % f) + +# +# utility classes +# + def test_valid_profile_name(self): + '''Test valid_profile_name''' + names = ['foo', + 'com.example.foo', + '/usr/bin/foo', + 'com.example.app_myapp_1:2.3+ab12~foo', + ] + for n in names: + self.assertTrue(easyprof.valid_profile_name(n), "'%s' should be valid" % n) + + def test_valid_profile_name_invalid(self): + '''Test valid_profile_name (invalid)''' + names = ['fo/o', + '/../../etc/passwd', + '../../etc/passwd', + './../etc/passwd', + './etc/passwd', + '/usr/bin//foo', + '/usr/bin/./foo', + 'foo`', + 'foo!', + 'foo@', + 'foo$', + 'foo#', + 'foo%', + 'foo^', + 'foo&', + 'foo*', + 'foo(', + 'foo)', + 'foo=', + 'foo{', + 'foo}', + 'foo[', + 'foo]', + 'foo|', + 'foo/', + 'foo\\', + 'foo;', + 'foo\'', + 'foo"', + 'foo<', + 'foo>', + 'foo?', + 'foo\/', + 'foo,', + '_foo', + ] + for n in names: + self.assertFalse(easyprof.valid_profile_name(n), "'%s' should be invalid" % n) + + def test_valid_path(self): + '''Test valid_path''' + names = ['/bin/bar', + '/etc/apparmor.d/com.example.app_myapp_1:2.3+ab12~foo', + ] + names_rel = ['bin/bar', + 'apparmor.d/com.example.app_myapp_1:2.3+ab12~foo', + 'com.example.app_myapp_1:2.3+ab12~foo', + ] + for n in names: + self.assertTrue(easyprof.valid_path(n), "'%s' should be valid" % n) + for n in names_rel: + self.assertTrue(easyprof.valid_path(n, relative_ok=True), "'%s' should be valid" % n) + + def test_zz_valid_path_invalid(self): + '''Test valid_path (invalid)''' + names = ['/bin//bar', + 'bin/bar', + '/../etc/passwd', + './bin/bar', + './', + ] + names_rel = ['bin/../bar', + 'apparmor.d/../passwd', + 'com.example.app_"myapp_1:2.3+ab12~foo', + ] + for n in names: + self.assertFalse(easyprof.valid_path(n, relative_ok=False), "'%s' should be invalid" % n) + for n in names_rel: + self.assertFalse(easyprof.valid_path(n, relative_ok=True), "'%s' should be invalid" % n) + # # End test class