2
0
mirror of https://gitlab.com/apparmor/apparmor synced 2025-08-28 21:07:56 +00:00

Add support for --show-matching-path and xattrs

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 <maxime.belair@canonical.com>
This commit is contained in:
Maxime Bélair 2025-05-13 16:38:36 +02:00 committed by Christian Boltz
parent db376c0458
commit b46f7a426c
9 changed files with 151 additions and 86 deletions

View File

@ -30,26 +30,35 @@ _ = init_translation()
MAX_RECURSION = 10 MAX_RECURSION = 10
def has_matching_file(pattern): def has_matching_file(pattern, xattrs=None):
for pat in expand_braces(pattern): for p in expand_braces(pattern):
if any(glob.iglob(pat, recursive=True)): for path in glob.iglob(p, recursive=True):
return True if os.path.realpath(path) != os.path.abspath(path): # remove symlinks
return False 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: if used:
print(_('Used profiles:')) print(_('Used profiles:'))
for (name, attach, path) in used: for (name, attach, path, match) in used:
print(_(' Profile {} for {} ({})').format(name, attach, path)) print(_(' Profile {} for {} ({}) {}').format(name, attach, path, ('→ ' + match) if show_matching_path else ''))
if unused: if unused:
print(_('Unused profiles:')) print(_('Unused profiles:'))
for (name, attach, path) in unused: for (name, attach, path, match) in unused:
print(_(' Profile {} for {} ({})').format(name, attach, path)) print(_(' Profile {} for {} ({}) ').format(name, attach, path))
def profiles_to_json(profiles): 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): 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)) 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) resolved = resolve_variables(a, var_dict)
found = False matching_path = None
for entry in resolved: for entry in resolved:
if has_matching_file(entry): matching_path = has_matching_file(entry)
found = True if matching_path:
break
if found and args.show_type != 'unused': if matching_path and args.show_type != 'unused':
used.append((profile_name, a, filename)) used.append((profile_name, a, filename, matching_path))
if not found and args.show_type != 'used': if not matching_path and args.show_type != 'used':
unused.append((profile_name, a, filename)) unused.append((profile_name, a, filename, matching_path))
return used, unused 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('-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('-j', '--json', action='store_true', help=_('Output in JSON'))
parser.add_argument('-d', '--dir', type=str, help=_('Path to profiles')) 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) parser.add_argument('--configdir', type=str, help=argparse.SUPPRESS)
filter_group = parser.add_argument_group(_('Filtering options'), filter_group = parser.add_argument_group(_('Filtering options'),
@ -137,7 +148,7 @@ def main():
if args.json: if args.json:
display_profile_json(used, unused) display_profile_json(used, unused)
else: else:
display_profile_text(used, unused) display_profile_text(used, unused, args.show_matching_path)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -50,6 +50,10 @@ Output in JSON
Path to profiles 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 =back
=head1 FILTERING OPTIONS =head1 FILTERING OPTIONS

View File

@ -13,9 +13,10 @@
# #
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
from types import NoneType
from apparmor.common import AppArmorBug, AppArmorException 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 import quote_if_needed
from apparmor.rule.abi import AbiRule, AbiRuleset from apparmor.rule.abi import AbiRule, AbiRuleset
from apparmor.rule.all import AllRule, AllRuleset 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['parent'] = '' # parent profile, or '' for top-level profiles and external hats
data['name'] = '' data['name'] = ''
data['attachment'] = '' data['attachment'] = ''
data['xattrs'] = '' data['xattrs'] = {}
data['flags'] = '' data['flags'] = ''
data['external'] = False data['external'] = False
data['header_comment'] = '' # comment in the profile/hat start line data['header_comment'] = '' # comment in the profile/hat start line
@ -100,28 +101,16 @@ class ProfileStorage:
if key not in self.data: if key not in self.data:
raise AppArmorBug('attempt to set unknown key %s' % key) raise AppArmorBug('attempt to set unknown key %s' % key)
# allow writing bool values allowed_types = {bool, str, dict, None, NoneType}
if isinstance(self.data[key], bool): old_type = type(self.data[key])
if isinstance(value, bool): 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 self.data[key] = value
else: else:
raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, type(self.data[key]), type(value), value)) raise AppArmorBug('Attempt to change type of "%s" from %s to %s, value %s' % (key, old_type, type(value), value))
self.data[key] = 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
else: else:
raise AppArmorBug('Attempt to overwrite "%s" with %s, type %s' % (key, value, type(value))) raise AppArmorBug('Attempt to overwrite "%s" with %s, type %s' % (key, value, type(value)))
@ -168,7 +157,7 @@ class ProfileStorage:
xattrs = '' xattrs = ''
if self.data['xattrs']: if self.data['xattrs']:
xattrs = ' xattrs=(%s)' % self.data['xattrs'] xattrs = ' xattrs=(%s)' % re_print_dict(self.data['xattrs'])
flags = '' flags = ''
if self.data['flags']: if self.data['flags']:
@ -263,7 +252,7 @@ class ProfileStorage:
else: else:
prof_storage['profile_keyword'] = matches['profile_keyword'] prof_storage['profile_keyword'] = matches['profile_keyword']
prof_storage['attachment'] = matches['attachment'] or '' prof_storage['attachment'] = matches['attachment'] or ''
prof_storage['xattrs'] = matches['xattrs'] or '' prof_storage['xattrs'] = matches['xattrs'] or {}
return (profile, hat, prof_storage) return (profile, hat, prof_storage)

View File

@ -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_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_PATH = r'/\S*|"/[^"]*"' # filename (starting with '/') without spaces, or quoted filename.
RE_VAR = r'@{[^}\s]+}' RE_VAR = r'@{[^}\s]+}'
RE_DICT_ENTRY = r'\s*(?P<key>[^,\s=]+)(?:=(?P<value>[^,\s=]+))?\s*'
RE_PROFILE_PATH = '(?P<%s>(' + RE_PATH + '))' # quoted or unquoted filename. %s is the match group name 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_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<execmode>(safe|unsafe))' RE_SAFE_OR_UNSAFE = '(?P<execmode>(safe|unsafe))'
RE_XATTRS = r'(\s+xattrs\s*=\s*\((?P<xattrs>([^)=]+(=[^)=]+)?\s?)+)\)\s*)?' RE_XATTRS = r'(\s+xattrs\s*=\s*\((?P<xattrs>([^)=]+(=[^)=]+)?\s?)*)\)\s*)?'
RE_FLAGS = r'(\s+(flags\s*=\s*)?\((?P<flags>[^)]+)\))?' RE_FLAGS = r'(\s+(flags\s*=\s*)?\((?P<flags>[^)]+)\))?'
RE_VARIABLE = re.compile(RE_VAR) RE_VARIABLE = re.compile(RE_VAR)
@ -166,6 +167,10 @@ def parse_profile_start_line(line, filename):
else: else:
result['profile'] = result['namedprofile'] result['profile'] = result['namedprofile']
result['profile_keyword'] = True result['profile_keyword'] = True
if 'xattrs' in result:
result['xattrs'] = re_parse_dict(result['xattrs'])
else:
result['xattrs'] = {}
return result return result
@ -239,6 +244,30 @@ def re_match_include(line):
return None 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): def strip_parenthesis(data):
"""strips parenthesis from the given string and returns the strip()ped result. """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. The parenthesis must be the first and last char, otherwise they won't be removed.

View File

@ -78,7 +78,7 @@ $bar = true
allow /home/foo/bar r, allow /home/foo/bar r,
} }
/what/ever/xattr xattrs=( foo=bar ) flags=( complain ) { /what/ever/xattr xattrs=(foo=bar) flags=( complain ) {
/what/ever r, /what/ever r,
} }

View File

@ -28,7 +28,8 @@ class AAShowUsageTest(AATest):
expected_output_1 = \ expected_output_1 = \
'''usage: aa-show-usage [-h] [-s {all,used,unused}] [-j] [-d DIR] '''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_attach PROFILE_ATTACH]
[--filter.profile_path PROFILE_PATH] [--filter.profile_path PROFILE_PATH]
@ -42,6 +43,7 @@ Check which profiles are used
Type of profiles to show Type of profiles to show
-j, --json Output in JSON -j, --json Output in JSON
-d, --dir DIR Path to profiles -d, --dir DIR Path to profiles
--show-matching-path Show the path of a file matching the profile
Filtering options: Filtering options:
Filters are used to reduce the output of information to only those entries Filters are used to reduce the output of information to only those entries

View File

@ -552,7 +552,7 @@ class AaTest_parse_profile_data(AATest):
self.assertEqual(prof['/foo']['name'], '/foo') self.assertEqual(prof['/foo']['name'], '/foo')
self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['filename'], 'somefile')
self.assertEqual(prof['/foo']['flags'], None) 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): def test_parse_xattrs_02(self):
prof = parse_profile_data('/foo xattrs=(user.bar=bar user.foo=*) {\n}\n'.split(), 'somefile', False, False) 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']['name'], '/foo')
self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['filename'], 'somefile')
self.assertEqual(prof['/foo']['flags'], None) 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): def test_parse_xattrs_03(self):
d = '/foo xattrs=(user.bar=bar) flags=(complain) {\n}\n' 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']['name'], '/foo')
self.assertEqual(prof['/foo']['filename'], 'somefile') self.assertEqual(prof['/foo']['filename'], 'somefile')
self.assertEqual(prof['/foo']['flags'], 'complain') 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): def test_parse_xattrs_04(self):
with self.assertRaises(AppArmorException): with self.assertRaises(AppArmorException):

View File

@ -41,30 +41,30 @@ class TestUnknownKey(AATest):
class AaTest_get_header(AATest): class AaTest_get_header(AATest):
tests = ( tests = (
# name embedded_hat depth flags attachment xattrs prof.keyw. comment expected # name embedded_hat depth flags attachment xattrs prof.keyw. comment expected
(('/foo', False, 1, 'complain', '', '', False, ''), ' /foo flags=(complain) {'), (('/foo', False, 1, 'complain', '', {}, False, ''), ' /foo flags=(complain) {'),
(('/foo', True, 1, 'complain', '', '', False, ''), ' profile /foo flags=(complain) {'), (('/foo', True, 1, 'complain', '', {}, False, ''), ' profile /foo flags=(complain) {'),
(('/foo sp', False, 2, 'complain', '', '', False, ''), ' "/foo sp" flags=(complain) {'), (('/foo sp', False, 2, 'complain', '', {}, False, ''), ' "/foo sp" flags=(complain) {'),
(('/foo', True, 2, 'complain', '', '', False, ''), ' profile /foo flags=(complain) {'), (('/foo', True, 2, 'complain', '', {}, False, ''), ' profile /foo flags=(complain) {'),
(('/foo', False, 0, None, '', '', False, ''), '/foo {'), (('/foo', False, 0, None, '', {}, False, ''), '/foo {'),
(('/foo', False, 0, None, '', 'user.foo=bar', False, ''), '/foo xattrs=(user.foo=bar) {'), (('/foo', False, 0, None, '', {'user.foo': 'bar'}, False, ''), '/foo xattrs=(user.foo=bar) {'),
(('/foo', True, 0, None, '', '', False, ''), 'profile /foo {'), (('/foo', True, 0, None, '', {}, False, ''), 'profile /foo {'),
(('bar', False, 1, 'complain', '', '', False, ''), ' profile bar flags=(complain) {'), (('bar', False, 1, 'complain', '', {}, False, ''), ' profile bar flags=(complain) {'),
(('bar', False, 1, 'complain', '/foo', '', False, ''), ' profile bar /foo 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', True, 1, 'complain', '/foo', {}, False, ''), ' profile bar /foo flags=(complain) {'),
(('bar baz', False, 1, None, '/foo', '', False, ''), ' profile "bar baz" /foo {'), (('bar baz', False, 1, None, '/foo', {}, False, ''), ' profile "bar baz" /foo {'),
(('bar', True, 1, None, '/foo', '', False, ''), ' profile bar /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', {}, 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) {'), (('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', False, 1, 'complain', '', {}, False, ''), ' profile ^foo flags=(complain) {'),
(('^foo', True, 1, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), (('^foo', True, 1, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'),
(('^foo', True, 1.5, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), (('^foo', True, 1.5, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'),
(('^foo', True, 1.3, 'complain', '', '', False, ''), ' ^foo flags=(complain) {'), (('^foo', True, 1.3, 'complain', '', {}, False, ''), ' ^foo flags=(complain) {'),
(('/foo', False, 1, 'complain', '', '', True, ''), ' profile /foo flags=(complain) {'), (('/foo', False, 1, 'complain', '', {}, True, ''), ' profile /foo flags=(complain) {'),
(('/foo', True, 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', False, 1, 'complain', '', {}, False, '# x'), ' /foo flags=(complain) { # x'),
(('/foo', True, 1, None, '', '', False, '# x'), ' profile /foo { # x'), (('/foo', True, 1, None, '', {}, False, '# x'), ' profile /foo { # x'),
(('/foo', False, 1, None, '', '', True, '# 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', True, 1, 'complain', '', {}, True, '# x'), ' profile /foo flags=(complain) { # x'),
) )
def _run_test(self, params, expected): 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'}, ' /foo flags=(complain) {'),
({'name': '/foo', 'depth': 1, 'flags': 'complain', 'profile_keyword': True}, ' profile /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', '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'}, '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'}, '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): def _run_test(self, params, expected):
@ -178,6 +180,7 @@ class TestSetInvalid(AATest):
(('attachment', None), AppArmorBug), (('attachment', None), AppArmorBug),
(('filename', True), AppArmorBug), # expects string or None (('filename', True), AppArmorBug), # expects string or None
(('allow', None), AppArmorBug), # doesn't allow overwriting at all (('allow', None), AppArmorBug), # doesn't allow overwriting at all
(('xattrs', 0), AppArmorBug), # Invalid type
) )
def _run_test(self, params, expected): def _run_test(self, params, expected):
@ -196,7 +199,7 @@ class AaTest_repr(AATest):
def testRepr(self): def testRepr(self):
prof_storage = ProfileStorage('foo', 'hat', 'TEST') prof_storage = ProfileStorage('foo', 'hat', 'TEST')
prof_storage['name'] = 'foo' prof_storage['name'] = 'foo'
prof_storage['xattrs'] = 'user.bar=bar' prof_storage['xattrs'] = {'user.bar': 'bar'}
prof_storage['capability'].add(CapabilityRule('dac_override')) prof_storage['capability'].add(CapabilityRule('dac_override'))
self.assertEqual(str(prof_storage), '\n<ProfileStorage>\nprofile foo xattrs=(user.bar=bar) {\n capability dac_override,\n\n}\n</ProfileStorage>\n') self.assertEqual(str(prof_storage), '\n<ProfileStorage>\nprofile foo xattrs=(user.bar=bar) {\n capability dac_override,\n\n}\n</ProfileStorage>\n')
@ -205,15 +208,21 @@ class AaTest_repr(AATest):
class AaTest_parse_profile_start(AATest): class AaTest_parse_profile_start(AATest):
tests = ( tests = (
# profile start line profile hat parent name profile hat attachment xattrs flags pps_set_hat_external # 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 {', None, None), ('', '/foo', '/foo', '/foo', '', {}, None, False)),
(('/foo (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', '', 'complain', 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 /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 {', '/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 xattrs=() {', '/bar', None), ('/bar', '/foo', '/bar', '/foo', '', {}, None, False)),
(('profile "/foo" (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', '', 'complain', False)), (('/foo//bar {', None, None), ('/foo', '/foo//bar', '/foo', 'bar', '', {}, None, True)), # external hat
(('profile "/foo" xattrs=(user.bar=bar) {', None, None), ('', '/foo', '/foo', '/foo', '', 'user.bar=bar', None, False)), (('profile "/foo" (complain) {', None, None), ('', '/foo', '/foo', '/foo', '', {}, 'complain', False)),
(('profile "/foo" xattrs=(user.bar=bar user.foo=*) {', None, None), ('', '/foo', '/foo', '/foo', '', 'user.bar=bar user.foo=*', None, False)), (('profile "/foo" xattrs=() {', None, None), ('', '/foo', '/foo', '/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)), (('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): def _run_test(self, params, expected):

View File

@ -18,7 +18,7 @@ from apparmor.regex import (
RE_PROFILE_START, parse_profile_start_line, re_match_include, RE_PROFILE_UNIX, RE_PROFILE_START, parse_profile_start_line, re_match_include, RE_PROFILE_UNIX,
RE_PROFILE_PIVOT_ROOT, RE_PROFILE_PIVOT_ROOT,
re_match_include_parse, strip_parenthesis, strip_quotes, resolve_variables, expand_braces, 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 from common_test import AATest, setup_aa, setup_all_loops
@ -857,6 +857,27 @@ class TestInvalidExpandString(AATest):
expand_string(var, var_dict, seen_vars) 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_aa(aa)
setup_all_loops(__name__) setup_all_loops(__name__)
if __name__ == '__main__': if __name__ == '__main__':