diff --git a/utils/apparmor/aa.py b/utils/apparmor/aa.py index 03e28b11e..f56d50b33 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 167196fcc..0b656ccbf 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)