# -*- coding: utf-8 -*-
"""Processor that parses OpenSSH sshd log messages into hitdb-ready properties."""
import re
from logging import getLogger
from .base_processor import BaseProcessor
MESSAGES = [
re.compile(r"(?P<error>Invalid user) (?P<user>.*?) from (?P<ip>\S+)"),
re.compile(
r"(?P<action>input_userauth_request): (?P<error>invalid user) (?P<user>.*)"
),
re.compile(
r"(?:error: )?(?P<action>Received disconnect)"
r" from (?P<ip>\S+) port \d+:\d+: (?P<error>.+)?"
),
re.compile(
r"(?P<action>Starting session): (?P<resource>.+?)"
r" for (?P<user>.+?) from (?P<ip>\S+)"
),
re.compile(r"(?P<error>Did not receive identification string) from (?P<ip>\S+)"),
re.compile(
r"(?P<action>(?:Connection|Disconnected)) from user (?P<user>\S+) (?P<ip>\S+)"
),
re.compile(r"(?P<action>(?:Connection|Disconnected)) from (?P<ip>\S+)"),
re.compile(
r"(?P<action>(?:Accepted|Failed) publickey) for (?P<user>.+?)"
r" from (?P<ip>\S+) port \d+ \S+: (?P<resource>.+)"
),
re.compile(r"(?P<action>Accepted key) (?P<resource>.+?) found at "),
re.compile(
r"pam_unix.sshd:session.: (?P<action>session (?:opened|closed))"
r" for user (?P<user>\S+)"
),
re.compile(r"(?P<action>Closing connection) to (?P<ip>\S+)"),
re.compile(r"(?P<action>Close session): user (?P<user>.+?) from (?P<ip>\S+)"),
re.compile(
r"error: (?P<error>maximum authentication attempts exceeded)"
r" for (invalid user )?(?P<user>.+?) from (?P<ip>\S+)"
),
re.compile(r"(?P<error>Bad protocol version .+?) from (?P<ip>\S+)"),
re.compile(r"(?P<action>Disconnecting): (?P<error>.+)"),
re.compile(r"(?P<action>Connection (?:reset|closed)) by (?P<ip>\S+)"),
re.compile(
r"fatal: (?P<action>Unable to negotiate)"
r" with (?P<ip>\S+) port \d+: (?P<error>.+?)."
r" Their offer: (?P<resource>.+)"
),
re.compile(
r"(?P<action>Read error) from remote host"
r" (?P<ip>\S+)(?: port \d+)?: (?P<error>.+)"
),
re.compile(r"error: (?P<action>kex_exchange_identification): (?P<error>.+)"),
re.compile(r"(?P<action>Postponed publickey) for (?P<user>.+?) from (?P<ip>\S+)"),
re.compile(r"channel \d+: open failed: (?P<action>connect failed): (?P<error>.+)"),
re.compile(r"(?P<action>(?:Transferred|User child))[: ]"),
]
class OpenSshdParser(BaseProcessor):
"""Processor that parses OpenSSH sshd log messages into hitdb-ready properties."""
LOG = getLogger(__name__)
def __init__(self, property="message", ignore_missing=False): # noqa: A002
"""Creates new processor.
Arguments:
property (str): Log-data property to parse (defaults to 'message').
ignore_missing (str): True to suppress errors when property is missing
(defaults to False).
"""
self.property = property
self.ignore_missing = ignore_missing
# complexity is high, but still fairly readable
def process(self, data): # noqa: CCR001
"""Processes the specified log-entry data.
Arguments:
data (dict): Dict of log-entry data.
Returns:
dict: Updated dict of log-entry data.
Raises:
KeyError: Configured property not in specified data.
"""
if not self.ignore_missing and self.property not in data:
raise KeyError("missing {} property: {}".format(self.property, data))
message = data.get(self.property)
if not message:
return data
# strip [preauth] from end of message
preauth = False
if message.endswith(" [preauth]"):
message = message[:-10]
preauth = True
for pattern in MESSAGES:
match = pattern.match(message)
if match:
break
if not match:
self.LOG.warning("opensshd message not matched: %s", message)
return data
groupdict = match.groupdict("")
data.update(groupdict)
# normalize action and error
_lowercase_first_letter(data, "action")
_lowercase_first_letter(data, "error")
# add [preauth] to action if present
if preauth and "action" in groupdict:
data["action"] += " [preauth]"
return data
def _lowercase_first_letter(data, key):
value = data.get(key, "")
if value:
data[key] = value[0].lower() + value[1:]