Source code for insights.parsers.ssh

"""
SshDConfig - file ``/etc/ssh/sshd_config``
==========================================

The ``ssh`` module provides parsing for the ``sshd_config``
file.  The ``SshDConfig`` class implements the parsing and
provides a ``list`` of all configuration lines present in
the file.

Sample input is provided in the *Examples*.

Examples:
    >>> sshd_config_input = '''
    ... #	$OpenBSD: sshd_config,v 1.93 2014/01/10 05:59:19 djm Exp $
    ...
    ... Port 22
    ... #AddressFamily any
    ... ListenAddress 10.110.0.1
    ... Port 22
    ... ListenAddress 10.110.1.1
    ... #ListenAddress ::
    ...
    ... # The default requires explicit activation of protocol 1
    ... #Protocol 2
    ... Protocol 1
    ... '''.strip()
    >>> from insights.tests import context_wrap
    >>> shared = {SshDConfig: SshDConfig(context_wrap(sshd_config_input))}
    >>> sshd_config = shared[SshDConfig]
    >>> 'Port' in sshd_config
    True
    >>> 'PORT' in sshd_config
    True
    >>> 'AddressFamily' in sshd_config
    False
    >>> sshd_config['port']
    ['22', '22']
    >>> sshd_config['Protocol']
    ['1']
    >>> [line for line in sshd_config if line.keyword == 'Port']
    [KeyValue(keyword='Port', value='22', kw_lower='port'), KeyValue(keyword='Port', value='22', kw_lower='port')]
    >>> sshd_config.last('ListenAddress')
    '10.110.1.1'
    >>> sshd_config.get_line('ListenAddress')
    'ListenAddress 10.110.1.1'
    >>> sshd_config.get_values('ListenAddress')
    ['10.110.0.1', '10.110.1.1']
    >>> sshd_config.get_values('ListenAddress', default='0.0.0.0')
    ['10.110.0.1', '10.110.1.1']
    >>> sshd_config.get_values('ListenAddress', join_with=',')
    '10.110.0.1,10.110.1.1'
"""
from collections import namedtuple
from .. import Parser, parser, get_active_lines
from insights.specs import Specs
import re

# optional whitespace, at least one non-whitespace (the keyword), at least one whitespace (space), a plus literal, anything
PLUS_PATTERN = re.compile(r'^\s*\S+\s+\+.*$')


[docs]@parser(Specs.sshd_config) class SshDConfig(Parser): """Parsing for ``/etc/ssh/sshd_config`` file. Properties: lines (list): List of `KeyValue` namedtupules for each line in the configuration file. keywords (set): Set of keywords present in the configuration file, each keyword has been converted to lowercase. """ KeyValue = namedtuple('KeyValue', ['keyword', 'value', 'kw_lower', 'line']) """namedtuple: Represent name value pair as a namedtuple with case .""" # Note about "+" options. In some openssh versions and for some keywords, # a "+" at the beginning of the value list signifies that the values are # added to the default list of values. # IT IS NOT DETECTED AND PARSED HERE. # `parse_content()` doesn't split the individual values because some # keywords don't have a list of values (just one string). # See the docstring in `line_uses_plus()` for more details. # Re: BZ#1697477 # Config lines may also be delimited by `=`, and values may be quoted # with `"`. Here it is assumed that config lines are well-formed.
[docs] def parse_content(self, content): self.lines = [] for line in get_active_lines(content): line_splits = [s.strip() for s in re.split(r"[\s=]+", line, 1)] kw, val = line_splits[0], line_splits[1].strip('"') if \ len(line_splits) == 2 else '' self.lines.append(self.KeyValue( kw, val, kw.lower(), line )) self.keywords = set([k.kw_lower for k in self.lines])
def __contains__(self, keyword): """ (bool) Is this keyword found in the configuration file? """ return keyword.lower() in self.keywords def __iter__(self): """ (iter) Yields a list of all the lines in the configuration file that actually contained a configuration option. """ for line in self.lines: yield line def __getitem__(self, keyword): """ (list) Get all values of this keyword. """ kw = keyword.lower() if kw in self.keywords: return [kv.value for kv in self.lines if kv.kw_lower == kw]
[docs] def get(self, keyword): """ Get all declarations of this keyword in the configuration file. Returns: (list): a list of named tuples with the following properties: - ``keyword`` - the keyword as given on that line - ``value`` - the value of the keyword - ``kw_lower`` - the keyword converted to lower case - ``line`` - the complete line as found in the config file """ kw = keyword.lower() return [kv for kv in self.lines if kv.kw_lower == kw]
[docs] def get_values(self, keyword, default='', join_with=None, split_on=None): """ Get all the values assigned to this keyword. Firstly, if the keyword is not found in the configuration file, the value of the ``default`` option is used (defaulting to ``''``). Then, if the ``join_with`` option is given, this string is used to join the values found on each separate definition line. Otherwise, each separate definition line is returned as a string. Finally, if the ``split_on`` option is given, this string is used to split the combined string above into a list. Otherwise, the combined string is returned as is. """ kw = keyword.lower() if kw in self.keywords: vals = [kv.value for kv in self.lines if kv.kw_lower == kw] else: vals = [default] # Should we join them together? if join_with is None: return vals # OK, join them together. val_str = join_with.join(vals) # Should we now split them? if split_on is None: return val_str else: return val_str.split(split_on)
[docs] def get_line(self, keyword, default=''): """ (str): Get the line with the last declarations of this keyword in the configuration file, optionally pretending that we had a line with the default value and a comment informing the user that this was a created default line. This is a hack, but it's commonly used in the sshd configuration because of the many lines that are commonly omitted because they have their default value. Parameters: keyword(str): Keyword to find default: optional value to supply if not found """ lines = self.get(keyword) if lines: return lines[-1].line else: return "{keyword} {value} # Implicit default".format( keyword=keyword, value=default )
[docs] def line_uses_plus(self, keyword): """ (union[bool, None]): Get the line with the last declarations of this keyword in the configuration file and returns whether the "+" option syntax is used. A "+" before the list of values denotes that the values are appended to the openssh defaults for the particular keyword. Returns True if the "+" is used, False if a line with the keyword was found but it doesn't use the "+" or None if such a line doesn't exist. Reasoning for the implementation: * The "+" means "added to the defaults". * The defaults depend on the particular openssh-server version and the parser doesn't know the version. * Therefore, it is infeasible to add the evaluation logic for "+" into `get_values()`. * Adding the logic into a combiner would mean a requirement that the combiner has a complete database of all defaults in all openssh-server version - infeasible again. * Not every keyword allows the use of "+" - it wouldn't make sense to parse "+" into `KeyValue` as it would make meaningless parsing for some options and meaningful for others. Building a database which options in which openssh-server versions support it or not would be infeasible. * The way chosen as the most sensible is this - `line_uses_plus()` used selectively by a rule for those options that support it, and it is up to the developer of such a rule to check it for those options manually. Parameters: keyword(str): Keyword to find """ lines = self.get(keyword) if lines: found_line = lines[-1].line match = PLUS_PATTERN.match(found_line) return bool(match) else: return None
[docs] def last(self, keyword, default=None): """ str: Returns the value of the last keyword found in config. Parameters: keyword(str): Keyword to find default: optional value to supply if not found """ entries = self.__getitem__(keyword) if entries: return entries[-1] else: return default