Loading

Enrich an alert with threat intelligence

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:

    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:

    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:

    - 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.