mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-08-31 06:16:03 +00:00
Merge Utils: add support for the 'all,' rule
MR: https://gitlab.com/apparmor/apparmor/-/merge_requests/1105 Approved-by: John Johansen <john@jjmx.net> Merged-by: John Johansen <john@jjmx.net>
This commit is contained in:
@@ -2125,6 +2125,7 @@ def match_line_against_rule_classes(line, profile, file, lineno, in_preamble):
|
||||
|
||||
for rule_name in (
|
||||
'abi',
|
||||
'all',
|
||||
'alias',
|
||||
'boolean',
|
||||
'variable',
|
||||
|
@@ -18,6 +18,7 @@ from apparmor.common import AppArmorBug, AppArmorException
|
||||
from apparmor.regex import parse_profile_start_line
|
||||
from apparmor.rule import quote_if_needed
|
||||
from apparmor.rule.abi import AbiRule, AbiRuleset
|
||||
from apparmor.rule.all import AllRule, AllRuleset
|
||||
from apparmor.rule.capability import CapabilityRule, CapabilityRuleset
|
||||
from apparmor.rule.change_profile import ChangeProfileRule, ChangeProfileRuleset
|
||||
from apparmor.rule.dbus import DbusRule, DbusRuleset
|
||||
@@ -37,6 +38,7 @@ _ = init_translation()
|
||||
ruletypes = {
|
||||
'abi': {'rule': AbiRule, 'ruleset': AbiRuleset},
|
||||
'inc_ie': {'rule': IncludeRule, 'ruleset': IncludeRuleset},
|
||||
'all': {'rule': AllRule, 'ruleset': AllRuleset},
|
||||
'capability': {'rule': CapabilityRule, 'ruleset': CapabilityRuleset},
|
||||
'change_profile': {'rule': ChangeProfileRule, 'ruleset': ChangeProfileRuleset},
|
||||
'dbus': {'rule': DbusRule, 'ruleset': DbusRuleset},
|
||||
|
@@ -34,6 +34,7 @@ RE_XATTRS = r'(\s+xattrs\s*=\s*\((?P<xattrs>([^)=]+(=[^)=]+)?\s?)+)\)\s*)?'
|
||||
RE_FLAGS = r'(\s+(flags\s*=\s*)?\((?P<flags>[^)]+)\))?'
|
||||
|
||||
RE_PROFILE_END = re.compile(r'^\s*\}' + RE_EOL)
|
||||
RE_PROFILE_ALL = re.compile(RE_AUDIT_DENY + r'all' + RE_COMMA_EOL)
|
||||
RE_PROFILE_CAP = re.compile(RE_AUDIT_DENY + r'capability(?P<capability>(\s+\S+)+)?' + RE_COMMA_EOL)
|
||||
RE_PROFILE_ALIAS = re.compile(r'^\s*alias\s+(?P<orig_path>"??.+?"??)\s+->\s*(?P<target>"??.+?"??)' + RE_COMMA_EOL)
|
||||
RE_PROFILE_RLIMIT = re.compile(r'^\s*set\s+rlimit\s+(?P<rlimit>[a-z]+)\s*<=\s*(?P<value>[^ ]+(\s+[a-zA-Z]+)?)' + RE_COMMA_EOL)
|
||||
|
83
utils/apparmor/rule/all.py
Normal file
83
utils/apparmor/rule/all.py
Normal file
@@ -0,0 +1,83 @@
|
||||
# ----------------------------------------------------------------------
|
||||
# Copyright (C) 2023 Christian Boltz <apparmor@cboltz.de>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
from apparmor.regex import RE_PROFILE_ALL
|
||||
from apparmor.rule import BaseRule, BaseRuleset, parse_modifiers
|
||||
from apparmor.translations import init_translation
|
||||
|
||||
_ = init_translation()
|
||||
|
||||
|
||||
class AllRule(BaseRule):
|
||||
"""Class to handle and store a single all rule"""
|
||||
|
||||
# This class doesn't have any localvars, therefore it doesn't need 'ALL'
|
||||
|
||||
can_glob = False
|
||||
rule_name = 'all'
|
||||
_match_re = RE_PROFILE_ALL
|
||||
|
||||
def __init__(self, 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)
|
||||
|
||||
# no localvars -> nothing more to do
|
||||
|
||||
@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)
|
||||
|
||||
return cls(audit=audit, deny=deny,
|
||||
allow_keyword=allow_keyword,
|
||||
comment=comment)
|
||||
|
||||
def get_clean(self, depth=0):
|
||||
"""return rule (in clean/default formatting)"""
|
||||
|
||||
space = ' ' * depth
|
||||
|
||||
return ('%s%sall,%s' % (space, self.modifiers_str(), self.comment))
|
||||
|
||||
def _is_covered_localvars(self, other_rule):
|
||||
"""check if other_rule is covered by this rule object"""
|
||||
|
||||
# no localvars, so there can't be a difference
|
||||
return True
|
||||
|
||||
def _is_equal_localvars(self, rule_obj, strict):
|
||||
"""compare if rule-specific variables are equal"""
|
||||
|
||||
# no localvars, so there can't be a difference
|
||||
return True
|
||||
|
||||
def severity(self, sev_db):
|
||||
# allowing _everything_ is the worst thing you could do, therefore hardcode highest severity
|
||||
severity = 10
|
||||
|
||||
return severity
|
||||
|
||||
def _logprof_header_localvars(self):
|
||||
return _('All'), _('Allow everything')
|
||||
|
||||
|
||||
class AllRuleset(BaseRuleset):
|
||||
"""Class to handle and store a collection of all rules"""
|
||||
|
||||
def get_glob(self, path_or_rule):
|
||||
# There's nothing to glob in all rules
|
||||
raise NotImplementedError
|
321
utils/test/test-all.py
Normal file
321
utils/test/test-all.py
Normal file
@@ -0,0 +1,321 @@
|
||||
#!/usr/bin/python3
|
||||
# ----------------------------------------------------------------------
|
||||
# Copyright (C) 2023 Christian Boltz <apparmor@cboltz.de>
|
||||
#
|
||||
# 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
|
||||
|
||||
import apparmor.severity as severity
|
||||
|
||||
from apparmor.common import AppArmorBug, AppArmorException
|
||||
from apparmor.rule.all import AllRule, AllRuleset
|
||||
from apparmor.translations import init_translation
|
||||
from common_test import AATest, setup_all_loops
|
||||
|
||||
_ = init_translation()
|
||||
|
||||
exp = namedtuple(
|
||||
'exp', ('audit', 'allow_keyword', 'deny', 'comment',
|
||||
# no localvars
|
||||
))
|
||||
|
||||
# --- tests for single AllRule --- #
|
||||
|
||||
|
||||
class AllTest(AATest):
|
||||
def _compare_obj(self, obj, expected):
|
||||
self.assertEqual(expected.allow_keyword, obj.allow_keyword)
|
||||
self.assertEqual(expected.audit, obj.audit)
|
||||
self.assertEqual(expected.deny, obj.deny)
|
||||
self.assertEqual(expected.comment, obj.comment)
|
||||
|
||||
|
||||
class AllTestParse(AllTest):
|
||||
tests = (
|
||||
# rawrule audit allow deny comment
|
||||
('all,', exp(False, False, False, '', )),
|
||||
('deny all, # comment', exp(False, False, True, ' # comment', )),
|
||||
('audit allow all,', exp(True, True, False, '', )),
|
||||
('audit allow all,', exp(True, True, False, '', )),
|
||||
)
|
||||
|
||||
def _run_test(self, rawrule, expected):
|
||||
self.assertTrue(AllRule.match(rawrule))
|
||||
obj = AllRule.create_instance(rawrule)
|
||||
self.assertEqual(rawrule.strip(), obj.raw_rule)
|
||||
self._compare_obj(obj, expected)
|
||||
|
||||
|
||||
class AllTestParseInvalid(AllTest):
|
||||
tests = (
|
||||
('all -> ,', AppArmorException),
|
||||
('owner all,', AppArmorException),
|
||||
('all foo ,', AppArmorException),
|
||||
)
|
||||
|
||||
def _run_test(self, rawrule, expected):
|
||||
self.assertFalse(AllRule.match(rawrule))
|
||||
with self.assertRaises(expected):
|
||||
AllRule.create_instance(rawrule)
|
||||
|
||||
|
||||
# we won't ever support converting a log event to an 'all,' rule
|
||||
# class AllTestParseFromLog(AllTest):
|
||||
|
||||
|
||||
class AllFromInit(AllTest):
|
||||
tests = (
|
||||
# AllRule object audit allow deny comment
|
||||
(AllRule(deny=True), exp(False, False, True, '', )),
|
||||
(AllRule(), exp(False, False, False, '', )),
|
||||
)
|
||||
|
||||
def _run_test(self, obj, expected):
|
||||
self._compare_obj(obj, expected)
|
||||
|
||||
|
||||
# no localvars -> no way to hand over invalid values, or to miss a required parameter
|
||||
# class InvalidAllInit(AATest):
|
||||
|
||||
|
||||
class InvalidAllTest(AATest):
|
||||
def _check_invalid_rawrule(self, rawrule):
|
||||
obj = None
|
||||
self.assertFalse(AllRule.match(rawrule))
|
||||
with self.assertRaises(AppArmorException):
|
||||
obj = AllRule.create_instance(rawrule)
|
||||
|
||||
self.assertIsNone(obj, 'AllRule handed back an object unexpectedly')
|
||||
|
||||
def test_invalid_net_missing_comma(self):
|
||||
self._check_invalid_rawrule('all') # missing comma
|
||||
|
||||
def test_invalid_net_non_AllRule(self):
|
||||
self._check_invalid_rawrule('dbus,') # not a all rule
|
||||
|
||||
# no localvars, therefore we can't break anything inside the class variables
|
||||
# def test_empty_all_data_1(self):
|
||||
|
||||
|
||||
class WriteAllTestAATest(AATest):
|
||||
tests = (
|
||||
# raw rule clean rule
|
||||
(' all , # foo ', 'all, # foo'),
|
||||
(' audit all ,', 'audit all,'),
|
||||
(' deny all ,# foo bar', 'deny all, # foo bar'),
|
||||
(' allow all ,# foo bar', 'allow all, # foo bar'),
|
||||
(' allow all ,', 'allow all,'),
|
||||
)
|
||||
|
||||
def _run_test(self, rawrule, expected):
|
||||
self.assertTrue(AllRule.match(rawrule))
|
||||
obj = AllRule.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')
|
||||
|
||||
def test_write_manually(self):
|
||||
obj = AllRule(allow_keyword=True)
|
||||
|
||||
expected = ' allow all,'
|
||||
|
||||
self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule')
|
||||
self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule')
|
||||
|
||||
|
||||
class AllCoveredTest(AATest):
|
||||
def _run_test(self, param, expected):
|
||||
obj = AllRule.create_instance(self.rule)
|
||||
check_obj = AllRule.create_instance(param)
|
||||
|
||||
self.assertTrue(AllRule.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 AllCoveredTest_01(AllCoveredTest):
|
||||
rule = 'all,'
|
||||
|
||||
tests = (
|
||||
# rule equal strict equal covered covered exact
|
||||
(' all,', (True, True, True, True)),
|
||||
(' allow all,', (True, False, True, True)),
|
||||
('audit all,', (False, False, False, False)),
|
||||
('audit deny all,', (False, False, False, False)),
|
||||
(' deny all,', (False, False, False, False)),
|
||||
)
|
||||
|
||||
|
||||
class AllCoveredTest_02(AllCoveredTest):
|
||||
rule = 'audit all,'
|
||||
|
||||
tests = (
|
||||
# rule equal strict equal covered covered exact
|
||||
( 'all,', (False, False, True, False)),
|
||||
('audit all,', (True, True, True, True)),
|
||||
)
|
||||
|
||||
|
||||
class AllCoveredTest_03(AllCoveredTest):
|
||||
rule = 'deny all,'
|
||||
|
||||
tests = (
|
||||
# rule equal strict equal covered covered exact
|
||||
( 'deny all,', (True, True, True, True)),
|
||||
('audit deny all,', (False, False, False, False)),
|
||||
( 'all,', (False, False, False, False)), # XXX should covered be true here?
|
||||
)
|
||||
|
||||
|
||||
class AllCoveredTest_Invalid(AATest):
|
||||
def test_invalid_is_covered(self):
|
||||
raw_rule = 'all,'
|
||||
class SomeOtherClass(AllRule):
|
||||
pass
|
||||
|
||||
obj = AllRule.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(self):
|
||||
raw_rule = 'all,'
|
||||
class SomeOtherClass(AllRule):
|
||||
pass
|
||||
|
||||
obj = AllRule.create_instance(raw_rule)
|
||||
testobj = SomeOtherClass.create_instance(raw_rule) # different type
|
||||
with self.assertRaises(AppArmorBug):
|
||||
obj.is_equal(testobj)
|
||||
|
||||
|
||||
class AllSeverityTest(AATest):
|
||||
tests = (
|
||||
('all,', 10),
|
||||
)
|
||||
|
||||
def _run_test(self, params, expected):
|
||||
sev_db = severity.Severity('../severity.db', 'unknown')
|
||||
obj = AllRule.create_instance(params)
|
||||
rank = obj.severity(sev_db)
|
||||
self.assertEqual(rank, expected)
|
||||
|
||||
|
||||
class AllLogprofHeaderTest(AATest):
|
||||
tests = (
|
||||
('all,', [ 'All', _('Allow everything'), ]),
|
||||
('deny all,', [_('Qualifier'), 'deny', 'All', _('Allow everything'), ]),
|
||||
('allow all,', [_('Qualifier'), 'allow', 'All', _('Allow everything'), ]),
|
||||
('audit deny all,', [_('Qualifier'), 'audit deny', 'All', _('Allow everything'), ]),
|
||||
)
|
||||
|
||||
def _run_test(self, params, expected):
|
||||
obj = AllRule.create_instance(params)
|
||||
self.assertEqual(obj.logprof_header(), expected)
|
||||
|
||||
|
||||
# --- tests for AllRuleset --- #
|
||||
|
||||
class AllRulesTest(AATest):
|
||||
def test_empty_ruleset(self):
|
||||
ruleset = AllRuleset()
|
||||
ruleset_2 = AllRuleset()
|
||||
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))
|
||||
|
||||
def test_ruleset_1(self):
|
||||
ruleset = AllRuleset()
|
||||
rules = (
|
||||
'all,',
|
||||
'all,',
|
||||
)
|
||||
|
||||
expected_raw = [
|
||||
'all,',
|
||||
'all,',
|
||||
'',
|
||||
]
|
||||
|
||||
expected_clean = [
|
||||
'all,',
|
||||
'all,',
|
||||
'',
|
||||
]
|
||||
|
||||
for rule in rules:
|
||||
ruleset.add(AllRule.create_instance(rule))
|
||||
|
||||
self.assertEqual(expected_raw, ruleset.get_raw())
|
||||
self.assertEqual(expected_clean, ruleset.get_clean())
|
||||
|
||||
def test_ruleset_2(self):
|
||||
ruleset = AllRuleset()
|
||||
rules = (
|
||||
'all,',
|
||||
'allow all,',
|
||||
'deny all, # example comment',
|
||||
)
|
||||
|
||||
expected_raw = [
|
||||
' all,',
|
||||
' allow all,',
|
||||
' deny all, # example comment',
|
||||
'',
|
||||
]
|
||||
|
||||
expected_clean = [
|
||||
' deny all, # example comment',
|
||||
'',
|
||||
' all,',
|
||||
' allow all,',
|
||||
'',
|
||||
]
|
||||
|
||||
for rule in rules:
|
||||
ruleset.add(AllRule.create_instance(rule))
|
||||
|
||||
self.assertEqual(expected_raw, ruleset.get_raw(1))
|
||||
self.assertEqual(expected_clean, ruleset.get_clean(1))
|
||||
|
||||
|
||||
class AllGlobTestAATest(AATest):
|
||||
def setUp(self):
|
||||
self.ruleset = AllRuleset()
|
||||
|
||||
def test_glob(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
# get_glob is not available for all rules
|
||||
self.ruleset.get_glob('all,')
|
||||
|
||||
def test_glob_ext(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
# get_glob_ext is not available for all rules
|
||||
self.ruleset.get_glob_ext('all,')
|
||||
|
||||
|
||||
class AllDeleteTestAATest(AATest):
|
||||
pass
|
||||
|
||||
|
||||
setup_all_loops(__name__)
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=1)
|
@@ -263,14 +263,6 @@ unknown_line = (
|
||||
'file/ok_other_2.sd',
|
||||
'file/ok_other_3.sd',
|
||||
|
||||
# 'all' keyword
|
||||
'all/ok_01.sd',
|
||||
'all/ok_02.sd',
|
||||
'all/ok_03.sd',
|
||||
'all/ok_04.sd',
|
||||
'all/ok_05.sd',
|
||||
'all/ok_06.sd',
|
||||
|
||||
# 'unsafe' keyword
|
||||
'file/file/front_perms_ok_1.sd',
|
||||
'file/front_perms_ok_1.sd',
|
||||
|
Reference in New Issue
Block a user