<div dir="ltr"><div><div><div><div><div><div>Hello,<br></div><div><br>The following patch:<br></div>1. Refactors Severity module in hopes of making it more readable and simplifying up some of the terse logic. (This resulted in re-write of some segments)<br></div>2. Updates relevant modules/tools accordingly (checked with grep for usages)<br><br></div>Side notes:<br></div>1. The tests were ultra-useful while refactoring, hopefully they cover sufficient stuff.<br></div>2. I missed not working with git (but then I am not familiar with bzr)<br></div>3. Comments are welcome regarding bugs and style<br><br><div><br clear="all"><div><div><div><div><div><div>=== modified file 'utils/aa-mergeprof'<br>--- utils/aa-mergeprof    2016-05-10 12:32:46 +0000<br>+++ utils/aa-mergeprof    2016-06-01 21:16:50 +0000<br>@@ -18,14 +18,18 @@<br> import os<br> <br> import apparmor.aa<br>-from apparmor.aa import available_buttons, combine_name, delete_duplicates, get_profile_filename, is_known_rule, match_includes<br> import apparmor.aamode<br>-from apparmor.common import AppArmorException<br>-from apparmor.regex import re_match_include<br>+<br> import apparmor.severity<br> import apparmor.cleanprofile as cleanprofile<br> import apparmor.ui as aaui<br> <br>+from apparmor.aa import (available_buttons, combine_name, delete_duplicates, <br>+                         get_profile_filename, is_known_rule, match_includes)<br>+from apparmor.common import AppArmorException<br>+from apparmor.regex import re_match_include<br>+<br>+<br> # setup exception handling<br> from apparmor.fail import enable_aa_exception_handler<br> enable_aa_exception_handler()<br>@@ -660,7 +664,7 @@<br> <br>                             # Load variables into sev_db? Not needed/used for capabilities and network rules.<br>                             severity = rule_obj.severity(sev_db)<br>-                            if severity != sev_db.NOT_IMPLEMENTED:<br>+                            if severity != apparmor.severity.NOT_IMPLEMENTED:<br>                                 q.headers += [_('Severity'), severity]<br> <br>                             q.functions = available_buttons(rule_obj)<br><br>=== modified file 'utils/apparmor/aa.py'<br>--- utils/apparmor/aa.py    2016-05-10 12:32:46 +0000<br>+++ utils/apparmor/aa.py    2016-05-29 18:40:31 +0000<br>@@ -1644,7 +1644,7 @@<br> <br>                             # Load variables into sev_db? Not needed/used for capabilities and network rules.<br>                             severity = rule_obj.severity(sev_db)<br>-                            if severity != sev_db.NOT_IMPLEMENTED:<br>+                            if severity != apparmor.severity.NOT_IMPLEMENTED:<br>                                 q.headers += [_('Severity'), severity]<br> <br>                             q.functions = available_buttons(rule_obj)<br><br>=== modified file 'utils/apparmor/rule/__init__.py'<br>--- utils/apparmor/rule/__init__.py    2016-01-25 22:48:34 +0000<br>+++ utils/apparmor/rule/__init__.py    2016-05-29 18:42:17 +0000<br>@@ -13,6 +13,8 @@<br> #<br> # ----------------------------------------------------------------------<br> <br>+import apparmor.severity<br>+<br> from apparmor.aare import AARE<br> from apparmor.common import AppArmorBug, type_is_str<br> <br>@@ -237,9 +239,9 @@<br>         '''return severity of this rule, which can be:<br>            - a number between 0 and 10, where 0 means harmless and 10 means critical,<br>            - "unknown" (to be exact: the value specified for "unknown" as set when loading the severity database), or<br>-           - sev_db.NOT_IMPLEMENTED if no severity check is implemented for this rule type.<br>+           - apparmor.severity.NOT_IMPLEMENTED if no severity check is implemented for this rule type.<br>            sev_db must be an apparmor.severity.Severity object.'''<br>-        return sev_db.NOT_IMPLEMENTED<br>+        return apparmor.severity.NOT_IMPLEMENTED<br> <br>     def logprof_header(self):<br>         '''return the headers (human-readable version of the rule) to display in aa-logprof for this rule object<br><br>=== modified file 'utils/apparmor/rule/capability.py'<br>--- utils/apparmor/rule/capability.py    2016-01-25 22:48:34 +0000<br>+++ utils/apparmor/rule/capability.py    2016-06-01 21:17:54 +0000<br>@@ -13,10 +13,11 @@<br> #<br> # ----------------------------------------------------------------------<br> <br>+import re<br>+<br> from apparmor.regex import RE_PROFILE_CAP<br> from apparmor.common import AppArmorBug, AppArmorException, type_is_str<br> from apparmor.rule import BaseRule, BaseRuleset, logprof_value_or_all, parse_modifiers<br>-import re<br> <br> # setup module translations<br> from apparmor.translations import init_translation<br><br>=== modified file 'utils/apparmor/severity.py'<br>--- utils/apparmor/severity.py    2015-06-19 19:43:19 +0000<br>+++ utils/apparmor/severity.py    2016-06-01 21:11:08 +0000<br>@@ -12,134 +12,170 @@<br> #<br> # ----------------------------------------------------------------------<br> from __future__ import with_statement<br>+<br> import os<br> import re<br>-from apparmor.common import AppArmorException, open_file_read, warn, convert_regexp  # , msg, error, debug<br>+<br>+from collections import defaultdict<br>+<br>+from apparmor.common import AppArmorException, open_file_read, warn, convert_regexp<br> from apparmor.regex import re_match_include<br>+from apparmor.utility import Trie, TrieNode<br>+<br>+CAPABILITY_ALL = '__ALL__'<br>+DEFAULT_PROFILE_DIR = '/etc/apparmor.d'<br>+DEFAULT_RANK = 10;<br>+NOT_IMPLEMENTED = '_-*not*implemented*-_'  # used for rule types that don't have severity ratings<br>+VALID_SEVERITY_RANGE = range(0,11)<br>+MINIMUM_SEVERITY = float('-inf')<br>+<br>+<br>+class SeverityRegexTrieNode(TrieNode):<br>+    """A Trie node to store information for regex trie used for calculating severity"""<br>+    def __init__(self, path_piece):<br>+        self.mode = None<br>+        self.path_piece = path_piece<br>+        self.child = dict()<br>+        self.globs = dict()<br>+<br>+    def get_mode(self):<br>+        return self.mode<br>+<br>+    def set_mode(self, mode):<br>+        if self.mode: raise Exception("Mode already exists for path piece: " + self.path_piece)<br>+<br>+        self.mode = mode<br>+<br>+    def update_mode(self, mode):<br>+        if not self.mode: raise Exception("Mode was never set for path piece: " + self.path_piece)<br>+<br>+        self.mode = mode<br>+<br>+    def add_glob(self, glob, mode):<br>+        self.globs[glob] = mode<br>+<br>+    def get_globs(self):<br>+        return self.globs<br>+<br>+    def is_terminal_node(self):<br>+        return self.mode is not None<br>+<br> <br> class Severity(object):<br>-    def __init__(self, dbname=None, default_rank=10):<br>-        """Initialises the class object"""<br>-        self.PROF_DIR = '/etc/apparmor.d'  # The profile directory<br>-        self.NOT_IMPLEMENTED = '_-*not*implemented*-_'  # used for rule types that don't have severity ratings<br>-        self.severity = dict()<br>-        self.severity['DATABASENAME'] = dbname<br>-        self.severity['CAPABILITIES'] = {}<br>-        self.severity['FILES'] = {}<br>-        self.severity['REGEXPS'] = {}<br>-        self.severity['DEFAULT_RANK'] = default_rank<br>-        # For variable expansions for the profile<br>-        self.severity['VARIABLES'] = dict()<br>-        if not dbname:<br>-            raise AppArmorException("No severity db file given")<br>-<br>-        with open_file_read(dbname) as database:  # open(dbname, 'r')<br>-            for lineno, line in enumerate(database, start=1):<br>-                line = line.strip()  # or only rstrip and lstrip?<br>+    def __init__(self, dbname=None, default_rank=DEFAULT_RANK, profile_dir=DEFAULT_PROFILE_DIR):<br>+        """Initialises the Severity object"""<br>+<br>+        if not dbname or not os.path.isfile(dbname):<br>+            raise AppArmorException("Invalid severity db file given: " + str(dbname))<br>+<br>+        self.profile_dir = profile_dir<br>+        self.database_name = dbname<br>+        self.default_rank = default_rank<br>+<br>+        self.capabilities = defaultdict(lambda: self.default_rank)<br>+        self.files = dict()<br>+        self.regex_trie = Trie(SeverityRegexTrieNode)<br>+        # For variable expansions in a profile<br>+        self.variables = dict()<br>+<br>+        # Init the the database<br>+        self.load_database()<br>+<br>+    def load_database(self):<br>+        with open_file_read(self.database_name) as database:<br>+            for line_no, line in enumerate(database, start=1):<br>+                line = line.strip()<br>                 if line == '' or line.startswith('#'):<br>                     continue<br>+<br>                 if line.startswith('/'):<br>-                    try:<br>-                        path, read, write, execute = line.split()<br>-                        read, write, execute = int(read), int(write), int(execute)<br>-                    except ValueError:<br>-                        raise AppArmorException("Insufficient values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))<br>-                    else:<br>-                        if read not in range(0, 11) or write not in range(0, 11) or execute not in range(0, 11):<br>-                            raise AppArmorException("Inappropriate values for permissions in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))<br>-                        path = path.lstrip('/')<br>-                        if '*' not in path:<br>-                            self.severity['FILES'][path] = {'r': read, 'w': write, 'x': execute}<br>-                        else:<br>-                            ptr = self.severity['REGEXPS']<br>-                            pieces = path.split('/')<br>-                            for index, piece in enumerate(pieces):<br>-                                if '*' in piece:<br>-                                    path = '/'.join(pieces[index:])<br>-                                    regexp = convert_regexp(path)<br>-                                    ptr[regexp] = {'AA_RANK': {'r': read, 'w': write, 'x': execute}}<br>-                                    break<br>-                                else:<br>-                                    ptr[piece] = ptr.get(piece, {})<br>-                                    ptr = ptr[piece]<br>-                elif line.startswith('CAP_'):<br>-                    try:<br>-                        resource, severity = line.split()<br>-                        severity = int(severity)<br>-                    except ValueError:<br>-                        error_message = 'No severity value present in file: %s\n\t[Line %s]: %s' % (dbname, lineno, line)<br>-                        #error(error_message)<br>-                        raise AppArmorException(error_message)  # from None<br>-                    else:<br>-                        if severity not in range(0, 11):<br>-                            raise AppArmorException("Inappropriate severity value present in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))<br>-                        self.severity['CAPABILITIES'][resource] = severity<br>+                    path, mode_severities = self.get_path_and_mode_severities(line, line_no)<br>+<br>+                    if not path_contains_regex(path):<br>+                        self.files[path] = mode_severities<br>+                    else:<br>+                        add_path_to_trie(self.regex_trie, path, mode_severities)<br>+<br>+                elif is_capability(line):<br>+                    resource, severity = self.get_resource_and_severity(line, line_no)<br>+                    self.capabilities[resource] = severity<br>+<br>                 else:<br>-                    raise AppArmorException("Unexpected line in file: %s\n\t[Line %s]: %s" % (dbname, lineno, line))<br>+                    raise AppArmorException("Unexpected line in file: %(database_name)s\n\t[Line %(line_no)d]: %(line)s"<br>+                                                   % {'database_name' : self.database_name, 'line_no' : line_no, 'line': line})<br>+<br>+    def get_path_and_mode_severities(self, line, line_no):<br>+        try:<br>+            path, read, write, execute = line.split()<br>+            read, write, execute = int(read), int(write), int(execute)<br>+        except ValueError:<br>+            raise AppArmorException("Insufficient values for permissions in file: %(database_name)s\n\t[Line %(line_no)d]: %(line)s"<br>+                                    % {'database_name' : self.database_name, 'line_no' : line_no, 'line': line})<br>+        else:<br>+            if not is_severity_valid(read) or not is_severity_valid(write) or not is_severity_valid(execute):<br>+                raise AppArmorException("Inappropriate values for permissions in file: %(database_name)s\n\t[Line %(line_no)d]: %(line)s"<br>+                                        % {'database_name' : self.database_name, 'line_no' : line_no, 'line': line})<br>+<br>+            return path.lstrip('/'), {'r': read, 'w': write, 'x': execute}<br>+<br>+    def get_resource_and_severity(self, line, line_no):<br>+        try:<br>+            resource, severity = line.split()<br>+            severity = int(severity)<br>+        except ValueError:<br>+            raise AppArmorException("No severity value present in file: %(database_name)s\n\t[Line %(line_no)d]: %(line)s"<br>+                                                   % {'database_name' : self.database_name, 'line_no' : line_no, 'line': line})<br>+        else:<br>+            if not is_severity_valid(severity):<br>+                raise AppArmorException("Inappropriate severity value present in file: %(database_name)s\n\t[Line %(line_no)d]: %(line)s"<br>+                                                   % {'database_name' : self.database_name, 'line_no' : line_no, 'line': line})<br>+<br>+            return resource, severity<br> <br>     def rank_capability(self, resource):<br>         """Returns the severity of for the capability resource, default value if no match"""<br>+        resource = resource.upper()<br>         cap = 'CAP_%s' % resource.upper()<br>-        if resource == '__ALL__':<br>-            return max(self.severity['CAPABILITIES'].values())<br>-        if cap in self.severity['CAPABILITIES'].keys():<br>-            return self.severity['CAPABILITIES'][cap]<br>-        # raise ValueError("unexpected capability rank input: %s"%resource)<br>-        warn("unknown capability: %s" % resource)<br>-        return self.severity['DEFAULT_RANK']<br>-<br>-    def check_subtree(self, tree, mode, sev, segments):<br>-        """Returns the max severity from the regex tree"""<br>-        if len(segments) == 0:<br>-            first = ''<br>-        else:<br>-            first = segments[0]<br>-        rest = segments[1:]<br>-        path = '/'.join([first] + rest)<br>-        # Check if we have a matching directory tree to descend into<br>-        if tree.get(first, False):<br>-            sev = self.check_subtree(tree[first], mode, sev, rest)<br>-        # If severity still not found, match against globs<br>-        if sev is None:<br>-            # Match against all globs at this directory level<br>-            for chunk in tree.keys():<br>-                if '*' in chunk:<br>-                    # Match rest of the path<br>-                    if re.search("^" + chunk, path):<br>-                        # Find max rank<br>-                        if "AA_RANK" in tree[chunk].keys():<br>-                            for m in mode:<br>-                                if sev is None or tree[chunk]["AA_RANK"].get(m, -1) > sev:<br>-                                    sev = tree[chunk]["AA_RANK"].get(m, None)<br>-        return sev<br>+<br>+        if resource == CAPABILITY_ALL:<br>+            return max(self.capabilities.values())<br>+<br>+        if not cap in self.capabilities.keys():<br>+            warn("unknown capability: %s" % resource)<br>+<br>+        return self.capabilities[cap]<br> <br>     def handle_file(self, resource, mode):<br>         """Returns the severity for the file, default value if no match found"""<br>         resource = resource[1:]    # remove initial / from path<br>         pieces = resource.split('/')    # break path into directory level chunks<br>-        sev = None<br>-        # Check for an exact match in the db<br>-        if resource in self.severity['FILES'].keys():<br>-            # Find max value among the given modes<br>-            for m in mode:<br>-                if sev is None or self.severity['FILES'][resource].get(m, -1) > sev:<br>-                    sev = self.severity['FILES'][resource].get(m, None)<br>+        severity = None<br>+        # Check for an exact match in the sverity db db<br>+        if resource in self.files:<br>+            severity = find_max_severity_for_modes(mode, self.files[resource])<br>         else:<br>-            # Search regex tree for matching glob<br>-            sev = self.check_subtree(self.severity['REGEXPS'], mode, sev, pieces)<br>-        if sev is None:<br>+            # Search regex trie for a matching glob<br>+            modes = self.regex_trie.find_node(pieces, find_matching_modes_for_path)<br>+            if modes is None:<br>+                severity = None<br>+            else:<br>+                modes = filter(lambda x: x is not None, modes)<br>+                for mode_severities in modes:<br>+                    severity = find_max_severity_for_modes(mode, mode_severities, severity)<br>+<br>+        if severity is None:<br>             # Return default rank if severity cannot be found<br>-            return self.severity['DEFAULT_RANK']<br>+            return self.default_rank<br>         else:<br>-            return sev<br>+            return severity<br> <br>     def rank(self, resource, mode=None):<br>         """Returns the rank for the resource file/capability"""<br>         if '@' in resource:    # path contains variable<br>             return self.handle_variable_rank(resource, mode)<br>-        elif resource[0] == '/':    # file resource<br>+        elif resource.startswith('/'):    # file resource<br>             return self.handle_file(resource, mode)<br>-        elif resource[0:4] == 'CAP_':    # capability resource<br>+        elif is_capability(resource):<br>             return self.rank_capability(resource[4:])<br>         else:<br>             raise AppArmorException("Unexpected rank input: %s" % resource)<br>@@ -149,16 +185,18 @@<br>         regex_variable = re.compile('@{([^{.]*)}')<br>         matches = regex_variable.search(resource)<br>         if matches:<br>-            rank = self.severity['DEFAULT_RANK']<br>+            rank = self.default_rank<br>             variable = '@{%s}' % matches.groups()[0]<br>-            #variables = regex_variable.findall(resource)<br>-            for replacement in self.severity['VARIABLES'][variable]:<br>+<br>+            for replacement in self.variables[variable]:<br>                 resource_replaced = self.variable_replace(variable, replacement, resource)<br>                 rank_new = self.handle_variable_rank(resource_replaced, mode)<br>-                if rank == self.severity['DEFAULT_RANK']:<br>-                    rank = rank_new<br>-                elif rank_new != self.severity['DEFAULT_RANK'] and rank_new > rank:<br>-                    rank = rank_new<br>+<br>+                if rank == self.default_rank:<br>+                    rank = rank_new<br>+                elif rank_new != self.default_rank and rank_new > rank:<br>+                    rank = rank_new<br>+<br>             return rank<br>         else:<br>             return self.handle_file(resource, mode)<br>@@ -178,35 +216,104 @@<br>             replacement = replacement[:-1]<br>         return resource.replace(variable, replacement)<br> <br>-    def load_variables(self, prof_path):<br>-        """Loads the variables for the given profile"""<br>-        if os.path.isfile(prof_path):<br>-            with open_file_read(prof_path) as f_in:<br>-                for line in f_in:<br>-                    line = line.strip()<br>-                    # If any includes, load variables from them first<br>-                    match = re_match_include(line)<br>-                    if match:<br>-                        new_path = self.PROF_DIR + '/' + match<br>-                        self.load_variables(new_path)<br>-                    else:<br>-                        # Remove any comments<br>-                        if '#' in line:<br>-                            line = line.split('#')[0].rstrip()<br>-                        # Expected format is @{Variable} = value1 value2 ..<br>-                        if line.startswith('@') and '=' in line:<br>-                            if '+=' in line:<br>-                                line = line.split('+=')<br>-                                try:<br>-                                    self.severity['VARIABLES'][line[0]] += [i.strip('"') for i in line[1].split()]<br>-                                except KeyError:<br>-                                    raise AppArmorException("Variable %s was not previously declared, but is being assigned additional value in file: %s" % (line[0], prof_path))<br>-                            else:<br>-                                line = line.split('=')<br>-                                if line[0] in self.severity['VARIABLES'].keys():<br>-                                    raise AppArmorException("Variable %s was previously declared in file: %s" % (line[0], prof_path))<br>-                                self.severity['VARIABLES'][line[0]] = [i.strip('"') for i in line[1].split()]<br>+    def load_variables(self, file_path):<br>+        """Loads the variables from the given file"""<br>+        if not os.path.isfile(file_path):<br>+            return<br>+<br>+        with open_file_read(file_path) as f_in:<br>+            for line in f_in:<br>+                line = line.strip()<br>+                # If any includes, load variables from them first<br>+                match = re_match_include(line)<br>+                if match:<br>+                    new_path = self.profile_dir + '/' + match<br>+                    self.load_variables(new_path)<br>+                else:<br>+                    # Remove any comments<br>+                    if '#' in line:<br>+                        line = line.split('#')[0].rstrip()<br>+                    # Expected format is @{Variable} = value1 value2 ..<br>+                    if line.startswith('@') and '=' in line:<br>+                        if '+=' in line:<br>+                            line = line.split('+=')<br>+                            try:<br>+                                self.variables[line[0]] += [i.strip('"') for i in line[1].split()]<br>+                            except KeyError:<br>+                                raise AppArmorException("Variable %s was not previously declared, but is being assigned additional value in file: %s" % (line[0], file_path))<br>+                        else:<br>+                            line = line.split('=')<br>+                            if line[0] in self.variables.keys():<br>+                                raise AppArmorException("Variable %s was previously declared in file: %s" % (line[0], file_path))<br>+                            self.variables[line[0]] = [i.strip('"') for i in line[1].split()]<br> <br>     def unload_variables(self):<br>         """Clears all loaded variables"""<br>-        self.severity['VARIABLES'] = dict()<br>+        self.variables = dict()<br>+<br>+<br>+def is_severity_valid(severity):<br>+    return severity in VALID_SEVERITY_RANGE<br>+<br>+def path_contains_regex(path):<br>+    return '*' in path<br>+<br>+def is_capability(resource):<br>+    return resource.startswith('CAP_')<br>+<br>+def find_max_severity_for_modes(modes, mode_severities, severity=MINIMUM_SEVERITY):<br>+    if severity is None:<br>+        severity = MINIMUM_SEVERITY<br>+<br>+    for mode in modes:<br>+        severity = max(severity, mode_severities.get(mode, MINIMUM_SEVERITY))<br>+<br>+    if severity == MINIMUM_SEVERITY:<br>+        return None<br>+<br>+    return severity<br>+<br>+def add_path_to_trie(trie, path, mode_severities):<br>+    node = trie.get_head()<br>+<br>+    path_pieces = path.split('/')<br>+<br>+    for index, path_piece in enumerate(path_pieces):<br>+        if path_contains_regex(path_piece):<br>+            path_onwards = '/'.join(path_pieces[index:])<br>+<br>+            node.add_glob(convert_regexp(path_onwards), mode_severities)<br>+            break<br>+        else:<br>+            if node.is_present(path_piece):<br>+                node = node.get_child(path_piece)<br>+            else:<br>+                child_node = SeverityRegexTrieNode(path_piece)<br>+                node.add_child(path_piece, child_node)<br>+                node = child_node<br>+<br>+    return trie<br>+<br>+def find_matching_modes_for_path(node, keys):<br>+    if not keys:<br>+        ## TODO: Possibly merge file paths into the trie<br>+        #if node.has_value():<br>+        #    return node.get_mode()<br>+        return None<br>+<br>+    key = keys[0]<br>+    path = '/'.join(keys)<br>+    modes = None<br>+<br>+    if node.is_present(key):<br>+        modes = find_matching_modes_for_path(node.get_child(key), keys[1:])<br>+<br>+    globs_to_mode = node.get_globs()<br>+    if modes is None and globs_to_mode is not None:<br>+        modes = []<br>+<br>+        for glob in globs_to_mode:<br>+            if re.search("^" + glob, path):<br>+                modes.append(globs_to_mode[glob])<br>+<br>+    return modes<br><br>=== added file 'utils/apparmor/utility.py'<br>--- utils/apparmor/utility.py    1970-01-01 00:00:00 +0000<br>+++ utils/apparmor/utility.py    2016-06-01 21:04:40 +0000<br>@@ -0,0 +1,43 @@<br>+class Trie(object):<br>+    """A basic skeleton of a trie data structure<br>+        supports basic head, find operations for a <br>+        type of node using custom search function<br>+    """<br>+    def __init__(self, node_type):<br>+        self.root = node_type(None)<br>+    <br>+    def get_head(self):<br>+        return self.root<br>+    <br>+    def find_node(self, keys, search_function):<br>+        if len(keys) > 0 and self.root.is_present(keys[0]):<br>+            return search_function(self.root.get_child(keys[0]), keys[1:])<br>+<br>+        return None<br>+<br>+<br>+class TrieNode(object):<br>+    """A basic trie node supporting basic operations to add child,<br>+        search presence of a child, find if it is a leaf node<br>+    """<br>+    def __init__(self):<br>+        self.child = dict()<br>+        self.is_value_present = False<br>+<br>+    def add_child(self, key, child_node):<br>+        # Currently sev_db has multiple entries for same path,<br>+        # maybe we want to do this in a strict mode or never<br>+        # if key in self.child: raise Exception("TrieNode already exists for key: " + key)<br>+        self.child[key] = child_node<br>+<br>+    def is_present(self, key):<br>+        return key in self.child<br>+<br>+    def get_child(self, key):<br>+        return self.child[key]<br>+<br>+    def has_value(self):<br>+        return self.is_value_present<br>+<br>+    def is_terminal_node(self):<br>+        return self.child.keys()<br><br>=== modified file 'utils/test/test-severity.py'<br>--- utils/test/test-severity.py    2015-06-06 11:59:11 +0000<br>+++ utils/test/test-severity.py    2016-06-01 20:35:09 +0000<br>@@ -65,6 +65,7 @@<br>         ('UNKNOWN', 'unknown'),<br>         ('K*', 'unknown'),<br>         ('__ALL__', 10),<br>+        ('__all__', 10)<br>     ]<br> <br>     def _run_test(self, params, expected):<br>@@ -167,7 +168,7 @@<br>         self.assertRaises(AppArmorException, severity.Severity, 'severity_broken.db')<br> <br>     def test_nonexistent_db(self):<br>-        self.assertRaises(IOError, severity.Severity, 'severity.db.does.not.exist')<br>+        self.assertRaises(AppArmorException, severity.Severity, 'severity.db.does.not.exist')<br> <br>     def test_no_arg_to_severity(self):<br>         with self.assertRaises(AppArmorException):<br><br><br><br><br>-- <br><div class="gmail_signature" data-smartmail="gmail_signature"><div dir="ltr"><div>Regards,<br><br></div>Kshitij Gupta<br></div></div>
</div></div></div></div></div></div></div></div>