Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import ACTION_ISOLATE_HOST_YAML from './action_isolate_host.yaml';
import type { ManagedWorkflowDefinition } from '../../../types';
import {
ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID,
ALERTZERO_RULE_WORKFLOW_MANAGEMENT,
} from '../constants';

export const ALERTZERO_ACTION_ISOLATE_HOST_WORKFLOW_ID = 'system-alertzero-action-isolate-host';

export const ALERTZERO_ACTION_ISOLATE_HOST_WORKFLOW = {
billable: false,
id: ALERTZERO_ACTION_ISOLATE_HOST_WORKFLOW_ID,
management: ALERTZERO_RULE_WORKFLOW_MANAGEMENT,
pluginId: ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID,
version: 1,
yaml: ACTION_ISOLATE_HOST_YAML,
} as const satisfies ManagedWorkflowDefinition;
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
version: "1"
name: Isolate host
description: >
AlertZero action: isolates one or more Elastic Defend endpoints from the
network. Executed by the investigation proposal gate once an analyst
approves, so the response action is attributed to the approver.
enabled: true
tags:
# Generic tag every action workflow carries, so the catalog can be
# discovered by tag rather than a hardcoded list.
- action
- alertzero
consts:
# Self-describing catalog metadata, read back by the catalog when a
# proposal referencing this workflow is created or rendered. Metadata has
# to live under `consts`: unknown top-level keys are stripped by the schema.
actionMetadata:
name: Isolate host
description: Isolates Elastic Defend endpoints from the network. Only Elastic Defend retains connectivity.
category: contain
impact: high
reversible: true
approvalPolicy: always-gate
actionLabel: Host isolation
triggers:
- type: manual
inputs:
properties:
actionInput:
type: object
description: >
Isolate fields. Every action workflow takes a single `actionInput`
object so the generic gate workflow never needs to know an
action's parameter names.
properties:
endpoint_ids:
type: array
minItems: 1
maxItems: 250
items:
type: string
minLength: 1
maxLength: 256
description: Elastic Defend agent IDs (agent.id) of the endpoints to isolate.
comment:
type: string
maxLength: 30000
description: Optional comment explaining why these hosts are being isolated.
required:
- endpoint_ids
required:
- actionInput
additionalProperties: false
outputs:
- name: action_id
type: string
- name: status
type: string
- name: was_successful
type: boolean
- name: message
type: string
steps:
# POST queues the command; 200 means accepted, not that the host is isolated.
# kibana.request is sent verbatim, so the path is prefixed with the executing space.
# No retries: the API is non-idempotent — each POST creates a new action ID.
- name: dispatch
type: kibana.request
with:
method: POST
path: "/s/{{ workflow.spaceId }}/api/endpoint/action/isolate"
headers:
kbn-xsrf: "true"
elastic-api-version: "2023-10-31"
Content-Type: application/json
body:
endpoint_ids: "${{ inputs.actionInput.endpoint_ids }}"
comment: "{{ inputs.actionInput.comment }}"
agent_type: endpoint

# Poll until the endpoint reports completion
# 10s interval, 60 attempts, 10 minutes. Do-while: the first GET always runs.
- name: poll
type: while
condition: "${{ steps.poll_status.output.data.isCompleted != true }}"
max-iterations:
limit: 60
on-limit: fail
timeout: 10m
steps:
- name: poll_status
type: kibana.request
on-failure:
retry:
max-attempts: 3
delay: 5s
with:
method: GET
path: "/s/{{ workflow.spaceId }}/api/endpoint/action/{{ steps.dispatch.output.data.id }}"
headers:
kbn-xsrf: "true"
elastic-api-version: "2023-10-31"
- name: wait_if_pending
type: if
condition: "${{ steps.poll_status.output.data.isCompleted != true }}"
steps:
- name: poll_delay
type: wait
with:
duration: 10s

# `workflow.execute` declares its output as unknown, so the parent falls back
# to a heuristic read of the last step unless the child emits explicitly.
# Fail the workflow when the endpoint-side action did not succeed so the
# proposal gate's on-failure path fires instead of record_success.
- name: emit_result
type: if
condition: "${{ steps.poll_status.output.data.wasSuccessful == true }}"
steps:
- name: emit_success
type: workflow.output
status: completed
with:
action_id: "{{ steps.poll_status.output.data.id }}"
status: "{{ steps.poll_status.output.data.status }}"
was_successful: true
message: "{{ consts.actionLabel }} completed successfully. Action ID: {{ steps.poll_status.output.data.id }}"
else:
- name: emit_failure
type: workflow.fail
with:
message: "{{ consts.actionLabel }} failed. Action ID: {{ steps.poll_status.output.data.id }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import ACTION_KILL_PROCESS_YAML from './action_kill_process.yaml';
import type { ManagedWorkflowDefinition } from '../../../types';
import {
ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID,
ALERTZERO_RULE_WORKFLOW_MANAGEMENT,
} from '../constants';

export const ALERTZERO_ACTION_KILL_PROCESS_WORKFLOW_ID = 'system-alertzero-action-kill-process';

export const ALERTZERO_ACTION_KILL_PROCESS_WORKFLOW = {
billable: false,
id: ALERTZERO_ACTION_KILL_PROCESS_WORKFLOW_ID,
management: ALERTZERO_RULE_WORKFLOW_MANAGEMENT,
pluginId: ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID,
version: 1,
yaml: ACTION_KILL_PROCESS_YAML,
} as const satisfies ManagedWorkflowDefinition;
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
version: "1"
name: Kill process
description: >
AlertZero action: terminates a process on an Elastic Defend endpoint.
Executed by the investigation proposal gate once an analyst approves, so
the response action is attributed to the approver.
enabled: true
tags:
# Generic tag every action workflow carries, so the catalog can be
# discovered by tag rather than a hardcoded list.
- action
- alertzero
consts:
# Self-describing catalog metadata, read back by the catalog when a
# proposal referencing this workflow is created or rendered. Metadata has
# to live under `consts`: unknown top-level keys are stripped by the schema.
actionMetadata:
name: Kill process
description: Terminates a process on an Elastic Defend endpoint, identified by PID or entity_id.
category: contain
impact: high
reversible: false
approvalPolicy: always-gate
actionLabel: Kill process
triggers:
- type: manual
inputs:
properties:
actionInput:
type: object
description: >
Kill-process fields. Every action workflow takes a single `actionInput`
object so the generic gate workflow never needs to know an
action's parameter names.
properties:
endpoint_ids:
type: array
minItems: 1
maxItems: 250
items:
type: string
minLength: 1
maxLength: 256
description: Elastic Defend agent IDs (agent.id) of the endpoints.
parameters:
type: object
description: Process selector. Provide either pid or entity_id.
oneOf:
- required:
- pid
properties:
pid:
type: integer
minimum: 1
description: Process ID (PID) of the process to kill.
kill_descendants:
type: boolean
description: Also kill descendant processes. Elastic Defend only.
- required:
- entity_id
properties:
entity_id:
type: string
minLength: 1
maxLength: 256
description: Elastic Defend entity_id of the process to kill.
kill_descendants:
type: boolean
description: Also kill descendant processes. Elastic Defend only.
comment:
type: string
maxLength: 30000
description: Optional comment.
required:
- endpoint_ids
- parameters
required:
- actionInput
additionalProperties: false
outputs:
- name: action_id
type: string
- name: status
type: string
- name: was_successful
type: boolean
- name: message
type: string
steps:
# POST queues the command; 200 means accepted, not that the process is dead.
# kibana.request is sent verbatim, so the path is prefixed with the executing space.
# No retries: the API is non-idempotent — each POST creates a new action ID.
- name: dispatch
type: kibana.request
with:
method: POST
path: "/s/{{ workflow.spaceId }}/api/endpoint/action/kill_process"
headers:
kbn-xsrf: "true"
elastic-api-version: "2023-10-31"
Content-Type: application/json
body:
endpoint_ids: "${{ inputs.actionInput.endpoint_ids }}"
parameters: "${{ inputs.actionInput.parameters }}"
comment: "{{ inputs.actionInput.comment }}"
agent_type: endpoint

# Poll until the endpoint reports completion.
# 10s interval, 60 attempts, 10 minutes. Do-while: the first GET always runs.
- name: poll
type: while
condition: "${{ steps.poll_status.output.data.isCompleted != true }}"
max-iterations:
limit: 60
on-limit: fail
timeout: 10m
steps:
- name: poll_status
type: kibana.request
on-failure:
retry:
max-attempts: 3
delay: 5s
with:
method: GET
path: "/s/{{ workflow.spaceId }}/api/endpoint/action/{{ steps.dispatch.output.data.id }}"
headers:
kbn-xsrf: "true"
elastic-api-version: "2023-10-31"
- name: wait_if_pending
type: if
condition: "${{ steps.poll_status.output.data.isCompleted != true }}"
steps:
- name: poll_delay
type: wait
with:
duration: 10s

# `workflow.execute` declares its output as unknown, so the parent falls back
# to a heuristic read of the last step unless the child emits explicitly.
# Fail the workflow when the endpoint-side action did not succeed so the
# proposal gate's on-failure path fires instead of record_success.
- name: emit_result
type: if
condition: "${{ steps.poll_status.output.data.wasSuccessful == true }}"
steps:
- name: emit_success
type: workflow.output
status: completed
with:
action_id: "{{ steps.poll_status.output.data.id }}"
status: "{{ steps.poll_status.output.data.status }}"
was_successful: true
message: "{{ consts.actionLabel }} completed successfully. Action ID: {{ steps.poll_status.output.data.id }}"
else:
- name: emit_failure
type: workflow.fail
with:
message: "{{ consts.actionLabel }} failed. Action ID: {{ steps.poll_status.output.data.id }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import ACTION_SUSPEND_PROCESS_YAML from './action_suspend_process.yaml';
import type { ManagedWorkflowDefinition } from '../../../types';
import {
ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID,
ALERTZERO_RULE_WORKFLOW_MANAGEMENT,
} from '../constants';

export const ALERTZERO_ACTION_SUSPEND_PROCESS_WORKFLOW_ID =
'system-alertzero-action-suspend-process';

export const ALERTZERO_ACTION_SUSPEND_PROCESS_WORKFLOW = {
billable: false,
id: ALERTZERO_ACTION_SUSPEND_PROCESS_WORKFLOW_ID,
management: ALERTZERO_RULE_WORKFLOW_MANAGEMENT,
pluginId: ALERTZERO_MANAGED_WORKFLOW_PLUGIN_ID,
version: 1,
yaml: ACTION_SUSPEND_PROCESS_YAML,
} as const satisfies ManagedWorkflowDefinition;
Loading
Loading