version: 2
title: APT Detection
summary: Endpoint logs and network traffic data merge to auto-detect exfiltration
contributor: https://github.com/rrwright
description: |-
  This APT (Advanced Persistent Threat) detection recipe ingests EDR (Endpoint
  Detection and Response) and network traffic logs, while monitoring for an IoB
  (Indicator of Behavior) that matches malicious data exfiltration patterns.

  SCENARIO:
  Using a standing query, the recipe monitors for covert interprocess
  communication using a file to pass data. When that pattern is matched, with a
  network SEND event, we have our smoking gun and a URL is logged linking to
  the Quine Exploration UI with the full activity and context for investigation.

  In this scenario, a malicious Excel macro collects personal data and stores
  it in a temporary file. The APT process "ntclean" infiltrated the system
  previously through an SSH exploit, and now reads from that temporary file
  and exfiltrates data from the network--hiding it as an HTTP GET request--
  before deleting the temporary file to cover its tracks.

  The source of the SSH exploit that planted the APT and the destination
  for exfiltrated data utilize the same IP address.

  SAMPLE DATA:
    endpoint.json - https://recipes.quine.io/apt-detection/endpoint-json
     network.json - https://recipes.quine.io/apt-detection/network-json

  Download the sample data to the same directory where Quine will be run.

  RESULTS:
  When the standing query detects the WRITE->READ->SEND->DELETE pattern, it
  will output a link to the console that can be copied and pasted into a
  browser to explore the event in the Quine Exploration UI.

ingestStreams:
  - name: endpoint-events
    source:
      type: File
      path: $endpoint_file
      format:
        type: Json
    query: >-
      MATCH (proc), (event), (object)
      WHERE id(proc) = idFrom($that.pid)
        AND id(event) = idFrom($that)
        AND id(object) = idFrom($that.object)

      SET proc.id = $that.pid,
          proc: Process,
          event.type = $that.event_type,
          event: EndpointEvent,
          event.time = $that.time,
          object.data = $that.object

      CREATE (proc)-[:EVENT]->(event)-[:EVENT]->(object)

  - name: network-events
    source:
      type: File
      path: $network_file
      format:
        type: Json
    query: >-
      MATCH (src), (dst), (event)
      WHERE id(src) = idFrom($that.src_ip+":"+$that.src_port)
        AND id(dst) = idFrom($that.dst_ip+":"+$that.dst_port)
        AND id(event) = idFrom('network_event', $that)

      SET src.ip = $that.src_ip+":"+$that.src_port,
          src: IP,
          dst.ip = $that.dst_ip+":"+$that.dst_port,
          dst: IP,
          event.proto = $that.proto,
          event.time = $that.time,
          event.detail = $that.detail,
          event: NetTraffic

      CREATE (src)-[:NET_TRAFFIC]->(event)-[:NET_TRAFFIC]->(dst)

standingQueries:
  - name: exfiltration-detection
    pattern:
      type: Cypher
      query: >-
        MATCH (e1)-[:EVENT]->(f)<-[:EVENT]-(e2),
              (f)<-[:EVENT]-(e3)<-[:EVENT]-(p2)-[:EVENT]->(e4)
        WHERE e1.type = "WRITE"
          AND e2.type = "READ"
          AND e3.type = "DELETE"
          AND e4.type = "SEND"
        RETURN DISTINCT id(f) as fileId
      mode: DistinctId
    outputs:
      - name: stolen-data
        preEnrichmentTransformation:
          type: InlineData
        resultEnrichment:
          query: >-
            MATCH (p1)-[:EVENT]->(e1)-[:EVENT]->(f)<-[:EVENT]-(e2)<-[:EVENT]-(p2),
                  (f)<-[:EVENT]-(e3)<-[:EVENT]-(p2)-[:EVENT]->(e4)-[:EVENT]->(ip)
            WHERE id(f) = $that.fileId
              AND e1.type = "WRITE"
              AND e2.type = "READ"
              AND e3.type = "DELETE"
              AND e4.type = "SEND"
              AND e1.time < e2.time
              AND e2.time < e3.time
              AND e2.time < e4.time

            CREATE (e1)-[:NEXT]->(e2)-[:NEXT]->(e4)-[:NEXT]->(e3)

            WITH e1, e2, e3, e4, p1, p2, f, ip, "http://localhost:8080/#MATCH" + text.urlencode(" (e1),(e2),(e3),(e4),(p1),(p2),(f),(ip) WHERE id(p1)='"+strId(p1)+"' AND id(e1)='"+strId(e1)+"' AND id(f)='"+strId(f)+"' AND id(e2)='"+strId(e2)+"' AND id(p2)='"+strId(p2)+"' AND id(e3)='"+strId(e3)+"' AND id(e4)='"+strId(e4)+"' AND id(ip)='"+strId(ip)+"' RETURN e1, e2, e3, e4, p1, p2, f, ip") as URL
            RETURN URL
          parameter: that
        destinations:
          - type: StandardOut

nodeAppearances:
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: Process
    icon: ion-load-a
    label:
      type: Property
      key: id
      prefix: "Process: "
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: IP
    icon: ion-ios-world
    label:
      type: Property
      key: ip
      prefix: ""
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: EndpointEvent
    icon: ion-android-checkmark-circle
    label:
      type: Property
      key: type
      prefix: ""
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: NetTraffic
    icon: ion-network
    label:
      type: Property
      key: proto
      prefix: ""
  - predicate:
      propertyKeys: []
      knownValues: {}
    icon: ion-ios-copy
    label:
      type: Property
      key: data
      prefix: ""

quickQueries:
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: Adjacent Nodes
      querySuffix: MATCH (n)--(m) RETURN DISTINCT m
      sort:
        type: Node
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: Refresh
      querySuffix: RETURN n
      sort:
        type: Node
  - predicate:
      propertyKeys: []
      knownValues: {}
    quickQuery:
      name: Local Properties
      querySuffix: RETURN id(n), properties(n)
      sort:
        type: Text
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: Process
    quickQuery:
      name: Files Read
      querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(f) WHERE e.type = "READ" RETURN f
      sort:
        type: Node
      edgeLabel: read
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: Process
    quickQuery:
      name: Files Written
      querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(f) WHERE e.type = "WRITE" RETURN f
      sort:
        type: Node
      edgeLabel: wrote
  - predicate:
      propertyKeys:
        - data
      knownValues: {}
    quickQuery:
      name: Read By
      querySuffix: MATCH (n)<-[:EVENT]-(e)<-[:EVENT]-(p) WHERE e.type = "READ" RETURN p
      sort:
        type: Node
      edgeLabel: written by
  - predicate:
      propertyKeys:
        - data
      knownValues: {}
    quickQuery:
      name: Written By
      querySuffix: MATCH (n)<-[:EVENT]-(e)<-[:EVENT]-(p) WHERE e.type = "WRITE" RETURN p
      sort:
        type: Node
      edgeLabel: written by
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: Process
    quickQuery:
      name: Received Data
      querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(i) WHERE e.type = "RECEIVE" RETURN i
      sort:
        type: Node
      edgeLabel: received
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: Process
    quickQuery:
      name: Sent Data
      querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(i) WHERE e.type = "SEND" RETURN i
      sort:
        type: Node
      edgeLabel: sent
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: Process
    quickQuery:
      name: Started By
      querySuffix: MATCH (n)<-[:EVENT]-(e)<-[:EVENT]-(p) WHERE e.type = "SPAWN" RETURN p
      sort:
        type: Node
      edgeLabel: parent process
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: Process
    quickQuery:
      name: Started Other Process
      querySuffix: MATCH (n)-[:EVENT]->(e)-[:EVENT]->(p) WHERE e.type = "SPAWN" RETURN p
      sort:
        type: Node
      edgeLabel: child process
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: IP
    quickQuery:
      name: Network Send
      querySuffix: MATCH (n)-[:NET_TRAFFIC]->(net) RETURN net
      sort:
        type: Node
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: IP
    quickQuery:
      name: Network Receive
      querySuffix: MATCH (n)<-[:NET_TRAFFIC]-(net) RETURN net
      sort:
        type: Node
  - predicate:
      propertyKeys: []
      knownValues: {}
      dbLabel: IP
    quickQuery:
      name: Network Communication
      querySuffix: MATCH (n)-[:NET_TRAFFIC]-(net)-[:NET_TRAFFIC]-(ip) RETURN ip
      sort:
        type: Node
      edgeLabel: Communication

sampleQueries: []
