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