• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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