[Security Solution] add ability for network map to be toggable, prevent map from displaying without permissions (#123336) (#125091)

* add ability for network map to be toggable, prevent map from displaying without permissions

* PR & test additions

* Found rogue semicolon

Co-authored-by: Kristof-Pierre Cummings <kristofpierre.cummings@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
(cherry picked from commit 57d507c121)
This commit is contained in:
Kristof C 2022-02-09 11:08:55 -06:00 committed by GitHub
parent 8f2596cd20
commit 4efc0e8e8e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 196 additions and 155 deletions

View file

@ -1,22 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EmbeddableHeader it renders 1`] = `
<Header>
<EuiFlexGroup
alignItems="center"
gutterSize="m"
>
<EuiFlexItem>
<EuiTitle
size="xxxs"
>
<h6
data-test-subj="header-embeddable-title"
>
Test title
</h6>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</Header>
`;

View file

@ -1,56 +0,0 @@
/*
* 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 { mount, shallow } from 'enzyme';
import React from 'react';
import { TestProviders } from '../../../common/mock';
import { EmbeddableHeader } from './embeddable_header';
describe('EmbeddableHeader', () => {
test('it renders', () => {
const wrapper = shallow(<EmbeddableHeader title="Test title" />);
expect(wrapper).toMatchSnapshot();
});
test('it renders the title', () => {
const wrapper = mount(
<TestProviders>
<EmbeddableHeader title="Test title" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="header-embeddable-title"]').first().exists()).toBe(true);
});
test('it renders supplements when children provided', () => {
const wrapper = mount(
<TestProviders>
<EmbeddableHeader title="Test title">
<p>{'Test children'}</p>
</EmbeddableHeader>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="header-embeddable-supplements"]').first().exists()).toBe(
true
);
});
test('it DOES NOT render supplements when children not provided', () => {
const wrapper = mount(
<TestProviders>
<EmbeddableHeader title="Test title" />
</TestProviders>
);
expect(wrapper.find('[data-test-subj="header-embeddable-supplements"]').first().exists()).toBe(
false
);
});
});

View file

@ -1,42 +0,0 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import React from 'react';
import styled from 'styled-components';
const Header = styled.header.attrs(({ className }) => ({
className: `siemEmbeddable__header ${className}`,
}))`
border-bottom: ${({ theme }) => theme.eui.euiBorderThin};
padding: ${({ theme }) => theme.eui.paddingSizes.m};
`;
Header.displayName = 'Header';
export interface EmbeddableHeaderProps {
children?: React.ReactNode;
title: string | React.ReactNode;
}
export const EmbeddableHeader = React.memo<EmbeddableHeaderProps>(({ children, title }) => (
<Header>
<EuiFlexGroup alignItems="center" gutterSize="m">
<EuiFlexItem>
<EuiTitle size="xxxs">
<h6 data-test-subj="header-embeddable-title">{title}</h6>
</EuiTitle>
</EuiFlexItem>
{children && (
<EuiFlexItem data-test-subj="header-embeddable-supplements" grow={false}>
{children}
</EuiFlexItem>
)}
</EuiFlexGroup>
</Header>
));
EmbeddableHeader.displayName = 'EmbeddableHeader';

View file

@ -25,9 +25,13 @@ jest.mock('../../../common/lib/kibana');
jest.mock('./embedded_map_helpers', () => ({
createEmbeddable: jest.fn(),
}));
const mockGetStorage = jest.fn();
const mockSetStorage = jest.fn();
jest.mock('../../../common/lib/kibana', () => {
return {
useKibana: jest.fn().mockReturnValue({
useKibana: () => ({
services: {
embeddable: {
EmbeddablePanel: jest.fn(() => <div data-test-subj="EmbeddablePanel" />),
@ -38,6 +42,10 @@ jest.mock('../../../common/lib/kibana', () => {
siem: { networkMap: '' },
},
},
storage: {
get: mockGetStorage,
set: mockSetStorage,
},
},
}),
};
@ -101,6 +109,11 @@ describe('EmbeddedMapComponent', () => {
beforeEach(() => {
setQuery.mockClear();
mockGetStorage.mockReturnValue(false);
});
afterEach(() => {
jest.clearAllMocks();
});
test('renders correctly against snapshot', () => {
@ -175,4 +188,38 @@ describe('EmbeddedMapComponent', () => {
expect(wrapper.find('[data-test-subj="loading-panel"]').exists()).toEqual(true);
});
});
test('map hidden on close', async () => {
const wrapper = mount(
<TestProviders>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
const container = wrapper.find('[data-test-subj="false-toggle-network-map"]').at(0);
container.simulate('click');
await waitFor(() => {
wrapper.update();
expect(mockSetStorage).toHaveBeenNthCalledWith(1, 'network_map_visbile', true);
});
});
test('map visible on open', async () => {
mockGetStorage.mockReturnValue(true);
const wrapper = mount(
<TestProviders>
<EmbeddedMapComponent {...testProps} />
</TestProviders>
);
const container = wrapper.find('[data-test-subj="true-toggle-network-map"]').at(0);
container.simulate('click');
await waitFor(() => {
wrapper.update();
expect(mockSetStorage).toHaveBeenNthCalledWith(1, 'network_map_visbile', false);
});
});
});

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { EuiLink, EuiText } from '@elastic/eui';
import { EuiAccordion, EuiLink, EuiText } from '@elastic/eui';
import deepEqual from 'fast-deep-equal';
import React, { useEffect, useState, useMemo } from 'react';
import React, { useCallback, useEffect, useState, useMemo } from 'react';
import { createPortalNode, InPortal } from 'react-reverse-portal';
import styled, { css } from 'styled-components';
@ -20,7 +20,6 @@ import { Loader } from '../../../common/components/loader';
import { displayErrorToast, useStateToaster } from '../../../common/components/toasters';
import { GlobalTimeArgs } from '../../../common/containers/use_global_time';
import { Embeddable } from './embeddable';
import { EmbeddableHeader } from './embeddable_header';
import { createEmbeddable } from './embedded_map_helpers';
import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt';
import { MapToolTip } from './map_tool_tip/map_tool_tip';
@ -34,6 +33,8 @@ import { SourcererScopeName } from '../../../common/store/sourcerer/model';
import { useDeepEqualSelector } from '../../../common/hooks/use_selector';
import { useSourcererDataView } from '../../../common/containers/sourcerer';
export const NETWORK_MAP_VISIBLE = 'network_map_visbile';
interface EmbeddableMapProps {
maintainRatio?: boolean;
}
@ -73,6 +74,17 @@ const EmbeddableMap = styled.div.attrs(() => ({
}
`}
`;
const StyledEuiText = styled(EuiText)`
margin-right: 16px;
`;
const StyledEuiAccordion = styled(EuiAccordion)`
& .euiAccordion__triggerWrapper {
padding: 16px;
}
`;
EmbeddableMap.displayName = 'EmbeddableMap';
export interface EmbeddedMapProps {
@ -93,8 +105,13 @@ export const EmbeddedMapComponent = ({
const [embeddable, setEmbeddable] = React.useState<MapEmbeddable | undefined | ErrorEmbeddable>(
undefined
);
const { services } = useKibana();
const { storage } = services;
const [isError, setIsError] = useState(false);
const [isIndexError, setIsIndexError] = useState(false);
const [storageValue, setStorageValue] = useState(storage.get(NETWORK_MAP_VISIBLE) ?? true);
const [, dispatchToaster] = useStateToaster();
@ -115,8 +132,6 @@ export const EmbeddedMapComponent = ({
// Search InPortal/OutPortal for implementation touch points
const portalNode = React.useMemo(() => createPortalNode(), []);
const { services } = useKibana();
useEffect(() => {
setMapIndexPatterns((prevMapIndexPatterns) => {
const newIndexPatterns = kibanaDataViews.filter((dataView) =>
@ -222,30 +237,50 @@ export const EmbeddedMapComponent = ({
}
}, [embeddable, startDate, endDate]);
const setDefaultMapVisibility = useCallback(
(isOpen: boolean) => {
storage.set(NETWORK_MAP_VISIBLE, isOpen);
setStorageValue(isOpen);
},
[storage]
);
return isError ? null : (
<Embeddable>
<EmbeddableHeader title={i18n.EMBEDDABLE_HEADER_TITLE}>
<EuiText size="xs">
<StyledEuiAccordion
onToggle={setDefaultMapVisibility}
id={'network-map'}
arrowDisplay="right"
arrowProps={{
color: 'primary',
'data-test-subj': `${storageValue}-toggle-network-map`,
}}
buttonContent={<strong>{i18n.EMBEDDABLE_HEADER_TITLE}</strong>}
extraAction={
<StyledEuiText size="xs">
<EuiLink href={`${services.docLinks.links.siem.networkMap}`} target="_blank">
{i18n.EMBEDDABLE_HEADER_HELP}
</EuiLink>
</EuiText>
</EmbeddableHeader>
</StyledEuiText>
}
paddingSize="none"
initialIsOpen={storageValue}
>
<Embeddable>
<InPortal node={portalNode}>
<MapToolTip />
</InPortal>
<InPortal node={portalNode}>
<MapToolTip />
</InPortal>
<EmbeddableMap maintainRatio={!isIndexError}>
{isIndexError ? (
<IndexPatternsMissingPrompt data-test-subj="missing-prompt" />
) : embeddable != null ? (
<services.embeddable.EmbeddablePanel embeddable={embeddable} />
) : (
<Loader data-test-subj="loading-panel" overlay size="xl" />
)}
</EmbeddableMap>
</Embeddable>
<EmbeddableMap maintainRatio={!isIndexError}>
{isIndexError ? (
<IndexPatternsMissingPrompt data-test-subj="missing-prompt" />
) : embeddable != null ? (
<services.embeddable.EmbeddablePanel embeddable={embeddable} />
) : (
<Loader data-test-subj="loading-panel" overlay size="xl" />
)}
</EmbeddableMap>
</Embeddable>
</StyledEuiAccordion>
);
};

View file

@ -70,8 +70,41 @@ const mockProps = {
capabilitiesFetched: true,
hasMlUserPermissions: true,
};
const mockMapVisibility = jest.fn();
jest.mock('../../common/lib/kibana', () => {
const original = jest.requireActual('../../common/lib/kibana');
return {
...original,
useKibana: () => ({
services: {
...original.useKibana().services,
application: {
...original.useKibana().services.application,
capabilities: {
siem: { crud_alerts: true, read_alerts: true },
maps: mockMapVisibility(),
},
},
storage: {
get: () => true,
},
},
}),
useToasts: jest.fn().mockReturnValue({
addError: jest.fn(),
addSuccess: jest.fn(),
addWarning: jest.fn(),
}),
};
});
const mockUseSourcererDataView = useSourcererDataView as jest.Mock;
describe('Network page - rendering', () => {
beforeAll(() => {
mockMapVisibility.mockReturnValue({ show: true });
});
test('it renders the Setup Instructions text when no index is available', () => {
mockUseSourcererDataView.mockReturnValue({
selectedPatterns: [],
@ -106,6 +139,41 @@ describe('Network page - rendering', () => {
});
});
test('it renders the network map if user has permissions', () => {
mockUseSourcererDataView.mockReturnValue({
selectedPatterns: [],
indicesExist: true,
indexPattern: {},
});
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Network {...mockProps} />
</Router>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="conditional-embeddable-map"]').exists()).toBe(true);
});
test('it does not render the network map if user does not have permissions', () => {
mockMapVisibility.mockReturnValue({ show: false });
mockUseSourcererDataView.mockReturnValue({
selectedPatterns: [],
indicesExist: true,
indexPattern: {},
});
const wrapper = mount(
<TestProviders>
<Router history={mockHistory}>
<Network {...mockProps} />
</Router>
</TestProviders>
);
expect(wrapper.find('[data-test-subj="conditional-embeddable-map"]').exists()).toBe(false);
});
test('it should add the new filters after init', async () => {
const newFilters: Filter[] = [
{

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiSpacer, EuiWindowEvent } from '@elastic/eui';
import { EuiPanel, EuiSpacer, EuiWindowEvent } from '@elastic/eui';
import { noop } from 'lodash/fp';
import React, { useCallback, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
@ -85,6 +85,8 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
const kibana = useKibana();
const { tabName } = useParams<{ tabName: string }>();
const canUseMaps = kibana.services.application.capabilities.maps.show;
const tabsFilters = useMemo(() => {
if (tabName === NetworkRouteType.alerts) {
return filters.length > 0 ? [...filters, ...filterNetworkData] : filterNetworkData;
@ -173,15 +175,24 @@ const NetworkComponent = React.memo<NetworkComponentProps>(
border
/>
<EmbeddedMap
query={query}
filters={filters}
startDate={from}
endDate={to}
setQuery={setQuery}
/>
<EuiSpacer />
{canUseMaps && (
<>
<EuiPanel
hasBorder
paddingSize="none"
data-test-subj="conditional-embeddable-map"
>
<EmbeddedMap
query={query}
filters={filters}
startDate={from}
endDate={to}
setQuery={setQuery}
/>
</EuiPanel>
<EuiSpacer />
</>
)}
<NetworkKpiComponent
filterQuery={filterQuery}