[apparmor] [patch 11/11] utils: add simple parsing of multi-line rules
Steve Beattie
steve at nxnw.org
Thu Mar 6 01:44:45 UTC 2014
D-Bus rules in particular seem to get written as multi-line rules. This
patch adds very simple hackish support for multiple lines. Essentially,
what it does is if the parsing of a line doesn't match anything and
falls all the way through, it saves the line and prepends it to the next
line that occurs in the profile, but *only* if the line does not have a
trailing comma to indicate the end of a rule. If the trailing comma
exists, then it assumes that it's a rule that it doesn't understand and
aborts.
With this patch, the simpler tools (aa-enforce, aa-complain, etc.) can
parse policies containing multi-line rules to an extent and continue to
function correctly. Again, aa-logprof and aa-genprof may have issues on
the writing back of profiles, so some assistance testing here would be
appreciated.
Some testcases are added to exercise the regex that looks for a rule
with a trailing comma but can still handle rules that have (,) or {,}
in them.
Signed-off-by: Steve Beattie <steve at nxnw.org>
---
utils/apparmor/aa.py | 29 +++++++++++++++++-
utils/apparmor/tools.py | 5 ---
utils/test/test-dbus_parse.py | 66 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 95 insertions(+), 5 deletions(-)
Index: b/utils/apparmor/aa.py
===================================================================
--- a/utils/apparmor/aa.py
+++ b/utils/apparmor/aa.py
@@ -2615,7 +2615,8 @@ RE_PROFILE_CHANGE_HAT = re.compile('^\s*
RE_PROFILE_HAT_DEF = re.compile('^\s*\^(\"??.+?\"??)\s+((flags=)?\((.+)\)\s+)*\{\s*(#.*)?$')
RE_NETWORK_FAMILY_TYPE = re.compile('\s+(\S+)\s+(\S+)\s*,$')
RE_NETWORK_FAMILY = re.compile('\s+(\S+)\s*,$')
-RE_PROFILE_DBUS = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?(dbus[^#]*)\s*(#.*)?$')
+RE_PROFILE_DBUS = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?(dbus[^#]*\s*,)\s*(#.*)?$')
+RE_RULE_HAS_COMMA = re.compile('[^,)({}]*(\(([^),]*,)*[^),]*\)|\{([^},]*,)*[^},]*\})*[^,)({}]*,\s*(#.*)?$')
def parse_profile_data(data, file, do_include):
profile_data = hasher()
@@ -2625,6 +2626,7 @@ def parse_profile_data(data, file, do_in
repo_data = None
parsed_profiles = []
initial_comment = ''
+ lastline = None
if do_include:
profile = file
@@ -2633,8 +2635,12 @@ def parse_profile_data(data, file, do_in
line = line.strip()
if not line:
continue
+ # we're dealing with a multiline statement
+ if lastline:
+ line = '%s %s' % (lastline, line)
# Starting line of a profile
if RE_PROFILE_START.search(line):
+ lastline = None
matches = RE_PROFILE_START.search(line).groups()
if profile:
@@ -2691,6 +2697,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][profile]['repo']['user'] = repo_data['user']
elif RE_PROFILE_END.search(line):
+ lastline = None
# If profile ends and we're not in one
if not profile:
raise AppArmorException(_('Syntax Error: Unexpected End of Profile reached in file: %s line: %s') % (file, lineno + 1))
@@ -2705,6 +2712,7 @@ def parse_profile_data(data, file, do_in
initial_comment = ''
elif RE_PROFILE_CAP.search(line):
+ lastline = None
matches = RE_PROFILE_CAP.search(line).groups()
if not profile:
@@ -2724,6 +2732,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat][allow]['capability'][capability]['audit'] = audit
elif RE_PROFILE_LINK.search(line):
+ lastline = None
matches = RE_PROFILE_LINK.search(line).groups()
if not profile:
@@ -2752,6 +2761,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat][allow]['link'][link]['audit'] = set()
elif RE_PROFILE_CHANGE_PROFILE.search(line):
+ lastline = None
matches = RE_PROFILE_CHANGE_PROFILE.search(line).groups()
if not profile:
@@ -2761,6 +2771,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat]['changes_profile'][cp] = True
elif RE_PROFILE_ALIAS.search(line):
+ lastline = None
matches = RE_PROFILE_ALIAS.search(line).groups()
from_name = strip_quotes(matches[0])
@@ -2774,6 +2785,7 @@ def parse_profile_data(data, file, do_in
filelist[file]['alias'][from_name] = to_name
elif RE_PROFILE_RLIMIT.search(line):
+ lastline = None
matches = RE_PROFILE_RLIMIT.search(line).groups()
if not profile:
@@ -2785,6 +2797,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat]['rlimit'][from_name] = to_name
elif RE_PROFILE_BOOLEAN.search(line):
+ lastline = None
matches = RE_PROFILE_BOOLEAN.search(line)
if not profile:
@@ -2796,6 +2809,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat]['lvar'][bool_var] = value
elif RE_PROFILE_VARIABLE.search(line):
+ lastline = None
# variable additions += and =
matches = RE_PROFILE_VARIABLE.search(line).groups()
@@ -2813,18 +2827,22 @@ def parse_profile_data(data, file, do_in
store_list_var(filelist[file]['lvar'], list_var, value, var_operation)
elif RE_PROFILE_CONDITIONAL.search(line):
+ lastline = None
# Conditional Boolean
pass
elif RE_PROFILE_CONDITIONAL_VARIABLE.search(line):
+ lastline = None
# Conditional Variable defines
pass
elif RE_PROFILE_CONDITIONAL_BOOLEAN.search(line):
+ lastline = None
# Conditional Boolean defined
pass
elif RE_PROFILE_PATH_ENTRY.search(line):
+ lastline = None
matches = RE_PROFILE_PATH_ENTRY.search(line).groups()
if not profile:
@@ -2874,6 +2892,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat][allow]['path'][path]['audit'] = set()
elif re_match_include(line):
+ lastline = None
# Include files
include_name = re_match_include(line)
if include_name.startswith('local/'):
@@ -2901,6 +2920,7 @@ def parse_profile_data(data, file, do_in
load_include(include_name)
elif RE_PROFILE_NETWORK.search(line):
+ lastline = None
matches = RE_PROFILE_NETWORK.search(line).groups()
if not profile:
@@ -2931,6 +2951,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat][allow]['netdomain']['audit']['all'] = audit # True
elif RE_PROFILE_DBUS.search(line):
+ lastline = None
matches = RE_PROFILE_DBUS.search(line).groups()
if not profile:
@@ -2954,6 +2975,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat][allow]['dbus'] = dbus_rules
elif RE_PROFILE_CHANGE_HAT.search(line):
+ lastline = None
matches = RE_PROFILE_CHANGE_HAT.search(line).groups()
if not profile:
@@ -2966,6 +2988,7 @@ def parse_profile_data(data, file, do_in
profile_data[profile][hat]['declared'] = True
elif RE_PROFILE_HAT_DEF.search(line):
+ lastline = None
# An embedded hat syntax definition starts
matches = RE_PROFILE_HAT_DEF.search(line).groups()
if not profile:
@@ -2989,6 +3012,7 @@ def parse_profile_data(data, file, do_in
filelist[file]['profiles'][profile][hat] = True
elif line[0] == '#':
+ lastline = None
# Handle initial comments
if not profile:
if line.startswith('# Last Modified:'):
@@ -3007,6 +3031,9 @@ def parse_profile_data(data, file, do_in
else:
initial_comment = initial_comment + line + '\n'
+ elif not RE_RULE_HAS_COMMA.search(line):
+ # Bah, line continues on to the next line
+ lastline = line
else:
raise AppArmorException(_('Syntax Error: Unknown line found in file: %s line: %s') % (file, lineno + 1))
Index: b/utils/test/test-dbus_parse.py
===================================================================
--- a/utils/test/test-dbus_parse.py
+++ b/utils/test/test-dbus_parse.py
@@ -26,5 +26,71 @@ class AAParseDBUSTest(unittest.TestCase)
self.assertEqual(dstring, dbus.serialize(),
'dbus object returned "%s", expected "%s"' % (dbus.serialize(), dstring))
+
+class AARegexHasComma(unittest.TestCase):
+
+ def _check(self, line, expected=True):
+ result = aa.RE_RULE_HAS_COMMA.search(line)
+ if expected:
+ self.assertTrue(result, 'Couldn\'t find a comma in "%s"' % line)
+ else:
+ self.assertEqual(None, result, 'Found an unexpected comma in "%s"' % line)
+
+ def test_with_comma_01(self):
+ '''simple comma check'''
+ self._check('dbus send,')
+
+ def test_with_comma_paren_01(self):
+ '''simple comma check w/commas embedded in parens'''
+ self._check('dbus (r, w, bind, eavesdrop),')
+
+ def test_with_comma_paren_02(self):
+ '''simple comma check w/commas embedded in parens'''
+ self._check('dbus (r, w,, bind, eavesdrop) ,')
+
+ def test_with_comma_paren_03(self):
+ '''simple comma check w/empty parens'''
+ self._check('dbus () ,')
+
+ def test_with_comma_curly_brace_01(self):
+ '''simple comma check w/commas embedded in curly braces'''
+ self._check('member={Hello,AddMatch,RemoveMatch,GetNameOwner,NameHasOwner,StartServiceByName} , ')
+
+ def test_with_comma_curly_brace_02(self):
+ '''simple comma check w/commas embedded in curly braces'''
+ self._check('member={Hello,,,,,,AddMatch,RemoveMatch,GetNameOwner,NameHasOwner,StartServiceByName} , ')
+
+ def test_with_comma_curly_brace_03(self):
+ '''simple comma check w/empty curly braces'''
+ self._check('member={} , ')
+
+ def test_without_comma_01(self):
+ '''simple no comma check'''
+ self._check('dbus eavesdrop # ', False)
+
+ def test_without_comma_paren_01(self):
+ '''simple no comma check w/commas embedded in parens'''
+ self._check('dbus (r, w, bind, eavesdrop)', False)
+
+ def test_without_comma_paren_02(self):
+ '''simple no comma check w/commas embedded in parens'''
+ self._check('dbus (r, w,,,, bind, eavesdrop)', False)
+
+ def test_without_comma_paren_03(self):
+ '''simple no comma check w/empty parens'''
+ self._check('dbus () ', False)
+
+ def test_without_comma_curly_brace_01(self):
+ '''simple no comma check w/commas embedded in curly braces'''
+ self._check('member={Hello,AddMatch,RemoveMatch,GetNameOwner,NameHasOwner,StartServiceByName}', False)
+
+ def test_without_comma_curly_brace_02(self):
+ '''simple no comma check w/commas embedded in curly braces'''
+ self._check('member={Hello,,,,,,AddMatch,RemoveMatch,GetNameOwner,NameHasOwner,StartServiceByName} ', False)
+
+ def test_without_comma_curly_brace_03(self):
+ '''simple no comma check w/empty curly braces'''
+ self._check('member={} ', False)
+
if __name__ == '__main__':
unittest.main()
More information about the AppArmor
mailing list