| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import json |
| import logging |
| import os |
| |
| from autotest_lib.client.bin import test, utils |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.client.common_lib.cros import chrome |
| |
| class security_BundledExtensions(test.test): |
| """Verify security properties of bundled (on-disk) extensions.""" |
| version = 1 |
| |
| def load_baseline(self): |
| """ |
| Loads the set of expected permissions. |
| |
| @return Dictionary of expected permissions. |
| """ |
| bfile = open(os.path.join(self.bindir, 'baseline')) |
| with open(os.path.join(self.bindir, 'baseline')) as bfile: |
| baseline = [] |
| for line in bfile: |
| if not line.startswith('#'): |
| baseline.append(line) |
| baseline = json.loads(''.join(baseline)) |
| self._ignored_extension_ids = baseline['ignored_extension_ids'] |
| self._bundled_crx_baseline = baseline['bundled_crx_baseline'] |
| self._component_extension_baseline = baseline[ |
| 'component_extension_baseline'] |
| self._official_components = baseline['official_components'] |
| self._extensions_info = None |
| |
| |
| def _get_stable_extensions_info(self, ext): |
| """ |
| Poll condition that verifies that we're getting a stable list of |
| extensions from chrome.autotestPrivate.getExtensionInfo. |
| |
| @return list of dicts, each representing an extension. |
| """ |
| logging.info("Poll") |
| prev = self._extensions_info |
| ext.ExecuteJavaScript(''' |
| window.__extensions_info = null; |
| chrome.autotestPrivate.getExtensionsInfo(function(s) { |
| window.__extensions_info = s.extensions; |
| }); |
| ''') |
| self._extensions_info = utils.poll_for_condition( |
| lambda: ext.EvaluateJavaScript('window.__extensions_info')) |
| if not prev: |
| return False |
| return len(prev) == len(self._extensions_info) |
| |
| def _get_extensions_info(self): |
| """ |
| Calls _get_stable_extensions_info to get a stable list of extensions. |
| Filters out extensions that are on the to-be-ignored list. |
| |
| @return list of dicts, each representing an extension. |
| """ |
| with chrome.Chrome(logged_in=True, autotest_ext=True) as cr: |
| ext = cr.autotest_ext |
| if not ext: |
| return None |
| |
| utils.poll_for_condition( |
| lambda: self._get_stable_extensions_info(ext), |
| sleep_interval=0.5, timeout=30) |
| logging.debug("getExtensionsInfo:\n%s", self._extensions_info) |
| filtered_info = [] |
| self._ignored_extension_ids.append(ext.extension_id) |
| for rec in self._extensions_info: |
| if not rec['id'] in self._ignored_extension_ids: |
| filtered_info.append(rec) |
| self._extensions_info = filtered_info |
| return filtered_info |
| |
| |
| def compare_extensions(self): |
| """Compare installed extensions to the expected set. |
| |
| Find the set of expected IDs. |
| Find the set of observed IDs. |
| Do set comparison to find the unexpected, and the expected/missing. |
| |
| """ |
| test_fail = False |
| combined_baseline = (self._bundled_crx_baseline + |
| self._component_extension_baseline) |
| # Filter out any baseline entries that don't apply to this board. |
| # If there is no 'boards' limiter on a given record, the record applies. |
| # If there IS a 'boards' limiter, check that it applies. |
| board = utils.get_current_board() |
| combined_baseline = [x for x in combined_baseline |
| if ((not 'boards' in x) or |
| ('boards' in x and board in x['boards']))] |
| |
| observed_extensions = self._get_extensions_info() |
| observed_ids = set([x['id'] for x in observed_extensions]) |
| expected_ids = set([x['id'] for x in combined_baseline]) |
| |
| missing_ids = expected_ids - observed_ids |
| missing_names = ['%s (%s)' % (x['name'], x['id']) |
| for x in combined_baseline if x['id'] in missing_ids] |
| |
| unexpected_ids = observed_ids - expected_ids |
| unexpected_names = ['%s (%s)' % (x['name'], x['id']) |
| for x in observed_extensions if |
| x['id'] in unexpected_ids] |
| |
| good_ids = expected_ids.intersection(observed_ids) |
| |
| if missing_names: |
| logging.error('Missing: %s', '; '.join(missing_names)) |
| test_fail = True |
| if unexpected_names: |
| logging.error('Unexpected: %s', '; '.join(unexpected_names)) |
| test_fail = True |
| |
| # For those IDs in both the expected-and-observed, ie, "good": |
| # Compare sets of expected-vs-actual API permissions, report diffs. |
| # Do same for host permissions. |
| for good_id in good_ids: |
| baseline = [x for x in combined_baseline if x['id'] == good_id][0] |
| actual = [x for x in observed_extensions if x['id'] == good_id][0] |
| # Check the API permissions. |
| baseline_apis = set(baseline['apiPermissions']) |
| actual_apis = set(actual['apiPermissions']) |
| missing_apis = baseline_apis - actual_apis |
| unexpected_apis = actual_apis - baseline_apis |
| if missing_apis or unexpected_apis: |
| test_fail = True |
| self._report_attribute_diffs(missing_apis, unexpected_apis, |
| actual) |
| # Check the host permissions. |
| baseline_hosts = set(baseline['effectiveHostPermissions']) |
| actual_hosts = set(actual['effectiveHostPermissions']) |
| missing_hosts = baseline_hosts - actual_hosts |
| unexpected_hosts = actual_hosts - baseline_hosts |
| if missing_hosts or unexpected_hosts: |
| test_fail = True |
| self._report_attribute_diffs(missing_hosts, unexpected_hosts, |
| actual) |
| if test_fail: |
| # TODO(jorgelo): make this fail again, see crbug.com/343271. |
| raise error.TestWarn('Baseline mismatch, see error log.') |
| |
| |
| def _report_attribute_diffs(self, missing, unexpected, rec): |
| logging.error('Problem with %s (%s):', rec['name'], rec['id']) |
| if missing: |
| logging.error('It no longer uses: %s', '; '.join(missing)) |
| if unexpected: |
| logging.error('It unexpectedly uses: %s', '; '.join(unexpected)) |
| |
| |
| def run_once(self, mode=None): |
| self.load_baseline() |
| self.compare_extensions() |