Skip to content

Commit

Permalink
[Security Solution][Endpoint][Response Actions] Update deleted file m…
Browse files Browse the repository at this point in the history
…essage for file link (#144454)

* update deleted file message for file link

fixes elastic/security-team/issues/5310

* update tests

fixes elastic/security-team/issues/5310

* update types

* update test

fixes elastic/security-team/issues/5310
  • Loading branch information
ashokaditya authored Nov 3, 2022
1 parent f065d66 commit 4e4955b
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
* 2.0.
*/

export const resolvePathVariables = (path: string, variables: { [K: string]: string | number }) =>
export const resolvePathVariables = (
path: string,
variables: { [K: string]: string | number }
): string =>
Object.keys(variables).reduce((acc, paramName) => {
return acc.replace(new RegExp(`\{${paramName}\}`, 'g'), String(variables[paramName]));
}, path);
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ import {
type AppContextTestRender,
} from '../../../common/mock/endpoint';
import { ResponseActionsLog } from './response_actions_log';
import type { ActionListApiResponse } from '../../../../common/endpoint/types';
import type {
ActionFileInfoApiResponse,
ActionListApiResponse,
} from '../../../../common/endpoint/types';
import { MANAGEMENT_PATH } from '../../../../common/constants';
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';
import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks';
import { waitFor } from '@testing-library/react';

let mockUseGetEndpointActionList: {
isFetched?: boolean;
Expand Down Expand Up @@ -116,6 +121,19 @@ jest.mock('../../hooks/endpoint/use_get_endpoints_list');

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

let mockUseGetFileInfo: {
isFetching?: boolean;
error?: Partial<IHttpFetchError> | null;
data?: ActionFileInfoApiResponse;
};
jest.mock('../../hooks/response_actions/use_get_file_info', () => {
const original = jest.requireActual('../../hooks/response_actions/use_get_file_info');
return {
...original,
useGetFileInfo: () => mockUseGetFileInfo,
};
});

const mockUseGetEndpointsList = useGetEndpointsList as jest.Mock;

describe('Response actions history', () => {
Expand All @@ -131,6 +149,7 @@ describe('Response actions history', () => {
let renderResult: ReturnType<typeof render>;
let history: AppContextTestRender['history'];
let mockedContext: AppContextTestRender;
let apiMocks: ReturnType<typeof responseActionsHttpMocks>;

const refetchFunction = jest.fn();
const baseMockedActionList = {
Expand Down Expand Up @@ -219,6 +238,10 @@ describe('Response actions history', () => {
});

describe('With Data', () => {
beforeEach(() => {
apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http);
});

it('should show table when there is data', async () => {
render();

Expand Down Expand Up @@ -422,18 +445,62 @@ describe('Response actions history', () => {
data: await getActionListMock({ actionCount: 1, commands: ['get-file'] }),
};

mockUseGetFileInfo = {
isFetching: false,
error: null,
data: apiMocks.responseProvider.fileInfo(),
};

render();
const { getByTestId } = renderResult;

const { getByTestId } = renderResult;
const expandButton = getByTestId(`${testPrefix}-expand-button`);
userEvent.click(expandButton);

await waitFor(() => {
expect(apiMocks.responseProvider.fileInfo).toHaveBeenCalled();
});

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

it('should show file unavailable for download for `get-file` action WITH file operation permission when file is deleted', async () => {
mockUseGetEndpointActionList = {
...baseMockedActionList,
data: await getActionListMock({ actionCount: 1, commands: ['get-file'] }),
};

const fileInfo = apiMocks.responseProvider.fileInfo();
fileInfo.data.status = 'DELETED';

apiMocks.responseProvider.fileInfo.mockReturnValue(fileInfo);

mockUseGetFileInfo = {
isFetching: false,
error: null,
data: apiMocks.responseProvider.fileInfo(),
};

render();

const { getByTestId } = renderResult;
const expandButton = getByTestId(`${testPrefix}-expand-button`);
userEvent.click(expandButton);

await waitFor(() => {
expect(apiMocks.responseProvider.fileInfo).toHaveBeenCalled();
});

const unavailableText = getByTestId(
`${testPrefix}-getFileDownloadLink-fileNoLongerAvailable`
);
expect(unavailableText).toBeTruthy();
});

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('When using the `ResponseActionFileDownloadLink` component', () => {
action: new EndpointActionGenerator('seed').generateActionDetails<
ResponseActionGetFileOutputContent,
ResponseActionGetFileParameters
>({ command: 'get-file', completedAt: new Date().toISOString() }),
>({ command: 'get-file' }),
'data-test-subj': 'test',
};

Expand All @@ -56,18 +56,24 @@ describe('When using the `ResponseActionFileDownloadLink` component', () => {
};
});

it('should show download button if file is available', () => {
it('should show download button if file is available', async () => {
render();
await waitFor(() => {
expect(apiMocks.responseProvider.fileInfo).toHaveBeenCalled();
});

expect(renderResult.getByTestId('test-downloadButton')).not.toBeNull();
expect(renderResult.getByTestId('test-passcodeMessage')).toHaveTextContent(
'(ZIP file passcode: elastic)'
);
});

it('should display custom button label', () => {
it('should display custom button label', async () => {
renderProps.buttonTitle = 'hello';
render();
await waitFor(() => {
expect(apiMocks.responseProvider.fileInfo).toHaveBeenCalled();
});

expect(renderResult.getByTestId('test-downloadButton')).toHaveTextContent('hello');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { resolvePathVariables } from '../../../common/utils/resolve_path_variables';
import { FormattedError } from '../formatted_error';
import { useGetFileInfo } from '../../hooks/response_actions/use_get_file_info';
Expand All @@ -36,7 +35,7 @@ const DEFAULT_BUTTON_TITLE = i18n.translate(

export const FILE_NO_LONGER_AVAILABLE_MESSAGE = i18n.translate(
'xpack.securitySolution.responseActionFileDownloadLink.fileNoLongerAvailable',
{ defaultMessage: 'File is no longer available for download.' }
{ defaultMessage: 'File has expired and is no longer available for download.' }
);

export interface ResponseActionFileDownloadLinkProps {
Expand Down Expand Up @@ -65,16 +64,11 @@ export const ResponseActionFileDownloadLink = memo<ResponseActionFileDownloadLin
const getTestId = useTestIdGenerator(dataTestSubj);
const { canWriteFileOperations } = useUserPrivileges().endpointPrivileges;

// We don't need to call the file info API every time, especially if this component is used from the
// console, where the link is displayed within a short time. So we only do the API call if the
// action was completed more than 2 days ago.
const checkIfStillAvailable = useMemo(() => {
return (
action.isCompleted && action.wasSuccessful && moment().diff(action.completedAt, 'days') > 2
);
}, [action.completedAt, action.isCompleted, action.wasSuccessful]);
const shouldFetchFileInfo: boolean = useMemo(() => {
return action.isCompleted && action.wasSuccessful;
}, [action.isCompleted, action.wasSuccessful]);

const downloadUrl = useMemo(() => {
const downloadUrl: string = useMemo(() => {
return resolvePathVariables(ACTION_AGENT_FILE_DOWNLOAD_ROUTE, {
action_id: action.id,
agent_id: agentId ?? action.agents[0],
Expand All @@ -86,7 +80,7 @@ export const ResponseActionFileDownloadLink = memo<ResponseActionFileDownloadLin
data: fileInfo,
error,
} = useGetFileInfo(action, undefined, {
enabled: canWriteFileOperations && checkIfStillAvailable,
enabled: canWriteFileOperations && shouldFetchFileInfo,
});

if (!canWriteFileOperations || !action.isCompleted || !action.wasSuccessful) {
Expand All @@ -100,7 +94,7 @@ export const ResponseActionFileDownloadLink = memo<ResponseActionFileDownloadLin
// Check if file is no longer available
if ((error && error?.response?.status === 404) || fileInfo?.data.status === 'DELETED') {
return (
<EuiText size="s" data-test-subj={getTestId('fileNoLongerAvailable')}>
<EuiText size={textSize} data-test-subj={getTestId('fileNoLongerAvailable')}>
{FILE_NO_LONGER_AVAILABLE_MESSAGE}
</EuiText>
);
Expand Down

0 comments on commit 4e4955b

Please sign in to comment.