Skip to content

Commit

Permalink
[Security Solution][Endpoint][Response Actions] Show download link fo…
Browse files Browse the repository at this point in the history
…r `get-file` action on response actions history (#144094)

* Show download link for get-file success

Show download link for successful get-file actions on action history

fixes elastic/security-team/issues/5076

* add missing help prefix

* add tests

fixes elastic/security-team/issues/5076

* update tests

review changes (@paul-tavares)

* use test ids instead

review change (@paul-tavares)

* Update use_response_actions_log_table.tsx

review change (@dasansol92)

* reorder if statements

review suggestion (@gergoabraham)
  • Loading branch information
ashokaditya authored Oct 31, 2022
1 parent bae2fb5 commit 43ffa96
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ export const getEndpointResponseActionsConsoleCommands = ({
capabilities: endpointCapabilities,
privileges: endpointPrivileges,
},
exampleUsage: 'get-file path "/full/path/to/file.txt" --comment "Possible malware"',
exampleUsage: 'get-file --path "/full/path/to/file.txt" --comment "Possible malware"',
exampleInstruction: ENTER_OR_ADD_COMMENT_ARG_INSTRUCTION,
validate: capabilitiesAndPrivilegesValidator,
mustHaveArgs: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@

import uuid from 'uuid';
import type { ActionListApiResponse } from '../../../../common/endpoint/types';
import type { ResponseActionStatus } from '../../../../common/endpoint/service/response_actions/constants';
import type {
ResponseActionsApiCommandNames,
ResponseActionStatus,
} from '../../../../common/endpoint/service/response_actions/constants';
import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator';

export const getActionListMock = async ({
Expand Down Expand Up @@ -49,6 +52,7 @@ export const getActionListMock = async ({
const actionDetails: ActionListApiResponse['data'] = actionIds.map((actionId) => {
return endpointActionGenerator.generateActionDetails({
agents: [id],
command: (commands?.[0] ?? 'isolate') as ResponseActionsApiCommandNames,
id: actionId,
isCompleted,
isExpired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { getActionListMock } from './mocks';
import { useGetEndpointsList } from '../../hooks/endpoint/use_get_endpoints_list';
import uuid from 'uuid';
import { RESPONSE_ACTION_API_COMMANDS_NAMES } from '../../../../common/endpoint/service/response_actions/constants';
import { useUserPrivileges as _useUserPrivileges } from '../../../common/components/user_privileges';

let mockUseGetEndpointActionList: {
isFetched?: boolean;
Expand Down Expand Up @@ -113,9 +114,15 @@ jest.mock('@kbn/kibana-react-plugin/public', () => {

jest.mock('../../hooks/endpoint/use_get_endpoints_list');

jest.mock('../../../common/components/user_privileges');

const mockUseGetEndpointsList = useGetEndpointsList as jest.Mock;

describe('Response actions history', () => {
const useUserPrivilegesMock = _useUserPrivileges as jest.Mock<
ReturnType<typeof _useUserPrivileges>
>;

const testPrefix = 'response-actions-list';

let render: (
Expand Down Expand Up @@ -409,6 +416,53 @@ describe('Response actions history', () => {
);
});

it('should contain download link in expanded row for `get-file` action WITH file operation permission', async () => {
mockUseGetEndpointActionList = {
...baseMockedActionList,
data: await getActionListMock({ actionCount: 1, commands: ['get-file'] }),
};

render();
const { getByTestId } = renderResult;

const expandButton = getByTestId(`${testPrefix}-expand-button`);
userEvent.click(expandButton);
const downloadLink = getByTestId(`${testPrefix}-getFileDownloadLink`);
expect(downloadLink).toBeTruthy();
expect(downloadLink.textContent).toEqual(
'Click here to download(ZIP file passcode: elastic)'
);
});

it('should not contain download link in expanded row for `get-file` action when NO file operation permission', async () => {
const privileges = useUserPrivilegesMock();

useUserPrivilegesMock.mockImplementationOnce(() => {
return {
...privileges,
endpointPrivileges: {
...privileges.endpointPrivileges,
canWriteFileOperations: false,
},
};
});

mockUseGetEndpointActionList = {
...baseMockedActionList,
data: await getActionListMock({ actionCount: 1, commands: ['get-file'] }),
};

render();
const { getByTestId, queryByTestId } = renderResult;

const expandButton = getByTestId(`${testPrefix}-expand-button`);
userEvent.click(expandButton);
const output = getByTestId(`${testPrefix}-details-tray-output`);
expect(output).toBeTruthy();
expect(output.textContent).toEqual('get-file completed successfully');
expect(queryByTestId(`${testPrefix}-getFileDownloadLink`)).toBeNull();
});

it('should refresh data when autoRefresh is toggled on', async () => {
render();
const { getByTestId } = renderResult;
Expand Down Expand Up @@ -552,17 +606,22 @@ describe('Response actions history', () => {

it('should show a list of actions when opened', () => {
render();
const { getByTestId } = renderResult;
const { getByTestId, getAllByTestId } = renderResult;

userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`));
const filterList = getByTestId(`${testPrefix}-${filterPrefix}-popoverList`);
expect(filterList).toBeTruthy();
expect(filterList.querySelectorAll('ul>li').length).toEqual(
expect(getAllByTestId(`${filterPrefix}-option`).length).toEqual(
RESPONSE_ACTION_API_COMMANDS_NAMES.length
);
expect(
Array.from(filterList.querySelectorAll('ul>li')).map((option) => option.textContent)
).toEqual(['isolate', 'release', 'kill-process', 'suspend-process', 'processes', 'get-file']);
expect(getAllByTestId(`${filterPrefix}-option`).map((option) => option.textContent)).toEqual([
'isolate',
'release',
'kill-process',
'suspend-process',
'processes',
'get-file',
]);
});

it('should have `clear all` button `disabled` when no selected values', () => {
Expand All @@ -580,15 +639,17 @@ describe('Response actions history', () => {

it('should show a list of statuses when opened', () => {
render();
const { getByTestId } = renderResult;
const { getByTestId, getAllByTestId } = renderResult;

userEvent.click(getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`));
const filterList = getByTestId(`${testPrefix}-${filterPrefix}-popoverList`);
expect(filterList).toBeTruthy();
expect(filterList.querySelectorAll('ul>li').length).toEqual(3);
expect(
Array.from(filterList.querySelectorAll('ul>li')).map((option) => option.textContent)
).toEqual(['Failed', 'Pending', 'Successful']);
expect(getAllByTestId(`${filterPrefix}-option`).length).toEqual(3);
expect(getAllByTestId(`${filterPrefix}-option`).map((option) => option.textContent)).toEqual([
'Failed',
'Pending',
'Successful',
]);
});

it('should have `clear all` button `disabled` when no selected values', () => {
Expand Down Expand Up @@ -623,13 +684,13 @@ describe('Response actions history', () => {

it('should show a list of host names when opened', () => {
render({ showHostNames: true });
const { getByTestId } = renderResult;
const { getByTestId, getAllByTestId } = renderResult;

const popoverButton = getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`);
userEvent.click(popoverButton);
const filterList = getByTestId(`${testPrefix}-${filterPrefix}-popoverList`);
expect(filterList).toBeTruthy();
expect(filterList.querySelectorAll('ul>li').length).toEqual(9);
expect(getAllByTestId(`${filterPrefix}-option`).length).toEqual(9);
expect(
getByTestId(`${testPrefix}-${filterPrefix}-popoverButton`).querySelector(
'.euiNotificationBadge'
Expand All @@ -652,16 +713,15 @@ describe('Response actions history', () => {
}
});

const filterList = renderResult.getByTestId(`${testPrefix}-${filterPrefix}-popoverList`);

const selectedFilterOptions = Array.from(filterList.querySelectorAll('ul>li')).reduce<
number[]
>((acc, curr, i) => {
if (curr.getAttribute('aria-checked') === 'true') {
acc.push(i);
}
return acc;
}, []);
const selectedFilterOptions = getAllByTestId(`${filterPrefix}-option`).reduce<number[]>(
(acc, curr, i) => {
if (curr.getAttribute('aria-checked') === 'true') {
acc.push(i);
}
return acc;
},
[]
);

expect(selectedFilterOptions).toEqual([1, 3, 5]);
});
Expand All @@ -686,16 +746,16 @@ describe('Response actions history', () => {

// re-open
userEvent.click(popoverButton);
const filterList = renderResult.getByTestId(`${testPrefix}-${filterPrefix}-popoverList`);

const selectedFilterOptions = Array.from(filterList.querySelectorAll('ul>li')).reduce<
number[]
>((acc, curr, i) => {
if (curr.getAttribute('aria-checked') === 'true') {
acc.push(i);
}
return acc;
}, []);
const selectedFilterOptions = getAllByTestId(`${filterPrefix}-option`).reduce<number[]>(
(acc, curr, i) => {
if (curr.getAttribute('aria-checked') === 'true') {
acc.push(i);
}
return acc;
},
[]
);

expect(selectedFilterOptions).toEqual([0, 1, 2]);
});
Expand Down Expand Up @@ -730,15 +790,15 @@ describe('Response actions history', () => {
}
});

const filterList = renderResult.getByTestId(`${testPrefix}-${filterPrefix}-popoverList`);
const selectedFilterOptions = Array.from(filterList.querySelectorAll('ul>li')).reduce<
number[]
>((acc, curr, i) => {
if (curr.getAttribute('aria-checked') === 'true') {
acc.push(i);
}
return acc;
}, []);
const selectedFilterOptions = getAllByTestId(`${filterPrefix}-option`).reduce<number[]>(
(acc, curr, i) => {
if (curr.getAttribute('aria-checked') === 'true') {
acc.push(i);
}
return acc;
},
[]
);

expect(selectedFilterOptions).toEqual([0, 1, 2, 4, 6, 8]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 2.0.
*/
import React, { useCallback, useMemo, useState } from 'react';
import type { HorizontalAlignment } from '@elastic/eui';

import {
EuiI18nNumber,
Expand All @@ -20,6 +19,7 @@ import {
EuiScreenReaderOnly,
EuiText,
EuiToolTip,
type HorizontalAlignment,
} from '@elastic/eui';
import { css, euiStyled } from '@kbn/kibana-react-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
Expand All @@ -33,6 +33,7 @@ import { getEmptyValue } from '../../../common/components/empty_value';
import { StatusBadge } from './components/status_badge';
import { useTestIdGenerator } from '../../hooks/use_test_id_generator';
import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../common/constants';
import { ResponseActionFileDownloadLink } from '../response_action_file_download_link';

const emptyValue = getEmptyValue();

Expand Down Expand Up @@ -137,6 +138,7 @@ export const useResponseActionsLogTable = ({
: undefined;

const command = getUiCommand(_command);
const isGetFileCommand = command === 'get-file';
const dataList = [
{
title: OUTPUT_MESSAGES.expandSection.placedAt,
Expand Down Expand Up @@ -169,6 +171,35 @@ export const useResponseActionsLogTable = ({
};
});

const getOutputContent = () => {
if (isExpired) {
return OUTPUT_MESSAGES.hasExpired(command);
}

if (!isCompleted) {
return OUTPUT_MESSAGES.isPending(command);
}

if (!wasSuccessful) {
return OUTPUT_MESSAGES.hasFailed(command);
}

if (isGetFileCommand) {
return (
<>
{OUTPUT_MESSAGES.wasSuccessful(command)}
<ResponseActionFileDownloadLink
action={item}
textSize="xs"
data-test-subj={getTestId('getFileDownloadLink')}
/>
</>
);
}

return OUTPUT_MESSAGES.wasSuccessful(command);
};

const outputList = [
{
title: (
Expand All @@ -177,13 +208,7 @@ export const useResponseActionsLogTable = ({
description: (
// codeblock for output
<StyledEuiCodeBlock data-test-subj={getTestId('details-tray-output')}>
{isExpired
? OUTPUT_MESSAGES.hasExpired(command)
: isCompleted
? wasSuccessful
? OUTPUT_MESSAGES.wasSuccessful(command)
: OUTPUT_MESSAGES.hasFailed(command)
: OUTPUT_MESSAGES.isPending(command)}
{getOutputContent()}
</StyledEuiCodeBlock>
),
},
Expand Down
Loading

0 comments on commit 43ffa96

Please sign in to comment.