Add AARE class

The AARE class is meant to handle the internals of path AppArmor regexes
at various places / rule types (filename, signal peer etc.). The goal is
to use it in rule classes to hide all regex magic, so that the rule
class can just use the match() method.

If log_event is given (which means handing over a raw path, not a regex),
the given path is converted to a regex in convert_expression_to_aare().
(Also, the raw path is used in match().)

BTW: The reason for delaying re.compile to match() is performance - I'd
guess a logprof run calls match() only for profiles with existing log
events, so we can save 90% of the re.compile() calls.


The patch also includes several tests.


Acked-by: John Johansen <john.johansen@canonical.com>
This commit is contained in:
Christian Boltz 2015-12-09 23:19:57 +01:00
parent 6ee8cc6203
commit 441d3d2ae2
2 changed files with 191 additions and 2 deletions

83
utils/apparmor/aare.py Normal file
View file

@ -0,0 +1,83 @@
# ----------------------------------------------------------------------
# Copyright (C) 2015 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 re
from apparmor.common import convert_regexp, AppArmorBug, AppArmorException
class AARE(object):
'''AARE (AppArmor Regular Expression) wrapper class'''
def __init__(self, regex, is_path, log_event=None):
'''create an AARE instance for the given AppArmor regex
If is_path is true, the regex is expected to be a path and therefore must start with / or a variable.'''
# using the specified variables when matching.
if is_path:
if regex.startswith('/'):
pass
elif regex.startswith('@{'):
pass # XXX ideally check variable content - each part must start with / - or another variable, which must start with /
else:
raise AppArmorException("Path doesn't start with / or variable: %s" % regex)
if log_event:
self.orig_regex = regex
self.regex = convert_expression_to_aare(regex)
else:
self.orig_regex = None
self.regex = regex
self._regex_compiled = None # done on first use in match() - that saves us some re.compile() calls
# self.variables = variables # XXX
def __repr__(self):
'''returns a "printable" representation of AARE'''
return "AARE('%s')" % self.regex
def match(self, expression):
'''check if the given expression (string or AARE) matches the regex'''
if type(expression) == AARE:
if expression.orig_regex:
expression = expression.orig_regex
else:
return self.is_equal(expression) # better safe than sorry
elif type(expression) != str:
raise AppArmorBug('AARE.match() called with unknown object: %s' % str(expression))
if self._regex_compiled is None:
self._regex_compiled = re.compile(convert_regexp(self.regex))
return bool(self._regex_compiled.match(expression))
def is_equal(self, expression):
'''check if the given expression is equal'''
if type(expression) == AARE:
return self.regex == expression.regex
elif type(expression) == str:
return self.regex == expression
else:
raise AppArmorBug('AARE.is_equal() called with unknown object: %s' % str(expression))
def convert_expression_to_aare(expression):
'''convert an expression (taken from audit.log) to an AARE string'''
aare_escape_chars = ['\\', '?', '*', '[', ']', '{', '}', '"']
for char in aare_escape_chars:
expression = expression.replace(char, '\\' + char)
return expression

View file

@ -14,7 +14,8 @@ import unittest
from common_test import AATest, setup_all_loops
import re
from apparmor.common import convert_regexp
from apparmor.common import convert_regexp, AppArmorBug, AppArmorException
from apparmor.aare import AARE, convert_expression_to_aare
class TestConvert_regexp(AATest):
tests = [
@ -34,7 +35,24 @@ class TestConvert_regexp(AATest):
def _run_test(self, params, expected):
self.assertEqual(convert_regexp(params), expected)
class TestExamplesConvert_regexp(AATest):
class Test_convert_expression_to_aare(AATest):
tests = [
# note that \ always needs to be escaped in python, so \\ is actually just \ in the string
('/foo', '/foo' ),
('/foo?', '/foo\\?' ),
('/foo*', '/foo\\*' ),
('/foo[bar]', '/foo\\[bar\\]' ),
('/foo{bar}', '/foo\\{bar\\}' ),
('/foo{', '/foo\\{' ),
('/foo\\', '/foo\\\\' ),
('/foo"', '/foo\\"' ),
('}]"\\[{', '\\}\\]\\"\\\\\\[\\{' ),
]
def _run_test(self, params, expected):
self.assertEqual(convert_expression_to_aare(params), expected)
class TestConvert_regexpAndAAREMatch(AATest):
tests = [
# aare path to check match expected?
(['/foo/**/bar/', '/foo/user/tools/bar/' ], True),
@ -117,6 +135,94 @@ class TestExamplesConvert_regexp(AATest):
parsed_regex = re.compile(convert_regexp(regex))
self.assertEqual(bool(parsed_regex.search(path)), expected, 'Incorrectly Parsed regex: %s' %regex)
aare_obj = AARE(regex, True)
self.assertEqual(aare_obj.match(path), expected, 'Incorrectly parsed AARE object: %s' % regex)
def test_multi_usage(self):
aare_obj = AARE('/foo/*', True)
self.assertTrue(aare_obj.match('/foo/bar'))
self.assertFalse(aare_obj.match('/foo/bar/'))
self.assertTrue(aare_obj.match('/foo/asdf'))
def test_match_against_AARE_1(self):
aare_obj_1 = AARE('@{foo}/[a-d]**', True)
aare_obj_2 = AARE('@{foo}/[a-d]**', True)
self.assertTrue(aare_obj_1.match(aare_obj_2))
self.assertTrue(aare_obj_1.is_equal(aare_obj_2))
def test_match_against_AARE_2(self):
aare_obj_1 = AARE('@{foo}/[a-d]**', True)
aare_obj_2 = AARE('@{foo}/*[a-d]*', True)
self.assertFalse(aare_obj_1.match(aare_obj_2))
self.assertFalse(aare_obj_1.is_equal(aare_obj_2))
def test_match_invalid_1(self):
aare_obj = AARE('@{foo}/[a-d]**', True)
with self.assertRaises(AppArmorBug):
aare_obj.match(set())
class TestAAREMatchFromLog(AATest):
tests = [
# AARE log event match expected?
(['/foo/bar', '/foo/bar' ], True),
(['/foo/*', '/foo/bar' ], True),
(['/**', '/foo/bar' ], True),
(['/foo/*', '/bar/foo' ], False),
(['/foo/*', '/foo/"*' ], True),
(['/foo/bar', '/foo/*' ], False),
(['/foo/?', '/foo/(' ], True),
(['/foo/{bar,baz}', '/foo/bar' ], True),
(['/foo/{bar,baz}', '/foo/bars' ], False),
]
def _run_test(self, params, expected):
regex, log_event = params
aare_obj_1 = AARE(regex, True)
aare_obj_2 = AARE(log_event, True, log_event=True)
self.assertEqual(aare_obj_1.match(aare_obj_2), expected)
class TestAAREIsEqual(AATest):
tests = [
# regex is path? check for expected
(['/foo', True, '/foo' ], True ),
(['@{foo}', True, '@{foo}' ], True ),
(['/**', True, '/foo' ], False),
]
def _run_test(self, params, expected):
regex, is_path, check_for = params
aare_obj_1 = AARE(regex, is_path)
aare_obj_2 = AARE(check_for, is_path)
self.assertEqual(expected, aare_obj_1.is_equal(check_for))
self.assertEqual(expected, aare_obj_1.is_equal(aare_obj_2))
def test_is_equal_invalid_1(self):
aare_obj = AARE('/foo/**', True)
with self.assertRaises(AppArmorBug):
aare_obj.is_equal(42)
class TestAAREIsPath(AATest):
tests = [
# regex is path? match for expected
(['/foo*', True, '/foobar' ], True ),
(['@{PROC}/', True, '/foobar' ], False),
(['foo*', False, 'foobar' ], True ),
]
def _run_test(self, params, expected):
regex, is_path, check_for = params
aare_obj = AARE(regex, is_path)
self.assertEqual(expected, aare_obj.match(check_for))
def test_path_missing_slash(self):
with self.assertRaises(AppArmorException):
AARE('foo*', True)
class TestAARERepr(AATest):
def test_repr(self):
obj = AARE('/foo', True)
self.assertEqual(str(obj), "AARE('/foo')")
setup_all_loops(__name__)
if __name__ == '__main__':