"""Text parser plugin for Windows Firewall Log files."""

import pyparsing

from dfdatetime import time_elements as dfdatetime_time_elements

from plaso.containers import events
from plaso.lib import errors
from plaso.parsers import text_parser
from plaso.parsers.text_plugins import interface


class WinFirewallEventData(events.EventData):
  """Windows Firewall event data.

  Attributes:
    action (str): action taken.
    destination_ip (str): destination IP address.
    destination_port (int): TCP or UDP destination port.
    icmp_code (int): ICMP code.
    icmp_type (int): ICMP type.
    information (str): additional information.
    last_written_time (dfdatetime.DateTimeValues): entry last written date and
        time.
    packet_size (int): packet size.
    path (str): direction of the communication, which can be: SEND, RECEIVE,
        FORWARD, and UNKNOWN.
    protocol (str): IP protocol.
    source_ip (str): source IP address.
    source_port (int): TCP or UDP source port.
    tcp_ack (int): TCP acknowledgement number.
    tcp_flags (str): TCP flags.
    tcp_sequence_number (int): TCP sequence number.
    tcp_window_size (int): TCP window size.
  """

  DATA_TYPE = 'windows:firewall_log:entry'

  def __init__(self):
    """Initializes event data."""
    super().__init__(data_type=self.DATA_TYPE)
    self.action = None
    self.destination_ip = None
    self.destination_port = None
    self.icmp_code = None
    self.icmp_type = None
    self.information = None
    self.last_written_time = None
    self.packet_size = None
    self.path = None
    self.protocol = None
    self.source_ip = None
    self.source_port = None
    self.tcp_ack = None
    self.tcp_flags = None
    self.tcp_sequence_number = None
    self.tcp_window_size = None


class WinFirewallLogTextPlugin(interface.TextPlugin):
  """Text parser plugin for Windows Firewall Log files."""

  NAME = 'winfirewall'
  DATA_FORMAT = 'Windows Firewall log file'

  # A Windows Firewall is encoded using the system codepage.
  ENCODING = None

  _TWO_DIGITS = pyparsing.Word(pyparsing.nums, exact=2).set_parse_action(
      lambda tokens: int(tokens[0], 10))

  _FOUR_DIGITS = pyparsing.Word(pyparsing.nums, exact=4).set_parse_action(
      lambda tokens: int(tokens[0], 10))

  _DATE = pyparsing.Group(
      _FOUR_DIGITS + pyparsing.Suppress('-') +
      _TWO_DIGITS + pyparsing.Suppress('-') + _TWO_DIGITS)

  _TIME = pyparsing.Group(
      _TWO_DIGITS + pyparsing.Suppress(':') +
      _TWO_DIGITS + pyparsing.Suppress(':') + _TWO_DIGITS)

  _ACTION = pyparsing.Word(pyparsing.alphanums + '-', min=2)

  _WORD_OR_BLANK = (
      pyparsing.Word(pyparsing.alphanums) | pyparsing.Suppress('-'))

  _IP_ADDRESS_OR_BLANK = (
      pyparsing.pyparsing_common.ipv4_address |
      pyparsing.pyparsing_common.ipv6_address | pyparsing.Suppress('-'))

  _PORT_NUMBER_OR_BLANK = (
      pyparsing.Word(pyparsing.nums, max=6).set_parse_action(
          lambda tokens: int(tokens[0], 10)) | pyparsing.Suppress('-'))

  _INTEGER_OR_BLANK = (
      pyparsing.Word(pyparsing.nums).set_parse_action(
          lambda tokens: int(tokens[0], 10)) | pyparsing.Suppress('-'))

  _END_OF_LINE = pyparsing.Suppress(pyparsing.LineEnd())

  _FIELDS_METADATA = (
      pyparsing.Suppress('Fields: ') +
      pyparsing.restOfLine().set_results_name('fields'))

  _TIME_FORMAT_METADATA = (
      pyparsing.Suppress('Time Format: ') +
      pyparsing.restOfLine().set_results_name('time_format'))

  _METADATA = (
      _FIELDS_METADATA | _TIME_FORMAT_METADATA | pyparsing.restOfLine())

  _COMMENT_LOG_LINE = pyparsing.Suppress('#') + _METADATA + _END_OF_LINE

  # Version 1.5 fields:
  # date time action protocol src-ip dst-ip src-port dst-port size tcpflags
  # tcpsyn tcpack tcpwin icmptype icmpcode info path

  _LOG_LINE_1_5 = (
      _DATE.set_results_name('date') +
      _TIME.set_results_name('time') +
      _ACTION.set_results_name('action') +
      _WORD_OR_BLANK.set_results_name('protocol') +
      _IP_ADDRESS_OR_BLANK.set_results_name('source_ip') +
      _IP_ADDRESS_OR_BLANK.set_results_name('destination_ip') +
      _PORT_NUMBER_OR_BLANK.set_results_name('source_port') +
      _PORT_NUMBER_OR_BLANK.set_results_name('destination_port') +
      _INTEGER_OR_BLANK.set_results_name('packet_size') +
      _WORD_OR_BLANK.set_results_name('tcp_flags') +
      _INTEGER_OR_BLANK.set_results_name('tcp_sequence_number') +
      _INTEGER_OR_BLANK.set_results_name('tcp_ack') +
      _INTEGER_OR_BLANK.set_results_name('tcp_window_size') +
      _INTEGER_OR_BLANK.set_results_name('icmp_type') +
      _INTEGER_OR_BLANK.set_results_name('icmp_code') +
      _WORD_OR_BLANK.set_results_name('information') +
      _WORD_OR_BLANK.set_results_name('path') +
      _END_OF_LINE)

  # Common fields. Set results name with underscores, not hyphens because regex
  # will not pick them up.

  _LOG_LINE_STRUCTURES = {
      'action': _ACTION.set_results_name('action'),
      'date': _DATE.set_results_name('date'),
      'dst-ip': _IP_ADDRESS_OR_BLANK.set_results_name('destination_ip'),
      'dst-port': _PORT_NUMBER_OR_BLANK.set_results_name('destination_port'),
      'icmpcode': _INTEGER_OR_BLANK.set_results_name('icmp_code'),
      'icmptype': _INTEGER_OR_BLANK.set_results_name('icmp_type'),
      'info': _WORD_OR_BLANK.set_results_name('information'),
      'path': _WORD_OR_BLANK.set_results_name('path'),
      'protocol': _WORD_OR_BLANK.set_results_name('protocol'),
      'size': _INTEGER_OR_BLANK.set_results_name('packet_size'),
      'src-ip': _IP_ADDRESS_OR_BLANK.set_results_name('source_ip'),
      'src-port': _PORT_NUMBER_OR_BLANK.set_results_name('source_port'),
      'tcpack': _INTEGER_OR_BLANK.set_results_name('tcp_ack'),
      'tcpflags': _WORD_OR_BLANK.set_results_name('tcp_flags'),
      'tcpsyn': _INTEGER_OR_BLANK.set_results_name('tcp_sequence_number'),
      'tcpwin': _INTEGER_OR_BLANK.set_results_name('tcp_window_size'),
      'time': _TIME.set_results_name('time')}

  _HEADER_GRAMMAR = pyparsing.OneOrMore(_COMMENT_LOG_LINE)

  _LINE_STRUCTURES = [('log_line', _LOG_LINE_1_5)]

  VERIFICATION_GRAMMAR = (
      pyparsing.ZeroOrMore(
          pyparsing.Regex('#(Fields|Time Format|Version): .*') + _END_OF_LINE) +
      pyparsing.Regex('#Software: Microsoft Windows Firewall') + _END_OF_LINE)

  VERIFICATION_LITERALS = ['#Software: Microsoft Windows Firewall ']

  def __init__(self):
    """Initializes a text parser plugin."""
    super().__init__()
    self._use_local_time = False

  def _ParseFieldsMetadata(self, parser_mediator, fields):
    """Parses the fields metadata and updates the log line definition to match.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      fields (str): field definitions.
    """
    log_line_structure = pyparsing.Empty()
    for member in fields.split(' '):
      if not member:
        continue

      field_structure = self._LOG_LINE_STRUCTURES.get(member)
      if not field_structure:
        field_structure = self._WORD_OR_BLANK
        parser_mediator.ProduceExtractionWarning((
            f'missing definition for field: {member:s} defaulting to '
            f'WORD_OR_BLANK'))

      log_line_structure += field_structure

    log_line_structure += self._END_OF_LINE

    self._SetLineStructures([('log_line', log_line_structure)])

  def _ParseHeader(self, parser_mediator, text_reader):
    """Parses a text-log file header.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      text_reader (EncodedTextReader): text reader.

    Raises:
      ParseError: when the header cannot be parsed.
    """
    try:
      structure_generator = self._HEADER_GRAMMAR.scan_string(
          text_reader.lines, max_matches=1)
      structure, start, end = next(structure_generator)

    except StopIteration:
      structure = None

    except pyparsing.ParseException as exception:
      raise errors.ParseError(exception)

    if not structure or start != 0:
      raise errors.ParseError('No match found.')

    fields = self._GetValueFromStructure(structure, 'fields', default_value='')
    fields = fields.strip()
    if fields:
      self._ParseFieldsMetadata(parser_mediator, fields)

    time_format = self._GetValueFromStructure(
        structure, 'time_format', default_value='')
    self._use_local_time = time_format.lower() == 'local'

    text_reader.SkipAhead(end)

  def _ParseLogLine(self, parser_mediator, structure):
    """Parse a single log line.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      structure (pyparsing.ParseResults): tokens from a parsed log line.
    """
    event_data = WinFirewallEventData()
    event_data.action = self._GetValueFromStructure(structure, 'action')
    event_data.destination_ip = self._GetValueFromStructure(
        structure, 'destination_ip')
    event_data.destination_port = self._GetValueFromStructure(
        structure, 'destination_port')
    event_data.icmp_code = self._GetValueFromStructure(structure, 'icmp_code')
    event_data.icmp_type = self._GetValueFromStructure(structure, 'icmp_type')
    event_data.information = self._GetValueFromStructure(
        structure, 'information')
    event_data.last_written_time = self._ParseTimeElements(structure)
    event_data.path = self._GetValueFromStructure(structure, 'path')
    event_data.protocol = self._GetValueFromStructure(structure, 'protocol')
    event_data.packet_size = self._GetValueFromStructure(
        structure, 'packet_size')
    event_data.source_ip = self._GetValueFromStructure(structure, 'source_ip')
    event_data.source_port = self._GetValueFromStructure(
        structure, 'source_port')
    event_data.tcp_ack = self._GetValueFromStructure(structure, 'tcp_ack')
    event_data.tcp_flags = self._GetValueFromStructure(structure, 'tcp_flags')
    event_data.tcp_sequence_number = self._GetValueFromStructure(
        structure, 'tcp_sequence_number')
    event_data.tcp_window_size = self._GetValueFromStructure(
        structure, 'tcp_window_size')

    parser_mediator.ProduceEventData(event_data)

  def _ParseRecord(self, parser_mediator, key, structure):
    """Parses a pyparsing structure.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      key (str): name of the parsed structure.
      structure (pyparsing.ParseResults): tokens from a parsed log line.

    Raises:
      ParseError: if the structure cannot be parsed.
    """
    self._ParseLogLine(parser_mediator, structure)

  def _ParseTimeElements(self, structure):
    """Parses date and time elements of a log line.

    Args:
      structure (pyparsing.ParseResults): tokens from a parsed log line.

    Returns:
      dfdatetime.TimeElements: date and time value.

    Raises:
      ParseError: if a valid date and time value cannot be derived from
          the time elements.
    """
    try:
      date_elements_structure = self._GetValueFromStructure(structure, 'date')
      time_elements_structure = self._GetValueFromStructure(structure, 'time')

      year, month, day_of_month = date_elements_structure
      hours, minutes, seconds = time_elements_structure

      time_elements_tuple = (year, month, day_of_month, hours, minutes, seconds)

      date_time = dfdatetime_time_elements.TimeElements(
          time_elements_tuple=time_elements_tuple)
      date_time.is_local_time = self._use_local_time

      return date_time

    except (IndexError, TypeError, ValueError) as exception:
      raise errors.ParseError(
          f'Unable to parse time elements with error: {exception!s}')

  def _ResetState(self):
    """Resets stored values."""
    self._use_local_time = False

    self._SetLineStructures(self._LINE_STRUCTURES)

  def CheckRequiredFormat(self, parser_mediator, text_reader):
    """Check if the log record has the minimal structure required by the plugin.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      text_reader (EncodedTextReader): text reader.

    Returns:
      bool: True if this is the correct plugin, False otherwise.
    """
    try:
      self._VerifyString(text_reader.lines)
    except errors.ParseError:
      return False

    self._ResetState()

    return True


text_parser.TextLogParser.RegisterPlugin(WinFirewallLogTextPlugin)
