From eec9a1508fd91a422a43fe9f3f356dab79e3b491 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Tue, 7 Dec 2021 09:39:19 +0100 Subject: [PATCH] [CTI] Threat Intel Card on Overview page needs to accommodate Fleet TI integrations - (#120459) * Add support integrations * Fix types * fix unit tests * Fix tests and types * fix eslint * fix file case * add cy tests * Revert test * Add tests * Add support of installed integrations * Fix types * Add isntalled ingtegration case for cypress tests * Fix cypress tests * Fix comments * Fix capital naming * Fix again capital naming * Add dynamic dashboard for a new integrations packages * intermidiate changes, to keep it remote * Big refactoring * Tests and refactoring * Remove unused constanrs * Fix e2e tests * PR comments fix * fix ts * Fix translations * Remove stubs * Rename isSomeIntegrationsDisabled -> allIntegrationsInstalled * Add buildQuery tests * Fix type * Add tests for Enable Source button * Remove copied file * Move api call to api.ts * Rename fetchFleetIntegrations * Remove __mocks__ * Fix path --- .../security_solution/common/cti/constants.ts | 13 +- .../security_solution/cti/index.ts | 37 +++- .../security_solution/index.ts | 7 + .../overview/cti_link_panel.spec.ts | 13 +- .../cypress/screens/overview.ts | 4 +- .../overview/components/link_panel/helpers.ts | 7 - .../overview/components/link_panel/index.ts | 1 - .../components/link_panel/link_panel.tsx | 20 +- .../overview/components/link_panel/types.ts | 1 + .../cti_disabled_module.tsx | 11 +- .../cti_enabled_module.test.tsx | 49 +---- .../overview_cti_links/cti_enabled_module.tsx | 49 ++--- .../overview_cti_links/cti_no_events.test.tsx | 70 ------- .../overview_cti_links/cti_no_events.tsx | 42 ----- .../cti_with_events.test.tsx | 57 ------ .../overview_cti_links/cti_with_events.tsx | 49 ----- .../overview_cti_links/index.test.tsx | 38 ++-- .../components/overview_cti_links/index.tsx | 36 ++-- .../components/overview_cti_links/mock.ts | 13 +- .../threat_intel_panel_view.tsx | 62 +++---- .../overview_cti_links/translations.ts | 21 ++- .../use_integrations_page_link.tsx | 11 ++ .../containers/overview_cti_links/api.ts | 28 +++ .../containers/overview_cti_links/helpers.ts | 60 ------ .../containers/overview_cti_links/index.tsx | 116 +++++------- .../use_all_ti_data_sources.ts | 22 +++ .../use_cti_event_counts.ts | 64 ------- .../use_is_threat_intel_module_enabled.ts | 32 ---- .../use_request_event_counts.ts | 54 ------ .../overview_cti_links/use_ti_data_sources.ts | 174 ++++++++++++++++++ .../overview_cti_links/use_ti_integrations.ts | 55 ++++++ .../public/overview/pages/overview.test.tsx | 28 ++- .../public/overview/pages/overview.tsx | 25 ++- .../security_solution/factory/cti/index.ts | 2 + .../factory/cti/threat_intel_source/index.ts | 33 ++++ .../query.threat_intel_source.dsl.test.ts | 71 +++++++ .../query.threat_intel_source.dsl.ts | 59 ++++++ .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../es_archives/threat_indicator/data.json | 5 +- .../threat_indicator/mappings.json | 8 + 41 files changed, 731 insertions(+), 720 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx delete mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts create mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index b33541c5057d8..7a88b065d8701 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -58,14 +58,5 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { export const DEFAULT_EVENT_ENRICHMENT_FROM = 'now-30d'; export const DEFAULT_EVENT_ENRICHMENT_TO = 'now'; -export const CTI_DATASET_KEY_MAP: { [key: string]: string } = { - 'AbuseCH URL': 'ti_abusech.url', - 'AbuseCH Malware': 'ti_abusech.malware', - 'AbuseCH MalwareBazaar': 'ti_abusech.malwarebazaar', - 'AlienVault OTX': 'ti_otx.threat', - 'Anomali Limo': 'ti_anomali.limo', - 'Anomali Threatstream': 'ti_anomali.threatstream', - MISP: 'ti_misp.threat', - ThreatQuotient: 'ti_threatq.threat', - Cybersixgill: 'ti_cybersixgill.threat', -}; +export const TI_INTEGRATION_PREFIX = 'ti'; +export const OTHER_TI_DATASET_KEY = '_others_ti_'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index 26bf4ce6740a9..a6e7eef88724b 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts @@ -5,13 +5,16 @@ * 2.0. */ -import type { IEsSearchResponse } from 'src/plugins/data/public'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IEsSearchResponse, IEsSearchRequest } from 'src/plugins/data/public'; +import { FactoryQueryTypes } from '../..'; import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../cti/constants'; -import { Inspect } from '../../common'; +import { Inspect, Maybe, TimerangeInput } from '../../common'; import { RequestBasicOptions } from '..'; export enum CtiQueries { eventEnrichment = 'eventEnrichment', + dataSource = 'dataSource', } export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions { @@ -40,3 +43,33 @@ export const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP export const isValidEventField = (field: string): field is EventField => validEventFields.includes(field as EventField); + +export interface CtiDataSourceRequestOptions extends IEsSearchRequest { + defaultIndex: string[]; + factoryQueryType?: FactoryQueryTypes; + timerange?: TimerangeInput; +} + +export interface BucketItem { + key: string; + doc_count: number; +} +export interface Bucket { + buckets: Array; +} + +export type DatasetBucket = { + name?: Bucket; + dashboard?: Bucket; +} & BucketItem; + +export interface CtiDataSourceStrategyResponse extends Omit { + inspect?: Maybe; + rawResponse: { + aggregations?: Record & { + dataset?: { + buckets: DatasetBucket[]; + }; + }; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 00cbdb941c11b..340093995b297 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -72,6 +72,8 @@ import { CtiEventEnrichmentRequestOptions, CtiEventEnrichmentStrategyResponse, CtiQueries, + CtiDataSourceRequestOptions, + CtiDataSourceStrategyResponse, } from './cti'; import { HostRulesRequestOptions, @@ -85,6 +87,7 @@ import { UserRulesStrategyResponse, } from './ueba'; +export * from './cti'; export * from './hosts'; export * from './matrix_histogram'; export * from './network'; @@ -178,6 +181,8 @@ export type StrategyResponseType = T extends HostsQ ? MatrixHistogramStrategyResponse : T extends CtiQueries.eventEnrichment ? CtiEventEnrichmentStrategyResponse + : T extends CtiQueries.dataSource + ? CtiDataSourceStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -238,6 +243,8 @@ export type StrategyRequestType = T extends HostsQu ? MatrixHistogramRequestOptions : T extends CtiQueries.eventEnrichment ? CtiEventEnrichmentRequestOptions + : T extends CtiQueries.dataSource + ? CtiDataSourceRequestOptions : never; export interface DocValueFieldsInput { diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts index 095401ff31422..75ff13b66b29c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts @@ -10,9 +10,8 @@ import { OVERVIEW_CTI_LINKS, OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL, OVERVIEW_CTI_LINKS_INFO_INNER_PANEL, - OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL, OVERVIEW_CTI_TOTAL_EVENT_COUNT, - OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON, + OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON, } from '../../screens/overview'; import { loginAndWaitForPage } from '../../tasks/login'; @@ -28,12 +27,11 @@ describe('CTI Link Panel', () => { it('renders disabled threat intel module as expected', () => { loginAndWaitForPage(OVERVIEW_URL); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL}`).should('exist'); - cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators'); cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`).should('exist'); cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`) .should('have.attr', 'href') - .and('match', /filebeat-module-threatintel.html/); + .and('match', /app\/integrations\/browse\?q=threat%20intelligence/); }); describe('enabled threat intel module', () => { @@ -49,17 +47,16 @@ describe('CTI Link Panel', () => { loginAndWaitForPage( `${OVERVIEW_URL}?sourcerer=(timerange:(from:%272021-07-08T04:00:00.000Z%27,kind:absolute,to:%272021-07-09T03:59:59.999Z%27))` ); - cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL}`).should('exist'); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_INFO_INNER_PANEL}`).should('exist'); - cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators'); }); it('renders dashboard module as expected when there are events in the selected time period', () => { loginAndWaitForPage(OVERVIEW_URL); - cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL}`).should('not.exist'); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_INFO_INNER_PANEL}`).should('exist'); - cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); + cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON}`).should('exist'); + cy.get(OVERVIEW_CTI_LINKS).should('not.contain.text', 'Anomali'); + cy.get(OVERVIEW_CTI_LINKS).should('contain.text', 'AbuseCH malware'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 1 indicator'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 1945b7e3ce3e7..bc335ff6680ee 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -150,9 +150,9 @@ export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timel export const OVERVIEW_CTI_LINKS = '[data-test-subj="cti-dashboard-links"]'; export const OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL = '[data-test-subj="cti-inner-panel-danger"]'; -export const OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL = '[data-test-subj="cti-inner-panel-warning"]'; export const OVERVIEW_CTI_LINKS_INFO_INNER_PANEL = '[data-test-subj="cti-inner-panel-info"]'; -export const OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON = '[data-test-subj="cti-view-dashboard-button"]'; +export const OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON = + '[data-test-subj="cti-enable-integrations-button"]'; export const OVERVIEW_CTI_TOTAL_EVENT_COUNT = `${OVERVIEW_CTI_LINKS} [data-test-subj="header-panel-subtitle"]`; export const OVERVIEW_CTI_ENABLE_MODULE_BUTTON = '[data-test-subj="cti-enable-module-button"]'; diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts index 45d26d9269f6e..e2adaaae35547 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts @@ -5,13 +5,6 @@ * 2.0. */ -import { LinkPanelListItem } from '.'; - -export const isLinkPanelListItem = ( - item: LinkPanelListItem | Partial -): item is LinkPanelListItem => - typeof item.title === 'string' && typeof item.path === 'string' && typeof item.count === 'number'; - export interface EventCounts { [key: string]: number; } diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts index 9d404abcf2223..9a827b137ae78 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts @@ -6,6 +6,5 @@ */ export { InnerLinkPanel } from './inner_link_panel'; -export { isLinkPanelListItem } from './helpers'; export { LinkPanel } from './link_panel'; export type { LinkPanelListItem } from './types'; diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx b/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx index ed67fdb1c96f6..00a225635fb8b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx @@ -71,7 +71,7 @@ const LinkPanelComponent = ({ splitPanel, subtitle, }: { - button: React.ReactNode; + button?: React.ReactNode; columns: Array>; dataTestSubj: string; defaultSortField?: string; @@ -134,14 +134,16 @@ const LinkPanelComponent = ({ {splitPanel} {infoPanel} - + {chunkedItems.length > 0 && ( + + )} diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts index f6c0fb6f3837f..1b8836fc2438d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts @@ -21,4 +21,5 @@ export interface LinkPanelViewProps { listItems: LinkPanelListItem[]; splitPanel?: JSX.Element; totalCount?: number; + allIntegrationsInstalled?: boolean; } 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 index 2697e4a571ad8..36f386e49c5c7 100644 --- 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 @@ -6,24 +6,21 @@ */ import React from 'react'; -import { EMPTY_LIST_ITEMS } from '../../containers/overview_cti_links/helpers'; -import { useKibana } from '../../../common/lib/kibana'; import * as i18n from './translations'; import { DisabledLinkPanel } from '../link_panel/disabled_link_panel'; import { ThreatIntelPanelView } from './threat_intel_panel_view'; +import { useIntegrationsPageLink } from './use_integrations_page_link'; export const CtiDisabledModuleComponent = () => { - const threatIntelDocLink = `${ - useKibana().services.docLinks.links.filebeat.base - }/filebeat-module-threatintel.html`; + const integrationsLink = useIntegrationsPageLink(); return ( 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 index db83d9e1bcfe5..fc36a0c4337cf 100644 --- 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 @@ -19,20 +19,15 @@ import { 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 { mockTheme, mockProps, mockTiDataSources, mockCtiLinksResponse } from './mock'; import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; -import { useRequestEventCounts } from '../../containers/overview_cti_links/use_request_event_counts'; +import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; 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/use_request_event_counts'); -const useRequestEventCountsMock = useRequestEventCounts as jest.Mock; -useRequestEventCountsMock.mockReturnValue([true, {}]); +jest.mock('../../containers/overview_cti_links/use_ti_data_sources'); +const useTiDataSourcesMock = useTiDataSources as jest.Mock; +useTiDataSourcesMock.mockReturnValue(mockTiDataSources); jest.mock('../../containers/overview_cti_links'); const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; @@ -54,42 +49,12 @@ describe('CtiEnabledModule', () => { - - - - - ); - - expect(screen.getByTestId('cti-with-events')).toBeInTheDocument(); - }); - - it('renders CtiWithNoEvents when there are no events', () => { - useCTIEventCountsMock.mockReturnValueOnce({ totalCount: 0 }); - render( - - - - - - - - ); - - expect(screen.getByTestId('cti-with-no-events')).toBeInTheDocument(); - }); - - it('renders null while event counts are loading', () => { - useCTIEventCountsMock.mockReturnValueOnce({ totalCount: -1 }); - const { container } = render( - - - - + ); - expect(container.firstChild).toBeNull(); + expect(screen.getByText('Showing: 5 indicators')).toBeInTheDocument(); }); }); 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 index 5a40c79d6e5ec..a339676ac361f 100644 --- 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 @@ -7,37 +7,28 @@ import React from 'react'; import { ThreatIntelLinkPanelProps } from '.'; -import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; -import { CtiNoEvents } from './cti_no_events'; -import { CtiWithEvents } from './cti_with_events'; +import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; +import { ThreatIntelPanelView } from './threat_intel_panel_view'; -export type CtiEnabledModuleProps = Omit; +export const CtiEnabledModuleComponent: React.FC = (props) => { + const { to, from, allIntegrationsInstalled, allTiDataSources, setQuery, deleteQuery } = props; + const { tiDataSources, totalCount } = useTiDataSources({ + to, + from, + allTiDataSources, + setQuery, + deleteQuery, + }); + const { listItems } = useCtiDashboardLinks({ to, from, tiDataSources }); -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 ( -
- -
- ); - } + return ( + + ); }; export const CtiEnabledModule = React.memo(CtiEnabledModuleComponent); 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 deleted file mode 100644 index 8f624dabd64d1..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { cloneDeep } from 'lodash/fp'; -import { render, screen } from '@testing-library/react'; -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', () => { - render( - - - - - - - - ); - - expect(screen.getByTestId('cti-dashboard-links')).toBeInTheDocument(); - expect(screen.getByTestId('cti-inner-panel-warning')).toBeInTheDocument(); - }); - - it('renders event counts as 0', () => { - render( - - - - - - - - ); - - expect(screen.getByText('Showing: 0 indicators')).toBeInTheDocument(); - }); -}); 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 deleted file mode 100644 index fa7ac50c08765..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; -import { ThreatIntelPanelView } from './threat_intel_panel_view'; -import { InnerLinkPanel } from '../link_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, isPluginDisabled } = useCtiDashboardLinks( - emptyEventCountsByDataset, - to, - from - ); - - return ( - - ); -}; - -export const CtiNoEvents = React.memo(CtiNoEventsComponent); -CtiNoEvents.displayName = 'CtiNoEvents'; 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 deleted file mode 100644 index a50e3e91ab9e5..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import 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} indicators` - ); - }); -}); 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 deleted file mode 100644 index f78451e205b1e..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { isEqual } from 'lodash'; -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, isPluginDisabled, listItems } = useCtiDashboardLinks( - eventCountsByDataset, - to, - from - ); - - return ( - - ); -}; - -CtiWithEventsComponent.displayName = 'CtiWithEvents'; - -export const CtiWithEvents = React.memo( - CtiWithEventsComponent, - (prevProps, nextProps) => - prevProps.to === nextProps.to && - prevProps.from === nextProps.from && - prevProps.totalCount === nextProps.totalCount && - isEqual(prevProps.eventCountsByDataset, nextProps.eventCountsByDataset) -); 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 index dfd9c6c9a7fcd..71d6d5eb0c583 100644 --- 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 @@ -19,19 +19,19 @@ import { mockGlobalState, SUB_PLUGINS_REDUCER, } from '../../../common/mock'; -import { mockTheme, mockProps, mockCtiEventCountsResponse } from './mock'; -import { useRequestEventCounts } from '../../containers/overview_cti_links/use_request_event_counts'; -import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { mockTheme, mockProps, mockTiDataSources, mockCtiLinksResponse } from './mock'; +import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../containers/overview_cti_links/use_request_event_counts'); -const useRequestEventCountsMock = useRequestEventCounts as jest.Mock; -useRequestEventCountsMock.mockReturnValue([true, {}]); +jest.mock('../../containers/overview_cti_links/use_ti_data_sources'); +const useTiDataSourcesMock = useTiDataSources as jest.Mock; +useTiDataSourcesMock.mockReturnValue(mockTiDataSources); -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('ThreatIntelLinkPanel', () => { const state: State = mockGlobalState; @@ -49,40 +49,44 @@ describe('ThreatIntelLinkPanel', () => { - + ); expect(wrapper.find('[data-test-subj="cti-enabled-module"]').length).toEqual(1); + expect(wrapper.find('[data-test-subj="cti-enable-integrations-button"]').length).toEqual(0); }); - it('renders CtiDisabledModule when Threat Intel module is disabled', () => { + it('renders Enable source buttons when not all integrations installed', () => { const wrapper = mount( - + ); - - expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); + expect(wrapper.find('[data-test-subj="cti-enable-integrations-button"]').length).not.toBe(0); }); - it('renders null while Threat Intel module state is loading', () => { + it('renders CtiDisabledModule when Threat Intel module is disabled', () => { const wrapper = mount( - + ); - expect(wrapper.html()).toEqual(''); + expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); }); }); 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 index 5348c12fb6c8e..c89199c2cb0c5 100644 --- 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 @@ -8,6 +8,7 @@ import React from 'react'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { TiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; import { CtiEnabledModule } from './cti_enabled_module'; import { CtiDisabledModule } from './cti_disabled_module'; @@ -15,27 +16,26 @@ export type ThreatIntelLinkPanelProps = Pick< GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'setQuery' > & { - isThreatIntelModuleEnabled: boolean | undefined; + allIntegrationsInstalled: boolean | undefined; + allTiDataSources: TiDataSources[]; }; const ThreatIntelLinkPanelComponent: React.FC = (props) => { - switch (props.isThreatIntelModuleEnabled) { - case true: - return ( -
- -
- ); - case false: - return ( -
- -
- ); - case undefined: - default: - return null; - } + const { allIntegrationsInstalled, allTiDataSources } = props; + const isThreatIntelModuleEnabled = allTiDataSources.length > 0; + return isThreatIntelModuleEnabled ? ( +
+ +
+ ) : ( +
+ +
+ ); }; 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 index 1d02acaf65f48..c4cf876cbdc7d 100644 --- 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 @@ -15,6 +15,13 @@ export const mockTheme = getMockTheme({ }, }); +export const mockTiDataSources = { + totalCount: 5, + tiDataSources: [ + { dataset: 'ti_abusech', name: 'AbuseCH', count: 5, path: '/dashboard_path_abuseurl' }, + ], +}; + export const mockEventCountsByDataset = { abuseurl: 1, abusemalware: 1, @@ -31,8 +38,6 @@ export const mockCtiEventCountsResponse = { }; export const mockCtiLinksResponse = { - isPluginDisabled: false, - buttonHref: '/button', listItems: [ { title: 'abuseurl', count: 1, path: '/dashboard_path_abuseurl' }, { title: 'abusemalware', count: 2, path: '/dashboard_path_abusemalware' }, @@ -63,6 +68,10 @@ export const mockProps = { from: '2020-01-21T20:49:57.080Z', setQuery: jest.fn(), deleteQuery: jest.fn(), + allIntegrationsInstalled: true, + allTiDataSources: [ + { dataset: 'ti_abusech', name: 'AbuseCH', count: 5, path: '/dashboard_path_abuseurl' }, + ], }; export const mockCtiWithEventsProps = { 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 index 189f230c02c8d..3697d27015fdc 100644 --- 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 @@ -9,14 +9,14 @@ import React, { useMemo } from 'react'; import { EuiButton, EuiTableFieldDataColumnType } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '../../../common/lib/kibana'; import * as i18n from './translations'; import { LinkPanel, InnerLinkPanel, LinkPanelListItem } from '../link_panel'; import { LinkPanelViewProps } from '../link_panel/types'; import { shortenCountIntoString } from '../../../common/utils/shorten_count_into_string'; import { Link } from '../link_panel/link'; -import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_ti_data_sources'; import { LINK_COPY } from '../overview_risky_host_links/translations'; +import { useIntegrationsPageLink } from './use_integrations_page_link'; const columns: Array> = [ { name: 'Name', field: 'title', sortable: true, truncateText: true, width: '100%' }, @@ -39,51 +39,43 @@ const columns: Array> = [ ]; export const ThreatIntelPanelView: React.FC = ({ - buttonHref = '', - isPluginDisabled, isInspectEnabled = true, listItems, splitPanel, totalCount = 0, + allIntegrationsInstalled, }) => { - const threatIntelDashboardDocLink = `${ - useKibana().services.docLinks.links.filebeat.base - }/load-kibana-dashboards.html`; + const integrationsLink = useIntegrationsPageLink(); return ( ( - - {i18n.VIEW_DASHBOARD} - - ), - [buttonHref] - ), columns, dataTestSubj: 'cti-dashboard-links', infoPanel: useMemo( - () => - isPluginDisabled ? ( - - {i18n.INFO_BUTTON} - - } - /> - ) : null, - [isPluginDisabled, threatIntelDashboardDocLink] + () => ( + <> + {allIntegrationsInstalled === false ? ( + + {i18n.DANGER_BUTTON} + + } + /> + ) : null} + + ), + [allIntegrationsInstalled, integrationsLink] ), inspectQueryId: isInspectEnabled ? CTIEventCountQueryId : undefined, listItems, 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 index 4a64462b27ad5..e112942b09749 100644 --- 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 @@ -53,15 +53,14 @@ export const DANGER_TITLE = i18n.translate( export const DANGER_BODY = i18n.translate( 'xpack.securitySolution.overview.ctiDashboardEnableThreatIntel', { - defaultMessage: - 'You need to enable the filebeat threatintel module in order to view data from different sources.', + defaultMessage: 'You need to enable threat intel sources in order to view data.', } ); export const DANGER_BUTTON = i18n.translate( - 'xpack.securitySolution.overview.ctiDashboardDangerPanelButton', + 'xpack.securitySolution.overview.ctiDashboardDangerButton', { - defaultMessage: 'Enable Module', + defaultMessage: 'Enable sources', } ); @@ -72,3 +71,17 @@ export const PANEL_TITLE = i18n.translate('xpack.securitySolution.overview.ctiDa export const VIEW_DASHBOARD = i18n.translate('xpack.securitySolution.overview.ctiViewDasboard', { defaultMessage: 'View dashboard', }); + +export const SOME_MODULES_DISABLE_TITLE = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardSomeModulesDisabledTItle', + { + defaultMessage: 'Some threat intel sources are disabled', + } +); + +export const OTHER_DATA_SOURCE_TITLE = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle', + { + defaultMessage: 'Others', + } +); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx new file mode 100644 index 0000000000000..de710c2f1b17c --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx @@ -0,0 +1,11 @@ +/* + * 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 { useBasePath } from '../../../common/lib/kibana'; + +export const useIntegrationsPageLink = () => + `${useBasePath()}/app/integrations/browse?q=threat%20intelligence`; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts new file mode 100644 index 0000000000000..ad737ac410e3b --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts @@ -0,0 +1,28 @@ +/* + * 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 { KibanaServices } from '../../../common/lib/kibana'; +import { EPM_API_ROUTES } from '../../../../../fleet/common'; + +export interface IntegrationResponse { + id: string; + status: string; + savedObject?: { + attributes?: { + installed_kibana: Array<{ + type: string; + id: string; + }>; + }; + }; +} + +export const fetchFleetIntegrations = () => + KibanaServices.get().http.fetch<{ + response: IntegrationResponse[]; + }>(EPM_API_ROUTES.LIST_PATTERN, { + method: 'GET', + }); 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 deleted file mode 100644 index 9ac61cc9487ee..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import { CTI_DATASET_KEY_MAP } from '../../../../common/cti/constants'; -import { LinkPanelListItem } from '../../components/link_panel'; -import { EventCounts } from '../../components/link_panel/helpers'; - -export const ctiTitles = Object.keys(CTI_DATASET_KEY_MAP) as string[]; - -export const EMPTY_LIST_ITEMS: LinkPanelListItem[] = ctiTitles.map((title) => ({ - title, - 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 const DASHBOARD_SO_TITLE_PREFIX = '[Filebeat Threat Intel] '; -export const OVERVIEW_DASHBOARD_LINK_TITLE = 'Overview'; - -export const getCtiListItemsWithoutLinks = (eventCounts: EventCounts): LinkPanelListItem[] => { - return EMPTY_LIST_ITEMS.map((item) => ({ - ...item, - count: eventCounts[CTI_DATASET_KEY_MAP[item.title]] ?? 0, - })); -}; - -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[CTI_DATASET_KEY_MAP[title]] : undefined, - path, - }; -}; - -export const emptyEventCountsByDataset = Object.values(CTI_DATASET_KEY_MAP).reduce((acc, id) => { - acc[id] = 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 index a546d20e49583..b1310e363eef0 100644 --- 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 @@ -6,34 +6,29 @@ */ import { useState, useEffect, useCallback } from 'react'; import { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; +import { TiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { LinkPanelListItem } from '../../components/link_panel'; import { useKibana } from '../../../common/lib/kibana'; -import { - TAG_REQUEST_BODY, - createLinkFromDashboardSO, - getCtiListItemsWithoutLinks, - isOverviewItem, - EMPTY_LIST_ITEMS, -} from './helpers'; -import { LinkPanelListItem, isLinkPanelListItem } from '../../components/link_panel'; -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(EMPTY_LIST_ITEMS); +const TAG_REQUEST_BODY_SEARCH = 'threat intel'; +export const TAG_REQUEST_BODY = { + type: 'tag', + search: TAG_REQUEST_BODY_SEARCH, + searchFields: ['name'], +}; - const [isPluginDisabled, setIsDashboardPluginDisabled] = useState(false); - const handleDisabledPlugin = useCallback(() => { - if (!isPluginDisabled) { - setIsDashboardPluginDisabled(true); - } - setListItems(getCtiListItemsWithoutLinks(eventCountsByDataset)); - }, [setIsDashboardPluginDisabled, setListItems, eventCountsByDataset, isPluginDisabled]); +export const useCtiDashboardLinks = ({ + to, + from, + tiDataSources = [], +}: { + to: string; + from: string; + tiDataSources?: TiDataSources[]; +}) => { + const [installedDashboardIds, setInstalledDashboardIds] = useState([]); + const dashboardLocator = useKibana().services.dashboard?.locator; + const savedObjectsClient = useKibana().services.savedObjects.client; const handleTagsReceived = useCallback( (TagsSO?) => { @@ -49,9 +44,7 @@ export const useCtiDashboardLinks = ( ); useEffect(() => { - if (!createDashboardUrl || !savedObjectsClient) { - handleDisabledPlugin(); - } else { + if (savedObjectsClient) { savedObjectsClient .find(TAG_REQUEST_BODY) .then(handleTagsReceived) @@ -63,53 +56,40 @@ export const useCtiDashboardLinks = ( }>; }) => { if (DashboardsSO?.savedObjects?.length) { - const dashboardUrls = await Promise.all( - DashboardsSO.savedObjects.map((SO) => - createDashboardUrl({ - dashboardId: SO.id, - timeRange: { - to, - from, - }, - }) - ) + setInstalledDashboardIds( + DashboardsSO.savedObjects.map((SO) => SO.id ?? '').filter(Boolean) ); - const items = DashboardsSO.savedObjects - ?.reduce((acc: LinkPanelListItem[], dashboardSO, i) => { - const item = createLinkFromDashboardSO( - dashboardSO, - eventCountsByDataset, - dashboardUrls[i] - ); - if (isOverviewItem(item)) { - setButtonHref(item.path); - } else if (isLinkPanelListItem(item)) { - acc.push(item); - } - return acc; - }, []) - .sort((a, b) => (a.title > b.title ? 1 : -1)); - setListItems(items); - } else { - handleDisabledPlugin(); } } ); } - }, [ - createDashboardUrl, - eventCountsByDataset, - from, - handleDisabledPlugin, - handleTagsReceived, - isPluginDisabled, - savedObjectsClient, - to, - ]); + }, [handleTagsReceived, savedObjectsClient]); + + const listItems = tiDataSources.map((tiDataSource) => { + const listItem: LinkPanelListItem = { + title: tiDataSource.name, + count: tiDataSource.count, + path: '', + }; + + if ( + tiDataSource.dashboardId && + installedDashboardIds.includes(tiDataSource.dashboardId) && + dashboardLocator + ) { + listItem.path = dashboardLocator.getRedirectUrl({ + dashboardId: tiDataSource.dashboardId, + timeRange: { + to, + from, + }, + }); + } + + return listItem; + }); return { - buttonHref, - isPluginDisabled, listItems, }; }; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts new file mode 100644 index 0000000000000..5686be269121a --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts @@ -0,0 +1,22 @@ +/* + * 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 { useTiDataSources } from './use_ti_data_sources'; + +export const useAllTiDataSources = () => { + const { to, from } = useMemo( + () => ({ + to: new Date().toISOString(), + from: new Date(0).toISOString(), + }), + [] + ); + + const { tiDataSources, isInitiallyLoaded } = useTiDataSources({ to, from }); + + return { tiDataSources, isInitiallyLoaded }; +}; 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 deleted file mode 100644 index c8076ab6a4484..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState, useMemo } from 'react'; -import { useRequestEventCounts } from './use_request_event_counts'; -import { emptyEventCountsByDataset } from './helpers'; -import { CtiEnabledModuleProps } from '../../components/overview_cti_links/cti_enabled_module'; - -export const ID = 'ctiEventCountQuery'; - -export const useCtiEventCounts = ({ deleteQuery, from, setQuery, to }: CtiEnabledModuleProps) => { - 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; - 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 deleted file mode 100644 index 0dc0e8a3fe1f2..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { 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 deleted file mode 100644 index a1bf4d9d35f65..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { convertToBuildEsQuery } from '../../../common/lib/keury'; -import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common'; -import { MatrixHistogramType } from '../../../../common/search_strategy'; -import { EVENT_DATASET } from '../../../../common/cti/constants'; -import { useMatrixHistogram } from '../../../common/containers/matrix_histogram'; -import { useKibana } from '../../../common/lib/kibana'; -import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants'; - -export const useRequestEventCounts = (to: string, from: string) => { - const { uiSettings } = useKibana().services; - const defaultThreatIndices = uiSettings.get(DEFAULT_THREAT_INDEX_KEY); - - const [filterQuery] = convertToBuildEsQuery({ - config: getEsQueryConfig(uiSettings), - indexPattern: { - fields: [ - { - name: 'event.kind', - type: 'string', - }, - ], - title: defaultThreatIndices.toString(), - }, - queries: [{ query: 'event.type:indicator', language: 'kuery' }], - filters: [], - }); - - const matrixHistogramRequest = useMemo(() => { - return { - endDate: to, - errorMessage: i18n.translate('xpack.securitySolution.overview.errorFetchingEvents', { - defaultMessage: 'Error fetching events', - }), - filterQuery, - histogramType: MatrixHistogramType.events, - indexNames: defaultThreatIndices, - stackByField: EVENT_DATASET, - startDate: from, - size: 0, - }; - }, [to, from, filterQuery, defaultThreatIndices]); - - const results = useMatrixHistogram(matrixHistogramRequest); - - return results; -}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts new file mode 100644 index 0000000000000..19ea075aee489 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts @@ -0,0 +1,174 @@ +/* + * 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 { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { useEffect, useState } from 'react'; +import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; +import { useKibana } from '../../../common/lib/kibana'; +import { + DataPublicPluginStart, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/public'; +import { + Bucket, + CtiQueries, + CtiDataSourceStrategyResponse, + CtiDataSourceRequestOptions, +} from '../../../../common/search_strategy/security_solution/cti'; +import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { OTHER_DATA_SOURCE_TITLE } from '../../components/overview_cti_links/translations'; +import { OTHER_TI_DATASET_KEY } from '../../../../common/cti/constants'; + +type GetThreatIntelSourcProps = CtiDataSourceRequestOptions & { + data: DataPublicPluginStart; + signal: AbortSignal; +}; +export const ID = 'ctiEventCountQuery'; + +export const getTiDataSources = ({ + data, + defaultIndex, + timerange, + signal, +}: GetThreatIntelSourcProps): Observable => + data.search.search( + { + defaultIndex, + factoryQueryType: CtiQueries.dataSource, + timerange, + }, + { + strategy: 'securitySolutionSearchStrategy', + abortSignal: signal, + } + ); + +export const getTiDataSourcesComplete = ( + props: GetThreatIntelSourcProps +): Observable => { + return getTiDataSources(props).pipe( + filter((response) => { + return isErrorResponse(response) || isCompleteResponse(response); + }) + ); +}; + +const getTiDataSourcesWithOptionalSignal = withOptionalSignal(getTiDataSourcesComplete); + +export const useTiDataSourcesComplete = () => useObservable(getTiDataSourcesWithOptionalSignal); + +export interface TiDataSources { + dataset: string; + name: string; + count: number; + dashboardId?: string; +} +interface TiDataSourcesProps extends Partial { + allTiDataSources?: TiDataSources[]; +} + +export const useTiDataSources = ({ + to, + from, + allTiDataSources, + setQuery, + deleteQuery, +}: TiDataSourcesProps) => { + const [tiDataSources, setTiDataSources] = useState([]); + const [isInitiallyLoaded, setIsInitiallyLoaded] = useState(false); + const { data, uiSettings } = useKibana().services; + const defaultThreatIndices = uiSettings.get(DEFAULT_THREAT_INDEX_KEY); + const { result, start, loading } = useTiDataSourcesComplete(); + + useEffect(() => { + start({ + data, + timerange: to && from ? { to, from, interval: '' } : undefined, + defaultIndex: defaultThreatIndices, + }); + }, [to, from, start, data, defaultThreatIndices]); + + useEffect(() => { + if (!loading && result?.rawResponse && result?.inspect && setQuery) { + setQuery({ + id: ID, + inspect: { + dsl: result?.inspect?.dsl ?? [], + response: [JSON.stringify(result.rawResponse, null, 2)], + }, + loading, + refetch: () => {}, + }); + } + }, [setQuery, loading, result]); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); + + useEffect(() => { + if (result && !isInitiallyLoaded) { + setIsInitiallyLoaded(true); + } + }, [isInitiallyLoaded, result]); + + useEffect(() => { + if (!loading && result) { + const datasets = result?.rawResponse?.aggregations?.dataset?.buckets ?? []; + const getChildAggregationValue = (aggregation?: Bucket) => aggregation?.buckets?.[0]?.key; + + const integrationMap = datasets.reduce((acc: Record, dataset) => { + const datasetName = getChildAggregationValue(dataset?.name); + if (datasetName) { + return { + ...acc, + [dataset.key]: { + dataset: dataset?.key, + name: datasetName, + dashboardId: getChildAggregationValue(dataset?.dashboard), + count: dataset?.doc_count, + }, + }; + } else { + const otherTiDatasetKey = OTHER_TI_DATASET_KEY; + const otherDatasetCount = acc[otherTiDatasetKey]?.count ?? 0; + return { + ...acc, + [otherTiDatasetKey]: { + dataset: otherTiDatasetKey, + name: OTHER_DATA_SOURCE_TITLE, + count: otherDatasetCount + (dataset?.doc_count ?? 0), + }, + }; + } + }, {}); + + if (Array.isArray(allTiDataSources)) { + allTiDataSources.forEach((integration) => { + if (!integrationMap[integration.dataset]) { + integrationMap[integration.dataset] = { + ...integration, + count: 0, + }; + } + }); + } + + setTiDataSources(Object.values(integrationMap)); + } + }, [result, loading, allTiDataSources]); + + const totalCount = tiDataSources.reduce((acc, val) => acc + val.count, 0); + + return { tiDataSources, totalCount, isInitiallyLoaded }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts new file mode 100644 index 0000000000000..24bdc191b3d66 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts @@ -0,0 +1,55 @@ +/* + * 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 } from 'react'; + +import { installationStatuses } from '../../../../../fleet/common'; +import { TI_INTEGRATION_PREFIX } from '../../../../common/cti/constants'; +import { fetchFleetIntegrations, IntegrationResponse } from './api'; + +export interface Integration { + id: string; + dashboardIds: string[]; +} + +interface TiIntegrationStatus { + allIntegrationsInstalled: boolean; +} + +export const useTiIntegrations = () => { + const [tiIntegrationsStatus, setTiIntegrationsStatus] = useState( + null + ); + + useEffect(() => { + const getPackages = async () => { + try { + const { response: integrations } = await fetchFleetIntegrations(); + const tiIntegrations = integrations.filter((integration: IntegrationResponse) => + integration.id.startsWith(TI_INTEGRATION_PREFIX) + ); + + const allIntegrationsInstalled = tiIntegrations.every( + (integration: IntegrationResponse) => + integration.status === installationStatuses.Installed + ); + + setTiIntegrationsStatus({ + allIntegrationsInstalled, + }); + } catch (e) { + setTiIntegrationsStatus({ + allIntegrationsInstalled: false, + }); + } + }; + + getPackages(); + }, []); + + return tiIntegrationsStatus; +}; 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 33fd1918dad59..ec335062e60bf 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 @@ -20,12 +20,9 @@ import { Overview } from './index'; import { useUserPrivileges } from '../../common/components/user_privileges'; import { useSourcererDataView } 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 { useAllTiDataSources } from '../containers/overview_cti_links/use_all_ti_data_sources'; +import { useTiIntegrations } from '../containers/overview_cti_links/use_ti_integrations'; +import { mockCtiLinksResponse, mockTiDataSources } from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score'; @@ -71,18 +68,17 @@ jest.mock('../../common/components/user_privileges', () => { 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'); 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_all_ti_data_sources'); +const useAllTiDataSourcesMock = useAllTiDataSources as jest.Mock; +useAllTiDataSourcesMock.mockReturnValue(mockTiDataSources); -jest.mock('../containers/overview_cti_links/use_is_threat_intel_module_enabled'); -const useIsThreatIntelModuleEnabledMock = useIsThreatIntelModuleEnabled as jest.Mock; -useIsThreatIntelModuleEnabledMock.mockReturnValue(true); +jest.mock('../containers/overview_cti_links/use_ti_integrations'); +const useTiIntegrationsMock = useTiIntegrations as jest.Mock; +useTiIntegrationsMock.mockReturnValue({}); jest.mock('../containers/overview_risky_host_links/use_hosts_risk_score'); const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; @@ -301,8 +297,8 @@ describe('Overview', () => { }); describe('Threat Intel Dashboard Links', () => { - it('invokes useIsThreatIntelModuleEnabled hook only once', () => { - useIsThreatIntelModuleEnabledMock.mockClear(); + it('invokes useAllTiDataSourcesMock hook only once', () => { + useAllTiDataSourcesMock.mockClear(); mount( @@ -310,7 +306,7 @@ describe('Overview', () => { ); - expect(useIsThreatIntelModuleEnabledMock).toHaveBeenCalledTimes(1); + expect(useAllTiDataSourcesMock).toHaveBeenCalledTimes(1); }); }); }); 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 67ee6c55ac06f..1df49fed07358 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -30,7 +30,8 @@ import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; -import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; +import { useAllTiDataSources } from '../containers/overview_cti_links/use_all_ti_data_sources'; +import { useTiIntegrations } from '../containers/overview_cti_links/use_ti_integrations'; import { useUserPrivileges } from '../../common/components/user_privileges'; import { RiskyHostLinks } from '../components/overview_risky_host_links'; import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges'; @@ -75,7 +76,10 @@ const OverviewComponent = () => { endpointPrivileges: { canAccessFleet }, } = useUserPrivileges(); const { hasIndexRead, hasKibanaREAD } = useAlertsPrivileges(); - const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); + const { tiDataSources: allTiDataSources, isInitiallyLoaded: allTiDataSourcesLoaded } = + useAllTiDataSources(); + const tiIntegrationStatus = useTiIntegrations(); + const isTiLoaded = tiIntegrationStatus && allTiDataSourcesLoaded; const riskyHostsEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); @@ -150,13 +154,16 @@ const OverviewComponent = () => { - + {isTiLoaded && ( + + )} {riskyHostsEnabled && ( diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts index 5857a0417239c..e43af97e84af0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts @@ -9,7 +9,9 @@ import type { FactoryQueryTypes } from '../../../../../common/search_strategy/se import { CtiQueries } from '../../../../../common/search_strategy/security_solution/cti'; import type { SecuritySolutionFactory } from '../types'; import { eventEnrichment } from './event_enrichment'; +import { dataSource } from './threat_intel_source'; export const ctiFactoryTypes: Record> = { [CtiQueries.eventEnrichment]: eventEnrichment, + [CtiQueries.dataSource]: dataSource, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts new file mode 100644 index 0000000000000..6f1c2cc7f60b0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts @@ -0,0 +1,33 @@ +/* + * 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 { SecuritySolutionFactory } from '../../types'; +import { + CtiDataSourceStrategyResponse, + CtiQueries, + CtiDataSourceRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/cti'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { buildTiDataSourceQuery } from './query.threat_intel_source.dsl'; + +export const dataSource: SecuritySolutionFactory = { + buildDsl: (options: CtiDataSourceRequestOptions) => buildTiDataSourceQuery(options), + parse: async ( + options: CtiDataSourceRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildTiDataSourceQuery(options))], + }; + + return { + ...response, + inspect, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts new file mode 100644 index 0000000000000..a0df71e733871 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts @@ -0,0 +1,71 @@ +/* + * 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 { buildTiDataSourceQuery } from './query.threat_intel_source.dsl'; +import { CtiQueries } from '../../../../../../common/search_strategy/security_solution/cti'; + +export const mockOptions = { + defaultIndex: ['logs-ti_*', 'filebeat-8*'], + docValueFields: [], + factoryQueryType: CtiQueries.dataSource, + filterQuery: '', + timerange: { + interval: '12h', + from: '2020-09-06T15:23:52.757Z', + to: '2020-09-07T15:23:52.757Z', + }, +}; + +export const expectedDsl = { + body: { + aggs: { + dataset: { + terms: { + field: 'event.dataset', + }, + aggs: { + name: { + terms: { + field: 'threat.feed.name', + }, + }, + dashboard: { + terms: { + field: 'threat.feed.dashboard_id', + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: '2020-09-06T15:23:52.757Z', + lte: '2020-09-07T15:23:52.757Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + ignore_unavailable: true, + index: ['logs-ti_*', 'filebeat-8*'], + size: 0, + track_total_hits: true, + allow_no_indices: true, +}; + +describe('buildbuildTiDataSourceQueryQuery', () => { + test('build query from options correctly', () => { + expect(buildTiDataSourceQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts new file mode 100644 index 0000000000000..62ad39111b134 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts @@ -0,0 +1,59 @@ +/* + * 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 { CtiDataSourceRequestOptions } from '../../../../../../common/search_strategy/security_solution/cti'; + +export const buildTiDataSourceQuery = ({ + timerange, + defaultIndex, +}: CtiDataSourceRequestOptions) => { + const filter = []; + + if (timerange) { + filter.push({ + range: { + '@timestamp': { + gte: timerange.from, + lte: timerange.to, + format: 'strict_date_optional_time', + }, + }, + }); + } + + const dslQuery = { + size: 0, + index: defaultIndex, + allow_no_indices: true, + ignore_unavailable: true, + track_total_hits: true, + body: { + aggs: { + dataset: { + terms: { field: 'event.dataset' }, + aggs: { + name: { + terms: { field: 'threat.feed.name' }, + }, + dashboard: { + terms: { + field: 'threat.feed.dashboard_id', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 191333688e5b8..ba1200855a413 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23575,7 +23575,6 @@ "xpack.securitySolution.overview.auditBeatProcessTitle": "プロセス", "xpack.securitySolution.overview.auditBeatSocketTitle": "ソケット", "xpack.securitySolution.overview.auditBeatUserTitle": "ユーザー", - "xpack.securitySolution.overview.ctiDashboardDangerPanelButton": "モジュールを有効にする", "xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "表示する脅威インテリジェンスデータがありません", "xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "別のソースからデータを表示するには、filebeat脅威インテリジェンスモジュールを有効にする必要があります。", "xpack.securitySolution.overview.ctiDashboardInfoPanelBody": "このガイドに従い、ダッシュボードを有効にして、ビジュアライゼーションにソースを表示できるようにしてください。", @@ -23597,7 +23596,6 @@ "xpack.securitySolution.overview.endpointNotice.message": "脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", "xpack.securitySolution.overview.endpointNotice.title": "Endpoint Security", "xpack.securitySolution.overview.endpointNotice.tryButton": "Endpoint Securityを試す", - "xpack.securitySolution.overview.errorFetchingEvents": "イベントの取得エラー", "xpack.securitySolution.overview.eventsTitle": "イベント数", "xpack.securitySolution.overview.filebeatCiscoTitle": "Cisco", "xpack.securitySolution.overview.filebeatNetflowTitle": "Netflow", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index da4e428a0bc5d..e10089afd7cb6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23965,7 +23965,6 @@ "xpack.securitySolution.overview.auditBeatProcessTitle": "进程", "xpack.securitySolution.overview.auditBeatSocketTitle": "套接字", "xpack.securitySolution.overview.auditBeatUserTitle": "用户", - "xpack.securitySolution.overview.ctiDashboardDangerPanelButton": "启用模块", "xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "没有可显示的威胁情报数据", "xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "您需要启用 filebeat threatintel 模块,以便查看不同源的数据。", "xpack.securitySolution.overview.ctiDashboardInfoPanelBody": "按照此指南启用您的仪表板,以便可以在可视化中查看您的源。", @@ -23988,7 +23987,6 @@ "xpack.securitySolution.overview.endpointNotice.message": "使用威胁防御、检测和深度安全数据可见性功能保护您的主机。", "xpack.securitySolution.overview.endpointNotice.title": "Endpoint Security", "xpack.securitySolution.overview.endpointNotice.tryButton": "试用 Endpoint Security", - "xpack.securitySolution.overview.errorFetchingEvents": "提取事件时出错", "xpack.securitySolution.overview.eventsTitle": "事件计数", "xpack.securitySolution.overview.filebeatCiscoTitle": "Cisco", "xpack.securitySolution.overview.filebeatNetflowTitle": "NetFlow", diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json index a2e0c2d2921dc..ec5e2aae6e2e2 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -31,6 +31,9 @@ } }, "type": "file" + }, + "feed": { + "name": "AbuseCH malware" } }, "abusemalware": { @@ -72,4 +75,4 @@ } } } -} +} \ No newline at end of file diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json index 8840cd4bee0dd..bc5f6e3db9169 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json @@ -796,6 +796,14 @@ "type": "keyword" } } + }, + "feed":{ + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } } } }