---
description: Pipeline for parsing Suricata EVE logs

processors:
  - set:
      field: event.ingested
      value: '{{_ingest.timestamp}}'

  # Handle the different Suricata event types.
  - lowercase:
      field: suricata.eve.event_type
      ignore_missing: true
  - script:
      lang: painless
      ignore_failure: true
      params:
        alert:
          kind: alert
          category:
            - network
            - intrusion_detection
        dns:
          type:
            - protocol
          network_protocol: dns
        flow:
          type:
            - connection
        ftp:
          type:
            - protocol
          network_protocol: ftp
        ftp_data:
          type:
            - protocol
          network_protocol: ftp
        http:
          category:
            - network
            - web
          type:
            - access
            - protocol
          network_protocol: http
        http2:
          category:
            - network
            - web
          type:
            - access
            - protocol
          network_protocol: http
        mqtt:
          type:
            - protocol
          network_protocol: mqtt
        smb:
          type:
            - protocol
          network_protocol: smb
        ssh:
          type:
            - protocol
          network_protocol: ssh
        stats:
          kind: metric
        tftp:
          type:
            - protocol
          network_protocol: tftp
        tls:
          type:
            - protocol
          network_protocol: tls
        rdp:
          type:
            - protocol
          network_protocol: rdp
        rfb: # RFB (Remote Framebuffer Protocol)
          type:
            - protocol
          network_protocol: rdp

      source: |
        ctx.event.kind = 'event';
        ctx.event.category = ['network'];
        def type_params = params.get(ctx?.suricata?.eve?.event_type);
        if (type_params == null) {
            return;
        }
        type_params.forEach((k, v) -> {
            if ('network_protocol' == k) {
                if (ctx.network == null) {
                    ctx.network = ['protocol': v];
                } else {
                    ctx.network.protocol = v;
                }
            } else {
                ctx.event[k] = v;
            }
        });

  ## Anomaly and Alert
  - lowercase:
      field: suricata.eve.app_proto
      ignore_missing: true
  - set:
      if: ctx?.suricata?.eve?.app_proto == "ftp-data"
      field: network.protocol
      value: ftp
  - set:
      if: >-
        ctx?.suricata?.eve?.app_proto != "failed" &&
        ctx?.suricata?.eve?.app_proto != "template" &&
        ctx?.suricata?.eve?.app_proto != "template-rust"
      field: network.protocol
      value: '{{suricata.eve.app_proto}}'
      ignore_empty_value: true
  ## HTTP
  - set:
      if: ctx?.suricata?.eve?.event_type == "http" && ctx?.suricata?.eve?.http?.status < 400
      field: event.outcome
      value: success
  - set:
      if: ctx?.suricata?.eve?.event_type == "http" && ctx?.suricata?.eve?.http?.status >= 400
      field: event.outcome
      value: failure
  ## DNS
  - pipeline:
      if: >-
        ctx?.network?.protocol == "dns"
      name: '{< IngestPipeline "dns" >}'
  ## TLS
  - pipeline:
      if: ctx?.network?.protocol == "tls"
      name: '{< IngestPipeline "tls" >}'
  ## Flow
  - append:
      if: ctx?.suricata?.eve?.flow?.state == "new"
      field: event.type
      value:
        - start
  - append:
      if: ctx?.suricata?.eve?.flow?.state == "closed"
      field: event.type
      value:
        - end


  - set:
      value: "{{suricata.eve.http.http_method}}"
      field: http.request.method
      if: "ctx?.suricata?.eve?.http?.http_method != null"
  - rename:
      field: suricata.eve.http.status
      target_field: http.response.status_code
      ignore_missing: true
  - append:
      if: ctx.suricata?.eve?.http?.hostname != null
      value: '{{suricata.eve.http.hostname}}'
      field: destination.domain
      allow_duplicates: false
  - remove:
      field: suricata.eve.http.hostname
      ignore_failure: true
  - script:
      lang: painless
      tag: suricata_deduplicate_dest_domain
      source: >
        def domain = ctx.destination?.domain;
        if (domain instanceof Collection) {
          domain = domain.stream().distinct().collect(Collectors.toList());
          if (domain.length == 1) {
            domain = domain[0];
          }
          ctx.destination.domain = domain;
        }
      ignore_failure: true
  - set:
      if: "ctx?.network?.protocol == 'http'"
      field: url.domain
      value: '{{destination.domain}}'
      ignore_empty_value: true
  - grok:
      field: suricata.eve.http.url
      patterns:
        - '%{PATH:url.path}(?:\?%{QUERY:url.query})?(?:#%{ANY:url.fragment})?'
      ignore_missing: true
      pattern_definitions:
        PATH: '[^?#]*'
        QUERY: '[^#]*'
        ANY: '.*'
  - rename:
      field: suricata.eve.http.url
      target_field: url.original
      ignore_missing: true
  - rename:
      field: suricata.eve.http.http_refer
      target_field: http.request.referrer
      ignore_missing: true
  - rename:
      field: suricata.eve.http.length
      target_field: http.response.body.bytes
      ignore_missing: true
  - rename:
      field: suricata.eve.fileinfo.filename
      target_field: file.path
      ignore_missing: true
  - rename:
      field: suricata.eve.fileinfo.size
      target_field: file.size
      ignore_missing: true
  - lowercase:
      field: network.transport
      ignore_missing: true
  - convert:
      field: suricata.eve.alert.category
      target_field: message
      type: string
      ignore_missing: true
  - set:
      field: rule.category
      value: "{{suricata.eve.alert.category}}"
      ignore_empty_value: true
  - set:
      field: rule.id
      value: "{{suricata.eve.alert.signature_id}}"
      ignore_empty_value: true
  - set:
      field: rule.name
      value: "{{suricata.eve.alert.signature}}"
      ignore_empty_value: true
  - set:
      field: suricata.eve.alert.action
      value: denied
      if:  "ctx?.suricata?.eve?.alert?.action == 'blocked'"
  - append:
      field: event.type
      value: "{{suricata.eve.alert.action}}"
      if: "ctx?.suricata?.eve?.alert?.action != null"
  - remove:
      field: suricata.eve.alert.action
      ignore_failure: true
  - rename:
      field: suricata.eve.alert.severity
      target_field: event.severity
      ignore_missing: true
  - rename:
      field: suricata.eve.flow.pkts_toclient
      target_field: destination.packets
      ignore_missing: true
  - rename:
      field: suricata.eve.flow.pkts_toserver
      target_field: source.packets
      ignore_missing: true
  - rename:
      field: suricata.eve.flow.bytes_toclient
      target_field: destination.bytes
      ignore_missing: true
  - rename:
      field: suricata.eve.flow.bytes_toserver
      target_field: source.bytes
      ignore_missing: true
  - script:
      lang: painless
      source: >
        long getOrZero(def map, def key) {
          if (map!=null && map[key]!=null) {
            return map[key];
          }
          return 0;
        }
        def network=ctx['network'], source=ctx['source'], dest=ctx['destination'];
        def sp=getOrZero(source,'packets'), sb=getOrZero(source,'bytes'), dp=getOrZero(dest,'packets'), db=getOrZero(dest,'bytes');
        if (sb+db+sp+dp > 0) {
          if (network == null) {
            network=new HashMap();
            ctx['network']=network;
          }
          if (sb+db > 0) {
            network['bytes'] = sb+db;
          }
          if(sp+dp>0) {
            network['packets'] = sp+dp;
          }
        }
  - date:
      field: suricata.eve.flow.start
      target_field: event.start
      formats:
        - ISO8601
      ignore_failure: true
  - date:
      field: suricata.eve.flow.end
      target_field: event.end
      formats:
        - ISO8601
      ignore_failure: true
  - script:
      lang: painless
      source: >
        Instant ins(def d) {
          try {
            return Instant.parse(d);
          } catch(Exception e) {
            return null;
          }
        }
        def ev = ctx['event'];
        if (ev != null) {
          def start = ins(ev['start']);
          def end = ins(ev['end']);
          if (start != null && end != null && !start.isAfter(end)) {
            ev['duration'] = Duration.between(start,end).toNanos();
          }
        }
  - lowercase:
      field: suricata.eve.proto
      target_field: network.transport
      ignore_missing: true
  - user_agent:
      field: suricata.eve.http.http_user_agent
      ignore_missing: true
  - geoip:
      if: ctx.source?.geo == null
      field: source.ip
      target_field: source.geo
      ignore_missing: true
  - geoip:
      if: ctx.destination?.geo == null
      field: destination.ip
      target_field: destination.geo
      ignore_missing: true
  - geoip:
      database_file: GeoLite2-ASN.mmdb
      field: source.ip
      target_field: source.as
      properties:
        - asn
        - organization_name
      ignore_missing: true
  - geoip:
      database_file: GeoLite2-ASN.mmdb
      field: destination.ip
      target_field: destination.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
  - rename:
      field: destination.as.asn
      target_field: destination.as.number
      ignore_missing: true
  - rename:
      field: destination.as.organization_name
      target_field: destination.as.organization.name
      ignore_missing: true
  - append:
      field: related.hosts
      value: '{{url.domain}}'
      if: ctx.url?.domain != null && ctx.url?.domain != ''
      allow_duplicates: false
  - append:
      if: ctx?.source?.ip != null
      field: related.ip
      value: '{{source.ip}}'
      allow_duplicates: false
  - append:
      if: ctx?.destination?.ip != null
      field: related.ip
      value: '{{destination.ip}}'
      allow_duplicates: false
  - remove:
      field:
        - suricata.eve.app_proto
        - suricata.eve.flow.end
        - suricata.eve.flow.start
        - suricata.eve.http.http_method
        - suricata.eve.http.http_user_agent
      ignore_missing: true
on_failure:
  - set:
      field: error.message
      value: '{{ _ingest.on_failure_message }}'
