From b65fbda092d80f9c59635cd0722c61164c1d6c32 Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Thu, 9 May 2024 21:54:36 +0200 Subject: [PATCH 1/4] Add 'details' labeled section to RE_PROFILE_PIVOT_ROOT --- utils/apparmor/regex.py | 2 +- utils/test/test-regex_matches.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/utils/apparmor/regex.py b/utils/apparmor/regex.py index b342c4b49..256b5acb1 100644 --- a/utils/apparmor/regex.py +++ b/utils/apparmor/regex.py @@ -50,7 +50,7 @@ RE_PROFILE_DBUS = re.compile(RE_AUDIT_DENY + r'(dbus\s*,|dbus(?P
\s+[^#] RE_PROFILE_MOUNT = re.compile(RE_AUDIT_DENY + r'((?Pmount|remount|umount|unmount)(?P
\s+[^#]*)?\s*,)' + RE_EOL) RE_PROFILE_SIGNAL = re.compile(RE_AUDIT_DENY + r'(signal\s*,|signal(?P
\s+[^#]*)\s*,)' + RE_EOL) RE_PROFILE_PTRACE = re.compile(RE_AUDIT_DENY + r'(ptrace\s*,|ptrace(?P
\s+[^#]*)\s*,)' + RE_EOL) -RE_PROFILE_PIVOT_ROOT = re.compile(RE_AUDIT_DENY + r'(pivot_root\s*,|pivot_root\s+[^#]*\s*,)' + RE_EOL) +RE_PROFILE_PIVOT_ROOT = re.compile(RE_AUDIT_DENY + r'(pivot_root\s*,|pivot_root(?P
\s+[^#]*),)' + RE_EOL) RE_PROFILE_UNIX = re.compile(RE_AUDIT_DENY + r'(unix\s*,|unix(?P
\s+[^#]*)\s*,)' + RE_EOL) RE_PROFILE_USERNS = re.compile(RE_AUDIT_DENY + r'(userns\s*,|userns(?P
\s+[^#]*)\s*,)' + RE_EOL) RE_PROFILE_MQUEUE = re.compile(RE_AUDIT_DENY + r'(mqueue\s*,|mqueue(?P
\s+[^#]*)\s*,)' + RE_EOL) diff --git a/utils/test/test-regex_matches.py b/utils/test/test-regex_matches.py index da83182a2..cd081dedf 100644 --- a/utils/test/test-regex_matches.py +++ b/utils/test/test-regex_matches.py @@ -316,12 +316,12 @@ class AARegexPivotRoot(AARegexTest): self.regex = aa.RE_PROFILE_PIVOT_ROOT tests = ( - (' pivot_root,', (None, None, 'pivot_root,', None)), - (' audit pivot_root,', ('audit', None, 'pivot_root,', None)), - (' pivot_root oldroot=/new/old,', (None, None, 'pivot_root oldroot=/new/old,', None)), - (' pivot_root oldroot=/new/old /new,', (None, None, 'pivot_root oldroot=/new/old /new,', None)), - (' pivot_root oldroot=/new/old /new -> child,', (None, None, 'pivot_root oldroot=/new/old /new -> child,', None)), - (' audit pivot_root oldroot=/new/old /new -> child,', ('audit', None, 'pivot_root oldroot=/new/old /new -> child,', None)), + (' pivot_root,', (None, None, 'pivot_root,', None, None)), + (' audit pivot_root,', ('audit', None, 'pivot_root,', None, None)), + (' pivot_root oldroot=/new/old,', (None, None, 'pivot_root oldroot=/new/old,', 'oldroot=/new/old', None)), + (' pivot_root oldroot=/new/old /new,', (None, None, 'pivot_root oldroot=/new/old /new,', 'oldroot=/new/old /new', None)), + (' pivot_root oldroot=/new/old /new -> child,', (None, None, 'pivot_root oldroot=/new/old /new -> child,', 'oldroot=/new/old /new -> child', None)), + (' audit pivot_root oldroot=/new/old /new -> child,', ('audit', None, 'pivot_root oldroot=/new/old /new -> child,', 'oldroot=/new/old /new -> child', None)), ('pivot_root', False), # comma missing From c48f7b625a11e60a0501d54fde69cab328f977ca Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Thu, 9 May 2024 22:54:02 +0200 Subject: [PATCH 2/4] Add PivotRootRule class ... and tests for it. --- utils/apparmor/logparser.py | 2 + utils/apparmor/rule/pivot_root.py | 198 +++++++++++ utils/test/test-pivot_root.py | 572 ++++++++++++++++++++++++++++++ 3 files changed, 772 insertions(+) create mode 100644 utils/apparmor/rule/pivot_root.py create mode 100644 utils/test/test-pivot_root.py diff --git a/utils/apparmor/logparser.py b/utils/apparmor/logparser.py index fce261da2..2cbcb640c 100644 --- a/utils/apparmor/logparser.py +++ b/utils/apparmor/logparser.py @@ -118,6 +118,8 @@ class ReadLog: ev['peer'] = event.peer elif ev['operation'] and ev['operation'] == 'ptrace': ev['peer'] = event.peer + elif ev['operation'] and ev['operation'] == 'pivotroot': + ev['src_name'] = event.src_name elif ev['operation'] and ev['operation'] == 'mount': ev['flags'] = event.flags ev['fs_type'] = event.fs_type diff --git a/utils/apparmor/rule/pivot_root.py b/utils/apparmor/rule/pivot_root.py new file mode 100644 index 000000000..f090c0484 --- /dev/null +++ b/utils/apparmor/rule/pivot_root.py @@ -0,0 +1,198 @@ +# ---------------------------------------------------------------------- +# Copyright (C) 2024 Christian Boltz +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# ---------------------------------------------------------------------- + +import re + +from apparmor.common import AppArmorBug, AppArmorException + +from apparmor.aare import AARE +from apparmor.regex import RE_PROFILE_PIVOT_ROOT, RE_PROFILE_NAME, RE_PROFILE_PATH_OR_VAR, strip_quotes +from apparmor.rule import BaseRule, BaseRuleset, parse_modifiers, logprof_value_or_all, quote_if_needed + +from apparmor.translations import init_translation + +_ = init_translation() + +RE_PIVOT_ROOT_DETAILS = re.compile( + r'^\s*' + + r'(\s+oldroot=' + RE_PROFILE_PATH_OR_VAR % 'oldroot' + r')?' # noqa: E221 + + r'(\s+' + RE_PROFILE_PATH_OR_VAR % 'newroot' + r')?' # noqa: E221 + + r'(\s+->\s+' + RE_PROFILE_NAME % 'profile_name' + r')?' # noqa: E221 + + r'\s*$' +) + + +class PivotRootRule(BaseRule): + '''Class to handle and store a single pivot_root rule''' + + # Nothing external should reference this class, all external users + # should reference the class field PivotRootRule.ALL + class __PivotRootAll(object): + pass + + ALL = __PivotRootAll + + rule_name = 'pivot_root' + _match_re = RE_PROFILE_PIVOT_ROOT + +# PIVOT ROOT RULE = [ QUALIFIERS ] pivot_root [ oldroot=OLD PUT FILEGLOB ] [ NEW ROOT FILEGLOB ] [ ’->’ PROFILE NAME ] + def __init__(self, oldroot, newroot, profile_name, audit=False, deny=False, allow_keyword=False, comment='', log_event=None): + + super().__init__(audit=audit, deny=deny, + allow_keyword=allow_keyword, + comment=comment, + log_event=log_event) + + self.oldroot, self.all_oldroots = self._aare_or_all(oldroot, 'oldroot', True, log_event) # noqa: E221 + self.newroot, self.all_newroots = self._aare_or_all(newroot, 'newroot', True, log_event) # noqa: E221 + self.profile_name, self.all_profile_names = self._aare_or_all(profile_name, 'profile_name', False, log_event) # noqa: E221 + + self.can_glob = not self.all_newroots + self.can_glob_ext = False + self.can_edit = not self.all_newroots + + @classmethod + def _create_instance(cls, raw_rule, matches): + '''parse raw_rule and return instance of this class''' + + audit, deny, allow_keyword, comment = parse_modifiers(matches) + + rule_details = '' + if matches.group('details'): + rule_details = matches.group('details') + + parsed = RE_PIVOT_ROOT_DETAILS.search(rule_details) + + if not parsed: + raise AppArmorException('Cannot parse pivot_root rule ' + raw_rule) + + r = parsed.groupdict() + + if r['oldroot']: + oldroot = strip_quotes(r['oldroot']) + else: + oldroot = cls.ALL + + if r['newroot']: + newroot = strip_quotes(r['newroot']) + else: + newroot = cls.ALL + + if r['profile_name']: + profile_name = strip_quotes(r['profile_name']) + else: + profile_name = cls.ALL + + else: + oldroot = cls.ALL + newroot = cls.ALL + profile_name = cls.ALL + + return cls(oldroot=oldroot, newroot=newroot, profile_name=profile_name, + audit=audit, deny=deny, allow_keyword=allow_keyword, comment=comment) + + def get_clean(self, depth=0): + space = ' ' * depth + + if self.all_oldroots: + oldroot = '' + elif self.oldroot: + oldroot = ' oldroot=' + quote_if_needed(self.oldroot.regex) + else: + raise AppArmorBug('Empty oldroot in pivot_root rule') + + if self.all_newroots: + newroot = '' + elif self.newroot: + newroot = ' ' + quote_if_needed(self.newroot.regex) + else: + raise AppArmorBug('Empty newroot in pivot_root rule') + + if self.all_profile_names: + profile_name = '' + elif self.profile_name: + profile_name = ' -> ' + quote_if_needed(self.profile_name.regex) + else: + raise AppArmorBug('Empty profile_name in pivot_root rule') + + return f'{space}{self.modifiers_str()}pivot_root{oldroot}{newroot}{profile_name},{self.comment}' + + def _is_covered_localvars(self, other_rule): + if not self._is_covered_aare(self.oldroot, self.all_oldroots, other_rule.oldroot, other_rule.all_oldroots, 'oldroot'): + return False + + if not self._is_covered_aare(self.newroot, self.all_newroots, other_rule.newroot, other_rule.all_newroots, 'newroot'): + return False + + if not self._is_covered_aare(self.profile_name, self.all_profile_names, other_rule.profile_name, other_rule.all_profile_names, 'profile_name'): + return False + + # still here? -> then it is covered + return True + + def _is_equal_localvars(self, rule_obj, strict): + if not self._is_equal_aare(self.oldroot, self.all_oldroots, rule_obj.oldroot, rule_obj.all_oldroots, 'oldroot'): + return False + + if not self._is_equal_aare(self.newroot, self.all_newroots, rule_obj.newroot, rule_obj.all_newroots, 'newroot'): + return False + + if not self._is_equal_aare(self.profile_name, self.all_profile_names, rule_obj.profile_name, rule_obj.all_profile_names, 'profile_name'): + return False + + return True + + def glob(self): + '''Change newroot path to next possible glob''' + if self.all_newroots: + return + + self.newroot = self.newroot.glob_path() + self.raw_rule = None + + def edit_header(self): + if self.all_newroots: + raise AppArmorBug('Attemp to edit pivot_root rule without newroot limitations') + + return (_('Enter new newroot: '), self.newroot.regex) + + def validate_edit(self, newpath): + if self.all_newroots: + raise AppArmorBug('Attemp to edit pivot_root rule without newroot limitations') + + newpath = AARE(newpath, True) # might raise AppArmorException if the new path doesn't start with / or a variable + return newpath.match(self.newroot) + + def store_edit(self, newpath): + if self.all_newroots: + raise AppArmorBug('Attemp to edit pivot_root rule without newroot limitations') + + self.newroot = AARE(newpath, True) # might raise AppArmorException if the new path doesn't start with / or a variable + self.raw_rule = None + + def _logprof_header_localvars(self): + + oldroot = logprof_value_or_all(self.oldroot, self.all_oldroots) + newroot = logprof_value_or_all(self.newroot, self.all_newroots) + profile_name = logprof_value_or_all(self.profile_name, self.all_profile_names) + + return ( + _('Old root'), oldroot, + _('New root'), newroot, + _('Target profile'), profile_name, + ) + + +class PivotRootRuleset(BaseRuleset): + '''Class to handle and store a collection of pivot_root rules''' diff --git a/utils/test/test-pivot_root.py b/utils/test/test-pivot_root.py new file mode 100644 index 000000000..ceaf59ab7 --- /dev/null +++ b/utils/test/test-pivot_root.py @@ -0,0 +1,572 @@ +#!/usr/bin/python3 +# ---------------------------------------------------------------------- +# Copyright (C) 2015 Christian Boltz +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# ---------------------------------------------------------------------- + +import unittest +from collections import namedtuple + +from apparmor.common import AppArmorBug, AppArmorException +from apparmor.logparser import ReadLog +from apparmor.aare import AARE +from apparmor.rule.pivot_root import PivotRootRule, PivotRootRuleset +from apparmor.translations import init_translation +from common_test import AATest, setup_all_loops + +_ = init_translation() + +exp = namedtuple( + 'exp', ('audit', 'allow_keyword', 'deny', 'comment', 'oldroot', 'all_oldroots', 'newroot', + 'all_newroots', 'profile_name', 'all_profile_names'), +) + + +# # --- tests for single PivotRootRule --- # + +class PivotRootTest(AATest): + def _compare_obj(self, obj, expected): + self.assertEqual(expected.audit, obj.audit) + self.assertEqual(expected.allow_keyword, obj.allow_keyword) + self.assertEqual(expected.deny, obj.deny) + self.assertEqual(expected.comment, obj.comment) + + if type(obj.oldroot) is AARE: + self.assertEqual(expected.oldroot, obj.oldroot.regex) + else: + self.assertEqual(expected.oldroot, obj.oldroot) + + self.assertEqual(expected.all_oldroots, obj.all_oldroots) + + if type(obj.newroot) is AARE: + self.assertEqual(expected.newroot, obj.newroot.regex) + else: + self.assertEqual(expected.newroot, obj.newroot) + + self.assertEqual(expected.all_newroots, obj.all_newroots) + + if type(obj.profile_name) is AARE: + self.assertEqual(expected.profile_name, obj.profile_name.regex) + else: + self.assertEqual(expected.profile_name, obj.profile_name) + + self.assertEqual(expected.all_profile_names, obj.all_profile_names) + + +class PivotRootTestParse(PivotRootTest): + tests = ( + # PivotRootRule object audit allow deny comment oldroot all? newroot all? profile_name all? + ('pivot_root,', exp(False, False, False, '', None, True, None, True, None, True)), + ('pivot_root oldroot=/oldroot, # cmt', exp(False, False, False, ' # cmt', '/oldroot', False, None, True, None, True)), + ('pivot_root oldroot=/oldroot /new/root, # cmt', exp(False, False, False, ' # cmt', '/oldroot', False, '/new/root', False, None, True)), + ('pivot_root oldroot=/oldroot /new/root -> targetprof, # cmt', exp(False, False, False, ' # cmt', '/oldroot', False, '/new/root', False, 'targetprof', False)), + ('pivot_root oldroot=/oldroot -> targetprof, # cmt', exp(False, False, False, ' # cmt', '/oldroot', False, None, True, 'targetprof', False)), + ('pivot_root /new/root, # cmt', exp(False, False, False, ' # cmt', None, True, '/new/root', False, None, True)), + ('pivot_root /new/root -> targetprof, # cmt', exp(False, False, False, ' # cmt', None, True, '/new/root', False, 'targetprof', False)), + ('pivot_root -> targetprof, # cmt', exp(False, False, False, ' # cmt', None, True, None, True, 'targetprof', False)), + ('pivot_root oldroot="/oldroot", # cmt', exp(False, False, False, ' # cmt', '/oldroot', False, None, True, None, True)), + ('pivot_root "/new/root", # cmt', exp(False, False, False, ' # cmt', None, True, '/new/root', False, None, True)), + ('pivot_root -> "targetprof", # cmt', exp(False, False, False, ' # cmt', None, True, None, True, 'targetprof', False)), + ) + + def _run_test(self, rawrule, expected): + self.assertTrue(PivotRootRule.match(rawrule)) + obj = PivotRootRule.create_instance(rawrule) + self.assertEqual(rawrule.strip(), obj.raw_rule) + self._compare_obj(obj, expected) + + +class PivotRootTestParseInvalid(PivotRootTest): + tests = ( + ('pivot_root foo,', AppArmorException), + ('pivot_root foo bar,', AppArmorException), + ('pivot_root oldroot= ,', AppArmorException), + ('pivot_root -> ,', AppArmorException), + ) + + def _run_test(self, rawrule, expected): + self.assertTrue(PivotRootRule.match(rawrule)) # the above invalid rules still match the main regex! + with self.assertRaises(expected): + PivotRootRule.create_instance(rawrule) + + def test_invalid_rule_name(self): + self.assertFalse(PivotRootRule.match('pivot_rootbeer,')) + with self.assertRaises(AppArmorException): + PivotRootRule.create_instance('pivot_rootbeer,') + + +class PivotRootTestParseFromLog(PivotRootTest): + def test_pivot_root_from_log(self): + parser = ReadLog('', '', '') + event = 'type=AVC msg=audit(1409700678.384:547594): apparmor="DENIED" operation="pivotroot" profile="/home/ubuntu/bzr/apparmor/tests/regression/apparmor/pivot_root" name="/tmp/sdtest.21082-7446-EeefO6/new_root/" pid=21162 comm="pivot_root" srcname="/tmp/sdtest.21082-7446-EeefO6/new_root/put_old/"' + + parsed_event = parser.parse_event(event) + + self.assertEqual(parsed_event, { + 'request_mask': None, + 'denied_mask': None, + 'error_code': 0, + 'magic_token': 0, + 'parent': 0, + 'profile': '/home/ubuntu/bzr/apparmor/tests/regression/apparmor/pivot_root', + 'operation': 'pivotroot', + 'resource': None, + 'info': None, + 'aamode': 'REJECTING', + 'time': 1409700678, + 'active_hat': None, + 'pid': 21162, + 'task': 0, + 'attr': None, + 'name2': None, + 'src_name': '/tmp/sdtest.21082-7446-EeefO6/new_root/put_old/', + 'name': '/tmp/sdtest.21082-7446-EeefO6/new_root/', + 'family': None, + 'protocol': None, + 'sock_type': None, + 'class': None, + }) + + obj = PivotRootRule(parsed_event['src_name'], parsed_event['name'], PivotRootRule.ALL, log_event=parsed_event) + + # audit allow deny comment oldroot all? newroot all? target all? + expected = exp(False, False, False, '', '/tmp/sdtest.21082-7446-EeefO6/new_root/put_old/', False, '/tmp/sdtest.21082-7446-EeefO6/new_root/', False, None, True) + + self._compare_obj(obj, expected) + + self.assertEqual( + obj.get_raw(1), + ' pivot_root oldroot=/tmp/sdtest.21082-7446-EeefO6/new_root/put_old/ /tmp/sdtest.21082-7446-EeefO6/new_root/,') + + +class PivotRootFromInit(PivotRootTest): + tests = ( + # PivotRootRule object audit allow deny comment oldroot all? newroot all? profile_name all? + (PivotRootRule('/oldroot', '/new/root', 'some_profile', deny=True), exp(False, False, True, '', '/oldroot', False, '/new/root', False, 'some_profile', False)), + (PivotRootRule('/oldroot', '/new/root', PivotRootRule.ALL, deny=True), exp(False, False, True, '', '/oldroot', False, '/new/root', False, None, True)), + (PivotRootRule('/oldroot', PivotRootRule.ALL, '/someprofile', deny=True), exp(False, False, True, '', '/oldroot', False, None, True, '/someprofile', False)), + (PivotRootRule(PivotRootRule.ALL, '/new/root', '/someprofile', deny=True), exp(False, False, True, '', None, True, '/new/root', False, '/someprofile', False)), + (PivotRootRule('/oldroot', PivotRootRule.ALL, PivotRootRule.ALL, deny=True), exp(False, False, True, '', '/oldroot', False, None, True, None, True)), + (PivotRootRule(PivotRootRule.ALL, '/new/root', PivotRootRule.ALL, deny=True), exp(False, False, True, '', None, True, '/new/root', False, None, True)), + (PivotRootRule(PivotRootRule.ALL, PivotRootRule.ALL, 'some_profile', deny=True), exp(False, False, True, '', None, True, None, True, 'some_profile', False)), + (PivotRootRule(PivotRootRule.ALL, PivotRootRule.ALL, PivotRootRule.ALL, deny=True), exp(False, False, True, '', None, True, None, True, None, True)), + ) + + def _run_test(self, obj, expected): + self._compare_obj(obj, expected) + + +class InvalidPivotRootInit(AATest): + tests = ( + # (init params, expected exception) + (('', '/foo', 'bar'), AppArmorBug), # empty oldroot + (('/old', '', 'bar'), AppArmorBug), # empty newroot + (('/old', '/foo', '' ), AppArmorBug), # empty targetprof # noqa: E202 + + (('old', '/foo', 'bar'), AppArmorException), # oldroot is not a path + (('/old', 'foo', 'bar'), AppArmorException), # newroot is not a path + + + ((None, '/foo', 'bar'), AppArmorBug), # wrong type + (('/old', None, 'bar'), AppArmorBug), # + (('/old', '/foo', None ), AppArmorBug), # noqa: E202 + + ((dict(), '/foo', 'bar'), AppArmorBug), # wrong type + (('/old', dict(), 'bar'), AppArmorBug), # + (('/old', '/foo', dict()), AppArmorBug), # + ) + + def _run_test(self, params, expected): + with self.assertRaises(expected): + PivotRootRule(*params) + + def test_missing_params_1(self): + with self.assertRaises(TypeError): + PivotRootRule() + + def test_missing_params_2(self): + with self.assertRaises(TypeError): + PivotRootRule('/foo') + + def test_missing_params_3(self): + with self.assertRaises(TypeError): + PivotRootRule('/foo', '/bar') + + +class InvalidPivotRootTest(AATest): + def _check_invalid_rawrule(self, rawrule): + obj = None + self.assertFalse(PivotRootRule.match(rawrule)) + with self.assertRaises(AppArmorException): + obj = PivotRootRule.create_instance(rawrule) + + self.assertIsNone(obj, 'PivotRootRule handed back an object unexpectedly') + + def test_invalid_pivot_root_missing_comma(self): + self._check_invalid_rawrule('pivot_root') # missing comma + + def test_invalid_non_PivotRootRule(self): + self._check_invalid_rawrule('dbus,') # not a pivot_root rule + + def test_empty_data_1(self): + obj = PivotRootRule('/foo', '/bar', 'prof') + obj.oldroot = '' + # no oldroot set, and ALL not set + with self.assertRaises(AppArmorBug): + obj.get_clean(1) + + def test_empty_data_2(self): + obj = PivotRootRule('/foo', '/bar', 'prof') + obj.newroot = '' + # no newroot set, and ALL not set + with self.assertRaises(AppArmorBug): + obj.get_clean(1) + + def test_empty_data_3(self): + obj = PivotRootRule('/foo', '/bar', 'prof') + obj.profile_name = '' + # no profile_name set, and ALL not set + with self.assertRaises(AppArmorBug): + obj.get_clean(1) + + +class WritePivotRootTestAATest(AATest): + def _run_test(self, rawrule, expected): + self.assertTrue(PivotRootRule.match(rawrule)) + obj = PivotRootRule.create_instance(rawrule) + clean = obj.get_clean() + raw = obj.get_raw() + + self.assertEqual(expected.strip(), clean, 'unexpected clean rule') + self.assertEqual(rawrule.strip(), raw, 'unexpected raw rule') + + tests = ( + # raw rule clean rule + ('pivot_root,', 'pivot_root,'), + (' pivot_root , # foo ', 'pivot_root, # foo'), + (' audit pivot_root /foo,', 'audit pivot_root /foo,'), + (' deny pivot_root /foo ,# foo bar', 'deny pivot_root /foo, # foo bar'), + (' deny pivot_root "/foo" ,# foo bar', 'deny pivot_root /foo, # foo bar'), + (' allow pivot_root ,# foo bar', 'allow pivot_root, # foo bar'), + (' pivot_root oldroot=/old , # foo ', 'pivot_root oldroot=/old, # foo'), + (' pivot_root oldroot="/old" , # foo ', 'pivot_root oldroot=/old, # foo'), + (' pivot_root oldroot=/old -> some_profile , ', 'pivot_root oldroot=/old -> some_profile,'), + (' pivot_root oldroot=/old /new -> some_profile , ', 'pivot_root oldroot=/old /new -> some_profile,'), + ) + + def test_write_manually(self): + obj = PivotRootRule('/old', '/new', 'target', allow_keyword=True) + + expected = ' allow pivot_root oldroot=/old /new -> target,' + + self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule') + self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule') + + +class PivotRootCoveredTest(AATest): + def _run_test(self, param, expected): + obj = PivotRootRule.create_instance(self.rule) + check_obj = PivotRootRule.create_instance(param) + + self.assertTrue(PivotRootRule.match(param)) + + self.assertEqual(obj.is_equal(check_obj), expected[0], 'Mismatch in is_equal, expected {}'.format(expected[0])) + self.assertEqual(obj.is_equal(check_obj, True), expected[1], 'Mismatch in is_equal/strict, expected {}'.format(expected[1])) + + self.assertEqual(obj.is_covered(check_obj), expected[2], 'Mismatch in is_covered, expected {}'.format(expected[2])) + self.assertEqual(obj.is_covered(check_obj, True, True), expected[3], 'Mismatch in is_covered/exact, expected {}'.format(expected[3])) + + +class PivotRootCoveredTest_01(PivotRootCoveredTest): + rule = 'pivot_root /new,' + + tests = ( + # rule equal strict equal covered covered exact + ('pivot_root,', (False, False, False, False)), + ('pivot_root /n*,', (False, False, False, False)), + ('pivot_root oldroot=/old,', (False, False, False, False)), + ('pivot_root /new,', (True, False, True, True)), + ('pivot_root -> target,', (False, False, False, False)), + ('pivot_root oldroot=/old /new,', (False, False, True, True)), + ('pivot_root /new -> target,', (False, False, True, True)), + ('pivot_root oldroot=/old -> target,', (False, False, False, False)), + ('pivot_root oldroot=/old /new -> target,', (False, False, True, True)), + ) + + +class PivotRootCoveredTest_02(PivotRootCoveredTest): + rule = 'audit pivot_root oldroot=/ol*,' + + tests = ( + # rule equal strict equal covered covered exact + ('audit pivot_root,', (False, False, False, False)), + ('audit pivot_root oldroot=/ol*,', (True, True, True, True)), + ('audit pivot_root oldroot=/old,', (False, False, True, True)), + ('audit pivot_root /new,', (False, False, False, False)), + ('audit pivot_root -> target,', (False, False, False, False)), + ('audit pivot_root oldroot=/old /new,', (False, False, True, True)), + ('audit pivot_root /new -> target,', (False, False, False, False)), + ('audit pivot_root oldroot=/old -> target,', (False, False, True, True)), # covered exact - really? + ('audit pivot_root oldroot=/old /new -> target,', (False, False, True, True)), # covered exact - really? + ) + + +class PivotRootCoveredTest_03(PivotRootCoveredTest): + rule = 'pivot_root -> target,' + + tests = ( + # rule equal strict equal covered covered exact + ('pivot_root,', (False, False, False, False)), + ('pivot_root oldroot=/ol*,', (False, False, False, False)), + ('pivot_root oldroot=/old,', (False, False, False, False)), + ('pivot_root /new,', (False, False, False, False)), + ('pivot_root -> target,', (True, False, True, True)), + ('pivot_root oldroot=/old /new,', (False, False, False, False)), + ('pivot_root /new -> target,', (False, False, True, True)), + ('pivot_root oldroot=/old -> target,', (False, False, True, True)), + ('pivot_root oldroot=/old /new -> target,', (False, False, True, True)), + ) + + +class PivotRootCoveredTest_04(PivotRootCoveredTest): + rule = 'deny pivot_root /foo,' + + tests = ( + # rule equal strict equal covered covered exact + (' deny pivot_root /foo,', (True, True, True, True)), + ('audit deny pivot_root /foo,', (False, False, False, False)), + (' pivot_root /foo,', (False, False, False, False)), # XXX should covered be true here? + (' deny pivot_root /bar,', (False, False, False, False)), + (' deny pivot_root,', (False, False, False, False)), + ) + + +class PivotRootCoveredTest_Invalid(AATest): + # TODO: should this be detected? + # def test_borked_obj_is_covered_1(self): + # obj = PivotRootRule.create_instance('pivot_root oldroot=/old /new -> target,') + + # testobj = PivotRootRule('/old', '/foo', 'targetprof') + # testobj.oldrooot = None + + # with self.assertRaises(AppArmorBug): + # obj.is_covered(testobj) + + def test_borked_obj_is_covered_2(self): + obj = PivotRootRule.create_instance('pivot_root oldroot=/old /new -> target,') + + testobj = PivotRootRule('/old', '/foo', 'targetprof') + testobj.newroot = '' + + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + # def test_borked_obj_is_covered_3(self): + # TODO: should this be detected? + # obj = PivotRootRule.create_instance('pivot_root oldroot=/old /new -> target,') + + # testobj = PivotRootRule('/old', '/foo', 'targetprof') + # testobj.profile_name = '' + + # with self.assertRaises(AppArmorBug): + # obj.is_covered(testobj) + + def test_invalid_is_covered(self): + raw_rule = 'pivot_root oldroot=/old /new -> target,' + + class SomeOtherClass(PivotRootRule): + pass + + obj = PivotRootRule.create_instance(raw_rule) + testobj = SomeOtherClass.create_instance(raw_rule) # different type + with self.assertRaises(AppArmorBug): + obj.is_covered(testobj) + + def test_invalid_is_equal_1(self): + raw_rule = 'pivot_root oldroot=/old /new -> target,' + + class SomeOtherClass(PivotRootRule): + pass + + obj = PivotRootRule.create_instance(raw_rule) + testobj = SomeOtherClass.create_instance(raw_rule) # different type + with self.assertRaises(AppArmorBug): + obj.is_equal(testobj) + +# def test_invalid_is_equal_2(self): + # TODO: should this be detected? +# obj = PivotRootRule.create_instance('pivot_root oldroot=/old /new -> target,') + +# testobj = PivotRootRule.create_instance('pivot_root oldroot=/old /new -> target,') +# testobj.all_oldroots = False # make testobj invalid (should trigger exception in _is_equal_aare()) + +# with self.assertRaises(AppArmorBug): +# obj.is_equal(testobj) + + +class PivotRootLogprofHeaderTest(AATest): + tests = ( + ('pivot_root,', [ _('Old root'), _('ALL'), _('New root'), _('ALL'), _('Target profile'), _('ALL')]), # noqa: E201 + ('pivot_root oldroot=/old,', [ _('Old root'), '/old', _('New root'), _('ALL'), _('Target profile'), _('ALL')]), # noqa: E201 + ('deny pivot_root,', [_('Qualifier'), 'deny', _('Old root'), _('ALL'), _('New root'), _('ALL'), _('Target profile'), _('ALL')]), + ('allow pivot_root oldroot=/old,', [_('Qualifier'), 'allow', _('Old root'), '/old', _('New root'), _('ALL'), _('Target profile'), _('ALL')]), + ('audit pivot_root /new,', [_('Qualifier'), 'audit', _('Old root'), _('ALL'), _('New root'), '/new', _('Target profile'), _('ALL')]), + ('audit deny pivot_root /new -> target,', [_('Qualifier'), 'audit deny', _('Old root'), _('ALL'), _('New root'), '/new', _('Target profile'), 'target']), + ('pivot_root oldroot=/old /new -> target,', [ _('Old root'), '/old', _('New root'), '/new', _('Target profile'), 'target']), # noqa: E201 + ) + + def _run_test(self, params, expected): + obj = PivotRootRule.create_instance(params) + self.assertEqual(obj.logprof_header(), expected) + + +class PivotRootEditHeaderTest(AATest): + def _run_test(self, params, expected): + rule_obj = PivotRootRule.create_instance(params) + self.assertEqual(rule_obj.can_edit, True) + prompt, path_to_edit = rule_obj.edit_header() + self.assertEqual(path_to_edit, expected) + + tests = ( + ('pivot_root oldroot=/old /foo/bar/baz -> target,', '/foo/bar/baz'), + ('pivot_root /foo/**/baz,', '/foo/**/baz'), + ('pivot_root /foo/** -> /bar,', '/foo/**'), + ) + + def test_edit_header_bare_pivot_root(self): + rule_obj = PivotRootRule.create_instance('pivot_root,') + self.assertEqual(rule_obj.can_edit, False) + with self.assertRaises(AppArmorBug): + rule_obj.edit_header() + + +class PivotRootValidateAndStoreEditTest(AATest): + def _run_test(self, params, expected): + rule_obj = PivotRootRule('/old/', '/foo/bar/baz', 'target', log_event=True) + + self.assertEqual(rule_obj.validate_edit(params), expected) + + rule_obj.store_edit(params) + self.assertEqual(rule_obj.get_raw(), 'pivot_root oldroot=/old/ ' + params + ' -> target,') + + tests = ( + # edited path match + ('/foo/bar/baz', True), + ('/foo/bar/*', True), + ('/foo/bar/???', True), + ('/foo/xy**', False), + ('/foo/bar/baz/', False), + ) + + def test_validate_not_a_path(self): + rule_obj = PivotRootRule.create_instance('pivot_root /foo/bar/baz,') + + with self.assertRaises(AppArmorException): + rule_obj.validate_edit('foo/bar/baz') + + with self.assertRaises(AppArmorException): + rule_obj.store_edit('foo/bar/baz') + + def test_validate_edit_bare_pivot_root(self): + rule_obj = PivotRootRule.create_instance('pivot_root,') + self.assertEqual(rule_obj.can_edit, False) + + with self.assertRaises(AppArmorBug): + rule_obj.validate_edit('/foo/bar') + + with self.assertRaises(AppArmorBug): + rule_obj.store_edit('/foo/bar') + + +# --- tests for PivotRootRuleset --- # + +class PivotRootRulesTest(AATest): + def test_empty_ruleset(self): + ruleset = PivotRootRuleset() + ruleset_2 = PivotRootRuleset() + self.assertEqual([], ruleset.get_raw(2)) + self.assertEqual([], ruleset.get_clean(2)) + self.assertEqual([], ruleset_2.get_raw(2)) + self.assertEqual([], ruleset_2.get_clean(2)) + + # test __repr__() for empty ruleset + self.assertEqual(str(ruleset), '') + + def test_ruleset_1(self): + ruleset = PivotRootRuleset() + rules = ( + 'pivot_root oldroot=/foo,', + 'pivot_root /new,', + ) + + expected_raw = [ + 'pivot_root oldroot=/foo,', + 'pivot_root /new,', + '', + ] + + expected_clean = [ + 'pivot_root /new,', + 'pivot_root oldroot=/foo,', + '', + ] + + for rule in rules: + ruleset.add(PivotRootRule.create_instance(rule)) + + self.assertEqual(expected_raw, ruleset.get_raw()) + self.assertEqual(expected_clean, ruleset.get_clean()) + + # test __repr__() for non-empty ruleset + self.assertEqual( + str(ruleset), '\n pivot_root oldroot=/foo,\n pivot_root /new,\n') + + +class PivotRootGlobTestAATest(AATest): + def test_glob(self): + glob_list = [( + 'pivot_root /foo/bar,', + 'pivot_root /foo/*,', + 'pivot_root /**,', + )] + for globs in glob_list: + for i in range(len(globs) - 1): + rule = PivotRootRule.create_instance(globs[i]) + rule.glob() + self.assertEqual(rule.get_clean(), globs[i + 1]) + + def test_glob_all(self): + glob_list = [( + 'pivot_root,', + 'pivot_root,', + )] + for globs in glob_list: + for i in range(len(globs) - 1): + rule = PivotRootRule.create_instance(globs[i]) + rule.glob() + self.assertEqual(rule.get_clean(), globs[i + 1]) + + +# def test_glob_ext(self): +# # rule = PivotRootRule.create_instance('pivot_root /foo/bar,') +# with self.assertRaises(NotImplementedError): +# # get_glob_ext is not available for pivot_root rules +# self.ruleset.get_glob_ext('pivot_root /foo,') + + +# class PivotRootDeleteTestAATest(AATest): +# pass + + +setup_all_loops(__name__) +if __name__ == '__main__': + unittest.main(verbosity=1) From a15a33474dc24df6486a4f69be389d73c213b7e7 Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Fri, 10 May 2024 22:11:28 +0200 Subject: [PATCH 3/4] Use PivotRootRule and PivotRootRuleset ... for handling pivot_root rules. This replaces the old code that just stores the full rule as text. We also get rid of the old ['allow'] and ['deny'] items in ProfileStorage, the handling of old write functions, and the last usage of _Raw_Rule (and therefore _Raw_Rule itsself). Also delete the old test-pivot_root_parse.py which relied on the ancient code, and even used a wrong syntax in its test rules. --- utils/apparmor/aa.py | 32 ++--------------------- utils/apparmor/common.py | 4 --- utils/apparmor/profile_storage.py | 40 +++-------------------------- utils/apparmor/rules.py | 33 ------------------------ utils/test/Makefile | 2 +- utils/test/test-pivot_root_parse.py | 33 ------------------------ utils/test/test-regex_matches.py | 3 ++- 7 files changed, 8 insertions(+), 139 deletions(-) delete mode 100644 utils/apparmor/rules.py delete mode 100644 utils/test/test-pivot_root_parse.py diff --git a/utils/apparmor/aa.py b/utils/apparmor/aa.py index 590337325..755fc2e1a 100644 --- a/utils/apparmor/aa.py +++ b/utils/apparmor/aa.py @@ -26,7 +26,6 @@ from tempfile import NamedTemporaryFile import apparmor.config import apparmor.logparser -import apparmor.rules as aarules import apparmor.severity import apparmor.ui as aaui from apparmor.aare import AARE @@ -38,7 +37,7 @@ from apparmor.profile_storage import ProfileStorage, add_or_remove_flag, ruletyp from apparmor.regex import ( RE_HAS_COMMENT_SPLIT, RE_PROFILE_CHANGE_HAT, RE_PROFILE_CONDITIONAL, RE_PROFILE_CONDITIONAL_BOOLEAN, RE_PROFILE_CONDITIONAL_VARIABLE, RE_PROFILE_END, - RE_PROFILE_HAT_DEF, RE_PROFILE_PIVOT_ROOT, RE_PROFILE_START, + RE_PROFILE_HAT_DEF, RE_PROFILE_START, RE_RULE_HAS_COMMA, parse_profile_start_line, re_match_include) from apparmor.rule.abi import AbiRule from apparmor.rule.capability import CapabilityRule @@ -1955,29 +1954,6 @@ def parse_profile_data(data, file, do_include, in_preamble): # Conditional Boolean defined pass - elif RE_PROFILE_PIVOT_ROOT.search(line): - matches = RE_PROFILE_PIVOT_ROOT.search(line).groups() - - if not profile: - raise AppArmorException(_('Syntax Error: Unexpected pivot_root entry found in file: %(file)s line: %(line)s') - % {'file': file, 'line': lineno + 1}) - - audit = False - if matches[0]: - audit = True - allow = 'allow' - if matches[1] and matches[1].strip() == 'deny': - allow = 'deny' - pivot_root = matches[2].strip() - - pivot_root_rule = parse_pivot_root_rule(pivot_root) - pivot_root_rule.audit = audit - pivot_root_rule.deny = (allow == 'deny') - - pivot_root_rules = profile_data[profname][allow].get('pivot_root', []) - pivot_root_rules.append(pivot_root_rule) - profile_data[profname][allow]['pivot_root'] = pivot_root_rules - elif RE_PROFILE_CHANGE_HAT.search(line): matches = RE_PROFILE_CHANGE_HAT.search(line).groups() @@ -2065,6 +2041,7 @@ def match_line_against_rule_classes(line, profile, file, lineno, in_preamble): 'mqueue', 'io_uring', 'mount', + 'pivot_root', 'unix', ): @@ -2117,11 +2094,6 @@ def split_to_merged(profile_data): return merged -def parse_pivot_root_rule(line): - # XXX Do real parsing here - return aarules.Raw_Pivot_Root_Rule(line) - - def write_piece(profile_data, depth, name, nhat): pre = ' ' * depth data = [] diff --git a/utils/apparmor/common.py b/utils/apparmor/common.py index 626440942..da426acd8 100644 --- a/utils/apparmor/common.py +++ b/utils/apparmor/common.py @@ -20,8 +20,6 @@ import termios import tty from tempfile import NamedTemporaryFile -import apparmor.rules as rules - DEBUGGING = False @@ -106,8 +104,6 @@ def recursive_print(src, dpth=0, key=''): for litem in src: recursive_print(litem, dpth + 1) print(tabs + "]") - elif isinstance(src, rules._Raw_Rule): - src.recursive_print(dpth) else: if key: print(tabs + '%s = %s' % (key, src)) diff --git a/utils/apparmor/profile_storage.py b/utils/apparmor/profile_storage.py index ca1adbbeb..2aec1153c 100644 --- a/utils/apparmor/profile_storage.py +++ b/utils/apparmor/profile_storage.py @@ -32,6 +32,7 @@ from apparmor.rule.userns import UserNamespaceRule, UserNamespaceRuleset from apparmor.rule.mqueue import MessageQueueRule, MessageQueueRuleset from apparmor.rule.io_uring import IOUringRule, IOUringRuleset from apparmor.rule.mount import MountRule, MountRuleset +from apparmor.rule.pivot_root import PivotRootRule, PivotRootRuleset from apparmor.rule.unix import UnixRule, UnixRuleset from apparmor.translations import init_translation @@ -54,8 +55,8 @@ ruletypes = { 'mqueue': {'rule': MessageQueueRule, 'ruleset': MessageQueueRuleset}, 'io_uring': {'rule': IOUringRule, 'ruleset': IOUringRuleset}, 'mount': {'rule': MountRule, 'ruleset': MountRuleset}, + 'pivot_root': {'rule': PivotRootRule, 'ruleset': PivotRootRuleset}, 'unix': {'rule': UnixRule, 'ruleset': UnixRuleset}, - } @@ -87,13 +88,6 @@ class ProfileStorage: data['is_hat'] = False # profile or hat? data['hat_keyword'] = False # True for 'hat foo', False for '^foo' - data['allow'] = dict() - data['deny'] = dict() - - # pivot_root has a .get() fallback to list() - initialize it nevertheless - data['allow']['pivot_root'] = [] - data['deny']['pivot_root'] = [] - self.data = data def __getitem__(self, key): @@ -184,11 +178,6 @@ class ProfileStorage: Note that the profile header and the closing "}" are _not_ included. """ - # "old" write functions for rule types not implemented as *Rule class yet - write_functions = { - 'pivot_root': write_pivot_root, - } - write_order = [ 'abi', 'inc_ie', @@ -211,10 +200,7 @@ class ProfileStorage: data = [] for ruletype in write_order: - if write_functions.get(ruletype): - data.extend(write_functions[ruletype](self.data, depth)) - else: - data.extend(self.data[ruletype].get_clean(depth)) + data.extend(self.data[ruletype].get_clean(depth)) return data @@ -310,23 +296,3 @@ def var_transform(ref): value = '""' data.append(quote_if_needed(value)) return ' '.join(data) - - -def write_pivot_root_rules(prof_data, depth, allow): - pre = ' ' * depth - data = [] - - # no pivot_root rules, so return - if not prof_data[allow].get('pivot_root', False): - return data - - for pivot_root_rule in prof_data[allow]['pivot_root']: - data.append('%s%s' % (pre, pivot_root_rule.serialize())) - data.append('') - return data - - -def write_pivot_root(prof_data, depth): - data = write_pivot_root_rules(prof_data, depth, 'deny') - data.extend(write_pivot_root_rules(prof_data, depth, 'allow')) - return data diff --git a/utils/apparmor/rules.py b/utils/apparmor/rules.py deleted file mode 100644 index 52c6d4042..000000000 --- a/utils/apparmor/rules.py +++ /dev/null @@ -1,33 +0,0 @@ -# ------------------------------------------------------------------ -# -# Copyright (C) 2014 Canonical Ltd. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of version 2 of the GNU General Public -# License published by the Free Software Foundation. -# -# ------------------------------------------------------------------ - -class _Raw_Rule: - audit = False - deny = False - - def __init__(self, rule): - self.rule = rule - - def serialize(self): - return "%s%s%s" % ('audit ' if self.audit else '', - 'deny ' if self.deny else '', - self.rule) - - def recursive_print(self, depth): - tabs = ' ' * depth * 4 - print('%s[%s]' % (tabs, type(self).__name__)) - tabs += ' ' * 4 - print('%saudit = %s' % (tabs, self.audit)) - print('%sdeny = %s' % (tabs, self.deny)) - print('%sraw rule = %s' % (tabs, self.rule)) - - -class Raw_Pivot_Root_Rule(_Raw_Rule): - pass diff --git a/utils/test/Makefile b/utils/test/Makefile index 3eca84e72..7f52c0d2c 100644 --- a/utils/test/Makefile +++ b/utils/test/Makefile @@ -21,7 +21,7 @@ COMMONDIR=../../common/ include $(COMMONDIR)/Make.rules # files that don't have 100% test coverage -INCOMPLETE_COVERAGE=libraries/libapparmor/swig/python/.*/LibAppArmor/LibAppArmor.py|utils/aa-logprof|utils/apparmor/aa.py|utils/apparmor/common.py|utils/apparmor/config.py|utils/apparmor/easyprof.py|utils/apparmor/fail.py|utils/apparmor/logparser.py|utils/apparmor/profile_storage.py|utils/apparmor/rules.py|utils/apparmor/ui.py|minitools_test.py +INCOMPLETE_COVERAGE=libraries/libapparmor/swig/python/.*/LibAppArmor/LibAppArmor.py|utils/aa-logprof|utils/apparmor/aa.py|utils/apparmor/common.py|utils/apparmor/config.py|utils/apparmor/easyprof.py|utils/apparmor/fail.py|utils/apparmor/logparser.py|utils/apparmor/ui.py|minitools_test.py ifdef USE_SYSTEM diff --git a/utils/test/test-pivot_root_parse.py b/utils/test/test-pivot_root_parse.py deleted file mode 100644 index d33532595..000000000 --- a/utils/test/test-pivot_root_parse.py +++ /dev/null @@ -1,33 +0,0 @@ -#! /usr/bin/python3 -# ------------------------------------------------------------------ -# -# Copyright (C) 2014 Canonical Ltd. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of version 2 of the GNU General Public -# License published by the Free Software Foundation. -# -# ------------------------------------------------------------------ - -import unittest - -import apparmor.aa as aa -from common_test import AAParseTest, setup_aa, setup_regex_tests - - -class AAParsePivotRootTest(AAParseTest): - def setUp(self): - self.parse_function = aa.parse_pivot_root_rule - - tests = ( - ('pivot_root,', 'pivot_root base keyword'), - ('pivot_root /old,', 'pivot_root oldroot rule'), - ('pivot_root /old /new,', 'pivot_root old and new root rule'), - ('pivot_root /old /new -> /usr/bin/child,', 'pivot_root child rule'), - ) - - -setup_aa(aa) -if __name__ == '__main__': - setup_regex_tests(AAParsePivotRootTest) - unittest.main(verbosity=1) diff --git a/utils/test/test-regex_matches.py b/utils/test/test-regex_matches.py index cd081dedf..97120fad3 100644 --- a/utils/test/test-regex_matches.py +++ b/utils/test/test-regex_matches.py @@ -16,6 +16,7 @@ from apparmor.common import AppArmorBug, AppArmorException from apparmor.regex import ( RE_PROFILE_CAP, RE_PROFILE_DBUS, RE_PROFILE_MOUNT, RE_PROFILE_PTRACE, RE_PROFILE_SIGNAL, 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) from common_test import AATest, setup_aa, setup_all_loops @@ -313,7 +314,7 @@ class AARegexPivotRoot(AARegexTest): """Tests for RE_PROFILE_PIVOT_ROOT""" def AASetup(self): - self.regex = aa.RE_PROFILE_PIVOT_ROOT + self.regex = RE_PROFILE_PIVOT_ROOT tests = ( (' pivot_root,', (None, None, 'pivot_root,', None, None)), From a4df6cba6a1f5d3206b68ec3936282307bb9862f Mon Sep 17 00:00:00 2001 From: Christian Boltz Date: Fri, 10 May 2024 22:50:23 +0200 Subject: [PATCH 4/4] Add support for asking about pivot_root to aa-logprof --- utils/apparmor/aa.py | 9 +++++++++ utils/apparmor/logparser.py | 5 +++++ utils/test/test-libapparmor-test_multi.py | 3 --- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/utils/apparmor/aa.py b/utils/apparmor/aa.py index 755fc2e1a..90c15ec5f 100644 --- a/utils/apparmor/aa.py +++ b/utils/apparmor/aa.py @@ -46,6 +46,7 @@ from apparmor.rule.dbus import DbusRule from apparmor.rule.file import FileRule from apparmor.rule.include import IncludeRule from apparmor.rule.network import NetworkRule +from apparmor.rule.pivot_root import PivotRootRule from apparmor.rule.ptrace import PtraceRule from apparmor.rule.signal import SignalRule from apparmor.rule.userns import UserNamespaceRule @@ -1730,6 +1731,14 @@ def collapse_log(hashlog, ignore_null_profiles=True): mount_event = MountRule(operation=operation, fstype=_fstype, options=_options, source=_source, dest=_dest) if not hat_exists or not is_known_rule(aa[profile][hat], 'mount', mount_event): log_dict[aamode][final_name]['mount'].add(mount_event) + + pivot_root = hashlog[aamode][full_profile]['pivot_root'] + for oldroot in pivot_root.keys(): + for newroot in pivot_root[oldroot]: + pivot_root_event = PivotRootRule(oldroot, newroot, PivotRootRule.ALL, log_event=True) + if not hat_exists or not is_known_rule(aa[profile][hat], 'pivot_root', pivot_root_event): + log_dict[aamode][final_name]['pivot_root'].add(pivot_root_event) + return log_dict diff --git a/utils/apparmor/logparser.py b/utils/apparmor/logparser.py index 2cbcb640c..c075b62c5 100644 --- a/utils/apparmor/logparser.py +++ b/utils/apparmor/logparser.py @@ -55,6 +55,7 @@ class ReadLog: 'exec': hasher(), 'network': hasher(), 'path': hasher(), + 'pivot_root': hasher(), 'ptrace': hasher(), 'signal': hasher(), 'userns': hasher(), @@ -240,6 +241,10 @@ class ReadLog: self.hashlog[aamode][full_profile]['mount'][e['operation']][e['flags']][e['fs_type']][e['name']][None] = True return + elif e['operation'] and e['operation'] == 'pivotroot': + # TODO: can the log contain the target profile? + self.hashlog[aamode][full_profile]['pivot_root'][e['src_name']][e['name']] = True + elif e['class'] and e['class'] == 'net' and e['family'] and e['family'] == 'unix': rule = (e['sock_type'], None) # Protocol is not supported yet. local = (e['addr'], None, e['attr'], None) diff --git a/utils/test/test-libapparmor-test_multi.py b/utils/test/test-libapparmor-test_multi.py index d7bd9af51..b9c08261f 100644 --- a/utils/test/test-libapparmor-test_multi.py +++ b/utils/test/test-libapparmor-test_multi.py @@ -153,8 +153,6 @@ log_to_skip = [ # tests that do not produce the expected profile (checked with assertNotEqual) log_to_profile_known_failures = [ - 'testcase_pivotroot_01', # pivot_rot not yet supported in logparser - # exec events 'testcase01', 'testcase12', @@ -175,7 +173,6 @@ log_to_profile_skip = [ # tests that cause an empty log log_to_profile_known_empty_log = [ 'change_onexec_lp1648143', # change_onexec not supported in logparser.py yet (and the log is about "no new privs" error) - 'testcase_pivotroot_01', # pivotroot not yet supported in logparser 'ptrace_garbage_lp1689667_1', # no denied= in log 'ptrace_no_denied_mask', # no denied= in log 'unconfined-change_hat', # unconfined trying to change_hat, which isn't allowed