[Security Solution] Enable endpoint actions for events (#206857)

## Summary

This PR enabled endpoint actions for generic events (in addition to
alerts). We want to allow users to perform endpoint related actions like
isolate host and respond in the flyout. Main use case is to perform
endpoint actions when investigating in analyzer.

**Before**

![image](https://github.com/user-attachments/assets/29464129-49ad-4816-9713-c5b3c6c0f06e)

**After**
Enabled for events when host uses elastic defend

![image](https://github.com/user-attachments/assets/0298022e-5606-4878-8ccd-b63a83d1feb0)

Disabled when host cannot be isolated

![image](https://github.com/user-attachments/assets/6aaf8fee-c83c-47f7-909b-5042be066f48)

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
christineweng 2025-03-14 16:28:30 -05:00 committed by GitHub
parent 448a0364d1
commit c329ccf87b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 19 additions and 43 deletions

View file

@ -41512,7 +41512,6 @@
"xpack.securitySolution.uncommonProcessTable.rows": "{numRows} {numRows, plural, =0 {ligne} =1 {ligne} other {lignes}}",
"xpack.securitySolution.uncommonProcessTable.unit": "{totalCount, plural, other {processus}}",
"xpack.securitySolution.useAlertResponseActionsSupport.missingAgentIdField": "Alerte, les données d'événement ne comportent pas le champ d'identificateur d'agent {agentTypeName} ({missingField}",
"xpack.securitySolution.useAlertResponseActionsSupport.notAnAlert": "Les actions de réponse ne sont prises en charge que pour les alertes (et non pour les événements)",
"xpack.securitySolution.useConsoleActionSubmitter.actionRequestFailure": "Échec de la création d'une requête d'action.",
"xpack.securitySolution.useInputHints.exampleInstructions": "Ex : [ {exampleUsage} ]",
"xpack.securitySolution.useInputHints.noArguments": "Appuyez sur Entrée pour exécuter",

View file

@ -41487,7 +41487,6 @@
"xpack.securitySolution.uncommonProcessTable.rows": "{numRows} {numRows, plural, other {行}}",
"xpack.securitySolution.uncommonProcessTable.unit": "{totalCount, plural, other {プロセス}}",
"xpack.securitySolution.useAlertResponseActionsSupport.missingAgentIdField": "{agentTypeName}エージェントIDフィールド{missingField})が見つからないイベントデータを警告",
"xpack.securitySolution.useAlertResponseActionsSupport.notAnAlert": "対応アクションはアラートでのみサポートされています(イベントはサポートされていません)",
"xpack.securitySolution.useConsoleActionSubmitter.actionRequestFailure": "アクションリクエストを作成できませんでした。",
"xpack.securitySolution.useInputHints.exampleInstructions": "例:[ {exampleUsage} ]",
"xpack.securitySolution.useInputHints.noArguments": "Enterを押すと実行します",

View file

@ -41550,7 +41550,6 @@
"xpack.securitySolution.uncommonProcessTable.rows": "{numRows} {numRows, plural, other {行}}",
"xpack.securitySolution.uncommonProcessTable.unit": "{totalCount, plural, other {个进程}}",
"xpack.securitySolution.useAlertResponseActionsSupport.missingAgentIdField": "告警事件数据缺少 {agentTypeName} 代理标识符字段 ({missingField})",
"xpack.securitySolution.useAlertResponseActionsSupport.notAnAlert": "仅告警(而不是事件)支持响应操作",
"xpack.securitySolution.useConsoleActionSubmitter.actionRequestFailure": "无法创建操作请求。",
"xpack.securitySolution.useInputHints.exampleInstructions": "例如:[ {exampleUsage} ]",
"xpack.securitySolution.useInputHints.noArguments": "按 Enter 键以执行",

View file

@ -108,13 +108,13 @@ describe('useHostIsolationAction', () => {
expect(hookProps.closePopover).toHaveBeenCalled();
});
it('should NOT return the menu item for Events', () => {
it('should return the menu item for Events', () => {
hookProps.detailsData = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType('foo', {
'kibana.alert.rule.uuid': undefined,
});
const { result } = render();
expect(result.current).toHaveLength(0);
expect(result.current).toHaveLength(1);
});
it('should NOT return menu item if user does not have authz', async () => {

View file

@ -25,6 +25,8 @@ export interface UseHostIsolationActionProps {
onAddIsolationStatusClick: (action: 'isolateHost' | 'unisolateHost') => void;
}
const emptyArray: AlertTableContextMenuItem[] = [];
export const useHostIsolationAction = ({
closePopover,
detailsData,
@ -33,7 +35,6 @@ export const useHostIsolationAction = ({
}: UseHostIsolationActionProps): AlertTableContextMenuItem[] => {
const {
isSupported: hostSupportsResponseActions,
isAlert,
unsupportedReason,
details: {
agentType,
@ -76,9 +77,9 @@ export const useHostIsolationAction = ({
}, [hostSupportsResponseActions, agentStatus]);
return useMemo<AlertTableContextMenuItem[]>(() => {
// If not an Alert OR user has no Authz, then don't show the menu item at all
if (!isAlert || (isHostIsolated && !canUnIsolateHost) || !canIsolateHost) {
return [];
// If user has no Authz, then don't show the menu item at all
if ((isHostIsolated && !canUnIsolateHost) || !canIsolateHost) {
return emptyArray;
}
const menuItem: AlertTableContextMenuItem = {
@ -109,7 +110,6 @@ export const useHostIsolationAction = ({
return [menuItem];
}, [
isAlert,
isHostIsolated,
canUnIsolateHost,
canIsolateHost,

View file

@ -51,11 +51,11 @@ describe('useResponderActionItem', () => {
expect(renderHook().result.current).toHaveLength(0);
});
it('should NOT return the Respond action menu item for Events', () => {
it('should return the Respond action menu item for Events', () => {
alertDetailItemData = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType('foo', {
'kibana.alert.rule.uuid': undefined,
});
expect(renderHook().result.current).toHaveLength(0);
expect(renderHook().result.current).toHaveLength(1);
});
});

View file

@ -8,7 +8,6 @@
import React, { useMemo } from 'react';
import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { useAlertResponseActionsSupport } from '../../../../hooks/endpoint/use_alert_response_actions_support';
import { useUserPrivileges } from '../../../user_privileges';
import type { AlertTableContextMenuItem } from '../../../../../detections/components/alerts_table/types';
import { useWithResponderActionDataFromAlert } from './use_responder_action_data';
@ -19,7 +18,6 @@ export const useResponderActionItem = (
): AlertTableContextMenuItem[] => {
const { loading: isAuthzLoading, canAccessResponseConsole } =
useUserPrivileges().endpointPrivileges;
const { isAlert } = useAlertResponseActionsSupport(eventDetailsData);
const { handleResponseActionsClick, isDisabled, tooltip } = useWithResponderActionDataFromAlert({
onClick,
eventData: eventDetailsData,
@ -28,7 +26,7 @@ export const useResponderActionItem = (
return useMemo(() => {
const actions: AlertTableContextMenuItem[] = [];
if (!isAuthzLoading && canAccessResponseConsole && isAlert) {
if (!isAuthzLoading && canAccessResponseConsole) {
actions.push({
key: 'endpointResponseActions-action-item',
'data-test-subj': 'endpointResponseActions-action-item',
@ -46,12 +44,5 @@ export const useResponderActionItem = (
}
return actions;
}, [
canAccessResponseConsole,
handleResponseActionsClick,
isAlert,
isAuthzLoading,
isDisabled,
tooltip,
]);
}, [canAccessResponseConsole, handleResponseActionsClick, isAuthzLoading, isDisabled, tooltip]);
};

View file

@ -17,7 +17,6 @@ import {
import type { AlertResponseActionsSupport } from './use_alert_response_actions_support';
import {
ALERT_EVENT_DATA_MISSING_AGENT_ID_FIELD,
RESPONSE_ACTIONS_ONLY_SUPPORTED_ON_ALERTS,
useAlertResponseActionsSupport,
} from './use_alert_response_actions_support';
import { isAgentTypeAndActionSupported } from '../../lib/endpoint';
@ -96,7 +95,6 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
{
isAlert: false,
isSupported: false,
unsupportedReason: RESPONSE_ACTIONS_ONLY_SUPPORTED_ON_ALERTS,
details: {
agentId: '',
agentIdField: '',
@ -109,7 +107,7 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
);
});
it('should set `isSupported` to `false` for if not an Alert', () => {
it('should set `isSupported` to `true` for if it is not an Alert but supported', () => {
alertDetailItemData = endpointAlertDataMock.generateAlertDetailsItemDataForAgentType(
'sentinel_one',
{ 'kibana.alert.rule.uuid': undefined }
@ -118,8 +116,7 @@ describe('When using `useAlertResponseActionsSupport()` hook', () => {
expect(renderHook().result.current).toEqual(
getExpectedResult({
isAlert: false,
isSupported: false,
unsupportedReason: RESPONSE_ACTIONS_ONLY_SUPPORTED_ON_ALERTS,
isSupported: true,
details: {
agentType: 'sentinel_one',
agentIdField: RESPONSE_ACTIONS_ALERT_AGENT_ID_FIELDS.sentinel_one[0],

View file

@ -37,11 +37,6 @@ export const ALERT_EVENT_DATA_MISSING_AGENT_ID_FIELD = (
);
};
export const RESPONSE_ACTIONS_ONLY_SUPPORTED_ON_ALERTS = i18n.translate(
'xpack.securitySolution.useAlertResponseActionsSupport.notAnAlert',
{ defaultMessage: 'Response actions are only supported for Alerts (not events)' }
);
export interface AlertResponseActionsSupport {
/** Does the host/agent for the given alert have support for response actions */
isSupported: boolean;
@ -135,8 +130,8 @@ export const useAlertResponseActionsSupport = (
}, [agentType, eventData]);
const doesHostSupportResponseActions = useMemo(() => {
return Boolean(isFeatureEnabled && isAlert && agentId && agentType);
}, [agentId, agentType, isAlert, isFeatureEnabled]);
return Boolean(isFeatureEnabled && agentId && agentType);
}, [agentId, agentType, isFeatureEnabled]);
const supportedActions = useMemo(() => {
return RESPONSE_ACTION_API_COMMANDS_NAMES.reduce<AlertAgentActionsSupported>(
@ -167,10 +162,6 @@ export const useAlertResponseActionsSupport = (
const unsupportedReason = useMemo(() => {
if (!doesHostSupportResponseActions) {
if (!isAlert) {
return RESPONSE_ACTIONS_ONLY_SUPPORTED_ON_ALERTS;
}
if (!agentType) {
// No message is provided for this condition because the
// return from this hook will always default to `endpoint`
@ -181,7 +172,7 @@ export const useAlertResponseActionsSupport = (
return ALERT_EVENT_DATA_MISSING_AGENT_ID_FIELD(getAgentTypeName(agentType), agentIdField);
}
}
}, [agentId, agentIdField, agentType, doesHostSupportResponseActions, isAlert]);
}, [agentId, agentIdField, agentType, doesHostSupportResponseActions]);
return useMemo<AlertResponseActionsSupport>(() => {
return {

View file

@ -60,7 +60,7 @@ export const PanelContent: FC = () => {
export const IsolateHostPanelContent: FC<{
isIsolateActionSuccessBannerVisible: boolean;
hostName: string;
alertId: string;
alertId?: string;
isolateAction: 'isolateHost' | 'unisolateHost';
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[];
showAlertDetails: () => void;

View file

@ -203,7 +203,7 @@ export const TakeActionButton: FC = () => {
/>
)}
{isHostIsolationPanelOpen && alertId && (
{isHostIsolationPanelOpen && (
// EUI TODO: This z-index override of EuiOverlayMask is a workaround, and ideally should be resolved with a cleaner UI/UX flow long-term
<EuiFlyout onClose={showAlertDetails} size="m" maskProps={flyoutZIndex}>
<IsolateHostPanelHeader
@ -213,7 +213,7 @@ export const TakeActionButton: FC = () => {
<IsolateHostPanelContent
isIsolateActionSuccessBannerVisible={isIsolateActionSuccessBannerVisible}
hostName={hostName}
alertId={alertId}
alertId={alertId ?? undefined}
isolateAction={isolateAction}
dataFormattedForFieldBrowser={dataFormattedForFieldBrowser}
showAlertDetails={showAlertDetails}