From b46f7a426c7d4da596dba23885de44955fbd14ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20B=C3=A9lair?= Date: Tue, 13 May 2025 16:38:36 +0200 Subject: [PATCH] Add support for --show-matching-path and xattrs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new option --show-matching-path shows a path that matches in the host filesystem, to prove that the profile is indeed used. Also, profiles' xattrs are now parsed into a dict and are taken in consideration when looking for matching profiles. Signed-off-by: Maxime Bélair --- utils/aa-show-usage | 49 +++++++++++------- utils/aa-show-usage.pod | 4 ++ utils/apparmor/profile_storage.py | 37 +++++--------- utils/apparmor/regex.py | 31 +++++++++++- utils/test/cleanprof_test.out | 2 +- utils/test/test-aa-show-usage.py | 4 +- utils/test/test-aa.py | 6 +-- utils/test/test-profile-storage.py | 81 +++++++++++++++++------------- utils/test/test-regex_matches.py | 23 ++++++++- 9 files changed, 151 insertions(+), 86 deletions(-) diff --git a/utils/aa-show-usage b/utils/aa-show-usage index 56116dfa7..8950576c1 100755 --- a/utils/aa-show-usage +++ b/utils/aa-show-usage @@ -30,26 +30,35 @@ _ = init_translation() MAX_RECURSION = 10 -def has_matching_file(pattern): - for pat in expand_braces(pattern): - if any(glob.iglob(pat, recursive=True)): - return True - return False +def has_matching_file(pattern, xattrs=None): + for p in expand_braces(pattern): + for path in glob.iglob(p, recursive=True): + if os.path.realpath(path) != os.path.abspath(path): # remove symlinks + continue + if not xattrs or all(os.getxattr(path, name) == val for name, val in xattrs.items()): + return path + return None -def display_profile_text(used, unused): +def display_profile_text(used, unused, show_matching_path): if used: print(_('Used profiles:')) - for (name, attach, path) in used: - print(_(' Profile {} for {} ({})').format(name, attach, path)) + for (name, attach, path, match) in used: + print(_(' Profile {} for {} ({}) {}').format(name, attach, path, ('→ ' + match) if show_matching_path else '')) if unused: print(_('Unused profiles:')) - for (name, attach, path) in unused: - print(_(' Profile {} for {} ({})').format(name, attach, path)) + for (name, attach, path, match) in unused: + print(_(' Profile {} for {} ({}) ').format(name, attach, path)) def profiles_to_json(profiles): - return [{'name': profile_name, 'attach': attach, 'path': path} for profile_name, attach, path in profiles] + result = [] + for profile_name, attach, path, matching_path in profiles: + entry = {'name': profile_name, 'attach': attach, 'path': path} + if matching_path: + entry['matching_path'] = matching_path + result.append(entry) + return result def display_profile_json(used, unused): @@ -90,15 +99,16 @@ def get_used_profiles(args, prof_filter): var_dict = aa.active_profiles.get_all_merged_variables(filename, aa.include_list_recursive(aa.active_profiles.files[filename], True)) resolved = resolve_variables(a, var_dict) - found = False + matching_path = None for entry in resolved: - if has_matching_file(entry): - found = True + matching_path = has_matching_file(entry) + if matching_path: + break - if found and args.show_type != 'unused': - used.append((profile_name, a, filename)) - if not found and args.show_type != 'used': - unused.append((profile_name, a, filename)) + if matching_path and args.show_type != 'unused': + used.append((profile_name, a, filename, matching_path)) + if not matching_path and args.show_type != 'used': + unused.append((profile_name, a, filename, matching_path)) return used, unused @@ -108,6 +118,7 @@ def main(): parser.add_argument('-s', '--show-type', type=str, default='all', choices=['all', 'used', 'unused'], help=_('Type of profiles to show')) parser.add_argument('-j', '--json', action='store_true', help=_('Output in JSON')) parser.add_argument('-d', '--dir', type=str, help=_('Path to profiles')) + parser.add_argument('--show-matching-path', action='store_true', help=_('Show the path of a file matching the profile')) parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS) filter_group = parser.add_argument_group(_('Filtering options'), @@ -137,7 +148,7 @@ def main(): if args.json: display_profile_json(used, unused) else: - display_profile_text(used, unused) + display_profile_text(used, unused, args.show_matching_path) if __name__ == '__main__': diff --git a/utils/aa-show-usage.pod b/utils/aa-show-usage.pod index 0caf07e4a..9eab60d5d 100644 --- a/utils/aa-show-usage.pod +++ b/utils/aa-show-usage.pod @@ -50,6 +50,10 @@ Output in JSON Path to profiles +=item --show-matching-path + +Show the path of a file matching the profile. Only the first matching path of an executable is shown (not the whole list). + =back =head1 FILTERING OPTIONS diff --git a/utils/apparmor/profile_storage.py b/utils/apparmor/profile_storage.py index 40306d150..26fd91ee8 100644 --- a/utils/apparmor/profile_storage.py +++ b/utils/apparmor/profile_storage.py @@ -13,9 +13,10 @@ # # ---------------------------------------------------------------------- +from types import NoneType from apparmor.common import AppArmorBug, AppArmorException -from apparmor.regex import parse_profile_start_line +from apparmor.regex import parse_profile_start_line, re_print_dict from apparmor.rule import quote_if_needed from apparmor.rule.abi import AbiRule, AbiRuleset from apparmor.rule.all import AllRule, AllRuleset @@ -79,7 +80,7 @@ class ProfileStorage: data['parent'] = '' # parent profile, or '' for top-level profiles and external hats data['name'] = '' data['attachment'] = '' - data['xattrs'] = '' + data['xattrs'] = {} data['flags'] = '' data['external'] = False data['header_comment'] = '' # comment in the profile/hat start line @@ -100,28 +101,16 @@ class ProfileStorage: if key not in self.data: raise AppArmorBug('attempt to set unknown key %s' % key) - # allow writing bool values - if isinstance(self.data[key], bool): - if isinstance(value, bool): + allowed_types = {bool, str, dict, None, NoneType} + old_type = type(self.data[key]) + if old_type in allowed_types: + if key in {'flags', 'filename'} and type(value) in {str, NoneType}: + self.data[key] = value + elif isinstance(value, old_type): self.data[key] = value else: - raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, type(self.data[key]), type(value), value)) - - # allow writing str or None to some keys - elif key in ('flags', 'filename'): - if isinstance(value, str) or value is None: - self.data[key] = value - else: - raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, type(self.data[key]), type(value), value)) - - # allow writing str values - elif isinstance(self.data[key], str): - if isinstance(value, str): - self.data[key] = value - else: - raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, type(self.data[key]), type(value), value)) - - # don't allow overwriting of other types + raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, old_type, type(value), value)) + self.data[key] = value else: raise AppArmorBug('Attempt to overwrite "%s" with %s, type %s' % (key, value, type(value))) @@ -168,7 +157,7 @@ class ProfileStorage: xattrs = '' if self.data['xattrs']: - xattrs = ' xattrs=(%s)' % self.data['xattrs'] + xattrs = ' xattrs=(%s)' % re_print_dict(self.data['xattrs']) flags = '' if self.data['flags']: @@ -263,7 +252,7 @@ class ProfileStorage: else: prof_storage['profile_keyword'] = matches['profile_keyword'] prof_storage['attachment'] = matches['attachment'] or '' - prof_storage['xattrs'] = matches['xattrs'] or '' + prof_storage['xattrs'] = matches['xattrs'] or {} return (profile, hat, prof_storage) diff --git a/utils/apparmor/regex.py b/utils/apparmor/regex.py index 121310d11..ed33cd1de 100644 --- a/utils/apparmor/regex.py +++ b/utils/apparmor/regex.py @@ -29,10 +29,11 @@ RE_COMMA_EOL = r'\s*,' + RE_EOL # optional whitespace, comma + RE_EOL RE_PROFILE_NAME = r'(?P<%s>(\S+|"[^"]+"))' # string without spaces, or quoted string. %s is the match group name RE_PATH = r'/\S*|"/[^"]*"' # filename (starting with '/') without spaces, or quoted filename. RE_VAR = r'@{[^}\s]+}' +RE_DICT_ENTRY = r'\s*(?P[^,\s=]+)(?:=(?P[^,\s=]+))?\s*' RE_PROFILE_PATH = '(?P<%s>(' + RE_PATH + '))' # quoted or unquoted filename. %s is the match group name RE_PROFILE_PATH_OR_VAR = '(?P<%s>(' + RE_PATH + '|' + RE_VAR + r'\S*|"' + RE_VAR + '[^"]*"))' # quoted or unquoted filename or variable. %s is the match group name RE_SAFE_OR_UNSAFE = '(?P(safe|unsafe))' -RE_XATTRS = r'(\s+xattrs\s*=\s*\((?P([^)=]+(=[^)=]+)?\s?)+)\)\s*)?' +RE_XATTRS = r'(\s+xattrs\s*=\s*\((?P([^)=]+(=[^)=]+)?\s?)*)\)\s*)?' RE_FLAGS = r'(\s+(flags\s*=\s*)?\((?P[^)]+)\))?' RE_VARIABLE = re.compile(RE_VAR) @@ -166,6 +167,10 @@ def parse_profile_start_line(line, filename): else: result['profile'] = result['namedprofile'] result['profile_keyword'] = True + if 'xattrs' in result: + result['xattrs'] = re_parse_dict(result['xattrs']) + else: + result['xattrs'] = {} return result @@ -239,6 +244,30 @@ def re_match_include(line): return None +def re_parse_dict(raw): + """returns a dict where entries are comma or space separated""" + result = {} + if not raw: + return result + + for key, value in re.findall(RE_DICT_ENTRY, raw): + if value == '': + value = None + result[key] = value + + return result + + +def re_print_dict(d): + parts = [] + for k, v in sorted(d.items()): + if v: + parts.append("{}={}".format(k, v)) + else: + parts.append(k) + return " ".join(parts) + + def strip_parenthesis(data): """strips parenthesis from the given string and returns the strip()ped result. The parenthesis must be the first and last char, otherwise they won't be removed. diff --git a/utils/test/cleanprof_test.out b/utils/test/cleanprof_test.out index bce58192e..aa501b1b1 100644 --- a/utils/test/cleanprof_test.out +++ b/utils/test/cleanprof_test.out @@ -78,7 +78,7 @@ $bar = true allow /home/foo/bar r, } -/what/ever/xattr xattrs=( foo=bar ) flags=( complain ) { +/what/ever/xattr xattrs=(foo=bar) flags=( complain ) { /what/ever r, } diff --git a/utils/test/test-aa-show-usage.py b/utils/test/test-aa-show-usage.py index 66f78fba9..10251e2fd 100644 --- a/utils/test/test-aa-show-usage.py +++ b/utils/test/test-aa-show-usage.py @@ -28,7 +28,8 @@ class AAShowUsageTest(AATest): expected_output_1 = \ '''usage: aa-show-usage [-h] [-s {all,used,unused}] [-j] [-d DIR] - [--filter.flags FLAGS] [--filter.profile_name PROFILE_NAME] + [--show-matching-path] [--filter.flags FLAGS] + [--filter.profile_name PROFILE_NAME] [--filter.profile_attach PROFILE_ATTACH] [--filter.profile_path PROFILE_PATH] @@ -42,6 +43,7 @@ Check which profiles are used Type of profiles to show -j, --json Output in JSON -d, --dir DIR Path to profiles + --show-matching-path Show the path of a file matching the profile Filtering options: Filters are used to reduce the output of information to only those entries diff --git a/utils/test/test-aa.py b/utils/test/test-aa.py index 6db9414b8..c5cde894c 100644 --- a/utils/test/test-aa.py +++ b/utils/test/test-aa.py @@ -552,7 +552,7 @@ class AaTest_parse_profile_data(AATest): self.assertEqual(prof['/foo']['name'], '/foo') self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['flags'], None) - self.assertEqual(prof['/foo']['xattrs'], 'user.bar=bar') + self.assertEqual(prof['/foo']['xattrs'], {'user.bar': 'bar'}) def test_parse_xattrs_02(self): prof = parse_profile_data('/foo xattrs=(user.bar=bar user.foo=*) {\n}\n'.split(), 'somefile', False, False) @@ -561,7 +561,7 @@ class AaTest_parse_profile_data(AATest): self.assertEqual(prof['/foo']['name'], '/foo') self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['flags'], None) - self.assertEqual(prof['/foo']['xattrs'], 'user.bar=bar user.foo=*') + self.assertEqual(prof['/foo']['xattrs'], {'user.bar': 'bar', 'user.foo': '*'}) def test_parse_xattrs_03(self): d = '/foo xattrs=(user.bar=bar) flags=(complain) {\n}\n' @@ -571,7 +571,7 @@ class AaTest_parse_profile_data(AATest): self.assertEqual(prof['/foo']['name'], '/foo') self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['flags'], 'complain') - self.assertEqual(prof['/foo']['xattrs'], 'user.bar=bar') + self.assertEqual(prof['/foo']['xattrs'], {'user.bar': 'bar'}) def test_parse_xattrs_04(self): with self.assertRaises(AppArmorException): diff --git a/utils/test/test-profile-storage.py b/utils/test/test-profile-storage.py index 21d4e0e34..e4a4ea349 100644 --- a/utils/test/test-profile-storage.py +++ b/utils/test/test-profile-storage.py @@ -41,30 +41,30 @@ class TestUnknownKey(AATest): class AaTest_get_header(AATest): tests = ( # name embedded_hat depth flags attachment xattrs prof.keyw. comment expected - (('/foo', False, 1, 'complain', '', '', False, ''), ' /foo flags=(complain) {'), - (('/foo', True, 1, 'complain', '', '', False, ''), ' profile /foo flags=(complain) {'), - (('/foo sp', False, 2, 'complain', '', '', False, ''), ' "/foo sp" flags=(complain) {'), - (('/foo', True, 2, 'complain', '', '', False, ''), ' profile /foo flags=(complain) {'), - (('/foo', False, 0, None, '', '', False, ''), '/foo {'), - (('/foo', False, 0, None, '', 'user.foo=bar', False, ''), '/foo xattrs=(user.foo=bar) {'), - (('/foo', True, 0, None, '', '', False, ''), 'profile /foo {'), - (('bar', False, 1, 'complain', '', '', False, ''), ' profile bar flags=(complain) {'), - (('bar', False, 1, 'complain', '/foo', '', False, ''), ' profile bar /foo flags=(complain) {'), - (('bar', True, 1, 'complain', '/foo', '', False, ''), ' profile bar /foo flags=(complain) {'), - (('bar baz', False, 1, None, '/foo', '', False, ''), ' profile "bar baz" /foo {'), - (('bar', True, 1, None, '/foo', '', False, ''), ' profile bar /foo {'), - (('bar baz', False, 1, 'complain', '/foo sp', '', False, ''), ' profile "bar baz" "/foo sp" flags=(complain) {'), - (('bar baz', False, 1, 'complain', '/foo sp', 'user.foo=bar', False, ''), ' profile "bar baz" "/foo sp" xattrs=(user.foo=bar) flags=(complain) {'), - (('^foo', False, 1, 'complain', '', '', False, ''), ' profile ^foo flags=(complain) {'), - (('^foo', True, 1, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), - (('^foo', True, 1.5, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), - (('^foo', True, 1.3, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), - (('/foo', False, 1, 'complain', '', '', True, ''), ' profile /foo flags=(complain) {'), - (('/foo', True, 1, 'complain', '', '', True, ''), ' profile /foo flags=(complain) {'), - (('/foo', False, 1, 'complain', '', '', False, '# x'), ' /foo flags=(complain) { # x'), - (('/foo', True, 1, None, '', '', False, '# x'), ' profile /foo { # x'), - (('/foo', False, 1, None, '', '', True, '# x'), ' profile /foo { # x'), - (('/foo', True, 1, 'complain', '', '', True, '# x'), ' profile /foo flags=(complain) { # x'), + (('/foo', False, 1, 'complain', '', {}, False, ''), ' /foo flags=(complain) {'), + (('/foo', True, 1, 'complain', '', {}, False, ''), ' profile /foo flags=(complain) {'), + (('/foo sp', False, 2, 'complain', '', {}, False, ''), ' "/foo sp" flags=(complain) {'), + (('/foo', True, 2, 'complain', '', {}, False, ''), ' profile /foo flags=(complain) {'), + (('/foo', False, 0, None, '', {}, False, ''), '/foo {'), + (('/foo', False, 0, None, '', {'user.foo': 'bar'}, False, ''), '/foo xattrs=(user.foo=bar) {'), + (('/foo', True, 0, None, '', {}, False, ''), 'profile /foo {'), + (('bar', False, 1, 'complain', '', {}, False, ''), ' profile bar flags=(complain) {'), + (('bar', False, 1, 'complain', '/foo', {}, False, ''), ' profile bar /foo flags=(complain) {'), + (('bar', True, 1, 'complain', '/foo', {}, False, ''), ' profile bar /foo flags=(complain) {'), + (('bar baz', False, 1, None, '/foo', {}, False, ''), ' profile "bar baz" /foo {'), + (('bar', True, 1, None, '/foo', {}, False, ''), ' profile bar /foo {'), + (('bar baz', False, 1, 'complain', '/foo sp', {}, False, ''), ' profile "bar baz" "/foo sp" flags=(complain) {'), + (('bar baz', False, 1, 'complain', '/foo sp', {'user.foo': 'bar'}, False, ''), ' profile "bar baz" "/foo sp" xattrs=(user.foo=bar) flags=(complain) {'), + (('^foo', False, 1, 'complain', '', {}, False, ''), ' profile ^foo flags=(complain) {'), + (('^foo', True, 1, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'), + (('^foo', True, 1.5, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'), + (('^foo', True, 1.3, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'), + (('/foo', False, 1, 'complain', '', {}, True, ''), ' profile /foo flags=(complain) {'), + (('/foo', True, 1, 'complain', '', {}, True, ''), ' profile /foo flags=(complain) {'), + (('/foo', False, 1, 'complain', '', {}, False, '# x'), ' /foo flags=(complain) { # x'), + (('/foo', True, 1, None, '', {}, False, '# x'), ' profile /foo { # x'), + (('/foo', False, 1, None, '', {}, True, '# x'), ' profile /foo { # x'), + (('/foo', True, 1, 'complain', '', {}, True, '# x'), ' profile /foo flags=(complain) { # x'), ) def _run_test(self, params, expected): @@ -88,8 +88,10 @@ class AaTest_get_header_01(AATest): ({'name': '/foo', 'depth': 1, 'flags': 'complain'}, ' /foo flags=(complain) {'), ({'name': '/foo', 'depth': 1, 'flags': 'complain', 'profile_keyword': True}, ' profile /foo flags=(complain) {'), ({'name': '/foo', 'flags': 'complain'}, '/foo flags=(complain) {'), - ({'name': '/foo', 'xattrs': 'user.foo=bar', 'flags': 'complain'}, '/foo xattrs=(user.foo=bar) flags=(complain) {'), - ({'name': '/foo', 'xattrs': 'user.foo=bar', 'embedded_hat': True}, 'profile /foo xattrs=(user.foo=bar) {'), + ({'name': '/foo', 'xattrs': {'user.foo': 'bar'}, 'flags': 'complain'}, '/foo xattrs=(user.foo=bar) flags=(complain) {'), + ({'name': '/foo', 'xattrs': {'user.foo': 'bar'}, 'embedded_hat': True}, 'profile /foo xattrs=(user.foo=bar) {'), + ({'name': '/foo', 'xattrs': {'user.foo': None}, 'embedded_hat': True}, 'profile /foo xattrs=(user.foo) {'), + ({'name': '/foo', 'xattrs': {'user.foo': None, 'user.bar': None}, 'embedded_hat': True}, 'profile /foo xattrs=(user.bar user.foo) {'), ) def _run_test(self, params, expected): @@ -178,6 +180,7 @@ class TestSetInvalid(AATest): (('attachment', None), AppArmorBug), (('filename', True), AppArmorBug), # expects string or None (('allow', None), AppArmorBug), # doesn't allow overwriting at all + (('xattrs', 0), AppArmorBug), # Invalid type ) def _run_test(self, params, expected): @@ -196,7 +199,7 @@ class AaTest_repr(AATest): def testRepr(self): prof_storage = ProfileStorage('foo', 'hat', 'TEST') prof_storage['name'] = 'foo' - prof_storage['xattrs'] = 'user.bar=bar' + prof_storage['xattrs'] = {'user.bar': 'bar'} prof_storage['capability'].add(CapabilityRule('dac_override')) self.assertEqual(str(prof_storage), '\n\nprofile foo xattrs=(user.bar=bar) {\n capability dac_override,\n\n}\n\n') @@ -205,15 +208,21 @@ class AaTest_repr(AATest): class AaTest_parse_profile_start(AATest): tests = ( # profile start line profile hat parent name profile hat attachment xattrs flags pps_set_hat_external - (('/foo {', None, None), ('', '/foo', '/foo', '/foo', '', '', None, False)), - (('/foo (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', '', 'complain', False)), - (('profile foo /foo {', None, None), ('', 'foo', 'foo', 'foo', '/foo', '', None, False)), # named profile - (('profile /foo {', '/bar', None), ('/bar', '/foo', '/bar', '/foo', '', '', None, False)), # child profile - (('/foo//bar {', None, None), ('/foo', '/foo//bar', '/foo', 'bar', '', '', None, True)), # external hat - (('profile "/foo" (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', '', 'complain', False)), - (('profile "/foo" xattrs=(user.bar=bar) {', None, None), ('', '/foo', '/foo', '/foo', '', 'user.bar=bar', None, False)), - (('profile "/foo" xattrs=(user.bar=bar user.foo=*) {', None, None), ('', '/foo', '/foo', '/foo', '', 'user.bar=bar user.foo=*', None, False)), - (('/usr/bin/xattrs-test xattrs=(myvalue="foo.bar") {', None, None), ('', '/usr/bin/xattrs-test', '/usr/bin/xattrs-test', '/usr/bin/xattrs-test', '', 'myvalue="foo.bar"', None, False)), + (('/foo {', None, None), ('', '/foo', '/foo', '/foo', '', {}, None, False)), + (('/foo (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', {}, 'complain', False)), + (('profile foo /foo {', None, None), ('', 'foo', 'foo', 'foo', '/foo', {}, None, False)), # named profile + (('profile /foo {', '/bar', None), ('/bar', '/foo', '/bar', '/foo', '', {}, None, False)), # child profile + (('profile /foo xattrs=() {', '/bar', None), ('/bar', '/foo', '/bar', '/foo', '', {}, None, False)), + (('/foo//bar {', None, None), ('/foo', '/foo//bar', '/foo', 'bar', '', {}, None, True)), # external hat + (('profile "/foo" (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', {}, 'complain', False)), + (('profile "/foo" xattrs=() {', None, None), ('', '/foo', '/foo', '/foo', '', {}, None, False)), + (('profile "/foo" xattrs=(user.bar) {', None, None), ('', '/foo', '/foo', '/foo', '', {'user.bar': None}, None, False)), + (('profile "/foo" xattrs=(user.foo user.bar) {', None, None), ('', '/foo', '/foo', '/foo', '', {'user.bar': None, 'user.foo': None}, + None, False)), # noqa: E127 + (('profile "/foo" xattrs=(user.bar=bar) {', None, None), ('', '/foo', '/foo', '/foo', '', {'user.bar': 'bar'}, None, False)), + (('profile "/foo" xattrs=(user.bar=bar user.foo=*) {', None, None), ('', '/foo', '/foo', '/foo', '', {'user.bar': 'bar', 'user.foo': '*'}, + None, False)), # noqa: E127 + (('/usr/bin/xattrs-test xattrs=(myvalue="foo.bar") {', None, None), ('', '/usr/bin/xattrs-test', '/usr/bin/xattrs-test', '/usr/bin/xattrs-test', '', {'myvalue': '"foo.bar"'}, None, False)), ) def _run_test(self, params, expected): diff --git a/utils/test/test-regex_matches.py b/utils/test/test-regex_matches.py index 2e3827909..d63046b60 100644 --- a/utils/test/test-regex_matches.py +++ b/utils/test/test-regex_matches.py @@ -18,7 +18,7 @@ from apparmor.regex import ( RE_PROFILE_START, parse_profile_start_line, re_match_include, RE_PROFILE_UNIX, RE_PROFILE_PIVOT_ROOT, re_match_include_parse, strip_parenthesis, strip_quotes, resolve_variables, expand_braces, - expand_var, expand_string) + expand_var, expand_string, re_print_dict, re_parse_dict) from common_test import AATest, setup_aa, setup_all_loops @@ -857,6 +857,27 @@ class TestInvalidExpandString(AATest): expand_string(var, var_dict, seen_vars) +class TestRePrintDict(AATest): + tests = ( + ({'a': 'b'}, 'a=b'), + ({'a': 'b', 'bb': 'cc'}, 'a=b bb=cc'), + ({'z': 'c', 'y': 'b', 'x': 'a'}, 'x=a y=b z=c'), + ) + + def _run_test(self, params, expected): + self.assertEqual(re_print_dict(params), expected) + + +class TestReParseDict(AATest): + tests = ( + ('a=b', {'a': 'b'}), + (' a=bbb bb=cc', {'a': 'bbb', 'bb': 'cc'}), + ) + + def _run_test(self, params, expected): + self.assertEqual(re_parse_dict(params), expected) + + setup_aa(aa) setup_all_loops(__name__) if __name__ == '__main__':