mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Defend Workflows] Add support for Processes commands in Automated Response Actions (#161645)
This commit is contained in:
parent
9999f3674b
commit
0ca5593b41
38 changed files with 1595 additions and 297 deletions
|
@ -7219,20 +7219,60 @@ Object {
|
|||
"type": "string",
|
||||
},
|
||||
"params": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"const": "isolate",
|
||||
"type": "string",
|
||||
"anyOf": Array [
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"const": "isolate",
|
||||
"type": "string",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"enum": Array [
|
||||
"kill-process",
|
||||
"suspend-process",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"config": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"field": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"overwrite": Object {
|
||||
"default": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"field",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
"config",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
|
@ -7857,20 +7897,60 @@ Object {
|
|||
"type": "string",
|
||||
},
|
||||
"params": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"const": "isolate",
|
||||
"type": "string",
|
||||
"anyOf": Array [
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"const": "isolate",
|
||||
"type": "string",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"command": Object {
|
||||
"enum": Array [
|
||||
"kill-process",
|
||||
"suspend-process",
|
||||
],
|
||||
"type": "string",
|
||||
},
|
||||
"comment": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"config": Object {
|
||||
"additionalProperties": false,
|
||||
"properties": Object {
|
||||
"field": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"overwrite": Object {
|
||||
"default": true,
|
||||
"type": "boolean",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"field",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
"config",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
"command",
|
||||
],
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
"required": Array [
|
||||
|
|
|
@ -81,22 +81,38 @@ export const RuleResponseOsqueryAction = z.object({
|
|||
params: OsqueryParamsCamelCase,
|
||||
});
|
||||
|
||||
export type EndpointParams = z.infer<typeof EndpointParams>;
|
||||
export const EndpointParams = z.object({
|
||||
export type DefaultParams = z.infer<typeof DefaultParams>;
|
||||
export const DefaultParams = z.object({
|
||||
command: z.literal('isolate'),
|
||||
comment: z.string().optional(),
|
||||
});
|
||||
|
||||
export type ProcessesParams = z.infer<typeof ProcessesParams>;
|
||||
export const ProcessesParams = z.object({
|
||||
command: z.enum(['kill-process', 'suspend-process']),
|
||||
comment: z.string().optional(),
|
||||
config: z.object({
|
||||
/**
|
||||
* Field to use instead of process.pid
|
||||
*/
|
||||
field: z.string(),
|
||||
/**
|
||||
* Whether to overwrite field with process.pid
|
||||
*/
|
||||
overwrite: z.boolean().optional().default(true),
|
||||
}),
|
||||
});
|
||||
|
||||
export type EndpointResponseAction = z.infer<typeof EndpointResponseAction>;
|
||||
export const EndpointResponseAction = z.object({
|
||||
action_type_id: z.literal('.endpoint'),
|
||||
params: EndpointParams,
|
||||
params: z.union([DefaultParams, ProcessesParams]),
|
||||
});
|
||||
|
||||
export type RuleResponseEndpointAction = z.infer<typeof RuleResponseEndpointAction>;
|
||||
export const RuleResponseEndpointAction = z.object({
|
||||
actionTypeId: z.literal('.endpoint'),
|
||||
params: EndpointParams,
|
||||
params: z.union([DefaultParams, ProcessesParams]),
|
||||
});
|
||||
|
||||
export type ResponseAction = z.infer<typeof ResponseAction>;
|
||||
|
|
|
@ -2,7 +2,7 @@ openapi: 3.0.0
|
|||
info:
|
||||
title: Response Actions Schema
|
||||
version: 'not applicable'
|
||||
paths: {}
|
||||
paths: { }
|
||||
components:
|
||||
x-codegen-enabled: true
|
||||
schemas:
|
||||
|
@ -113,7 +113,7 @@ components:
|
|||
- actionTypeId
|
||||
- params
|
||||
|
||||
EndpointParams:
|
||||
DefaultParams:
|
||||
type: object
|
||||
properties:
|
||||
command:
|
||||
|
@ -125,6 +125,32 @@ components:
|
|||
required:
|
||||
- command
|
||||
|
||||
ProcessesParams:
|
||||
type: object
|
||||
properties:
|
||||
command:
|
||||
type: string
|
||||
enum:
|
||||
- kill-process
|
||||
- suspend-process
|
||||
comment:
|
||||
type: string
|
||||
config:
|
||||
required:
|
||||
- field
|
||||
type: object
|
||||
properties:
|
||||
field:
|
||||
type: string
|
||||
description: Field to use instead of process.pid
|
||||
overwrite:
|
||||
type: boolean
|
||||
description: Whether to overwrite field with process.pid
|
||||
default: true
|
||||
required:
|
||||
- command
|
||||
- config
|
||||
|
||||
EndpointResponseAction:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -133,7 +159,9 @@ components:
|
|||
enum:
|
||||
- .endpoint
|
||||
params:
|
||||
$ref: '#/components/schemas/EndpointParams'
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DefaultParams'
|
||||
- $ref: '#/components/schemas/ProcessesParams'
|
||||
required:
|
||||
- action_type_id
|
||||
- params
|
||||
|
@ -147,7 +175,9 @@ components:
|
|||
enum:
|
||||
- .endpoint
|
||||
params:
|
||||
$ref: '#/components/schemas/EndpointParams'
|
||||
oneOf:
|
||||
- $ref: '#/components/schemas/DefaultParams'
|
||||
- $ref: '#/components/schemas/ProcessesParams'
|
||||
required:
|
||||
- actionTypeId
|
||||
- params
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
import { arrayQueries, ecsMapping } from '@kbn/osquery-io-ts-types';
|
||||
import * as t from 'io-ts';
|
||||
import { ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS } from '../../../../endpoint/service/response_actions/constants';
|
||||
|
||||
// to enable using RESPONSE_ACTION_API_COMMANDS_NAMES as a type
|
||||
function keyObject<T extends readonly string[]>(arr: T): { [K in T[number]]: null } {
|
||||
|
@ -15,7 +14,9 @@ function keyObject<T extends readonly string[]>(arr: T): { [K in T[number]]: nul
|
|||
|
||||
export type EndpointParams = t.TypeOf<typeof EndpointParams>;
|
||||
export const EndpointParams = t.type({
|
||||
command: t.keyof(keyObject(ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS)),
|
||||
// TODO: TC- change these when we go GA with automated process actions
|
||||
command: t.keyof(keyObject(['isolate', 'kill-process', 'suspend-process'])),
|
||||
// command: t.keyof(keyObject(ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS)),
|
||||
comment: t.union([t.string, t.undefined]),
|
||||
});
|
||||
|
||||
|
|
|
@ -287,6 +287,28 @@ export class EndpointRuleAlertGenerator extends BaseDataGenerator {
|
|||
comment: 'test',
|
||||
},
|
||||
},
|
||||
{
|
||||
params: {
|
||||
command: 'suspend-process',
|
||||
comment: 'Suspend host',
|
||||
config: {
|
||||
field: 'entity_id',
|
||||
overwrite: false,
|
||||
},
|
||||
},
|
||||
action_type_id: '.endpoint',
|
||||
},
|
||||
{
|
||||
params: {
|
||||
command: 'kill-process',
|
||||
comment: 'Kill host',
|
||||
config: {
|
||||
field: '',
|
||||
overwrite: true,
|
||||
},
|
||||
},
|
||||
action_type_id: '.endpoint',
|
||||
},
|
||||
],
|
||||
rule_id: ELASTIC_SECURITY_RULE_ID,
|
||||
rule_name_override: 'message',
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { Client } from '@elastic/elasticsearch';
|
|||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '@kbn/fleet-plugin/common';
|
||||
import type { BulkRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { ResponseActionsApiCommandNames } from '../service/response_actions/constants';
|
||||
import { EndpointError } from '../errors';
|
||||
import { usageTracker } from './usage_tracker';
|
||||
import type {
|
||||
|
@ -117,6 +118,15 @@ interface BuildIEndpointAndFleetActionsBulkOperationsResponse
|
|||
operations: Required<BulkRequest>['operations'];
|
||||
}
|
||||
|
||||
const getAutomatedActionsSample = (): Array<{
|
||||
command: ResponseActionsApiCommandNames;
|
||||
config?: { overwrite: boolean };
|
||||
}> => [
|
||||
{ command: 'isolate' },
|
||||
{ command: 'suspend-process', config: { overwrite: true } },
|
||||
{ command: 'kill-process', config: { overwrite: true } },
|
||||
];
|
||||
|
||||
export const buildIEndpointAndFleetActionsBulkOperations = ({
|
||||
endpoints,
|
||||
count = 1,
|
||||
|
@ -138,13 +148,14 @@ export const buildIEndpointAndFleetActionsBulkOperations = ({
|
|||
for (const endpoint of endpoints) {
|
||||
const agentId = endpoint.elastic.agent.id;
|
||||
|
||||
const automatedActions = getAutomatedActionsSample();
|
||||
for (let i = 0; i < count; i++) {
|
||||
// start with endpoint action
|
||||
const logsEndpointAction: LogsEndpointAction = endpointActionGenerator.generate({
|
||||
EndpointActions: {
|
||||
data: {
|
||||
comment: 'data generator: this host is bad',
|
||||
...(alertIds ? { command: 'isolate' } : {}),
|
||||
...(alertIds ? automatedActions[i] : {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -31,7 +31,12 @@ export const RESPONSE_ACTION_API_COMMANDS_NAMES = [
|
|||
|
||||
export type ResponseActionsApiCommandNames = typeof RESPONSE_ACTION_API_COMMANDS_NAMES[number];
|
||||
|
||||
export const ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS = ['isolate'] as const;
|
||||
export const ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS: ResponseActionsApiCommandNames[] = [
|
||||
'isolate',
|
||||
// TODO: TC- Uncomment these when we go GA with automated process actions
|
||||
// 'kill-process',
|
||||
// 'suspend-process'
|
||||
];
|
||||
|
||||
export type EnabledAutomatedResponseActionsCommands =
|
||||
typeof ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS[number];
|
||||
|
|
|
@ -70,6 +70,11 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
*/
|
||||
responseActionUploadEnabled: true,
|
||||
|
||||
/*
|
||||
* Enables Automated Endpoint Process actions
|
||||
*/
|
||||
automatedProcessActionsEnabled: false,
|
||||
|
||||
/**
|
||||
* Enables the ability to send Response actions to SentinelOne
|
||||
*/
|
||||
|
|
|
@ -13,6 +13,7 @@ import { SuperSelectField } from '@kbn/es-ui-shared-plugin/static/forms/componen
|
|||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiLink } from '@elastic/eui';
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features';
|
||||
import { getRbacControl } from '../../../../common/endpoint/service/response_actions/utils';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { CHOOSE_FROM_THE_LIST, LEARN_MORE } from './translations';
|
||||
|
@ -44,14 +45,29 @@ const ActionTypeFieldComponent = ({
|
|||
},
|
||||
} = useKibana().services;
|
||||
|
||||
const automatedProcessActionsEnabled = useIsExperimentalFeatureEnabled(
|
||||
'automatedProcessActionsEnabled'
|
||||
);
|
||||
|
||||
const enabledActions = useMemo(
|
||||
() =>
|
||||
[
|
||||
...ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS,
|
||||
...(automatedProcessActionsEnabled ? ['kill-process', 'suspend-process'] : []),
|
||||
] as ['isolate', 'kill-process', 'suspend-process'],
|
||||
[automatedProcessActionsEnabled]
|
||||
);
|
||||
|
||||
const fieldOptions = useMemo(
|
||||
() =>
|
||||
ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS.map((name) => {
|
||||
enabledActions.map((name) => {
|
||||
const missingRbac = !getRbacControl({
|
||||
commandName: RESPONSE_ACTION_API_COMMAND_TO_CONSOLE_COMMAND_MAP[name],
|
||||
privileges: endpointPrivileges,
|
||||
});
|
||||
const commandAlreadyExists = map(data.responseActions, 'params.command').includes(name);
|
||||
const currentActions = map(data.responseActions, 'params.command');
|
||||
// we enable just one instance of each action
|
||||
const commandAlreadyExists = currentActions.includes(name);
|
||||
const isDisabled = commandAlreadyExists || missingRbac;
|
||||
|
||||
return {
|
||||
|
@ -62,7 +78,7 @@ const ActionTypeFieldComponent = ({
|
|||
'data-test-subj': `command-type-${name}`,
|
||||
};
|
||||
}),
|
||||
[data.responseActions, endpointPrivileges]
|
||||
[data.responseActions, enabledActions, endpointPrivileges]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -71,6 +71,31 @@ const EndpointActionCalloutComponent = ({ basePath, editDisabled }: EndpointCall
|
|||
</>
|
||||
);
|
||||
}
|
||||
if (currentCommand === 'kill-process' || currentCommand === 'suspend-process') {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActionsList.endpoint.cautionTitle"
|
||||
defaultMessage="Proceed with caution"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiText size={'xs'}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActionsList.endpoint.processesCautionDescription"
|
||||
defaultMessage="Only select this option if you’re certain that you want to terminate the process running on this host."
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useFormData } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { get } from 'lodash';
|
||||
import { OverwriteField } from './overwrite_process_field';
|
||||
import { FieldNameField } from './field_name';
|
||||
|
||||
interface AdditionalConfigFieldProps {
|
||||
basePath: string;
|
||||
disabled: boolean;
|
||||
readDefaultValueOnForm: boolean;
|
||||
}
|
||||
|
||||
export const ConfigFieldsComponent = ({
|
||||
basePath,
|
||||
disabled,
|
||||
readDefaultValueOnForm,
|
||||
}: AdditionalConfigFieldProps) => {
|
||||
const commandPath = `${basePath}.command`;
|
||||
const overWritePath = `${basePath}.config.overwrite`;
|
||||
const [data] = useFormData({ watch: [commandPath, overWritePath] });
|
||||
const currentCommand = get(data, commandPath);
|
||||
const currentOverwrite = get(data, overWritePath);
|
||||
|
||||
if (currentCommand === 'kill-process' || currentCommand === 'suspend-process') {
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<OverwriteField
|
||||
path={`${basePath}.config.overwrite`}
|
||||
disabled={disabled}
|
||||
readDefaultValueOnForm={readDefaultValueOnForm}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<FieldNameField
|
||||
path={`${basePath}.config.field`}
|
||||
disabled={disabled}
|
||||
readDefaultValueOnForm={readDefaultValueOnForm}
|
||||
isRequired={!currentOverwrite}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const ConfigFields = React.memo(ConfigFieldsComponent);
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ConfigFields } from './config_fields';
|
||||
import type { ArrayItem } from '../../../shared_imports';
|
||||
import { CommentField } from './comment_field';
|
||||
import { ActionTypeField } from './action_type_field';
|
||||
|
@ -29,6 +30,12 @@ export const EndpointResponseAction = React.memo((props: EndpointResponseActionP
|
|||
|
||||
<EndpointActionCallout basePath={paramsPath} editDisabled={props.editDisabled} />
|
||||
|
||||
<ConfigFields
|
||||
basePath={paramsPath}
|
||||
disabled={props.editDisabled}
|
||||
readDefaultValueOnForm={!props.item.isNew}
|
||||
/>
|
||||
|
||||
<CommentField
|
||||
basePath={paramsPath}
|
||||
disabled={props.editDisabled}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import {
|
||||
getFieldValidityAndErrorMessage,
|
||||
UseField,
|
||||
useFormData,
|
||||
useFormContext,
|
||||
} from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { get } from 'lodash';
|
||||
import { EuiComboBox, EuiSpacer, EuiFormRow } from '@elastic/eui';
|
||||
import ECSSchema from './v.8.10.0_process.json';
|
||||
|
||||
interface FieldNameFieldProps {
|
||||
path: string;
|
||||
disabled: boolean;
|
||||
readDefaultValueOnForm: boolean;
|
||||
isRequired: boolean;
|
||||
}
|
||||
|
||||
const ECSSchemaOptions = ECSSchema.map((ecs) => ({
|
||||
label: ecs.field,
|
||||
value: ecs,
|
||||
}));
|
||||
|
||||
const SINGLE_SELECTION = Object.freeze({ asPlainText: true });
|
||||
|
||||
const FIELD_LABEL: string = 'Custom field name';
|
||||
const FieldNameFieldComponent = ({
|
||||
path,
|
||||
disabled,
|
||||
readDefaultValueOnForm,
|
||||
isRequired,
|
||||
}: FieldNameFieldProps) => {
|
||||
const [data] = useFormData();
|
||||
const fieldValue = get(data, path);
|
||||
const context = useFormContext();
|
||||
|
||||
const currentFieldNameField = context.getFields()[path];
|
||||
|
||||
useEffect(() => {
|
||||
// hackish way to clear errors on this field - because we base this validation on the value of overwrite toggle
|
||||
if (currentFieldNameField && !isRequired) {
|
||||
currentFieldNameField?.clearErrors();
|
||||
}
|
||||
}, [currentFieldNameField, isRequired]);
|
||||
|
||||
const renderEntityIdNote = useMemo(() => {
|
||||
const contains = fieldValue?.includes('entity_id');
|
||||
if (contains) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActions.endpoint.fieldDescription"
|
||||
defaultMessage="Entity_id is an Elastic Defend agent specific field, if the alert does not come from Elastic Defend agent we will not be able to send the action."
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, [fieldValue]);
|
||||
|
||||
const CONFIG = useMemo(() => {
|
||||
return {
|
||||
label: FIELD_LABEL,
|
||||
helpText: renderEntityIdNote,
|
||||
validations: [
|
||||
{
|
||||
validator: ({ value }: { value: string }) => {
|
||||
if (isRequired && value === '') {
|
||||
return {
|
||||
code: 'ERR_FIELD_MISSING',
|
||||
path,
|
||||
message: i18n.translate(
|
||||
'xpack.securitySolution.responseActions.endpoint.validations.fieldNameIsRequiredErrorMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'{field} is a required field when process.pid toggle is turned off',
|
||||
values: { field: FIELD_LABEL },
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [isRequired, path, renderEntityIdNote]);
|
||||
|
||||
const optionsAsComboBoxOptions = useMemo(() => {
|
||||
return ECSSchemaOptions.map(({ label }) => ({
|
||||
label,
|
||||
value: label,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UseField<string> path={path} readDefaultValueOnForm={readDefaultValueOnForm} config={CONFIG}>
|
||||
{(field) => {
|
||||
const { value, setValue } = field;
|
||||
const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
|
||||
|
||||
const valueInList = !!optionsAsComboBoxOptions.find((option) => option.label === value);
|
||||
return (
|
||||
<EuiFormRow
|
||||
label={field.label}
|
||||
helpText={field.helpText}
|
||||
fullWidth
|
||||
error={errorMessage}
|
||||
isInvalid={isInvalid}
|
||||
>
|
||||
<EuiComboBox
|
||||
isInvalid={isInvalid}
|
||||
isDisabled={disabled || !isRequired}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
noSuggestions={false}
|
||||
options={optionsAsComboBoxOptions}
|
||||
fullWidth
|
||||
selectedOptions={value && valueInList ? [{ value, label: value }] : undefined}
|
||||
onChange={(newValue) => {
|
||||
if (newValue.length === 0) {
|
||||
// Don't allow clearing the type. One must always be selected
|
||||
return;
|
||||
}
|
||||
setValue(newValue[0].label);
|
||||
}}
|
||||
data-test-subj="config-custom-field-name"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}}
|
||||
</UseField>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const FieldNameField = React.memo(FieldNameFieldComponent);
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { UseField } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { ToggleField } from '@kbn/es-ui-shared-plugin/static/forms/components';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
interface OverwriteFieldProps {
|
||||
path: string;
|
||||
disabled: boolean;
|
||||
readDefaultValueOnForm: boolean;
|
||||
}
|
||||
|
||||
const OverwriteFieldComponent = ({
|
||||
path,
|
||||
disabled,
|
||||
readDefaultValueOnForm,
|
||||
}: OverwriteFieldProps) => {
|
||||
const CONFIG = useMemo(() => {
|
||||
return {
|
||||
defaultValue: true,
|
||||
label: i18n.translate('xpack.securitySolution.responseActions.endpoint.overwriteFieldLabel', {
|
||||
defaultMessage: 'Use process.pid as process identifier',
|
||||
}),
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<UseField
|
||||
component={ToggleField}
|
||||
euiFieldProps={{
|
||||
'data-test-subj': 'config-overwrite-toggle',
|
||||
}}
|
||||
path={path}
|
||||
readDefaultValueOnForm={readDefaultValueOnForm}
|
||||
config={CONFIG}
|
||||
isDisabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const OverwriteField = React.memo(OverwriteFieldComponent);
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { EuiText, EuiTitle, EuiSpacer, EuiToolTip } from '@elastic/eui';
|
||||
import { EuiText, EuiSpacer, EuiToolTip } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { EnabledAutomatedResponseActionsCommands } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
|
||||
|
@ -20,11 +20,11 @@ const EndpointActionTextComponent = ({ name, isDisabled }: EndpointActionTextPro
|
|||
|
||||
const content = (
|
||||
<>
|
||||
<EuiTitle size="xs">
|
||||
<EuiText>{title}</EuiText>
|
||||
</EuiTitle>
|
||||
<EuiText size="s">
|
||||
<b>{title}</b>
|
||||
</EuiText>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiText>{description}</EuiText>
|
||||
<EuiText size="xs">{description}</EuiText>
|
||||
</>
|
||||
);
|
||||
if (isDisabled) {
|
||||
|
@ -62,6 +62,48 @@ const useGetCommandText = (
|
|||
/>
|
||||
),
|
||||
};
|
||||
case 'kill-process':
|
||||
return {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActions.endpoint.killProcess"
|
||||
defaultMessage="Kill process"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActions.endpoint.killProcessDescription"
|
||||
defaultMessage="Kill/terminate a process"
|
||||
/>
|
||||
),
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActions.endpoint.killProcessTooltip"
|
||||
defaultMessage="Insufficient privileges to kill process. Contact your Kibana administrator if you think you should have this permission."
|
||||
/>
|
||||
),
|
||||
};
|
||||
case 'suspend-process':
|
||||
return {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActions.endpoint.suspendProcess"
|
||||
defaultMessage="Suspend process"
|
||||
/>
|
||||
),
|
||||
description: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActions.endpoint.suspendProcessDescription"
|
||||
defaultMessage="Temporarily suspend a process"
|
||||
/>
|
||||
),
|
||||
tooltip: (
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActions.endpoint.suspendProcessTooltip"
|
||||
defaultMessage="Insufficient privileges to supend process. Contact your Kibana administrator if you think you should have this permission."
|
||||
/>
|
||||
),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: '',
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
[
|
||||
{
|
||||
"field": "process.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.parent.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.parent.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.parent.session_leader.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.parent.session_leader.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.parent.session_leader.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.parent.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.entry_leader.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.group_leader.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.group_leader.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.group_leader.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.parent.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.parent.group_leader.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.parent.group_leader.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.parent.group_leader.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.parent.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.parent.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.parent.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.parent.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.parent.session_leader.entity_id",
|
||||
"description": "Unique identifier for the process."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.parent.session_leader.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.parent.session_leader.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.parent.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.pid",
|
||||
"description": "Process id."
|
||||
},
|
||||
{
|
||||
"field": "process.session_leader.vpid",
|
||||
"description": "Virtual process id."
|
||||
},
|
||||
{
|
||||
"field": "process.vpid",
|
||||
"description": "Virtual process id."
|
||||
}
|
||||
]
|
|
@ -53,14 +53,24 @@ export const ResponseActionsForm = ({
|
|||
const fieldErrors = reduce<string[], Array<{ type: string; errors: string[] }>>(
|
||||
map(items, 'path'),
|
||||
(acc, path) => {
|
||||
if (fields[`${path}.params`]?.errors?.length) {
|
||||
acc.push({
|
||||
type: upperFirst((fields[`${path}.actionTypeId`].value as string).substring(1)),
|
||||
errors: map(fields[`${path}.params`].errors, 'message'),
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
map(fields, (_, name) => {
|
||||
const paramsPath = `${path}.params`;
|
||||
|
||||
if (name.includes(paramsPath)) {
|
||||
if (fields[name]?.errors?.length) {
|
||||
const responseActionType = upperFirst(
|
||||
(fields[`${path}.actionTypeId`].value as string).substring(1)
|
||||
);
|
||||
acc.push({
|
||||
type: responseActionType,
|
||||
errors: map(fields[name].errors, 'message'),
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
|
|
|
@ -17,8 +17,8 @@ export const useSupportedResponseActionTypes = () => {
|
|||
>();
|
||||
|
||||
const isEndpointEnabled = useIsExperimentalFeatureEnabled('endpointResponseActionsEnabled');
|
||||
const { canIsolateHost } = useUserPrivileges().endpointPrivileges;
|
||||
|
||||
const { canIsolateHost, canKillProcess, canSuspendProcess } =
|
||||
useUserPrivileges().endpointPrivileges;
|
||||
const enabledFeatures = useMemo(
|
||||
() => ({
|
||||
endpoint: isEndpointEnabled,
|
||||
|
@ -28,9 +28,9 @@ export const useSupportedResponseActionTypes = () => {
|
|||
|
||||
const userHasPermissionsToExecute = useMemo(
|
||||
() => ({
|
||||
endpoint: canIsolateHost,
|
||||
endpoint: canIsolateHost || canKillProcess || canSuspendProcess,
|
||||
}),
|
||||
[canIsolateHost]
|
||||
[canIsolateHost, canKillProcess, canSuspendProcess]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { closeAllToasts } from '../../tasks/toasts';
|
|||
import { toggleRuleOffAndOn, visitRuleAlerts } from '../../tasks/isolate';
|
||||
import { cleanupRule, loadRule } from '../../tasks/api_fixtures';
|
||||
import { login } from '../../tasks/login';
|
||||
import { disableExpandableFlyoutAdvancedSettings, loadPage } from '../../tasks/common';
|
||||
import { loadPage } from '../../tasks/common';
|
||||
import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy';
|
||||
import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet';
|
||||
import { changeAlertsFilter } from '../../tasks/alerts';
|
||||
|
@ -23,14 +23,16 @@ import { enableAllPolicyProtections } from '../../tasks/endpoint_policy';
|
|||
describe(
|
||||
'Automated Response Actions',
|
||||
{
|
||||
tags: [
|
||||
'@ess',
|
||||
'@serverless',
|
||||
// Not supported in serverless!
|
||||
// The `disableExpandableFlyoutAdvancedSettings()` fails because the API
|
||||
// `internal/kibana/settings` is not accessible in serverless
|
||||
'@brokenInServerless',
|
||||
],
|
||||
tags: ['@ess', '@serverless'],
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'automatedProcessActionsEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
let indexedPolicy: IndexedFleetEndpointPolicyResponse;
|
||||
|
@ -67,16 +69,11 @@ describe(
|
|||
}
|
||||
});
|
||||
|
||||
const hostname = new URL(Cypress.env('FLEET_SERVER_URL')).port;
|
||||
const fleetHostname = `dev-fleet-server.${hostname}`;
|
||||
|
||||
beforeEach(() => {
|
||||
login();
|
||||
disableExpandableFlyoutAdvancedSettings();
|
||||
});
|
||||
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/169828
|
||||
describe.skip('From alerts', () => {
|
||||
describe('From alerts', () => {
|
||||
let ruleId: string;
|
||||
let ruleName: string;
|
||||
|
||||
|
@ -102,18 +99,14 @@ describe(
|
|||
visitRuleAlerts(ruleName);
|
||||
closeAllToasts();
|
||||
|
||||
changeAlertsFilter('event.category: "file"');
|
||||
cy.getByTestSubj('expand-event').first().click();
|
||||
cy.getByTestSubj('responseActionsViewTab').click();
|
||||
cy.getByTestSubj('response-actions-notification').should('not.have.text', '0');
|
||||
changeAlertsFilter('process.name: "sshd"');
|
||||
cy.getByTestSubj('expand-event').eq(0).click();
|
||||
cy.getByTestSubj('securitySolutionFlyoutNavigationExpandDetailButton').click();
|
||||
cy.getByTestSubj('securitySolutionFlyoutResponseTab').click();
|
||||
|
||||
cy.getByTestSubj(`response-results-${createdHost.hostname}-details-tray`)
|
||||
.should('contain', 'isolate completed successfully')
|
||||
.and('contain', createdHost.hostname);
|
||||
|
||||
cy.getByTestSubj(`response-results-${fleetHostname}-details-tray`)
|
||||
.should('contain', 'The host does not have Elastic Defend integration installed')
|
||||
.and('contain', 'dev-fleet-server');
|
||||
cy.contains(/isolate is pending|isolate completed successfully/g);
|
||||
cy.contains(/kill-process is pending|kill-process completed successfully/g);
|
||||
cy.contains('The action was called with a non-existing event field name: entity_id');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,22 +17,26 @@ import { cleanupRule, generateRandomStringName, loadRule } from '../../tasks/api
|
|||
import { ResponseActionTypesEnum } from '../../../../../common/api/detection_engine';
|
||||
import { login, ROLE } from '../../tasks/login';
|
||||
|
||||
export const RESPONSE_ACTIONS_ERRORS = 'response-actions-error';
|
||||
|
||||
describe(
|
||||
'Form',
|
||||
{
|
||||
tags: [
|
||||
'@ess',
|
||||
'@serverless',
|
||||
|
||||
// Not supported in serverless! Test suite uses custom roles
|
||||
'@brokenInServerless',
|
||||
],
|
||||
tags: ['@ess', '@serverless'],
|
||||
env: {
|
||||
ftrConfig: {
|
||||
kbnServerArgs: [
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify([
|
||||
'automatedProcessActionsEnabled',
|
||||
])}`,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
() => {
|
||||
// FLAKY: https://github.com/elastic/kibana/issues/169334
|
||||
describe.skip('User with no access can not create an endpoint response action', () => {
|
||||
describe('User with no access can not create an endpoint response action', () => {
|
||||
beforeEach(() => {
|
||||
login(ROLE.endpoint_response_actions_no_access);
|
||||
login(ROLE.rule_author);
|
||||
});
|
||||
|
||||
it('no endpoint response action option during rule creation', () => {
|
||||
|
@ -43,11 +47,12 @@ describe(
|
|||
|
||||
describe('User with access can create and save an endpoint response action', () => {
|
||||
const testedCommand = 'isolate';
|
||||
const secondTestedCommand = 'suspend-process';
|
||||
let ruleId: string;
|
||||
const [ruleName, ruleDescription] = generateRandomStringName(2);
|
||||
|
||||
beforeEach(() => {
|
||||
login(ROLE.endpoint_response_actions_access);
|
||||
login(ROLE.soc_manager);
|
||||
});
|
||||
afterEach(() => {
|
||||
if (ruleId) {
|
||||
|
@ -75,20 +80,53 @@ describe(
|
|||
});
|
||||
cy.getByTestSubj(`command-type-${testedCommand}`).should('not.have.attr', 'disabled');
|
||||
cy.getByTestSubj(`command-type-${testedCommand}`).click();
|
||||
|
||||
addEndpointResponseAction();
|
||||
focusAndOpenCommandDropdown(1);
|
||||
cy.getByTestSubj(`command-type-${secondTestedCommand}`).click();
|
||||
cy.getByTestSubj('config-overwrite-toggle').click();
|
||||
cy.getByTestSubj('config-custom-field-name').should('have.value', '');
|
||||
|
||||
cy.intercept('POST', '/api/detection_engine/rules', (request) => {
|
||||
const result = {
|
||||
const isolateResult = {
|
||||
action_type_id: ResponseActionTypesEnum['.endpoint'],
|
||||
params: {
|
||||
command: testedCommand,
|
||||
comment: 'example1',
|
||||
},
|
||||
};
|
||||
expect(request.body.response_actions[0]).to.deep.equal(result);
|
||||
const processResult = {
|
||||
action_type_id: ResponseActionTypesEnum['.endpoint'],
|
||||
params: {
|
||||
command: secondTestedCommand,
|
||||
comment: 'example1',
|
||||
config: {
|
||||
field: 'process.entity_id',
|
||||
overwrite: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(request.body.response_actions[0]).to.deep.equal(isolateResult);
|
||||
expect(request.body.response_actions[1]).to.deep.equal(processResult);
|
||||
request.continue((response) => {
|
||||
ruleId = response.body.id;
|
||||
response.send(response.body);
|
||||
});
|
||||
});
|
||||
cy.getByTestSubj(RESPONSE_ACTIONS_ERRORS).should('not.exist');
|
||||
|
||||
cy.getByTestSubj('create-enabled-false').click();
|
||||
|
||||
cy.getByTestSubj(RESPONSE_ACTIONS_ERRORS).within(() => {
|
||||
cy.contains(
|
||||
'Custom field name is a required field when process.pid toggle is turned off'
|
||||
);
|
||||
});
|
||||
|
||||
cy.getByTestSubj(`response-actions-list-item-1`).within(() => {
|
||||
cy.getByTestSubj('config-custom-field-name').type('process.entity_id{downArrow}{enter}');
|
||||
});
|
||||
|
||||
cy.getByTestSubj('create-enabled-false').click();
|
||||
cy.contains(`${ruleName} was created`);
|
||||
});
|
||||
|
@ -97,11 +135,11 @@ describe(
|
|||
describe('User with access can edit and delete an endpoint response action', () => {
|
||||
let ruleId: string;
|
||||
let ruleName: string;
|
||||
const testedCommand = 'isolate';
|
||||
const newDescription = 'Example isolate host description';
|
||||
const newDescription = 'Example suspend process description';
|
||||
const secondTestedCommand = 'suspend-process';
|
||||
|
||||
beforeEach(() => {
|
||||
login(ROLE.endpoint_response_actions_access);
|
||||
login(ROLE.soc_manager);
|
||||
loadRule().then((res) => {
|
||||
ruleId = res.id;
|
||||
ruleName = res.name;
|
||||
|
@ -115,24 +153,30 @@ describe(
|
|||
visitRuleActions(ruleId);
|
||||
cy.getByTestSubj('edit-rule-actions-tab').click();
|
||||
|
||||
cy.getByTestSubj(`response-actions-list-item-0`).within(() => {
|
||||
cy.getByTestSubj('input').should('have.value', 'Isolate host');
|
||||
cy.getByTestSubj('input').should('have.value', 'Isolate host');
|
||||
cy.getByTestSubj(`response-actions-list-item-1`).within(() => {
|
||||
cy.getByTestSubj('input').should('have.value', 'Suspend host');
|
||||
cy.getByTestSubj('input').type(`{selectall}{backspace}${newDescription}`);
|
||||
cy.getByTestSubj('commandTypeField').click();
|
||||
cy.getByTestSubj('config-overwrite-toggle').click();
|
||||
cy.getByTestSubj('config-custom-field-name').should('have.value', '');
|
||||
cy.getByTestSubj('config-overwrite-toggle').click();
|
||||
cy.getByTestSubj('config-custom-field-name').type('process.entity_id{downArrow}{enter}');
|
||||
});
|
||||
validateAvailableCommands();
|
||||
|
||||
cy.intercept('PUT', '/api/detection_engine/rules').as('updateResponseAction');
|
||||
cy.getByTestSubj('ruleEditSubmitButton').click();
|
||||
cy.wait('@updateResponseAction').should(({ request }) => {
|
||||
const query = {
|
||||
action_type_id: ResponseActionTypesEnum['.endpoint'],
|
||||
params: {
|
||||
command: testedCommand,
|
||||
command: secondTestedCommand,
|
||||
comment: newDescription,
|
||||
config: {
|
||||
field: 'process.entity_id',
|
||||
overwrite: false,
|
||||
},
|
||||
},
|
||||
action_type_id: ResponseActionTypesEnum['.endpoint'],
|
||||
};
|
||||
expect(request.body.response_actions[0]).to.deep.equal(query);
|
||||
expect(request.body.response_actions[1]).to.deep.equal(query);
|
||||
});
|
||||
cy.contains(`${ruleName} was saved`).should('exist');
|
||||
});
|
||||
|
@ -147,7 +191,7 @@ describe(
|
|||
cy.intercept('PUT', '/api/detection_engine/rules').as('deleteResponseAction');
|
||||
cy.getByTestSubj('ruleEditSubmitButton').click();
|
||||
cy.wait('@deleteResponseAction').should(({ request }) => {
|
||||
expect(request.body.response_actions).to.be.equal(undefined);
|
||||
expect(request.body.response_actions.length).to.be.equal(2);
|
||||
});
|
||||
cy.contains(`${ruleName} was saved`).should('exist');
|
||||
});
|
||||
|
@ -157,9 +201,10 @@ describe(
|
|||
const [ruleName, ruleDescription] = generateRandomStringName(2);
|
||||
|
||||
beforeEach(() => {
|
||||
login(ROLE.endpoint_response_actions_no_access);
|
||||
login(ROLE.rule_author);
|
||||
});
|
||||
|
||||
// let currentUrl
|
||||
it('response actions are disabled', () => {
|
||||
fillUpNewRule(ruleName, ruleDescription);
|
||||
cy.getByTestSubj('response-actions-wrapper').within(() => {
|
||||
|
@ -174,7 +219,7 @@ describe(
|
|||
let ruleId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
login(ROLE.endpoint_response_actions_no_access);
|
||||
login(ROLE.rule_author);
|
||||
loadRule().then((res) => {
|
||||
ruleId = res.id;
|
||||
});
|
||||
|
@ -200,8 +245,8 @@ describe(
|
|||
// Try removing action
|
||||
cy.getByTestSubj('remove-response-action').click({ force: true });
|
||||
});
|
||||
cy.getByTestSubj(`response-actions-list-item-0`).should('exist');
|
||||
tryAddingDisabledResponseAction(1);
|
||||
cy.getByTestSubj(`response-actions-list-item-2`).should('exist');
|
||||
tryAddingDisabledResponseAction(3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { navigateToAlertsList } from '../../screens/alerts';
|
||||
import { disableExpandableFlyoutAdvancedSettings } from '../../tasks/common';
|
||||
import { closeAllToasts } from '../../tasks/toasts';
|
||||
import { fillUpNewRule } from '../../tasks/response_actions';
|
||||
import { login, ROLE } from '../../tasks/login';
|
||||
|
@ -39,7 +38,6 @@ describe('No License', { tags: '@ess', env: { ftrConfig: { license: 'basic' } }
|
|||
const [endpointAgentId, endpointHostname] = generateRandomStringName(2);
|
||||
beforeEach(() => {
|
||||
login();
|
||||
disableExpandableFlyoutAdvancedSettings();
|
||||
indexEndpointRuleAlerts({
|
||||
endpointAgentId,
|
||||
endpointHostname,
|
||||
|
@ -73,8 +71,8 @@ describe('No License', { tags: '@ess', env: { ftrConfig: { license: 'basic' } }
|
|||
navigateToAlertsList(`query=(language:kuery,query:'agent.id: "${endpointAgentId}" ')`);
|
||||
closeAllToasts();
|
||||
cy.getByTestSubj('expand-event').first().click();
|
||||
cy.getByTestSubj('response-actions-notification').should('not.have.text', '0');
|
||||
cy.getByTestSubj('responseActionsViewTab').click();
|
||||
cy.getByTestSubj('securitySolutionFlyoutNavigationExpandDetailButton').click();
|
||||
cy.getByTestSubj('securitySolutionFlyoutResponseTab').click();
|
||||
cy.contains('Permission denied');
|
||||
cy.contains(
|
||||
'To access these results, ask your administrator for Elastic Defend Kibana privileges.'
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { disableExpandableFlyoutAdvancedSettings } from '../../tasks/common';
|
||||
import { navigateToAlertsList } from '../../screens/alerts';
|
||||
import { generateRandomStringName } from '../../tasks/utils';
|
||||
import { APP_ALERTS_PATH } from '../../../../../common/constants';
|
||||
import { closeAllToasts } from '../../tasks/toasts';
|
||||
import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts';
|
||||
import type { ReturnTypeFromChainable } from '../../types';
|
||||
|
@ -50,57 +49,36 @@ describe('Results', { tags: ['@ess', '@serverless'] }, () => {
|
|||
}
|
||||
});
|
||||
|
||||
describe(
|
||||
'see results when has RBAC',
|
||||
{
|
||||
// Not supported in serverless!
|
||||
// The `disableExpandableFlyoutAdvancedSettings()` fails because the API
|
||||
// `internal/kibana/settings` is not accessible in serverless
|
||||
tags: ['@brokenInServerless'],
|
||||
},
|
||||
() => {
|
||||
before(() => {
|
||||
login(ROLE.endpoint_response_actions_access);
|
||||
disableExpandableFlyoutAdvancedSettings();
|
||||
});
|
||||
describe('see results when has RBAC', () => {
|
||||
before(() => {
|
||||
login(ROLE.soc_manager);
|
||||
});
|
||||
|
||||
it('see endpoint action', () => {
|
||||
cy.visit(APP_ALERTS_PATH);
|
||||
closeAllToasts();
|
||||
cy.getByTestSubj('expand-event').first().click();
|
||||
cy.getByTestSubj('response-actions-notification').should('not.have.text', '0');
|
||||
cy.getByTestSubj('responseActionsViewTab').click();
|
||||
cy.getByTestSubj('endpoint-results-comment');
|
||||
cy.contains(/isolate is pending|isolate completed successfully/g);
|
||||
});
|
||||
}
|
||||
);
|
||||
describe(
|
||||
'do not see results results when does not have RBAC',
|
||||
{
|
||||
// Not supported in serverless!
|
||||
// The `disableExpandableFlyoutAdvancedSettings()` fails because the API
|
||||
// `internal/kibana/settings` is not accessible in serverless
|
||||
tags: ['@brokenInServerless'],
|
||||
},
|
||||
() => {
|
||||
before(() => {
|
||||
login(ROLE.endpoint_response_actions_no_access);
|
||||
disableExpandableFlyoutAdvancedSettings();
|
||||
});
|
||||
it('see endpoint action', () => {
|
||||
navigateToAlertsList(`query=(language:kuery,query:'_id: ${alertData?.alerts[0]._id}')`);
|
||||
closeAllToasts();
|
||||
cy.getByTestSubj('expand-event').first().click();
|
||||
cy.getByTestSubj('securitySolutionFlyoutNavigationExpandDetailButton').click();
|
||||
cy.getByTestSubj('securitySolutionFlyoutResponseTab').click();
|
||||
cy.contains(/isolate is pending|isolate completed successfully/g);
|
||||
});
|
||||
});
|
||||
describe('do not see results results when does not have RBAC', () => {
|
||||
before(() => {
|
||||
login(ROLE.t1_analyst);
|
||||
});
|
||||
|
||||
it('show the permission denied callout', () => {
|
||||
cy.visit(APP_ALERTS_PATH);
|
||||
closeAllToasts();
|
||||
it('show the permission denied callout', () => {
|
||||
navigateToAlertsList(`query=(language:kuery,query:'_id: ${alertData?.alerts[0]._id}')`);
|
||||
closeAllToasts();
|
||||
|
||||
cy.getByTestSubj('expand-event').first().click();
|
||||
cy.getByTestSubj('response-actions-notification').should('not.have.text', '0');
|
||||
cy.getByTestSubj('responseActionsViewTab').click();
|
||||
cy.contains('Permission denied');
|
||||
cy.contains(
|
||||
'To access these results, ask your administrator for Elastic Defend Kibana privileges.'
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
cy.getByTestSubj('expand-event').first().click();
|
||||
cy.getByTestSubj('securitySolutionFlyoutNavigationExpandDetailButton').click();
|
||||
cy.getByTestSubj('securitySolutionFlyoutResponseTab').click();
|
||||
cy.contains('Permission denied');
|
||||
cy.contains(
|
||||
'To access these results, ask your administrator for Elastic Defend Kibana privileges.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -69,6 +69,28 @@ export const loadRule = (body = {}, includeResponseActions = true) =>
|
|||
params: { command: 'isolate', comment: 'Isolate host' },
|
||||
action_type_id: '.endpoint',
|
||||
},
|
||||
{
|
||||
params: {
|
||||
command: 'suspend-process',
|
||||
comment: 'Suspend host',
|
||||
config: {
|
||||
field: 'entity_id',
|
||||
overwrite: false,
|
||||
},
|
||||
},
|
||||
action_type_id: '.endpoint',
|
||||
},
|
||||
{
|
||||
params: {
|
||||
command: 'kill-process',
|
||||
comment: 'Kill host',
|
||||
config: {
|
||||
field: '',
|
||||
overwrite: true,
|
||||
},
|
||||
},
|
||||
action_type_id: '.endpoint',
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
|
|
|
@ -25,11 +25,19 @@ import type { ResponseActionsApiCommandNames } from '../../../../common/endpoint
|
|||
import { ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS } from '../../../../common/endpoint/service/response_actions/constants';
|
||||
|
||||
export const validateAvailableCommands = () => {
|
||||
cy.get('[data-test-subj^="command-type"]').should(
|
||||
'have.length',
|
||||
ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS.length
|
||||
);
|
||||
ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS.forEach((command) => {
|
||||
// TODO: TC- use ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS when we go GA with automated process actions
|
||||
const config = Cypress.config();
|
||||
const automatedActionsPAttern = /automatedProcessActionsEnabled/;
|
||||
const automatedProcessActionsEnabled =
|
||||
config.env.ftrConfig.kbnServerArgs[0].match(automatedActionsPAttern);
|
||||
|
||||
const enabledActions = [
|
||||
...ENABLED_AUTOMATED_RESPONSE_ACTION_COMMANDS,
|
||||
...(automatedProcessActionsEnabled ? ['kill-process', 'suspend-process'] : []),
|
||||
];
|
||||
|
||||
cy.get('[data-test-subj^="command-type"]').should('have.length', enabledActions.length);
|
||||
enabledActions.forEach((command) => {
|
||||
cy.getByTestSubj(`command-type-${command}`);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -151,7 +151,7 @@ const combineResponse = (
|
|||
|
||||
return {
|
||||
id: action.EndpointActions.action_id,
|
||||
agents: action.agent.id as string[],
|
||||
agents: Array.isArray(action.agent.id) ? action.agent.id : [action.agent.id],
|
||||
agentType: 'endpoint',
|
||||
parameters,
|
||||
...(alertId?.length ? { alertIds: alertId } : {}),
|
||||
|
@ -169,7 +169,7 @@ const combineResponse = (
|
|||
completedAt: responseData?.completedAt,
|
||||
isCompleted: !!responseData?.isCompleted,
|
||||
isExpired: !!responseData?.isExpired,
|
||||
wasSuccessful: !!responseData?.isCompleted,
|
||||
wasSuccessful: responseData.status === 'successful',
|
||||
status: responseData.status,
|
||||
agentState: {},
|
||||
errors: action.error ? [action.error.message as string] : undefined,
|
||||
|
|
|
@ -7,17 +7,19 @@
|
|||
|
||||
import type { LicenseType } from '@kbn/licensing-plugin/common/types';
|
||||
import type { EcsError } from '@kbn/ecs';
|
||||
import { validateAgents, validateEndpointLicense } from './validate';
|
||||
import { validateAgents, validateAlertError, validateEndpointLicense } from './validate';
|
||||
import type { LicenseService } from '../../../../../common/license/license';
|
||||
|
||||
export const addErrorsToActionIfAny = ({
|
||||
agents,
|
||||
licenseService,
|
||||
minimumLicenseRequired = 'basic',
|
||||
error,
|
||||
}: {
|
||||
agents: string[];
|
||||
licenseService: LicenseService;
|
||||
minimumLicenseRequired: LicenseType;
|
||||
error?: string;
|
||||
}):
|
||||
| {
|
||||
error: {
|
||||
|
@ -28,7 +30,8 @@ export const addErrorsToActionIfAny = ({
|
|||
| undefined => {
|
||||
const licenseError = validateEndpointLicense(licenseService, minimumLicenseRequired);
|
||||
const agentsError = validateAgents(agents);
|
||||
const alertActionError = licenseError || agentsError;
|
||||
const actionError = validateAlertError(error);
|
||||
const alertActionError = licenseError || agentsError || actionError;
|
||||
|
||||
if (alertActionError) {
|
||||
return {
|
||||
|
|
|
@ -9,7 +9,10 @@ import { i18n } from '@kbn/i18n';
|
|||
import type { LicenseType } from '@kbn/licensing-plugin/server';
|
||||
import type { LicenseService } from '../../../../../common/license';
|
||||
|
||||
export const validateEndpointLicense = (license: LicenseService, licenseType: LicenseType) => {
|
||||
export const validateEndpointLicense = (
|
||||
license: LicenseService,
|
||||
licenseType: LicenseType
|
||||
): string | undefined => {
|
||||
const hasEnterpriseLicense = license.isAtLeast(licenseType);
|
||||
|
||||
if (!hasEnterpriseLicense) {
|
||||
|
@ -17,12 +20,18 @@ export const validateEndpointLicense = (license: LicenseService, licenseType: Li
|
|||
}
|
||||
};
|
||||
|
||||
export const validateAgents = (agents: string[]) => {
|
||||
export const validateAgents = (agents: string[]): string | undefined => {
|
||||
if (!agents.length) {
|
||||
return HOST_NOT_ENROLLED;
|
||||
}
|
||||
};
|
||||
|
||||
export const validateAlertError = (field?: string): string | undefined => {
|
||||
if (field) {
|
||||
return FIELD_NOT_EXIST(field);
|
||||
}
|
||||
};
|
||||
|
||||
export const LICENSE_TOO_LOW = i18n.translate(
|
||||
'xpack.securitySolution.responseActionsList.error.licenseTooLow',
|
||||
{
|
||||
|
@ -36,3 +45,9 @@ export const HOST_NOT_ENROLLED = i18n.translate(
|
|||
defaultMessage: 'The host does not have Elastic Defend integration installed',
|
||||
}
|
||||
);
|
||||
|
||||
export const FIELD_NOT_EXIST = (field: string): string =>
|
||||
i18n.translate('xpack.securitySolution.responseActionsList.error.nonExistingFieldName', {
|
||||
defaultMessage: 'The action was called with a non-existing event field name: {field}',
|
||||
values: { field },
|
||||
});
|
||||
|
|
|
@ -72,6 +72,7 @@ export const writeActionToIndices = async ({
|
|||
agents,
|
||||
licenseService,
|
||||
minimumLicenseRequired,
|
||||
error: payload.error,
|
||||
}),
|
||||
...addRuleInfoToAction(payload),
|
||||
};
|
||||
|
|
|
@ -181,11 +181,12 @@ describe('Create rule route', () => {
|
|||
// @ts-expect-error We're writting to a read only property just for the purpose of the test
|
||||
clients.config.experimentalFeatures.endpointResponseActionsEnabled = true;
|
||||
});
|
||||
const getResponseAction = (command: string = 'isolate') => ({
|
||||
const getResponseAction = (command: string = 'isolate', config?: object) => ({
|
||||
action_type_id: '.endpoint',
|
||||
params: {
|
||||
command,
|
||||
comment: '',
|
||||
...(config ? { config } : {}),
|
||||
},
|
||||
});
|
||||
const defaultAction = getResponseAction();
|
||||
|
@ -224,8 +225,22 @@ describe('Create rule route', () => {
|
|||
'User is not authorized to change isolate response actions'
|
||||
);
|
||||
});
|
||||
test('pass when provided with process action', async () => {
|
||||
const processAction = getResponseAction('kill-process', { overwrite: true, field: '' });
|
||||
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
body: {
|
||||
...getCreateRulesSchemaMock(),
|
||||
response_actions: [processAction],
|
||||
},
|
||||
});
|
||||
const result = await server.validate(request);
|
||||
expect(result.badRequest).not.toHaveBeenCalled();
|
||||
});
|
||||
test('fails when provided with an unsupported command', async () => {
|
||||
const wrongAction = getResponseAction('processes');
|
||||
const wrongAction = getResponseAction('execute');
|
||||
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
|
@ -237,7 +252,23 @@ describe('Create rule route', () => {
|
|||
});
|
||||
const result = await server.validate(request);
|
||||
expect(result.badRequest).toHaveBeenCalledWith(
|
||||
'response_actions.0.action_type_id: Invalid literal value, expected ".osquery", response_actions.0.params.command: Invalid literal value, expected "isolate"'
|
||||
`response_actions.0.action_type_id: Invalid literal value, expected \".osquery\", response_actions.0.params.command: Invalid literal value, expected \"isolate\", response_actions.0.params.command: Invalid enum value. Expected 'kill-process' | 'suspend-process', received 'execute', response_actions.0.params.config: Required`
|
||||
);
|
||||
});
|
||||
test('fails when provided with payload missing data', async () => {
|
||||
const wrongAction = getResponseAction('kill-process', { overwrite: true });
|
||||
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
body: {
|
||||
...getCreateRulesSchemaMock(),
|
||||
response_actions: [wrongAction],
|
||||
},
|
||||
});
|
||||
const result = await server.validate(request);
|
||||
expect(result.badRequest).toHaveBeenCalledWith(
|
||||
`response_actions.0.action_type_id: Invalid literal value, expected \".osquery\", response_actions.0.params.command: Invalid literal value, expected \"isolate\", response_actions.0.params.config.field: Required`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ import {
|
|||
getUpdateRulesSchemaMock,
|
||||
} from '../../../../../../../common/api/detection_engine/model/rule_schema/mocks';
|
||||
import { getQueryRuleParams } from '../../../../rule_schema/mocks';
|
||||
import { ResponseActionTypesEnum } from '../../../../../../../common/api/detection_engine/model/rule_response_actions';
|
||||
import { ResponseActionTypesEnum } from '../../../../../../../common/api/detection_engine';
|
||||
|
||||
jest.mock('../../../../../machine_learning/authz');
|
||||
|
||||
|
@ -189,11 +189,12 @@ describe('Update rule route', () => {
|
|||
// @ts-expect-error We're writting to a read only property just for the purpose of the test
|
||||
clients.config.experimentalFeatures.endpointResponseActionsEnabled = true;
|
||||
});
|
||||
const getResponseAction = (command: string = 'isolate') => ({
|
||||
const getResponseAction = (command: string = 'isolate', config?: object) => ({
|
||||
action_type_id: '.endpoint',
|
||||
params: {
|
||||
command,
|
||||
comment: '',
|
||||
...(config ? { config } : {}),
|
||||
},
|
||||
});
|
||||
const defaultAction = getResponseAction();
|
||||
|
@ -249,6 +250,7 @@ describe('Update rule route', () => {
|
|||
params: {
|
||||
command: 'isolate',
|
||||
comment: '',
|
||||
config: undefined,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -272,7 +274,7 @@ describe('Update rule route', () => {
|
|||
);
|
||||
});
|
||||
test('fails when provided with an unsupported command', async () => {
|
||||
const wrongAction = getResponseAction('processes');
|
||||
const wrongAction = getResponseAction('execute');
|
||||
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
|
@ -284,7 +286,23 @@ describe('Update rule route', () => {
|
|||
});
|
||||
const result = await server.validate(request);
|
||||
expect(result.badRequest).toHaveBeenCalledWith(
|
||||
`response_actions.0.action_type_id: Invalid literal value, expected \".osquery\", response_actions.0.params.command: Invalid literal value, expected \"isolate\"`
|
||||
`response_actions.0.action_type_id: Invalid literal value, expected \".osquery\", response_actions.0.params.command: Invalid literal value, expected \"isolate\", response_actions.0.params.command: Invalid enum value. Expected 'kill-process' | 'suspend-process', received 'execute', response_actions.0.params.config: Required`
|
||||
);
|
||||
});
|
||||
test('fails when provided with payload missing data', async () => {
|
||||
const wrongAction = getResponseAction('kill-process', { overwrite: true });
|
||||
|
||||
const request = requestMock.create({
|
||||
method: 'post',
|
||||
path: DETECTION_ENGINE_RULES_URL,
|
||||
body: {
|
||||
...getCreateRulesSchemaMock(),
|
||||
response_actions: [wrongAction],
|
||||
},
|
||||
});
|
||||
const result = await server.validate(request);
|
||||
expect(result.badRequest).toHaveBeenCalledWith(
|
||||
`response_actions.0.action_type_id: Invalid literal value, expected \".osquery\", response_actions.0.params.command: Invalid literal value, expected \"isolate\", response_actions.0.params.config.field: Required`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
DefaultParams,
|
||||
ProcessesParams,
|
||||
RuleResponseEndpointAction,
|
||||
} from '../../../../common/api/detection_engine';
|
||||
|
||||
export const isIsolateAction = (
|
||||
params: RuleResponseEndpointAction['params']
|
||||
): params is DefaultParams => {
|
||||
return params.command === 'isolate';
|
||||
};
|
||||
|
||||
export const isProcessesAction = (
|
||||
params: RuleResponseEndpointAction['params']
|
||||
): params is ProcessesParams => {
|
||||
return params.command === 'kill-process' || params.command === 'suspend-process';
|
||||
};
|
|
@ -5,49 +5,81 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { each, map, uniq } from 'lodash';
|
||||
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
import type { ResponseActionAlerts } from './types';
|
||||
import { each } from 'lodash';
|
||||
|
||||
import type { ExperimentalFeatures } from '../../../../common';
|
||||
import { isIsolateAction, isProcessesAction } from './endpoint_params_type_guards';
|
||||
import type { RuleResponseEndpointAction } from '../../../../common/api/detection_engine';
|
||||
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
import type { RuleResponseEndpointAction } from '../../../../common/api/detection_engine/model/rule_response_actions';
|
||||
import { getProcessAlerts, getIsolateAlerts, getErrorProcessAlerts } from './utils';
|
||||
|
||||
import type { ResponseActionAlerts, AlertsAction } from './types';
|
||||
|
||||
export const endpointResponseAction = (
|
||||
responseAction: RuleResponseEndpointAction,
|
||||
endpointAppContextService: EndpointAppContextService,
|
||||
{ alerts }: ResponseActionAlerts
|
||||
{ alerts }: ResponseActionAlerts,
|
||||
experimentalFeatures: ExperimentalFeatures
|
||||
) => {
|
||||
const { comment, command } = responseAction.params;
|
||||
|
||||
const commonData = {
|
||||
comment,
|
||||
command,
|
||||
rule_id: alerts[0][ALERT_RULE_UUID],
|
||||
rule_name: alerts[0][ALERT_RULE_NAME],
|
||||
agent_type: 'endpoint' as const,
|
||||
};
|
||||
const agentIds = uniq(map(alerts, 'agent.id'));
|
||||
const alertIds = map(alerts, '_id');
|
||||
|
||||
const hosts = alerts.reduce<Record<string, string>>((acc, alert) => {
|
||||
if (alert.agent?.name && !acc[alert.agent.id]) {
|
||||
acc[alert.agent.id] = alert.agent.name;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return Promise.all(
|
||||
each(agentIds, async (agent) =>
|
||||
endpointAppContextService.getActionCreateService().createActionFromAlert(
|
||||
if (isIsolateAction(responseAction.params)) {
|
||||
const alertsPerAgent = getIsolateAlerts(alerts);
|
||||
each(alertsPerAgent, (actionPayload) => {
|
||||
return endpointAppContextService.getActionCreateService().createActionFromAlert(
|
||||
{
|
||||
hosts: {
|
||||
[agent]: {
|
||||
name: hosts[agent],
|
||||
},
|
||||
},
|
||||
agent_type: 'endpoint',
|
||||
endpoint_ids: [agent],
|
||||
alert_ids: alertIds,
|
||||
...actionPayload,
|
||||
...commonData,
|
||||
},
|
||||
[agent]
|
||||
)
|
||||
)
|
||||
);
|
||||
actionPayload.endpoint_ids
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const automatedProcessActionsEnabled = experimentalFeatures?.automatedProcessActionsEnabled;
|
||||
|
||||
if (automatedProcessActionsEnabled) {
|
||||
const createProcessActionFromAlerts = (
|
||||
actionAlerts: Record<string, Record<string, AlertsAction>>
|
||||
) => {
|
||||
const createAction = async (alert: AlertsAction) => {
|
||||
const { hosts, parameters, error } = alert;
|
||||
|
||||
const actionData = {
|
||||
hosts,
|
||||
endpoint_ids: alert.endpoint_ids,
|
||||
alert_ids: alert.alert_ids,
|
||||
error,
|
||||
parameters,
|
||||
...commonData,
|
||||
};
|
||||
|
||||
return endpointAppContextService
|
||||
.getActionCreateService()
|
||||
.createActionFromAlert(actionData, alert.endpoint_ids);
|
||||
};
|
||||
return each(actionAlerts, (actionPerAgent) => {
|
||||
return each(actionPerAgent, createAction);
|
||||
});
|
||||
};
|
||||
|
||||
if (isProcessesAction(responseAction.params)) {
|
||||
const foundFields = getProcessAlerts(alerts, responseAction.params.config);
|
||||
const notFoundField = getErrorProcessAlerts(alerts, responseAction.params.config);
|
||||
|
||||
const processActions = createProcessActionFromAlerts(foundFields);
|
||||
const processActionsWithError = createProcessActionFromAlerts(notFoundField);
|
||||
|
||||
return Promise.all([processActions, processActionsWithError]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,105 +6,213 @@
|
|||
*/
|
||||
|
||||
import { getScheduleNotificationResponseActionsService } from './schedule_notification_response_actions';
|
||||
import type { RuleResponseAction } from '../../../../common/api/detection_engine/model/rule_response_actions';
|
||||
import { ResponseActionTypesEnum } from '../../../../common/api/detection_engine/model/rule_response_actions';
|
||||
import type { RuleResponseAction } from '../../../../common/api/detection_engine';
|
||||
import { ResponseActionTypesEnum } from '../../../../common/api/detection_engine';
|
||||
import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils';
|
||||
|
||||
describe('ScheduleNotificationResponseActions', () => {
|
||||
const signalOne = { agent: { id: 'agent-id-1' }, _id: 'alert-id-1', user: { id: 'S-1-5-20' } };
|
||||
const signalOne = {
|
||||
agent: { id: 'agent-id-1' },
|
||||
_id: 'alert-id-1',
|
||||
user: { id: 'S-1-5-20' },
|
||||
process: {
|
||||
pid: 123,
|
||||
},
|
||||
[ALERT_RULE_UUID]: 'rule-id-1',
|
||||
[ALERT_RULE_NAME]: 'rule-name-1',
|
||||
};
|
||||
const signalTwo = { agent: { id: 'agent-id-2' }, _id: 'alert-id-2' };
|
||||
const signals = [signalOne, signalTwo];
|
||||
const defaultQueryParams = {
|
||||
ecsMapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
savedQueryId: 'testSavedQueryId',
|
||||
query: undefined,
|
||||
queries: [],
|
||||
packId: undefined,
|
||||
};
|
||||
const defaultPackParams = {
|
||||
packId: 'testPackId',
|
||||
queries: [],
|
||||
query: undefined,
|
||||
ecsMapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
savedQueryId: undefined,
|
||||
};
|
||||
const defaultQueries = {
|
||||
ecs_mapping: undefined,
|
||||
platform: 'windows',
|
||||
version: '1.0.0',
|
||||
snapshot: true,
|
||||
removed: false,
|
||||
};
|
||||
const getSignals = () => [signalOne, signalTwo];
|
||||
|
||||
const defaultResultParams = {
|
||||
agent_ids: ['agent-id-1', 'agent-id-2'],
|
||||
alert_ids: ['alert-id-1', 'alert-id-2'],
|
||||
};
|
||||
const defaultQueryResultParams = {
|
||||
...defaultResultParams,
|
||||
ecs_mapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
ecsMapping: undefined,
|
||||
saved_query_id: 'testSavedQueryId',
|
||||
savedQueryId: undefined,
|
||||
queries: [],
|
||||
};
|
||||
const defaultPackResultParams = {
|
||||
...defaultResultParams,
|
||||
query: undefined,
|
||||
saved_query_id: undefined,
|
||||
ecs_mapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
};
|
||||
const osqueryActionMock = {
|
||||
create: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
const endpointActionMock = jest.fn();
|
||||
|
||||
const endpointActionMock = {
|
||||
getActionCreateService: jest.fn().mockReturnValue({
|
||||
createActionFromAlert: jest.fn(),
|
||||
}),
|
||||
};
|
||||
const scheduleNotificationResponseActions = getScheduleNotificationResponseActionsService({
|
||||
osqueryCreateActionService: osqueryActionMock,
|
||||
endpointAppContextService: endpointActionMock as never,
|
||||
experimentalFeatures: {
|
||||
automatedProcessActionsEnabled: true,
|
||||
endpointResponseActionsEnabled: true,
|
||||
} as never,
|
||||
});
|
||||
|
||||
const simpleQuery = 'select * from uptime';
|
||||
it('should handle osquery response actions with query', async () => {
|
||||
const responseActions: RuleResponseAction[] = [
|
||||
{
|
||||
actionTypeId: ResponseActionTypesEnum['.osquery'],
|
||||
params: {
|
||||
...defaultQueryParams,
|
||||
query: simpleQuery,
|
||||
},
|
||||
},
|
||||
];
|
||||
scheduleNotificationResponseActions({ signals, responseActions });
|
||||
describe('Osquery', () => {
|
||||
const simpleQuery = 'select * from uptime';
|
||||
const defaultQueryParams = {
|
||||
ecsMapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
savedQueryId: 'testSavedQueryId',
|
||||
query: undefined,
|
||||
queries: [],
|
||||
packId: undefined,
|
||||
};
|
||||
const defaultPackParams = {
|
||||
packId: 'testPackId',
|
||||
queries: [],
|
||||
query: undefined,
|
||||
ecsMapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
savedQueryId: undefined,
|
||||
};
|
||||
const defaultQueries = {
|
||||
ecs_mapping: undefined,
|
||||
platform: 'windows',
|
||||
version: '1.0.0',
|
||||
snapshot: true,
|
||||
removed: false,
|
||||
};
|
||||
|
||||
expect(osqueryActionMock.create).toHaveBeenCalledWith({
|
||||
...defaultQueryResultParams,
|
||||
query: simpleQuery,
|
||||
const defaultResultParams = {
|
||||
agent_ids: ['agent-id-1', 'agent-id-2'],
|
||||
alert_ids: ['alert-id-1', 'alert-id-2'],
|
||||
};
|
||||
const defaultQueryResultParams = {
|
||||
...defaultResultParams,
|
||||
ecs_mapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
ecsMapping: undefined,
|
||||
saved_query_id: 'testSavedQueryId',
|
||||
savedQueryId: undefined,
|
||||
queries: [],
|
||||
};
|
||||
const defaultPackResultParams = {
|
||||
...defaultResultParams,
|
||||
query: undefined,
|
||||
saved_query_id: undefined,
|
||||
ecs_mapping: { testField: { field: 'testField', value: 'testValue' } },
|
||||
};
|
||||
it('should handle osquery response actions with query', async () => {
|
||||
const signals = getSignals();
|
||||
const responseActions: RuleResponseAction[] = [
|
||||
{
|
||||
actionTypeId: ResponseActionTypesEnum['.osquery'],
|
||||
params: {
|
||||
...defaultQueryParams,
|
||||
query: simpleQuery,
|
||||
},
|
||||
},
|
||||
];
|
||||
scheduleNotificationResponseActions({ signals, responseActions });
|
||||
|
||||
expect(osqueryActionMock.create).toHaveBeenCalledWith({
|
||||
...defaultQueryResultParams,
|
||||
query: simpleQuery,
|
||||
});
|
||||
});
|
||||
//
|
||||
});
|
||||
it('should handle osquery response actions with packs', async () => {
|
||||
const responseActions: RuleResponseAction[] = [
|
||||
{
|
||||
actionTypeId: ResponseActionTypesEnum['.osquery'],
|
||||
params: {
|
||||
...defaultPackParams,
|
||||
queries: [
|
||||
{
|
||||
...defaultQueries,
|
||||
id: 'query-1',
|
||||
query: simpleQuery,
|
||||
},
|
||||
],
|
||||
packId: 'testPackId',
|
||||
},
|
||||
},
|
||||
];
|
||||
scheduleNotificationResponseActions({ signals, responseActions });
|
||||
|
||||
expect(osqueryActionMock.create).toHaveBeenCalledWith({
|
||||
...defaultPackResultParams,
|
||||
queries: [{ ...defaultQueries, id: 'query-1', query: simpleQuery }],
|
||||
it('should handle osquery response actions with packs', async () => {
|
||||
const signals = getSignals();
|
||||
|
||||
const responseActions: RuleResponseAction[] = [
|
||||
{
|
||||
actionTypeId: ResponseActionTypesEnum['.osquery'],
|
||||
params: {
|
||||
...defaultPackParams,
|
||||
queries: [
|
||||
{
|
||||
...defaultQueries,
|
||||
id: 'query-1',
|
||||
query: simpleQuery,
|
||||
},
|
||||
],
|
||||
packId: 'testPackId',
|
||||
},
|
||||
},
|
||||
];
|
||||
scheduleNotificationResponseActions({ signals, responseActions });
|
||||
|
||||
expect(osqueryActionMock.create).toHaveBeenCalledWith({
|
||||
...defaultPackResultParams,
|
||||
queries: [{ ...defaultQueries, id: 'query-1', query: simpleQuery }],
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('Endpoint', () => {
|
||||
it('should handle endpoint isolate actions', async () => {
|
||||
const signals = getSignals();
|
||||
|
||||
const responseActions: RuleResponseAction[] = [
|
||||
{
|
||||
actionTypeId: ResponseActionTypesEnum['.endpoint'],
|
||||
params: {
|
||||
command: 'isolate',
|
||||
comment: 'test isolate comment',
|
||||
},
|
||||
},
|
||||
];
|
||||
scheduleNotificationResponseActions({ signals, responseActions });
|
||||
|
||||
expect(
|
||||
endpointActionMock.getActionCreateService().createActionFromAlert
|
||||
).toHaveBeenCalledTimes(signals.length);
|
||||
expect(
|
||||
endpointActionMock.getActionCreateService().createActionFromAlert
|
||||
).toHaveBeenCalledWith(
|
||||
{
|
||||
alert_ids: ['alert-id-1'],
|
||||
command: 'isolate',
|
||||
comment: 'test isolate comment',
|
||||
endpoint_ids: ['agent-id-1'],
|
||||
agent_type: 'endpoint',
|
||||
hosts: {
|
||||
'agent-id-1': {
|
||||
id: 'agent-id-1',
|
||||
name: '',
|
||||
},
|
||||
},
|
||||
rule_id: 'rule-id-1',
|
||||
rule_name: 'rule-name-1',
|
||||
},
|
||||
['agent-id-1']
|
||||
);
|
||||
});
|
||||
it('should handle endpoint kill-process actions', async () => {
|
||||
const signals = getSignals();
|
||||
const responseActions: RuleResponseAction[] = [
|
||||
{
|
||||
actionTypeId: ResponseActionTypesEnum['.endpoint'],
|
||||
params: {
|
||||
command: 'kill-process',
|
||||
comment: 'test process comment',
|
||||
config: {
|
||||
overwrite: true,
|
||||
field: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
scheduleNotificationResponseActions({
|
||||
signals,
|
||||
responseActions,
|
||||
});
|
||||
|
||||
expect(
|
||||
endpointActionMock.getActionCreateService().createActionFromAlert
|
||||
).toHaveBeenCalledWith(
|
||||
{
|
||||
agent_type: 'endpoint',
|
||||
alert_ids: ['alert-id-1'],
|
||||
command: 'kill-process',
|
||||
comment: 'test process comment',
|
||||
endpoint_ids: ['agent-id-1'],
|
||||
error: undefined,
|
||||
hosts: {
|
||||
'agent-id-1': {
|
||||
id: 'agent-id-1',
|
||||
name: undefined,
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
pid: 123,
|
||||
},
|
||||
rule_id: 'rule-id-1',
|
||||
rule_name: 'rule-name-1',
|
||||
},
|
||||
['agent-id-1']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,23 +6,26 @@
|
|||
*/
|
||||
|
||||
import { each } from 'lodash';
|
||||
import type { ExperimentalFeatures } from '../../../../common';
|
||||
import type { EndpointAppContextService } from '../../../endpoint/endpoint_app_context_services';
|
||||
import type { SetupPlugins } from '../../../plugin_contract';
|
||||
import { ResponseActionTypesEnum } from '../../../../common/api/detection_engine/model/rule_response_actions';
|
||||
import { osqueryResponseAction } from './osquery_response_action';
|
||||
import { endpointResponseAction } from './endpoint_response_action';
|
||||
import type { ScheduleNotificationActions } from '../rule_types/types';
|
||||
import type { Alert, AlertWithAgent } from './types';
|
||||
import type { AlertWithAgent, Alert } from './types';
|
||||
|
||||
interface ScheduleNotificationResponseActionsService {
|
||||
endpointAppContextService: EndpointAppContextService;
|
||||
osqueryCreateActionService?: SetupPlugins['osquery']['createActionService'];
|
||||
experimentalFeatures: ExperimentalFeatures;
|
||||
}
|
||||
|
||||
export const getScheduleNotificationResponseActionsService =
|
||||
({
|
||||
osqueryCreateActionService,
|
||||
endpointAppContextService,
|
||||
experimentalFeatures,
|
||||
}: ScheduleNotificationResponseActionsService) =>
|
||||
({ signals, responseActions }: ScheduleNotificationActions) => {
|
||||
const alerts = (signals as Alert[]).filter((alert) => alert.agent?.id) as AlertWithAgent[];
|
||||
|
@ -37,9 +40,14 @@ export const getScheduleNotificationResponseActionsService =
|
|||
});
|
||||
}
|
||||
if (responseAction.actionTypeId === ResponseActionTypesEnum['.endpoint']) {
|
||||
endpointResponseAction(responseAction, endpointAppContextService, {
|
||||
alerts,
|
||||
});
|
||||
endpointResponseAction(
|
||||
responseAction,
|
||||
endpointAppContextService,
|
||||
{
|
||||
alerts,
|
||||
},
|
||||
experimentalFeatures
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -6,11 +6,17 @@
|
|||
*/
|
||||
|
||||
import type { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
|
||||
import type { CreateActionPayload } from '../../../endpoint/services/actions/create/types';
|
||||
|
||||
export type Alert = ParsedTechnicalFields & {
|
||||
_id: string;
|
||||
agent?: AlertAgent;
|
||||
process?: { pid: string };
|
||||
host?: {
|
||||
name: string;
|
||||
};
|
||||
process?: {
|
||||
pid: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface AlertAgent {
|
||||
|
@ -25,3 +31,8 @@ export interface AlertWithAgent extends Alert {
|
|||
export interface ResponseActionAlerts {
|
||||
alerts: AlertWithAgent[];
|
||||
}
|
||||
|
||||
export type AlertsAction = Pick<
|
||||
CreateActionPayload,
|
||||
'alert_ids' | 'endpoint_ids' | 'hosts' | 'parameters' | 'error'
|
||||
>;
|
||||
|
|
|
@ -0,0 +1,242 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getErrorProcessAlerts, getIsolateAlerts, getProcessAlerts } from './utils';
|
||||
import type { AlertWithAgent } from './types';
|
||||
|
||||
const getSampleAlerts = (): AlertWithAgent[] => {
|
||||
const alert = {
|
||||
_id: 'alert1',
|
||||
process: {
|
||||
pid: 1,
|
||||
},
|
||||
agent: {
|
||||
name: 'jammy-1',
|
||||
id: 'agent-id-1',
|
||||
},
|
||||
};
|
||||
const alert2 = {
|
||||
_id: 'alert2',
|
||||
process: {
|
||||
pid: 2,
|
||||
},
|
||||
agent: {
|
||||
name: 'jammy-2',
|
||||
id: 'agent-id-2',
|
||||
},
|
||||
};
|
||||
const alert3 = {
|
||||
_id: 'alert3',
|
||||
process: {
|
||||
pid: 2,
|
||||
},
|
||||
agent: {
|
||||
name: 'jammy-1',
|
||||
id: 'agent-id-1',
|
||||
},
|
||||
};
|
||||
const alert4 = {
|
||||
_id: 'alert4',
|
||||
agent: {
|
||||
name: 'jammy-1',
|
||||
id: 'agent-id-1',
|
||||
},
|
||||
};
|
||||
const alert5 = {
|
||||
_id: 'alert5',
|
||||
process: {
|
||||
entity_id: 2,
|
||||
},
|
||||
agent: {
|
||||
name: 'jammy-1',
|
||||
id: 'agent-id-1',
|
||||
},
|
||||
};
|
||||
// Casted as unknown first because we do not need all the data to test the functionality
|
||||
return [alert, alert2, alert3, alert4, alert5] as unknown as AlertWithAgent[];
|
||||
};
|
||||
describe('EndpointResponseActionsUtils', () => {
|
||||
describe('getIsolateAlerts', () => {
|
||||
const alerts = getSampleAlerts();
|
||||
it('should return proper number of actions divided per agents with specified alert_ids', async () => {
|
||||
const isolateAlerts = getIsolateAlerts(alerts);
|
||||
|
||||
const result = {
|
||||
'agent-id-1': {
|
||||
alert_ids: ['alert1', 'alert3', 'alert4', 'alert5'],
|
||||
endpoint_ids: ['agent-id-1'],
|
||||
hosts: {
|
||||
'agent-id-1': {
|
||||
id: 'agent-id-1',
|
||||
name: 'jammy-1',
|
||||
},
|
||||
},
|
||||
},
|
||||
'agent-id-2': {
|
||||
alert_ids: ['alert2'],
|
||||
endpoint_ids: ['agent-id-2'],
|
||||
hosts: {
|
||||
'agent-id-2': {
|
||||
id: 'agent-id-2',
|
||||
name: 'jammy-2',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(isolateAlerts).toEqual(result);
|
||||
});
|
||||
});
|
||||
describe('getProcessAlerts', () => {
|
||||
const alerts = getSampleAlerts();
|
||||
|
||||
it('should return actions that are valid based on default field (pid)', async () => {
|
||||
const processAlerts = getProcessAlerts(alerts, {
|
||||
overwrite: true,
|
||||
field: '',
|
||||
});
|
||||
|
||||
const result = {
|
||||
'agent-id-1': {
|
||||
'1': {
|
||||
alert_ids: ['alert1'],
|
||||
endpoint_ids: ['agent-id-1'],
|
||||
hosts: {
|
||||
'agent-id-1': {
|
||||
id: 'agent-id-1',
|
||||
name: 'jammy-1',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
pid: 1,
|
||||
},
|
||||
},
|
||||
'2': {
|
||||
alert_ids: ['alert3'],
|
||||
endpoint_ids: ['agent-id-1'],
|
||||
hosts: {
|
||||
'agent-id-1': {
|
||||
id: 'agent-id-1',
|
||||
name: 'jammy-1',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
pid: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
'agent-id-2': {
|
||||
'2': {
|
||||
alert_ids: ['alert2'],
|
||||
endpoint_ids: ['agent-id-2'],
|
||||
hosts: {
|
||||
'agent-id-2': {
|
||||
id: 'agent-id-2',
|
||||
name: 'jammy-2',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
pid: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(processAlerts).toEqual(result);
|
||||
});
|
||||
|
||||
it('should return actions that do not have value from default field (pid)', async () => {
|
||||
const processAlerts = getProcessAlerts(alerts, {
|
||||
overwrite: false,
|
||||
field: 'process.entity_id',
|
||||
});
|
||||
|
||||
const result = {
|
||||
'agent-id-1': {
|
||||
'2': {
|
||||
alert_ids: ['alert5'],
|
||||
endpoint_ids: ['agent-id-1'],
|
||||
hosts: {
|
||||
'agent-id-1': {
|
||||
id: 'agent-id-1',
|
||||
name: 'jammy-1',
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
entity_id: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(processAlerts).toEqual(result);
|
||||
});
|
||||
});
|
||||
describe('getErrorProcessAlerts', () => {
|
||||
const alerts = getSampleAlerts();
|
||||
|
||||
it('should return actions that do not have value from default field (pid)', async () => {
|
||||
const processAlerts = getErrorProcessAlerts(alerts, {
|
||||
overwrite: true,
|
||||
field: '',
|
||||
});
|
||||
|
||||
const result = {
|
||||
'agent-id-1': {
|
||||
'process.pid': {
|
||||
alert_ids: ['alert4', 'alert5'],
|
||||
endpoint_ids: ['agent-id-1'],
|
||||
error: 'process.pid',
|
||||
hosts: {
|
||||
'agent-id-1': {
|
||||
id: 'agent-id-1',
|
||||
name: 'jammy-1',
|
||||
},
|
||||
},
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(processAlerts).toEqual(result);
|
||||
});
|
||||
it('should return actions that do not have value from custom field name', async () => {
|
||||
const processAlerts = getErrorProcessAlerts(alerts, {
|
||||
overwrite: false,
|
||||
field: 'process.entity_id',
|
||||
});
|
||||
|
||||
const result = {
|
||||
'agent-id-1': {
|
||||
'process.entity_id': {
|
||||
alert_ids: ['alert1', 'alert3', 'alert4'],
|
||||
endpoint_ids: ['agent-id-1'],
|
||||
error: 'process.entity_id',
|
||||
hosts: {
|
||||
'agent-id-1': {
|
||||
id: 'agent-id-1',
|
||||
name: 'jammy-1',
|
||||
},
|
||||
},
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
'agent-id-2': {
|
||||
'process.entity_id': {
|
||||
alert_ids: ['alert2'],
|
||||
endpoint_ids: ['agent-id-2'],
|
||||
error: 'process.entity_id',
|
||||
hosts: {
|
||||
'agent-id-2': {
|
||||
id: 'agent-id-2',
|
||||
name: 'jammy-2',
|
||||
},
|
||||
},
|
||||
parameters: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(processAlerts).toEqual(result);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import type { AlertAgent, AlertWithAgent, AlertsAction } from './types';
|
||||
import type { ProcessesParams } from '../../../../common/api/detection_engine';
|
||||
|
||||
interface ProcessAlertsAcc {
|
||||
[key: string]: Record<string, AlertsAction>;
|
||||
}
|
||||
|
||||
export const getProcessAlerts = (
|
||||
alerts: AlertWithAgent[],
|
||||
config: ProcessesParams['config']
|
||||
): ProcessAlertsAcc => {
|
||||
if (!config) {
|
||||
return {};
|
||||
}
|
||||
const { overwrite, field } = config;
|
||||
|
||||
return alerts.reduce((acc: ProcessAlertsAcc, alert) => {
|
||||
const valueFromAlert: number = overwrite ? alert.process?.pid : get(alert, field);
|
||||
|
||||
if (valueFromAlert) {
|
||||
const isEntityId = !overwrite && field.includes('entity_id');
|
||||
const paramKey = isEntityId ? 'entity_id' : 'pid';
|
||||
const { _id, agent } = alert;
|
||||
const { id: agentId, name } = agent as AlertAgent;
|
||||
const hostName = alert.host?.name;
|
||||
|
||||
const currentAgent = acc[agentId];
|
||||
const currentValue = currentAgent?.[valueFromAlert];
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[agentId]: {
|
||||
...(currentAgent || {}),
|
||||
[valueFromAlert]: {
|
||||
...(currentValue || {}),
|
||||
alert_ids: [...(currentValue?.alert_ids || []), _id],
|
||||
parameters: { [paramKey]: valueFromAlert },
|
||||
endpoint_ids: [agentId],
|
||||
hosts: {
|
||||
...currentValue?.hosts,
|
||||
[agentId]: { name: name || hostName, id: agentId },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const getErrorProcessAlerts = (
|
||||
alerts: AlertWithAgent[],
|
||||
config: ProcessesParams['config']
|
||||
): ProcessAlertsAcc => {
|
||||
if (!config) {
|
||||
return {};
|
||||
}
|
||||
const { overwrite, field } = config;
|
||||
|
||||
return alerts.reduce((acc: ProcessAlertsAcc, alert) => {
|
||||
const valueFromAlert: number = overwrite ? alert.process?.pid : get(alert, field);
|
||||
|
||||
if (!valueFromAlert) {
|
||||
const { _id, agent } = alert;
|
||||
const { id: agentId, name } = agent as AlertAgent;
|
||||
const hostName = alert.host?.name;
|
||||
|
||||
const errorField = overwrite ? 'process.pid' : field;
|
||||
const currentAgent = acc[agentId];
|
||||
const currentValue = currentAgent?.[errorField];
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[agentId]: {
|
||||
...(currentAgent || {}),
|
||||
[errorField]: {
|
||||
...(currentValue || {}),
|
||||
alert_ids: [...(currentValue?.alert_ids || []), _id],
|
||||
parameters: {},
|
||||
endpoint_ids: [agentId],
|
||||
hosts: {
|
||||
...currentValue?.hosts,
|
||||
[agentId]: { name: name || hostName || '', id: agentId },
|
||||
},
|
||||
error: errorField,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const getIsolateAlerts = (alerts: AlertWithAgent[]): Record<string, AlertsAction> =>
|
||||
alerts.reduce((acc: Record<string, AlertsAction>, alert) => {
|
||||
const { id: agentId, name: agentName } = alert.agent || {};
|
||||
|
||||
const hostName = alert.host?.name;
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[agentId]: {
|
||||
...(acc?.[agentId] || {}),
|
||||
hosts: {
|
||||
...(acc[agentId]?.hosts || {}),
|
||||
[agentId]: {
|
||||
name: agentName || hostName || '',
|
||||
id: agentId,
|
||||
},
|
||||
},
|
||||
endpoint_ids: [agentId],
|
||||
alert_ids: [...(acc[agentId]?.alert_ids || []), alert._id],
|
||||
},
|
||||
};
|
||||
}, {});
|
|
@ -294,6 +294,7 @@ export class Plugin implements ISecuritySolutionPlugin {
|
|||
scheduleNotificationResponseActionsService: getScheduleNotificationResponseActionsService({
|
||||
endpointAppContextService: this.endpointAppContextService,
|
||||
osqueryCreateActionService: plugins.osquery.createActionService,
|
||||
experimentalFeatures: config.experimentalFeatures,
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue