[SECURITY_SOLUTION] adjust policy onboarding view, check for Ingest permissions (#70536)

* adjust policy onboarding view

* correct test subj

* fix tests

* re-enable tests

* add no permissions view

* adjust onbording look

* adjust text

* use ingest hook, add tests

* adjust text

* address comments

* beta badges

* fix test

* correct timeline flyout

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Kevin Logan 2020-07-09 15:33:44 -04:00 committed by GitHub
parent d58f52de2b
commit 09da11047d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 327 additions and 164 deletions

View file

@ -7,7 +7,7 @@
import { useState, useEffect } from 'react';
import { useRouteSpy } from '../route/use_route_spy';
const hideTimelineForRoutes = [`/cases/configure`, '/management'];
const hideTimelineForRoutes = [`/cases/configure`, '/administration'];
export const useShowTimeline = () => {
const [{ pageName, pathName }] = useRouteSpy();

View file

@ -16,6 +16,7 @@ import {
EuiSelectable,
EuiSelectableMessage,
EuiSelectableProps,
EuiIcon,
EuiLoadingSpinner,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -35,71 +36,121 @@ const PolicyEmptyState = React.memo<{
onActionClick: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
actionDisabled?: boolean;
}>(({ loading, onActionClick, actionDisabled }) => {
const policySteps = useMemo(
() => [
{
title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepOneTitle', {
defaultMessage: 'Head over to Ingest Manager.',
}),
children: (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.stepOne"
defaultMessage="Here, youll add the Elastic Endpoint Security Integration to your Agent Configuration."
/>
</EuiText>
),
},
{
title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepTwoTitle', {
defaultMessage: 'Well create a recommended security policy for you.',
}),
children: (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.stepTwo"
defaultMessage="You can edit this policy in the “Policies” tab after youve added the Elastic Endpoint integration."
/>
</EuiText>
),
},
{
title: i18n.translate('xpack.securitySolution.endpoint.policyList.stepThreeTitle', {
defaultMessage: 'Enroll your agents through Fleet.',
}),
children: (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.stepThree"
defaultMessage="If you havent already, enroll your agents through Fleet using the same agent configuration."
/>
</EuiText>
),
},
],
[]
);
return (
<ManagementEmptyState
loading={loading}
onActionClick={onActionClick}
actionDisabled={actionDisabled}
dataTestSubj="emptyPolicyTable"
steps={policySteps}
headerComponent={
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.noPolicyPrompt"
defaultMessage="Looks like you're not using the Elastic Endpoint"
/>
}
bodyComponent={
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.noPolicyInstructions"
defaultMessage="Elastic Endpoint Security gives you the power to keep your endpoints safe from attack, as well as unparalleled visibility into any threat in your environment."
/>
}
/>
<div data-test-subj="emptyPolicyTable">
{loading ? (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" className="essentialAnimation" />
</EuiFlexItem>
</EuiFlexGroup>
) : (
<EuiFlexGroup data-test-subj="policyOnboardingInstructions">
<EuiFlexItem>
<EuiText>
<h3>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingTitle"
defaultMessage="Get started with Elastic Endpoint Security"
/>
</h3>
</EuiText>
<EuiSpacer size="xl" />
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingSectionOne"
defaultMessage="Elastic Endpoint Security gives you the power to keep your endpoints safe from attack, as well as unparalleled visibility into any threat in your environment."
/>
</EuiText>
<EuiSpacer size="xl" />
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingSectionTwo"
defaultMessage="Youll be able to view and manage hosts in your environment running the Elastic Endpoint from this page."
/>
</EuiText>
<EuiSpacer size="xl" />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ marginRight: '10px' }}>
<EuiIcon type="search" />
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ marginLeft: '0' }}>
<EuiText>
<h4>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingHostTitle"
defaultMessage="Hosts"
/>
</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingHostInfo"
defaultMessage="Hosts running the Elastic Endpoint"
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup>
<EuiFlexItem grow={false} style={{ marginRight: '10px' }}>
<EuiIcon type="tableDensityExpanded" />
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ marginLeft: '0' }}>
<EuiText>
<h4>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingPolicyTitle"
defaultMessage="Policies"
/>
</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText size="xs" color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingPolicyInfo"
defaultMessage="View and configure protections"
/>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xl" />
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingSectionThree"
defaultMessage="To get started, youll have to add the Elastic Endpoint integration to your Agents. Lets do that now!"
/>
</EuiText>
<EuiSpacer size="xl" />
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButton
fill
iconType="plusInCircle"
onClick={onActionClick}
isDisabled={actionDisabled}
data-test-subj="onboardingStartButton"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.actionButtonText"
defaultMessage="Add Endpoint Security"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiIcon type="logoSecurity" size="xl" />
</EuiFlexItem>
</EuiFlexGroup>
)}
</div>
);
});
@ -114,17 +165,17 @@ const HostsEmptyState = React.memo<{
() => [
{
title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepOneTitle', {
defaultMessage: 'Select a policy you created from the list below.',
defaultMessage: 'Select the policy you want to use to protect your hosts',
}),
children: (
<>
<EuiText color="subdued" size="xs">
<EuiText color="subdued" size="m" grow={false}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostList.stepOne"
defaultMessage="These are existing policies."
defaultMessage="Existing policies are listed below. This can be changed later."
/>
</EuiText>
<EuiSpacer size="m" />
<EuiSpacer size="xxl" />
<EuiSelectable
options={selectionOptions}
singleSelection="always"
@ -158,38 +209,54 @@ const HostsEmptyState = React.memo<{
{
title: i18n.translate('xpack.securitySolution.endpoint.hostList.stepTwoTitle', {
defaultMessage:
'Head over to Ingest to deploy your Agent with Endpoint Security enabled.',
'Enroll your agents enabled with Endpoint Security through Ingest Manager',
}),
status: actionDisabled ? 'disabled' : '',
children: (
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.securitySolution.endpoint.hostList.stepTwo"
defaultMessage="You'll be given a command in Ingest to get you started."
/>
</EuiText>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiText color="subdued" size="m" grow={false}>
<FormattedMessage
id="xpack.securitySolution.endpoint.hostList.stepTwo"
defaultMessage="Youll be provided with the necessary commands to get started."
/>
</EuiText>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
fill
onClick={onActionClick}
isDisabled={actionDisabled}
data-test-subj="onboardingStartButton"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
defaultMessage="Enroll Agent"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
),
},
],
[selectionOptions, handleSelectableOnChange, loading]
[selectionOptions, handleSelectableOnChange, loading, actionDisabled, onActionClick]
);
return (
<ManagementEmptyState
loading={loading}
onActionClick={onActionClick}
actionDisabled={actionDisabled}
dataTestSubj="emptyHostsTable"
steps={policySteps}
headerComponent={
<FormattedMessage
id="xpack.securitySolution.endpoint.hostList.noHostsPrompt"
defaultMessage="You have a policy, but no hosts are deployed!"
id="xpack.securitySolution.endpoint.hostList.noEndpointsPrompt"
defaultMessage="Enable Elastic Endpoint Security on your agents"
/>
}
bodyComponent={
<FormattedMessage
id="xpack.securitySolution.endpoint.hostList.noHostsInstructions"
defaultMessage="Elastic Endpoint Security gives you the power to keep your hosts safe from attack, as well as unparalleled visibility into any threat in your environment."
id="xpack.securitySolution.endpoint.hostList.noEndpointsInstructions"
defaultMessage="Youve created your security policy. Now you need to enable the Elastic Endpoint Security capabilities on your agents following the steps below."
/>
}
/>
@ -198,80 +265,45 @@ const HostsEmptyState = React.memo<{
const ManagementEmptyState = React.memo<{
loading: boolean;
onActionClick?: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
actionDisabled?: boolean;
actionButton?: JSX.Element;
dataTestSubj: string;
steps?: ManagementStep[];
headerComponent: JSX.Element;
bodyComponent: JSX.Element;
}>(
({
loading,
onActionClick,
actionDisabled,
dataTestSubj,
steps,
actionButton,
headerComponent,
bodyComponent,
}) => {
return (
<div data-test-subj={dataTestSubj}>
{loading ? (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" className="essentialAnimation" />
</EuiFlexItem>
</EuiFlexGroup>
) : (
<>
<EuiSpacer size="xxl" />
<EuiTitle size="m">
<h2 style={TEXT_ALIGN_CENTER}>{headerComponent}</h2>
</EuiTitle>
<EuiSpacer size="xxl" />
<EuiText textAlign="center" color="subdued" size="s">
{bodyComponent}
</EuiText>
<EuiSpacer size="xxl" />
{steps && (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiSteps steps={steps} data-test-subj={'onboardingSteps'} />
</EuiFlexItem>
</EuiFlexGroup>
)}
}>(({ loading, dataTestSubj, steps, headerComponent, bodyComponent }) => {
return (
<div data-test-subj={dataTestSubj}>
{loading ? (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<EuiLoadingSpinner size="xl" className="essentialAnimation" />
</EuiFlexItem>
</EuiFlexGroup>
) : (
<>
<EuiSpacer size="xxl" />
<EuiTitle size="m">
<h2 style={TEXT_ALIGN_CENTER}>{headerComponent}</h2>
</EuiTitle>
<EuiSpacer size="xxl" />
<EuiText textAlign="center" color="subdued" size="m">
{bodyComponent}
</EuiText>
<EuiSpacer size="xxl" />
{steps && (
<EuiFlexGroup alignItems="center" justifyContent="center">
<EuiFlexItem grow={false}>
<>
{actionButton ? (
actionButton
) : (
<EuiButton
fill
onClick={onActionClick}
isDisabled={actionDisabled}
data-test-subj="onboardingStartButton"
>
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.emptyCreateNewButton"
defaultMessage="Click here to get started"
/>
</EuiButton>
)}
</>
<EuiSteps steps={steps} data-test-subj={'onboardingSteps'} />
</EuiFlexItem>
</EuiFlexGroup>
</>
)}
</div>
);
}
);
)}
</>
)}
</div>
);
});
PolicyEmptyState.displayName = 'PolicyEmptyState';
HostsEmptyState.displayName = 'HostsEmptyState';
ManagementEmptyState.displayName = 'ManagementEmptyState';
export { PolicyEmptyState, HostsEmptyState, ManagementEmptyState };
export { PolicyEmptyState, HostsEmptyState };

View file

@ -16,6 +16,9 @@ import {
EuiHealth,
EuiToolTip,
EuiSelectableProps,
EuiBetaBadge,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
@ -374,14 +377,25 @@ export const HostList = () => {
data-test-subj="hostPage"
headerLeft={
<>
<EuiTitle size="l">
<h1 data-test-subj="pageViewHeaderLeftTitle">
<FormattedMessage
id="xpack.securitySolution.hostList.pageTitle"
defaultMessage="Hosts"
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1 data-test-subj="pageViewHeaderLeftTitle">
<FormattedMessage
id="xpack.securitySolution.hostList.pageTitle"
defaultMessage="Hosts"
/>
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={i18n.translate('xpack.securitySolution.endpoint.hostList.beta', {
defaultMessage: 'Beta',
})}
/>
</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<p>

View file

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { ManagementContainer } from './index';
import { AppContextTestRender, createAppRootMockRenderer } from '../../common/mock/endpoint';
import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled';
jest.mock('../../common/hooks/endpoint/ingest_enabled');
describe('when in the Admistration tab', () => {
let render: () => ReturnType<AppContextTestRender['render']>;
beforeEach(() => {
const mockedContext = createAppRootMockRenderer();
render = () => mockedContext.render(<ManagementContainer />);
});
it('should display the No Permissions view when Ingest is OFF', async () => {
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false });
const renderResult = render();
const noIngestPermissions = await renderResult.findByTestId('noIngestPermissions');
expect(noIngestPermissions).not.toBeNull();
});
it('should display the Management view when Ingest is ON', async () => {
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true });
const renderResult = render();
const hostPage = await renderResult.findByTestId('hostPage');
expect(hostPage).not.toBeNull();
});
});

View file

@ -7,6 +7,8 @@
import React, { memo } from 'react';
import { useHistory, Route, Switch } from 'react-router-dom';
import { EuiText, EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { PolicyContainer } from './policy';
import {
MANAGEMENT_ROUTING_HOSTS_PATH,
@ -16,9 +18,49 @@ import {
import { NotFoundPage } from '../../app/404';
import { HostsContainer } from './endpoint_hosts';
import { getHostListPath } from '../common/routing';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { SecurityPageName } from '../../app/types';
import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled';
const NoPermissions = memo(() => {
return (
<>
<EuiEmptyPrompt
iconType="alert"
iconColor="danger"
titleSize="l"
data-test-subj="noIngestPermissions"
title={
<FormattedMessage
id="xpack.securitySolution.endpointManagemnet.noPermissionsText"
defaultMessage="You do not have the required Kibana permissions to use Elastic Security Administration"
/>
}
body={
<p>
<EuiText color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpointManagemnet.noPermissionsSubText"
defaultMessage="It looks like Ingest Manager is disabled. Ingest Manager must be enabled to use this feature. If you do not have permissions to enable Ingest Manager, contact your Kibana administrator."
/>
</EuiText>
</p>
}
/>
<SpyRoute pageName={SecurityPageName.management} />
</>
);
});
NoPermissions.displayName = 'NoPermissions';
export const ManagementContainer = memo(() => {
const history = useHistory();
const { allEnabled: isIngestEnabled } = useIngestEnabledCheck();
if (!isIngestEnabled) {
return <Route path="*" component={NoPermissions} />;
}
return (
<Switch>
<Route path={MANAGEMENT_ROUTING_HOSTS_PATH} component={HostsContainer} />

View file

@ -37,9 +37,9 @@ describe('when on the policies page', () => {
expect(table).not.toBeNull();
});
it('should display the onboarding steps', async () => {
it('should display the instructions', async () => {
const renderResult = render();
const onboardingSteps = await renderResult.findByTestId('onboardingSteps');
const onboardingSteps = await renderResult.findByTestId('policyOnboardingInstructions');
expect(onboardingSteps).not.toBeNull();
});

View file

@ -23,6 +23,7 @@ import {
EuiConfirmModal,
EuiCallOut,
EuiButton,
EuiBetaBadge,
EuiHorizontalRule,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -395,14 +396,25 @@ export const PolicyList = React.memo(() => {
data-test-subj="policyListPage"
headerLeft={
<>
<EuiTitle size="l">
<h1 data-test-subj="pageViewHeaderLeftTitle">
<FormattedMessage
id="xpack.securitySolution.policyList.pageTitle"
defaultMessage="Policies"
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h1 data-test-subj="pageViewHeaderLeftTitle">
<FormattedMessage
id="xpack.securitySolution.policyList.pageTitle"
defaultMessage="Policies"
/>
</h1>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBetaBadge
label={i18n.translate('xpack.securitySolution.endpoint.policyList.beta', {
defaultMessage: 'Beta',
})}
/>
</h1>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiText size="s" color="subdued">
<p>

View file

@ -104,6 +104,7 @@ describe('Overview', () => {
const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock<UseMessagesStorage>;
mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false));
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true });
const wrapper = mount(
<TestProviders>
@ -128,6 +129,7 @@ describe('Overview', () => {
const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock<UseMessagesStorage>;
mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false));
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true });
const wrapper = mount(
<TestProviders>
@ -152,6 +154,7 @@ describe('Overview', () => {
const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock<UseMessagesStorage>;
mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true));
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true });
const wrapper = mount(
<TestProviders>
@ -171,6 +174,7 @@ describe('Overview', () => {
const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock<UseMessagesStorage>;
mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true));
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true });
const wrapper = mount(
<TestProviders>
@ -190,6 +194,27 @@ describe('Overview', () => {
const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock<UseMessagesStorage>;
mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(false));
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: true });
const wrapper = mount(
<TestProviders>
<MemoryRouter>
<Overview />
</MemoryRouter>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="endpoint-prompt-banner"]').exists()).toBe(false);
});
test('it does NOT render the Endpoint banner when Ingest is NOT available', () => {
(useWithSource as jest.Mock).mockReturnValue({
indicesExist: true,
indexPattern: {},
});
const mockuseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock<UseMessagesStorage>;
mockuseMessagesStorage.mockImplementation(() => endpointNoticeMessage(true));
(useIngestEnabledCheck as jest.Mock).mockReturnValue({ allEnabled: false });
const wrapper = mount(
<TestProviders>

View file

@ -29,6 +29,7 @@ import { SecurityPageName } from '../../app/types';
import { EndpointNotice } from '../components/endpoint_notice';
import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage';
import { ENDPOINT_METADATA_INDEX } from '../../../common/constants';
import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled';
const DEFAULT_QUERY: Query = { query: '', language: 'kuery' };
const NO_FILTERS: Filter[] = [];
@ -64,6 +65,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({
setDismissMessage(true);
addMessage('management', 'dismissEndpointNotice');
}, [addMessage]);
const { allEnabled: isIngestEnabled } = useIngestEnabledCheck();
return (
<>
@ -74,7 +76,7 @@ const OverviewComponent: React.FC<PropsFromRedux> = ({
</FiltersGlobal>
<WrapperPage>
{!dismissMessage && !metadataIndexExists && (
{!dismissMessage && !metadataIndexExists && isIngestEnabled && (
<>
<EndpointNotice onDismiss={dismissEndpointNotice} />
<EuiSpacer size="l" />