---
description: Pipeline for parsing Salesforce Apex logs
processors:
  - json:
      field: message
      target_field: json
      on_failure:
        - append:
            field: error.message
            value: "Failed to parse JSON: {{{_ingest.on_failure_message}}}"
  - set:
      field: event.original
      copy_from: message
      ignore_empty_value: true
      ignore_failure: true
      if: "ctx.event?.original == null"
  - set:
      field: ecs.version
      value: "8.11.0"
      ignore_failure: true
      ignore_empty_value: true
  - set:
      field: event.ingested
      copy_from: _ingest.timestamp
  - set:
      field: salesforce.apex.document_id
      copy_from: _id
      ignore_empty_value: true
  - date:
      field: json.TIMESTAMP_DERIVED
      target_field: "@timestamp"
      formats:
        - ISO8601
      on_failure:
        - append:
            field: error.message
            value: "Failed to parse TIMESTAMP_DERIVED field: {{{_ingest.on_failure_message}}}"
  - rename:
      field: json.ACTION
      target_field: salesforce.apex.action
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.CALLOUT_TIME
      target_field: salesforce.apex.callout_time
      type: float
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.CLASS_NAME
      target_field: salesforce.apex.class_name
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.CLIENT_NAME
      target_field: salesforce.apex.client_name
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.CPU_TIME
      target_field: salesforce.apex.cpu_time
      type: float
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.DB_BLOCKS
      target_field: salesforce.apex.db_blocks
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.DB_CPU_TIME
      target_field: salesforce.apex.db_cpu_time
      type: float
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.DB_TOTAL_TIME
      target_field: salesforce.apex.db_total_time
      type: float
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.ENTITY
      target_field: salesforce.apex.entity
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.ENTITY_NAME
      target_field: salesforce.apex.entity_name
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.ENTRY_POINT
      target_field: salesforce.apex.entry_point
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.EVENT_TYPE
      target_field: salesforce.apex.event_type
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.EXECUTE_MS
      target_field: salesforce.apex.execute_ms
      type: float
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.FETCH_MS
      target_field: salesforce.apex.fetch_ms
      type: float
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.FILTER
      target_field: salesforce.apex.filter
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.IS_LONG_RUNNING_REQUEST
      target_field: salesforce.apex.is_long_running_request
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.LOGIN_KEY
      target_field: salesforce.apex.login_key
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.LIMIT
      target_field: salesforce.apex.limit
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.LIMIT_USAGE_PERCENT
      target_field: salesforce.apex.limit_usage_pct
      type: float
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.MEDIA_TYPE
      target_field: salesforce.apex.media_type
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.MESSAGE
      target_field: salesforce.apex.message
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.METHOD_NAME
      target_field: salesforce.apex.method_name
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.NUMBER_FIELDS
      target_field: salesforce.apex.fields_count
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.NUMBER_SOQL_QUERIES
      target_field: salesforce.apex.soql_queries_count
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.OFFSET
      target_field: salesforce.apex.offset
      type: long
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.ORDERBY
      target_field: salesforce.apex.orderby
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.ORGANIZATION_ID
      target_field: salesforce.apex.organization_id
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.QUERY
      target_field: salesforce.apex.query
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.QUIDDITY
      target_field: salesforce.apex.quiddity
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.REQUEST_ID
      target_field: salesforce.apex.request_id
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.REQUEST_STATUS
      target_field: salesforce.apex.request_status
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.ROWS
      target_field: salesforce.apex.rows_total
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.ROWS_FETCHED
      target_field: salesforce.apex.rows_fetched
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.ROWS_PROCESSED
      target_field: salesforce.apex.rows_processed
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.RUN_TIME
      target_field: salesforce.apex.run_time
      type: float
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.SELECT
      target_field: salesforce.apex.select
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.SUBQUERIES
      target_field: salesforce.apex.subqueries
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.THROUGHPUT
      target_field: salesforce.apex.throughput
      type: float
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.TRIGGER_ID
      target_field: salesforce.apex.trigger_id
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.TRIGGER_NAME
      target_field: salesforce.apex.trigger_name
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.TRIGGER_TYPE
      target_field: salesforce.apex.trigger_type
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.TYPE
      target_field: salesforce.apex.type
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.URI
      target_field: salesforce.apex.uri
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.URI_ID_DERIVED
      target_field: salesforce.apex.uri_derived_id
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: json.USER_AGENT
      target_field: salesforce.apex.user_agent
      ignore_missing: true
      ignore_failure: true
  - script:
      description: Set request user agent value from user agent numeric code.
      lang: painless
      params:
        user_agent_map:
          "100": "Internet Explorer"
          "110": "Firefox"
          "130": "Chrome"
          "140": "Safari"
          "150": "Opera"
          "160": "Android"
          "170": "Netscape"
          "180": "Webkit"
          "190": "Gecko"
          "230": "Blackberry"
          "240": "Good Access"
          "999": "Unknown"
      if: "ctx.salesforce?.apex?.user_agent != null"
      source: |
        if (params.user_agent_map.containsKey(ctx.salesforce.apex.user_agent.toString().substring(0,3))) {
          ctx.salesforce.apex.user_agent = params.user_agent_map[ctx.salesforce.apex.user_agent.toString().substring(0,3)];
        }
      on_failure:
        - append:
            field: error.message
            value: "Failed to set salesforce.apex.user_agent: {{{_ingest.on_failure_message}}}"
  - rename:
      field: json.USER_ID_DERIVED
      target_field: salesforce.apex.user_id_derived
      ignore_missing: true
      ignore_failure: true

  #######################
  ## ECS Event Mapping ##
  #######################

  - set:
      field: event.kind
      value: event
      if: ctx.event?.kind == null
  - set:
      field: event.type
      value: connection
      if: ctx.salesforce?.apex?.event_type != "ApexExecution"
      ignore_failure: true
  - append:
      field: event.category
      value: network
      if: ctx.salesforce?.apex?.event_type != "ApexTrigger" && ctx.salesforce?.apex?.event_type != "ApexExecution"
      allow_duplicates: false
      ignore_failure: true
  - script:
      description: Set event.action field based on the type of Apex event received.
      if: ctx.salesforce?.apex?.event_type != null
      lang: painless
      params:
        event_action_map:
          apexcallout: apex-callout
          apextrigger: apex-trigger
          apexexecution: apex-execution
          apexrestapi: apex-rest
          apexsoap: apex-soap
          externalcustomapexcallout: apex-external-custom-callout
      source: |
        def eventType = ctx?.salesforce?.apex?.event_type?.toLowerCase();
        if (params.event_action_map.containsKey(eventType)) {
          ctx.event.action = params.event_action_map[eventType];
        }
      on_failure:
        - append:
            field: error.message
            value: "Failed to set event.action from salesforce.apex.event_type: {{{_ingest.on_failure_message}}}"
  - convert:
      field: json.TIME
      target_field: event.duration
      type: float
      if: ctx.salesforce?.apex?.event_type == "ApexCallout" && ctx.json?.TIME != null
      ignore_failure: true
  - convert:
      field: json.EXEC_TIME
      target_field: event.duration
      type: float
      if: (ctx.salesforce?.apex?.event_type == "ApexTrigger" || ctx.salesforce?.apex?.event_type == "ApexExecution") && ctx.json?.EXEC_TIME != null
      ignore_failure: true
  - convert:
      field: salesforce.apex.run_time
      target_field: event.duration
      type: float
      if: (ctx.salesforce?.apex?.event_type == "ApexRestApi" || ctx.salesforce?.apex?.event_type == "ApexSoap") && ctx.salesforce?.apex?.run_time != null
      ignore_failure: true
  - convert:
      field: json.TOTAL_MS
      target_field: event.duration
      type: float
      if: ctx.salesforce?.apex?.event_type == "ExternalCustomApexCallout" && ctx.json?.TOTAL_MS != null
      ignore_failure: true
  - set:
      field: event.outcome
      value: success
      if: (ctx.json?.SUCCESS == "1" && ctx.json?.SUCCESS != null) || (ctx.json?.STATUS == "1" && ctx.json?.STATUS != null)
      ignore_failure: true
  - set:
      field: event.outcome
      value: failure
      if: (ctx.json?.SUCCESS != "1" && ctx.json?.SUCCESS != null) || (ctx.json?.STATUS != "1" && ctx.json?.STATUS != null)
      ignore_failure: true
  - rename:
      field: json.URL
      target_field: event.url
      if: ctx.salesforce?.apex?.event_type == "ApexCallout"
      ignore_missing: true
      ignore_failure: true
  - rename:
      field: salesforce.apex.uri
      target_field: event.url
      if: ctx.salesforce?.apex?.event_type != "ApexCallout" && ctx.salesforce?.apex?.event_type != "ExternalCustomApexCallout"
      ignore_missing: true
      ignore_failure: true
  - set:
      field: event.dataset
      value: salesforce.apex
      if: ctx.event?.dataset == null
  - set:
      field: event.module
      value: salesforce
      if: ctx.event?.module == null

  ######################
  ## ECS User Mapping ##
  ######################

  - rename:
      field: json.USER_ID
      target_field: user.id
      ignore_missing: true
      ignore_failure: true
  - append:
      field: user.roles
      value: "{{{json.USER_TYPE}}}"
      allow_duplicates: false
      ignore_failure: true
  - remove:
      field: json.USER_TYPE
      ignore_missing: true
      ignore_failure: true

  ########################
  ## ECS Source Mapping ##
  ########################

  # A Salesforce internal IP (such as a login from Salesforce Workbench or AppExchange) is shown as "Salesforce.com IP"
  - rename:
      field: json.CLIENT_IP
      target_field: source.ip
      if: ctx.json?.CLIENT_IP != "Salesforce.com IP" && ctx.json?.CLIENT_IP != null
      ignore_missing: true
      ignore_failure: true

  ############################
  ## ECS source.geo Mapping ##
  ############################

  # A Salesforce internal IP (such as a login from Salesforce Workbench or AppExchange) is shown as "Salesforce.com IP"
  - geoip:
      field: source.ip
      target_field: source.geo
      if: ctx.source?.ip != "Salesforce.com IP" && ctx.source?.ip != null
      ignore_missing: true
      ignore_failure: true

  ############################
  ## ECS related.ip Mapping ##
  ############################

  - append:
      field: related.ip
      value: "{{{source.ip}}}"
      if: ctx?.source?.ip != null
      allow_duplicates: false
      ignore_failure: true

  ######################
  ## ECS HTTP Mapping ##
  ######################

  - rename:
      field: json.METHOD
      target_field: http.request.method
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.REQUEST_SIZE
      target_field: http.request.bytes
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.RESPONSE_SIZE
      target_field: http.response.bytes
      type: long
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: json.STATUS_CODE
      target_field: http.response.status_code
      type: long
      ignore_missing: true
      ignore_failure: true

  #############
  ## Cleanup ##
  #############

  - script:
      description: Drops null/empty values recursively
      lang: painless
      source: |
        boolean dropEmptyFields(Object object) {
          if (object == null || object == "") {
            return true;
          } else if (object instanceof Map) {
            ((Map) object).values().removeIf(value -> dropEmptyFields(value));
            return ((Map) object).isEmpty();
          } else if (object instanceof List) {
            ((List) object).removeIf(value -> dropEmptyFields(value));
            return ((List) object).isEmpty();
          }
          return false;
        }
        dropEmptyFields(ctx);
  - remove:
      field:
        - json
        - message
      ignore_failure: true
      ignore_missing: true
  - remove:
      field: event.original
      if: "ctx.tags == null || !(ctx.tags.contains('preserve_original_event'))"
      ignore_failure: true
      ignore_missing: true

on_failure:
  - set:
      field: event.kind
      value: pipeline_error
  - set:
      field: error.message
      value: "{{{_ingest.on_failure_message}}}"
  - set:
      field: error.type
      value: "{{{_ingest.on_failure_processor_type}}}"
