mirror of
https://gitlab.com/apparmor/apparmor
synced 2025-08-22 10:07:12 +00:00
403 lines
22 KiB
Python
403 lines
22 KiB
Python
#!/usr/bin/python3
|
|
# ----------------------------------------------------------------------
|
|
# Copyright (C) 2025 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 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 apparmor.common import AppArmorException, AppArmorBug, combine_profname
|
|
from apparmor.profile_storage import ProfileStorage
|
|
from apparmor.profile_list import ProfileList
|
|
from apparmor.rule.file import FileRule
|
|
from apparmor.rule.variable import VariableRule
|
|
from apparmor.rule.boolean import BooleanRule
|
|
from apparmor.rule.conditional import ConditionalBlock, AppArmorAst, Term, CompareCondition, ConditionalRule, BooleanCondition
|
|
from common_test import AATest, setup_all_loops
|
|
|
|
|
|
class TestConditional(AATest):
|
|
""" Base class to test conditionals, profile_data does not contain any rules """
|
|
filename = 'somefile'
|
|
condition_contents = '\n'
|
|
|
|
tests = (
|
|
# ConditionalBlock clean rule
|
|
(['if $FOO {', '} else if $BAR {', '} else {'], 'if $FOO {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not $FOO {', '} else if not $BAR {', '} else {'], 'if not $FOO {' + condition_contents + '} else if not $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if defined $FOO {', '} else if not defined $BAR {', '} else {'], 'if defined $FOO {' + condition_contents + '} else if not defined $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {', '} else if defined @{VAR1} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else if defined @{VAR1} {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {'], 'if not defined @{VAR2} {' + condition_contents + '}'),
|
|
)
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestConditional, cls).setUpClass()
|
|
cls.active_profiles = ProfileList()
|
|
cls.profile_data = {}
|
|
cls.condition_contents = '\n'
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
|
|
cls.profile_data[profile] = prof_storage
|
|
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR1} = "test"'))
|
|
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR2} = "test1 test2"'))
|
|
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR3} = 10'))
|
|
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$FOO=true'))
|
|
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$BAR=false'))
|
|
|
|
def _run_test(self, params, expected):
|
|
conditional_block = None
|
|
for condition in params:
|
|
if condition == params[0]:
|
|
conditional_block = ConditionalBlock(condition, self.active_profiles.files[self.filename])
|
|
conditional_block.store_profile_data(self.profile_data)
|
|
else:
|
|
conditional_block.add_conditional(condition, self.active_profiles.files[self.filename])
|
|
conditional_block.store_profile_data(self.profile_data)
|
|
|
|
self.assertEqual(conditional_block.get_clean(), expected)
|
|
|
|
|
|
class TestConditionalComplex(TestConditional):
|
|
""" Class to test complex conditionals (and, ors, comparisons and boolean operations), profile_data contains one file rule """
|
|
# directly related to how setUpClass sets up profile_data
|
|
condition_contents = '\n /bin/false rix,\n\n'
|
|
|
|
tests = (
|
|
# ConditionalBlock clean rule
|
|
(['if $FOO and ($BAR) {', '} else if $BAR {', '} else {'], 'if $FOO and $BAR {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if $FOO and ($BAR or defined @{VAR1}) {', '} else if $BAR {', '} else {'], 'if $FOO and ($BAR or defined @{VAR1}) {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if $FOO or ($BAR or defined @{VAR1}) {', '} else if $BAR {', '} else {'], 'if $FOO or $BAR or defined @{VAR1} {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if $FOO or ($BAR and defined @{VAR1}) {', '} else if $BAR {', '} else {'], 'if $FOO or $BAR and defined @{VAR1} {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not $FOO {', '} else if not not $BAR {', '} else {'], 'if not $FOO {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if ((test in @{VAR1})) and defined $FOO {'], 'if test in @{VAR1} and defined $FOO {' + condition_contents + '}'),
|
|
(['if test3 in @{VAR2} {'], 'if test3 in @{VAR2} {' + condition_contents + '}'),
|
|
(['if 9 < @{VAR3} {'], 'if 9 < @{VAR3} {' + condition_contents + '}'),
|
|
(['if test in "test2 test" {'], 'if test in "test2 test" {' + condition_contents + '}'),
|
|
)
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestConditionalComplex, cls).setUpClass()
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
|
|
cls.profile_data[profile] = prof_storage
|
|
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
|
|
|
|
|
|
class TestConditionalSimple(TestConditional):
|
|
""" Class to test simple conditionals (boolean and defined operations), profile_data contains one file rule """
|
|
# directly related to how setUpClass sets up profile_data
|
|
condition_contents = '\n /bin/false rix,\n\n'
|
|
|
|
tests = (
|
|
# ConditionalBlock clean rule
|
|
(['if $FOO {', '} else if $BAR {', '} else {'], 'if $FOO {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not $FOO {', '} else if not $BAR {', '} else {'], 'if not $FOO {' + condition_contents + '} else if not $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if defined $FOO {', '} else if not defined ${BAR} {', '} else {'], 'if defined $FOO {' + condition_contents + '} else if not defined ${BAR} {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {', '} else if defined @{VAR1} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else if defined @{VAR1} {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {'], 'if not defined @{VAR2} {' + condition_contents + '}'),
|
|
)
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestConditionalSimple, cls).setUpClass()
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
|
|
cls.profile_data[profile] = prof_storage
|
|
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
|
|
|
|
|
|
class TestConditionalSubProfile(TestConditional):
|
|
""" Class to test simple conditionals, profile_data contains hat definition """
|
|
# directly related to how setUpClass sets up profile_data
|
|
condition_contents = '\n /** w,\n /bin/false rix,\n\n ^bar {\n /bin/true rix,\n\n }\n'
|
|
|
|
tests = (
|
|
# ConditionalBlock clean rule
|
|
(['if $FOO {', '} else if $BAR {', '} else {'], 'if $FOO {' + condition_contents + '} else if $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not $FOO {', '} else if not $BAR {', '} else {'], 'if not $FOO {' + condition_contents + '} else if not $BAR {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if defined $FOO {', '} else if not defined ${BAR} {', '} else {'], 'if defined $FOO {' + condition_contents + '} else if not defined ${BAR} {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {', '} else if defined @{VAR1} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else if defined @{VAR1} {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {', '} else {'], 'if not defined @{VAR2} {' + condition_contents + '} else {' + condition_contents + '}'),
|
|
(['if not defined @{VAR2} {'], 'if not defined @{VAR2} {' + condition_contents + '}'),
|
|
)
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestConditionalSubProfile, cls).setUpClass()
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
|
|
cls.profile_data[profile] = prof_storage
|
|
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
|
|
cls.profile_data[profile]['file'].add(FileRule.create_instance('/** w,'))
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('^bar {', cls.filename, 0, profile, None)
|
|
profname = combine_profname((profile, hat))
|
|
cls.profile_data[profname] = prof_storage
|
|
cls.profile_data[profname]['in_cond'] = True
|
|
cls.profile_data[profname]['file'].add(FileRule.create_instance('/bin/true rix,'))
|
|
|
|
|
|
class TestConditionalLogprof(TestConditional):
|
|
""" Class to test output of Logprof """
|
|
# directly related to how setUpClass sets up profile_data
|
|
condition_contents = '\n /** w,\n /bin/false rix,\n\n ^bar {\n /bin/true rix,\n\n }\n'
|
|
|
|
tests = (
|
|
(['if $FOO {', '} else if $BAR {', '} else {'],
|
|
['ConditionalBlock', [['Conditional', 'if $FOO {' + condition_contents + '}'],
|
|
['Conditional', 'else if $BAR {' + condition_contents + '}'],
|
|
['Conditional', 'else {' + condition_contents + '}']]]),
|
|
(['if not $FOO {', '} else if not $BAR {', '} else {'],
|
|
['ConditionalBlock', [['Conditional', 'if not $FOO {' + condition_contents + '}'],
|
|
['Conditional', 'else if not $BAR {' + condition_contents + '}'],
|
|
['Conditional', 'else {' + condition_contents + '}']]]),
|
|
(['if defined $FOO {', '} else if not defined $BAR {', '} else {'],
|
|
['ConditionalBlock', [['Conditional', 'if defined $FOO {' + condition_contents + '}'],
|
|
['Conditional', 'else if not defined $BAR {' + condition_contents + '}'],
|
|
['Conditional', 'else {' + condition_contents + '}']]]),
|
|
(['if not defined @{VAR2} {', '} else if defined @{VAR1} {', '} else {'],
|
|
['ConditionalBlock', [['Conditional', 'if not defined @{VAR2} {' + condition_contents + '}'],
|
|
['Conditional', 'else if defined @{VAR1} {' + condition_contents + '}'],
|
|
['Conditional', 'else {' + condition_contents + '}']]]),
|
|
(['if not defined @{VAR2} {', '} else {'],
|
|
['ConditionalBlock', [['Conditional', 'if not defined @{VAR2} {' + condition_contents + '}'],
|
|
['Conditional', 'else {' + condition_contents + '}']]]),
|
|
(['if not defined @{VAR2} {'],
|
|
['ConditionalBlock', [['Conditional', 'if not defined @{VAR2} {' + condition_contents + '}']]]),
|
|
)
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestConditionalLogprof, cls).setUpClass()
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
|
|
cls.profile_data[profile] = prof_storage
|
|
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
|
|
cls.profile_data[profile]['file'].add(FileRule.create_instance('/** w,'))
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('^bar {', cls.filename, 0, profile, None)
|
|
profname = combine_profname((profile, hat))
|
|
cls.profile_data[profname] = prof_storage
|
|
cls.profile_data[profname]['in_cond'] = True
|
|
cls.profile_data[profname]['file'].add(FileRule.create_instance('/bin/true rix,'))
|
|
|
|
def _run_test(self, params, expected):
|
|
conditional_block = None
|
|
for condition in params:
|
|
if condition == params[0]:
|
|
conditional_block = ConditionalBlock(condition, self.active_profiles.files[self.filename])
|
|
conditional_block.store_profile_data(self.profile_data)
|
|
else:
|
|
conditional_block.add_conditional(condition, self.active_profiles.files[self.filename])
|
|
conditional_block.store_profile_data(self.profile_data)
|
|
|
|
self.assertEqual(conditional_block.logprof_header(), expected)
|
|
|
|
|
|
class TestConditionalEquality(AATest):
|
|
""" Class to test equality tests """
|
|
filename = 'somefile'
|
|
profile_data = {}
|
|
|
|
tests = (
|
|
# should always be different
|
|
# first condition second condition
|
|
((['if not $FOO {'], profile_data), (['if $FOO {'], profile_data)),
|
|
((['if defined $FOO {'], profile_data), (['if $FOO {'], profile_data)),
|
|
((['if $BAR {'], profile_data), (['if $FOO {'], profile_data)),
|
|
((['if $FOO {'], None), (['if $FOO {'], profile_data)),
|
|
((['if $FOO {', '} else if $BAR {'], profile_data), (['if $FOO {', '} else {'], profile_data)),
|
|
((['if $FOO and $BAR {'], profile_data), (['if $FOO and $BAR and defined $BAZ{'], profile_data)),
|
|
)
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestConditionalEquality, cls).setUpClass()
|
|
cls.active_profiles = ProfileList()
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
|
|
cls.profile_data[profile] = prof_storage
|
|
cls.profile_data[profile]['file'].add(FileRule.create_instance('/bin/false rix,'))
|
|
cls.profile_data[profile]['file'].add(FileRule.create_instance('/** w,'))
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('^bar {', cls.filename, 0, profile, None)
|
|
profname = combine_profname((profile, hat))
|
|
cls.profile_data[profname] = prof_storage
|
|
cls.profile_data[profname]['in_cond'] = True
|
|
cls.profile_data[profname]['file'].add(FileRule.create_instance('/bin/true rix,'))
|
|
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR1} = "test"'))
|
|
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$FOO=true'))
|
|
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$BAR=false'))
|
|
|
|
def init_cond(self, conds, profile_data):
|
|
block = None
|
|
for cond in conds:
|
|
if cond == conds[0]:
|
|
block = ConditionalBlock(cond, self.active_profiles.files[self.filename])
|
|
block.store_profile_data(profile_data)
|
|
else:
|
|
block.add_conditional(cond, self.active_profiles.files[self.filename])
|
|
block.store_profile_data(profile_data)
|
|
return block
|
|
|
|
def _run_test(self, cond1, cond2):
|
|
conditional_block1 = self.init_cond(cond1[0], cond1[1])
|
|
conditional_block2 = self.init_cond(cond2[0], cond2[1])
|
|
|
|
self.assertFalse(conditional_block1.is_equal(conditional_block2))
|
|
self.assertFalse(conditional_block1.is_covered(conditional_block2))
|
|
|
|
def test_is_equal_condition(self):
|
|
conditional_block1 = self.init_cond(['if $FOO {', '} else if $BAR {', '} else {'], self.profile_data)
|
|
conditional_block2 = self.init_cond(['if $FOO {', '} else {'], self.profile_data)
|
|
|
|
self.assertFalse(conditional_block1.is_equal(conditional_block2))
|
|
self.assertFalse(conditional_block1.is_covered(conditional_block2))
|
|
|
|
def test_is_equal(self):
|
|
conditional_block1 = ConditionalBlock('if not defined $FOO {', self.active_profiles.files[self.filename])
|
|
conditional_block1.store_profile_data(self.profile_data)
|
|
|
|
self.assertTrue(conditional_block1.is_equal(conditional_block1))
|
|
self.assertTrue(conditional_block1.is_covered(conditional_block1))
|
|
|
|
|
|
class TestConditionalException(AATest):
|
|
filename = 'somefile'
|
|
profile_data = {}
|
|
|
|
tests = (
|
|
(['if @{VAR1} {', '} else if $BAR {', '} else {'], AppArmorException), # set variable used in boolean comparison
|
|
(['if $FOO {', '} else if @{VAR} {', '} else {'], AppArmorException), # undefined variable in else if
|
|
(['if ${BAZ} {', '} else {'], AppArmorException), # undefined boolean variable with else
|
|
(['if ${VAR} {'], AppArmorException), # undefined boolean variable
|
|
(['if foo in @{UNDERFINED} {'], AppArmorException), # undefined set variable
|
|
)
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(TestConditionalException, cls).setUpClass()
|
|
cls.active_profiles = ProfileList()
|
|
cls.condition_contents = '\n'
|
|
(profile, hat, prof_storage) = ProfileStorage.parse('profile foo {', cls.filename, 0, False, False)
|
|
cls.profile_data[profile] = prof_storage
|
|
cls.active_profiles.add_rule(cls.filename, 'variable', VariableRule.create_instance('@{VAR1} = "test"'))
|
|
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$FOO=true'))
|
|
cls.active_profiles.add_rule(cls.filename, 'boolean', BooleanRule.create_instance('$BAR=false'))
|
|
|
|
def _run_test(self, params, expected):
|
|
conditional_block = None
|
|
with self.assertRaises(expected):
|
|
for condition in params:
|
|
if condition == params[0]:
|
|
conditional_block = ConditionalBlock(condition, self.active_profiles.files[self.filename])
|
|
conditional_block.store_profile_data(self.profile_data)
|
|
else:
|
|
conditional_block.add_conditional(condition, self.active_profiles.files[self.filename])
|
|
conditional_block.store_profile_data(self.profile_data)
|
|
|
|
def test_create_instance_block1(self):
|
|
with self.assertRaises(NotImplementedError):
|
|
ConditionalBlock.create_instance('test')
|
|
|
|
def test_create_instance_block2(self):
|
|
with self.assertRaises(NotImplementedError):
|
|
ConditionalBlock._create_instance('test', None)
|
|
|
|
|
|
class TestAppArmorAstException(AATest):
|
|
tests = (
|
|
('foo = "1"', AppArmorBug), # AppArmorAst only supports the actual condition
|
|
('~ $FOO', AppArmorBug), # Invalid unary operator
|
|
('func(${BAZ})', AppArmorBug), # Invalid function name
|
|
)
|
|
|
|
def _run_test(self, params, expected):
|
|
ast = AppArmorAst(params)
|
|
with self.assertRaises(expected):
|
|
ast.get_clean()
|
|
with self.assertRaises(expected):
|
|
ast.evaluate(None)
|
|
|
|
|
|
class TestMiscClassException(AATest):
|
|
def test_invalid_op_1(self):
|
|
term = Term.create_instance('10')
|
|
op = 'is'
|
|
with self.assertRaises(AppArmorBug):
|
|
CompareCondition(term, op, term)
|
|
|
|
def test_invalid_op_2(self):
|
|
term = Term.create_instance('10')
|
|
cond = CompareCondition(term, 'in', term)
|
|
cond.op = 'is'
|
|
with self.assertRaises(AppArmorBug):
|
|
cond.evaluate(None)
|
|
|
|
def test_invalid_term(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
Term.create_instance('#')
|
|
|
|
def test_invalid_conditional_condition(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
ConditionalRule('invalid', AppArmorAst('defined $BAR'))
|
|
|
|
def test_invalid_conditional_ast_tree(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
ConditionalRule(ConditionalRule.IF, 'defined $BAR')
|
|
|
|
def test_invalid_conditional_clean(self):
|
|
cond = ConditionalRule(ConditionalRule.IF, AppArmorAst('defined $BAR'))
|
|
cond.condition = 'invalid'
|
|
with self.assertRaises(AppArmorBug):
|
|
cond.get_clean()
|
|
|
|
def test_invalid_boolean_defined(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
BooleanCondition(None, Term.create_instance('10'))
|
|
|
|
def test_invalid_boolean_varible(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
BooleanCondition('defined', '10')
|
|
|
|
def test_invalid_compare_left(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
CompareCondition(None, '==', Term.create_instance('10'))
|
|
|
|
def test_invalid_compare_right(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
CompareCondition(Term.create_instance('10'), '==', None)
|
|
|
|
def test_invalid_compare_op(self):
|
|
with self.assertRaises(AppArmorBug):
|
|
CompareCondition(Term.create_instance('10'), '~', Term.create_instance('10'))
|
|
|
|
def test_invalid_compare(self):
|
|
term = Term.create_instance('10')
|
|
cond = CompareCondition(term, '==', term)
|
|
with self.assertRaises(AppArmorBug):
|
|
cond.compare(cond.op, term.get_set(None), 10)
|
|
|
|
def test_invalid_compare_evaluate(self):
|
|
term = InvalidTerm(10)
|
|
cond = CompareCondition(term, '==', term)
|
|
with self.assertRaises(AppArmorBug):
|
|
cond.evaluate(None)
|
|
|
|
|
|
class InvalidTerm(Term):
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
def get_set(self, prof_storage):
|
|
return self.value # not a set
|
|
|
|
|
|
setup_all_loops(__name__)
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=1)
|