mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] add ability for network map to be toggable, prevent map from displaying without permissions (#123336)
* 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>
This commit is contained in:
parent
607feecb20
commit
57d507c121
7 changed files with 196 additions and 155 deletions
|
@ -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>
|
||||
`;
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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';
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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[] = [
|
||||
{
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue