mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Security Solution][Endpoint][Response Actions]Show file is truncated message if execute response metadata has that … (#154181)
## Summary Informs the user that the `execute` response full output file is truncated in case one of the compressed file exceeds size limit. <img width="1457" alt="Screenshot 2023-04-03 at 13 35 03" src="https://user-images.githubusercontent.com/1849116/229497936-2e50dbcd-6d9d-4684-aae0-06a0189f53b6.png"> **action parameters with a long command (no changes in this PR except adding some space after semicolon)** <img width="969" alt="Screenshot 2023-04-03 at 13 40 29" src="https://user-images.githubusercontent.com/1849116/229499048-740f250c-38db-4efe-8ee6-8bc80ca6e604.png"> <img width="1951" alt="Screenshot 2023-04-03 at 13 40 12" src="https://user-images.githubusercontent.com/1849116/229499034-189700c9-b67b-44c0-8867-f274e7a79f82.png"> ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) (https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [x] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers)
This commit is contained in:
parent
06f7a3529c
commit
8b68ce8558
10 changed files with 240 additions and 114 deletions
|
@ -232,6 +232,7 @@ export class EndpointActionGenerator extends BaseDataGenerator {
|
|||
[details.agents[0]]: this.generateExecuteActionResponseOutput({
|
||||
content: {
|
||||
output_file_id: getFileDownloadId(details, details.agents[0]),
|
||||
...overrides.outputs?.[details.agents[0]].content,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -310,11 +311,13 @@ export class EndpointActionGenerator extends BaseDataGenerator {
|
|||
content: {
|
||||
stdout: this.randomChoice([
|
||||
this.randomString(1280),
|
||||
this.randomString(3580),
|
||||
`-rw-r--r-- 1 elastic staff 458 Jan 26 09:10 doc.txt\
|
||||
-rw-r--r-- 1 elastic staff 298 Feb 2 09:10 readme.md`,
|
||||
]),
|
||||
stderr: this.randomChoice([
|
||||
this.randomString(1280),
|
||||
this.randomString(3580),
|
||||
`error line 1\
|
||||
error line 2\
|
||||
error line 3 that is quite very long and will be truncated, and should not be visible in the UI\
|
||||
|
@ -326,6 +329,8 @@ export class EndpointActionGenerator extends BaseDataGenerator {
|
|||
shell: 'bash',
|
||||
cwd: '/some/path',
|
||||
output_file_id: 'some-output-file-id',
|
||||
output_file_stdout_truncated: this.randomChoice([true, false]),
|
||||
output_file_stderr_truncated: this.randomChoice([true, false]),
|
||||
},
|
||||
},
|
||||
overrides
|
||||
|
|
|
@ -77,6 +77,11 @@ export interface ResponseActionExecuteOutputContent {
|
|||
/* The current working directory used when the command was executed */
|
||||
cwd: string;
|
||||
output_file_id: string;
|
||||
/** Informs whether the stdout/stderr files are
|
||||
* truncated due to size limitations (50 Mb each)
|
||||
* */
|
||||
output_file_stdout_truncated: boolean;
|
||||
output_file_stderr_truncated: boolean;
|
||||
}
|
||||
|
||||
export const ActivityLogItemTypes = {
|
||||
|
|
|
@ -7,94 +7,96 @@
|
|||
import type { AppContextTestRender } from '../../../common/mock/endpoint';
|
||||
import { createAppRootMockRenderer } from '../../../common/mock/endpoint';
|
||||
import type {
|
||||
ActionDetails,
|
||||
ResponseActionExecuteOutputContent,
|
||||
ResponseActionsExecuteParameters,
|
||||
ActionDetails,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import React from 'react';
|
||||
import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator';
|
||||
import {
|
||||
ExecuteActionHostResponseOutput,
|
||||
type ExecuteActionHostResponseOutputProps,
|
||||
} from './execute_action_host_response_output';
|
||||
ExecuteActionHostResponse,
|
||||
type ExecuteActionHostResponseProps,
|
||||
} from './execute_action_host_response';
|
||||
import { getEmptyValue } from '@kbn/cases-plugin/public/components/empty_value';
|
||||
|
||||
describe('When using the `ExecuteActionHostResponseOutput` component', () => {
|
||||
describe('When using the `ExecuteActionHostResponse` component', () => {
|
||||
let render: () => ReturnType<AppContextTestRender['render']>;
|
||||
let renderResult: ReturnType<AppContextTestRender['render']>;
|
||||
let renderProps: ExecuteActionHostResponseOutputProps;
|
||||
let renderProps: ExecuteActionHostResponseProps;
|
||||
const action = new EndpointActionGenerator('seed').generateActionDetails<
|
||||
ResponseActionExecuteOutputContent,
|
||||
ResponseActionsExecuteParameters
|
||||
>({ command: 'execute', agents: ['agent-a'] });
|
||||
|
||||
beforeEach(() => {
|
||||
const appTestContext = createAppRootMockRenderer();
|
||||
|
||||
renderProps = {
|
||||
action: new EndpointActionGenerator('seed').generateActionDetails<
|
||||
ResponseActionExecuteOutputContent,
|
||||
ResponseActionsExecuteParameters
|
||||
>({ command: 'execute' }),
|
||||
action,
|
||||
canAccessFileDownloadLink: true,
|
||||
'data-test-subj': 'test',
|
||||
};
|
||||
|
||||
render = () => {
|
||||
renderResult = appTestContext.render(<ExecuteActionHostResponseOutput {...renderProps} />);
|
||||
renderResult = appTestContext.render(<ExecuteActionHostResponse {...renderProps} />);
|
||||
return renderResult;
|
||||
};
|
||||
});
|
||||
|
||||
it('should show execute output and execute errors', async () => {
|
||||
render();
|
||||
expect(renderResult.getByTestId('test')).toBeTruthy();
|
||||
expect(renderResult.getByTestId('test-executeResponseOutput')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show execute output as `open`', async () => {
|
||||
it('should show execute output accordion as `open`', async () => {
|
||||
render();
|
||||
const accordionOutputButton = Array.from(
|
||||
renderResult.getByTestId('test').querySelectorAll('.euiAccordion')
|
||||
renderResult.getByTestId('test-executeResponseOutput').querySelectorAll('.euiAccordion')
|
||||
)[0];
|
||||
expect(accordionOutputButton.className).toContain('isOpen');
|
||||
});
|
||||
|
||||
it('should show `-` when no output content', async () => {
|
||||
it('should show `-` in output accordion when no output content', async () => {
|
||||
(renderProps.action as ActionDetails).outputs = {
|
||||
'agent-a': {
|
||||
type: 'json',
|
||||
content: {
|
||||
...(renderProps.action as ActionDetails)?.outputs?.['agent-a'].content,
|
||||
stdout: undefined,
|
||||
...(renderProps.action as ActionDetails).outputs?.[action.agents[0]].content,
|
||||
stdout: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
render();
|
||||
const accordionOutputButton = Array.from(
|
||||
renderResult.getByTestId('test').querySelectorAll('.euiAccordion')
|
||||
renderResult.getByTestId('test-executeResponseOutput').querySelectorAll('.euiAccordion')
|
||||
)[0];
|
||||
expect(accordionOutputButton.textContent).toContain(
|
||||
`Execution output (truncated)${getEmptyValue()}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should show `-` when no error content', async () => {
|
||||
it('should show `-` in error accordion when no error content', async () => {
|
||||
(renderProps.action as ActionDetails).outputs = {
|
||||
'agent-a': {
|
||||
type: 'json',
|
||||
content: {
|
||||
...(renderProps.action as ActionDetails)?.outputs?.['agent-a'].content,
|
||||
stderr: undefined,
|
||||
...(renderProps.action as ActionDetails).outputs?.[action.agents[0]].content,
|
||||
stderr: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
render();
|
||||
const accordionErrorButton = Array.from(
|
||||
renderResult.getByTestId('test').querySelectorAll('.euiAccordion')
|
||||
renderResult.getByTestId('test-executeResponseOutput').querySelectorAll('.euiAccordion')
|
||||
)[1];
|
||||
expect(accordionErrorButton.textContent).toContain(
|
||||
`Execution error (truncated)${getEmptyValue()}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should show nothing when no output in action details', () => {
|
||||
(renderProps.action as ActionDetails).outputs = {};
|
||||
it('should not show execute output accordions when no output in action details', () => {
|
||||
(renderProps.action as ActionDetails).outputs = undefined;
|
||||
render();
|
||||
expect(renderResult.queryByTestId('test')).toBeNull();
|
||||
expect(renderResult.queryByTestId('test-executeResponseOutput')).toBeNull();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { EuiFlexItem } from '@elastic/eui';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import type {
|
||||
ActionDetails,
|
||||
MaybeImmutable,
|
||||
ResponseActionExecuteOutputContent,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { EXECUTE_FILE_LINK_TITLE } from '../endpoint_response_actions_list/translations';
|
||||
import { ResponseActionFileDownloadLink } from '../response_action_file_download_link';
|
||||
import { ExecuteActionHostResponseOutput } from './execute_action_host_response_output';
|
||||
|
||||
export interface ExecuteActionHostResponseProps {
|
||||
action: MaybeImmutable<ActionDetails>;
|
||||
agentId?: string;
|
||||
canAccessFileDownloadLink: boolean;
|
||||
'data-test-subj'?: string;
|
||||
textSize?: 'xs' | 's';
|
||||
}
|
||||
|
||||
export const ExecuteActionHostResponse = memo<ExecuteActionHostResponseProps>(
|
||||
({
|
||||
action,
|
||||
agentId = action.agents[0],
|
||||
canAccessFileDownloadLink,
|
||||
textSize = 's',
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const outputContent = useMemo(
|
||||
() =>
|
||||
action.outputs &&
|
||||
action.outputs[agentId] &&
|
||||
(action.outputs[agentId].content as ResponseActionExecuteOutputContent),
|
||||
[action.outputs, agentId]
|
||||
);
|
||||
|
||||
const isTruncatedFile = useMemo<boolean>(
|
||||
() =>
|
||||
(outputContent?.output_file_stderr_truncated ||
|
||||
outputContent?.output_file_stdout_truncated) ??
|
||||
false,
|
||||
[outputContent]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<ResponseActionFileDownloadLink
|
||||
action={action}
|
||||
buttonTitle={EXECUTE_FILE_LINK_TITLE}
|
||||
canAccessFileDownloadLink={canAccessFileDownloadLink}
|
||||
isTruncatedFile={isTruncatedFile}
|
||||
data-test-subj={`${dataTestSubj}-getExecuteLink`}
|
||||
textSize={textSize}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{outputContent && (
|
||||
<ExecuteActionHostResponseOutput
|
||||
outputContent={outputContent}
|
||||
data-test-subj={`${dataTestSubj}-executeResponseOutput`}
|
||||
textSize={textSize}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ExecuteActionHostResponse.displayName = 'ExecuteActionHostResponse';
|
|
@ -4,15 +4,12 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { memo, useMemo } from 'react';
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiText, useGeneratedHtmlId } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type {
|
||||
ActionDetails,
|
||||
MaybeImmutable,
|
||||
ResponseActionExecuteOutputContent,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import type { ResponseActionExecuteOutputContent } from '../../../../common/endpoint/types';
|
||||
import { getEmptyValue } from '../../../common/components/empty_value';
|
||||
|
||||
const emptyValue = getEmptyValue();
|
||||
|
@ -84,45 +81,30 @@ const ExecutionActionOutputAccordion = memo<ExecuteActionOutputProps>(
|
|||
ExecutionActionOutputAccordion.displayName = 'ExecutionActionOutputAccordion';
|
||||
|
||||
export interface ExecuteActionHostResponseOutputProps {
|
||||
action: MaybeImmutable<ActionDetails>;
|
||||
agentId?: string;
|
||||
outputContent: ResponseActionExecuteOutputContent;
|
||||
'data-test-subj'?: string;
|
||||
textSize?: 's' | 'xs';
|
||||
}
|
||||
|
||||
export const ExecuteActionHostResponseOutput = memo<ExecuteActionHostResponseOutputProps>(
|
||||
({ action, agentId = action.agents[0], 'data-test-subj': dataTestSubj, textSize = 'xs' }) => {
|
||||
const outputContent = useMemo(
|
||||
() =>
|
||||
action.outputs &&
|
||||
action.outputs[agentId] &&
|
||||
(action.outputs[agentId].content as ResponseActionExecuteOutputContent),
|
||||
[action.outputs, agentId]
|
||||
);
|
||||
|
||||
if (!outputContent) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexItem data-test-subj={dataTestSubj}>
|
||||
<EuiSpacer size="m" />
|
||||
<ExecutionActionOutputAccordion
|
||||
content={outputContent.stdout}
|
||||
isTruncated={outputContent.stdout_truncated}
|
||||
initialIsOpen
|
||||
textSize={textSize}
|
||||
type="output"
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<ExecutionActionOutputAccordion
|
||||
content={outputContent.stderr}
|
||||
isTruncated={outputContent.stderr_truncated}
|
||||
textSize={textSize}
|
||||
type="error"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
({ outputContent, 'data-test-subj': dataTestSubj, textSize = 'xs' }) => (
|
||||
<EuiFlexItem data-test-subj={dataTestSubj}>
|
||||
<EuiSpacer size="m" />
|
||||
<ExecutionActionOutputAccordion
|
||||
content={outputContent.stdout.length ? outputContent.stdout : undefined}
|
||||
isTruncated={outputContent.stdout_truncated}
|
||||
initialIsOpen
|
||||
textSize={textSize}
|
||||
type="output"
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<ExecutionActionOutputAccordion
|
||||
content={outputContent.stderr.length ? outputContent.stderr : undefined}
|
||||
isTruncated={outputContent.stderr_truncated}
|
||||
textSize={textSize}
|
||||
type="error"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
);
|
||||
ExecuteActionHostResponseOutput.displayName = 'ExecuteActionHostResponseOutput';
|
||||
|
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { ExecuteActionHostResponseOutput } from './execute_action_host_response_output';
|
||||
export { ExecuteActionHostResponse } from './execute_action_host_response';
|
||||
|
|
|
@ -8,16 +8,13 @@
|
|||
import React, { memo, useMemo } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import type { ExecuteActionRequestBody } from '../../../../../common/endpoint/schema/actions';
|
||||
import { useConsoleActionSubmitter } from '../hooks/use_console_action_submitter';
|
||||
import type { ResponseActionExecuteOutputContent } from '../../../../../common/endpoint/types';
|
||||
import { useSendExecuteEndpoint } from '../../../hooks/response_actions/use_send_execute_endpoint_request';
|
||||
import type { ActionRequestComponentProps } from '../types';
|
||||
import { parsedExecuteTimeout } from '../lib/utils';
|
||||
import { ExecuteActionHostResponseOutput } from '../../endpoint_execute_action';
|
||||
import { ResponseActionFileDownloadLink } from '../../response_action_file_download_link';
|
||||
import { EXECUTE_FILE_LINK_TITLE } from '../../endpoint_response_actions_list/translations';
|
||||
import { ExecuteActionHostResponse } from '../../endpoint_execute_action';
|
||||
|
||||
export const ExecuteActionResult = memo<
|
||||
ActionRequestComponentProps<{
|
||||
|
@ -75,20 +72,12 @@ export const ExecuteActionResult = memo<
|
|||
{ defaultMessage: 'Command execution was successful.' }
|
||||
)}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<ResponseActionFileDownloadLink
|
||||
action={completedActionDetails}
|
||||
buttonTitle={EXECUTE_FILE_LINK_TITLE}
|
||||
canAccessFileDownloadLink={true}
|
||||
data-test-subj="consoleGetExecuteLink"
|
||||
textSize="s"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<ExecuteActionHostResponseOutput
|
||||
<ExecuteActionHostResponse
|
||||
action={completedActionDetails}
|
||||
canAccessFileDownloadLink={true}
|
||||
agentId={command.commandDefinition?.meta?.endpointId}
|
||||
data-test-subj="consoleExecuteResponseOutput"
|
||||
textSize="s"
|
||||
data-test-subj="console"
|
||||
/>
|
||||
</ResultComponent>
|
||||
);
|
||||
|
|
|
@ -9,11 +9,11 @@ import React, { memo, useMemo } from 'react';
|
|||
import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem, EuiDescriptionList } from '@elastic/eui';
|
||||
import { css, euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { useUserPrivileges } from '../../../../common/components/user_privileges';
|
||||
import { OUTPUT_MESSAGES, EXECUTE_FILE_LINK_TITLE } from '../translations';
|
||||
import { OUTPUT_MESSAGES } from '../translations';
|
||||
import { getUiCommand } from './hooks';
|
||||
import { useTestIdGenerator } from '../../../hooks/use_test_id_generator';
|
||||
import { ResponseActionFileDownloadLink } from '../../response_action_file_download_link';
|
||||
import { ExecuteActionHostResponseOutput } from '../../endpoint_execute_action';
|
||||
import { ExecuteActionHostResponse } from '../../endpoint_execute_action';
|
||||
import { getEmptyValue } from '../../../../common/components/empty_value';
|
||||
|
||||
import { type ActionDetails, type MaybeImmutable } from '../../../../../common/endpoint/types';
|
||||
|
@ -119,22 +119,14 @@ const OutputContent = memo<{ action: MaybeImmutable<ActionDetails>; 'data-test-s
|
|||
{action.agents.map((agentId) => (
|
||||
<div key={agentId}>
|
||||
{OUTPUT_MESSAGES.wasSuccessful(command)}
|
||||
<EuiFlexItem>
|
||||
<ResponseActionFileDownloadLink
|
||||
action={action}
|
||||
buttonTitle={EXECUTE_FILE_LINK_TITLE}
|
||||
canAccessFileDownloadLink={
|
||||
canAccessEndpointActionsLogManagement || canReadActionsLogManagement
|
||||
}
|
||||
data-test-subj={getTestId('getExecuteLink')}
|
||||
textSize="xs"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<ExecuteActionHostResponseOutput
|
||||
<ExecuteActionHostResponse
|
||||
action={action}
|
||||
agentId={agentId}
|
||||
data-test-subj={getTestId('executeResponseOutput')}
|
||||
canAccessFileDownloadLink={
|
||||
canAccessEndpointActionsLogManagement || canReadActionsLogManagement
|
||||
}
|
||||
textSize="xs"
|
||||
data-test-subj={getTestId('actionsLogTray')}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
@ -160,7 +152,7 @@ export const ActionsLogExpandedTray = memo<{
|
|||
() =>
|
||||
parameters
|
||||
? Object.entries(parameters).map(([key, value]) => {
|
||||
return `${key}:${value}`;
|
||||
return `${key}: ${value}`;
|
||||
})
|
||||
: undefined,
|
||||
[parameters]
|
||||
|
|
|
@ -16,6 +16,9 @@ import React from 'react';
|
|||
import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator';
|
||||
import {
|
||||
FILE_NO_LONGER_AVAILABLE_MESSAGE,
|
||||
FILE_TRUNCATED_MESSAGE,
|
||||
FILE_DELETED_MESSAGE,
|
||||
FILE_PASSCODE_INFO_MESSAGE,
|
||||
ResponseActionFileDownloadLink,
|
||||
type ResponseActionFileDownloadLinkProps,
|
||||
} from './response_action_file_download_link';
|
||||
|
@ -42,6 +45,7 @@ describe('When using the `ResponseActionFileDownloadLink` component', () => {
|
|||
>({ command: 'get-file' }),
|
||||
'data-test-subj': 'test',
|
||||
canAccessFileDownloadLink: true,
|
||||
isTruncatedFile: false,
|
||||
};
|
||||
|
||||
render = () => {
|
||||
|
@ -58,10 +62,10 @@ describe('When using the `ResponseActionFileDownloadLink` component', () => {
|
|||
|
||||
expect(renderResult.getByTestId('test-downloadButton')).not.toBeNull();
|
||||
expect(renderResult.getByTestId('test-passcodeMessage')).toHaveTextContent(
|
||||
'(ZIP file passcode: elastic)'
|
||||
FILE_PASSCODE_INFO_MESSAGE
|
||||
);
|
||||
expect(renderResult.getByTestId('test-fileDeleteMessage')).toHaveTextContent(
|
||||
'Files are periodically deleted to clear storage space. Download and save file locally if needed.'
|
||||
FILE_DELETED_MESSAGE
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -114,6 +118,20 @@ describe('When using the `ResponseActionFileDownloadLink` component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should show file is truncated if execute file output is truncated', async () => {
|
||||
renderProps.isTruncatedFile = true;
|
||||
render();
|
||||
await waitFor(() => {
|
||||
expect(apiMocks.responseProvider.fileInfo).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(renderResult.getByTestId('test-fileTruncatedMessage')).toHaveTextContent(
|
||||
FILE_TRUNCATED_MESSAGE
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show file no longer available message if file info api returns 404', async () => {
|
||||
const error = { message: 'not found', response: { status: 404 } } as IHttpFetchError;
|
||||
|
||||
|
|
|
@ -6,8 +6,15 @@
|
|||
*/
|
||||
|
||||
import React, { memo, useMemo, type CSSProperties } from 'react';
|
||||
import { EuiButtonEmpty, EuiSkeletonText, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiSkeletonText,
|
||||
EuiText,
|
||||
EuiSpacer,
|
||||
EuiIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { getFileDownloadId } from '../../../../common/endpoint/service/response_actions/get_file_download_id';
|
||||
|
@ -33,18 +40,72 @@ export const FILE_NO_LONGER_AVAILABLE_MESSAGE = i18n.translate(
|
|||
{ defaultMessage: 'File has expired and is no longer available for download.' }
|
||||
);
|
||||
|
||||
export const FILE_DELETED_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.responseActionFileDownloadLink.deleteNotice',
|
||||
{
|
||||
defaultMessage:
|
||||
'Files are periodically deleted to clear storage space. Download and save file locally if needed.',
|
||||
}
|
||||
);
|
||||
|
||||
export const FILE_PASSCODE_INFO_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.responseActionFileDownloadLink.passcodeInfo',
|
||||
{
|
||||
defaultMessage: '(ZIP file passcode: {passcode}).',
|
||||
values: {
|
||||
passcode: 'elastic',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export const FILE_TRUNCATED_MESSAGE = i18n.translate(
|
||||
'xpack.securitySolution.responseActionFileDownloadLink.fileTruncated',
|
||||
{
|
||||
defaultMessage:
|
||||
'Output data in the provided zip file is truncated due to file size limitations.',
|
||||
}
|
||||
);
|
||||
|
||||
const FileDownloadLinkContainer = styled.div`
|
||||
& > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
`;
|
||||
|
||||
interface TruncatedTextInfoProps {
|
||||
textSize: Required<ResponseActionFileDownloadLinkProps['textSize']>;
|
||||
'data-test-subj'?: string;
|
||||
}
|
||||
|
||||
const TruncatedTextInfo = memo<TruncatedTextInfoProps>(
|
||||
({ textSize, 'data-test-subj': dataTestSubj }) => {
|
||||
const alertIconSize = useMemo(() => (textSize === 'xs' ? 's' : 'm'), [textSize]);
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiFlexGroup gutterSize="s" justifyContent="flexStart" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size={alertIconSize} type="warning" color="warning" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiText size={textSize} data-test-subj={dataTestSubj}>
|
||||
{FILE_TRUNCATED_MESSAGE}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
TruncatedTextInfo.displayName = 'TruncatedTextInfo';
|
||||
export interface ResponseActionFileDownloadLinkProps {
|
||||
action: MaybeImmutable<ActionDetails>;
|
||||
/** If left undefined, the first agent that the action was sent to will be used */
|
||||
agentId?: string;
|
||||
buttonTitle?: string;
|
||||
canAccessFileDownloadLink: boolean;
|
||||
isTruncatedFile?: boolean;
|
||||
'data-test-subj'?: string;
|
||||
textSize?: 's' | 'xs';
|
||||
}
|
||||
|
@ -61,8 +122,9 @@ export const ResponseActionFileDownloadLink = memo<ResponseActionFileDownloadLin
|
|||
agentId,
|
||||
buttonTitle = DEFAULT_BUTTON_TITLE,
|
||||
canAccessFileDownloadLink,
|
||||
'data-test-subj': dataTestSubj,
|
||||
isTruncatedFile = false,
|
||||
textSize = 's',
|
||||
'data-test-subj': dataTestSubj,
|
||||
}) => {
|
||||
const action = _action as ActionDetails; // cast to remove `Immutable`
|
||||
const getTestId = useTestIdGenerator(dataTestSubj);
|
||||
|
@ -123,20 +185,17 @@ export const ResponseActionFileDownloadLink = memo<ResponseActionFileDownloadLin
|
|||
data-test-subj={getTestId('passcodeMessage')}
|
||||
className="eui-displayInline"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActionFileDownloadLink.passcodeInfo"
|
||||
defaultMessage="(ZIP file passcode: {passcode})."
|
||||
values={{
|
||||
passcode: 'elastic',
|
||||
}}
|
||||
/>
|
||||
{FILE_PASSCODE_INFO_MESSAGE}
|
||||
</EuiText>
|
||||
<EuiText size={textSize} data-test-subj={getTestId('fileDeleteMessage')}>
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.responseActionFileDownloadLink.deleteNotice"
|
||||
defaultMessage="Files are periodically deleted to clear storage space. Download and save file locally if needed."
|
||||
/>
|
||||
{FILE_DELETED_MESSAGE}
|
||||
</EuiText>
|
||||
{isTruncatedFile && (
|
||||
<TruncatedTextInfo
|
||||
textSize={textSize}
|
||||
data-test-subj={getTestId('fileTruncatedMessage')}
|
||||
/>
|
||||
)}
|
||||
</FileDownloadLinkContainer>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue