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;