mirror of
https://gitlab.com/apparmor/apparmor.git
synced 2025-03-04 08:24:42 +01:00
422 lines
17 KiB
Python
422 lines
17 KiB
Python
#!/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.common import AppArmorBug, AppArmorException
|
|
from apparmor.rule.variable import VariableRule, VariableRuleset, separate_vars
|
|
from apparmor.translations import init_translation
|
|
|
|
_ = init_translation()
|
|
|
|
exp = namedtuple('exp', ('comment', 'varname', 'mode', 'values'))
|
|
|
|
# --- tests for single VariableRule --- #
|
|
|
|
|
|
class VariableTest(AATest):
|
|
def _compare_obj(self, obj, expected):
|
|
# 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.mode, obj.mode)
|
|
self.assertEqual(expected.values, obj.values)
|
|
self.assertEqual(expected.comment, obj.comment)
|
|
|
|
|
|
class AaTest_separate_vars(AATest):
|
|
tests = (
|
|
('', set()),
|
|
(' ', set()),
|
|
(' foo bar', {'foo', 'bar'}),
|
|
('foo " ', AppArmorException),
|
|
(' " foo ', AppArmorException), # half-quoted
|
|
(' foo bar ', {'foo', 'bar'}),
|
|
(' foo bar # comment', {'foo', 'bar', '#', 'comment'}), # XXX should comments be stripped?
|
|
('foo', {'foo'}),
|
|
('"foo" "bar baz"', {'foo', 'bar baz'}),
|
|
('foo "bar baz" xy', {'foo', 'bar baz', 'xy'}),
|
|
('foo "bar baz ', AppArmorException), # half-quoted
|
|
(' " foo" bar', {' foo', 'bar'}),
|
|
(' " foo" bar x', {' foo', 'bar', 'x'}),
|
|
('""', {''}), # empty value
|
|
('"" foo', {'', 'foo'}), # empty value + 'foo'
|
|
('"" foo "bar"', {'', 'foo', 'bar'}), # empty value + 'foo' + 'bar' (bar has superfluous quotes)
|
|
('"bar"', {'bar'}), # 'bar' with superfluous quotes
|
|
)
|
|
|
|
def _run_test(self, params, expected):
|
|
if expected == AppArmorException:
|
|
with self.assertRaises(expected):
|
|
separate_vars(params)
|
|
else:
|
|
result = separate_vars(params)
|
|
self.assertEqual(result, expected)
|
|
|
|
|
|
class VariableTestParse(VariableTest):
|
|
tests = (
|
|
# rawrule comment varname mode values
|
|
('@{foo}=/bar', exp('', '@{foo}', '=', {'/bar'})),
|
|
('@{foo}+=/bar', exp('', '@{foo}', '+=', {'/bar'})),
|
|
(' @{foo} = /bar ', exp('', '@{foo}', '=', {'/bar'})),
|
|
(' @{foo} += /bar', exp('', '@{foo}', '+=', {'/bar'})),
|
|
(' @{foo} = /bar # comment', exp(' # comment', '@{foo}', '=', {'/bar'})),
|
|
(' @{foo} += /bar # comment', exp(' # comment', '@{foo}', '+=', {'/bar'})),
|
|
('@{foo}=/bar /baz', exp('', '@{foo}', '=', {'/bar', '/baz'})),
|
|
('@{foo} = "/bar," # comment', exp(' # comment', '@{foo}', '=', {'/bar,'})), # value with trailing comma, needs to be quoted
|
|
)
|
|
|
|
def _run_test(self, rawrule, expected):
|
|
self.assertTrue(VariableRule.match(rawrule))
|
|
obj = VariableRule.create_instance(rawrule)
|
|
self.assertEqual(rawrule.strip(), obj.raw_rule)
|
|
self._compare_obj(obj, expected)
|
|
|
|
|
|
class VariableTestParseInvalid(VariableTest):
|
|
tests = (
|
|
# rawrule matches regex exception
|
|
('@{foo} =', (False, AppArmorException)),
|
|
('@ {foo} = # comment', (False, AppArmorException)),
|
|
('@ {foo} = ', (False, AppArmorException)),
|
|
('@{foo} = /foo,', (True, AppArmorException)), # trailing comma
|
|
('@{foo} = /foo, ', (True, AppArmorException)), # trailing comma
|
|
('@{foo} = /foo, # comment', (True, AppArmorException)), # trailing comma
|
|
('@{foo} = /foo, /bar', (True, AppArmorException)), # trailing comma in first value
|
|
('@{foo = /foo f', (True, AppArmorException)), # variable name broken, missing }
|
|
)
|
|
|
|
def _run_test(self, rawrule, expected):
|
|
self.assertEqual(VariableRule.match(rawrule), expected[0])
|
|
with self.assertRaises(expected[1]):
|
|
VariableRule.create_instance(rawrule)
|
|
|
|
|
|
class VariableFromInit(VariableTest):
|
|
tests = (
|
|
# VariableRule object comment varname mode values
|
|
(VariableRule('@{foo}', '=', {'/bar'}), exp('', '@{foo}', '=', {'/bar'})),
|
|
(VariableRule('@{foo}', '+=', {'/bar'}), exp('', '@{foo}', '+=', {'/bar'})),
|
|
(VariableRule('@{foo}', '=', {'/bar', '/baz'}), exp('', '@{foo}', '=', {'/bar', '/baz'})),
|
|
(VariableRule('@{foo}', '+=', {'/bar', '/baz'}), exp('', '@{foo}', '+=', {'/bar', '/baz'})),
|
|
(VariableRule('@{foo}', '=', {'/bar'}, comment='# cmt'), exp('# cmt', '@{foo}', '=', {'/bar'})),
|
|
(VariableRule('@{foo}', '+=', {'/bar'}, comment='# cmt'), exp('# cmt', '@{foo}', '+=', {'/bar'})),
|
|
)
|
|
|
|
def _run_test(self, obj, expected):
|
|
self._compare_obj(obj, expected)
|
|
|
|
|
|
class InvalidVariableInit(AATest):
|
|
tests = (
|
|
# init params expected exception
|
|
((None, '=', ['/bar']), AppArmorBug), # varname not a str
|
|
(('', '=', ['/bar']), AppArmorException), # empty varname
|
|
(('foo', '=', ['/bar']), AppArmorException), # varname not starting with '@{'
|
|
(('foo', '=', ['/bar']), AppArmorException), # varname not starting with '@{'
|
|
|
|
(('@{foo}', '', ['/bar']), AppArmorBug), # mode not '=' or '+='
|
|
(('@{foo}', '-=', ['/bar']), AppArmorBug), # mode not '=' or '+='
|
|
(('@{foo}', ' ', ['/bar']), AppArmorBug), # mode not '=' or '+='
|
|
(('@{foo}', None, ['/bar']), AppArmorBug), # mode not '=' or '+='
|
|
|
|
(('@{foo}', '=', None), AppArmorBug), # values not a set
|
|
(('@{foo}', '=', set()), AppArmorException), # empty values
|
|
)
|
|
|
|
def _run_test(self, params, expected):
|
|
with self.assertRaises(expected):
|
|
VariableRule(*params)
|
|
|
|
def test_missing_params_1(self):
|
|
with self.assertRaises(TypeError):
|
|
VariableRule()
|
|
|
|
def test_missing_params_2(self):
|
|
with self.assertRaises(TypeError):
|
|
VariableRule('@{foo}')
|
|
|
|
def test_missing_params_3(self):
|
|
with self.assertRaises(TypeError):
|
|
VariableRule('@{foo}', '=')
|
|
|
|
def test_invalid_audit(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
VariableRule('@{foo}', '=', '/bar', audit=True)
|
|
|
|
def test_invalid_deny(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
VariableRule('@{foo}', '=', '/bar', deny=True)
|
|
|
|
|
|
class InvalidVariableTest(AATest):
|
|
def _check_invalid_rawrule(self, rawrule, matches_regex=False):
|
|
obj = None
|
|
self.assertEqual(VariableRule.match(rawrule), matches_regex)
|
|
with self.assertRaises(AppArmorException):
|
|
obj = VariableRule.create_instance(rawrule)
|
|
|
|
self.assertIsNone(obj, 'VariableRule handed back an object unexpectedly')
|
|
|
|
def test_invalid_missing_values(self):
|
|
self._check_invalid_rawrule('@{foo} = ', matches_regex=True) # missing values
|
|
|
|
def test_invalid_net_non_VariableRule(self):
|
|
self._check_invalid_rawrule('dbus,') # not a variable rule
|
|
|
|
|
|
class WriteVariableTestAATest(AATest):
|
|
tests = (
|
|
# raw rule clean rule
|
|
(' @{foo} = /bar ', '@{foo} = /bar'),
|
|
(' @{foo} = /bar # comment', '@{foo} = /bar'),
|
|
(' @{foo} = /bar ""', '@{foo} = "" /bar'),
|
|
(' @{foo} += /bar ', '@{foo} += /bar'),
|
|
(' @{foo} += /bar # comment', '@{foo} += /bar'),
|
|
(' @{foo} += /bar /baz', '@{foo} += /bar /baz'),
|
|
(' @{foo} += /bar /baz', '@{foo} += /bar /baz'),
|
|
(' @{foo} += /bar @{baz}', '@{foo} += /bar @{baz}'),
|
|
(' @{foo} += /bar @{baz}', '@{foo} += /bar @{baz}'),
|
|
)
|
|
|
|
def _run_test(self, rawrule, expected):
|
|
self.assertTrue(VariableRule.match(rawrule))
|
|
obj = VariableRule.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_1(self):
|
|
obj = VariableRule('@{foo}', '=', {'/bar'})
|
|
|
|
expected = ' @{foo} = /bar'
|
|
|
|
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 = VariableRule('@{foo}', '=', {'/bar', ''})
|
|
|
|
expected = ' @{foo} = "" /bar'
|
|
|
|
self.assertEqual(expected, obj.get_clean(2), 'unexpected clean rule')
|
|
self.assertEqual(expected, obj.get_raw(2), 'unexpected raw rule')
|
|
|
|
|
|
class VariableCoveredTest(AATest):
|
|
def _run_test(self, param, expected):
|
|
obj = VariableRule.create_instance(self.rule)
|
|
check_obj = VariableRule.create_instance(param)
|
|
|
|
self.assertTrue(VariableRule.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 VariableCoveredTest_01(VariableCoveredTest):
|
|
rule = '@{foo} = /bar'
|
|
|
|
tests = (
|
|
# rule equal strict equal covered covered exact
|
|
(' @{foo} = /bar', (True, True, True, True)),
|
|
(' @{foo} += /bar', (False, False, False, False)),
|
|
(' @{foo} = /bar # comment', (True, False, True, True)),
|
|
(' @{foo} += /bar # comment', (False, False, False, False)),
|
|
(' @{foo} = /baz /bar', (False, False, False, False)),
|
|
(' @{foo} += /baz /bar', (False, False, False, False)),
|
|
(' @{foo} = /baz /bar # cmt', (False, False, False, False)),
|
|
(' @{foo} += /baz /bar # cmt', (False, False, False, False)),
|
|
(' @{bar} = /bar', (False, False, False, False)), # different variable name
|
|
)
|
|
|
|
|
|
class VariableCoveredTest_02(VariableCoveredTest):
|
|
rule = '@{foo} = /bar /baz'
|
|
|
|
tests = (
|
|
# rule equal strict equal covered covered exact
|
|
(' @{foo} = /bar /baz', (True, True, True, True)),
|
|
(' @{foo} += /bar /baz', (False, False, False, False)),
|
|
(' @{foo} = /bar /baz # cmt', (True, False, True, True)),
|
|
(' @{foo} += /bar /baz # cmt', (False, False, False, False)),
|
|
# changed order of values
|
|
(' @{foo} = /baz /bar', (True, False, True, True)),
|
|
(' @{foo} += /baz /bar', (False, False, False, False)),
|
|
(' @{foo} = /baz /bar # cmt', (True, False, True, True)),
|
|
(' @{foo} += /baz /bar # cmt', (False, False, False, False)),
|
|
# only one value
|
|
(' @{foo} = /bar', (False, False, True, True)),
|
|
(' @{foo} += /bar', (False, False, False, False)),
|
|
(' @{foo} = /bar # comment', (False, False, True, True)),
|
|
(' @{foo} += /bar # comment', (False, False, False, False)),
|
|
(' @{bar} = /bar', (False, False, False, False)), # different variable name
|
|
)
|
|
|
|
|
|
class VariableCoveredTest_Invalid(AATest):
|
|
# def test_borked_obj_is_covered_1(self):
|
|
# obj = VariableRule.create_instance('@{foo} = /bar')
|
|
#
|
|
# testobj = VariableRule('@{foo}', '=', '/bar')
|
|
# testobj.mode = ''
|
|
#
|
|
# with self.assertRaises(AppArmorBug):
|
|
# obj.is_covered(testobj)
|
|
|
|
def test_borked_obj_is_covered_2(self):
|
|
obj = VariableRule.create_instance('@{foo} = /bar')
|
|
|
|
testobj = VariableRule('@{foo}', '=', {'/bar'})
|
|
testobj.values = ''
|
|
|
|
with self.assertRaises(AppArmorBug):
|
|
obj.is_covered(testobj)
|
|
|
|
def test_invalid_is_covered_3(self):
|
|
raw_rule = '@{foo} = /bar'
|
|
class SomeOtherClass(VariableRule):
|
|
pass
|
|
|
|
obj = VariableRule.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 = '@{foo} = /bar'
|
|
class SomeOtherClass(VariableRule):
|
|
pass
|
|
|
|
obj = VariableRule.create_instance(raw_rule)
|
|
testobj = SomeOtherClass.create_instance(raw_rule) # different type
|
|
with self.assertRaises(AppArmorBug):
|
|
obj.is_equal(testobj)
|
|
|
|
|
|
class VariableLogprofHeaderTest(AATest):
|
|
tests = (
|
|
('@{foo} = /bar', [_('Variable'), '@{foo} = /bar']),
|
|
)
|
|
|
|
def _run_test(self, params, expected):
|
|
obj = VariableRule.create_instance(params)
|
|
self.assertEqual(obj.logprof_header(), expected)
|
|
|
|
# --- tests for VariableRuleset --- #
|
|
|
|
|
|
class VariableRulesTest(AATest):
|
|
def test_empty_ruleset(self):
|
|
ruleset = VariableRuleset()
|
|
ruleset_2 = VariableRuleset()
|
|
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))
|
|
self.assertEqual({'=': {}, '+=': {}}, ruleset_2.get_merged_variables())
|
|
|
|
def test_ruleset_1(self):
|
|
ruleset = VariableRuleset()
|
|
rules = (
|
|
'@{foo} = /bar',
|
|
'@{baz}= /asdf',
|
|
'@{foo} += /whatever',
|
|
'@{foo} += /morestuff',
|
|
)
|
|
|
|
expected_raw = [
|
|
'@{foo} = /bar',
|
|
'@{baz}= /asdf',
|
|
'@{foo} += /whatever',
|
|
'@{foo} += /morestuff',
|
|
'',
|
|
]
|
|
|
|
expected_clean = [
|
|
'@{baz} = /asdf',
|
|
'@{foo} += /morestuff',
|
|
'@{foo} += /whatever',
|
|
'@{foo} = /bar',
|
|
'',
|
|
]
|
|
|
|
expected_clean_unsorted = [
|
|
'@{foo} = /bar',
|
|
'@{baz} = /asdf',
|
|
'@{foo} += /whatever',
|
|
'@{foo} += /morestuff',
|
|
'',
|
|
]
|
|
|
|
expected_merged = {
|
|
'=': {
|
|
'@{foo}': {'/bar'},
|
|
'@{baz}': {'/asdf'},
|
|
},
|
|
'+=': {
|
|
'@{foo}': {'/whatever', '/morestuff'},
|
|
}
|
|
}
|
|
|
|
for rule in rules:
|
|
ruleset.add(VariableRule.create_instance(rule))
|
|
|
|
self.assertEqual(expected_raw, ruleset.get_raw())
|
|
self.assertEqual(expected_clean, ruleset.get_clean())
|
|
self.assertEqual(expected_clean_unsorted, ruleset.get_clean_unsorted())
|
|
self.assertEqual(expected_merged, ruleset.get_merged_variables())
|
|
|
|
def test_ruleset_overwrite(self):
|
|
ruleset = VariableRuleset()
|
|
|
|
ruleset.add(VariableRule.create_instance('@{foo} = /bar'))
|
|
with self.assertRaises(AppArmorException):
|
|
ruleset.add(VariableRule.create_instance('@{foo} = /asdf')) # attempt to redefine @{foo}
|
|
self.assertEqual({'=': {'@{foo}': {'/bar'}}, '+=': {}}, ruleset.get_merged_variables())
|
|
|
|
|
|
class VariableGlobTestAATest(AATest):
|
|
def setUp(self):
|
|
self.ruleset = VariableRuleset()
|
|
|
|
# def test_glob_1(self):
|
|
# with self.assertRaises(NotImplementedError):
|
|
# self.ruleset.get_glob('@{foo} = /bar')
|
|
|
|
def test_glob_ext(self):
|
|
with self.assertRaises(NotImplementedError):
|
|
# get_glob_ext is not available for change_profile rules
|
|
self.ruleset.get_glob_ext('@{foo} = /bar')
|
|
|
|
|
|
class VariableDeleteTestAATest(AATest):
|
|
pass
|
|
|
|
|
|
setup_all_loops(__name__)
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=1)
|