From 311163203ae64c567411bcc15b60dbefec26af14 Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Thu, 13 Feb 2014 22:19:26 +0100 Subject: [PATCH 01/13] dovecot profiles - use abstractions/nameservice After testing the dovecot profiles on a new server, I noticed /usr/lib/dovecot/dict and /usrlib/dovecot/lmtp need more nameservice- related permissions. Therefore include abstractions/nameservice instead of adding more and more files. Acked-by: John Johansen (on IRC) --- profiles/apparmor.d/usr.lib.dovecot.dict | 3 +-- profiles/apparmor.d/usr.lib.dovecot.lmtp | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/profiles/apparmor.d/usr.lib.dovecot.dict b/profiles/apparmor.d/usr.lib.dovecot.dict index c48a5b74b..021fdb11c 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.dict +++ b/profiles/apparmor.d/usr.lib.dovecot.dict @@ -14,6 +14,7 @@ /usr/lib/dovecot/dict { #include #include + #include capability setgid, capability setuid, @@ -22,8 +23,6 @@ /etc/dovecot/dovecot-database.conf.ext r, /etc/dovecot/dovecot-dict-sql.conf.ext r, - /etc/nsswitch.conf r, - /etc/services r, /usr/lib/dovecot/dict mr, # Site-specific additions and overrides. See local/README for details. diff --git a/profiles/apparmor.d/usr.lib.dovecot.lmtp b/profiles/apparmor.d/usr.lib.dovecot.lmtp index dd8cd5a34..2c43d4c39 100644 --- a/profiles/apparmor.d/usr.lib.dovecot.lmtp +++ b/profiles/apparmor.d/usr.lib.dovecot.lmtp @@ -14,6 +14,7 @@ /usr/lib/dovecot/lmtp { #include + #include deny capability block_suspend, @@ -24,7 +25,6 @@ @{DOVECOT_MAILSTORE}/ rw, @{DOVECOT_MAILSTORE}/** rwkl, - /etc/resolv.conf r, /proc/*/mounts r, /tmp/dovecot.lmtp.* rw, /usr/lib/dovecot/lmtp mr, From 37ecdcfce59e53e7cc9c7ac62b38d624a1aac354 Mon Sep 17 00:00:00 2001 From: Seth Arnold Date: Thu, 13 Feb 2014 17:15:03 -0800 Subject: [PATCH 02/13] =?UTF-8?q?Description:=20Allow=20using=20sssd=20for?= =?UTF-8?q?=20group=20and=20password=20lookups=20Author:=20St=C3=A9phane?= =?UTF-8?q?=20Graber=20=20Acked-by:=20Steve=20Beattie?= =?UTF-8?q?=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was originally patch 0018-lp1056391.patch in the Ubuntu apparmor packaging; Steve noticed the now-redundant line for /var/lib/sss/mc/passwd so I removed that at the same time. --- profiles/apparmor.d/abstractions/nameservice | 6 ++++++ profiles/apparmor.d/usr.sbin.smbd | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/profiles/apparmor.d/abstractions/nameservice b/profiles/apparmor.d/abstractions/nameservice index a9d542be9..9492fa082 100644 --- a/profiles/apparmor.d/abstractions/nameservice +++ b/profiles/apparmor.d/abstractions/nameservice @@ -21,6 +21,12 @@ /etc/passwd r, /etc/protocols r, + # When using sssd, the passwd and group files are stored in an alternate path + # and the nss plugin also needs to talk to a pipe + /var/lib/sss/mc/group r, + /var/lib/sss/mc/passwd r, + /var/lib/sss/pipes/nss rw, + /etc/resolv.conf r, # on systems using resolvconf, /etc/resolv.conf is a symlink to # /{,var/}run/resolvconf/resolv.conf and a file sometimes referenced in diff --git a/profiles/apparmor.d/usr.sbin.smbd b/profiles/apparmor.d/usr.sbin.smbd index fc257671a..e4072d951 100644 --- a/profiles/apparmor.d/usr.sbin.smbd +++ b/profiles/apparmor.d/usr.sbin.smbd @@ -36,7 +36,6 @@ /var/cache/samba/** rwk, /var/cache/samba/printing/printers.tdb mrw, /var/lib/samba/** rwk, - /var/lib/sss/mc/passwd r, /var/lib/sss/pubconf/kdcinfo.* r, /{,var/}run/cups/cups.sock rw, /{,var/}run/dbus/system_bus_socket rw, From f88539d230777a1c553320eea4e9fef31555ffa6 Mon Sep 17 00:00:00 2001 From: Seth Arnold Date: Thu, 13 Feb 2014 17:17:46 -0800 Subject: [PATCH 03/13] Description: /etc/vdpau_wrapper.cfg needed for Firefox 18+ on quantal Author: Micah Gersten Acked-by: Steve Beattie Modified by Seth Arnold; nvidia nvpau_wrapper.cfg permission was hoisted up into an nvidia abstraction. --- profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia | 3 +++ 1 file changed, 3 insertions(+) diff --git a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia index 09ddaf78c..e7c55c5b4 100644 --- a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia +++ b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/multimedia @@ -55,3 +55,6 @@ # Virus scanners /usr/bin/clamscan Cx -> sanitized_helper, + + # gxine (LP: #1057642) + /var/lib/xine/gxine.desktop r, From 8e5f15c603db33cbdf4ed795a12616fab96b4293 Mon Sep 17 00:00:00 2001 From: Seth Arnold Date: Thu, 13 Feb 2014 17:21:41 -0800 Subject: [PATCH 04/13] Author: Jamie Strandboge Description: update mod_apparmor man page for Apache 2.4 and add new apparmor.d/usr.sbin.apache2 profile (based on the prefork profile) Acked-by: Steve Beattie Differs from original 0036-libapache2-mod-apparmor-profile-2.4.patch ubuntu patch -- I've deleted the "delete the apache 2.2 profile" part of the patch. So apache 2.2's profile is also still supported. --- changehat/mod_apparmor/mod_apparmor.pod | 3 +- profiles/apparmor.d/usr.sbin.apache2 | 83 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 profiles/apparmor.d/usr.sbin.apache2 diff --git a/changehat/mod_apparmor/mod_apparmor.pod b/changehat/mod_apparmor/mod_apparmor.pod index 7de7bc676..75616416c 100644 --- a/changehat/mod_apparmor/mod_apparmor.pod +++ b/changehat/mod_apparmor/mod_apparmor.pod @@ -116,7 +116,8 @@ will mod_apparmor() currently only supports apache2, and has only been tested with the prefork MPM configuration -- threaded configurations of Apache -may not work correctly. +may not work correctly. For Apache 2.4 users, you should enable the mpm_prefork +module. There are likely other bugs lurking about; if you find any, please report them at L. diff --git a/profiles/apparmor.d/usr.sbin.apache2 b/profiles/apparmor.d/usr.sbin.apache2 new file mode 100644 index 000000000..95c477860 --- /dev/null +++ b/profiles/apparmor.d/usr.sbin.apache2 @@ -0,0 +1,83 @@ +# Author: Marc Deslauriers + +#include +/usr/sbin/apache2 { + + # This profile is completely permissive. + # It is designed to target specific applications using mod_apparmor, + # hats, and the apache2.d directory. + # + # In order to enable this profile, you must: + # + # 1- Enable it: + # sudo aa-enforce /etc/apparmor.d/usr.sbin.apache2 + # + # 2- Load the mpm_prefork and mod_apparmor modules: + # sudo a2dismod + # sudo a2enmod mpm_prefork + # sudo a2enmod apparmor + # sudo service apache2 restart + # + # 3- Place an appropriate profile containing the desired hat in the + # /etc/apparmor.d/apache2.d directory. Such profiles should probably + # include the "apache2-common" abstraction. + # + # 4- Use the "AADefaultHatName" apache configuration option to specify a + # hat to be used for a given apache virtualhost or "AAHatName" for + # a given apache directory or location directive. + # + # + # There is an example profile for phpsysinfo included in the + # apparmor-profiles package. To try it: + # + # 1- Install the phpsysinfo and the apparmor-profiles packages: + # sudo apt-get install phpsysinfo apparmor-profiles + # + # 2- Enable the main apache2 profile + # sudo aa-enforce /etc/apparmor.d/usr.sbin.apache2 + # + # 3- Configure apache with the following: + # + # AAHatName phpsysinfo + # + # + + #include + #include + + capability dac_override, + capability kill, + capability net_bind_service, + capability setgid, + capability setuid, + capability sys_tty_config, + + / rw, + /** mrwlkix, + + + ^DEFAULT_URI { + #include + #include + + / rw, + /** mrwlkix, + + } + + ^HANDLING_UNTRUSTED_INPUT { + #include + + / rw, + /** mrwlkix, + + } + + # This directory contains web application + # package-specific apparmor files. + + #include + + # Site-specific additions and overrides. See local/README for details. + #include +} From b70d3fe48e29d8c4d9564b3fd123b186f63d498d Mon Sep 17 00:00:00 2001 From: Seth Arnold Date: Thu, 13 Feb 2014 17:23:56 -0800 Subject: [PATCH 05/13] Author: Jamie Strandboge Description: allow mmap of fglrx dri libraries Bug-Ubuntu: https://launchpad.net/bugs/1200392 Acked-by: Steve Beattie Came from 0038-lp1200392.patch. --- profiles/apparmor.d/abstractions/X | 1 + 1 file changed, 1 insertion(+) diff --git a/profiles/apparmor.d/abstractions/X b/profiles/apparmor.d/abstractions/X index f1c3e1cbb..89f829e55 100644 --- a/profiles/apparmor.d/abstractions/X +++ b/profiles/apparmor.d/abstractions/X @@ -35,6 +35,7 @@ # DRI /usr/lib{,32,64}/dri/** mr, /usr/lib/@{multiarch}/dri/** mr, + /usr/lib/fglrx/dri/** mr, /dev/dri/** rw, /etc/drirc r, owner @{HOME}/.drirc r, From 3ee30ca14cf107a389d36f7eeaa648cc9d37f89f Mon Sep 17 00:00:00 2001 From: Seth Arnold Date: Thu, 13 Feb 2014 17:25:31 -0800 Subject: [PATCH 06/13] Description: Remove access to pulseaudio debug socket from audio abstraction Grant access to specific files in the /var/run/user/UID/pulse/ directory to remove access to potentially dangerous and non-essential files such as the debug (cli) socket provided by the module-cli-protocol-unix module. Author: Tyler Hicks Bug-Ubuntu: https://launchpad.net/bugs/1211380 Acked-by: Steve Beattie --- profiles/apparmor.d/abstractions/audio | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/profiles/apparmor.d/abstractions/audio b/profiles/apparmor.d/abstractions/audio index ef9b4310c..e9643253e 100644 --- a/profiles/apparmor.d/abstractions/audio +++ b/profiles/apparmor.d/abstractions/audio @@ -56,7 +56,7 @@ owner @{HOME}/.pulse-cookie rwk, owner @{HOME}/.pulse/ rw, owner @{HOME}/.pulse/* rwk, owner /{,var/}run/user/*/pulse/ rw, -owner /{,var/}run/user/*/pulse/* rwk, +owner /{,var/}run/user/*/pulse/{native,pid} rwk, owner @{HOME}/.config/pulse/cookie rwk, owner /tmp/pulse-*/ rw, owner /tmp/pulse-*/* rw, From b432cf45c984595d4428c3bce31736b20dadeb7f Mon Sep 17 00:00:00 2001 From: Seth Arnold Date: Thu, 13 Feb 2014 17:53:40 -0800 Subject: [PATCH 07/13] Add aa-easyprof and easyprof.py and related pieces from the Ubuntu apparmor packaging. These were originally 0030-easyprof-sdk.patch and 0037-easyprof-sdk-pt2.patch. Jamie posted an updated 0030-easyprof-sdk_v2.patch and I squashed both patches into one commit. Acked-By: Jamie Strandboge --- utils/aa-easyprof | 88 +- utils/aa-easyprof.pod | 171 ++- utils/apparmor/easyprof.py | 630 +++++++- utils/easyprof/policygroups/networking | 2 - utils/easyprof/templates/default | 2 +- utils/easyprof/templates/sandbox | 2 +- utils/easyprof/templates/sandbox-x | 2 +- utils/easyprof/templates/user-application | 2 +- utils/test/test-aa-easyprof.py | 1664 ++++++++++++++++++++- 9 files changed, 2456 insertions(+), 107 deletions(-) delete mode 100644 utils/easyprof/policygroups/networking 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 From 0aefb378f9e6f0cc829c5aaa3a4568fb6244e195 Mon Sep 17 00:00:00 2001 From: Seth Arnold Date: Thu, 13 Feb 2014 18:10:05 -0800 Subject: [PATCH 08/13] Subject: using webapps triggers firefox rejections Bug: https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/1056418 From: Steve Beattie Came from 0021-webapps_abstraction.patch in the Ubuntu apparmor packaging. jdstrand: +1 (this is in the Ubuntu namespace, so feel free to commit) --- .../abstractions/ubuntu-browsers.d/ubuntu-integration | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/ubuntu-integration b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/ubuntu-integration index d80b85e6e..0cd0928ef 100644 --- a/profiles/apparmor.d/abstractions/ubuntu-browsers.d/ubuntu-integration +++ b/profiles/apparmor.d/abstractions/ubuntu-browsers.d/ubuntu-integration @@ -33,3 +33,9 @@ /usr/lib/@{multiarch}/xfce4/exo-1/exo-helper-1 ixr, /etc/xdg/xdg-xubuntu/xfce4/helpers.rc r, /etc/xdg/xfce4/helpers.rc r, + + # unity webapps integration. Could go in its own abstraction + owner /run/user/*/dconf/user rw, + owner @{HOME}/.local/share/unity-webapps/availableapps*.db rwk, + /usr/bin/debconf-communicate Cxr -> sanitized_helper, + owner @{HOME}/.config/libaccounts-glib/accounts.db rk, From 35e79ef66dbdb6395e67257f392a35b2827df960 Mon Sep 17 00:00:00 2001 From: Seth Arnold Date: Thu, 13 Feb 2014 18:11:54 -0800 Subject: [PATCH 09/13] Author: Jamie Strandboge Description: Allow applications run under sanitized_helper to connect to DBus This was originally 0076_sanitized_helper_dbus_access.patch in the Ubuntu apparmor packaging. jdstrand: +1 (this is in the Ubuntu namespace, so feel free to commit) --- profiles/apparmor.d/abstractions/ubuntu-helpers | 3 +++ 1 file changed, 3 insertions(+) diff --git a/profiles/apparmor.d/abstractions/ubuntu-helpers b/profiles/apparmor.d/abstractions/ubuntu-helpers index 42bd431bb..4da98e690 100644 --- a/profiles/apparmor.d/abstractions/ubuntu-helpers +++ b/profiles/apparmor.d/abstractions/ubuntu-helpers @@ -38,6 +38,9 @@ profile sanitized_helper { network inet, network inet6, + # Allow all DBus communications + dbus, + # Allow exec of anything, but under this profile. Allow transition # to other profiles if they exist. /bin/* Pixr, From e9c30a9361e377117df2be555de147ab7ab22db9 Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Fri, 14 Feb 2014 14:28:12 -0600 Subject: [PATCH 10/13] libthai-data is used by LibThai which is the library used to deal with Thai-specific functions like word-breaking, input and output methods and basic character and string support. This is: https://launchpad.net/bugs/1278702 Acked-By: Jamie Strandboge Acked-by: Steve Beattie --- profiles/apparmor.d/abstractions/fonts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/profiles/apparmor.d/abstractions/fonts b/profiles/apparmor.d/abstractions/fonts index 85a86ab9b..e75166900 100644 --- a/profiles/apparmor.d/abstractions/fonts +++ b/profiles/apparmor.d/abstractions/fonts @@ -52,3 +52,6 @@ # poppler CMap tables /usr/share/poppler/cMap/** r, + + # data files for LibThai + /usr/share/libthai/thbrk.tri r, From 8a0951be18a66ac520c58e75b02c21cfa7c53025 Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Fri, 14 Feb 2014 16:24:52 -0600 Subject: [PATCH 11/13] = Background = MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The xdg-user-dirs specification[1] allows for translatable and movable common directories. While this may be beneficial for users who for example want to have ~/Pictures translated into their own language, this flexibility provides challenges for AppArmor. Untranslated xdg user directories are typically (see ~/.config/user-dirs.dirs): XDG_DESKTOP_DIR="$HOME/Desktop" XDG_DOWNLOAD_DIR="$HOME/Downloads" XDG_TEMPLATES_DIR="$HOME/Templates" XDG_PUBLICSHARE_DIR="$HOME/Public" XDG_DOCUMENTS_DIR="$HOME/Documents" XDG_MUSIC_DIR="$HOME/Music" XDG_PICTURES_DIR="$HOME/Pictures" XDG_VIDEOS_DIR="$HOME/Videos" On an Ubuntu system with the fr_CA locale installed, these become: XDG_DESKTOP_DIR="$HOME/Desktop" XDG_DOWNLOAD_DIR="$HOME/Téléchargements" XDG_TEMPLATES_DIR="$HOME/Templates" XDG_PUBLICSHARE_DIR="$HOME/Public" XDG_DOCUMENTS_DIR="$HOME/Documents" XDG_MUSIC_DIR="$HOME/Musique" XDG_PICTURES_DIR="$HOME/Images" XDG_VIDEOS_DIR="$HOME/Vidéos" While the kernel and AppArmor parser handle these translations fine, the profiles do not. As an upstream, we can vastly improve the situation by simply creating the xdg-user-dirs tunable using the default 'C' xdg-user-dirs values: $ cat /etc/apparmor.d/tunables/xdg-user-dirs @{XDG_DESKTOP_DIR}=Desktop @{XDG_DOWNLOAD_DIR}=Downloads @{XDG_TEMPLATES_DIR}=Templates @{XDG_PUBLICSHARE_DIR}=Public @{XDG_DOCUMENTS_DIR}=Documents @{XDG_MUSIC_DIR}=Music @{XDG_PICTURES_DIR}=Pictures @{XDG_VIDEOS_DIR}=Videos # Also, include files in tunables/xdg-user-dirs.d for site-specific adjustments # to the various XDG directories #include and then create the /etc/apparmor.d/tunables/xdg-user-dirs.d directory. With that alone, we can start using rules like this in policy: owner @{HOME}/@{XDG_MUSIC_DIR}/** r, and users/admins can adjust /etc/apparmor.d/tunables/xdg-user-dirs or drop files into /etc/apparmor.d/tunables/xdg-user-dirs.d, providing a welcome convenience. This of course doesn't solve everything. Because users can modify their ~/.config/user-dirs.dirs file at will and have it point anywhere, so we can't examine those files and do anything automatic there (when we have user policy we can revisit this). This patch handles translations well though since use of translations for these directories happens outside of the user's control. Users who modify ~/.config/user-dirs.dirs can update policy like they need to now (ie, this patch doesn't change anything for them). [0] https://lists.ubuntu.com/archives/apparmor/2013-August/004183.html [1] http://freedesktop.org/wiki/Software/xdg-user-dirs/ This patch adds basic support for XDG user dirs: 1. Update profiles/apparmor.d/tunables/global to include xdg-user-dirs. 2. Create the xdg-user-dirs tunable using the default 'C' xdg-user-dirs values and includes tunables/xdg-user-dirs.d 3. Add profiles/apparmor.d/tunables/xdg-user-dirs.d/site.local with commented out examples on how to use the directory. Acked-By: Jamie Strandboge Acked-By: Christian Boltz --- profiles/apparmor.d/tunables/global | 3 ++- profiles/apparmor.d/tunables/xdg-user-dirs | 24 +++++++++++++++++++ .../tunables/xdg-user-dirs.d/site.local | 21 ++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 profiles/apparmor.d/tunables/xdg-user-dirs create mode 100644 profiles/apparmor.d/tunables/xdg-user-dirs.d/site.local diff --git a/profiles/apparmor.d/tunables/global b/profiles/apparmor.d/tunables/global index 69d827b91..58d087fbe 100644 --- a/profiles/apparmor.d/tunables/global +++ b/profiles/apparmor.d/tunables/global @@ -1,7 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (C) 2006-2009 Novell/SUSE -# Copyright (C) 2010-2011 Canonical Ltd. +# Copyright (C) 2010-2014 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of version 2 of the GNU General Public @@ -17,3 +17,4 @@ #include #include #include +#include diff --git a/profiles/apparmor.d/tunables/xdg-user-dirs b/profiles/apparmor.d/tunables/xdg-user-dirs new file mode 100644 index 000000000..fcaf8d40d --- /dev/null +++ b/profiles/apparmor.d/tunables/xdg-user-dirs @@ -0,0 +1,24 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License published by the Free Software Foundation. +# +# ------------------------------------------------------------------ + +# Define the common set of XDG user directories (usually defined in +# /etc/xdg/user-dirs.defaults) +@{XDG_DESKTOP_DIR}="Desktop" +@{XDG_DOWNLOAD_DIR}="Downloads" +@{XDG_TEMPLATES_DIR}="Templates" +@{XDG_PUBLICSHARE_DIR}="Public" +@{XDG_DOCUMENTS_DIR}="Documents" +@{XDG_MUSIC_DIR}="Music" +@{XDG_PICTURES_DIR}="Pictures" +@{XDG_VIDEOS_DIR}="Videos" + +# Also, include files in tunables/xdg-user-dirs.d for site-specific adjustments +# to the various XDG directories +#include diff --git a/profiles/apparmor.d/tunables/xdg-user-dirs.d/site.local b/profiles/apparmor.d/tunables/xdg-user-dirs.d/site.local new file mode 100644 index 000000000..8fcabfa0d --- /dev/null +++ b/profiles/apparmor.d/tunables/xdg-user-dirs.d/site.local @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------ +# +# Copyright (C) 2014 Canonical Ltd. +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License published by the Free Software Foundation. +# +# ------------------------------------------------------------------ + +# The following may be used to add additional entries such as for +# translations. See tunables/xdg-user-dirs for details. Eg: +#@{XDG_MUSIC_DIR}+="Musique" + +#@{XDG_DESKTOP_DIR}+="" +#@{XDG_DOWNLOAD_DIR}+="" +#@{XDG_TEMPLATES_DIR}+="" +#@{XDG_PUBLICSHARE_DIR}+="" +#@{XDG_DOCUMENTS_DIR}+="" +#@{XDG_MUSIC_DIR}+="" +#@{XDG_PICTURES_DIR}+="" +#@{XDG_VIDEOS_DIR}+="" From 6812e5e550b15bf2d6cdb347e0af4ec2bd12f3e1 Mon Sep 17 00:00:00 2001 From: Jamie Strandboge Date: Fri, 14 Feb 2014 16:28:16 -0600 Subject: [PATCH 12/13] Update abstractions to use new XDG_*_DIR values. Thanks to Christian Boltz for the suggestion to use @{XDG_DOWNLOAD_DIR} in abstractions/user-download as well as the existing entries. Acked-By: Jamie Strandboge Acked-By: Christian Boltz --- .../apparmor.d/abstractions/user-download | 7 +++++-- profiles/apparmor.d/abstractions/user-write | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/profiles/apparmor.d/abstractions/user-download b/profiles/apparmor.d/abstractions/user-download index efa946068..ffe1a1ff5 100644 --- a/profiles/apparmor.d/abstractions/user-download +++ b/profiles/apparmor.d/abstractions/user-download @@ -1,6 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (C) 2002-2006 Novell/SUSE +# Copyright (C) 2014 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of version 2 of the GNU General Public @@ -15,7 +16,9 @@ owner @{HOME}/[dD]ownload{,s}/ r, owner @{HOME}/[dD]ownload{,s}/** rwl, owner @{HOME}/[a-zA-Z0-9]* rwl, - owner @{HOME}/Desktop/ r, - owner @{HOME}/Desktop/* rwl, + owner @{HOME}/@{XDG_DESKTOP_DIR}/ r, + owner @{HOME}/@{XDG_DESKTOP_DIR}/* rwl, + owner @{HOME}/@{XDG_DOWNLOAD_DIR}/ r, + owner @{HOME}/@{XDG_DOWNLOAD_DIR}/* rwl, owner "@{HOME}/My Downloads/" r, owner "@{HOME}/My Downloads/**" rwl, diff --git a/profiles/apparmor.d/abstractions/user-write b/profiles/apparmor.d/abstractions/user-write index adf8773f7..79a550aac 100644 --- a/profiles/apparmor.d/abstractions/user-write +++ b/profiles/apparmor.d/abstractions/user-write @@ -1,6 +1,7 @@ # ------------------------------------------------------------------ # # Copyright (C) 2002-2006 Novell/SUSE +# Copyright (C) 2014 Canonical Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of version 2 of the GNU General Public @@ -9,12 +10,12 @@ # ------------------------------------------------------------------ # per-user write directories - owner @{HOME}/ r, - owner @{HOME}/Desktop/ r, - owner @{HOME}/Documents/ r, - owner @{HOME}/Public/ r, - owner @{HOME}/[a-zA-Z0-9]*/ rw, - owner @{HOME}/[a-zA-Z0-9]* rwl, - owner @{HOME}/Desktop/** rwl, - owner @{HOME}/Documents/** rwl, - owner @{HOME}/Public/** rwl, + owner @{HOME}/ r, + owner @{HOME}/@{XDG_DESKTOP_DIR}/ r, + owner @{HOME}/@{XDG_DOCUMENTS_DIR}/ r, + owner @{HOME}/@{XDG_PUBLICSHARE_DIR}/ r, + owner @{HOME}/[a-zA-Z0-9]*/ rw, + owner @{HOME}/[a-zA-Z0-9]* rwl, + owner @{HOME}/@{XDG_DESKTOP_DIR}/** rwl, + owner @{HOME}/@{XDG_DOCUMENTS_DIR}/** rwl, + owner @{HOME}/@{XDG_PUBLICSHARE_DIR}/** rwl, From 503d95167345f671987c9608387dc18cf2ac5652 Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Fri, 14 Feb 2014 23:37:13 +0100 Subject: [PATCH 13/13] update abstractions/winbind - some *.dat files live in a different directory nowadays (at least in openSUSE) - the openSUSE smb.conf includes the (autogenerated) dhcp.conf, so this file also needs to be readable. References: https://bugzilla.novell.com/show_bug.cgi?id=863226 Acked-by: Seth Arnold --- profiles/apparmor.d/abstractions/winbind | 2 ++ 1 file changed, 2 insertions(+) diff --git a/profiles/apparmor.d/abstractions/winbind b/profiles/apparmor.d/abstractions/winbind index bc06f2c60..e982889ea 100644 --- a/profiles/apparmor.d/abstractions/winbind +++ b/profiles/apparmor.d/abstractions/winbind @@ -13,7 +13,9 @@ /tmp/.winbindd/pipe rw, /var/{lib,run}/samba/winbindd_privileged/pipe rw, /etc/samba/smb.conf r, + /etc/samba/dhcp.conf r, /usr/lib*/samba/valid.dat r, /usr/lib*/samba/upcase.dat r, /usr/lib*/samba/lowcase.dat r, + /usr/share/samba/codepages/{lowcase,upcase,valid}.dat r,