1# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import logging 6import os 7import shutil 8 9from autotest_lib.client.bin import test 10from autotest_lib.client.common_lib import error 11 12class security_AccountsBaseline(test.test): 13 """Enforces a whitelist of known user and group IDs.""" 14 15 version = 1 16 17 18 @staticmethod 19 def validate_passwd(entry): 20 """Check users that are not in the baseline. 21 The user ID should match the group ID, and the user's home directory 22 and shell should be invalid.""" 23 uid = int(entry[2]) 24 gid = int(entry[3]) 25 26 if uid != gid: 27 logging.error("New user '%s' has uid %d and different gid %d", 28 entry[0], uid, gid) 29 return False 30 31 if entry[5] != '/dev/null': 32 logging.error("New user '%s' has valid home dir '%s'", entry[0], 33 entry[5]) 34 return False 35 36 if entry[6] != '/bin/false': 37 logging.error("New user '%s' has valid shell '%s'", entry[0], 38 entry[6]) 39 return False 40 41 return True 42 43 44 @staticmethod 45 def validate_group(entry): 46 """Check groups that are not in the baseline. 47 Allow groups that have no users and groups with only the matching 48 user.""" 49 group_name = entry[0] 50 users = entry[3] 51 52 # Groups with no users and groups with only the matching user are OK. 53 if len(users) == 0 or users == group_name: 54 return True 55 56 logging.error("New group '%s' has users '%s'", group_name, users) 57 return False 58 59 60 @staticmethod 61 def match_passwd(expected, actual): 62 """Match login shell (2nd field), uid (3rd field), 63 and gid (4th field).""" 64 if expected[1:4] != actual[1:4]: 65 logging.error( 66 "Expected shell/uid/gid %s for user '%s', got %s.", 67 tuple(expected[1:4]), expected[0], tuple(actual[1:4])) 68 return False 69 return True 70 71 72 @staticmethod 73 def match_group(expected, actual): 74 """Match login shell (2nd field), gid (3rd field), 75 and members (4th field, comma-separated).""" 76 matched = True 77 if expected[1:3] != actual[1:3]: 78 matched = False 79 logging.error( 80 "Expected shell/id %s for group '%s', got %s.", 81 tuple(expected[1:3]), expected[0], tuple(actual[1:3])) 82 if set(expected[3].split(',')) != set(actual[3].split(',')): 83 matched = False 84 logging.error( 85 "Expected members '%s' for group '%s', got '%s'.", 86 expected[3], expected[0], actual[3]) 87 return matched 88 89 90 def load_path(self, path): 91 """Load the given passwd/group file.""" 92 return [x.strip().split(':') for x in open(path).readlines()] 93 94 95 def capture_files(self): 96 for f in ['passwd','group']: 97 shutil.copyfile(os.path.join('/etc', f), 98 os.path.join(self.resultsdir, f)) 99 100 101 def check_file(self, basename): 102 match_func = getattr(self, 'match_%s' % basename) 103 validate_func = getattr(self, 'validate_%s' % basename) 104 success = True 105 106 expected_entries = self.load_path( 107 os.path.join(self.bindir, 'baseline.%s' % basename)) 108 109 # TODO(jorgelo): Merge this into the main baseline once Freon users 110 # are included in the main overlay. 111 extra_baseline = 'baseline.%s.freon' % basename 112 113 expected_entries += self.load_path( 114 os.path.join(self.bindir, extra_baseline)) 115 116 actual_entries = self.load_path('/etc/%s' % basename) 117 118 if len(actual_entries) > len(expected_entries): 119 logging.warning( 120 '%s baseline mismatch: expected %d entries, got %d.', 121 basename, len(expected_entries), len(actual_entries)) 122 123 for actual in actual_entries: 124 expected = [entry for entry in expected_entries 125 if entry[0] == actual[0]] 126 if not expected: 127 logging.warning("Unexpected %s entry for '%s'.", 128 basename, actual[0]) 129 success = success and validate_func(actual) 130 continue 131 expected = expected[0] 132 match_res = match_func(expected, actual) 133 success = success and match_res 134 135 for expected in expected_entries: 136 actual = [x for x in actual_entries if x[0] == expected[0]] 137 if not actual: 138 logging.info("Ignoring missing %s entry for '%s'.", 139 basename, expected[0]) 140 141 return success 142 143 144 def run_once(self): 145 self.capture_files() 146 147 passwd_ok = self.check_file('passwd') 148 group_ok = self.check_file('group') 149 150 # Fail after all mismatches have been reported. 151 if not (passwd_ok and group_ok): 152 raise error.TestFail('Baseline mismatch, see error log') 153