[Security solution] [Endpoint] Improve policy response info debugging (#134351)

* [Security Solution] bubble up policy response errors

* Adds policy troubleshooting links in doc links package

* Bubble up certain errors on policy response like full_disk_access error

* Fixes unit test and removes unused component

* Removes unused style

* Use link instead of button for documentation links

Co-authored-by: Joey F. Poon <joey.poon@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
David Sánchez 2022-06-16 16:35:16 +02:00 committed by GitHub
parent d071e52c51
commit 60be4e3db8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 536 additions and 290 deletions

View file

@ -343,6 +343,9 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
trustedApps: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/trusted-apps-ov.html`,
eventFilters: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/event-filters.html`,
blocklist: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/blocklist.html`,
policyResponseTroubleshooting: {
full_disk_access: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/deploy-elastic-endpoint.html#enable-fda-endpoint`,
},
},
query: {
eql: `${ELASTICSEARCH_DOCS}eql.html`,

View file

@ -248,6 +248,9 @@ export interface DocLinks {
readonly trustedApps: string;
readonly eventFilters: string;
readonly blocklist: string;
readonly policyResponseTroubleshooting: {
full_disk_access: string;
};
};
readonly query: {
readonly eql: string;

View file

@ -8,7 +8,9 @@
import React, { memo, useCallback } from 'react';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n-react';
import { DocLinksStart } from '@kbn/core/public';
import { EuiHealth, EuiText, EuiTreeView, EuiNotificationBadge } from '@elastic/eui';
import { useKibana } from '../../../common/lib/kibana';
import {
HostPolicyResponseActionStatus,
HostPolicyResponseAppliedAction,
@ -17,7 +19,7 @@ import {
ImmutableArray,
ImmutableObject,
} from '../../../../common/endpoint/types';
import { formatResponse } from './policy_response_friendly_names';
import { formatResponse, PolicyResponseActionFormatter } from './policy_response_friendly_names';
import { PolicyResponseActionItem } from './policy_response_action_item';
// Most of them are needed in order to display large react nodes (PolicyResponseActionItem) in child levels.
@ -59,6 +61,7 @@ export const PolicyResponse = memo(
policyResponseActions,
policyResponseAttentionCount,
}: PolicyResponseProps) => {
const { docLinks } = useKibana().services;
const getEntryIcon = useCallback(
(status: HostPolicyResponseActionStatus, unsuccessCounts: number) =>
status === HostPolicyResponseActionStatus.success ? (
@ -88,6 +91,12 @@ export const PolicyResponse = memo(
(currentAction) => currentAction.name === actionKey
) as ImmutableObject<HostPolicyResponseAppliedAction>;
const policyResponseActionFormatter = new PolicyResponseActionFormatter(
action || {},
docLinks.links.securitySolution.policyResponseTroubleshooting[
action.name as keyof DocLinksStart['links']['securitySolution']['policyResponseTroubleshooting']
]
);
return {
label: (
<EuiText
@ -99,7 +108,7 @@ export const PolicyResponse = memo(
}
data-test-subj="endpointPolicyResponseAction"
>
{formatResponse(actionKey)}
{policyResponseActionFormatter.title}
</EuiText>
),
id: actionKey,
@ -116,11 +125,7 @@ export const PolicyResponse = memo(
{
label: (
<PolicyResponseActionItem
status={action.status}
actionTitle={action.name}
actionMessage={action.message}
// actionButtonLabel="Do something" // TODO
// actionButtonOnClick={() => {}} // TODO
policyResponseActionFormatter={policyResponseActionFormatter}
/>
),
id: `action_message_${actionKey}`,
@ -135,7 +140,11 @@ export const PolicyResponse = memo(
};
});
},
[getEntryIcon, policyResponseActions]
[
docLinks.links.securitySolution.policyResponseTroubleshooting,
getEntryIcon,
policyResponseActions,
]
);
const getResponseConfigs = useCallback(

View file

@ -7,8 +7,8 @@
import React, { memo } from 'react';
import styled from 'styled-components';
import { EuiButton, EuiCallOut, EuiText, EuiSpacer } from '@elastic/eui';
import { HostPolicyResponseActionStatus } from '../../../../common/endpoint/types';
import { EuiLink, EuiCallOut, EuiText } from '@elastic/eui';
import { PolicyResponseActionFormatter } from './policy_response_friendly_names';
const StyledEuiCallout = styled(EuiCallOut)`
padding: ${({ theme }) => theme.eui.paddingSizes.s};
@ -19,39 +19,36 @@ const StyledEuiCallout = styled(EuiCallOut)`
`;
interface PolicyResponseActionItemProps {
status: HostPolicyResponseActionStatus;
actionTitle: string;
actionMessage: string;
actionButtonLabel?: string;
actionButtonOnClick?: () => void;
policyResponseActionFormatter: PolicyResponseActionFormatter;
}
/**
* A policy response action item
*/
export const PolicyResponseActionItem = memo(
({
status,
actionTitle,
actionMessage,
actionButtonLabel,
actionButtonOnClick,
}: PolicyResponseActionItemProps) => {
return status !== HostPolicyResponseActionStatus.success &&
status !== HostPolicyResponseActionStatus.unsupported ? (
<StyledEuiCallout title={actionTitle} color="danger" iconType="alert">
({ policyResponseActionFormatter }: PolicyResponseActionItemProps) => {
return policyResponseActionFormatter.hasError ? (
<StyledEuiCallout
title={policyResponseActionFormatter.errorTitle}
color="danger"
iconType="alert"
data-test-subj="endpointPolicyResponseErrorCallOut"
>
<EuiText size="s" className="action-message" data-test-subj="endpointPolicyResponseMessage">
{actionMessage}
{policyResponseActionFormatter.errorDescription}
{policyResponseActionFormatter.linkText && policyResponseActionFormatter.linkUrl && (
<EuiLink
target="_blank"
href={`${policyResponseActionFormatter.linkUrl}`}
data-test-subj="endpointPolicyResponseErrorCallOutLink"
>
{policyResponseActionFormatter.linkText}
</EuiLink>
)}
</EuiText>
<EuiSpacer size="s" />
{actionButtonLabel && actionButtonOnClick && (
<EuiButton onClick={actionButtonOnClick} color="danger">
{actionButtonLabel}
</EuiButton>
)}
</StyledEuiCallout>
) : (
<EuiText size="xs" data-test-subj="endpointPolicyResponseMessage">
{actionMessage}
{policyResponseActionFormatter.description || policyResponseActionFormatter.title}
</EuiText>
);
}

View file

@ -6,270 +6,418 @@
*/
import { i18n } from '@kbn/i18n';
import {
HostPolicyResponseActionStatus,
HostPolicyResponseAppliedAction,
ImmutableObject,
} from '../../../../common/endpoint/types';
const policyResponses: Array<[string, string]> = [
[
'configure_dns_events',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.configure_dns_events', {
defaultMessage: 'Configure DNS Events',
}),
],
[
'configure_elasticsearch_connection',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_elasticsearch_connection',
{ defaultMessage: 'Configure Elasticsearch Connection' }
),
],
[
'configure_file_events',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.configure_file_events', {
defaultMessage: 'Configure File Events',
}),
],
[
'configure_imageload_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_imageload_events',
{ defaultMessage: 'Configure Image Load Events' }
),
],
[
'configure_kernel',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.configure_kernel', {
defaultMessage: 'Configure Kernel',
}),
],
[
'configure_logging',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.configure_logging', {
defaultMessage: 'Configure Logging',
}),
],
[
'configure_malware',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.configure_malware', {
defaultMessage: 'Configure Malware',
}),
],
[
'configure_network_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_network_events',
{ defaultMessage: 'Configure Network Events' }
),
],
[
'configure_process_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_process_events',
{ defaultMessage: 'Configure Process Events' }
),
],
[
'configure_registry_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_registry_events',
{ defaultMessage: 'Configure Registry Events' }
),
],
[
'configure_security_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_security_events',
{ defaultMessage: 'Configure Security Events' }
),
],
[
'connect_kernel',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.connect_kernel', {
defaultMessage: 'Connect Kernel',
}),
],
[
'detect_async_image_load_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_async_image_load_events',
{ defaultMessage: 'Detect Async Image Load Events' }
),
],
[
'detect_file_open_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_file_open_events',
{ defaultMessage: 'Detect File Open Events' }
),
],
[
'detect_file_write_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_file_write_events',
{ defaultMessage: 'Detect File Write Events' }
),
],
[
'detect_network_events',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.detect_network_events', {
defaultMessage: 'Detect Network Events',
}),
],
[
'detect_process_events',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.detect_process_events', {
defaultMessage: 'Detect Process Events',
}),
],
[
'detect_registry_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_registry_events',
{ defaultMessage: 'Detect Registry Events' }
),
],
[
'detect_sync_image_load_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_sync_image_load_events',
{ defaultMessage: 'Detect Sync Image Load Events' }
),
],
[
'download_global_artifacts',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.download_global_artifacts',
{ defaultMessage: 'Download Global Artifacts' }
),
],
[
'download_user_artifacts',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.download_user_artifacts',
{ defaultMessage: 'Download User Artifacts' }
),
],
[
'load_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.load_config', {
defaultMessage: 'Load Config',
}),
],
[
'load_malware_model',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.load_malware_model', {
defaultMessage: 'Load Malware Model',
}),
],
[
'read_elasticsearch_config',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.read_elasticsearch_config',
{ defaultMessage: 'Read Elasticsearch Config' }
),
],
[
'read_events_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.read_events_config', {
defaultMessage: 'Read Events Config',
}),
],
[
'read_kernel_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.read_kernel_config', {
defaultMessage: 'Read Kernel Config',
}),
],
[
'read_logging_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.read_logging_config', {
defaultMessage: 'Read Logging Config',
}),
],
[
'read_malware_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.read_malware_config', {
defaultMessage: 'Read Malware Config',
}),
],
[
'workflow',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.workflow', {
defaultMessage: 'Workflow',
}),
],
];
type PolicyResponseSections =
| 'logging'
| 'streaming'
| 'malware'
| 'events'
| 'memory_protection'
| 'behavior_protection';
const responseMap = new Map<string, string>(policyResponses);
// Additional values used in the Policy Response UI
responseMap.set(
'success',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.success', {
defaultMessage: 'Success',
})
);
responseMap.set(
'warning',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.warning', {
defaultMessage: 'Warning',
})
);
responseMap.set(
'failure',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.failed', {
defaultMessage: 'Failed',
})
);
responseMap.set(
'logging',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.logging', {
defaultMessage: 'Logging',
})
);
responseMap.set(
'streaming',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.streaming', {
defaultMessage: 'Streaming',
})
);
responseMap.set(
'malware',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.malware', {
defaultMessage: 'Malware',
})
);
responseMap.set(
'events',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.events', {
defaultMessage: 'Events',
})
);
responseMap.set(
'memory_protection',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.memory_protection', {
defaultMessage: 'Memory Threat',
})
);
responseMap.set(
'behavior_protection',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.behavior_protection', {
defaultMessage: 'Malicious Behavior',
})
const policyResponseSections = Object.freeze(
new Map<PolicyResponseSections | string, string>([
[
'logging',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.logging', {
defaultMessage: 'Logging',
}),
],
[
'streaming',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.streaming', {
defaultMessage: 'Streaming',
}),
],
[
'malware',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.malware', {
defaultMessage: 'Malware',
}),
],
[
'events',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.events', {
defaultMessage: 'Events',
}),
],
[
'memory_protection',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.memory_protection', {
defaultMessage: 'Memory Threat',
}),
],
[
'behavior_protection',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.behavior_protection', {
defaultMessage: 'Malicious Behavior',
}),
],
])
);
/**
* Maps a server provided value to corresponding i18n'd string.
*/
export function formatResponse(responseString: string) {
if (responseMap.has(responseString)) {
return responseMap.get(responseString);
export function formatResponse(responseString: PolicyResponseSections | string) {
if (policyResponseSections.has(responseString)) {
return policyResponseSections.get(responseString);
}
// Its possible for the UI to receive an Action name that it does not yet have a translation,
// thus we generate a label for it here by making it more user fiendly
responseMap.set(
policyResponseSections.set(
responseString,
responseString.replace(/_/g, ' ').replace(/\b(\w)/g, (m) => m.toUpperCase())
);
return responseMap.get(responseString);
return policyResponseSections.get(responseString);
}
type PolicyResponseAction =
| 'configure_dns_events'
| 'configure_dns_events'
| 'configure_elasticsearch_connection'
| 'configure_file_events'
| 'configure_imageload_events'
| 'configure_kernel'
| 'configure_logging'
| 'configure_malware'
| 'configure_network_events'
| 'configure_process_events'
| 'configure_registry_events'
| 'configure_security_events'
| 'connect_kernel'
| 'detect_async_image_load_events'
| 'detect_file_open_events'
| 'detect_file_write_events'
| 'detect_network_events'
| 'detect_process_events'
| 'detect_registry_events'
| 'detect_sync_image_load_events'
| 'download_global_artifacts'
| 'download_user_artifacts'
| 'load_config'
| 'load_malware_model'
| 'read_elasticsearch_config'
| 'read_events_config'
| 'read_kernel_config'
| 'read_logging_config'
| 'read_malware_config'
| 'workflow'
| 'full_disk_access';
const policyResponseTitles = Object.freeze(
new Map<PolicyResponseAction | string, string>([
[
'configure_dns_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_dns_events',
{
defaultMessage: 'Configure DNS Events',
}
),
],
[
'configure_elasticsearch_connection',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_elasticsearch_connection',
{ defaultMessage: 'Configure Elasticsearch Connection' }
),
],
[
'configure_file_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_file_events',
{
defaultMessage: 'Configure File Events',
}
),
],
[
'configure_imageload_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_imageload_events',
{ defaultMessage: 'Configure Image Load Events' }
),
],
[
'configure_kernel',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.configure_kernel', {
defaultMessage: 'Configure Kernel',
}),
],
[
'configure_logging',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.configure_logging', {
defaultMessage: 'Configure Logging',
}),
],
[
'configure_malware',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.configure_malware', {
defaultMessage: 'Configure Malware',
}),
],
[
'configure_network_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_network_events',
{ defaultMessage: 'Configure Network Events' }
),
],
[
'configure_process_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_process_events',
{ defaultMessage: 'Configure Process Events' }
),
],
[
'configure_registry_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_registry_events',
{ defaultMessage: 'Configure Registry Events' }
),
],
[
'configure_security_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.configure_security_events',
{ defaultMessage: 'Configure Security Events' }
),
],
[
'connect_kernel',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.connect_kernel', {
defaultMessage: 'Connect Kernel',
}),
],
[
'detect_async_image_load_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_async_image_load_events',
{ defaultMessage: 'Detect Async Image Load Events' }
),
],
[
'detect_file_open_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_file_open_events',
{ defaultMessage: 'Detect File Open Events' }
),
],
[
'detect_file_write_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_file_write_events',
{ defaultMessage: 'Detect File Write Events' }
),
],
[
'detect_network_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_network_events',
{
defaultMessage: 'Detect Network Events',
}
),
],
[
'detect_process_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_process_events',
{
defaultMessage: 'Detect Process Events',
}
),
],
[
'detect_registry_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_registry_events',
{ defaultMessage: 'Detect Registry Events' }
),
],
[
'detect_sync_image_load_events',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.detect_sync_image_load_events',
{ defaultMessage: 'Detect Sync Image Load Events' }
),
],
[
'download_global_artifacts',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.download_global_artifacts',
{ defaultMessage: 'Download Global Artifacts' }
),
],
[
'download_user_artifacts',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.download_user_artifacts',
{ defaultMessage: 'Download User Artifacts' }
),
],
[
'load_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.load_config', {
defaultMessage: 'Load Config',
}),
],
[
'load_malware_model',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.load_malware_model', {
defaultMessage: 'Load Malware Model',
}),
],
[
'read_elasticsearch_config',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.read_elasticsearch_config',
{ defaultMessage: 'Read Elasticsearch Config' }
),
],
[
'read_events_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.read_events_config', {
defaultMessage: 'Read Events Config',
}),
],
[
'read_kernel_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.read_kernel_config', {
defaultMessage: 'Read Kernel Config',
}),
],
[
'read_logging_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.read_logging_config', {
defaultMessage: 'Read Logging Config',
}),
],
[
'read_malware_config',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.read_malware_config', {
defaultMessage: 'Read Malware Config',
}),
],
[
'workflow',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.workflow', {
defaultMessage: 'Workflow',
}),
],
[
'full_disk_access',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.full_disk_access', {
defaultMessage: 'Full Disk Access',
}),
],
])
);
type PolicyResponseStatus = `${HostPolicyResponseActionStatus}`;
const policyResponseStatuses = Object.freeze(
new Map<PolicyResponseStatus, string>([
[
'success',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.success', {
defaultMessage: 'Success',
}),
],
[
'warning',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.warning', {
defaultMessage: 'Warning',
}),
],
[
'failure',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.failed', {
defaultMessage: 'Failed',
}),
],
[
'unsupported',
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.unsupported', {
defaultMessage: 'Unsupported',
}),
],
])
);
const descriptions = Object.freeze(
new Map<Partial<PolicyResponseAction> | string, string>([
[
'full_disk_access',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.description.full_disk_access',
{
defaultMessage: 'You must enable full disk access for Elastic Endpoint on your machine. ',
}
),
],
])
);
const linkTexts = Object.freeze(
new Map<Partial<PolicyResponseAction> | string, string>([
[
'full_disk_access',
i18n.translate(
'xpack.securitySolution.endpoint.details.policyResponse.link.text.full_disk_access',
{
defaultMessage: 'Learn more.',
}
),
],
])
);
/**
* An array with errors we want to bubble up in policy response
*/
const GENERIC_ACTION_ERRORS: readonly string[] = Object.freeze(['full_disk_access']);
export class PolicyResponseActionFormatter {
public key: string;
public title: string;
public description: string;
public hasError: boolean;
public errorTitle: string;
public errorDescription?: string;
public status?: string;
public linkText?: string;
public linkUrl?: string;
constructor(
policyResponseAppliedAction: ImmutableObject<HostPolicyResponseAppliedAction>,
link?: string
) {
this.key = policyResponseAppliedAction.name;
this.title =
policyResponseTitles.get(this.key) ??
this.key.replace(/_/g, ' ').replace(/\b(\w)/g, (m) => m.toUpperCase());
this.hasError =
policyResponseAppliedAction.status === 'failure' ||
policyResponseAppliedAction.status === 'warning';
this.description = descriptions.get(this.key) || policyResponseAppliedAction.message;
this.errorDescription = descriptions.get(this.key) || policyResponseAppliedAction.message;
this.errorTitle = this.errorDescription ? this.title : policyResponseAppliedAction.name;
this.status = policyResponseStatuses.get(policyResponseAppliedAction.status);
this.linkText = linkTexts.get(this.key);
this.linkUrl = link;
}
public isGeneric(): boolean {
return GENERIC_ACTION_ERRORS.includes(this.key);
}
}

View file

@ -219,4 +219,47 @@ describe('when on the policy response', () => {
const component = await renderOpenedTree();
expect(component.getByText('A New Unknown Action')).not.toBeNull();
});
it('should not display error callout if status success', async () => {
const policyResponse = createPolicyResponse();
policyResponse.Endpoint.policy.applied.actions.forEach(
(action) => (action.status = HostPolicyResponseActionStatus.success)
);
runMock(policyResponse);
const component = await renderOpenedTree();
expect(component.queryAllByTestId('endpointPolicyResponseErrorCallOut')).toHaveLength(0);
});
describe('error callout', () => {
let policyResponse: HostPolicyResponse;
beforeEach(() => {
policyResponse = createPolicyResponse(HostPolicyResponseActionStatus.failure);
runMock(policyResponse);
});
it('should not display link if type is NOT mapped', async () => {
const component = await renderOpenedTree();
const calloutLink = component.queryByTestId('endpointPolicyResponseErrorCallOutLink');
expect(calloutLink).toBeNull();
});
it('should display link if type is mapped', async () => {
const action = {
name: 'full_disk_access',
message:
'You must enable full disk access for Elastic Endpoint on your machine. See our troubleshooting documentation for more information',
status: HostPolicyResponseActionStatus.failure,
};
policyResponse.Endpoint.policy.applied.actions.push(action);
policyResponse.Endpoint.policy.applied.response.configurations.malware.concerned_actions.push(
'full_disk_access'
);
const component = await renderOpenedTree();
const calloutLinks = component.queryAllByTestId('endpointPolicyResponseErrorCallOutLink');
expect(calloutLinks.length).toEqual(2);
});
});
});

View file

@ -4,14 +4,18 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { memo, useEffect, useState } from 'react';
import React, { memo, useEffect, useState, useMemo } from 'react';
import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DocLinksStart } from '@kbn/core/public';
import { useKibana } from '../../../common/lib/kibana';
import type { HostPolicyResponse } from '../../../../common/endpoint/types';
import { PreferenceFormattedDateFromPrimitive } from '../../../common/components/formatted_date';
import { useGetEndpointPolicyResponse } from '../../hooks/endpoint/use_get_endpoint_policy_response';
import { PolicyResponse } from './policy_response';
import { getFailedOrWarningActionCountFromPolicyResponse } from '../../pages/endpoint_hosts/store/utils';
import { PolicyResponseActionItem } from './policy_response_action_item';
import { PolicyResponseActionFormatter } from './policy_response_friendly_names';
export interface PolicyResponseWrapperProps {
endpointId: string;
@ -23,6 +27,8 @@ export const PolicyResponseWrapper = memo<PolicyResponseWrapperProps>(
({ endpointId, showRevisionMessage = true, onShowNeedsAttentionBadge }) => {
const { data, isLoading, isFetching, isError } = useGetEndpointPolicyResponse(endpointId);
const { docLinks } = useKibana().services;
const [policyResponseConfig, setPolicyResponseConfig] =
useState<HostPolicyResponse['Endpoint']['policy']['applied']['response']['configurations']>();
const [policyResponseActions, setPolicyResponseActions] =
@ -58,6 +64,34 @@ export const PolicyResponseWrapper = memo<PolicyResponseWrapperProps>(
}
}, [policyResponseAttentionCount, onShowNeedsAttentionBadge]);
const genericErrors = useMemo(() => {
if (!policyResponseConfig && !policyResponseActions) {
return [];
}
return policyResponseActions?.reduce<PolicyResponseActionFormatter[]>(
(acc, currentAction) => {
const policyResponseActionFormatter = new PolicyResponseActionFormatter(
currentAction,
docLinks.links.securitySolution.policyResponseTroubleshooting[
currentAction.name as keyof DocLinksStart['links']['securitySolution']['policyResponseTroubleshooting']
]
);
if (policyResponseActionFormatter.isGeneric() && policyResponseActionFormatter.hasError) {
acc.push(policyResponseActionFormatter);
}
return acc;
},
[]
);
}, [
docLinks.links.securitySolution.policyResponseTroubleshooting,
policyResponseActions,
policyResponseConfig,
]);
return (
<>
{showRevisionMessage && (
@ -91,11 +125,20 @@ export const PolicyResponseWrapper = memo<PolicyResponseWrapperProps>(
)}
{isLoading && <EuiLoadingSpinner size="m" />}
{policyResponseConfig !== undefined && policyResponseActions !== undefined && (
<PolicyResponse
policyResponseConfig={policyResponseConfig}
policyResponseActions={policyResponseActions}
policyResponseAttentionCount={policyResponseAttentionCount}
/>
<>
<PolicyResponse
policyResponseConfig={policyResponseConfig}
policyResponseActions={policyResponseActions}
policyResponseAttentionCount={policyResponseAttentionCount}
/>
<EuiSpacer size="m" />
{genericErrors?.map((genericActionError) => (
<React.Fragment key={genericActionError.key}>
<PolicyResponseActionItem policyResponseActionFormatter={genericActionError} />
<EuiSpacer size="m" />
</React.Fragment>
))}
</>
)}
</>
);