Triage a security alert into a case
Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud. Availability This functionality is generally available and ready for production usage. Serverless interfaces and procedures might differ from classic Elastic Stack deployments. The Elastic Stack includes Elastic's core products such as Elasticsearch, Kibana, Logstash, and Beats. Availability 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.
Serverless
Generally available
Stack
9.4+
Generally available since 9.4
This guide walks through building a workflow that turns a raw security alert into a triaged case. The workflow fires when a detection rule matches, enriches the alert with threat intel, opens a case with the alert and its indicators attached, isolates the affected host, and notifies the on-call analyst in Slack.
The workflow is adapted from traditional-triage.yaml in the elastic/workflows library.
If you're new to workflows, complete Build your first workflow first for a walkthrough of the YAML editor and how to run a workflow.
- Permissions.
Allprivileges for Analytics → Workflows, plusAllon Security → Cases in the target space. Refer to Kibana privileges. - Detection rule. An enabled detection rule that generates the kind of alert you want to triage. For this workflow, the rule should produce alerts with
file.hash.sha256,host.name, andelastic.agent.idpopulated. - Attach the workflow to the rule. After you save the workflow, attach it to the detection rule so the rule invokes the workflow when it fires. Refer to Alert triggers.
- Connectors. A configured VirusTotal connector for the hash lookup, and a Slack connector for the notification. Note the connector IDs. You'll paste them into the workflow.
- Host isolation capability. The affected host must run Elastic Defend for the isolation step to succeed.
The workflow runs in a single pass when an alert arrives:
- An alert trigger fires when the detection rule matches.
- A VirusTotal lookup enriches the alert with a reputation score.
- An
ifstep branches on the reputation score. If the file is confirmed malicious, the workflow opens a case, attaches the alert and observables, isolates the host, and notifies Slack. Otherwise, it closes the alert as a false positive.
-
Configure the alert trigger
The workflow fires every time the attached detection rule generates an alert. Inside the workflow, the alert payload is available as
event.alerts[0].YAML triggers: - type: alertAfter you save the workflow, open the detection rule's Actions tab and attach this workflow so the rule invokes it.
-
Enrich the alert with threat intel
Call the VirusTotal connector to score the file hash. Wrap the call in
retry + continueso a transient VirusTotal outage doesn't fail the whole workflow.YAML - name: lookup_reputation type: virustotal.scanFileHash connector-id: "my-virustotal" on-failure: retry: max-attempts: 3 delay: "5s" strategy: exponential max-delay: "30s" continue: true with: hash: "{{ event.alerts[0].file.hash.sha256 }}"The output lives at
steps.lookup_reputation.output. Usesteps.lookup_reputation.output.stats.maliciousto decide what to do next. -
Branch on the reputation result
Most of the workflow only runs when the file is confirmed malicious. Wrap the
case,isolation, andnotificationsteps in anifstep:YAML - name: handle_malicious_file type: if condition: "steps.lookup_reputation.output.stats.malicious > 10" steps: # Case creation, host isolation, and Slack notification go here. else: - name: close_false_positive type: kibana.SetAlertsStatus with: status: closed reason: false_positive signal_ids: - "{{ event.alerts[0]._id }}"The
elsebranch closes the alert as a false positive usingkibana.SetAlertsStatus. -
Open a case with the alert context
Inside the
ifbranch, create the case withcases.createCase. Fill the title and description from the alert payload:YAML - name: create_case type: cases.createCase with: title: "Malware detected: {{ event.alerts[0].file.hash.sha256 }}" description: | Auto-created from detection rule `{{ event.rule.name }}`. VirusTotal malicious engines: {{ steps.lookup_reputation.output.stats.malicious | default: "n/a" }} owner: "securitySolution" severity: "high" tags: ["auto-triage", "malware"]title,description, andownerare required.ownermust be one ofsecuritySolution,observability, orcases. -
Attach the alert and observables to the case
Link the alert that triggered the workflow with
cases.addAlerts, then attach the file hash and source IP as observables withcases.addObservables:YAML - name: attach_alert type: cases.addAlerts with: case_id: "{{ steps.create_case.output.case.id }}" alerts: - alertId: "{{ event.alerts[0]._id }}" index: "{{ event.alerts[0]._index }}" rule: id: "{{ event.rule.id }}" name: "{{ event.rule.name }}" - name: attach_observables type: cases.addObservables with: case_id: "{{ steps.create_case.output.case.id }}" observables: - typeKey: "observable-type-file-hash" value: "{{ event.alerts[0].file.hash.sha256 }}" - typeKey: "observable-type-ipv4" value: "{{ event.alerts[0].source.ip }}" description: "Source of the malicious activity"Observable
typeKeyvalues must match the built-in observable types. Refer tocases.addObservablesfor the full list. -
Isolate the affected host
Call the endpoint isolation API with
kibana.request. Link the isolation action to the case and alert so the audit trail is complete:YAML - name: isolate_host type: kibana.request with: method: POST path: /api/endpoint/action/isolate body: endpoint_ids: - "{{ event.alerts[0].elastic.agent.id }}" comment: "Automated isolation: case {{ steps.create_case.output.case.id }}" case_ids: - "{{ steps.create_case.output.case.id }}" alert_ids: - "{{ event.alerts[0]._id }}" -
Notify the on-call analyst
Post a rich message to the SOC Slack channel with links to the case and the VirusTotal report. Use the
{{kibanaUrl}}context variable for the case deep link:YAML - name: notify_slack type: http with: url: https://slack.com/api/chat.postMessage method: POST headers: Content-Type: application/json; charset=utf-8 Authorization: "Bearer {{ consts.slack_token }}" body: channel: "#soc-oncall" text: "Malware detected on {{ event.alerts[0].host.name }}" blocks: >- [{"type":"section","text":{"type":"mrkdwn","text":"*Malicious file on {{ event.alerts[0].host.name }}*\nHash: `{{ event.alerts[0].file.hash.sha256 }}`\nMalicious engines: {{ steps.lookup_reputation.output.stats.malicious }}"}}, {"type":"actions","elements":[{"type":"button","text":{"type":"plain_text","text":"View case"},"url":"{{ kibanaUrl }}/app/security/cases/{{ steps.create_case.output.case.id }}"}]}] timeout: 30sStore the Slack bot token in a
constsblock so you can swap environments without editing step bodies.
Full workflow YAML
name: security--triage-alert
description: Auto-triage a detection alert. Enrich with VirusTotal, open a case, attach observables, isolate the host, and notify Slack.
enabled: true
triggers:
- type: alert
consts:
slack_token: "YOUR-SLACK-BOT-TOKEN"
settings:
concurrency:
key: "{{ event.alerts[0].host.name }}"
strategy: drop
max: 1
steps:
- name: lookup_reputation
type: virustotal.scanFileHash
connector-id: "my-virustotal"
on-failure:
retry:
max-attempts: 3
delay: "5s"
strategy: exponential
max-delay: "30s"
continue: true
with:
hash: "{{ event.alerts[0].file.hash.sha256 }}"
- name: handle_malicious_file
type: if
condition: "steps.lookup_reputation.output.stats.malicious > 10"
steps:
- name: create_case
type: cases.createCase
with:
title: "Malware detected: {{ event.alerts[0].file.hash.sha256 }}"
description: |
Auto-created from detection rule `{{ event.rule.name }}`.
VirusTotal malicious engines: {{ steps.lookup_reputation.output.stats.malicious | default: "n/a" }}
owner: "securitySolution"
severity: "high"
tags: ["auto-triage", "malware"]
- name: attach_alert
type: cases.addAlerts
with:
case_id: "{{ steps.create_case.output.case.id }}"
alerts:
- alertId: "{{ event.alerts[0]._id }}"
index: "{{ event.alerts[0]._index }}"
rule:
id: "{{ event.rule.id }}"
name: "{{ event.rule.name }}"
- name: attach_observables
type: cases.addObservables
with:
case_id: "{{ steps.create_case.output.case.id }}"
observables:
- typeKey: "observable-type-file-hash"
value: "{{ event.alerts[0].file.hash.sha256 }}"
- typeKey: "observable-type-ipv4"
value: "{{ event.alerts[0].source.ip }}"
description: "Source of the malicious activity"
- name: isolate_host
type: kibana.request
with:
method: POST
path: /api/endpoint/action/isolate
body:
endpoint_ids:
- "{{ event.alerts[0].elastic.agent.id }}"
comment: "Automated isolation: case {{ steps.create_case.output.case.id }}"
case_ids:
- "{{ steps.create_case.output.case.id }}"
alert_ids:
- "{{ event.alerts[0]._id }}"
- name: notify_slack
type: http
with:
url: https://slack.com/api/chat.postMessage
method: POST
headers:
Content-Type: application/json; charset=utf-8
Authorization: "Bearer {{ consts.slack_token }}"
body:
channel: "#soc-oncall"
text: "Malware detected on {{ event.alerts[0].host.name }}"
timeout: 30s
else:
- name: close_false_positive
type: kibana.SetAlertsStatus
with:
status: closed
reason: false_positive
signal_ids:
- "{{ event.alerts[0]._id }}"
- Add historical context. Before opening the case, run an
elasticsearch.esql.queryto count how many times the hash appears across your logs. Attach the count to the case withcases.addComment. - Route by severity. Replace the single
ifbranch with aswitchstep that opens cases of different severities based on the malicious-engine count. - Enrich with an AI summary. Add an
ai.summarizestep afterattach_observablesto produce a triage summary, then append it to the case withcases.addComment. - Assign the case. Query your on-call schedule and use
cases.assignCaseto assign the case to the current on-call analyst.
- Automate security operations: The outcomes this workflow supports.
- Cases action steps: Reference for every
cases.*step. - Pass data and handle errors: How retry, fallback, and continue work together.
- Triage alerts: The interactive triage flow this workflow automates.
- Security cases: The product surface this workflow drives.
elastic/workflowslibrary: More security workflow examples.