"""Text file parser plugin for Santa 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 SantaMountEventData(events.EventData):
    """Santa mount event data.

    Attributes:
      action (str): event type recorded by Santa.
      appearance_time (dfdatetime.DateTimeValues): date and time the disk
          appeared.
      bsd_name (str): disk BSD name.
      bus (str): device protocol.
      dmg_path (str): DMG file path.
      fs (str): disk volume kind.
      last_written_time (dfdatetime.DateTimeValues): entry last written date and
          time.
      model (str): disk model.
      mount (str): disk mount point.
      serial (str): disk serial.
      volume (str): disk volume name.
    """

    DATA_TYPE = "santa:diskmount"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.action = None
        self.appearance_time = None
        self.bsd_name = None
        self.bus = None
        self.dmg_path = None
        self.fs = None
        self.last_written_time = None
        self.model = None
        self.mount = None
        self.serial = None
        self.volume = None


class SantaExecutionEventData(events.EventData):
    """Santa execution event data.

    Attributes:
      action (str): action recorded by Santa.
      certificate_common_name (str): certificate common name.
      certificate_hash (str): SHA256 hash for the certificate associated with the
          executed process.
      decision (str): if the process was allowed or blocked.
      gid (str): group identifier associated with the executed process.
      group (str): group name associated with the executed process.
      last_run_time (dfdatetime.DateTimeValues): executable (binary) last run
          date and time.
      long_reason (str): further explanation behind Santa decision to execute
          or block a process.
      mode (str): Santa execution mode, for example Monitor or Lockdown.
      pid (str): process identifier for the process.
      pid_version (str): the process identifier version extracted from the Mach
          audit token. The version can sed to identify process identifier
          rollovers.
      ppid (str): parent process identifier for the executed process.
      process_arguments (str): executed process with its arguments.
      process_hash (str): SHA256 hash for the executed process.
      process_path (str): process file path.
      reason (str): reason behind Santa decision to execute or block a process.
      uid (str): user identifier associated with the executed process.
      username (str): username associated with the executed process.
    """

    DATA_TYPE = "santa:execution"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.action = None
        self.certificate_common_name = None
        self.certificate_hash = None
        self.decision = None
        self.gid = None
        self.group = None
        self.last_run_time = None
        self.long_reason = None
        self.mode = None
        self.pid = None
        self.pid_version = None
        self.ppid = None
        self.process_arguments = None
        self.process_hash = None
        self.process_path = None
        self.quarantine_url = None
        self.reason = None
        self.uid = None
        self.username = None


class SantaFileSystemEventData(events.EventData):
    """Santa file system event data.

    Attributes:
      action (str): event type recorded by Santa.
      file_new_path (str): new file path and name for RENAME events.
      file_path (str): file path and name for WRITE/DELETE events.
      gid (str): group identifier associated with the executed process.
      group (str): group name associated with the executed process.
      last_written_time (dfdatetime.DateTimeValues): entry last written date and
          time.
      pid (str): process identifier for the process.
      pid_version (str): the process identifier version extracted from the Mach
          audit token. The version can be used to identify process identifier
          rollovers.
      ppid (str): parent process identifier for the executed process.
      process_path (str): process file path.
      process (str): process name.
      uid (str): user identifier associated with the executed process.
      username (str): username associated with the executed process.
    """

    DATA_TYPE = "santa:file_system_event"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.action = None
        self.file_new_path = None
        self.file_path = None
        self.gid = None
        self.group = None
        self.last_written_time = None
        self.pid = None
        self.pid_version = None
        self.ppid = None
        self.process = None
        self.process_path = None
        self.uid = None
        self.username = None


class SantaProcessExitEventData(events.EventData):
    """Santa process exit event data.

    Attributes:
      action (str): action recorded by Santa.
      exit_time (dfdatetime.DateTimeValues): process exit date and time.
      gid (str): group identifier associated with the executed process.
      pid (str): process identifier for the process.
      pid_version (str): the process identifier version extracted from the Mach
          audit token. The version can be used to identify process identifier
          rollovers.
      ppid (str): parent process identifier for the executed process.
      uid (str): user identifier associated with the executed process.
    """

    DATA_TYPE = "santa:process_exit"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.action = None
        self.exit_time = None
        self.gid = None
        self.pid = None
        self.pid_version = None
        self.ppid = None
        self.uid = None


class SantaTextPlugin(interface.TextPlugin):
    """Text file parser plugin for Santa log files."""

    NAME = "santa"
    DATA_FORMAT = "Santa log (santa.log) file"

    ENCODING = "utf-8"

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

    _THREE_DIGITS = pyparsing.Word(pyparsing.nums, exact=3).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)
    )

    _SEPARATOR = pyparsing.Suppress("|")

    # Using CharsNotIn is more efficient than SkipTo('|'). Optional is needed
    # since CharsNotIn does not support a minimum of 0.
    _SKIP_TO_SEPARATOR = pyparsing.Optional(pyparsing.CharsNotIn("|\n"))

    # Date and time values are formatted as: 2018-08-19T03:09:13.120Z
    _DATE_AND_TIME = (
        _FOUR_DIGITS
        + pyparsing.Suppress("-")
        + _TWO_DIGITS
        + pyparsing.Suppress("-")
        + _TWO_DIGITS
        + pyparsing.Suppress("T")
        + _TWO_DIGITS
        + pyparsing.Suppress(":")
        + _TWO_DIGITS
        + pyparsing.Suppress(":")
        + _TWO_DIGITS
        + pyparsing.Suppress(".")
        + _THREE_DIGITS
        + pyparsing.Suppress("Z")
    ).set_results_name("date_time")

    _DATE_TIME_BLOCK = (
        pyparsing.Suppress("[") + _DATE_AND_TIME + pyparsing.Suppress("]")
    )

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

    _SANTAD_PREAMBLE = pyparsing.Suppress("I santad:")

    _APPEARANCE = pyparsing.Suppress("|appearance=") + _DATE_AND_TIME.set_results_name(
        "appearance"
    )

    _ARGS = pyparsing.Suppress("|args=") + _SKIP_TO_SEPARATOR.set_results_name("args")

    _BSD_NAME = pyparsing.Suppress("|bsdname=") + _SKIP_TO_SEPARATOR.set_results_name(
        "bsd_name"
    )

    _BUS = pyparsing.Suppress("|bus=") + _SKIP_TO_SEPARATOR.set_results_name("bus")

    _CERT_CN = pyparsing.Suppress("|cert_cn=") + _SKIP_TO_SEPARATOR.set_results_name(
        "cert_cn"
    )

    _CERT_SHA256 = pyparsing.Suppress(
        "|cert_sha256="
    ) + _SKIP_TO_SEPARATOR.set_results_name("cert_sha256")

    _DECISION = pyparsing.Suppress("|decision=") + _SKIP_TO_SEPARATOR.set_results_name(
        "decision"
    )

    _DMG_PATH = pyparsing.Suppress("|dmgpath=") + _SKIP_TO_SEPARATOR.set_results_name(
        "dmg_path"
    )

    _EXPLAIN = pyparsing.Suppress("|explain=") + _SKIP_TO_SEPARATOR.set_results_name(
        "explain"
    )

    _FILE_SYSTEM = pyparsing.Suppress("|fs=") + _SKIP_TO_SEPARATOR.set_results_name(
        "fs"
    )

    _GID = pyparsing.Suppress("|gid=") + _SKIP_TO_SEPARATOR.set_results_name("gid")

    _GROUP = pyparsing.Suppress("|group=") + _SKIP_TO_SEPARATOR.set_results_name(
        "group"
    )

    _MODE = pyparsing.Suppress("|mode=") + _SKIP_TO_SEPARATOR.set_results_name("mode")

    _MODEL = pyparsing.Suppress("|model=") + _SKIP_TO_SEPARATOR.set_results_name(
        "model"
    )

    _MOUNT = pyparsing.Suppress("|mount=") + _SKIP_TO_SEPARATOR.set_results_name(
        "mount"
    )

    _NEW_PATH = pyparsing.Suppress("|newpath=") + _SKIP_TO_SEPARATOR.set_results_name(
        "newpath"
    )

    _QUARANTINE_URL = pyparsing.Suppress(
        "|quarantine_url="
    ) + _SKIP_TO_SEPARATOR.set_results_name("quarantine_url")

    _PATH = pyparsing.Suppress("|path=") + _SKIP_TO_SEPARATOR.set_results_name("path")

    _PID = pyparsing.Suppress("|pid=") + _SKIP_TO_SEPARATOR.set_results_name("pid")

    _PID_VERSION = pyparsing.Suppress(
        "|pidversion="
    ) + _SKIP_TO_SEPARATOR.set_results_name("pidversion")

    _PPID = pyparsing.Suppress("|ppid=") + _SKIP_TO_SEPARATOR.set_results_name("ppid")

    _PROCESS = pyparsing.Suppress("|process=") + _SKIP_TO_SEPARATOR.set_results_name(
        "process"
    )

    _PROCESS_PATH = pyparsing.Suppress(
        "|processpath="
    ) + _SKIP_TO_SEPARATOR.set_results_name("processpath")

    _REASON = pyparsing.Suppress("|reason=") + _SKIP_TO_SEPARATOR.set_results_name(
        "reason"
    )

    _SERIAL = pyparsing.Suppress("|serial=") + _SKIP_TO_SEPARATOR.set_results_name(
        "serial"
    )

    _SHA256 = pyparsing.Suppress("|sha256=") + _SKIP_TO_SEPARATOR.set_results_name(
        "sha256"
    )

    _UID = pyparsing.Suppress("|uid=") + _SKIP_TO_SEPARATOR.set_results_name("uid")

    _USER = pyparsing.Suppress("|user=") + _SKIP_TO_SEPARATOR.set_results_name(
        "username"
    )

    _VOLUME = pyparsing.Suppress("|volume=") + _SKIP_TO_SEPARATOR.set_results_name(
        "volume"
    )

    _DISK_MOUNT_LINE = (
        _DATE_TIME_BLOCK
        + _SANTAD_PREAMBLE
        + pyparsing.Suppress("action=")
        + pyparsing.Literal("DISKAPPEAR").set_results_name("action")
        + _MOUNT
        + _VOLUME
        + _BSD_NAME
        + _FILE_SYSTEM
        + _MODEL
        + _SERIAL
        + _BUS
        + _DMG_PATH
        + _APPEARANCE
        + _END_OF_LINE
    )

    _DISK_UNMOUNT_LINE = (
        _DATE_TIME_BLOCK
        + _SANTAD_PREAMBLE
        + pyparsing.Suppress("action=")
        + pyparsing.Literal("DISKDISAPPEAR").set_results_name("action")
        + _MOUNT
        + _VOLUME
        + _BSD_NAME
        + _END_OF_LINE
    )

    _EXECUTION_LINE = (
        _DATE_TIME_BLOCK
        + _SANTAD_PREAMBLE
        + pyparsing.Suppress("action=")
        + pyparsing.Literal("EXEC").set_results_name("action")
        + _DECISION
        + _REASON
        + pyparsing.Optional(_EXPLAIN)
        + _SHA256
        + pyparsing.Optional(_CERT_SHA256)
        + pyparsing.Optional(_CERT_CN)
        + pyparsing.Optional(_QUARANTINE_URL)
        + _PID
        + pyparsing.Optional(_PID_VERSION)
        + _PPID
        + _UID
        + _USER
        + _GID
        + _GROUP
        + _MODE
        + _PATH
        + pyparsing.Optional(_ARGS)
        + _END_OF_LINE
    )

    _FILE_OPERATION_LINE = (
        _DATE_TIME_BLOCK
        + _SANTAD_PREAMBLE
        + pyparsing.Suppress("action=")
        + (
            pyparsing.Literal("DELETE")
            ^ pyparsing.Literal("LINK")
            ^ pyparsing.Literal("RENAME")
            ^ pyparsing.Literal("WRITE")
        ).set_results_name("action")
        + _PATH
        + pyparsing.Optional(_NEW_PATH)
        + _PID
        + pyparsing.Optional(_PID_VERSION)
        + _PPID
        + _PROCESS
        + _PROCESS_PATH
        + _UID
        + _USER
        + _GID
        + _GROUP
        + _END_OF_LINE
    )

    _PROCESS_EXIT_LINE = (
        _DATE_TIME_BLOCK
        + _SANTAD_PREAMBLE
        + pyparsing.Suppress("action=")
        + pyparsing.Literal("EXIT").set_results_name("action")
        + _PID
        + _PID_VERSION
        + _PPID
        + _UID
        + _GID
        + _END_OF_LINE
    )

    _QUOTA_EXCEEDED_LINE = (
        _DATE_TIME_BLOCK
        + pyparsing.Literal(
            (
                "*** LOG MESSAGE QUOTA EXCEEDED - SOME MESSAGES FROM THIS PROCESS "
                "HAVE BEEN DISCARDED ***"
            )
        )
        + _END_OF_LINE
    )

    _LINE_STRUCTURES = [
        ("execution_line", _EXECUTION_LINE),
        ("file_system_event_line", _FILE_OPERATION_LINE),
        ("mount_line", _DISK_MOUNT_LINE),
        ("process_exit_line", _PROCESS_EXIT_LINE),
        ("quota_exceeded_line", _QUOTA_EXCEEDED_LINE),
        ("unmount_line", _DISK_UNMOUNT_LINE),
    ]

    VERIFICATION_GRAMMAR = (
        _DISK_MOUNT_LINE
        ^ _DISK_UNMOUNT_LINE
        ^ _EXECUTION_LINE
        ^ _FILE_OPERATION_LINE
        ^ _PROCESS_EXIT_LINE
    )

    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.
        """
        if key == "quota_exceeded_line":
            # skip this line
            return

        time_elements_structure = self._GetValueFromStructure(structure, "date_time")

        date_time = self._ParseTimeElements(time_elements_structure)

        if key == "execution_line":
            event_data = SantaExecutionEventData()
            event_data.action = self._GetValueFromStructure(structure, "action")
            event_data.certificate_common_name = self._GetValueFromStructure(
                structure, "cert_cn"
            )
            event_data.certificate_hash = self._GetValueFromStructure(
                structure, "cert_sha256"
            )
            event_data.decision = self._GetValueFromStructure(structure, "decision")
            event_data.gid = self._GetValueFromStructure(structure, "gid")
            event_data.group = self._GetValueFromStructure(structure, "group")
            event_data.last_run_time = date_time
            event_data.long_reason = self._GetValueFromStructure(structure, "explain")
            event_data.mode = self._GetValueFromStructure(structure, "mode")
            event_data.quarantine_url = self._GetValueFromStructure(
                structure, "quarantine_url"
            )
            event_data.pid = self._GetValueFromStructure(structure, "pid")
            event_data.pid_version = self._GetValueFromStructure(
                structure, "pidversion"
            )
            event_data.ppid = self._GetValueFromStructure(structure, "ppid")
            event_data.process_arguments = self._GetValueFromStructure(
                structure, "args"
            )
            event_data.process_hash = self._GetValueFromStructure(structure, "sha256")
            event_data.process_path = self._GetValueFromStructure(structure, "path")
            event_data.reason = self._GetValueFromStructure(structure, "reason")
            event_data.uid = self._GetValueFromStructure(structure, "uid")
            event_data.username = self._GetValueFromStructure(structure, "username")

        if key == "process_exit_line":
            event_data = SantaProcessExitEventData()
            event_data.action = self._GetValueFromStructure(structure, "action")
            event_data.exit_time = date_time
            event_data.pid = self._GetValueFromStructure(structure, "pid")
            event_data.pid_version = self._GetValueFromStructure(
                structure, "pidversion"
            )
            event_data.ppid = self._GetValueFromStructure(structure, "ppid")
            event_data.uid = self._GetValueFromStructure(structure, "uid")
            event_data.gid = self._GetValueFromStructure(structure, "gid")

        elif key == "file_system_event_line":
            event_data = SantaFileSystemEventData()
            event_data.action = self._GetValueFromStructure(structure, "action")
            event_data.file_new_path = self._GetValueFromStructure(structure, "newpath")
            event_data.file_path = self._GetValueFromStructure(structure, "path")
            event_data.gid = self._GetValueFromStructure(structure, "gid")
            event_data.group = self._GetValueFromStructure(structure, "group")
            event_data.last_written_time = date_time
            event_data.pid = self._GetValueFromStructure(structure, "pid")
            event_data.pid_version = self._GetValueFromStructure(
                structure, "pidversion"
            )
            event_data.ppid = self._GetValueFromStructure(structure, "ppid")
            event_data.process = self._GetValueFromStructure(structure, "process")
            event_data.process_path = self._GetValueFromStructure(
                structure, "processpath"
            )
            event_data.uid = self._GetValueFromStructure(structure, "uid")
            event_data.username = self._GetValueFromStructure(structure, "username")

        elif key == "unmount_line":
            event_data = SantaMountEventData()
            event_data.action = self._GetValueFromStructure(structure, "action")
            event_data.bsd_name = self._GetValueFromStructure(structure, "bsd_name")
            event_data.last_written_time = date_time
            event_data.mount = self._GetValueFromStructure(structure, "mount") or None
            event_data.volume = self._GetValueFromStructure(structure, "volume")

        elif key == "mount_line":
            event_data = SantaMountEventData()
            event_data.action = self._GetValueFromStructure(structure, "action")
            event_data.bsd_name = self._GetValueFromStructure(structure, "bsd_name")
            event_data.bus = self._GetValueFromStructure(structure, "bus")
            event_data.dmg_path = self._GetValueFromStructure(structure, "dmg_path")
            event_data.fs = self._GetValueFromStructure(structure, "fs")
            event_data.last_written_time = date_time
            event_data.model = self._GetValueFromStructure(structure, "model")
            event_data.mount = self._GetValueFromStructure(structure, "mount") or None
            event_data.serial = self._GetValueFromStructure(structure, "serial") or None
            event_data.volume = self._GetValueFromStructure(structure, "volume")

            appearance = self._GetValueFromStructure(structure, "appearance")
            if appearance:
                # pylint: disable=attribute-defined-outside-init
                event_data.appearance_time = self._ParseTimeElements(appearance)

        parser_mediator.ProduceEventData(event_data)

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

        Args:
          time_elements_structure (pyparsing.ParseResults): date and time elements
              of a 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:
            year, month, day_of_month, hours, minutes, seconds, milliseconds = (
                time_elements_structure
            )
            # Ensure time_elements_tuple is not a pyparsing.ParseResults otherwise
            # copy.deepcopy() of the dfDateTime object will fail on Python 3.8 with:
            # "TypeError: 'str' object is not callable" due to pyparsing.ParseResults
            # overriding __getattr__ with a function that returns an empty string
            # when named token does not exist.
            time_elements_tuple = (
                year,
                month,
                day_of_month,
                hours,
                minutes,
                seconds,
                milliseconds,
            )
            return dfdatetime_time_elements.TimeElementsInMilliseconds(
                time_elements_tuple=time_elements_tuple
            )

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

    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:
            structure = self._VerifyString(text_reader.lines)
        except errors.ParseError:
            return False

        time_elements_structure = self._GetValueFromStructure(structure, "date_time")

        try:
            self._ParseTimeElements(time_elements_structure)
        except errors.ParseError:
            return False

        return True


text_parser.TextLogParser.RegisterPlugin(SantaTextPlugin)
