Home | History | Annotate | Download | only in security_NetworkListeners
      1 # Copyright (c) 2010 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 
      5 import logging
      6 import os
      7 
      8 from autotest_lib.client.bin import test, utils
      9 from autotest_lib.client.common_lib import error
     10 from autotest_lib.client.common_lib.cros import chrome
     11 from autotest_lib.client.common_lib.cros.tendo import webservd_helper
     12 
     13 # Since we parse lsof output in several places, these centralize the
     14 # column numbering for finding things in lsof output.  For example:
     15 # autotest 1915 root 3u IPv4 9221 0t0 TCP *:https (LISTEN)
     16 _LSOF_COMMAND = 0
     17 _LSOF_PID = 1
     18 _LSOF_USER = 2
     19 _LSOF_FD = 3
     20 _LSOF_TYPE = 4
     21 _LSOF_DEVICE = 5
     22 _LSOF_NAME = 7
     23 # In certain cases, the size/offset column is empty, making it more
     24 # reliable to locate the last couple columns by counting from the right.
     25 _LSOF_SIZE_OFF = 6
     26 _LSOF_NODE = -3
     27 _LSOF_NAME = -2
     28 
     29 # Open ports on ARC-enabled test firmwares are different from the non-ARC case
     30 # These files provide a whitelist of services expected to listen in each case
     31 # (ARC and non-ARC)
     32 _BASELINE_DEFAULT_NAME = 'baseline'
     33 _BASELINE_ARC_NAME = 'baseline.arc'
     34 
     35 # We log in so that we include any daemons that
     36 # might be spawned at login in our test results.
     37 class security_NetworkListeners(test.test):
     38     """Check the system against a whitelist of expected network-listeners."""
     39     version = 1
     40 
     41     def load_baseline(self, baseline_filename):
     42         """Loads the baseline of expected listeners.
     43 
     44         @param baseline_filename: string name of file containing relevant rules.
     45 
     46         """
     47         baseline_path = os.path.join(self.bindir, baseline_filename)
     48         with open(baseline_path) as f:
     49             lines = [line.strip() for line in f.readlines()]
     50         return set([line for line in lines
     51                     if line and not line.startswith('#')])
     52 
     53 
     54     def remove_autotest_noise(self, lsof_lines):
     55         """
     56         Processes underneath 'autotest' in the process tree
     57         unfortunately can inherit open sockets created by
     58         autotest. That leads to crazy-looking test failures where
     59         e.g. "sed" and "bash" appear to be listening on ports
     60         80/443. So, this takes the output of lsof and returns a
     61         filtered subset of it, with autotest and telemetry stuff removed.
     62 
     63         @param lsof_lines: a list of lines as output by the 'lsof' util.
     64         """
     65         # Compile a set of the listening sockets to ignore.
     66         sockets_to_ignore = set([])
     67         for line in lsof_lines:
     68             fields = line.split()
     69             if (fields[_LSOF_COMMAND] == 'autotest' or (
     70                 fields[_LSOF_COMMAND] == 'python' and
     71                 fields[_LSOF_NAME].startswith('127.0.0.1:')) or
     72                 fields[_LSOF_NAME] == '127.0.0.1:%d' %
     73                 utils.get_chrome_remote_debugging_port()):
     74                 sockets_to_ignore.add(fields[_LSOF_DEVICE])
     75 
     76         # Now that we know which ones to ignore, iterate the output again.
     77         lines_to_keep = []
     78         for line in lsof_lines:
     79             fields = line.split()
     80             if fields[_LSOF_DEVICE] in sockets_to_ignore:
     81                 logging.debug('Ignoring %s', line)
     82             else:
     83                 lines_to_keep.append(line)
     84         return lines_to_keep
     85 
     86 
     87     def run_once(self):
     88         """
     89         Compare a list of processes, listening on TCP ports, to a
     90         baseline. Test fails if there are mismatches.
     91         """
     92         baseline_filename = _BASELINE_DEFAULT_NAME
     93         arc_mode = None
     94 
     95         if utils.is_arc_available():
     96             baseline_filename = _BASELINE_ARC_NAME
     97             arc_mode = 'enabled'
     98 
     99         with chrome.Chrome(arc_mode=arc_mode):
    100             cmd = (r'lsof -n -i -sTCP:LISTEN')
    101             cmd_output = utils.system_output(cmd, ignore_status=True,
    102                                              retain_output=True)
    103             # Use the [1:] slice to discard line 0, the lsof output header.
    104             lsof_lines = cmd_output.splitlines()[1:]
    105             # Unlike ps, we don't have a format option so we have to parse
    106             # lines that look like this:
    107             # sshd 1915 root 3u IPv4 9221 0t0 TCP *:ssh (LISTEN)
    108             # Out of that, we just want e.g. sshd *:ssh
    109             observed_set = set([])
    110             for line in self.remove_autotest_noise(lsof_lines):
    111                 fields = line.split()
    112                 observed_set.add('%s %s' % (fields[_LSOF_COMMAND],
    113                                             fields[_LSOF_NAME]))
    114 
    115             baseline_set = self.load_baseline(baseline_filename)
    116             # TODO(wiley) Remove when we get per-board
    117             #             baselines (crbug.com/406013)
    118             if webservd_helper.webservd_is_installed():
    119                 baseline_set.update(self.load_baseline('baseline.webservd'))
    120 
    121             # If something in the observed set is not
    122             # covered by the baseline...
    123             new_listeners = observed_set.difference(baseline_set)
    124             if new_listeners:
    125                 for daemon in new_listeners:
    126                     logging.error('Unexpected network listener: %s', daemon)
    127 
    128             # Or, things in baseline are missing from the system:
    129             missing_listeners = baseline_set.difference(observed_set)
    130             if missing_listeners:
    131                 for daemon in missing_listeners:
    132                     logging.warning('Missing expected network listener: %s',
    133                                     daemon)
    134 
    135             # Only fail if there's unexpected listeners.
    136             if new_listeners:
    137                 raise error.TestFail('Found unexpected network listeners')
    138