diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index 10452996eae6f..4e935f3e497f4 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -57,3 +57,15 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { 'url.full': 'threatintel.indicator.url.full', 'registry.path': 'threatintel.indicator.registry.path', }; + +export const CTI_DEFAULT_SOURCES = [ + 'Abuse URL', + 'Abuse Malware', + 'AlienVault OTX', + 'Anomali', + 'Anomali ThreatStream', + 'Malware Bazaar', + 'MISP', +]; + +export const DEFAULT_CTI_SOURCE_INDEX = ['filebeat-*']; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index e26f0d9b65bfa..f707e650643ec 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -25,6 +25,7 @@ "encryptedSavedObjects", "fleet", "ml", + "dashboard", "newsfeed", "security", "spaces", diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.test.tsx new file mode 100644 index 0000000000000..2c519fe34412e --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.test.tsx @@ -0,0 +1,54 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CtiDisabledModule } from './cti_disabled_module'; +import { ThemeProvider } from 'styled-components'; +import { createStore, State } from '../../../common/store'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { mockTheme } from './mock'; + +jest.mock('../../../common/lib/kibana'); + +describe('CtiDisabledModule', () => { + const state: State = mockGlobalState; + + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + }); + + it('renders splitPanel with "danger" variant', () => { + const wrapper = mount( + + + + + + + + ); + + expect( + wrapper.find( + '[data-test-subj="cti-dashboard-links"] [data-test-subj="cti-inner-panel-danger"]' + ).length + ).toEqual(1); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx new file mode 100644 index 0000000000000..e22fec1861f8b --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx @@ -0,0 +1,45 @@ +/* + * 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 React, { useMemo } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { ThreatIntelPanelView } from './threat_intel_panel_view'; +import { EMPTY_LIST_ITEMS } from '../../containers/overview_cti_links/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { CtiInnerPanel } from './cti_inner_panel'; +import * as i18n from './translations'; + +export const CtiDisabledModuleComponent = () => { + const threatIntelDocLink = `${ + useKibana().services.docLinks.links.filebeat.base + }/filebeat-module-threatintel.html`; + + const danger = useMemo( + () => ( + + {i18n.DANGER_BUTTON} + + } + data-test-subj="cti-inner-panel-danger" + /> + ), + [threatIntelDocLink] + ); + + return ( + + ); +}; + +CtiDisabledModuleComponent.displayName = 'CtiDisabledModule'; + +export const CtiDisabledModule = React.memo(CtiDisabledModuleComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx new file mode 100644 index 0000000000000..a40448d471fe5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CtiEnabledModule } from './cti_enabled_module'; +import { ThemeProvider } from 'styled-components'; +import { createStore, State } from '../../../common/store'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { mockTheme, mockProps, mockCtiEventCountsResponse, mockCtiLinksResponse } from './mock'; +import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; + +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../containers/overview_cti_links/use_cti_event_counts'); +const useCTIEventCountsMock = useCtiEventCounts as jest.Mock; +useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse); + +jest.mock('../../containers/overview_cti_links'); +const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; +useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); + +describe('CtiEnabledModule', () => { + const state: State = mockGlobalState; + + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + }); + + it('renders CtiWithEvents when there are events', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.exists('[data-test-subj="cti-with-events"]')).toBe(true); + }); + + it('renders CtiWithNoEvents when there are no events', () => { + useCTIEventCountsMock.mockReturnValueOnce({ totalCount: 0 }); + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.exists('[data-test-subj="cti-with-no-events"]')).toBe(true); + }); + + it('renders null while event counts are loading', () => { + useCTIEventCountsMock.mockReturnValueOnce({ totalCount: -1 }); + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.html()).toEqual(''); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx new file mode 100644 index 0000000000000..5f7be2ac2e6bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx @@ -0,0 +1,38 @@ +/* + * 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 React from 'react'; +import { ThreatIntelLinkPanelProps } from './index'; +import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { CtiNoEvents } from './cti_no_events'; +import { CtiWithEvents } from './cti_with_events'; + +export const CtiEnabledModuleComponent: React.FC = (props) => { + const { eventCountsByDataset, totalCount } = useCtiEventCounts(props); + const { to, from } = props; + + switch (totalCount) { + case -1: + return null; + case 0: + return ; + default: + return ( + + ); + } +}; + +CtiEnabledModuleComponent.displayName = 'CtiEnabledModule'; + +export const CtiEnabledModule = React.memo(CtiEnabledModuleComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_inner_panel.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_inner_panel.tsx new file mode 100644 index 0000000000000..08bf0a432f9bb --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_inner_panel.tsx @@ -0,0 +1,69 @@ +/* + * 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 React from 'react'; +import styled from 'styled-components'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSplitPanel, EuiText } from '@elastic/eui'; + +const PanelContainer = styled(EuiSplitPanel.Inner)` + margin-bottom: ${({ theme }) => theme.eui.paddingSizes.m}; +`; + +const ButtonContainer = styled(EuiFlexGroup)` + padding: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const Title = styled(EuiText)<{ textcolor: 'primary' | 'warning' | 'danger' }>` + color: ${({ theme, textcolor }) => + textcolor === 'primary' + ? theme.eui.euiColorPrimary + : textcolor === 'warning' + ? theme.eui.euiColorWarningText + : theme.eui.euiColorDangerText}; + margin-bottom: ${({ theme }) => theme.eui.paddingSizes.m}; +`; + +const Icon = styled(EuiIcon)` + padding: 0; + margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; + margin-left: 12px; + transform: scale(${({ color }) => (color === 'primary' ? 1.4 : 1)}); +`; + +export const CtiInnerPanel = ({ + color, + title, + body, + button, +}: { + color: 'primary' | 'warning' | 'danger'; + title: string; + body: string; + button?: JSX.Element; +}) => { + const iconType = color === 'primary' ? 'iInCircle' : color === 'warning' ? 'help' : 'alert'; + return ( + + + + + + + {title} + + + {body} + + {button && ( + + {button} + + )} + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx new file mode 100644 index 0000000000000..5e1697279dd4c --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx @@ -0,0 +1,75 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CtiNoEvents } from './cti_no_events'; +import { ThemeProvider } from 'styled-components'; +import { createStore, State } from '../../../common/store'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { mockEmptyCtiLinksResponse, mockTheme, mockProps } from './mock'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; + +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../containers/overview_cti_links'); +const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; +useCtiDashboardLinksMock.mockReturnValue(mockEmptyCtiLinksResponse); + +describe('CtiNoEvents', () => { + const state: State = mockGlobalState; + + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + }); + + it('renders warning inner panel', () => { + const wrapper = mount( + + + + + + + + ); + + expect( + wrapper.find( + '[data-test-subj="cti-dashboard-links"] [data-test-subj="cti-inner-panel-warning"]' + ).length + ).toEqual(1); + }); + + it('renders event counts as 0', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('[data-test-subj="cti-total-event-count"]').text()).toEqual( + 'Showing: 0 events' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx new file mode 100644 index 0000000000000..3adccb4f4e3f3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx @@ -0,0 +1,44 @@ +/* + * 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 React from 'react'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; +import { ThreatIntelPanelView } from './threat_intel_panel_view'; +import { CtiInnerPanel } from './cti_inner_panel'; +import * as i18n from './translations'; +import { emptyEventCountsByDataset } from '../../containers/overview_cti_links/helpers'; + +const warning = ( + +); + +export const CtiNoEventsComponent = ({ to, from }: { to: string; from: string }) => { + const { buttonHref, listItems, isDashboardPluginDisabled } = useCtiDashboardLinks( + { ...emptyEventCountsByDataset }, + to, + from + ); + + return ( + + ); +}; + +CtiNoEventsComponent.displayName = 'CtiNoEvents'; + +export const CtiNoEvents = React.memo(CtiNoEventsComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx new file mode 100644 index 0000000000000..3b03b9c418a1c --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; +import { CtiWithEvents } from './cti_with_events'; +import { ThemeProvider } from 'styled-components'; +import { createStore, State } from '../../../common/store'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { mockCtiLinksResponse, mockTheme, mockCtiWithEventsProps } from './mock'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; + +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../containers/overview_cti_links'); +const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; +useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); + +describe('CtiWithEvents', () => { + const state: State = mockGlobalState; + + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + }); + + it('renders total event count as expected', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('[data-test-subj="cti-total-event-count"]').text()).toEqual( + `Showing: ${mockCtiWithEventsProps.totalCount} events` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx new file mode 100644 index 0000000000000..f9640e9a232f2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx @@ -0,0 +1,41 @@ +/* + * 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 React from 'react'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; +import { ThreatIntelPanelView } from './threat_intel_panel_view'; + +export const CtiWithEventsComponent = ({ + eventCountsByDataset, + from, + to, + totalCount, +}: { + eventCountsByDataset: { [key: string]: number }; + from: string; + to: string; + totalCount: number; +}) => { + const { buttonHref, isDashboardPluginDisabled, listItems } = useCtiDashboardLinks( + eventCountsByDataset, + to, + from + ); + + return ( + + ); +}; + +CtiWithEventsComponent.displayName = 'CtiWithEvents'; + +export const CtiWithEvents = React.memo(CtiWithEventsComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx new file mode 100644 index 0000000000000..ca3d0ddde401d --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; +import { ThreatIntelLinkPanel } from '.'; +import { ThemeProvider } from 'styled-components'; +import { createStore, State } from '../../../common/store'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { mockTheme, mockProps } from './mock'; +import { useIsThreatIntelModuleEnabled } from '../../containers/overview_cti_links/use_is_threat_intel_module_enabled'; + +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../containers/overview_cti_links/use_is_threat_intel_module_enabled'); +const useIsThreatIntelModuleEnabledMock = useIsThreatIntelModuleEnabled as jest.Mock; +useIsThreatIntelModuleEnabledMock.mockReturnValue(true); + +describe('ThreatIntelLinkPanel', () => { + const state: State = mockGlobalState; + + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + }); + + it('renders CtiEnabledModule when Threat Intel module is enabled', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('[data-test-subj="cti-enabled-module"]').length).toEqual(1); + }); + + it('renders CtiDisabledModule when Threat Intel module is disabled', () => { + useIsThreatIntelModuleEnabledMock.mockReturnValueOnce(false); + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); + }); + + it('renders null while Threat Intel module state is loading', () => { + useIsThreatIntelModuleEnabledMock.mockReturnValueOnce(undefined); + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.html()).toEqual(''); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx new file mode 100644 index 0000000000000..1ae00face7c8e --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { useIsThreatIntelModuleEnabled } from '../../containers/overview_cti_links/use_is_threat_intel_module_enabled'; +import { CtiEnabledModule } from './cti_enabled_module'; +import { CtiDisabledModule } from './cti_disabled_module'; + +export type ThreatIntelLinkPanelProps = Pick< + GlobalTimeArgs, + 'from' | 'to' | 'deleteQuery' | 'setQuery' +>; + +const ThreatIntelLinkPanelComponent: React.FC = (props) => { + const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); + + switch (isThreatIntelModuleEnabled) { + case true: + return ; + case false: + return ; + case undefined: + default: + return null; + } +}; + +ThreatIntelLinkPanelComponent.displayName = 'ThreatIntelDashboardLinksComponent'; + +export const ThreatIntelLinkPanel = React.memo(ThreatIntelLinkPanelComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts new file mode 100644 index 0000000000000..5da69d8c1af3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts @@ -0,0 +1,79 @@ +/* + * 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 { getMockTheme } from '../../../common/lib/kibana/kibana_react.mock'; + +export const mockTheme = getMockTheme({ + eui: { + euiSizeL: '10px', + euiBreakpoints: { s: '10px' }, + paddingSizes: { s: '10px', m: '10px', l: '10px' }, + }, +}); + +export const mockEventCountsByDataset = { + abuseurl: 1, + abusemalware: 1, + alienvaultotx: 0, + anomali: 2, + anomalithreatstream: 0, + malwarebazaar: 2, + misp: 4, +}; + +export const mockCtiEventCountsResponse = { + eventCountsByDataset: mockEventCountsByDataset, + totalCount: 10, +}; + +export const mockCtiLinksResponse = { + isDashboardPluginDisabled: false, + buttonHref: '/button', + listItems: [ + { title: 'abuseurl', count: 1, path: '/dashboard_path_abuseurl' }, + { title: 'abusemalware', count: 2, path: '/dashboard_path_abusemalware' }, + { title: 'alienvaultotx', count: 7, path: '/dashboard_path_alienvaultotx' }, + { title: 'anomali', count: 0, path: '/dashboard_path_anomali' }, + { title: 'anomalithreatstream', count: 0, path: '/dashboard_path_anomalithreatstream' }, + { title: 'malwarebazaar', count: 4, path: '/dashboard_path_malwarebazaar' }, + { title: 'misp', count: 6, path: '/dashboard_path_misp' }, + ], +}; + +export const mockEmptyCtiLinksResponse = { + isDashboardPluginDisabled: false, + buttonHref: '/button', + listItems: [ + { title: 'abuseurl', count: 0, path: '/dashboard_path_abuseurl' }, + { title: 'abusemalware', count: 0, path: '/dashboard_path_abusemalware' }, + { title: 'alienvaultotx', count: 0, path: '/dashboard_path_alienvaultotx' }, + { title: 'anomali', count: 0, path: '/dashboard_path_anomali' }, + { title: 'anomalithreatstream', count: 0, path: '/dashboard_path_anomalithreatstream' }, + { title: 'malwarebazaar', count: 0, path: '/dashboard_path_malwarebazaar' }, + { title: 'misp', count: 0, path: '/dashboard_path_misp' }, + ], +}; + +export const mockProps = { + to: '2020-01-20T20:49:57.080Z', + from: '2020-01-21T20:49:57.080Z', + setQuery: jest.fn(), + deleteQuery: jest.fn(), +}; + +export const mockCtiWithEventsProps = { + ...mockProps, + ...mockCtiEventCountsResponse, +}; + +export const mockThreatIntelPanelViewProps = { + buttonHref: '/button_href', + isDashboardPluginDisabled: false, + listItems: mockCtiLinksResponse.listItems, + splitPanel: undefined, + totalEventCount: 1337, +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx new file mode 100644 index 0000000000000..59ee1e5447ba3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.test.tsx @@ -0,0 +1,147 @@ +/* + * 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 React from 'react'; +import { Provider } from 'react-redux'; +import { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import { I18nProvider } from '@kbn/i18n/react'; +import { ThreatIntelPanelView } from './threat_intel_panel_view'; +import { ThemeProvider } from 'styled-components'; +import { createStore, State } from '../../../common/store'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { mockTheme, mockThreatIntelPanelViewProps } from './mock'; + +jest.mock('../../../common/lib/kibana'); + +describe('ThreatIntelPanelView', () => { + const state: State = mockGlobalState; + + const { storage } = createSecuritySolutionStorageMock(); + let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + }); + + it('renders enabled button when there is a button href', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('button').props().disabled).toEqual(false); + }); + + it('renders disabled button when there is no button href', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('button').at(1).props().disabled).toEqual(true); + }); + + it('renders info panel if dashboard plugin is disabled', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('[data-test-subj="cti-inner-panel-info"]').length).toEqual(1); + }); + + it('does not render info panel if dashboard plugin is disabled', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('[data-test-subj="cti-inner-panel-info"]').length).toEqual(0); + }); + + it('renders split panel if split panel is passed in as a prop', () => { + const wrapper = mount( + + + + , + }} + /> + + + + ); + + expect(wrapper.find('[data-test-subj="mock-split-panel"]').length).toEqual(1); + }); + + it('renders list items with links', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('li a').at(0).props().href).toEqual( + mockThreatIntelPanelViewProps.listItems[0].path + ); + }); + + it('renders total event count', () => { + const wrapper = mount( + + + + + + + + ); + + expect(wrapper.find('[data-test-subj="cti-total-event-count"]').text()).toEqual( + `Showing: ${mockThreatIntelPanelViewProps.totalEventCount} events` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx new file mode 100644 index 0000000000000..9e8375e263088 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx @@ -0,0 +1,187 @@ +/* + * 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 React, { useMemo } from 'react'; +import styled from 'styled-components'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { InspectButtonContainer } from '../../../common/components/inspect'; +import { HeaderSection } from '../../../common/components/header_section'; +import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { CtiListItem } from '../../containers/overview_cti_links/helpers'; +import { LinkButton } from '../../../common/components/links'; +import { useKibana } from '../../../common/lib/kibana'; +import { CtiInnerPanel } from './cti_inner_panel'; +import * as i18n from './translations'; + +const DashboardLink = styled.li` + margin: 0 ${({ theme }) => theme.eui.paddingSizes.s} 0 ${({ theme }) => theme.eui.paddingSizes.m}; +`; + +const DashboardLinkItems = styled(EuiFlexGroup)` + width: 100%; +`; + +const Title = styled(EuiFlexItem)` + min-width: 140px; +`; + +const List = styled.ul` + margin-bottom: ${({ theme }) => theme.eui.paddingSizes.l}; +`; + +const DashboardRightSideElement = styled(EuiFlexItem)` + align-items: flex-end; + max-width: 160px; +`; + +const RightSideLink = styled(EuiLink)` + text-align: right; + min-width: 140px; +`; + +interface ThreatIntelPanelViewProps { + buttonHref?: string; + isDashboardPluginDisabled?: boolean; + listItems: CtiListItem[]; + splitPanel?: JSX.Element; + totalEventCount: number; +} + +const linkCopy = ( + +); + +const panelTitle = ( + +); + +export const ThreatIntelPanelView: React.FC = ({ + buttonHref = '', + isDashboardPluginDisabled, + listItems, + splitPanel, + totalEventCount, +}) => { + const subtitle = useMemo( + () => ( + + ), + [totalEventCount] + ); + + const button = useMemo( + () => ( + + + + ), + [buttonHref] + ); + + const threatIntelDashboardDocLink = `${ + useKibana().services.docLinks.links.filebeat.base + }/load-kibana-dashboards.html`; + const infoPanel = useMemo( + () => + isDashboardPluginDisabled ? ( + {i18n.INFO_BUTTON}} + /> + ) : null, + [isDashboardPluginDisabled, threatIntelDashboardDocLink] + ); + + return ( + <> + + + + + + + <>{button} + + {splitPanel} + {infoPanel} + + + {listItems.map(({ title, path, count }) => ( + + + + + {title} + + + + {count} + + + {path ? ( + {linkCopy} + ) : ( + + {linkCopy} + + )} + + + + + ))} + + + + + + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts new file mode 100644 index 0000000000000..663ec3a75c902 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts @@ -0,0 +1,65 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const INFO_TITLE = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardInfoPanelTitle', + { + defaultMessage: 'Enable Kibana dashboard to view sources', + } +); + +export const INFO_BODY = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardInfoPanelBody', + { + defaultMessage: + 'Follow this guide to enable your dashboard so that you can view your sources in visualizations.', + } +); + +export const INFO_BUTTON = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardInfoPanelButton', + { + defaultMessage: 'How to load Kibana dashboards', + } +); + +export const WARNING_TITLE = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardWarningPanelTitle', + { + defaultMessage: 'No threat intel data available to display', + } +); + +export const WARNING_BODY = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardWarningPanelBody', + { + defaultMessage: `We haven't detected any data from the selected time range, please try to search for another time range.`, + } +); + +export const DANGER_TITLE = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardDangerPanelTitle', + { + defaultMessage: 'No threat intel data available to display', + } +); + +export const DANGER_BODY = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardDangerPanelBody', + { + defaultMessage: 'You need to enable module in order to view data from different sources.', + } +); + +export const DANGER_BUTTON = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardDangerPanelButton', + { + defaultMessage: 'Enable Module', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts new file mode 100644 index 0000000000000..dbe5b07d9da3c --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts @@ -0,0 +1,72 @@ +/* + * 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 { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; +import { CTI_DEFAULT_SOURCES } from '../../../../common/cti/constants'; + +export interface CtiListItem { + path: string; + title: string; + count: number; +} + +export const EMPTY_LIST_ITEMS: CtiListItem[] = CTI_DEFAULT_SOURCES.map((item) => ({ + title: item, + count: 0, + path: '', +})); + +const TAG_REQUEST_BODY_SEARCH = 'threat intel'; +export const TAG_REQUEST_BODY = { + type: 'tag', + search: TAG_REQUEST_BODY_SEARCH, + searchFields: ['name'], +}; + +export interface EventCounts { + [key: string]: number; +} + +export const DASHBOARD_SO_TITLE_PREFIX = '[Filebeat Threat Intel] '; +export const OVERVIEW_DASHBOARD_LINK_TITLE = 'Overview'; + +export const getListItemsWithoutLinks = (eventCounts: EventCounts): CtiListItem[] => { + return EMPTY_LIST_ITEMS.map((item) => ({ + ...item, + count: eventCounts[item.title.replace(' ', '').toLowerCase()] ?? 0, + })); +}; + +export const isCtiListItem = (item: CtiListItem | Partial): item is CtiListItem => + typeof item.title === 'string' && typeof item.path === 'string' && typeof item.count === 'number'; + +export const isOverviewItem = (item: { path?: string; title?: string }) => + item.title === OVERVIEW_DASHBOARD_LINK_TITLE; + +export const createLinkFromDashboardSO = ( + dashboardSO: { attributes?: SavedObjectAttributes }, + eventCountsByDataset: EventCounts, + path: string +) => { + const title = + typeof dashboardSO.attributes?.title === 'string' + ? dashboardSO.attributes.title.replace(DASHBOARD_SO_TITLE_PREFIX, '') + : undefined; + return { + title, + count: + typeof title === 'string' + ? eventCountsByDataset[title.replace(' ', '').toLowerCase()] + : undefined, + path, + }; +}; + +export const emptyEventCountsByDataset = CTI_DEFAULT_SOURCES.reduce((acc, item) => { + acc[item.toLowerCase().replace(' ', '')] = 0; + return acc; +}, {} as { [key: string]: number }); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx new file mode 100644 index 0000000000000..b7f919dc97013 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx @@ -0,0 +1,116 @@ +/* + * 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 { useState, useEffect, useCallback } from 'react'; +import { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; +import { useKibana } from '../../../common/lib/kibana'; +import { + CtiListItem, + TAG_REQUEST_BODY, + createLinkFromDashboardSO, + getListItemsWithoutLinks, + isCtiListItem, + isOverviewItem, +} from './helpers'; + +export const useCtiDashboardLinks = ( + eventCountsByDataset: { [key: string]: number }, + to: string, + from: string +) => { + const createDashboardUrl = useKibana().services.dashboard?.dashboardUrlGenerator?.createUrl; + const savedObjectsClient = useKibana().services.savedObjects.client; + + const [buttonHref, setButtonHref] = useState(); + const [listItems, setListItems] = useState([]); + + const [isDashboardPluginDisabled, setIsDashboardPluginDisabled] = useState(false); + const handleDisabledPlugin = useCallback(() => { + if (!isDashboardPluginDisabled) { + setIsDashboardPluginDisabled(true); + } + setListItems(getListItemsWithoutLinks(eventCountsByDataset)); + }, [setIsDashboardPluginDisabled, setListItems, eventCountsByDataset, isDashboardPluginDisabled]); + + const handleTagsReceived = useCallback( + (TagsSO?) => { + if (TagsSO?.savedObjects?.length) { + return savedObjectsClient.find({ + type: 'dashboard', + hasReference: { id: TagsSO.savedObjects[0].id, type: 'tag' }, + }); + } + return undefined; + }, + [savedObjectsClient] + ); + + useEffect(() => { + if (!createDashboardUrl || !savedObjectsClient) { + handleDisabledPlugin(); + } else { + savedObjectsClient + .find(TAG_REQUEST_BODY) + .then(handleTagsReceived) + .then( + async (DashboardsSO?: { + savedObjects?: Array<{ + attributes?: SavedObjectAttributes; + id?: string; + }>; + }) => { + if (DashboardsSO?.savedObjects?.length) { + const dashboardUrls = await Promise.all( + DashboardsSO.savedObjects.map((SO) => + createDashboardUrl({ + dashboardId: SO.id, + timeRange: { + to, + from, + }, + }) + ) + ); + const items = DashboardsSO.savedObjects?.reduce( + (acc: CtiListItem[], dashboardSO, i) => { + const item = createLinkFromDashboardSO( + dashboardSO, + eventCountsByDataset, + dashboardUrls[i] + ); + if (isOverviewItem(item)) { + setButtonHref(item.path); + } else if (isCtiListItem(item)) { + acc.push(item); + } + return acc; + }, + [] + ); + setListItems(items); + } else { + handleDisabledPlugin(); + } + } + ); + } + }, [ + createDashboardUrl, + eventCountsByDataset, + from, + handleDisabledPlugin, + handleTagsReceived, + isDashboardPluginDisabled, + savedObjectsClient, + to, + ]); + + return { + buttonHref, + isDashboardPluginDisabled, + listItems, + }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts new file mode 100644 index 0000000000000..cc06f593a06c1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts @@ -0,0 +1,70 @@ +/* + * 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 { useEffect, useState, useMemo } from 'react'; +import { ThreatIntelLinkPanelProps } from '../../components/overview_cti_links'; +import { useRequestEventCounts } from './use_request_event_counts'; +import { emptyEventCountsByDataset } from './helpers'; + +export const ID = 'ctiEventCountQuery'; +const PREFIX = 'threatintel.'; + +export const useCtiEventCounts = ({ + deleteQuery, + from, + setQuery, + to, +}: ThreatIntelLinkPanelProps) => { + const [isInitialLoading, setIsInitialLoading] = useState(true); + + const [loading, { data, inspect, totalCount, refetch }] = useRequestEventCounts(to, from); + + const eventCountsByDataset = useMemo( + () => + data.reduce( + (acc, item) => { + if (item.y && item.g) { + const id = item.g.replace(PREFIX, ''); + acc[id] += item.y; + } + return acc; + }, + { ...emptyEventCountsByDataset } as { [key: string]: number } + ), + [data] + ); + + useEffect(() => { + if (isInitialLoading && data) { + setIsInitialLoading(false); + } + }, [isInitialLoading, data]); + + useEffect(() => { + if (!loading && !isInitialLoading) { + setQuery({ id: ID, inspect, loading, refetch }); + } + }, [setQuery, inspect, loading, refetch, isInitialLoading, setIsInitialLoading]); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); + + useEffect(() => { + refetch(); + }, [to, from, refetch]); + + return { + eventCountsByDataset, + loading, + totalCount, + }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts new file mode 100644 index 0000000000000..0dc0e8a3fe1f2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts @@ -0,0 +1,32 @@ +/* + * 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 { useState, useEffect, useMemo } from 'react'; +import { useRequestEventCounts } from './use_request_event_counts'; + +export const useIsThreatIntelModuleEnabled = () => { + const [isThreatIntelModuleEnabled, setIsThreatIntelModuleEnabled] = useState< + boolean | undefined + >(); + + const { to, from } = useMemo( + () => ({ + to: new Date().toISOString(), + from: new Date(0).toISOString(), + }), + [] + ); + + const [, { totalCount }] = useRequestEventCounts(to, from); + + useEffect(() => { + if (totalCount !== -1) { + setIsThreatIntelModuleEnabled(totalCount > 0); + } + }, [totalCount]); + + return isThreatIntelModuleEnabled; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts new file mode 100644 index 0000000000000..a6990c726dcf2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts @@ -0,0 +1,53 @@ +/* + * 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 { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { MatrixHistogramType } from '../../../../common/search_strategy'; +import { EVENT_DATASET, DEFAULT_CTI_SOURCE_INDEX } from '../../../../common/cti/constants'; +import { useMatrixHistogram } from '../../../common/containers/matrix_histogram'; +import { useKibana } from '../../../common/lib/kibana'; + +export const useRequestEventCounts = (to: string, from: string) => { + const { uiSettings } = useKibana().services; + + const matrixHistogramRequest = useMemo(() => { + return { + endDate: to, + errorMessage: i18n.translate('xpack.securitySolution.overview.errorFetchingEvents', { + defaultMessage: 'Error fetching events', + }), + filterQuery: convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern: { + fields: [ + { + name: 'event.kind', + aggregatable: true, + searchable: true, + type: 'string', + esTypes: ['keyword'], + }, + ], + title: 'filebeat-*', + }, + queries: [{ query: 'event.type:indicator', language: 'kuery' }], + filters: [], + }), + histogramType: MatrixHistogramType.events, + indexNames: DEFAULT_CTI_SOURCE_INDEX, + stackByField: EVENT_DATASET, + startDate: from, + size: 0, + }; + }, [to, from, uiSettings]); + + const results = useMatrixHistogram(matrixHistogramRequest); + + return results; +}; diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index c66976469530d..f003e6084b3c6 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -19,6 +19,13 @@ import { Overview } from './index'; import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled'; import { useSourcererScope } from '../../common/containers/sourcerer'; import { useFetchIndex } from '../../common/containers/source'; +import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; +import { useCtiEventCounts } from '../containers/overview_cti_links/use_cti_event_counts'; +import { + mockCtiEventCountsResponse, + mockCtiLinksResponse, +} from '../components/overview_cti_links/mock'; +import { useCtiDashboardLinks } from '../containers/overview_cti_links'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); @@ -43,6 +50,20 @@ jest.mock('../../common/components/query_bar', () => ({ jest.mock('../../common/hooks/endpoint/ingest_enabled'); jest.mock('../../common/containers/local_storage/use_messages_storage'); +jest.mock('../containers/overview_cti_links'); +jest.mock('../containers/overview_cti_links/use_cti_event_counts'); +jest.mock('../containers/overview_cti_links'); +const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; +useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); + +jest.mock('../containers/overview_cti_links/use_cti_event_counts'); +const useCTIEventCountsMock = useCtiEventCounts as jest.Mock; +useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse); + +jest.mock('../containers/overview_cti_links/use_is_threat_intel_module_enabled'); +const useIsThreatIntelModuleEnabledMock = useIsThreatIntelModuleEnabled as jest.Mock; +useIsThreatIntelModuleEnabledMock.mockReturnValue(true); + const endpointNoticeMessage = (hasMessageValue: boolean) => { return { hasMessage: () => hasMessageValue, @@ -56,6 +77,7 @@ const mockUseSourcererScope = useSourcererScope as jest.Mock; const mockUseIngestEnabledCheck = useIngestEnabledCheck as jest.Mock; const mockUseFetchIndex = useFetchIndex as jest.Mock; const mockUseMessagesStorage: jest.Mock = useMessagesStorage as jest.Mock; + describe('Overview', () => { beforeEach(() => { mockUseFetchIndex.mockReturnValue([ diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 2cf998e5e133a..3c8612ed6cd95 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -32,6 +32,7 @@ import { useSourcererScope } from '../../common/containers/sourcerer'; import { Sourcerer } from '../../common/components/sourcerer'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; +import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; const SidebarFlexItem = styled(EuiFlexItem)` margin-right: 24px; @@ -140,6 +141,14 @@ const OverviewComponent = () => { to={to} /> + + + diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index aad685f9fb103..27d89130941bd 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -36,6 +36,7 @@ import { Overview } from './overview'; import { Timelines } from './timelines'; import { Management } from './management'; import { LicensingPluginStart, LicensingPluginSetup } from '../../licensing/public'; +import { DashboardStart } from '../../../../src/plugins/dashboard/public'; export interface SetupPlugins { home?: HomePublicPluginSetup; @@ -50,6 +51,7 @@ export interface SetupPlugins { export interface StartPlugins { cases: CasesUiStart; data: DataPublicPluginStart; + dashboard?: DashboardStart; embeddable: EmbeddableStart; inspector: InspectorStart; fleet?: FleetStart;