[apparmor] [PATCH v2] utils: Basic support for file prefix in path rules

Tyler Hicks tyhicks at canonical.com
Thu Apr 3 19:50:35 UTC 2014


Bug: https://bugs.launchpad.net/bugs/1295346

Add the ability to read and write path rules containing the file prefix.
This also includes bare "file," rules.

The ALL global is updated to include a preceding NUL char to eliminate
possibilities of a real file path colliding with the ALL global.

Signed-off-by: Tyler Hicks <tyhicks at canonical.com>
---
 utils/apparmor/aa.py             | 151 +++++++++++++++++++++++++++++++++++++--
 utils/test/test-regex_matches.py | 124 ++++++++++++++++++++++++++++++++
 2 files changed, 270 insertions(+), 5 deletions(-)

diff --git a/utils/apparmor/aa.py b/utils/apparmor/aa.py
index 03e28b1..f56d50b 100644
--- a/utils/apparmor/aa.py
+++ b/utils/apparmor/aa.py
@@ -79,7 +79,7 @@ seen_events = 0  # was our
 user_globs = []
 
 # The key for representing bare rules such as "capability," or "file,"
-ALL = '_ALL'
+ALL = '\0ALL'
 
 ## Variables used under logprof
 ### Were our
@@ -2615,6 +2615,7 @@ RE_PROFILE_VARIABLE = re.compile('^\s*(@\{?\w+\}?)\s*(\+?=)\s*(@*.+?)\s*,?\s*(#.
 RE_PROFILE_CONDITIONAL = re.compile('^\s*if\s+(not\s+)?(\$\{?\w*\}?)\s*\{\s*(#.*)?$')
 RE_PROFILE_CONDITIONAL_VARIABLE = re.compile('^\s*if\s+(not\s+)?defined\s+(@\{?\w+\}?)\s*\{\s*(#.*)?$')
 RE_PROFILE_CONDITIONAL_BOOLEAN = re.compile('^\s*if\s+(not\s+)?defined\s+(\$\{?\w+\}?)\s*\{\s*(#.*)?$')
+RE_PROFILE_FILE_ENTRY = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?(owner\s+)?file(?:\s+([\"@/].*?)\s+(\S+)(\s+->\s*(.*?))?)?\s*,\s*(#.*)?$')
 RE_PROFILE_PATH_ENTRY = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?(owner\s+)?([\"@/].*?)\s+(\S+)(\s+->\s*(.*?))?\s*,\s*(#.*)?$')
 RE_PROFILE_NETWORK = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?network(.*)\s*(#.*)?$')
 RE_PROFILE_CHANGE_HAT = re.compile('^\s*\^(\"??.+?\"??)\s*,\s*(#.*)?$')
@@ -2896,6 +2897,69 @@ def parse_profile_data(data, file, do_include):
             else:
                 profile_data[profile][hat][allow]['path'][path]['audit'] = set()
 
+        elif RE_PROFILE_FILE_ENTRY.search(line):
+            matches = RE_PROFILE_FILE_ENTRY.search(line).groups()
+
+            if not profile:
+                raise AppArmorException(_('Syntax Error: Unexpected file entry found in file: %s line: %s') % (file, lineno + 1))
+
+            audit = False
+            if matches[0]:
+                audit = True
+
+            allow = 'allow'
+            if matches[1] and matches[1].strip() == 'deny':
+                allow = 'deny'
+
+            user = False
+            if matches[2]:
+                user = True
+
+            path = None
+            if matches[3]:
+                path = matches[3].strip()
+
+            mode = None
+            if matches[4]:
+                mode = matches[4]
+
+            nt_name = None
+            if matches[6]:
+                nt_name = matches[6].strip()
+
+            if not path and not mode and not nt_name:
+                path = ALL
+            elif (path and not mode) or (nt_name and (not path or not mode)):
+                raise AppArmorException(_('Syntax Error: Invalid file entry found in file: %s line: %s') % (file, lineno + 1))
+
+            p_re = convert_regexp(path)
+            try:
+                re.compile(p_re)
+            except:
+                raise AppArmorException(_('Syntax Error: Invalid Regex %s in file: %s line: %s') % (path, file, lineno + 1))
+
+            tmpmode = set()
+            if mode:
+                if not validate_profile_mode(mode, allow, nt_name):
+                    raise AppArmorException(_('Invalid mode %s in file: %s line: %s') % (mode, file, lineno + 1))
+
+                if user:
+                    tmpmode = str_to_mode('%s::' % mode)
+                else:
+                    tmpmode = str_to_mode(mode)
+
+            profile_data[profile][hat][allow]['path'][path]['mode'] = profile_data[profile][hat][allow]['path'][path].get('mode', set()) | tmpmode
+
+            profile_data[profile][hat][allow]['path'][path]['file_prefix'] = True
+
+            if nt_name:
+                profile_data[profile][hat][allow]['path'][path]['to'] = nt_name
+
+            if audit:
+                profile_data[profile][hat][allow]['path'][path]['audit'] = profile_data[profile][hat][allow]['path'][path].get('audit', set()) | tmpmode
+            else:
+                profile_data[profile][hat][allow]['path'][path]['audit'] = set()
+
         elif re_match_include(line):
             # Include files
             include_name = re_match_include(line)
@@ -3359,13 +3423,19 @@ def write_path_rules(prof_data, depth, allow):
 
     if prof_data[allow].get('path', False):
         for path in sorted(prof_data[allow]['path'].keys()):
+            filestr = ''
+            if prof_data[allow]['path'][path].get('file_prefix', False):
+                filestr = 'file '
             mode = prof_data[allow]['path'][path]['mode']
             audit = prof_data[allow]['path'][path]['audit']
             tail = ''
             if prof_data[allow]['path'][path].get('to', False):
                 tail = ' -> %s' % prof_data[allow]['path'][path]['to']
-            user, other = split_mode(mode)
-            user_audit, other_audit = split_mode(audit)
+            user = None
+            other = None
+            if mode or audit:
+                user, other = split_mode(mode)
+                user_audit, other_audit = split_mode(audit)
 
             while user or other:
                 ownerstr = ''
@@ -3393,13 +3463,19 @@ def write_path_rules(prof_data, depth, allow):
                 if tmpmode & tmpaudit:
                     modestr = mode_to_str(tmpmode & tmpaudit)
                     path = quote_if_needed(path)
-                    data.append('%saudit %s%s%s %s%s,' % (pre, allowstr, ownerstr, path, modestr, tail))
+                    data.append('%saudit %s%s%s%s %s%s,' % (pre, allowstr, ownerstr, filestr, path, modestr, tail))
                     tmpmode = tmpmode - tmpaudit
 
                 if tmpmode:
                     modestr = mode_to_str(tmpmode)
                     path = quote_if_needed(path)
-                    data.append('%s%s%s%s %s%s,' % (pre, allowstr, ownerstr, path, modestr, tail))
+                    data.append('%s%s%s%s%s %s%s,' % (pre, allowstr, ownerstr, filestr, path, modestr, tail))
+
+            if filestr and path == ALL:
+                auditstr = ''
+                if audit == 0:
+                    auditstr = 'audit '
+                data.append('%s%s%s%s%s,' % (pre, auditstr, allowstr, filestr, tail))
 
         data.append('')
     return data
@@ -3969,6 +4045,71 @@ def serialize_profile_from_old_profile(profile_data, name, options):
                     #To-Do
                     pass
 
+            elif RE_PROFILE_FILE_ENTRY.search(line):
+                matches = RE_PROFILE_FILE_ENTRY.search(line).groups()
+                audit = False
+                if matches[0]:
+                    audit = True
+                allow = 'allow'
+                if matches[1] and matches[1].split() == 'deny':
+                    allow = 'deny'
+
+                user = False
+                if matches[2]:
+                    user = True
+
+                path = None
+                if matches[3]:
+                    path = matches[3].strip()
+
+                mode = None
+                if matches[4]:
+                    mode = matches[4].strip()
+
+                nt_name = None
+                if matches[6]:
+                    nt_name = matches[6].strip()
+
+                if not path and not mode and not nt_name:
+                    path = ALL
+                elif (path and not mode) or (nt_name and (not path or not mode)):
+                    correct = False
+
+                tmpmode = set()
+                if mode:
+                    if user:
+                        tmpmode = str_to_mode('%s::' % mode)
+                    else:
+                        tmpmode = str_to_mode(mode)
+
+                if not write_prof_data[hat][allow]['path'][path].get('mode', set()) & tmpmode:
+                    if path != ALL:
+                        correct = False
+
+                if nt_name and not write_prof_data[hat][allow]['path'][path].get('to', False) == nt_name:
+                    correct = False
+
+                if audit and not write_prof_data[hat][allow]['path'][path].get('audit', set()) & tmpmode:
+                    if path != ALL:
+                        correct = False
+
+                if correct:
+                    if not segments['path'] and True in segments.values():
+                        for segs in list(filter(lambda x: segments[x], segments.keys())):
+                            depth = len(line) - len(line.lstrip())
+                            data += write_methods[segs](write_prof_data[name], int(depth / 2))
+                            segments[segs] = False
+                            if write_prof_data[name]['allow'].get(segs, False):
+                                write_prof_data[name]['allow'].pop(segs)
+                            if write_prof_data[name]['deny'].get(segs, False):
+                                write_prof_data[name]['deny'].pop(segs)
+                    segments['path'] = True
+                    write_prof_data[hat][allow]['path'].pop(path)
+                    data.append(line)
+                else:
+                    #To-Do
+                    pass
+
             elif re_match_include(line):
                 include_name = re_match_include(line)
                 if profile:
diff --git a/utils/test/test-regex_matches.py b/utils/test/test-regex_matches.py
index 167196f..0b656cc 100644
--- a/utils/test/test-regex_matches.py
+++ b/utils/test/test-regex_matches.py
@@ -96,6 +96,9 @@ regex_split_comment_testcases = [
     ('dbus send member=no_comment, ', False),
     ('audit "/tmp/foo, # bar" rw', False),
     ('audit "/tmp/foo, # bar" rw # comment', ('audit "/tmp/foo, # bar" rw ', '# comment')),
+    ('file,', False),
+    ('file, # bare', ('file, ', '# bare')),
+    ('file /tmp/foo rw, # read-write', ('file /tmp/foo rw, ', '# read-write')),
 ]
 
 def setup_split_comment_testcases():
@@ -154,6 +157,125 @@ class AARegexCapability(unittest.TestCase):
         result = aa.RE_PROFILE_CAP.search(line)
         self.assertFalse(result, 'Found unexpected capability rule in "%s"' % line)
 
+class AARegexPath(unittest.TestCase):
+    '''Tests for RE_PROFILE_PATH_ENTRY'''
+
+    def test_simple_path_01(self):
+        '''test '   /tmp/foo r,' '''
+
+        line = '   /tmp/foo r,'
+        result = aa.RE_PROFILE_PATH_ENTRY.search(line)
+        self.assertTrue(result, 'Couldn\'t find file rule in "%s"' % line)
+        mode = result.groups()[4].strip()
+        self.assertEqual(mode, 'r', 'Expected mode "r", got "%s"' % (mode))
+
+    def test_simple_path_02(self):
+        '''test '   audit /tmp/foo rw,' '''
+
+        line = '   audit /tmp/foo rw,'
+        result = aa.RE_PROFILE_PATH_ENTRY.search(line)
+        self.assertTrue(result, 'Couldn\'t find file rule in "%s"' % line)
+        audit = result.groups()[0].strip()
+        self.assertEqual(audit, 'audit', 'Couldn\t find audit modifier')
+        mode = result.groups()[4].strip()
+        self.assertEqual(mode, 'rw', 'Expected mode "rw", got "%s"' % (mode))
+
+    def test_simple_path_03(self):
+        '''test '   audit deny /tmp/foo rw,' '''
+
+        line = '   audit deny /tmp/foo rw,'
+        result = aa.RE_PROFILE_PATH_ENTRY.search(line)
+        self.assertTrue(result, 'Couldn\'t find file rule in "%s"' % line)
+        audit = result.groups()[0].strip()
+        self.assertEqual(audit, 'audit', 'Couldn\t find audit modifier')
+        deny = result.groups()[1].strip()
+        self.assertEqual(deny, 'deny', 'Couldn\t find deny modifier')
+        mode = result.groups()[4].strip()
+        self.assertEqual(mode, 'rw', 'Expected mode "rw", got "%s"' % (mode))
+
+    def test_simple_bad_path_01(self):
+        '''test '   file,' '''
+
+        line = '   file,'
+        result = aa.RE_PROFILE_PATH_ENTRY.search(line)
+        self.assertFalse(result, 'RE_PROFILE_PATH_ENTRY unexpectedly matched "%s"' % line)
+
+    def test_simple_bad_path_02(self):
+        '''test '   file /tmp/foo rw,' '''
+
+        line = '   file /tmp/foo rw,'
+        result = aa.RE_PROFILE_PATH_ENTRY.search(line)
+        self.assertFalse(result, 'RE_PROFILE_PATH_ENTRY unexpectedly matched "%s"' % line)
+
+class AARegexFile(unittest.TestCase):
+    '''Tests for RE_PROFILE_FILE_ENTRY'''
+
+    def _assertEqualStrings(self, str1, str2):
+        self.assertEqual(str1, str2, 'Expected %s, got "%s"' % (str1, str2))
+
+    def test_simple_file_01(self):
+        '''test '   file /tmp/foo rw,' '''
+
+        path = '/tmp/foo'
+        mode = 'rw'
+        line = '   file %s %s,' % (path, mode)
+        result = aa.RE_PROFILE_FILE_ENTRY.search(line)
+        self.assertTrue(result, 'Couldn\'t find file rule in "%s"' % line)
+        self._assertEqualStrings(path, result.groups()[3].strip())
+        self._assertEqualStrings(mode, result.groups()[4].strip())
+
+    def test_simple_file_02(self):
+        '''test '   file,' '''
+
+        line = '   file,'
+        result = aa.RE_PROFILE_FILE_ENTRY.search(line)
+        self.assertTrue(result, 'Couldn\'t find file rule in "%s"' % line)
+        path = result.groups()[3]
+        self.assertEqual(path, None, 'Unexpected path, got "%s"' % path)
+        mode = result.groups()[4]
+        self.assertEqual(mode, None, 'Unexpected mode, got "%s"' % (mode))
+
+    def test_simple_file_03(self):
+        '''test '   audit file,' '''
+
+        line = '   audit file,'
+        result = aa.RE_PROFILE_FILE_ENTRY.search(line)
+        self.assertTrue(result, 'Couldn\'t find file rule in "%s"' % line)
+        audit = result.groups()[0].strip()
+        self.assertEqual(audit, 'audit', 'Couldn\t find audit modifier')
+        path = result.groups()[3]
+        self.assertEqual(path, None, 'Unexpected path, got "%s"' % path)
+        mode = result.groups()[4]
+        self.assertEqual(mode, None, 'Unexpected mode, got "%s"' % (mode))
+
+    def test_simple_bad_file_01(self):
+        '''test '   dbus,' '''
+
+        line = '   dbus,'
+        result = aa.RE_PROFILE_FILE_ENTRY.search(line)
+        self.assertFalse(result, 'RE_PROFILE_FILE_ENTRY unexpectedly matched "%s"' % line)
+
+    def test_simple_bad_file_02(self):
+        '''test '   /tmp/foo rw,' '''
+
+        line = '   /tmp/foo rw,'
+        result = aa.RE_PROFILE_FILE_ENTRY.search(line)
+        self.assertFalse(result, 'RE_PROFILE_FILE_ENTRY unexpectedly matched "%s"' % line)
+
+    def test_simple_bad_file_03(self):
+        '''test '   file /tmp/foo,' '''
+
+        line = '   file /tmp/foo,'
+        result = aa.RE_PROFILE_FILE_ENTRY.search(line)
+        self.assertFalse(result, 'RE_PROFILE_FILE_ENTRY unexpectedly matched "%s"' % line)
+
+    def test_simple_bad_file_04(self):
+        '''test '   file r,' '''
+
+        line = '   file r,'
+        result = aa.RE_PROFILE_FILE_ENTRY.search(line)
+        self.assertFalse(result, 'RE_PROFILE_FILE_ENTRY unexpectedly matched "%s"' % line)
+
 if __name__ == '__main__':
     verbosity = 2
 
@@ -164,6 +286,8 @@ if __name__ == '__main__':
     test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexHasComma))
     test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexSplitComment))
     test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexCapability))
+    test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexPath))
+    test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexFile))
     result = unittest.TextTestRunner(verbosity=verbosity).run(test_suite)
     if not result.wasSuccessful():
         exit(1)
-- 
1.9.1




More information about the AppArmor mailing list