Type to search.

Enrich an alert with threat intelligence

Serverless

Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.

Availability

  • Generally available

    This functionality is generally available and ready for production usage.

Serverless interfaces and procedures might differ from classic Elastic Stack deployments.

Stack 9.4+

The Elastic Stack includes Elastic's core products such as Elasticsearch, Kibana, Logstash, and Beats.

Availability

  • Generally available since 9.4

    This functionality is generally available and ready for production usage.

Unless stated otherwise on the page, this functionality is available when your Elastic Stack is deployed on Elastic Cloud Hosted, Elastic Cloud Enterprise, Elastic Cloud on Kubernetes, and self-managed environments.

This documentation corresponds to the latest patch available for each minor version. If you're not using the latest patch, check the release notes for changes.

This guide walks through building a focused enrichment workflow. The workflow calls a threat intelligence API (VirusTotal) with a hash or indicator, optionally runs additional enrichment providers, and formats the result. It's a small, useful pattern on its own, and a building block you can drop into larger automations like Triage a security alert into a case.

The workflow is adapted from send-hash-to-virustotal.yaml and ip-reputation-check.yaml in the elastic/workflows library.

If you're new to workflows, complete Build your first workflow first.

  • Permissions. All on Analytics → Workflows. Refer to Kibana privileges.
  • Threat intel API key. A VirusTotal, AbuseIPDB, or similar API key. Store keys in the workflow's consts block so you can swap environments without touching step bodies.
  • Optional: an alert-triggered parent workflow. If you want the enrichment to run automatically when an alert fires, attach it to a detection rule. This guide uses a manual trigger so you can test the workflow in isolation first.

The workflow runs on demand, takes the indicator you want to enrich as an input, calls one or more threat intel APIs, and logs the combined result:

  1. Manual trigger with a hash or ip_address input.
  2. http step calls the primary threat intel API (VirusTotal for hashes, AbuseIPDB for IPs). Retry transient errors and continue on failure so a single outage doesn't kill the workflow.
  3. Optional http step adds geolocation or secondary scoring.
  4. console step formats a human-readable summary with a risk assessment.
  1. Declare the input and constants

    Inputs let you run the workflow against any indicator without editing YAML. Constants hold the API key and base URL:

    YAML
    inputs:
      - name: hash
        type: string
        description: SHA256 file hash to look up.
        required: true
    
    consts:
      vt_api_key: "YOUR-VIRUSTOTAL-API-KEY"
      vt_base_url: "https://www.virustotal.com/api/v3"
    
    triggers:
      - type: manual
    

    When you run the workflow from the YAML editor, Kibana prompts you for the hash input.

  2. Call VirusTotal with retry and continue

    The http step queries the VirusTotal file-lookup endpoint. on-failure.retry backs off on transient failures, and continue: true lets downstream formatting still run if VirusTotal is unreachable:

    YAML
    steps:
      - name: lookup_hash
        type: http
        with:
          url: "{{ consts.vt_base_url }}/files/{{ inputs.hash }}"
          method: GET
          headers:
            x-apikey: "{{ consts.vt_api_key }}"
            Accept: application/json
          timeout: 30s
        on-failure:
          retry:
            max-attempts: 3
            delay: "5s"
            strategy: exponential
            max-delay: "30s"
          continue: true
    

    The response body lands at steps.lookup_hash.output.data. The relevant fields are attributes.last_analysis_stats.malicious, attributes.last_analysis_stats.suspicious, and attributes.names.

  3. Format and log the result

    Use a console step with Liquid conditionals to produce a short, human-readable report. Console output appears in the workflow execution log, so this step is useful whether the workflow is being run manually or composed into a larger automation:

    YAML
    - name: format_report
      type: console
      with:
        message: |
          === Threat Intel Report ===
          Hash: {{ inputs.hash }}
    
          Malicious engines: {{ steps.lookup_hash.output.data.data.attributes.last_analysis_stats.malicious | default: "n/a" }}
          Suspicious engines: {{ steps.lookup_hash.output.data.data.attributes.last_analysis_stats.suspicious | default: "n/a" }}
          Known filenames: {{ steps.lookup_hash.output.data.data.attributes.names | join: ", " | default: "n/a" }}
    
          Assessment:
          {% if steps.lookup_hash.output.data.data.attributes.last_analysis_stats.malicious > 10 %}
          HIGH RISK: more than 10 engines flag this hash as malicious.
          {% elsif steps.lookup_hash.output.data.data.attributes.last_analysis_stats.malicious > 0 %}
          MEDIUM RISK: at least one engine flagged this hash.
          {% else %}
          LOW RISK: no engines flagged this hash.
          {% endif %}
    
Full workflow YAML
YAML
name: enrich--hash-with-virustotal
description: Enrich a file hash with VirusTotal reputation data and print a short report.
enabled: true
tags: ["enrichment", "threat-intel"]

inputs:
  - name: hash
    type: string
    description: SHA256 file hash to look up.
    required: true

consts:
  vt_api_key: "YOUR-VIRUSTOTAL-API-KEY"
  vt_base_url: "https://www.virustotal.com/api/v3"

triggers:
  - type: manual

steps:
  - name: lookup_hash
    type: http
    with:
      url: "{{ consts.vt_base_url }}/files/{{ inputs.hash }}"
      method: GET
      headers:
        x-apikey: "{{ consts.vt_api_key }}"
        Accept: application/json
      timeout: 30s
    on-failure:
      retry:
        max-attempts: 3
        delay: "5s"
        strategy: exponential
        max-delay: "30s"
      continue: true

  - name: format_report
    type: console
    with:
      message: |
        === Threat Intel Report ===
        Hash: {{ inputs.hash }}

        Malicious engines: {{ steps.lookup_hash.output.data.data.attributes.last_analysis_stats.malicious | default: "n/a" }}
        Suspicious engines: {{ steps.lookup_hash.output.data.data.attributes.last_analysis_stats.suspicious | default: "n/a" }}
        Known filenames: {{ steps.lookup_hash.output.data.data.attributes.names | join: ", " | default: "n/a" }}

        Assessment:
        {% if steps.lookup_hash.output.data.data.attributes.last_analysis_stats.malicious > 10 %}
        HIGH RISK: more than 10 engines flag this hash as malicious.
        {% elsif steps.lookup_hash.output.data.data.attributes.last_analysis_stats.malicious > 0 %}
        MEDIUM RISK: at least one engine flagged this hash.
        {% else %}
        LOW RISK: no engines flagged this hash.
        {% endif %}
  • Trigger from an alert instead of manually. Replace the manual trigger with an alert trigger and read the hash from event.alerts[0].file.hash.sha256.
  • Add a second enrichment provider. Chain an additional http step against AbuseIPDB or a private intel feed. The ip-reputation-check.yaml source workflow shows the two-provider pattern.
  • Store the enrichment for later. Write the result to an index with elasticsearch.request so dashboards and subsequent workflows can query it.
  • Compose into triage. Extract these steps into a child workflow named shared--enrich-hash and call it from your triage workflow with workflow.execute.
  • Decide on next actions. Branch on the malicious count with an if step to open a case, post to Slack, or stop early when the hash is clean.