"""Parser for the Microsoft User Access Logging (UAL) ESE database.

User Access Logging (UAL) is present in Windows Server editions starting with
Window Server 2012.

Also see:
  https://www.crowdstrike.com/en-us/blog/user-access-logging-ual-overview
"""

import ipaddress
import uuid

from dfvfs.resolver import resolver as path_spec_resolver
from dfvfs.path import factory as path_spec_factory

from plaso.containers import events
from plaso.parsers import esedb
from plaso.parsers.esedb_plugins import interface


class UserAccessLoggingClientsEventsData(events.EventData):
    """Windows User Access Logging CLIENTS table event data.

    Attributes:
      access_time (dfdatetime.DateTimeValues): last access date and time.
      authenticated_username (str): domain/user account name performing the access.
      client_name (str): client name, use unknown.
      insert_time (dfdatetime.DateTimeValues): date and time the entry was first
          inserted into the table.
      role_identifier (str): identifier of the service accessed.
      role_name (str): Name of the service accessed.
      source_ip_address (str): source IP address.
      tenant_identifier (str): unique identifier of a tenant client.
      total_accesses (int): Count of accesses for the year.
    """

    DATA_TYPE = "windows:user_access_logging:clients"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.access_time = None
        self.authenticated_username = None
        self.client_name = None
        self.insert_time = None
        self.role_identifier = None
        self.role_name = None
        self.source_ip_address = None
        self.tenant_identifier = None
        self.total_accesses = None


class UserAccessLoggingDNSEventData(events.EventData):
    """Windows User Access Logging DNS table event data.

    Attributes:
      hostname (str): hostname.
      ip_address (str): IP address.
      last_seen_time (dfdatetime.DateTimeValues): date and time the hostname to
          IP address mapping was last observed.
    """

    DATA_TYPE = "windows:user_access_logging:dns"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.hostname = None
        self.ip_address = None
        self.last_seen_time = None


class UserAccessLoggingRoleAccessEventsData(events.EventData):
    """Windows User Access Logging ROLE_ACCESS table event data.

    Attributes:
      first_seen_time (dfdatetime.DateTimeValues): date and time the role was
          first observed to be used.
      last_seen_time (dfdatetime.DateTimeValues): date and time the role was
          last observed to be used.
      role_identifier (str): identifier of the role.
      role_name (str): name of the role.
    """

    DATA_TYPE = "windows:user_access_logging:role_access"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.first_seen_time = None
        self.last_seen_time = None
        self.role_identifier = None
        self.role_name = None


class UserAccessLoggingSystemIdentityEventdata(events.EventData):
    """Windows User Access Logging SYSTEM_IDENTITY table event data.

    Attributes:
      creation_time (dfdatetime.DateTimeValues): date and time the system
          identity was created.
      operating_system_build (int): operating system build.
      system_dns_hostname (str): System hostname.
      system_domain_name (str): System domain name.
    """

    DATA_TYPE = "windows:user_access_logging:system_identity"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.creation_time = None
        self.operating_system_build = None
        self.system_dns_hostname = None
        self.system_domain_name = None


class UserAccessLoggingVirtualMachinesEventData(events.EventData):
    """Windows User Access Logging VIRTUALMACHINES table event data.

    Attributes:
      bios_identifier (str): BIOS identifier.
      creation_time (dfdatetime.DateTimeValues): date and time the virtual
          machine was created.
      last_active_time (dfdatetime.DateTimeValues): date and time the virtual
          machine was last observed to be active.
      serial_number (str): Serial number.
      vm_identifier (str): identifier of the virtual machine.
    """

    DATA_TYPE = "windows:user_access_logging:virtual_machines"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.bios_identifier = None
        self.creation_time = None
        self.last_active_time = None
        self.serial_number = None
        self.vm_identifier = None


class UserAccessLoggingESEDBPlugin(interface.ESEDBPlugin):
    """Parses Windows User Access Logging ESE database file."""

    NAME = "user_access_logging"
    DATA_FORMAT = "Windows User Access Logging ESE database file"

    REQUIRED_TABLES = {
        "CLIENTS": "ParseClientsTable",
        "DNS": "ParseDNSTable",
        "ROLE_ACCESS": "ParseRoleAccessTable",
        "VIRTUALMACHINES": "ParseVirtualMachinesTable",
    }

    _CLIENTS_TABLE_VALUE_MAPPINGS = {
        "Address": "_ConvertIPAddressValue",
        "RoleGuid": "_ConvertGUIDToString",
        "TenantId": "_ConvertGUIDToString",
    }

    _DNS_TABLE_VALUE_MAPPINGS = {"Address": "_ConvertDNSAddressValue"}

    _VIRTUALMACHINES_TABLE_VALUE_MAPPINGS = {
        "BIOSGuid": "_ConvertGUIDToString",
        "VmGuid": "_ConvertGUIDToString",
    }

    _ROLE_ACCESS_TABLE_VALUE_MAPPINGS = {"RoleGuid": "_ConvertGUIDToString"}

    _ROLE_IDS_TABLE_VALUE_MAPPINGS = {"RoleGuid": "_ConvertGUIDToString"}

    def __init__(self):
        """Initializes an UAL ESE database file parser plugin."""
        super().__init__()
        self._role_mappings = {}

    def _ConvertDNSAddressValue(self, value):
        """Converts the address column value of a DNS table into a string.

        Args:
          value (bytes): DNS address.

        Returns:
          str: string representation of the DNS address.
        """
        return value.replace(b"\x00", b"").decode("utf-8")

    def _ConvertGUIDToString(self, value):
        """Converts a GUID into a string representation.

        Args:
          value (bytes): a little-endian GUID value.

        Returns:
          str: string representation of the GUID.
        """
        guid_value = uuid.UUID(bytes_le=value)
        return f"{{{guid_value!s}}}".lower()

    def _ConvertIPAddressValue(self, value):
        """Converts bytes representation of an IP to a string.

        Args:
         value (bytes): IP address.

        Returns:
          str: string representation of the IP address.
        """
        return str(ipaddress.ip_address(value))

    def ParseClientsTable(
        self, parser_mediator, database=None, table=None, **unused_kwargs
    ):
        """Parses a CLIENTS table.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          database (Optional[ESEDatabase]): ESE database.
          table (Optional[pyesedb.table]): table.

        Raises:
          ValueError: if the database or table value is missing.
        """
        if database is None:
            raise ValueError("Missing database value.")

        if table is None:
            raise ValueError("Missing table value.")

        if not self._role_mappings:
            system_identity_file_entry = self._GetSystemIdentityDatabase(
                parser_mediator
            )
            if system_identity_file_entry:
                self._ProcessSystemInformationDatabase(
                    parser_mediator, system_identity_file_entry
                )

        for record_index, esedb_record in enumerate(table.records):
            if parser_mediator.abort:
                break

            try:
                record_values, corrupted = self._GetRecordValues(
                    parser_mediator,
                    table.name,
                    record_index,
                    esedb_record,
                    value_mappings=self._CLIENTS_TABLE_VALUE_MAPPINGS,
                )
            except (UnicodeDecodeError, ValueError):
                parser_mediator.ProduceWarning(
                    f"Unable to retrieve record values from record: {record_index:d} "
                    f"in table: {table.name:s}"
                )
                continue

            event_data = UserAccessLoggingClientsEventsData()
            event_data.access_time = self._GetFiletimeRecordValue(
                record_values, "LastAccess"
            )
            event_data.authenticated_username = record_values.get(
                "AuthenticatedUserName", None
            )
            event_data.client_name = record_values.get("ClientName")
            event_data.role_identifier = record_values.get("RoleGuid")
            event_data.insert_time = self._GetFiletimeRecordValue(
                record_values, "InsertDate"
            )
            event_data.role_name = self._role_mappings.get(
                event_data.role_identifier, "Unknown"
            )
            event_data.source_ip_address = record_values.get("Address")
            event_data.tenant_identifier = record_values.get("TenantId")
            event_data.total_accesses = record_values.get("TotalAccesses")

            parser_mediator.ProduceEventData(event_data, corrupted=corrupted)

    def ParseRoleAccessTable(
        self, parser_mediator, database=None, table=None, **unused_kwargs
    ):
        """Parses a ROLE_ACCESS table.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          database (Optional[ESEDatabase]): ESE database.
          table (Optional[pyesedb.table]): table.

        Raises:
          ValueError: if the database or table value is missing.
        """
        if database is None:
            raise ValueError("Missing database value.")

        if table is None:
            raise ValueError("Missing table value.")

        if not self._role_mappings:
            system_identity_file_object = self._GetSystemIdentityDatabase(
                parser_mediator
            )
            if system_identity_file_object:
                self._ProcessSystemInformationDatabase(
                    parser_mediator, system_identity_file_object
                )

        for record_index, esedb_record in enumerate(table.records):
            if parser_mediator.abort:
                break

            try:
                record_values, corrupted = self._GetRecordValues(
                    parser_mediator,
                    table.name,
                    record_index,
                    esedb_record,
                    value_mappings=self._ROLE_ACCESS_TABLE_VALUE_MAPPINGS,
                )
            except (UnicodeDecodeError, ValueError):
                parser_mediator.ProduceWarning(
                    f"Unable to retrieve record values from record: {record_index:d} "
                    f"in table: {table.name:s}"
                )
                continue

            event_data = UserAccessLoggingRoleAccessEventsData()
            event_data.first_seen_time = self._GetFiletimeRecordValue(
                record_values, "FirstSeen"
            )
            event_data.last_seen_time = self._GetFiletimeRecordValue(
                record_values, "LastSeen"
            )
            event_data.role_identifier = record_values.get("RoleGuid")
            event_data.role_name = self._role_mappings.get(
                event_data.role_identifier, "Unknown"
            )
            parser_mediator.ProduceEventData(event_data, corrupted=corrupted)

    def ParseDNSTable(
        self, parser_mediator, database=None, table=None, **unused_kwargs
    ):
        """Parses a DNS table.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          database (Optional[ESEDatabase]): ESE database.
          table (Optional[pyesedb.table]): table.

        Raises:
          ValueError: if the database or table value is missing.
        """
        if database is None:
            raise ValueError("Missing database value.")

        if table is None:
            raise ValueError("Missing table value.")

        for record_index, esedb_record in enumerate(table.records):
            if parser_mediator.abort:
                break

            try:
                record_values, corrupted = self._GetRecordValues(
                    parser_mediator,
                    table.name,
                    record_index,
                    esedb_record,
                    value_mappings=self._DNS_TABLE_VALUE_MAPPINGS,
                )
            except (UnicodeDecodeError, ValueError):
                parser_mediator.ProduceWarning(
                    f"Unable to retrieve record values from record: {record_index:d} "
                    f"in table: {table.name:s}"
                )
                continue

            event_data = UserAccessLoggingDNSEventData()
            event_data.hostname = record_values.get("HostName")
            event_data.ip_address = record_values.get("Address")
            event_data.last_seen_time = self._GetFiletimeRecordValue(
                record_values, "LastSeen"
            )
            parser_mediator.ProduceEventData(event_data, corrupted=corrupted)

    def ParseVirtualMachinesTable(
        self, parser_mediator, database=None, table=None, **unused_kwargs
    ):
        """Parses a VIRTUALMACHINES table.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          database (Optional[ESEDatabase]): ESE database.
          table (Optional[pyesedb.table]): table.

        Raises:
          ValueError: if the database or table value is missing.
        """
        if database is None:
            raise ValueError("Missing database value.")

        if table is None:
            raise ValueError("Missing table value.")

        for record_index, esedb_record in enumerate(table.records):
            if parser_mediator.abort:
                break

            try:
                record_values, corrupted = self._GetRecordValues(
                    parser_mediator,
                    table.name,
                    record_index,
                    esedb_record,
                    value_mappings=self._VIRTUALMACHINES_TABLE_VALUE_MAPPINGS,
                )
            except (UnicodeDecodeError, ValueError):
                parser_mediator.ProduceWarning(
                    f"Unable to retrieve record values from record: {record_index:d} "
                    f"in table: {table.name:s}"
                )
                continue

            event_data = UserAccessLoggingVirtualMachinesEventData()
            event_data.bios_identifier = record_values.get("BIOSGuid")
            event_data.creation_time = self._GetFiletimeRecordValue(
                record_values, "CreationTime"
            )
            event_data.last_active_time = self._GetFiletimeRecordValue(
                record_values, "LastSeenActive"
            )
            event_data.serial_number = record_values.get("SerialNumber")
            event_data.vm_identifier = record_values.get("VMGuid")

            parser_mediator.ProduceEventData(event_data, corrupted=corrupted)

    def _GetSystemIdentityDatabase(self, parser_mediator):
        """Locate SystemIdentity.mdb.

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

        Returns:
          dfvfs.FileEntry: a file entry or None if the database cannot be located.
        """
        file_entry = parser_mediator.GetFileEntry()
        file_system = file_entry.GetFileSystem()
        path_segments = file_system.SplitPath(file_entry.path_spec.location)
        path_segments.pop()
        path_segments.append("SystemIdentity.mdb")

        kwargs = {}
        if file_entry.path_spec.parent:
            kwargs["parent"] = file_entry.path_spec.parent
        kwargs["location"] = file_system.JoinPath(path_segments)

        system_identity_file_path_spec = path_spec_factory.Factory.NewPathSpec(
            file_entry.path_spec.TYPE_INDICATOR, **kwargs
        )
        try:
            return path_spec_resolver.Resolver.OpenFileEntry(
                system_identity_file_path_spec
            )
        except RuntimeError as exception:
            warning_message = (
                f'Unable to open SystemIdentity.mdb file: {kwargs["location"]:s} '
                f"with error: {exception!s}"
            )
            parser_mediator.ProduceWarning(warning_message)

        return None

    def _ProcessSystemInformationDatabase(self, parser_mediator, file_entry):
        """Process SystemIdentity.mdb and extract Role GUID -> Role name mappings.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          file_entry (dfvfs.FileEntry): a file entry
        """
        file_object = file_entry.GetFileObject()
        database = esedb.ESEDatabase()

        try:
            database.Open(file_object)
        except (OSError, ValueError) as exception:
            parser_mediator.ProduceWarning(
                f"unable to open SystemInformation.mdb with error: {exception!s}"
            )
            return

        role_ids_table = database.GetTableByName("ROLE_IDS")
        if not role_ids_table:
            parser_mediator.ProduceWarning(
                "unable to get ROLE_IDS table in SystemInformation.mdb"
            )
        else:
            self._ParseRoleIDsTable(
                parser_mediator, database=database, table=role_ids_table
            )

        system_identity_table = database.GetTableByName("SYSTEM_IDENTITY")
        if not system_identity_table:
            parser_mediator.ProduceWarning(
                "unable to get SYSTEM_IDENTITY table in SystemInformation.mdb"
            )
        else:
            self._ParseSystemIdentityTable(
                parser_mediator, database=database, table=system_identity_table
            )

        database.Close()

    def _ParseRoleIDsTable(
        self, parser_mediator, database=None, table=None, **unused_kwargs
    ):
        """Parses a SystemIdentity.mdb ROLE_IDS table.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          database (Optional[ESEDatabase]): ESE database.
          table (Optional[pyesedb.table]): table.

        Raises:
          ValueError: if the database or table value is missing.
        """
        if database is None:
            raise ValueError("Missing database value.")

        if table is None:
            raise ValueError("Missing table value.")

        for record_index, esedb_record in enumerate(table.records):
            if parser_mediator.abort:
                break
            try:
                record_values, _ = self._GetRecordValues(
                    parser_mediator,
                    table.name,
                    record_index,
                    esedb_record,
                    value_mappings=self._ROLE_IDS_TABLE_VALUE_MAPPINGS,
                )
            except (UnicodeDecodeError, ValueError):
                parser_mediator.ProduceWarning(
                    f"Unable to retrieve record values from record: {record_index:d} "
                    f"in table: {table.name:s}"
                )
                continue

            # TODO: make corrupted record values transparent to the user.
            role_identifier = record_values.get("RoleGuid")
            role_name = record_values.get("RoleName")
            if role_identifier and role_name:
                self._role_mappings[role_identifier] = role_name

    def _ParseSystemIdentityTable(
        self, parser_mediator, database=None, table=None, **unused_kwargs
    ):
        """Parses a SystemIdentity.mdb SYSTEM_IDENTITY table.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          database (Optional[ESEDatabase]): ESE database.
          table (Optional[pyesedb.table]): table.

        Raises:
          ValueError: if the database or table value is missing.
        """
        if database is None:
            raise ValueError("Missing database value.")

        if table is None:
            raise ValueError("Missing table value.")

        for record_index, esedb_record in enumerate(table.records):
            if parser_mediator.abort:
                break
            try:
                record_values, corrupted = self._GetRecordValues(
                    parser_mediator, table.name, record_index, esedb_record
                )
            except (UnicodeDecodeError, ValueError):
                parser_mediator.ProduceWarning(
                    f"Unable to retrieve record values from record: {record_index:d} "
                    f"in table: {table.name:s}"
                )
                continue

            event_data = UserAccessLoggingSystemIdentityEventdata()
            event_data.creation_time = self._GetFiletimeRecordValue(
                record_values, "CreationTime"
            )
            event_data.operating_system_build = record_values.get("OSBuildNumber", None)
            event_data.system_dns_hostname = record_values.get(
                "SystemDNSHostName", None
            )
            event_data.system_domain_name = record_values.get("SystemDomainName", None)

            parser_mediator.ProduceEventData(event_data, corrupted=corrupted)


esedb.ESEDBParser.RegisterPlugin(UserAccessLoggingESEDBPlugin)
