---
description: Pipeline for parsing Azure sign-in logs.
processors:
  - set:
      field: event.ingested
      value: '{{_ingest.timestamp}}'
  - rename:
      field: azure
      target_field: azure-eventhub
      ignore_missing: true
  - json:
      field: message
      target_field: azure.signinlogs
  - script:
      description: Convert Azure JSON keys to snake case.
      tag: azure-json-keys-to-snake-case
      lang: painless
      source: |
        Map keysToSnakeCase(Map m) {
          def regex = /([a-z])([A-Z]+)/;
          def out = [:];

          for (entry in m.entrySet()) {
            def k = entry.getKey();
            def v = entry.getValue();

            if (v instanceof Map) {
              v = keysToSnakeCase(v);
            } else if (v instanceof List) {
              for (int i = 0; i < v.size(); i++) {
                def item = v.get(i);
                if (item instanceof Map) {
                  v.set(i, keysToSnakeCase(item));
                }
              }
            }

            k = regex.matcher(k).replaceAll('$1_$2').toLowerCase();
            out.put(k, v);
          }

          return out;
        }

        ctx.azure['signinlogs'] = keysToSnakeCase(ctx.azure.signinlogs);
  - drop:
      description: Drop non-SignInLogs.
      if: ctx?.azure?.signinlogs?.category == null || !ctx.azure.signinlogs.category.endsWith('SignInLogs')
  - date:
      field: azure.signinlogs.time
      formats:
          - ISO8601
  - remove:
      field: azure.signinlogs.time
  - rename:
      field: message
      target_field: event.original
  - rename:
      field: azure.signinlogs.resource_id
      target_field: azure.resource_id
      ignore_missing: true

  #
  # Sign-in logs use a few different fields for the source IP so try them all.
  # This assumes that they contain the same value if more than one is populated.
  #
  - set:
      if: ctx?.source?.address == null
      field: source.address
      value: '{{{azure.signinlogs.properties.ipaddress}}}'
      ignore_empty_value: true
  - set:
      if: ctx?.source?.address == null
      field: source.address
      value: '{{{azure.signinlogs.properties.ip_address}}}'
      ignore_empty_value: true
  - set:
      if: ctx?.source?.address == null
      field: source.address
      value: '{{{azure.signinlogs.caller_ip_address}}}'
      ignore_empty_value: true
  - convert:
      field: source.address
      target_field: source.ip
      type: ip
      ignore_missing: true
      ignore_failure: true
  - remove:
      field:
        - azure.signinlogs.properties.ipaddress
        - azure.signinlogs.properties.ip_address
        - azure.signinlogs.caller_ip_address
      ignore_missing: true
  - append:
      if: ctx?.source?.ip != null
      field: related.ip
      value: '{{{source.ip}}}'
      allow_duplicates: false
  - set:
      field: client.ip
      value: '{{{source.ip}}}'
      ignore_empty_value: true

  - convert:
      field: azure.signinlogs.level
      target_field: log.level
      type: string
      ignore_missing: true
  - remove:
      field: azure.signinlogs.level
      ignore_missing: true
  - rename:
      field: azure.signinlogs.duration_ms
      target_field: event.duration
      ignore_missing: true
  - script:
      if: ctx?.event?.duration != null
      lang: painless
      source: ctx.event.duration = ctx.event.duration * 1000000
  - rename:
      field: azure.signinlogs.location
      target_field: geo.country_iso_code
      ignore_missing: true
  - convert:
      field: azure.signinlogs.operation_name
      target_field: event.action
      type: string
      ignore_missing: true
  - rename:
      field: azure.signinlogs.tenant_id
      target_field: azure.tenant_id
      ignore_missing: true
  - rename:
      field: azure.signinlogs.correlation_id
      target_field: azure.correlation_id
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.created_date_time
      target_field: azure.signinlogs.properties.created_at
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.processing_time_in_milliseconds
      target_field: azure.signinlogs.properties.processing_time_ms
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.risk_level_during_sign_in
      target_field: azure.signinlogs.properties.risk_level_during_signin
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.status.failure_reason
      target_field: message
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.status.additional_details
      target_field: message
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.location.city
      target_field: geo.city_name
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.location.state
      target_field: geo.country_name
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.location.geo_coordinates.latitude
      target_field: geo.location.lat
      ignore_missing: true
  - rename:
      field: azure.signinlogs.properties.location.geo_coordinates.longitude
      target_field: geo.location.lon
      ignore_missing: true
  - remove:
      field:
        - azure.signinlogs.properties.location
      ignore_missing: true
  - script:
      description: "Turns the authentication_processing_details array elements into key/value pairs. For example, the array element ``{key: 'key1', value: 'value1'}`` becomes ``{key1: 'value1'}``."
      lang: painless
      source: |
        def tmp = [:];
        for (item in ctx.azure.signinlogs.properties.authentication_processing_details) {
            tmp[item.key] = item.value;
        }
        ctx.azure.signinlogs.properties.authentication_processing_details = tmp;
      if: ctx.azure?.signinlogs?.properties?.authentication_processing_details != null && ctx.azure.signinlogs.properties.authentication_processing_details instanceof List
  - set:
      field: event.kind
      value: event
  - set:
      field: event.category
      value:
        - authentication
  - set:
      field: event.type
      value:
        - info
  - set:
      field: event.outcome
      value: success
      if: "ctx?.azure?.signinlogs?.properties?.status?.error_code == null || ctx.azure.signinlogs.properties.status.error_code == 0"
  - set:
      field: event.outcome
      value: failure
      if: "ctx?.azure?.signinlogs?.properties?.status?.error_code != null && ctx.azure.signinlogs.properties.status.error_code > 0"
  - set:
      field: event.id
      value: '{{{azure.signinlogs.properties.id}}}'
      ignore_empty_value: true
  - grok:
      field: azure.signinlogs.properties.user_principal_name
      patterns:
        - '%{USERNAME:user.name}@%{HOSTNAME:user.domain}'
        - '%{GREEDYDATA:user.name}'
      ignore_missing: true
      ignore_failure: true
  - convert:
      field: azure.signinlogs.properties.user_display_name
      target_field: user.full_name
      ignore_missing: true
      type: string
  - remove:
      description: Drop user_id field if value is null.
      if: ctx?.azure?.signinlogs?.properties?.user_id == null
      field: azure.signinlogs.properties.user_id
      ignore_missing: true
  - convert:
      field: azure.signinlogs.properties.user_id
      target_field: user.id
      ignore_missing: true
      type: string
  - geoip:
      field: source.ip
      target_field: source.geo
      ignore_missing: true
  - geoip:
      database_file: GeoLite2-ASN.mmdb
      field: source.ip
      target_field: source.as
      properties:
        - asn
        - organization_name
      ignore_missing: true
  - rename:
      field: source.as.asn
      target_field: source.as.number
      ignore_missing: true
  - rename:
      field: source.as.organization_name
      target_field: source.as.organization.name
      ignore_missing: true

  #
  # User Agent
  #
  - rename:
      field: azure.signinlogs.properties.user_agent
      target_field: user_agent.original
      ignore_missing: true
  - user_agent:
      field: user_agent.original
      ignore_missing: true

  - pipeline:
      name: '{< IngestPipeline "azure-shared-pipeline" >}'
on_failure:
  - set:
      field: error.message
      value: '{{ _ingest.on_failure_message }}'
