~apparmor-dev/apparmor/apparmor-ubuntu-citrain-trusty

« back to all changes in this revision

Viewing changes to utils/apparmor/severity.py

  • Committer: Seth Arnold
  • Date: 2014-03-12 02:05:16 UTC
  • mto: This revision was merged to the branch mainline in revision 1496.
  • Revision ID: seth.arnold@canonical.com-20140312020516-zjike3pmw6hi861h
[ Jamie Strandboge ]
 * debian/debhelper/dh_apparmor: exit with error if aa-easyprof does not
   exist
 * debian/control: drop Depends on apparmor-easyprof to Suggests for
   dh-apparmor
[ Seth Arnold, Jamie Strandboge, Steve Beattie, John Johansen, Tyler Hicks ]
* New upstream snapshot (LP: #1278702, #1061693, #1285653) dropping very
  large Ubuntu delta and fixing the following bugs:
  - Adjust fonts abstraction for libthai (LP: #1278702)
  - Support translated XDG user directories (LP: #1061693)
  - Adjust abstractions/web-data to include /var/www/html (LP: #1285653)
    Refresh 0002-add-debian-integration-to-lighttpd.patch to include
    /etc/lighttpd/conf-available/*.conf
  - Adjust debian/libapparmor1.symbols to reflect new upstream versioning
    for the aa_query_label() function
  - Raise exceptions in Python bindings when something fails
* ship new Python replacements for previous Perl-based tools
  - debian/apparmor-utils.install: remove usr/share/perl5/Immunix/*.pm and add
    usr/sbin/aa-autodep, usr/sbin/aa-cleanprof and usr/sbin/aa-mergeprof
  - debian/control:
    + remove various Perl dependencies
    + add python-apparmor and python3-apparmor
    + python3-apparmor Breaks: apparmor-easyprof to move the file since it
      ships dist-packages/apparmor/__init__.py now
  - debian/apparmor-utils.manpages: ship new manpages for aa-cleanprof and
    aa-mergeprof
  - debian/rules: build and install Python tools
* debian/apparmor.install:
  - install apparmorfs, dovecot, kernelvars, securityfs, sys,
    and xdg-user-dirs tunables and xdg-user-dirs.d directory
* debian/apparmor.dirs:
  - install /etc/apparmor.d/tunables/xdg-user-dirs.d
* debian/apparmor.postinst: create xdg-user-dirs.d
* debian/apparmor.postrm: remove xdg-user-dirs.d
* Remaining patches:
  - 0001-add-chromium-browser.patch
  - 0002-add-debian-integration-to-lighttpd.patch
  - 0003-ubuntu-manpage-updates.patch
  - 0004-libapparmor-layout-deb.patch (renamed from 0008)
  - 0005-libapparmor-mention-dbus-method-in-getcon-man.patch (renamed from
    0068)
  - 0006-etc-writable.patch (renamed from 0070)
  - 0007-aa-utils_are_bilingual.patch (renamed from 0077)
  - 0008-remove-ptrace.patch
  - 0009-convert-to-rules.patch
  - 0010-list-fns.patch
  - 0011-parse-mode.patch
  - 0012-add-decimal-interp.patch
  - 0013-policy_mediates.patch
  - 0014-fix-failpath.patch
  - 0015-feature_file.patch
  - 0016-fix-network.patch
  - 0017-aare-to-class.patch
  - 0018-add-mediation-unix.patch
  - 0019-parser_version.patch
  - 0020-caching.patch
  - 0021-label-class.patch
  - 0022-signal.patch
  - 0023-fix-lexer-debug.patch
  - 0024-ptrace.patch
  - 0025-use-diff-encode.patch
  - 0026-fix-serialize.patch
  - 0027-fix-af.patch
  - 0028-opt_arg.patch
  - 0029-tests-cond-dbus.patch
  - 0030-tests.diff
* Move manpages from libapparmor1 to libapparmor-dev
  - debian/libapparmor-dev.manpages: install aa_change_hat.2,
    aa_change_profile.2, aa_find_mountpoint.2, aa_getcon.2
  - debian/control: libapparmor-dev Replaces: and Breaks: libapparmor1
* Move /usr/lib/python3/dist-packages/apparmor/__init__.py from
  apparmor-easyprof to python3-apparmor
  - debian/control: python3-apparmor Breaks: apparmor-easyprof
  - debian/apparmor-easyprof.install: remove
    usr/lib/python*.*/site-packages/apparmor*
* New profiles and abstractions:
  - debian/apparmor.install: tunables/dovecot, tunables/kernelvars,
    tunables/xdg-user-dirs, tunables/xdg-user-dirs.d
* Test merge from upstream new pyutils branch (rev 2385)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# ----------------------------------------------------------------------
 
2
#    Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
 
3
#
 
4
#    This program is free software; you can redistribute it and/or
 
5
#    modify it under the terms of version 2 of the GNU General Public
 
6
#    License as published by the Free Software Foundation.
 
7
#
 
8
#    This program is distributed in the hope that it will be useful,
 
9
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
#    GNU General Public License for more details.
 
12
#
 
13
# ----------------------------------------------------------------------
 
14
from __future__ import with_statement
 
15
import os
 
16
import re
 
17
from apparmor.common import AppArmorException, open_file_read, warn, convert_regexp  # , msg, error, debug
 
18
 
 
19
class Severity(object):
 
20
    def __init__(self, dbname=None, default_rank=10):
 
21
        """Initialises the class object"""
 
22
        self.PROF_DIR = '/etc/apparmor.d'  # The profile directory
 
23
        self.severity = dict()
 
24
        self.severity['DATABASENAME'] = dbname
 
25
        self.severity['CAPABILITIES'] = {}
 
26
        self.severity['FILES'] = {}
 
27
        self.severity['REGEXPS'] = {}
 
28
        self.severity['DEFAULT_RANK'] = default_rank
 
29
        # For variable expansions for the profile
 
30
        self.severity['VARIABLES'] = dict()
 
31
        if not dbname:
 
32
            return None
 
33
 
 
34
        with open_file_read(dbname) as database:  # open(dbname, 'r')
 
35
            for lineno, line in enumerate(database, start=1):
 
36
                line = line.strip()  # or only rstrip and lstrip?
 
37
                if line == '' or line.startswith('#'):
 
38
                    continue
 
39
                if line.startswith('/'):
 
40
                    try:
 
41
                        path, read, write, execute = line.split()
 
42
                        read, write, execute = int(read), int(write), int(execute)
 
43
                    except ValueError:
 
44
                        raise AppArmorException("Insufficient values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
 
45
                    else:
 
46
                        if read not in range(0, 11) or write not in range(0, 11) or execute not in range(0, 11):
 
47
                            raise AppArmorException("Inappropriate values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
 
48
                        path = path.lstrip('/')
 
49
                        if '*' not in path:
 
50
                            self.severity['FILES'][path] = {'r': read, 'w': write, 'x': execute}
 
51
                        else:
 
52
                            ptr = self.severity['REGEXPS']
 
53
                            pieces = path.split('/')
 
54
                            for index, piece in enumerate(pieces):
 
55
                                if '*' in piece:
 
56
                                    path = '/'.join(pieces[index:])
 
57
                                    regexp = convert_regexp(path)
 
58
                                    ptr[regexp] = {'AA_RANK': {'r': read, 'w': write, 'x': execute}}
 
59
                                    break
 
60
                                else:
 
61
                                    ptr[piece] = ptr.get(piece, {})
 
62
                                    ptr = ptr[piece]
 
63
                elif line.startswith('CAP_'):
 
64
                    try:
 
65
                        resource, severity = line.split()
 
66
                        severity = int(severity)
 
67
                    except ValueError:
 
68
                        error_message = 'No severity value present in file: %s\n\t[Line %s]: %s' % (dbname, lineno, line)
 
69
                        #error(error_message)
 
70
                        raise AppArmorException(error_message)  # from None
 
71
                    else:
 
72
                        if severity not in range(0, 11):
 
73
                            raise AppArmorException("Inappropriate severity value present in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
 
74
                        self.severity['CAPABILITIES'][resource] = severity
 
75
                else:
 
76
                    raise AppArmorException("Unexpected line in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))
 
77
 
 
78
    def handle_capability(self, resource):
 
79
        """Returns the severity of for the capability resource, default value if no match"""
 
80
        if resource in self.severity['CAPABILITIES'].keys():
 
81
            return self.severity['CAPABILITIES'][resource]
 
82
        # raise ValueError("unexpected capability rank input: %s"%resource)
 
83
        warn("unknown capability: %s" % resource)
 
84
        return self.severity['DEFAULT_RANK']
 
85
 
 
86
    def check_subtree(self, tree, mode, sev, segments):
 
87
        """Returns the max severity from the regex tree"""
 
88
        if len(segments) == 0:
 
89
            first = ''
 
90
        else:
 
91
            first = segments[0]
 
92
        rest = segments[1:]
 
93
        path = '/'.join([first] + rest)
 
94
        # Check if we have a matching directory tree to descend into
 
95
        if tree.get(first, False):
 
96
            sev = self.check_subtree(tree[first], mode, sev, rest)
 
97
        # If severity still not found, match against globs
 
98
        if sev is None:
 
99
            # Match against all globs at this directory level
 
100
            for chunk in tree.keys():
 
101
                if '*' in chunk:
 
102
                    # Match rest of the path
 
103
                    if re.search("^" + chunk, path):
 
104
                        # Find max rank
 
105
                        if "AA_RANK" in tree[chunk].keys():
 
106
                            for m in mode:
 
107
                                if sev is None or tree[chunk]["AA_RANK"].get(m, -1) > sev:
 
108
                                    sev = tree[chunk]["AA_RANK"].get(m, None)
 
109
        return sev
 
110
 
 
111
    def handle_file(self, resource, mode):
 
112
        """Returns the severity for the file, default value if no match found"""
 
113
        resource = resource[1:]    # remove initial / from path
 
114
        pieces = resource.split('/')    # break path into directory level chunks
 
115
        sev = None
 
116
        # Check for an exact match in the db
 
117
        if resource in self.severity['FILES'].keys():
 
118
            # Find max value among the given modes
 
119
            for m in mode:
 
120
                if sev is None or self.severity['FILES'][resource].get(m, -1) > sev:
 
121
                    sev = self.severity['FILES'][resource].get(m, None)
 
122
        else:
 
123
            # Search regex tree for matching glob
 
124
            sev = self.check_subtree(self.severity['REGEXPS'], mode, sev, pieces)
 
125
        if sev is None:
 
126
            # Return default rank if severity cannot be found
 
127
            return self.severity['DEFAULT_RANK']
 
128
        else:
 
129
            return sev
 
130
 
 
131
    def rank(self, resource, mode=None):
 
132
        """Returns the rank for the resource file/capability"""
 
133
        if '@' in resource:    # path contains variable
 
134
            return self.handle_variable_rank(resource, mode)
 
135
        elif resource[0] == '/':    # file resource
 
136
            return self.handle_file(resource, mode)
 
137
        elif resource[0:4] == 'CAP_':    # capability resource
 
138
            return self.handle_capability(resource)
 
139
        else:
 
140
            raise AppArmorException("Unexpected rank input: %s" % resource)
 
141
 
 
142
    def handle_variable_rank(self, resource, mode):
 
143
        """Returns the max possible rank for file resources containing variables"""
 
144
        regex_variable = re.compile('@{([^{.]*)}')
 
145
        rank = None
 
146
        if '@' in resource:
 
147
            variable = regex_variable.search(resource).groups()[0]
 
148
            variable = '@{%s}' % variable
 
149
            #variables = regex_variable.findall(resource)
 
150
            for replacement in self.severity['VARIABLES'][variable]:
 
151
                resource_replaced = self.variable_replace(variable, replacement, resource)
 
152
                rank_new = self.handle_variable_rank(resource_replaced, mode)
 
153
                #rank_new = self.handle_variable_rank(resource.replace('@{'+variable+'}', replacement), mode)
 
154
                if rank is None or rank_new > rank:
 
155
                    rank = rank_new
 
156
            return rank
 
157
        else:
 
158
            return self.handle_file(resource, mode)
 
159
 
 
160
    def variable_replace(self, variable, replacement, resource):
 
161
        """Returns the expanded path for the passed variable"""
 
162
        leading = False
 
163
        trailing = False
 
164
        # Check for leading or trailing / that may need to be collapsed
 
165
        if resource.find("/" + variable) != -1 and resource.find("//" + variable) == -1:  # find that a single / exists before variable or not
 
166
            leading = True
 
167
        if resource.find(variable + "/") != -1 and resource.find(variable + "//") == -1:
 
168
            trailing = True
 
169
        if replacement[0] == '/' and replacement[:2] != '//' and leading:  # finds if the replacement has leading / or not
 
170
            replacement = replacement[1:]
 
171
        if replacement[-1] == '/' and replacement[-2:] != '//' and trailing:
 
172
            replacement = replacement[:-1]
 
173
        return resource.replace(variable, replacement)
 
174
 
 
175
    def load_variables(self, prof_path):
 
176
        """Loads the variables for the given profile"""
 
177
        regex_include = re.compile('^#?include\s*<(\S*)>')
 
178
        if os.path.isfile(prof_path):
 
179
            with open_file_read(prof_path) as f_in:
 
180
                for line in f_in:
 
181
                    line = line.strip()
 
182
                    # If any includes, load variables from them first
 
183
                    match = regex_include.search(line)
 
184
                    if match:
 
185
                        new_path = match.groups()[0]
 
186
                        new_path = self.PROF_DIR + '/' + new_path
 
187
                        self.load_variables(new_path)
 
188
                    else:
 
189
                        # Remove any comments
 
190
                        if '#' in line:
 
191
                            line = line.split('#')[0].rstrip()
 
192
                        # Expected format is @{Variable} = value1 value2 ..
 
193
                        if line.startswith('@') and '=' in line:
 
194
                            if '+=' in line:
 
195
                                line = line.split('+=')
 
196
                                try:
 
197
                                    self.severity['VARIABLES'][line[0]] += [i.strip('"') for i in line[1].split()]
 
198
                                except KeyError:
 
199
                                    raise AppArmorException("Variable %s was not previously declared, but is being assigned additional value in file: %s" % (line[0], prof_path))
 
200
                            else:
 
201
                                line = line.split('=')
 
202
                                if line[0] in self.severity['VARIABLES'].keys():
 
203
                                    raise AppArmorException("Variable %s was previously declared in file: %s" % (line[0], prof_path))
 
204
                                self.severity['VARIABLES'][line[0]] = [i.strip('"') for i in line[1].split()]
 
205
 
 
206
    def unload_variables(self):
 
207
        """Clears all loaded variables"""
 
208
        self.severity['VARIABLES'] = dict()