mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] bubble up macos system extension errors (#136038)
This commit is contained in:
parent
4f6fad21ae
commit
1469d60490
7 changed files with 185 additions and 60 deletions
|
@ -341,6 +341,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
|
|||
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`,
|
||||
macos_system_ext: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/deploy-elastic-endpoint.html#system-extension-endpoint`,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
|
|
|
@ -247,6 +247,7 @@ export interface DocLinks {
|
|||
readonly blocklist: string;
|
||||
readonly policyResponseTroubleshooting: {
|
||||
full_disk_access: string;
|
||||
macos_system_ext: string;
|
||||
};
|
||||
};
|
||||
readonly query: {
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React, { memo, useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { DocLinksStart } from '@kbn/core/public';
|
||||
import { EuiHealth, EuiText, EuiTreeView, EuiNotificationBadge } from '@elastic/eui';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import type {
|
||||
|
@ -47,6 +46,7 @@ const StyledEuiTreeView = styled(EuiTreeView)`
|
|||
`;
|
||||
|
||||
interface PolicyResponseProps {
|
||||
hostOs: string;
|
||||
policyResponseConfig: Immutable<HostPolicyResponseConfiguration>;
|
||||
policyResponseActions: Immutable<HostPolicyResponseAppliedAction[]>;
|
||||
policyResponseAttentionCount: Map<string, number>;
|
||||
|
@ -57,6 +57,7 @@ interface PolicyResponseProps {
|
|||
*/
|
||||
export const PolicyResponse = memo(
|
||||
({
|
||||
hostOs,
|
||||
policyResponseConfig,
|
||||
policyResponseActions,
|
||||
policyResponseAttentionCount,
|
||||
|
@ -93,9 +94,8 @@ export const PolicyResponse = memo(
|
|||
|
||||
const policyResponseActionFormatter = new PolicyResponseActionFormatter(
|
||||
action || {},
|
||||
docLinks.links.securitySolution.policyResponseTroubleshooting[
|
||||
action.name as keyof DocLinksStart['links']['securitySolution']['policyResponseTroubleshooting']
|
||||
]
|
||||
docLinks.links.securitySolution.policyResponseTroubleshooting,
|
||||
hostOs
|
||||
);
|
||||
return {
|
||||
label: (
|
||||
|
@ -144,6 +144,7 @@ export const PolicyResponse = memo(
|
|||
docLinks.links.securitySolution.policyResponseTroubleshooting,
|
||||
getEntryIcon,
|
||||
policyResponseActions,
|
||||
hostOs,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ export const PolicyResponseActionItem = memo(
|
|||
{policyResponseActionFormatter.linkText && policyResponseActionFormatter.linkUrl && (
|
||||
<EuiLink
|
||||
target="_blank"
|
||||
href={`${policyResponseActionFormatter.linkUrl}`}
|
||||
href={policyResponseActionFormatter.linkUrl}
|
||||
data-test-subj="endpointPolicyResponseErrorCallOutLink"
|
||||
>
|
||||
{policyResponseActionFormatter.linkText}
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DocLinks } from '@kbn/doc-links';
|
||||
import { HostPolicyResponseActionStatus } from '../../../../common/endpoint/types';
|
||||
import type {
|
||||
HostPolicyResponseActionStatus,
|
||||
HostPolicyResponseAppliedAction,
|
||||
ImmutableObject,
|
||||
} from '../../../../common/endpoint/types';
|
||||
|
@ -320,6 +321,12 @@ const policyResponseTitles = Object.freeze(
|
|||
defaultMessage: 'Full Disk Access',
|
||||
}),
|
||||
],
|
||||
[
|
||||
'macos_system_ext',
|
||||
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.macos_system_ext', {
|
||||
defaultMessage: 'Permissions required',
|
||||
}),
|
||||
],
|
||||
])
|
||||
);
|
||||
|
||||
|
@ -328,25 +335,25 @@ type PolicyResponseStatus = `${HostPolicyResponseActionStatus}`;
|
|||
const policyResponseStatuses = Object.freeze(
|
||||
new Map<PolicyResponseStatus, string>([
|
||||
[
|
||||
'success',
|
||||
HostPolicyResponseActionStatus.success,
|
||||
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.success', {
|
||||
defaultMessage: 'Success',
|
||||
}),
|
||||
],
|
||||
[
|
||||
'warning',
|
||||
HostPolicyResponseActionStatus.warning,
|
||||
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.warning', {
|
||||
defaultMessage: 'Warning',
|
||||
}),
|
||||
],
|
||||
[
|
||||
'failure',
|
||||
HostPolicyResponseActionStatus.failure,
|
||||
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.failed', {
|
||||
defaultMessage: 'Failed',
|
||||
}),
|
||||
],
|
||||
[
|
||||
'unsupported',
|
||||
HostPolicyResponseActionStatus.unsupported,
|
||||
i18n.translate('xpack.securitySolution.endpoint.details.policyResponse.unsupported', {
|
||||
defaultMessage: 'Unsupported',
|
||||
}),
|
||||
|
@ -361,7 +368,17 @@ const descriptions = Object.freeze(
|
|||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.details.policyResponse.description.full_disk_access',
|
||||
{
|
||||
defaultMessage: 'You must enable full disk access for Elastic Endpoint on your machine. ',
|
||||
defaultMessage: 'You must enable full disk access for Elastic Endpoint on your machine.',
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
'macos_system_ext',
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.details.policyResponse.description.macos_system_ext',
|
||||
{
|
||||
defaultMessage:
|
||||
'You must enable the Mac system extension for Elastic Endpoint on your machine.',
|
||||
}
|
||||
),
|
||||
],
|
||||
|
@ -375,17 +392,33 @@ const linkTexts = Object.freeze(
|
|||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.details.policyResponse.link.text.full_disk_access',
|
||||
{
|
||||
defaultMessage: 'Learn more.',
|
||||
defaultMessage: ' Learn more.',
|
||||
}
|
||||
),
|
||||
],
|
||||
[
|
||||
'macos_system_ext',
|
||||
i18n.translate(
|
||||
'xpack.securitySolution.endpoint.details.policyResponse.link.text.macos_system_ext',
|
||||
{
|
||||
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']);
|
||||
function isMacosFullDiskAccessError(os: string, policyAction: HostPolicyResponseAppliedAction) {
|
||||
return os === 'macos' && policyAction.name === 'full_disk_access';
|
||||
}
|
||||
|
||||
function isMacosSystemExtensionError(os: string, policyAction: HostPolicyResponseAppliedAction) {
|
||||
return (
|
||||
os === 'macos' &&
|
||||
policyAction.name === 'connect_kernel' &&
|
||||
policyAction.status === HostPolicyResponseActionStatus.failure
|
||||
);
|
||||
}
|
||||
|
||||
export class PolicyResponseActionFormatter {
|
||||
public key: string;
|
||||
|
@ -396,28 +429,49 @@ export class PolicyResponseActionFormatter {
|
|||
public errorDescription?: string;
|
||||
public status?: string;
|
||||
public linkText?: string;
|
||||
public linkUrl?: string;
|
||||
|
||||
constructor(
|
||||
policyResponseAppliedAction: ImmutableObject<HostPolicyResponseAppliedAction>,
|
||||
link?: string
|
||||
private policyResponseAppliedAction: ImmutableObject<HostPolicyResponseAppliedAction>,
|
||||
private docLinks: DocLinks['securitySolution']['policyResponseTroubleshooting'],
|
||||
private os: string = ''
|
||||
) {
|
||||
this.key = policyResponseAppliedAction.name;
|
||||
this.title =
|
||||
policyResponseTitles.get(this.key) ??
|
||||
policyResponseTitles.get(this.errorKey || this.key) ??
|
||||
this.key.replace(/_/g, ' ').replace(/\b(\w)/g, (m) => m.toUpperCase());
|
||||
this.hasError =
|
||||
policyResponseAppliedAction.status === 'failure' ||
|
||||
policyResponseAppliedAction.status === 'warning';
|
||||
policyResponseAppliedAction.status === HostPolicyResponseActionStatus.failure ||
|
||||
policyResponseAppliedAction.status === HostPolicyResponseActionStatus.warning;
|
||||
this.description = descriptions.get(this.key) || policyResponseAppliedAction.message;
|
||||
this.errorDescription = descriptions.get(this.key) || policyResponseAppliedAction.message;
|
||||
this.errorDescription =
|
||||
descriptions.get(this.errorKey || this.key) || this.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;
|
||||
this.linkText = linkTexts.get(this.errorKey || this.key);
|
||||
}
|
||||
|
||||
public isGeneric(): boolean {
|
||||
return GENERIC_ACTION_ERRORS.includes(this.key);
|
||||
public get linkUrl(): string {
|
||||
return this.docLinks[this.errorKey];
|
||||
}
|
||||
|
||||
public get isGeneric(): boolean {
|
||||
if (isMacosFullDiskAccessError(this.os, this.policyResponseAppliedAction)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isMacosSystemExtensionError(this.os, this.policyResponseAppliedAction)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private get errorKey(): keyof DocLinks['securitySolution']['policyResponseTroubleshooting'] {
|
||||
if (isMacosSystemExtensionError(this.os, this.policyResponseAppliedAction)) {
|
||||
return 'macos_system_ext';
|
||||
}
|
||||
|
||||
return this.policyResponseAppliedAction
|
||||
.name as keyof DocLinks['securitySolution']['policyResponseTroubleshooting'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,48 +18,76 @@ import type {
|
|||
HostPolicyResponseAppliedAction,
|
||||
} from '../../../../common/endpoint/types';
|
||||
import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data';
|
||||
import { useGetEndpointDetails } from '../../hooks';
|
||||
|
||||
jest.mock('../../hooks/endpoint/use_get_endpoint_policy_response');
|
||||
jest.mock('../../hooks/endpoint/use_get_endpoint_details');
|
||||
|
||||
describe('when on the policy response', () => {
|
||||
const docGenerator = new EndpointDocGenerator();
|
||||
const createPolicyResponse = (
|
||||
overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success
|
||||
overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success,
|
||||
extraActions: HostPolicyResponseAppliedAction[] = [
|
||||
{
|
||||
name: 'download_model',
|
||||
message: 'Failed to apply a portion of the configuration (kernel)',
|
||||
status: overallStatus,
|
||||
},
|
||||
]
|
||||
): HostPolicyResponse => {
|
||||
const policyResponse = docGenerator.generatePolicyResponse();
|
||||
const malwareResponseConfigurations =
|
||||
policyResponse.Endpoint.policy.applied.response.configurations.malware;
|
||||
policyResponse.Endpoint.policy.applied.status = overallStatus;
|
||||
malwareResponseConfigurations.status = overallStatus;
|
||||
let downloadModelAction = policyResponse.Endpoint.policy.applied.actions.find(
|
||||
(action) => action.name === 'download_model'
|
||||
|
||||
for (const extraAction of extraActions) {
|
||||
let foundExtraAction = policyResponse.Endpoint.policy.applied.actions.find(
|
||||
(action) => action.name === extraAction.name
|
||||
);
|
||||
|
||||
if (!foundExtraAction) {
|
||||
foundExtraAction = extraAction;
|
||||
policyResponse.Endpoint.policy.applied.actions.push(foundExtraAction);
|
||||
} else {
|
||||
// Else, make sure the status of the generated action matches what was passed in
|
||||
foundExtraAction.status = overallStatus;
|
||||
}
|
||||
|
||||
if (
|
||||
overallStatus === HostPolicyResponseActionStatus.failure ||
|
||||
overallStatus === HostPolicyResponseActionStatus.warning
|
||||
) {
|
||||
foundExtraAction.message = 'no action taken';
|
||||
}
|
||||
|
||||
// Make sure that at least one configuration has the above action, else
|
||||
// we get into an out-of-sync condition
|
||||
if (malwareResponseConfigurations.concerned_actions.indexOf(foundExtraAction.name) === -1) {
|
||||
malwareResponseConfigurations.concerned_actions.push(foundExtraAction.name);
|
||||
}
|
||||
}
|
||||
|
||||
// if extra actions exist more than once, remove dupes to maintain exact counts
|
||||
Object.entries(policyResponse.Endpoint.policy.applied.response.configurations).forEach(
|
||||
([responseConfigurationKey, responseConfiguration]) => {
|
||||
if (responseConfigurationKey === 'malware') {
|
||||
return;
|
||||
}
|
||||
|
||||
extraActions.forEach((extraAction) => {
|
||||
const extraActionIndex = responseConfiguration.concerned_actions.indexOf(
|
||||
extraAction.name
|
||||
);
|
||||
if (extraActionIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
responseConfiguration.concerned_actions.splice(extraActionIndex, 1);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (!downloadModelAction) {
|
||||
downloadModelAction = {
|
||||
name: 'download_model',
|
||||
message: 'Failed to apply a portion of the configuration (kernel)',
|
||||
status: overallStatus,
|
||||
};
|
||||
policyResponse.Endpoint.policy.applied.actions.push(downloadModelAction);
|
||||
} else {
|
||||
// Else, make sure the status of the generated action matches what was passed in
|
||||
downloadModelAction.status = overallStatus;
|
||||
}
|
||||
|
||||
if (
|
||||
overallStatus === HostPolicyResponseActionStatus.failure ||
|
||||
overallStatus === HostPolicyResponseActionStatus.warning
|
||||
) {
|
||||
downloadModelAction.message = 'no action taken';
|
||||
}
|
||||
|
||||
// Make sure that at least one configuration has the above action, else
|
||||
// we get into an out-of-sync condition
|
||||
if (malwareResponseConfigurations.concerned_actions.indexOf(downloadModelAction.name) === -1) {
|
||||
malwareResponseConfigurations.concerned_actions.push(downloadModelAction.name);
|
||||
}
|
||||
|
||||
// Add an unknown Action Name - to ensure we handle the format of it on the UI
|
||||
const unknownAction: HostPolicyResponseAppliedAction = {
|
||||
status: HostPolicyResponseActionStatus.success,
|
||||
|
@ -75,6 +103,7 @@ describe('when on the policy response', () => {
|
|||
let commonPolicyResponse: HostPolicyResponse;
|
||||
|
||||
const useGetEndpointPolicyResponseMock = useGetEndpointPolicyResponse as jest.Mock;
|
||||
const useGetEndpointDetailsMock = useGetEndpointDetails as jest.Mock;
|
||||
let render: (
|
||||
props?: Partial<PolicyResponseWrapperProps>
|
||||
) => ReturnType<AppContextTestRender['render']>;
|
||||
|
@ -87,6 +116,14 @@ describe('when on the policy response', () => {
|
|||
isFetching: false,
|
||||
isError: false,
|
||||
});
|
||||
useGetEndpointDetailsMock.mockReturnValue({
|
||||
data: {
|
||||
metadata: { host: { os: { name: 'macOS' } } },
|
||||
},
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -263,5 +300,34 @@ describe('when on the policy response', () => {
|
|||
const calloutLinks = component.queryAllByTestId('endpointPolicyResponseErrorCallOutLink');
|
||||
expect(calloutLinks.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should display correct description and link for macos_system_ext failure', async () => {
|
||||
policyResponse = createPolicyResponse(HostPolicyResponseActionStatus.failure, [
|
||||
{
|
||||
name: 'connect_kernel',
|
||||
message: '',
|
||||
status: HostPolicyResponseActionStatus.failure,
|
||||
},
|
||||
]);
|
||||
runMock(policyResponse);
|
||||
|
||||
const component = await renderOpenedTree();
|
||||
|
||||
const macosSystemExtTitle = 'Permissions required';
|
||||
const calloutTitles = component
|
||||
.queryAllByTestId('endpointPolicyResponseErrorCallOut')
|
||||
.filter((calloutTitle) => calloutTitle.innerHTML.includes(macosSystemExtTitle));
|
||||
expect(calloutTitles.length).toEqual(2);
|
||||
|
||||
const macosSystemExtMessage =
|
||||
'You must enable the Mac system extension for Elastic Endpoint on your machine.';
|
||||
const calloutMessages = component
|
||||
.queryAllByTestId('endpointPolicyResponseErrorCallOut')
|
||||
.filter((calloutMessage) => calloutMessage.innerHTML.includes(macosSystemExtMessage));
|
||||
expect(calloutMessages.length).toEqual(2);
|
||||
|
||||
const calloutLinks = component.queryAllByTestId('endpointPolicyResponseErrorCallOutLink');
|
||||
expect(calloutLinks.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import React, { memo, useEffect, useState, useMemo } from 'react';
|
||||
import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { 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';
|
||||
|
@ -16,6 +15,7 @@ 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';
|
||||
import { useGetEndpointDetails } from '../../hooks';
|
||||
|
||||
export interface PolicyResponseWrapperProps {
|
||||
endpointId: string;
|
||||
|
@ -26,6 +26,7 @@ export interface PolicyResponseWrapperProps {
|
|||
export const PolicyResponseWrapper = memo<PolicyResponseWrapperProps>(
|
||||
({ endpointId, showRevisionMessage = true, onShowNeedsAttentionBadge }) => {
|
||||
const { data, isLoading, isFetching, isError } = useGetEndpointPolicyResponse(endpointId);
|
||||
const { data: endpointDetails } = useGetEndpointDetails(endpointId);
|
||||
|
||||
const { docLinks } = useKibana().services;
|
||||
|
||||
|
@ -73,12 +74,11 @@ export const PolicyResponseWrapper = memo<PolicyResponseWrapperProps>(
|
|||
(acc, currentAction) => {
|
||||
const policyResponseActionFormatter = new PolicyResponseActionFormatter(
|
||||
currentAction,
|
||||
docLinks.links.securitySolution.policyResponseTroubleshooting[
|
||||
currentAction.name as keyof DocLinksStart['links']['securitySolution']['policyResponseTroubleshooting']
|
||||
]
|
||||
docLinks.links.securitySolution.policyResponseTroubleshooting,
|
||||
endpointDetails?.metadata.host.os.name.toLowerCase()
|
||||
);
|
||||
|
||||
if (policyResponseActionFormatter.isGeneric() && policyResponseActionFormatter.hasError) {
|
||||
if (policyResponseActionFormatter.isGeneric && policyResponseActionFormatter.hasError) {
|
||||
acc.push(policyResponseActionFormatter);
|
||||
}
|
||||
|
||||
|
@ -90,6 +90,7 @@ export const PolicyResponseWrapper = memo<PolicyResponseWrapperProps>(
|
|||
docLinks.links.securitySolution.policyResponseTroubleshooting,
|
||||
policyResponseActions,
|
||||
policyResponseConfig,
|
||||
endpointDetails?.metadata.host.os.name,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@ -127,6 +128,7 @@ export const PolicyResponseWrapper = memo<PolicyResponseWrapperProps>(
|
|||
{policyResponseConfig !== undefined && policyResponseActions !== undefined && (
|
||||
<>
|
||||
<PolicyResponse
|
||||
hostOs={endpointDetails?.metadata.host.os.name.toLowerCase() ?? ''}
|
||||
policyResponseConfig={policyResponseConfig}
|
||||
policyResponseActions={policyResponseActions}
|
||||
policyResponseAttentionCount={policyResponseAttentionCount}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue