Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Security Solution][Endpoint] Add an additional hint message for Console commands pending more than 15s #135500

Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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 { ConsoleProps } from '..';
import { AppContextTestRender } from '../../../../common/mock/endpoint';
import { getConsoleTestSetup } from '../mocks';
import { act } from '@testing-library/react';
import { CommandExecutionComponentProps } from '../types';

describe('When using CommandExecutionOutput component', () => {
let render: (props?: Partial<ConsoleProps>) => ReturnType<AppContextTestRender['render']>;
let renderResult: ReturnType<typeof render>;
let setCmd1ToComplete: () => void;

beforeEach(() => {
const { renderConsole, commands, enterCommand } = getConsoleTestSetup();

const cmd1 = commands.find((command) => command.name === 'cmd1');

if (!cmd1) {
throw new Error('cmd1 command not found in test mocks');
}

(cmd1.RenderComponent as jest.Mock).mockImplementation(
(props: CommandExecutionComponentProps) => {
setCmd1ToComplete = () => props.setStatus('success');

return <div>{'output'}</div>;
}
);

render = (props = {}) => {
renderResult = renderConsole(props);
enterCommand('cmd1');
return renderResult;
};
});

it('should show long running hint message if pending and >15s have passed', () => {
jest.useFakeTimers();
render();

expect(renderResult.queryByTestId('test-longRunningCommandHint')).toBeNull();

act(() => {
jest.advanceTimersByTime(16 * 1000);
});

expect(renderResult.getByTestId('test-longRunningCommandHint')).not.toBeNull();
});

it('should remove long running hint message if command completes', async () => {
jest.useFakeTimers();
render();

act(() => {
jest.advanceTimersByTime(16 * 1000);
});

expect(renderResult.getByTestId('test-longRunningCommandHint')).not.toBeNull();

act(() => {
setCmd1ToComplete();
});

expect(renderResult.queryByTestId('test-longRunningCommandHint')).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
* 2.0.
*/

import React, { memo, useCallback, useMemo } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { EuiLoadingChart, EuiSpacer } from '@elastic/eui';
import styled from 'styled-components';
import moment from 'moment';
import { LongRunningCommandHint } from './long_running_command_hint';
import { CommandExecutionResult } from './command_execution_result';
import type { CommandExecutionComponentProps } from '../types';
import type { CommandExecutionState, CommandHistoryItem } from './console_state/types';
Expand All @@ -26,9 +28,10 @@ export interface CommandExecutionOutputProps {
item: CommandHistoryItem;
}
export const CommandExecutionOutput = memo<CommandExecutionOutputProps>(
({ item: { command, state, id } }) => {
({ item: { command, state, id, enteredAt } }) => {
const dispatch = useConsoleStateDispatch();
const RenderComponent = command.commandDefinition.RenderComponent;
const [isLongRunningCommand, setIsLongRunningCommand] = useState(false);

const isRunning = useMemo(() => {
return state.status === 'pending';
Expand Down Expand Up @@ -62,6 +65,30 @@ export const CommandExecutionOutput = memo<CommandExecutionOutputProps>(
[dispatch, id]
);

// keep track if this becomes a long running command
useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout>;

if (isRunning && !isLongRunningCommand) {
const elapsedSeconds = moment().diff(moment(enteredAt), 'seconds');

if (elapsedSeconds >= 15) {
setIsLongRunningCommand(true);
return;
}

timeoutId = setTimeout(() => {
setIsLongRunningCommand(true);
}, (15 - elapsedSeconds) * 1000);
}
Comment on lines +73 to +83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here the elapsedSeconds never increments or is always 0. Basically, the timeout is set to execute after 15 seconds and sets the isLongRunningCommand to true. We're not actually using the time difference here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what you mean.

Let me explain this logic around elapsedSeconds. You can enter a command and then close the Responder window... When we re-open that same responder window, we need to determine how much time has elapsed since the command was entered so that we can correctly calculate the setTimeout() ms.

Example:

  • I enter a command at :00 s and immediately close the console
  • I reopen the console just 5s later.
  • the setTimeout() should now only wait 10s since 5s has already elapsed... so the (15 - elapsedSeconds) does that.
  • This logic here will only be triggered if 15s has not yet elapsed

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. When the command is executed and the console stays open beyond 15 seconds the elapsedSeconds is always 0 and thus the elapsedSeconds >= 15 block does not execute is what I meant. I didn't test it by closing and then re-opening the console, in which case the block does execute and it works as expected.


return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [enteredAt, isLongRunningCommand, isRunning]);

return (
<CommandOutputContainer>
<div>
Expand All @@ -80,6 +107,13 @@ export const CommandExecutionOutput = memo<CommandExecutionOutputProps>(
/>

{isRunning && <EuiLoadingChart className="busy-indicator" mono={true} />}

{isRunning && isLongRunningCommand && (
<>
<EuiSpacer size="s" />
<LongRunningCommandHint />
</>
)}
</div>
</CommandOutputContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import React, { memo, PropsWithChildren, ComponentType, useMemo } from 'react';
import type { ReactNode } from 'react';
import { i18n } from '@kbn/i18n';
import { CommonProps, EuiPanel, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui';
import { CommonProps, EuiPanel, EuiSpacer } from '@elastic/eui';
import classNames from 'classnames';
import { useDataTestSubj } from '../hooks/state_selectors/use_data_test_subj';
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
import { ConsoleText } from './console_text';

const COMMAND_EXECUTION_RESULT_SUCCESS_TITLE = i18n.translate(
'xpack.securitySolution.commandExecutionResult.successTitle',
Expand Down Expand Up @@ -84,24 +85,19 @@ export const CommandExecutionResult = memo<CommandExecutionResultProps>(
data-test-subj={dataTestSubj ? dataTestSubj : getTestId('commandExecutionResult')}
>
{showAs === 'pending' ? (
<EuiText size="s">
<EuiTextColor color="subdued">
{children ?? COMMAND_EXECUTION_RESULT_PENDING}
</EuiTextColor>
</EuiText>
<ConsoleText>{children ?? COMMAND_EXECUTION_RESULT_PENDING}</ConsoleText>
) : (
<>
{showTitle && (
<>
<EuiText size="s">
<EuiTextColor color={showAs === 'success' ? 'success' : 'danger'}>
{title
? title
: showAs === 'success'
? COMMAND_EXECUTION_RESULT_SUCCESS_TITLE
: COMMAND_EXECUTION_RESULT_FAILURE_TITLE}
</EuiTextColor>
</EuiText>
<ConsoleText color={showAs === 'success' ? 'success' : 'danger'}>
{title
? title
: showAs === 'success'
? COMMAND_EXECUTION_RESULT_SUCCESS_TITLE
: COMMAND_EXECUTION_RESULT_FAILURE_TITLE}
</ConsoleText>

<EuiSpacer size="s" />
</>
)}
Expand Down
Loading