Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion examples/security/response/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

Incident response, case management, and remediation workflows

## Workflows (4)
## Workflows (7)

| Workflow | Description |
|----------|-------------|
| [Remediate Startup Folder Persistence](./remediate-persistence-windows-startup-folder.yaml) | ES\|QL correlates user/ProgramData Startup persistence with endpoint alerts in 5m windows; skips if get-file exists in history, then downloads, waits 20s, and deletes |
| [Remediate Run Key Persistence](./remediate-persistence-windows-run-key.yaml) | ES\|QL correlates Run-key registry persistence with endpoint alerts in 5m windows; skips if delete exists in history, then removes the Run value via PowerShell |
| [Remediate Scheduled Task Persistence](./remediate-persistence-windows-scheduled-task.yaml) | ES\|QL correlates scheduled task creation with endpoint alerts in 5m windows; skips if schtasks delete exists in history, then removes the task |
| [📁 Case workflow - Prod](./case-workflow-prod.yaml) | The YAML workflow outlines a security operations process that triggers on alerts |
| [📁 Traditional Triage](./traditional-triage.yaml) | The "Traditional Triage" workflow automates the response to security alerts, par |
| [🔒 AD - Automated Triaging](./ad-automated-triaging.yaml) | The YAML workflow outlines an automated triaging process for security operations |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# =============================================================================
# Workflow: Remediate Run Key Persistence
# Category: security/response
#
# Finds Run-key and Policies\Explorer\Run registry persistence via ES|QL on
# logs-endpoint.*, correlating with Malicious Behavior Prevention alerts within
# 5-minute windows on the same agent/host, then for each hit:
# 0. Checks newest execute in response history (pageSize 1) for same path.
# 1. execute — PowerShell Remove-ItemProperty on registry.hive\registry.key.
#
# ES|QL columns: [0] agent.id, [1] host.id, [2] registry.hive, [3] registry.key,
# [4] registry.value, [5] registry.path, [6] registry.data.strings
#
# registry.path ends with the value name; remediation uses registry.hive + key + value.
#
## The workflow is set to run every 15m, you can also attach it as a custom workflow action to the "Endpoint Security (Elastic Defend)" https://www.elastic.co/guide/en/security/8.19/endpoint-security-elastic-defend.html SIEM rule so it triggers everytime a behavior alert is generated.
# =============================================================================
version: "1"
name: Remediate Run Key Persistence
description: |
Query logs-endpoint.* for Run-key registry persistence correlated with endpoint
alerts within 5-minute windows, skip values already removed per response actions
history, then delete the Run value via PowerShell Remove-ItemProperty.
enabled: false
tags:
- security
- endpoint
- response
- remediation
- persistence
- registry

triggers:
- type: manual
inputs:
- name: esql_query
type: string
description: ES|QL returning agent.id, host.id, registry.hive, registry.key, registry.value, registry.path, registry.data.strings per row.
required: true
default: |
FROM logs-endpoint.*
| WHERE @timestamp > NOW() - 15 minutes and
(message like "Malicious Behavior Prevention Alert*" OR (event.category == "registry" and (registry.path like """*\\Windows\\CurrentVersion\\Run\\*""" or registry.path like """*CurrentVersion\\Policies\\Explorer\\Run\\*""") and registry.data.strings IS NOT NULL))
| EVAL is_endpoint_alert = CASE(message like "Malicious Behavior Prevention Alert*", agent.id, NULL), is_run_persistence = CASE(event.category == "registry" and (registry.path like """*\\Windows\\CurrentVersion\\Run\\*""" or registry.path like """*CurrentVersion\\Policies\\Explorer\\Run\\*""") and registry.data.strings IS NOT NULL, agent.id, NULL)
| STATS endpoint_alerts_count = COUNT_DISTINCT(is_endpoint_alert), persistence_count = COUNT_DISTINCT(is_run_persistence), rules = VALUES(message), registry.hive = VALUES(registry.hive), registry.key = VALUES(registry.key), registry.value = VALUES(registry.value), registry.path = VALUES(registry.path), registry.data.strings = VALUES(registry.data.strings) by agent.id, host.id, DATE_TRUNC(5 minutes, @timestamp)
| WHERE endpoint_alerts_count >= 1 and persistence_count >= 1
| STATS BY agent.id, host.id, registry.hive, registry.key, registry.value, registry.path, registry.data.strings
- name: history_lookback
type: string
description: How far back to search response actions history (Date Math, e.g. now-1d).
default: now-1d
- type: scheduled
with:
every: 15m

steps:
- name: run_esql
type: elasticsearch.esql.query
with:
query: "{{ inputs.esql_query }}"

- name: process_hits
type: if
condition: steps.run_esql.output.values.length > 0
steps:
- name: per_persistence_hit
type: foreach
foreach: "{{ steps.run_esql.output.values }}"
steps:
- name: set_hit_context
type: data.set
with:
registry_path: "{{ foreach.item[5] | downcase }}"
reg_key: >-
{% if foreach.item[2] != '' and foreach.item[2] != null and foreach.item[3] != '' and foreach.item[3] != null %}{{ foreach.item[2] }}\{{ foreach.item[3] }}{% else %}{% assign rp = foreach.item[5] %}{% assign parts = rp | split: '\' %}{% assign key_path = '' %}{% for part in parts %}{% unless forloop.last %}{% if key_path != '' %}{% assign key_path = key_path | append: '\' %}{% endif %}{% assign key_path = key_path | append: part %}{% endunless %}{% endfor %}{{ key_path }}{% endif %}
reg_value: >-
{% if foreach.item[4] != '' and foreach.item[4] != null %}{{ foreach.item[4] }}{% else %}{% assign rp = foreach.item[5] %}{% assign parts = rp | split: '\' %}{{ parts | last }}{% endif %}

- name: log_hit
type: console
with:
message: "Checking Run-key persistence — agent {% if foreach.item[0] != '' and foreach.item[0] != null %}{{ foreach.item[0] }}{% else %}{{ foreach.item[1] }}{% endif %}, key {{ steps.set_hit_context.output.reg_key }}, value {{ steps.set_hit_context.output.reg_value }} = {{ foreach.item[6] }} (row {{ foreach.index }})."

- name: check_execute_history
type: kibana.request
with:
method: GET
path: "/api/endpoint/action"
query:
agentIds: "{% if foreach.item[0] != '' and foreach.item[0] != null %}{{ foreach.item[0] }}{% else %}{{ foreach.item[1] }}{% endif %}"
commands: execute
startDate: "{{ inputs.history_lookback }}"
endDate: now
pageSize: 1
headers:
kbn-xsrf: "true"
on-failure:
continue: true

- name: capture_history
type: data.set
with:
should_skip: "{% if steps.check_execute_history.output.total == 0 %}false{% else %}{% for action in steps.check_execute_history.output.data limit:1 %}{% assign rp = steps.set_hit_context.output.registry_path %}{% assign rk = steps.set_hit_context.output.reg_key | downcase %}{% assign rv = steps.set_hit_context.output.reg_value | downcase %}{% assign hc = action.parameters.command | downcase %}{% assign cm = action.comment | downcase %}{% if hc contains rp %}true{% elsif hc contains rk and hc contains rv %}true{% elsif cm contains rp %}true{% elsif cm contains rv and cm contains 'remediate run key' %}true{% else %}false{% endif %}{% endfor %}{% endif %}"

- name: already_remediated
type: if
condition: 'steps.capture_history.output.should_skip: "true"'
steps:
- name: log_already_in_history
type: console
with:
message: "Skipping {{ foreach.item[5] }} on agent {% if foreach.item[0] != '' and foreach.item[0] != null %}{{ foreach.item[0] }}{% else %}{{ foreach.item[1] }}{% endif %} — delete already in response actions history ({{ inputs.history_lookback }})."
else:
- name: delete_registry_value
type: kibana.request
with:
method: POST
path: "/api/endpoint/action/execute"
headers:
kbn-xsrf: "true"
body:
endpoint_ids:
- "{% if foreach.item[0] != '' and foreach.item[0] != null %}{{ foreach.item[0] }}{% else %}{{ foreach.item[1] }}{% endif %}"
comment: "Workflow Remediate Run Key Persistence — delete {{ steps.set_hit_context.output.reg_key }} value {{ steps.set_hit_context.output.reg_value }} ({{ foreach.item[6] }})."
parameters:
command: 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Remove-ItemProperty -LiteralPath ''Registry::{{ steps.set_hit_context.output.reg_key }}'' -Name ''{{ steps.set_hit_context.output.reg_value }}'' -Force -ErrorAction Stop"'
timeout: 120
on-failure:
continue: true

- name: log_remediation_result
type: console
with:
message: "{% if steps.delete_registry_value.error %}Delete warning for {{ steps.set_hit_context.output.reg_key }}\\{{ steps.set_hit_context.output.reg_value }}: {{ steps.delete_registry_value.error }}{% else %}Remove-ItemProperty submitted for {{ steps.set_hit_context.output.reg_key }} value {{ steps.set_hit_context.output.reg_value }} (data: {{ foreach.item[6] }}).{% endif %}"

else:
- name: log_no_hits
type: console
with:
message: "No Run-key persistence correlated with endpoint alerts in the configured ES|QL window."

- name: log_summary
type: console
with:
message: |
Processed {{ steps.run_esql.output.values | size }} unique Run-key persistence target(s). Check Security → Endpoints → Response actions history.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# =============================================================================
# Workflow: Remediate Scheduled Task Persistence
# Category: security/response
#
# Finds scheduled task file creation under C:\Windows\System32\Tasks via ES|QL on
# logs-endpoint.*, correlating with Malicious Behavior Prevention alerts within
# 5-minute windows on the same agent/host, then for each hit:
# 0. Checks newest execute in response history (pageSize 1) for same task.
# 1. execute — schtasks /Delete using file.name as the task name.
#
# ES|QL columns: [0] agent.id, [1] host.id, [2] file.path, [3] file.name
#
# The workflow is set to run every 15m; you can also attach it as a custom
# workflow action to the "Endpoint Security (Elastic Defend)" SIEM rule.
# =============================================================================
version: "1"
name: Remediate Scheduled Task Persistence
description: |
Query logs-endpoint.* for scheduled task persistence correlated with endpoint
alerts within 5-minute windows, skip tasks already deleted per response actions
history, then remove the task via schtasks /Delete.
enabled: false
tags:
- security
- endpoint
- response
- remediation
- persistence
- scheduled-task

triggers:
- type: manual
inputs:
- name: esql_query
type: string
description: ES|QL returning agent.id, host.id, file.path, file.name per row.
required: true
default: |
FROM logs-endpoint.*
| WHERE @timestamp > NOW() - 15 minutes and
(message like "Malicious Behavior Prevention Alert*" OR (event.category == "file" and process.name == "svchost.exe" and file.path like """C:\\Windows\\System32\\Tasks\\*""" and MV_CONTAINS(event.action, "creation")))
| EVAL is_endpoint_alert = CASE(message like "Malicious Behavior Prevention Alert*", agent.id, NULL), is_task_persistence = CASE(event.category == "file" and process.name == "svchost.exe" and file.path like """C:\\Windows\\System32\\Tasks\\*""", agent.id, NULL)
| STATS endpoint_alerts_count = COUNT_DISTINCT(is_endpoint_alert), persistence_count = COUNT_DISTINCT(is_task_persistence), file.path = VALUES(file.path), file.name = VALUES(file.name) by agent.id, host.id, DATE_TRUNC(5 minutes, @timestamp)
| WHERE endpoint_alerts_count >= 1 and persistence_count >= 1
| STATS BY agent.id, host.id, file.path, file.name
- name: history_lookback
type: string
description: How far back to search response actions history (Date Math, e.g. now-1d).
default: now-1d
- type: scheduled
with:
every: 15m

steps:
- name: run_esql
type: elasticsearch.esql.query
with:
query: "{{ inputs.esql_query }}"

- name: process_hits
type: if
condition: steps.run_esql.output.values.length > 0
steps:
- name: per_persistence_hit
type: foreach
foreach: "{{ steps.run_esql.output.values }}"
steps:
- name: set_hit_context
type: data.set
with:
file_path: "{{ foreach.item[2] | downcase }}"
task_name: >-
{% if foreach.item[3] != '' and foreach.item[3] != null %}{{ foreach.item[3] }}{% else %}{% assign fp = foreach.item[2] %}{% assign parts = fp | split: '\' %}{{ parts | last }}{% endif %}

- name: log_hit
type: console
with:
message: "Checking scheduled task persistence — agent {% if foreach.item[0] != '' and foreach.item[0] != null %}{{ foreach.item[0] }}{% else %}{{ foreach.item[1] }}{% endif %}, task {{ steps.set_hit_context.output.task_name }}, path {{ foreach.item[2] }} (row {{ foreach.index }})."

- name: check_execute_history
type: kibana.request
with:
method: GET
path: "/api/endpoint/action"
query:
agentIds: "{% if foreach.item[0] != '' and foreach.item[0] != null %}{{ foreach.item[0] }}{% else %}{{ foreach.item[1] }}{% endif %}"
commands: execute
startDate: "{{ inputs.history_lookback }}"
endDate: now
pageSize: 1
headers:
kbn-xsrf: "true"
on-failure:
continue: true

- name: capture_history
type: data.set
with:
should_skip: "{% if steps.check_execute_history.output.total == 0 %}false{% else %}{% for action in steps.check_execute_history.output.data limit:1 %}{% assign fp = steps.set_hit_context.output.file_path %}{% assign tn = steps.set_hit_context.output.task_name | downcase %}{% assign hc = action.parameters.command | downcase %}{% assign cm = action.comment | downcase %}{% if hc contains fp %}true{% elsif hc contains tn and hc contains 'schtasks' %}true{% elsif cm contains fp %}true{% elsif cm contains tn and cm contains 'scheduled task' %}true{% else %}false{% endif %}{% endfor %}{% endif %}"

- name: already_remediated
type: if
condition: 'steps.capture_history.output.should_skip: "true"'
steps:
- name: log_already_in_history
type: console
with:
message: "Skipping task {{ steps.set_hit_context.output.task_name }} on agent {% if foreach.item[0] != '' and foreach.item[0] != null %}{{ foreach.item[0] }}{% else %}{{ foreach.item[1] }}{% endif %} — delete already in response actions history ({{ inputs.history_lookback }})."
else:
- name: delete_scheduled_task
type: kibana.request
with:
method: POST
path: "/api/endpoint/action/execute"
headers:
kbn-xsrf: "true"
body:
endpoint_ids:
- "{% if foreach.item[0] != '' and foreach.item[0] != null %}{{ foreach.item[0] }}{% else %}{{ foreach.item[1] }}{% endif %}"
comment: "Workflow Remediate Scheduled Task Persistence — delete task {{ steps.set_hit_context.output.task_name }} ({{ foreach.item[2] }})."
parameters:
command: 'schtasks /Delete /TN "{{ steps.set_hit_context.output.task_name }}" /F'
timeout: 120
on-failure:
continue: true

- name: log_remediation_result
type: console
with:
message: "{% if steps.delete_scheduled_task.error %}Delete warning for task {{ steps.set_hit_context.output.task_name }}: {{ steps.delete_scheduled_task.error }}{% else %}schtasks /Delete submitted for {{ steps.set_hit_context.output.task_name }} (path: {{ foreach.item[2] }}).{% endif %}"

else:
- name: log_no_hits
type: console
with:
message: "No scheduled task persistence correlated with endpoint alerts in the configured ES|QL window."

- name: log_summary
type: console
with:
message: |
Processed {{ steps.run_esql.output.values | size }} unique scheduled task persistence target(s). Check Security → Endpoints → Response actions history.
Loading