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.
Allon Analytics > Workflows. Refer to Kibana privileges. - Threat intel API key. A VirusTotal, AbuseIPDB, or similar API key. Store keys in the workflow's
constsblock 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:
- Manual trigger with a
hashorip_addressinput. httpstep 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.- Optional
httpstep adds geolocation or secondary scoring. consolestep formats a human-readable summary with a risk assessment.
-
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: manualWhen you run the workflow from the YAML editor, Kibana prompts you for the
hashinput. -
Call VirusTotal with retry and continue
The
httpstep queries the VirusTotal file-lookup endpoint.on-failure.retrybacks off on transient failures, andcontinue: truelets 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: trueThe response body lands at
steps.lookup_hash.output.data. The relevant fields areattributes.last_analysis_stats.malicious,attributes.last_analysis_stats.suspicious, andattributes.names. -
Format and log the result
Use a
consolestep 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 %}
Full workflow 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
manualtrigger with an alert trigger and read the hash fromevent.alerts[0].file.hash.sha256. - Add a second enrichment provider. Chain an additional
httpstep against AbuseIPDB or a private intel feed. Theip-reputation-check.yamlsource workflow shows the two-provider pattern. - Store the enrichment for later. Write the result to an index with
elasticsearch.requestso dashboards and subsequent workflows can query it. - Compose into triage. Extract these steps into a child workflow named
shared--enrich-hashand call it from your triage workflow withworkflow.execute. - Decide on next actions. Branch on the
maliciouscount with anifstep to open a case, post to Slack, or stop early when the hash is clean.
- Triage a security alert into a case: Pair enrichment with case creation for full triage.
- HTTP step: Full
httpstep reference. - Pass data and handle errors: Retry, fallback, and continue strategies in more depth.
elastic/workflowsenrichment folder: More enrichment examples.