[Security Solution] bubble up macos system extension errors (#136038)

This commit is contained in:
Joey F. Poon 2022-07-12 16:40:18 -05:00 committed by GitHub
parent 4f6fad21ae
commit 1469d60490
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 185 additions and 60 deletions

View file

@ -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: {

View file

@ -247,6 +247,7 @@ export interface DocLinks {
readonly blocklist: string;
readonly policyResponseTroubleshooting: {
full_disk_access: string;
macos_system_ext: string;
};
};
readonly query: {

View file

@ -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,
]
);

View file

@ -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}

View file

@ -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'];
}
}

View file

@ -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);
});
});
});

View file

@ -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}