"""JSON-L parser plugin for Google Cloud (GCP) log files."""

import re

from dfdatetime import time_elements as dfdatetime_time_elements

from plaso.containers import events
from plaso.parsers import jsonl_parser
from plaso.parsers.jsonl_plugins import interface


class GCPLogEventData(events.EventData):
    """Google Cloud (GCP) log event data.

    Attributes:
      caller_ip (str): IP address of the client that requested the operation.
      container (str): container.
      dcsa_emails (list[str]): default compute service account attached to a
          Google Compute Engine (GCE) instance.
      dcsa_scopes (list[str]): OAuth scopes granted to the default compute service
          account.
      delegation_chain (str): service account delegation chain.
      event_subtype (str): JSON event sub type or protocol buffer method.
      event_type (str): event type.
      filename (str): filename.
      firewall_rules (list[str]): firewall rules.
      firewall_source_ranges (list[str]): firewall source ranges.
      gcloud_command_identity (str): unique gcloud command identity.
      gcloud_command_partial (str): partial gcloud command.
      log_name (str): name of the log entry.
      message_body (str): message string.
      method_name (str): operation performed.
      permissions (list[str]): IAM permission used for the operation.
      policy_deltas (list[str]): policy deltas.
      principal_email (str): email address of the requester.
      principal_subject (str): subject name of the requester.
      recorded_time (dfdatetime.DateTimeValues): date and time the log entry
          was recorded.
      request_account_identifier (str): GCP account identifier of the request.
      request_address (str): IP address assigned to a Google Cloud Engine (GCE)
          instance.
      request_description (str): description of the request.
      request_direction (str): direction of the request.
      request_email (str): email address of the request.
      request_member (str): member of the request.
      request_metadata (list[str]): request metadata values.
      request_name (str): name of the request.
      request_target_tags (str): request target tags.
      resource_labels (list[str]): resource labels.
      resource_name (str): name of the resource.
      service_account_delegation (list[str]): service accounts delegation in the
          authentication.
      service_account_display_name (str): display name of the service account.
      service_account_key_name (str): service account key name used in
          authentication.
      service_name (str): name of the service.
      severity (str): log entry severity.
      source_images (list[str]): source images of disks attached to a compute
          engine instance.
      status_code (str): operation success or failure code.
      status_reasons (list[str]): reasons for operation failure.
      text_payload (str): text payload for logs not using a JSON or proto payload.
      user_agent (str): user agent used in the request.
      username (str): username.
    """

    DATA_TYPE = "gcp:log:entry"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.caller_ip = None
        self.container = None
        self.dcsa_emails = None
        self.dcsa_scopes = None
        self.delegation_chain = None
        self.event_subtype = None
        self.event_type = None
        self.filename = None
        self.firewall_rules = None
        self.firewall_source_ranges = None
        self.gcloud_command_identity = None
        self.gcloud_command_partial = None
        self.log_name = None
        self.method_name = None
        self.message_body = None
        self.permissions = None
        self.policy_deltas = None
        self.principal_email = None
        self.principal_subject = None
        self.recorded_time = None
        self.request_account_identifier = None
        self.request_address = None
        self.request_description = None
        self.request_direction = None
        self.request_email = None
        self.request_member = None
        self.request_metadata = None
        self.request_name = None
        self.request_target_tags = None
        self.resource_labels = None
        self.resource_name = None
        self.service_account_delegation = None
        self.service_account_display_name = None
        self.service_account_key_name = None
        self.service_name = None
        self.severity = None
        self.source_images = None
        self.status_code = None
        self.status_reasons = None
        self.text_payload = None
        self.user_agent = None
        self.username = None


class GCPLogJSONLPlugin(interface.JSONLPlugin):
    """JSON-L parser plugin for Google Cloud (GCP) log files."""

    NAME = "gcp_log"
    DATA_FORMAT = "Google Cloud (GCP) log"

    _USER_AGENT_COMMAND_RE = re.compile(r"command/([^\s]+)")

    _USER_AGENT_INVOCATION_ID_RE = re.compile(r"invocation-id/([^\s]+)")

    def _ParseAuthenticationInfo(self, proto_payload, event_data):
        """Extracts information from `protoPayload.authenticationInfo`.

        Args:
          proto_payload (dict): JSON dictionary of the `protoPayload` value.
          event_data (GCPLogEventData): event data.
        """
        authentication_info = self._GetJSONValue(proto_payload, "authenticationInfo")
        if not authentication_info:
            return

        principal_email = self._GetJSONValue(authentication_info, "principalEmail")
        if principal_email:
            event_data.principal_email = principal_email

        principal_subject = self._GetJSONValue(authentication_info, "principalSubject")
        if principal_subject:
            event_data.principal_subject = principal_subject

        service_account_key_name = self._GetJSONValue(
            authentication_info, "serviceAccountKeyName"
        )
        if service_account_key_name:
            event_data.service_account_key_name = service_account_key_name

        delegations = []

        delegation_info_list = self._GetJSONValue(
            authentication_info, "serviceAccountDelegationInfo", []
        )
        for delegation_info in delegation_info_list:
            first_party_principal = self._GetJSONValue(
                delegation_info, "firstPartyPrincipal", {}
            )

            first_party_principal_email = self._GetJSONValue(
                first_party_principal, "principalEmail"
            )
            if first_party_principal_email:
                delegations.append(first_party_principal_email)
            else:
                first_party_principal_subject = self._GetJSONValue(
                    first_party_principal, "principalSubject"
                )
                if first_party_principal_subject:
                    delegations.append(first_party_principal_subject)

        if delegations:
            event_data.service_account_delegation = delegations
            event_data.delegation_chain = "->".join(delegations)

    def _ParseAuthorizationInfo(self, proto_payload, event_data):
        """Extracts information from `protoPayload.authorizationInfo`.

        Args:
          proto_payload (dict): JSON dictionary of the `protoPayload` value.
          event_data (GCPLogEventData): event data.
        """
        permissions = []

        authorization_info_list = self._GetJSONValue(
            proto_payload, "authorizationInfo", []
        )
        for authorization_info in authorization_info_list:
            permission = self._GetJSONValue(authorization_info, "permission")
            if permission:
                permissions.append(permission)

        if permissions:
            event_data.permissions = permissions

    def _ParseRequestMetadata(self, proto_payload, event_data):
        """Extracts information from `protoPayload.requestMetadata`.

        Args:
          proto_payload (dict): JSON dictionary of the `protoPayload` value.
          event_data (GCPLogEventData): event data.
        """
        request_metadata = self._GetJSONValue(proto_payload, "requestMetadata")
        if not request_metadata:
            return

        event_data.caller_ip = self._GetJSONValue(request_metadata, "callerIp")
        event_data.user_agent = self._GetJSONValue(
            request_metadata, "callerSuppliedUserAgent"
        )

        if event_data.user_agent:
            if "command/" in event_data.user_agent:
                matches = self._USER_AGENT_COMMAND_RE.search(event_data.user_agent)
                if matches:
                    command_string = matches.group(1).replace(".", " ")
                    event_data.gcloud_command_partial = command_string

            if "invocation-id" in event_data.user_agent:
                matches = self._USER_AGENT_INVOCATION_ID_RE.search(
                    event_data.user_agent
                )
                if matches:
                    event_data.gcloud_command_identity = matches.group(1)

    def _ParseProtoPayloadStatus(self, proto_payload, event_data):
        """Extracts information from `protoPayload.status`.

        Args:
          proto_payload (dict): JSON dictionary of the `protoPayload` value.
          event_data (GCPLogEventData): event data.
        """
        status = self._GetJSONValue(proto_payload, "status")
        if status:
            # Non empty `protoPayload.status` field could have empty
            # `protoPayload.status.code` field.
            #
            # Empty `code` and `message` fields indicate the operation was successful.

            event_data.status_code = str(self._GetJSONValue(status, "code", ""))
            event_data.message_body = self._GetJSONValue(status, "message")

            # `protoPayload.status.details[].reason` contains reason for an operation
            # failure.
            status_reasons = []

            for status_detail in self._GetJSONValue(status, "details", []):
                status_reason = self._GetJSONValue(status_detail, "reason")
                if status_reason:
                    status_reasons.append(status_reason)

            if status_reasons:
                event_data.status_reasons = status_reasons

    def _ParseComputeInsertRequest(self, request, event_data):
        """Extracts compute.instances.insert information.

        Args:
          request (dict): JSON dictionary of the `protoPayload.request` field.
          event_data (GCPLogEventData): event data.
        """
        # source_images hold Google Cloud source disk path used in creating a GCE
        # instance.
        source_images = []

        for disk in self._GetJSONValue(request, "disks", []):
            initialize_params = self._GetJSONValue(disk, "initializeParams", {})

            source_image = self._GetJSONValue(initialize_params, "sourceImage")
            if source_image:
                source_images.append(source_image)

        if source_images:
            event_data.source_images = source_images

        # Default compute service account aka dcsa
        dcsa_emails = []
        dcsa_scopes = []

        service_account_list = self._GetJSONValue(request, "serviceAccounts", [])
        for service_account in service_account_list:
            email = self._GetJSONValue(service_account, "email")
            if email:
                dcsa_emails.append(email)

            scopes = self._GetJSONValue(service_account, "scopes")
            if scopes:
                dcsa_scopes.extend(scopes)

        if dcsa_emails:
            event_data.dcsa_emails = dcsa_emails

        if dcsa_scopes:
            event_data.dcsa_scopes = dcsa_scopes

    def _ParseComputeProtoPayload(self, proto_payload, event_data):
        """Extracts compute.googleapis.com information.

        Args:
          proto_payload (dict): JSON dictionary of the `protoPayload` value.
          event_data (GCPLogEventData): event data.
        """
        request = self._GetJSONValue(proto_payload, "request")
        if not request:
            return

        request_type = self._GetJSONValue(request, "@type")
        if not request_type:
            return

        if request_type == "type.googleapis.com/compute.instances.insert":
            self._ParseComputeInsertRequest(request, event_data)

    def _ParseProtoPayloadRequest(self, proto_payload, event_data):
        """Extracts information from the request field of a protoPayload field.

        Args:
          proto_payload (dict): JSON dictionary of the `protoPayload` value.
          event_data (GCPLogEventData): event data.
        """
        request = self._GetJSONValue(proto_payload, "request")
        if not request:
            return

        event_data.request_account_identifier = self._GetJSONValue(
            request, "account_id"
        )
        event_data.request_address = self._GetJSONValue(request, "address")
        event_data.request_description = self._GetJSONValue(request, "description")
        event_data.request_direction = self._GetJSONValue(request, "direction")
        event_data.request_email = self._GetJSONValue(request, "email")
        event_data.request_member = self._GetJSONValue(request, "member")
        event_data.request_name = self._GetJSONValue(request, "name")
        event_data.request_target_tags = self._GetJSONValue(request, "targetTags")

        # Firewall specific attributes.
        event_data.firewall_source_ranges = self._GetJSONValue(request, "sourceRanges")

        firewall_rules = []

        for allowed in self._GetJSONValue(request, "alloweds") or []:
            ip_protocol = self._GetJSONValue(allowed, "IPProtocol")
            ports = self._GetJSONValue(allowed, "ports", default_value="all")

            firewall_rules.append(f"ALLOW: {ip_protocol:s} {ports!s}")

        for denied in self._GetJSONValue(request, "denieds") or []:
            ip_protocol = self._GetJSONValue(denied, "IPProtocol")
            ports = self._GetJSONValue(denied, "ports", default_value="all")

            firewall_rules.append(f"DENY: {ip_protocol:s} {ports!s}")

        event_data.firewall_rules = firewall_rules or None

        # Service account specific attributes
        service_account = self._GetJSONValue(request, "service_account")
        if service_account:
            event_data.service_account_display_name = self._GetJSONValue(
                service_account, "display_name"
            )

    def _ParseProtoPayloadServiceData(self, proto_payload, event_data):
        """Extracts information from the serviceData in the protoPayload value.

        Args:
          proto_payload (dict): JSON dictionary of the `protoPayload` value.
          event_data (GCPLogEventData): event data.
        """
        service_data = self._GetJSONValue(proto_payload, "serviceData") or {}
        policy_delta = self._GetJSONValue(service_data, "policyDelta") or {}

        policy_deltas = []

        for binding_delta_value in (
            self._GetJSONValue(policy_delta, "bindingDeltas") or []
        ):
            action = self._GetJSONValue(binding_delta_value, "action") or "N/A"
            member = self._GetJSONValue(binding_delta_value, "member") or "N/A"
            role = self._GetJSONValue(binding_delta_value, "role") or "N/A"

            policy_deltas.append(f"{action:s} {member:s} with role {role:s}")

        event_data.policy_deltas = policy_deltas or None

    def _ParseRecord(self, parser_mediator, json_dict):
        """Parses a Google Cloud (GCP) log record.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          json_dict (dict): JSON dictionary of the log record.
        """
        resource = self._GetJSONValue(json_dict, "resource") or {}
        labels = self._GetJSONValue(resource, "labels") or {}
        json_payload = self._GetJSONValue(json_dict, "jsonPayload") or {}
        proto_payload = self._GetJSONValue(json_dict, "protoPayload") or {}
        actor = self._GetJSONValue(json_payload, "actor") or {}

        resource_labels = [f"{name:s}: {value!s}" for name, value in labels.items()]

        date_time, corrupted = self._ParseISO8601DateTimeString(
            parser_mediator, json_dict, "timestamp"
        )
        event_data = GCPLogEventData()
        event_data.container = self._GetJSONValue(json_payload, "container")
        event_data.event_subtype = self._GetJSONValue(json_payload, "event_subtype")
        event_data.event_type = self._GetJSONValue(json_payload, "event_type")
        event_data.filename = self._GetJSONValue(json_payload, "filename")
        event_data.log_name = self._GetJSONValue(json_dict, "logName")
        event_data.message_body = self._GetJSONValue(json_payload, "message")
        event_data.recorded_time = date_time
        event_data.resource_labels = resource_labels or None
        event_data.resource_name = self._GetJSONValue(proto_payload, "resourceName")
        event_data.service_name = self._GetJSONValue(proto_payload, "serviceName")
        event_data.severity = self._GetJSONValue(json_dict, "severity")
        event_data.text_payload = self._GetJSONValue(json_dict, "textPayload")
        event_data.username = self._GetJSONValue(actor, "user")

        method_name = self._GetJSONValue(proto_payload, "methodName")
        if method_name and not event_data.event_subtype:
            event_data.event_subtype = method_name
            event_data.method_name = method_name

        self._ParseAuthenticationInfo(proto_payload, event_data)
        self._ParseAuthorizationInfo(proto_payload, event_data)
        self._ParseRequestMetadata(proto_payload, event_data)
        self._ParseProtoPayloadStatus(proto_payload, event_data)
        self._ParseProtoPayloadRequest(proto_payload, event_data)
        self._ParseProtoPayloadServiceData(proto_payload, event_data)

        if event_data.service_name == "compute.googleapis.com":
            self._ParseComputeProtoPayload(proto_payload, event_data)

        parser_mediator.ProduceEventData(event_data, corrupted=corrupted)

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

        Args:
          json_dict (dict): JSON dictionary of the log record.

        Returns:
          bool: True if this is the correct parser, False otherwise.
        """
        log_name = json_dict.get("logName") or None
        timestamp = json_dict.get("timestamp") or None

        if None in (log_name, timestamp):
            return False

        date_time = dfdatetime_time_elements.TimeElementsInMicroseconds()

        try:
            date_time.CopyFromStringISO8601(timestamp)
        except ValueError:
            return False

        return True


jsonl_parser.JSONLParser.RegisterPlugin(GCPLogJSONLPlugin)
