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 <jamie@canonical.com>
This commit is contained in:
Seth Arnold 2014-02-13 17:53:40 -08:00
parent 3ee30ca14c
commit b432cf45c9
9 changed files with 2456 additions and 107 deletions

View file

@ -1,7 +1,7 @@
#! /usr/bin/env python #! /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 # This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public # modify it under the terms of version 2 of the GNU General Public
@ -22,6 +22,7 @@ if __name__ == "__main__":
(opt, args) = apparmor.easyprof.parse_args() (opt, args) = apparmor.easyprof.parse_args()
binary = None binary = None
manifest = None
m = usage() m = usage()
if opt.show_policy_group and not opt.policy_groups: if opt.show_policy_group and not opt.policy_groups:
@ -33,32 +34,65 @@ if __name__ == "__main__":
if len(args) >= 1: if len(args) >= 1:
binary = args[0] binary = args[0]
# 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: try:
easyp = apparmor.easyprof.AppArmorEasyProfile(binary, opt) # 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) )
count = 0
for (binary, options) in profiles:
if len(profiles) > 1:
count += 1
try:
easyp = apparmor.easyprof.AppArmorEasyProfile(binary, options)
except AppArmorException as e: except AppArmorException as e:
error(e.value) error(e.value)
except Exception: except Exception:
raise raise
if opt.list_templates: if options.list_templates:
apparmor.easyprof.print_basefilenames(easyp.get_templates()) apparmor.easyprof.print_basefilenames(easyp.get_templates())
sys.exit(0) sys.exit(0)
elif opt.template and opt.show_template: elif options.template and options.show_template:
files = [os.path.join(easyp.dirs['templates'], opt.template)] files = [os.path.join(easyp.dirs['templates'], options.template)]
apparmor.easyprof.print_files(files) apparmor.easyprof.print_files(files)
sys.exit(0) sys.exit(0)
elif opt.list_policy_groups: elif options.list_policy_groups:
apparmor.easyprof.print_basefilenames(easyp.get_policy_groups()) apparmor.easyprof.print_basefilenames(easyp.get_policy_groups())
sys.exit(0) sys.exit(0)
elif opt.policy_groups and opt.show_policy_group: elif options.policy_groups and options.show_policy_group:
for g in opt.policy_groups.split(','): for g in options.policy_groups.split(','):
files = [os.path.join(easyp.dirs['policygroups'], g)] files = [os.path.join(easyp.dirs['policygroups'], g)]
apparmor.easyprof.print_files(files) apparmor.easyprof.print_files(files)
sys.exit(0) sys.exit(0)
elif binary is None: elif binary == None and not options.profile_name and \
error("Must specify full path to binary\n%s" % m) 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, options)
params = apparmor.easyprof.gen_policy_params(binary, opt) if options.manifest and options.verify_manifest and \
p = easyp.gen_policy(**params) not apparmor.easyprof.verify_manifest(params):
sys.stdout.write('%s\n' % p) 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)

View file

@ -78,8 +78,15 @@ Like --read-path but also allow owner writes in additions to reads.
=item -n NAME, --name=NAME =item -n NAME, --name=NAME
Specify NAME of policy. If not specified, NAME is set to the name of the 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 binary. The NAME of the policy is typically only used for profile meta
various templates. 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" =item --template-var="@{VAR}=VALUE"
@ -110,6 +117,32 @@ Display policy groups specified with --policy.
Use PATH instead of system policy-groups directory. 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<aa-easyprof> 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 =item --author
Specify author of the policy. Specify author of the policy.
@ -122,6 +155,104 @@ Specify copyright of the policy.
Specify comment for the policy. Specify comment for the policy.
=item -m MANIFEST, --manifest=MANIFEST
B<aa-easyprof> 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": {
"<profile name 1>": {
... attributes specific to this profile ...
},
"<profile name 2>": {
...
}
}
}
}
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<text> (default if unspecified) for AppArmor policy output or
B<json> for JSON manifest format.
=item --output-directory=DIR
Specify output directory for profile. If unspecified, policy is sent to stdout.
=back =back
=head1 EXAMPLE =head1 EXAMPLE
@ -130,7 +261,41 @@ Example usage for a program named 'foo' which is installed in /opt/foo:
=over =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 =back

View file

@ -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 # This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public # modify it under the terms of version 2 of the GNU General Public
@ -11,10 +11,13 @@
from __future__ import with_statement from __future__ import with_statement
import codecs import codecs
import copy
import glob import glob
import json
import optparse import optparse
import os import os
import re import re
import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
@ -123,29 +126,117 @@ def valid_binary_path(path):
return True return True
def valid_variable_name(var): def valid_variable(v):
'''Validate variable name''' '''Validate variable name'''
if re.search(r'[a-zA-Z0-9_]+$', var): debug("Checking '%s'" % v)
return True try:
(key, value) = v.split('=')
except Exception:
return False return False
if not re.search(r'^@\{[a-zA-Z0-9_]+\}$', key):
return False
def valid_path(path): 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''' '''Valid path'''
# No relative paths
m = "Invalid path: %s" % (path) m = "Invalid path: %s" % (path)
if not path.startswith('/'): if not relative_ok and not path.startswith('/'):
debug("%s (relative)" % (m)) debug("%s (relative)" % (m))
return False 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: try:
os.path.normpath(path) p = os.path.normpath(path)
except Exception: except Exception:
debug("%s (could not normalize)" % (m)) debug("%s (could not normalize)" % (m))
return False 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 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_policy_vendor(s):
'''Verify the policy vendor'''
return _is_safe(s)
def valid_policy_version(v):
'''Verify the policy version'''
try:
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): def get_directory_contents(path):
'''Find contents of the given directory''' '''Find contents of the given directory'''
if not valid_path(path): if not valid_path(path):
@ -202,6 +293,7 @@ def verify_policy(policy):
class AppArmorEasyProfile: class AppArmorEasyProfile:
'''Easy profile class''' '''Easy profile class'''
def __init__(self, binary, opt): def __init__(self, binary, opt):
verify_options(opt)
opt.ensure_value("conffile", "/etc/apparmor/easyprof.conf") opt.ensure_value("conffile", "/etc/apparmor/easyprof.conf")
self.conffile = os.path.abspath(opt.conffile) 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): if opt.policy_groups_dir and os.path.isdir(opt.policy_groups_dir):
self.dirs['policygroups'] = os.path.abspath(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: if not 'templates' in self.dirs:
raise AppArmorException("Could not find templates directory") raise AppArmorException("Could not find templates directory")
if not 'policygroups' in self.dirs: if not 'policygroups' in self.dirs:
@ -230,19 +341,29 @@ class AppArmorEasyProfile:
self.aa_topdir = "/etc/apparmor.d" self.aa_topdir = "/etc/apparmor.d"
self.binary = binary self.binary = binary
if binary != None: if binary:
if not valid_binary_path(binary): if not valid_binary_path(binary):
raise AppArmorException("Invalid path for binary: '%s'" % binary) raise AppArmorException("Invalid path for binary: '%s'" % binary)
if opt.manifest:
self.set_template(opt.template, allow_abs_path=False)
else:
self.set_template(opt.template) self.set_template(opt.template)
self.set_policygroup(opt.policy_groups) self.set_policygroup(opt.policy_groups)
if opt.name: if opt.name:
self.set_name(opt.name) self.set_name(opt.name)
elif self.binary != None: elif self.binary != None:
self.set_name(self.binary) self.set_name(self.binary)
self.templates = get_directory_contents(self.dirs['templates']) self.templates = []
self.policy_groups = get_directory_contents(self.dirs['policygroups']) 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): def _get_defaults(self):
'''Read in defaults from configuration''' '''Read in defaults from configuration'''
@ -282,11 +403,18 @@ class AppArmorEasyProfile:
'''Get contents of current template''' '''Get contents of current template'''
return open(self.template).read() return open(self.template).read()
def set_template(self, template): def set_template(self, template, allow_abs_path=True):
'''Set current template''' '''Set current template'''
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 self.template = template
if not template.startswith('/'): else:
self.template = os.path.join(self.dirs['templates'], template) self.template = os.path.join(self.dirs['templates'], template)
if not os.path.exists(self.template): if not os.path.exists(self.template):
raise AppArmorException('%s does not exist' % (self.template)) raise AppArmorException('%s does not exist' % (self.template))
@ -327,9 +455,11 @@ class AppArmorEasyProfile:
def gen_variable_declaration(self, dec): def gen_variable_declaration(self, dec):
'''Generate a variable declaration''' '''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) 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): def gen_path_rule(self, path, access):
rule = [] rule = []
@ -352,7 +482,18 @@ class AppArmorEasyProfile:
return rule 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): def find_prefix(t, s):
'''Calculate whitespace prefix based on occurrence of s in t''' '''Calculate whitespace prefix based on occurrence of s in t'''
pat = re.compile(r'^ *%s' % s) pat = re.compile(r'^ *%s' % s)
@ -375,12 +516,22 @@ class AppArmorEasyProfile:
tmp += line + "\n" tmp += line + "\n"
policy = tmp policy = tmp
# Fill-in profile name and binary attachment = ""
policy = re.sub(r'###NAME###', name, policy) if binary:
if binary.startswith('/'): if not valid_binary_path(binary):
policy = re.sub(r'###BINARY###', binary, policy) raise AppArmorException("Invalid path for binary: '%s'" % \
binary)
if profile_name:
attachment = 'profile "%s" "%s"' % (profile_name, binary)
else: else:
policy = re.sub(r'###BINARY###', "profile %s" % binary, policy) attachment = '"%s"' % binary
elif profile_name:
attachment = 'profile "%s"' % profile_name
else:
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 # Fill-in various comment fields
if comment != None: if comment != None:
@ -398,7 +549,9 @@ class AppArmorEasyProfile:
s = "%s# No abstractions specified" % prefix s = "%s# No abstractions specified" % prefix
if abstractions != None: if abstractions != None:
s = "%s# Specified abstractions" % (prefix) 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)) s += "\n%s%s" % (prefix, self.gen_abstraction_rule(i))
policy = re.sub(r' *%s' % search, s, policy) policy = re.sub(r' *%s' % search, s, policy)
@ -407,7 +560,9 @@ class AppArmorEasyProfile:
s = "%s# No policy groups specified" % prefix s = "%s# No policy groups specified" % prefix
if policy_groups != None: if policy_groups != None:
s = "%s# Rules specified via policy groups" % (prefix) 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(): for line in self.get_policygroup(i).splitlines():
s += "\n%s%s" % (prefix, line) s += "\n%s%s" % (prefix, line)
if i != policy_groups.split(',')[-1]: if i != policy_groups.split(',')[-1]:
@ -419,6 +574,7 @@ class AppArmorEasyProfile:
s = "%s# No template variables specified" % prefix s = "%s# No template variables specified" % prefix
if len(template_var) > 0: if len(template_var) > 0:
s = "%s# Specified profile variables" % (prefix) s = "%s# Specified profile variables" % (prefix)
template_var.sort()
for i in template_var: for i in template_var:
s += "\n%s%s" % (prefix, self.gen_variable_declaration(i)) s += "\n%s%s" % (prefix, self.gen_variable_declaration(i))
policy = re.sub(r' *%s' % search, s, policy) policy = re.sub(r' *%s' % search, s, policy)
@ -428,8 +584,9 @@ class AppArmorEasyProfile:
s = "%s# No read paths specified" % prefix s = "%s# No read paths specified" % prefix
if len(read_path) > 0: if len(read_path) > 0:
s = "%s# Specified read permissions" % (prefix) s = "%s# Specified read permissions" % (prefix)
read_path.sort()
for i in read_path: 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) s += "\n%s%s" % (prefix, r)
policy = re.sub(r' *%s' % search, s, policy) policy = re.sub(r' *%s' % search, s, policy)
@ -438,17 +595,110 @@ class AppArmorEasyProfile:
s = "%s# No write paths specified" % prefix s = "%s# No write paths specified" % prefix
if len(write_path) > 0: if len(write_path) > 0:
s = "%s# Specified write permissions" % (prefix) s = "%s# Specified write permissions" % (prefix)
write_path.sort()
for i in write_path: for i in write_path:
for r in self.gen_path_rule(i, 'rwk'): for r in self.gen_path_rule(i, 'rwk'):
s += "\n%s%s" % (prefix, r) s += "\n%s%s" % (prefix, r)
policy = re.sub(r' *%s' % search, s, policy) policy = re.sub(r' *%s' % search, s, policy)
if not verify_policy(policy): if no_verify:
debug("\n" + policy) debug("Skipping policy verification")
elif not verify_policy(policy):
msg("\n" + policy)
raise AppArmorException("Invalid policy") raise AppArmorException("Invalid policy")
return 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): def print_basefilenames(files):
for i in files: for i in files:
sys.stdout.write("%s\n" % (os.path.basename(i))) sys.stdout.write("%s\n" % (os.path.basename(i)))
@ -458,22 +708,65 @@ def print_files(files):
with open(i) as f: with open(i) as f:
sys.stdout.write(f.read()+"\n") 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): def add_parser_policy_args(parser):
'''Add parser arguments''' '''Add parser arguments'''
parser.add_option("-a", "--abstractions", parser.add_option("-a", "--abstractions",
action="callback",
callback=check_for_manifest_arg,
type=str,
dest="abstractions", dest="abstractions",
help="Comma-separated list of abstractions", help="Comma-separated list of abstractions",
metavar="ABSTRACTIONS") metavar="ABSTRACTIONS")
parser.add_option("--read-path", parser.add_option("--read-path",
action="callback",
callback=check_for_manifest_arg_append,
type=str,
dest="read_path", dest="read_path",
help="Path allowing owner reads", help="Path allowing owner reads",
metavar="PATH", metavar="PATH")
action="append")
parser.add_option("--write-path", parser.add_option("--write-path",
action="callback",
callback=check_for_manifest_arg_append,
type=str,
dest="write_path", dest="write_path",
help="Path allowing owner writes", help="Path allowing owner writes",
metavar="PATH", metavar="PATH")
action="append")
parser.add_option("-t", "--template", parser.add_option("-t", "--template",
dest="template", dest="template",
help="Use non-default policy template", help="Use non-default policy template",
@ -484,12 +777,36 @@ def add_parser_policy_args(parser):
help="Use non-default templates directory", help="Use non-default templates directory",
metavar="DIR") metavar="DIR")
parser.add_option("-p", "--policy-groups", parser.add_option("-p", "--policy-groups",
action="callback",
callback=check_for_manifest_arg,
type=str,
help="Comma-separated list of policy groups", help="Comma-separated list of policy groups",
metavar="POLICYGROUPS") metavar="POLICYGROUPS")
parser.add_option("--policy-groups-dir", parser.add_option("--policy-groups-dir",
dest="policy_groups_dir", dest="policy_groups_dir",
help="Use non-default policy-groups directory", help="Use non-default policy-groups directory",
metavar="DIR") 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): def parse_args(args=None, parser=None):
'''Parse arguments''' '''Parse arguments'''
@ -506,6 +823,10 @@ def parse_args(args=None, parser=None):
help="Show debugging output", help="Show debugging output",
action='store_true', action='store_true',
default=False) 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", parser.add_option("--list-templates",
help="List available templates", help="List available templates",
action='store_true', action='store_true',
@ -523,31 +844,72 @@ def parse_args(args=None, parser=None):
action='store_true', action='store_true',
default=False) default=False)
parser.add_option("-n", "--name", parser.add_option("-n", "--name",
action="callback",
callback=check_for_manifest_arg,
type=str,
dest="name", dest="name",
help="Name of policy", help="Name of policy (not AppArmor profile name)",
metavar="NAME") metavar="COMMENT")
parser.add_option("--comment", parser.add_option("--comment",
action="callback",
callback=check_for_manifest_arg,
type=str,
dest="comment", dest="comment",
help="Comment for policy", help="Comment for policy",
metavar="COMMENT") metavar="COMMENT")
parser.add_option("--author", parser.add_option("--author",
action="callback",
callback=check_for_manifest_arg,
type=str,
dest="author", dest="author",
help="Author of policy", help="Author of policy",
metavar="COMMENT") metavar="COMMENT")
parser.add_option("--copyright", parser.add_option("--copyright",
action="callback",
callback=check_for_manifest_arg,
type=str,
dest="copyright", dest="copyright",
help="Copyright for policy", help="Copyright for policy",
metavar="COMMENT") metavar="COMMENT")
parser.add_option("--template-var", parser.add_option("--template-var",
action="callback",
callback=check_for_manifest_arg_append,
type=str,
dest="template_var", dest="template_var",
help="Declare AppArmor variable", help="Declare AppArmor variable",
metavar="@{VARIABLE}=VALUE", metavar="@{VARIABLE}=VALUE")
action="append") 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 policy args now
add_parser_policy_args(parser) add_parser_policy_args(parser)
(my_opt, my_args) = parser.parse_args(args) (my_opt, my_args) = parser.parse_args(args)
if my_opt.debug: if my_opt.debug:
DEBUGGING = True DEBUGGING = True
return (my_opt, my_args) return (my_opt, my_args)
@ -555,10 +917,21 @@ def parse_args(args=None, parser=None):
def gen_policy_params(binary, opt): def gen_policy_params(binary, opt):
'''Generate parameters for gen_policy''' '''Generate parameters for gen_policy'''
params = dict(binary=binary) 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: if opt.name:
params['name'] = opt.name params['name'] = opt.name
else: else:
if opt.profile_name:
params['name'] = opt.profile_name
elif binary:
params['name'] = os.path.basename(binary) params['name'] = os.path.basename(binary)
if opt.template_var: # What about specified multiple times? if opt.template_var: # What about specified multiple times?
params['template_var'] = opt.template_var params['template_var'] = opt.template_var
if opt.abstractions: if opt.abstractions:
@ -569,14 +942,183 @@ def gen_policy_params(binary, opt):
params['read_path'] = opt.read_path params['read_path'] = opt.read_path
if opt.write_path: if opt.write_path:
params['write_path'] = opt.write_path params['write_path'] = opt.write_path
if opt.abstractions:
params['abstractions'] = opt.abstractions
if opt.comment: if opt.comment:
params['comment'] = opt.comment params['comment'] = opt.comment
if opt.author: if opt.author:
params['author'] = opt.author params['author'] = opt.author
if opt.copyright: if opt.copyright:
params['copyright'] = 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 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": {
# <profile_name>: {
# "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

View file

@ -1,2 +0,0 @@
# Policygroup to allow networking
#include <abstractions/nameservice>

View file

@ -13,7 +13,7 @@
###VAR### ###VAR###
###BINARY### { ###PROFILEATTACH### {
#include <abstractions/base> #include <abstractions/base>
###ABSTRACTIONS### ###ABSTRACTIONS###

View file

@ -13,7 +13,7 @@
###VAR### ###VAR###
###BINARY### { ###PROFILEATTACH### {
#include <abstractions/base> #include <abstractions/base>
/ r, / r,
/**/ r, /**/ r,

View file

@ -13,7 +13,7 @@
###VAR### ###VAR###
###BINARY### { ###PROFILEATTACH### {
#include <abstractions/base> #include <abstractions/base>
#include <abstractions/gnome> #include <abstractions/gnome>
#include <abstractions/kde> #include <abstractions/kde>

View file

@ -16,7 +16,7 @@
###VAR### ###VAR###
###BINARY### { ###PROFILEATTACH### {
#include <abstractions/base> #include <abstractions/base>
###ABSTRACTIONS### ###ABSTRACTIONS###

File diff suppressed because it is too large Load diff