mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 08:24:42 +01:00
Add BooleanRule and BooleanRuleset
These two classes are meant to handle the definition of boolean rules like `$foo = true`. Also extend RE_PROFILE_BOOLEAN to provide named matches. As usual, add tests for the new classes.
This commit is contained in:
parent
20234d240e
commit
3f11eebc17
3 changed files with 450 additions and 1 deletions
|
@ -37,7 +37,7 @@ RE_PROFILE_END = re.compile('^\s*\}' + RE_EOL)
|
|||
RE_PROFILE_CAP = re.compile(RE_AUDIT_DENY + 'capability(?P<capability>(\s+\S+)+)?' + RE_COMMA_EOL)
|
||||
RE_PROFILE_ALIAS = re.compile('^\s*alias\s+(?P<orig_path>"??.+?"??)\s+->\s*(?P<target>"??.+?"??)' + RE_COMMA_EOL)
|
||||
RE_PROFILE_RLIMIT = re.compile('^\s*set\s+rlimit\s+(?P<rlimit>[a-z]+)\s*<=\s*(?P<value>[^ ]+(\s+[a-zA-Z]+)?)' + RE_COMMA_EOL)
|
||||
RE_PROFILE_BOOLEAN = re.compile('^\s*(\$\{?\w*\}?)\s*=\s*(true|false)\s*,?' + RE_EOL, flags=re.IGNORECASE)
|
||||
RE_PROFILE_BOOLEAN = re.compile('^\s*(?P<varname>\$\{?\w*\}?)\s*=\s*(?P<value>true|false)\s*,?' + RE_EOL, flags=re.IGNORECASE)
|
||||
RE_PROFILE_VARIABLE = re.compile('^\s*(?P<varname>@\{?\w+\}?)\s*(?P<mode>\+?=)\s*(?P<values>@*.+?)' + RE_EOL)
|
||||
RE_PROFILE_CONDITIONAL = re.compile('^\s*if\s+(not\s+)?(\$\{?\w*\}?)\s*\{' + RE_EOL)
|
||||
RE_PROFILE_CONDITIONAL_VARIABLE = re.compile('^\s*if\s+(not\s+)?defined\s+(@\{?\w+\}?)\s*\{\s*(#.*)?$')
|
||||
|
|
134
utils/apparmor/rule/boolean.py
Normal file
134
utils/apparmor/rule/boolean.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
# ----------------------------------------------------------------------
|
||||
# Copyright (C) 2013 Kshitij Gupta <kgupta8592@gmail.com>
|
||||
# Copyright (C) 2020 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_BOOLEAN
|
||||
from apparmor.common import AppArmorBug, AppArmorException, type_is_str
|
||||
from apparmor.rule import BaseRule, BaseRuleset, parse_comment
|
||||
|
||||
# setup module translations
|
||||
from apparmor.translations import init_translation
|
||||
_ = init_translation()
|
||||
|
||||
|
||||
class BooleanRule(BaseRule):
|
||||
'''Class to handle and store a single variable rule'''
|
||||
|
||||
rule_name = 'boolean'
|
||||
|
||||
def __init__(self, varname, value, audit=False, deny=False, allow_keyword=False,
|
||||
comment='', log_event=None):
|
||||
|
||||
super(BooleanRule, self).__init__(audit=audit, deny=deny,
|
||||
allow_keyword=allow_keyword,
|
||||
comment=comment,
|
||||
log_event=log_event)
|
||||
|
||||
# boolean variables don't support audit or deny
|
||||
if audit:
|
||||
raise AppArmorBug('Attempt to initialize %s with audit flag' % self.__class__.__name__)
|
||||
if deny:
|
||||
raise AppArmorBug('Attempt to initialize %s with deny flag' % self.__class__.__name__)
|
||||
|
||||
if not type_is_str(varname):
|
||||
raise AppArmorBug('Passed unknown type for boolean variable to %s: %s' % (self.__class__.__name__, varname))
|
||||
if not varname.startswith('$'):
|
||||
raise AppArmorException("Passed invalid boolean to %s (doesn't start with '$'): %s" % (self.__class__.__name__, varname))
|
||||
|
||||
if not type_is_str(value):
|
||||
raise AppArmorBug('Passed unknown type for value to %s: %s' % (self.__class__.__name__, value))
|
||||
if not value:
|
||||
raise AppArmorException('Passed empty value to %s: %s' % (self.__class__.__name__, value))
|
||||
|
||||
value = value.lower()
|
||||
if value not in ['true', 'false']:
|
||||
raise AppArmorException('Passed invalid value to %s: %s' % (self.__class__.__name__, value))
|
||||
|
||||
self.varname = varname
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def _match(cls, raw_rule):
|
||||
return RE_PROFILE_BOOLEAN.search(raw_rule)
|
||||
|
||||
@classmethod
|
||||
def _parse(cls, raw_rule):
|
||||
'''parse raw_rule and return BooleanRule'''
|
||||
|
||||
matches = cls._match(raw_rule)
|
||||
if not matches:
|
||||
raise AppArmorException(_("Invalid boolean variable rule '%s'") % raw_rule)
|
||||
|
||||
comment = parse_comment(matches)
|
||||
|
||||
varname = matches.group('varname')
|
||||
value = matches.group('value')
|
||||
|
||||
return BooleanRule(varname, value,
|
||||
audit=False, deny=False, allow_keyword=False, comment=comment)
|
||||
|
||||
def get_clean(self, depth=0):
|
||||
'''return rule (in clean/default formatting)'''
|
||||
|
||||
space = ' ' * depth
|
||||
|
||||
return '%s%s = %s' % (space, self.varname, self.value)
|
||||
|
||||
def is_covered_localvars(self, other_rule):
|
||||
'''check if other_rule is covered by this rule object'''
|
||||
|
||||
if self.varname != other_rule.varname:
|
||||
return False
|
||||
|
||||
if not self._is_covered_list(self.value, None, set(other_rule.value), None, 'value'):
|
||||
return False
|
||||
|
||||
# still here? -> then it is covered
|
||||
return True
|
||||
|
||||
def is_equal_localvars(self, rule_obj, strict):
|
||||
'''compare if rule-specific variables are equal'''
|
||||
|
||||
if not type(rule_obj) == BooleanRule:
|
||||
raise AppArmorBug('Passed non-boolean rule: %s' % str(rule_obj))
|
||||
|
||||
if self.varname != rule_obj.varname:
|
||||
return False
|
||||
|
||||
if self.value != rule_obj.value:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def logprof_header_localvars(self):
|
||||
headers = []
|
||||
|
||||
return headers + [
|
||||
_('Boolean Variable'), self.get_clean(),
|
||||
]
|
||||
|
||||
class BooleanRuleset(BaseRuleset):
|
||||
'''Class to handle and store a collection of variable rules'''
|
||||
|
||||
def add(self, rule, cleanup=False):
|
||||
''' Add boolean variable rule object
|
||||
|
||||
If the variable name is already known, raise an exception because re-defining a variable isn't allowed.
|
||||
'''
|
||||
|
||||
for knownrule in self.rules:
|
||||
if rule.varname == knownrule.varname:
|
||||
raise AppArmorException(_('Redefining existing variable %(variable)s: %(value)s') % { 'variable': rule.varname, 'value': rule.value })
|
||||
|
||||
super(BooleanRuleset, self).add(rule, cleanup)
|
315
utils/test/test-boolean.py
Normal file
315
utils/test/test-boolean.py
Normal file
|
@ -0,0 +1,315 @@
|
|||
#!/usr/bin/python3
|
||||
# ----------------------------------------------------------------------
|
||||
# Copyright (C) 2020 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
|
||||
from common_test import AATest, setup_all_loops
|
||||
|
||||
from apparmor.rule.boolean import BooleanRule, BooleanRuleset
|
||||
from apparmor.rule import BaseRule
|
||||
from apparmor.common import AppArmorException, AppArmorBug
|
||||
from apparmor.translations import init_translation
|
||||
_ = init_translation()
|
||||
|
||||
exp = namedtuple('exp', ['comment',
|
||||
'varname', 'value'])
|
||||
|
||||
# --- tests for single BooleanRule --- #
|
||||
|
||||
class BooleanTest(AATest):
|
||||
def _compare_obj(self, obj, expected):
|
||||
# boolean variables don't support the allow, audit or deny keyword
|
||||
self.assertEqual(False, obj.allow_keyword)
|
||||
self.assertEqual(False, obj.audit)
|
||||
self.assertEqual(False, obj.deny)
|
||||
|
||||
self.assertEqual(expected.varname, obj.varname)
|
||||
self.assertEqual(expected.value, obj.value)
|
||||
self.assertEqual(expected.comment, obj.comment)
|
||||
|
||||
class BooleanTestParse(BooleanTest):
|
||||
tests = [
|
||||
# rawrule comment varname value
|
||||
('$foo=true', exp('', '$foo', 'true' )),
|
||||
('$foo = false', exp('', '$foo', 'false' )),
|
||||
('$foo=TrUe', exp('', '$foo', 'true' )),
|
||||
('$foo = FaLsE', exp('', '$foo', 'false' )),
|
||||
(' $foo = true ', exp('', '$foo', 'true' )),
|
||||
(' $foo = true # comment', exp(' # comment', '$foo', 'true' )),
|
||||
]
|
||||
|
||||
def _run_test(self, rawrule, expected):
|
||||
self.assertTrue(BooleanRule.match(rawrule))
|
||||
obj = BooleanRule.parse(rawrule)
|
||||
self.assertEqual(rawrule.strip(), obj.raw_rule)
|
||||
self._compare_obj(obj, expected)
|
||||
|
||||
class BooleanTestParseInvalid(BooleanTest):
|
||||
tests = [
|
||||
# rawrule matches regex exception
|
||||
('$foo =', (False, AppArmorException)),
|
||||
('$ foo = # comment', (False, AppArmorException)),
|
||||
('${foo = ', (False, AppArmorException)),
|
||||
# XXX RE_PROFILE_BOOLEAN allows a trailing comma even if the parser disallows it
|
||||
# ('$foo = true,', (True, AppArmorException)), # trailing comma
|
||||
# ('$foo = false , ', (True, AppArmorException)), # trailing comma
|
||||
# ('$foo = true, # comment', (True, AppArmorException)), # trailing comma
|
||||
]
|
||||
|
||||
def _run_test(self, rawrule, expected):
|
||||
self.assertEqual(BooleanRule.match(rawrule), expected[0])
|
||||
with self.assertRaises(expected[1]):
|
||||
BooleanRule.parse(rawrule)
|
||||
|
||||
class BooleanFromInit(BooleanTest):
|
||||
# tests = [
|
||||
# # BooleanRule object comment varname value
|
||||
# (BooleanRule('$foo', True, exp('', '$foo', True ))),
|
||||
# (BooleanRule('$foo', False, exp('', '$foo', False ))),
|
||||
# (BooleanRule('$foo', True, comment='# cmt'), exp('# cmt', '$foo', True ))),
|
||||
# (BooleanRule('$foo', False, comment='# cmt'), exp('# cmt', '$foo', False ))),
|
||||
# ]
|
||||
|
||||
def _run_test(self, obj, expected):
|
||||
self._compare_obj(obj, expected)
|
||||
|
||||
|
||||
class InvalidBooleanInit(AATest):
|
||||
tests = [
|
||||
# init params expected exception
|
||||
([None, True ], AppArmorBug), # varname not a str
|
||||
(['', True ], AppArmorException), # empty varname
|
||||
(['foo', True ], AppArmorException), # varname not starting with '$'
|
||||
(['foo', True ], AppArmorException), # varname not starting with '$'
|
||||
|
||||
(['$foo', None ], AppArmorBug), # value not a string
|
||||
(['$foo', '' ], AppArmorException), # empty value
|
||||
(['$foo', 'maybe' ], AppArmorException), # invalid value
|
||||
]
|
||||
|
||||
def _run_test(self, params, expected):
|
||||
with self.assertRaises(expected):
|
||||
BooleanRule(params[0], params[1])
|
||||
|
||||
def test_missing_params_1(self):
|
||||
with self.assertRaises(TypeError):
|
||||
BooleanRule()
|
||||
|
||||
def test_missing_params_2(self):
|
||||
with self.assertRaises(TypeError):
|
||||
BooleanRule('$foo')
|
||||
|
||||
def test_invalid_audit(self):
|
||||
with self.assertRaises(AppArmorBug):
|
||||
BooleanRule('$foo', 'true', audit=True)
|
||||
|
||||
def test_invalid_deny(self):
|
||||
with self.assertRaises(AppArmorBug):
|
||||
BooleanRule('$foo', 'true', deny=True)
|
||||
|
||||
|
||||
class InvalidBooleanTest(AATest):
|
||||
def _check_invalid_rawrule(self, rawrule, matches_regex=False):
|
||||
obj = None
|
||||
self.assertEqual(BooleanRule.match(rawrule), matches_regex)
|
||||
with self.assertRaises(AppArmorException):
|
||||
obj = BooleanRule.parse(rawrule)
|
||||
|
||||
self.assertIsNone(obj, 'BooleanRule handed back an object unexpectedly')
|
||||
|
||||
def test_invalid_missing_value(self):
|
||||
self._check_invalid_rawrule('$foo = ', matches_regex=False) # missing value
|
||||
|
||||
def test_invalid_net_non_BooleanRule(self):
|
||||
self._check_invalid_rawrule('dbus,') # not a boolean rule
|
||||
|
||||
|
||||
class WriteBooleanTestAATest(AATest):
|
||||
tests = [
|
||||
# raw rule clean rule
|
||||
(' $foo = true ', '$foo = true'),
|
||||
(' $foo = true # comment', '$foo = true'),
|
||||
(' $foo = false ', '$foo = false'),
|
||||
(' $foo = false # comment', '$foo = false'),
|
||||
]
|
||||
|
||||
def _run_test(self, rawrule, expected):
|
||||
self.assertTrue(BooleanRule.match(rawrule))
|
||||
obj = BooleanRule.parse(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_1(self):
|
||||
obj = BooleanRule('$foo', 'true')
|
||||
|
||||
expected = ' $foo = true'
|
||||
|
||||
self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule')
|
||||
self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule')
|
||||
|
||||
def test_write_manually_2(self):
|
||||
obj = BooleanRule('$foo', 'false')
|
||||
|
||||
expected = ' $foo = false'
|
||||
|
||||
self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule')
|
||||
self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule')
|
||||
|
||||
|
||||
class BooleanCoveredTest(AATest):
|
||||
def _run_test(self, param, expected):
|
||||
obj = BooleanRule.parse(self.rule)
|
||||
check_obj = BooleanRule.parse(param)
|
||||
|
||||
self.assertTrue(BooleanRule.match(param))
|
||||
|
||||
self.assertEqual(obj.is_equal(check_obj), expected[0], 'Mismatch in is_equal, expected %s' % expected[0])
|
||||
self.assertEqual(obj.is_equal(check_obj, True), expected[1], 'Mismatch in is_equal/strict, expected %s' % expected[1])
|
||||
|
||||
self.assertEqual(obj.is_covered(check_obj), expected[2], 'Mismatch in is_covered, expected %s' % expected[2])
|
||||
self.assertEqual(obj.is_covered(check_obj, True, True), expected[3], 'Mismatch in is_covered/exact, expected %s' % expected[3])
|
||||
|
||||
class BooleanCoveredTest_01(BooleanCoveredTest):
|
||||
rule = '$foo = true'
|
||||
|
||||
tests = [
|
||||
# rule equal strict equal covered covered exact
|
||||
(' $foo = true' , [ True , True , True , True ]),
|
||||
(' $foo = TRUE' , [ True , False , True , True ]), # upper vs. lower case
|
||||
(' $foo = true # comment' , [ True , False , True , True ]),
|
||||
(' $foo = false' , [ False , False , False , False ]),
|
||||
(' $foo = false # cmt' , [ False , False , False , False ]),
|
||||
(' $bar = true' , [ False , False , False , False ]), # different variable name
|
||||
]
|
||||
|
||||
class BooleanCoveredTest_02(BooleanCoveredTest):
|
||||
rule = '$foo = false'
|
||||
|
||||
tests = [
|
||||
# rule equal strict equal covered covered exact
|
||||
(' $foo = false' , [ True , True , True , True ]),
|
||||
(' $foo = false # comment' , [ True , False , True , True ]),
|
||||
(' $foo = true' , [ False , False , False , False ]),
|
||||
(' $foo = true # cmt' , [ False , False , False , False ]),
|
||||
(' $bar = false' , [ False , False , False , False ]), # different variable name
|
||||
]
|
||||
|
||||
class BooleanCoveredTest_Invalid(AATest):
|
||||
def test_borked_obj_is_covered_2(self):
|
||||
obj = BooleanRule.parse('$foo = true')
|
||||
|
||||
testobj = BooleanRule('$foo', 'true')
|
||||
testobj.value = ''
|
||||
|
||||
with self.assertRaises(AppArmorBug):
|
||||
obj.is_covered(testobj)
|
||||
|
||||
def test_invalid_is_covered_3(self):
|
||||
obj = BooleanRule.parse('$foo = true')
|
||||
|
||||
testobj = BaseRule() # different type
|
||||
|
||||
with self.assertRaises(AppArmorBug):
|
||||
obj.is_covered(testobj)
|
||||
|
||||
def test_invalid_is_equal(self):
|
||||
obj = BooleanRule.parse('$foo = true')
|
||||
|
||||
testobj = BaseRule() # different type
|
||||
|
||||
with self.assertRaises(AppArmorBug):
|
||||
obj.is_equal(testobj)
|
||||
|
||||
class BooleanLogprofHeaderTest(AATest):
|
||||
tests = [
|
||||
('$foo = true', [_('Boolean Variable'), '$foo = true' ]),
|
||||
]
|
||||
|
||||
def _run_test(self, params, expected):
|
||||
obj = BooleanRule._parse(params)
|
||||
self.assertEqual(obj.logprof_header(), expected)
|
||||
|
||||
# --- tests for BooleanRuleset --- #
|
||||
|
||||
class BooleanRulesTest(AATest):
|
||||
def test_empty_ruleset(self):
|
||||
ruleset = BooleanRuleset()
|
||||
ruleset_2 = BooleanRuleset()
|
||||
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 = BooleanRuleset()
|
||||
rules = [
|
||||
'$foo = true',
|
||||
'$baz= false',
|
||||
]
|
||||
|
||||
expected_raw = [
|
||||
'$foo = true',
|
||||
'$baz= false',
|
||||
'',
|
||||
]
|
||||
|
||||
expected_clean = [
|
||||
'$baz = false',
|
||||
'$foo = true',
|
||||
'',
|
||||
]
|
||||
|
||||
expected_clean_unsorted = [
|
||||
'$foo = true',
|
||||
'$baz = false',
|
||||
'',
|
||||
]
|
||||
|
||||
for rule in rules:
|
||||
ruleset.add(BooleanRule.parse(rule))
|
||||
|
||||
self.assertEqual(expected_raw, ruleset.get_raw())
|
||||
self.assertEqual(expected_clean, ruleset.get_clean())
|
||||
self.assertEqual(expected_clean_unsorted, ruleset.get_clean_unsorted())
|
||||
|
||||
def test_ruleset_overwrite(self):
|
||||
ruleset = BooleanRuleset()
|
||||
|
||||
ruleset.add(BooleanRule.parse('$foo = true'))
|
||||
with self.assertRaises(AppArmorException):
|
||||
ruleset.add(BooleanRule.parse('$foo = false')) # attempt to redefine @{foo}
|
||||
|
||||
class BooleanGlobTestAATest(AATest):
|
||||
def setUp(self):
|
||||
self.ruleset = BooleanRuleset()
|
||||
|
||||
# def test_glob_1(self):
|
||||
# with self.assertRaises(NotImplementedError):
|
||||
# self.ruleset.get_glob('$foo = true')
|
||||
|
||||
def test_glob_ext(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
# get_glob_ext is not available for boolean rules
|
||||
self.ruleset.get_glob_ext('$foo = true')
|
||||
|
||||
class BooleanDeleteTestAATest(AATest):
|
||||
pass
|
||||
|
||||
setup_all_loops(__name__)
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=1)
|
Loading…
Add table
Reference in a new issue