2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-08-30 05:47:59 +00:00

Sync with trunk.

This commit is contained in:
Steve Beattie 2014-02-14 14:47:18 -08:00
commit 7fab3a7a69
27 changed files with 2630 additions and 125 deletions

View File

@ -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<https://bugs.launchpad.net/apparmor/+filebug>.

View File

@ -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,

View File

@ -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,

View File

@ -52,3 +52,6 @@
# poppler CMap tables
/usr/share/poppler/cMap/** r,
# data files for LibThai
/usr/share/libthai/thbrk.tri r,

View File

@ -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

View File

@ -55,3 +55,6 @@
# Virus scanners
/usr/bin/clamscan Cx -> sanitized_helper,
# gxine (LP: #1057642)
/var/lib/xine/gxine.desktop r,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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 <tunables/proc>
#include <tunables/alias>
#include <tunables/kernelvars>
#include <tunables/xdg-user-dirs>

View File

@ -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 <tunables/xdg-user-dirs.d>

View File

@ -0,0 +1,21 @@
# ------------------------------------------------------------------
#
# Copyright (C) 2014 Canonical Ltd.
# This program is free software; you can redistribute it and/or
# modify it under the terms of version 2 of the GNU General Public
# License published by the Free Software Foundation.
#
# ------------------------------------------------------------------
# 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}+=""

View File

@ -14,6 +14,7 @@
/usr/lib/dovecot/dict {
#include <abstractions/base>
#include <abstractions/mysql>
#include <abstractions/nameservice>
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.

View File

@ -14,6 +14,7 @@
/usr/lib/dovecot/lmtp {
#include <abstractions/base>
#include <abstractions/nameservice>
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,

View File

@ -0,0 +1,83 @@
# Author: Marc Deslauriers <marc.deslauriers@ubuntu.com>
#include <tunables/global>
/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 <other non-prefork mpm>
# 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:
# <Directory /var/www/phpsysinfo/>
# AAHatName phpsysinfo
# </Directory>
#
#include <abstractions/base>
#include <abstractions/nameservice>
capability dac_override,
capability kill,
capability net_bind_service,
capability setgid,
capability setuid,
capability sys_tty_config,
/ rw,
/** mrwlkix,
^DEFAULT_URI {
#include <abstractions/base>
#include <abstractions/nameservice>
/ rw,
/** mrwlkix,
}
^HANDLING_UNTRUSTED_INPUT {
#include <abstractions/nameservice>
/ rw,
/** mrwlkix,
}
# This directory contains web application
# package-specific apparmor files.
#include <apache2.d>
# Site-specific additions and overrides. See local/README for details.
#include <local/usr.sbin.apache2>
}

View File

@ -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,

View File

@ -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)

View File

@ -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<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
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<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
=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

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
# 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": {
# <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###
###BINARY### {
###PROFILEATTACH### {
#include <abstractions/base>
###ABSTRACTIONS###

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff