Home | History | Annotate | Download | only in security_BundledExtensions
      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 
      5 import json
      6 import logging
      7 import os
      8 
      9 from autotest_lib.client.bin import test, utils
     10 from autotest_lib.client.common_lib import error
     11 from autotest_lib.client.common_lib.cros import chrome
     12 
     13 class 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