1# Copyright (c) 2012 The Chromium 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 json 6import logging 7import os 8 9from autotest_lib.client.bin import test, utils 10from autotest_lib.client.common_lib import error 11from autotest_lib.client.common_lib.cros import chrome 12 13class security_BundledExtensions(test.test): 14 """Verify security properties of bundled (on-disk) extensions.""" 15 version = 1 16 17 def load_baseline(self): 18 """ 19 Loads the set of expected permissions. 20 21 @return Dictionary of expected permissions. 22 """ 23 bfile = open(os.path.join(self.bindir, 'baseline')) 24 with open(os.path.join(self.bindir, 'baseline')) as bfile: 25 baseline = [] 26 for line in bfile: 27 if not line.startswith('#'): 28 baseline.append(line) 29 baseline = json.loads(''.join(baseline)) 30 self._ignored_extension_ids = baseline['ignored_extension_ids'] 31 self._bundled_crx_baseline = baseline['bundled_crx_baseline'] 32 self._component_extension_baseline = baseline[ 33 'component_extension_baseline'] 34 self._official_components = baseline['official_components'] 35 self._extensions_info = None 36 37 38 def _get_stable_extensions_info(self, ext): 39 """ 40 Poll condition that verifies that we're getting a stable list of 41 extensions from chrome.autotestPrivate.getExtensionInfo. 42 43 @return list of dicts, each representing an extension. 44 """ 45 logging.info("Poll") 46 prev = self._extensions_info 47 ext.ExecuteJavaScript(''' 48 window.__extensions_info = null; 49 chrome.autotestPrivate.getExtensionsInfo(function(s) { 50 window.__extensions_info = s.extensions; 51 }); 52 ''') 53 self._extensions_info = utils.poll_for_condition( 54 lambda: ext.EvaluateJavaScript('window.__extensions_info')) 55 if not prev: 56 return False 57 return len(prev) == len(self._extensions_info) 58 59 def _get_extensions_info(self): 60 """ 61 Calls _get_stable_extensions_info to get a stable list of extensions. 62 Filters out extensions that are on the to-be-ignored list. 63 64 @return list of dicts, each representing an extension. 65 """ 66 with chrome.Chrome(logged_in=True, autotest_ext=True) as cr: 67 ext = cr.autotest_ext 68 if not ext: 69 return None 70 71 utils.poll_for_condition( 72 lambda: self._get_stable_extensions_info(ext), 73 sleep_interval=0.5, timeout=30) 74 logging.debug("getExtensionsInfo:\n%s", self._extensions_info) 75 filtered_info = [] 76 self._ignored_extension_ids.append(ext.extension_id) 77 for rec in self._extensions_info: 78 if not rec['id'] in self._ignored_extension_ids: 79 filtered_info.append(rec) 80 self._extensions_info = filtered_info 81 return filtered_info 82 83 84 def compare_extensions(self): 85 """Compare installed extensions to the expected set. 86 87 Find the set of expected IDs. 88 Find the set of observed IDs. 89 Do set comparison to find the unexpected, and the expected/missing. 90 91 """ 92 test_fail = False 93 combined_baseline = (self._bundled_crx_baseline + 94 self._component_extension_baseline) 95 # Filter out any baseline entries that don't apply to this board. 96 # If there is no 'boards' limiter on a given record, the record applies. 97 # If there IS a 'boards' limiter, check that it applies. 98 board = utils.get_current_board() 99 combined_baseline = [x for x in combined_baseline 100 if ((not 'boards' in x) or 101 ('boards' in x and board in x['boards']))] 102 103 observed_extensions = self._get_extensions_info() 104 observed_ids = set([x['id'] for x in observed_extensions]) 105 expected_ids = set([x['id'] for x in combined_baseline]) 106 107 missing_ids = expected_ids - observed_ids 108 missing_names = ['%s (%s)' % (x['name'], x['id']) 109 for x in combined_baseline if x['id'] in missing_ids] 110 111 unexpected_ids = observed_ids - expected_ids 112 unexpected_names = ['%s (%s)' % (x['name'], x['id']) 113 for x in observed_extensions if 114 x['id'] in unexpected_ids] 115 116 good_ids = expected_ids.intersection(observed_ids) 117 118 if missing_names: 119 logging.error('Missing: %s', '; '.join(missing_names)) 120 test_fail = True 121 if unexpected_names: 122 logging.error('Unexpected: %s', '; '.join(unexpected_names)) 123 test_fail = True 124 125 # For those IDs in both the expected-and-observed, ie, "good": 126 # Compare sets of expected-vs-actual API permissions, report diffs. 127 # Do same for host permissions. 128 for good_id in good_ids: 129 baseline = [x for x in combined_baseline if x['id'] == good_id][0] 130 actual = [x for x in observed_extensions if x['id'] == good_id][0] 131 # Check the API permissions. 132 baseline_apis = set(baseline['apiPermissions']) 133 actual_apis = set(actual['apiPermissions']) 134 missing_apis = baseline_apis - actual_apis 135 unexpected_apis = actual_apis - baseline_apis 136 if missing_apis or unexpected_apis: 137 test_fail = True 138 self._report_attribute_diffs(missing_apis, unexpected_apis, 139 actual) 140 # Check the host permissions. 141 baseline_hosts = set(baseline['effectiveHostPermissions']) 142 actual_hosts = set(actual['effectiveHostPermissions']) 143 missing_hosts = baseline_hosts - actual_hosts 144 unexpected_hosts = actual_hosts - baseline_hosts 145 if missing_hosts or unexpected_hosts: 146 test_fail = True 147 self._report_attribute_diffs(missing_hosts, unexpected_hosts, 148 actual) 149 if test_fail: 150 # TODO(jorgelo): make this fail again, see crbug.com/343271. 151 raise error.TestWarn('Baseline mismatch, see error log.') 152 153 154 def _report_attribute_diffs(self, missing, unexpected, rec): 155 logging.error('Problem with %s (%s):', rec['name'], rec['id']) 156 if missing: 157 logging.error('It no longer uses: %s', '; '.join(missing)) 158 if unexpected: 159 logging.error('It unexpectedly uses: %s', '; '.join(unexpected)) 160 161 162 def run_once(self, mode=None): 163 self.load_baseline() 164 self.compare_extensions() 165