Skip to content

Commit

Permalink
[Security Solution][Endpoint]Activity Log API/UX changes (#114905)
Browse files Browse the repository at this point in the history
* rename legacy actions/responses

fixes elastic/security-team/issues/1702

* use correct name for responses index

refs /pull/113621

* extract helper method to utils

* append endpoint responses docs to activity log

* Show completed responses on activity log

fixes elastic/security-team/issues/1703

* remove width restriction on date picker

* add a simple test to verify endpoint responses

fixes elastic/security-team/issues/1702

* find unique action_ids from `.fleet-actions` and `.logs-endpoint.actions-default` indices

fixes elastic/security-team/issues/1702

* do not filter out endpoint only actions/responses that did not make it to Fleet

review comments

* use a constant to manage various doc types

review comments

* refactor `getActivityLog`

Simplify `getActivityLog` so it is easier to reason with.
review comments

* skip this for now

will mock this better in a new PR

* improve types

* display endpoint actions similar to fleet actions, but with success icon color

* Correctly do mocks for tests

* Include only errored endpoint actions, remove successful duplicates

fixes elastic/security-team/issues/1703

* Update tests to use non duplicate action_ids

review comments
fixes elastic/security-team/issues/1703

* show correct action title

review fixes

* statusCode constant

review change

* rename

review changes

* Update translations.ts

refs 74a8340

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
ashokaditya and kibanamachine authored Oct 19, 2021
1 parent b103a54 commit d0bc10f
Show file tree
Hide file tree
Showing 16 changed files with 949 additions and 203 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
export const ENDPOINT_ACTIONS_DS = '.logs-endpoint.actions';
export const ENDPOINT_ACTIONS_INDEX = `${ENDPOINT_ACTIONS_DS}-default`;
export const ENDPOINT_ACTION_RESPONSES_DS = '.logs-endpoint.action.responses';
export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTIONS_DS}-default`;
export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTION_RESPONSES_DS}-default`;

export const eventsIndexPattern = 'logs-endpoint.events.*';
export const alertsIndexPattern = 'logs-endpoint.alerts-*';
Expand Down Expand Up @@ -60,3 +60,5 @@ export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`;
/** Endpoint Actions Log Routes */
export const ENDPOINT_ACTION_LOG_ROUTE = `/api/endpoint/action_log/{agent_id}`;
export const ACTION_STATUS_ROUTE = `/api/endpoint/action_status`;

export const failedFleetActionErrorCode = '424';
33 changes: 30 additions & 3 deletions x-pack/plugins/security_solution/common/endpoint/types/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import { ActionStatusRequestSchema, HostIsolationRequestSchema } from '../schema

export type ISOLATION_ACTIONS = 'isolate' | 'unisolate';

export const ActivityLogItemTypes = {
ACTION: 'action' as const,
RESPONSE: 'response' as const,
FLEET_ACTION: 'fleetAction' as const,
FLEET_RESPONSE: 'fleetResponse' as const,
};

interface EcsError {
code?: string;
id?: string;
Expand Down Expand Up @@ -87,8 +94,24 @@ export interface EndpointActionResponse {
action_data: EndpointActionData;
}

export interface EndpointActivityLogAction {
type: typeof ActivityLogItemTypes.ACTION;
item: {
id: string;
data: LogsEndpointAction;
};
}

export interface EndpointActivityLogActionResponse {
type: typeof ActivityLogItemTypes.RESPONSE;
item: {
id: string;
data: LogsEndpointActionResponse;
};
}

export interface ActivityLogAction {
type: 'action';
type: typeof ActivityLogItemTypes.FLEET_ACTION;
item: {
// document _id
id: string;
Expand All @@ -97,15 +120,19 @@ export interface ActivityLogAction {
};
}
export interface ActivityLogActionResponse {
type: 'response';
type: typeof ActivityLogItemTypes.FLEET_RESPONSE;
item: {
// document id
id: string;
// document _source
data: EndpointActionResponse;
};
}
export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse;
export type ActivityLogEntry =
| ActivityLogAction
| ActivityLogActionResponse
| EndpointActivityLogAction
| EndpointActivityLogActionResponse;
export interface ActivityLog {
page: number;
pageSize: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ interface Range {

const DatePickerWrapper = styled.div`
width: ${(props) => props.theme.eui.fractions.single.percentage};
max-width: 350px;
`;
const StickyFlexItem = styled(EuiFlexItem)`
background: ${(props) => `${props.theme.eui.euiHeaderBackgroundColor}`};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,54 @@ import React, { memo, useMemo } from 'react';
import styled from 'styled-components';

import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui';
import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types';
import {
Immutable,
ActivityLogEntry,
ActivityLogItemTypes,
} from '../../../../../../../common/endpoint/types';
import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date';
import { LogEntryTimelineIcon } from './log_entry_timeline_icon';
import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme';

import * as i18 from '../../translations';

const useLogEntryUIProps = (
logEntry: Immutable<ActivityLogEntry>
logEntry: Immutable<ActivityLogEntry>,
theme: ReturnType<typeof useEuiTheme>
): {
actionEventTitle: string;
avatarColor: EuiAvatarProps['color'];
avatarIconColor: EuiAvatarProps['iconColor'];
avatarSize: EuiAvatarProps['size'];
commentText: string;
commentType: EuiCommentProps['type'];
displayComment: boolean;
displayResponseEvent: boolean;
failedActionEventTitle: string;
iconType: IconType;
isResponseEvent: boolean;
isSuccessful: boolean;
isCompleted: boolean;
responseEventTitle: string;
username: string | React.ReactNode;
} => {
return useMemo(() => {
let iconType: IconType = 'dot';
let commentType: EuiCommentProps['type'] = 'update';
let commentText: string = '';
let avatarColor: EuiAvatarProps['color'] = theme.euiColorLightestShade;
let avatarIconColor: EuiAvatarProps['iconColor'];
let avatarSize: EuiAvatarProps['size'] = 's';
let failedActionEventTitle: string = '';
let isIsolateAction: boolean = false;
let isResponseEvent: boolean = false;
let isSuccessful: boolean = false;
let isCompleted: boolean = false;
let displayComment: boolean = false;
let displayResponseEvent: boolean = true;
let username: EuiCommentProps['username'] = '';

if (logEntry.type === 'action') {
if (logEntry.type === ActivityLogItemTypes.FLEET_ACTION) {
avatarSize = 'm';
commentType = 'regular';
commentText = logEntry.item.data.data.comment?.trim() ?? '';
Expand All @@ -59,13 +73,51 @@ const useLogEntryUIProps = (
displayComment = true;
}
}
} else if (logEntry.type === 'response') {
}
if (logEntry.type === ActivityLogItemTypes.ACTION) {
avatarSize = 'm';
commentType = 'regular';
commentText = logEntry.item.data.EndpointActions.data.comment?.trim() ?? '';
displayResponseEvent = false;
iconType = 'lockOpen';
username = logEntry.item.data.user.id;
avatarIconColor = theme.euiColorVis9_behindText;
failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointReleaseAction;
if (logEntry.item.data.EndpointActions.data) {
const data = logEntry.item.data.EndpointActions.data;
if (data.command === 'isolate') {
iconType = 'lock';
failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointIsolateAction;
}
if (commentText) {
displayComment = true;
}
}
} else if (logEntry.type === ActivityLogItemTypes.FLEET_RESPONSE) {
isResponseEvent = true;
if (logEntry.item.data.action_data.command === 'isolate') {
isIsolateAction = true;
}
if (!!logEntry.item.data.completed_at && !logEntry.item.data.error) {
isSuccessful = true;
} else {
avatarColor = theme.euiColorVis9_behindText;
}
} else if (logEntry.type === ActivityLogItemTypes.RESPONSE) {
iconType = 'check';
isResponseEvent = true;
if (logEntry.item.data.EndpointActions.data.command === 'isolate') {
isIsolateAction = true;
}
if (logEntry.item.data.EndpointActions.completed_at) {
isCompleted = true;
if (!logEntry.item.data.error) {
isSuccessful = true;
avatarColor = theme.euiColorVis0_behindText;
} else {
isSuccessful = false;
avatarColor = theme.euiColorVis9_behindText;
}
}
}

Expand All @@ -75,13 +127,23 @@ const useLogEntryUIProps = (

const getResponseEventTitle = () => {
if (isIsolateAction) {
if (isSuccessful) {
if (isCompleted) {
if (isSuccessful) {
return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful;
}
return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful;
} else if (isSuccessful) {
return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful;
} else {
return i18.ACTIVITY_LOG.LogEntry.response.isolationFailed;
}
} else {
if (isSuccessful) {
if (isCompleted) {
if (isSuccessful) {
return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful;
}
return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful;
} else if (isSuccessful) {
return i18.ACTIVITY_LOG.LogEntry.response.unisolationSuccessful;
} else {
return i18.ACTIVITY_LOG.LogEntry.response.unisolationFailed;
Expand All @@ -91,18 +153,22 @@ const useLogEntryUIProps = (

return {
actionEventTitle,
avatarColor,
avatarIconColor,
avatarSize,
commentText,
commentType,
displayComment,
displayResponseEvent,
failedActionEventTitle,
iconType,
isResponseEvent,
isSuccessful,
isCompleted,
responseEventTitle: getResponseEventTitle(),
username,
};
}, [logEntry]);
}, [logEntry, theme]);
};

const StyledEuiComment = styled(EuiComment)`
Expand All @@ -126,28 +192,41 @@ const StyledEuiComment = styled(EuiComment)`
`;

export const LogEntry = memo(({ logEntry }: { logEntry: Immutable<ActivityLogEntry> }) => {
const theme = useEuiTheme();
const {
actionEventTitle,
avatarColor,
avatarIconColor,
avatarSize,
commentText,
commentType,
displayComment,
displayResponseEvent,
failedActionEventTitle,
iconType,
isResponseEvent,
isSuccessful,
responseEventTitle,
username,
} = useLogEntryUIProps(logEntry);
} = useLogEntryUIProps(logEntry, theme);

return (
<StyledEuiComment
type={(commentType ?? 'regular') as EuiCommentProps['type']}
username={username}
timestamp={<FormattedRelativePreferenceDate value={logEntry.item.data['@timestamp']} />}
event={<b>{displayResponseEvent ? responseEventTitle : actionEventTitle}</b>}
event={
<b>
{displayResponseEvent
? responseEventTitle
: failedActionEventTitle
? failedActionEventTitle
: actionEventTitle}
</b>
}
timelineIcon={
<LogEntryTimelineIcon {...{ avatarSize, iconType, isResponseEvent, isSuccessful }} />
<LogEntryTimelineIcon
{...{ avatarSize, iconType, isResponseEvent, avatarColor, avatarIconColor }}
/>
}
data-test-subj="timelineEntry"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,27 @@

import React, { memo } from 'react';
import { EuiAvatar, EuiAvatarProps } from '@elastic/eui';
import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme';

export const LogEntryTimelineIcon = memo(
({
avatarColor,
avatarIconColor,
avatarSize,
isResponseEvent,
isSuccessful,
iconType,
isResponseEvent,
}: {
avatarColor: EuiAvatarProps['color'];
avatarIconColor?: EuiAvatarProps['iconColor'];
avatarSize: EuiAvatarProps['size'];
isResponseEvent: boolean;
isSuccessful: boolean;
iconType: EuiAvatarProps['iconType'];
isResponseEvent: boolean;
}) => {
const euiTheme = useEuiTheme();

return (
<EuiAvatar
name="Timeline Icon"
size={avatarSize ?? 's'}
color={
isResponseEvent && !isSuccessful
? euiTheme.euiColorVis9_behindText
: euiTheme.euiColorLightestShade
}
iconColor="default"
color={avatarColor}
iconColor={avatarIconColor ?? 'default'}
iconType={iconType ?? 'dot'}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
import React, { ComponentType } from 'react';
import moment from 'moment';

import { ActivityLog, Immutable } from '../../../../../../common/endpoint/types';
import {
ActivityLog,
Immutable,
ActivityLogItemTypes,
} from '../../../../../../common/endpoint/types';
import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs';
import { EndpointActivityLog } from './endpoint_activity_log';
import { EndpointDetailsFlyout } from '.';
Expand All @@ -26,7 +30,7 @@ export const dummyEndpointActivityLog = (
endDate: moment().toString(),
data: [
{
type: 'action',
type: ActivityLogItemTypes.FLEET_ACTION,
item: {
id: '',
data: {
Expand All @@ -44,7 +48,7 @@ export const dummyEndpointActivityLog = (
},
},
{
type: 'action',
type: ActivityLogItemTypes.FLEET_ACTION,
item: {
id: '',
data: {
Expand All @@ -63,7 +67,7 @@ export const dummyEndpointActivityLog = (
},
},
{
type: 'action',
type: ActivityLogItemTypes.FLEET_ACTION,
item: {
id: '',
data: {
Expand All @@ -82,7 +86,7 @@ export const dummyEndpointActivityLog = (
},
},
{
type: 'action',
type: ActivityLogItemTypes.FLEET_ACTION,
item: {
id: '',
data: {
Expand Down
Loading

0 comments on commit d0bc10f

Please sign in to comment.