From 6640357eb6e2a4b5db39e468dc315bce74109259 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Fri, 3 Dec 2021 14:54:44 +0100 Subject: [PATCH 01/25] [CTI] Threat Intel Card on Overview page needs to accommodate Fleet TI integrations (#115940) * 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__ Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../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 b33541c5057d..7a88b065d870 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 26bf4ce6740a..a6e7eef88724 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 00cbdb941c11..340093995b29 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 095401ff3142..75ff13b66b29 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 1945b7e3ce3e..bc335ff6680e 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 45d26d9269f6..e2adaaae3554 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 9d404abcf222..9a827b137ae7 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 ed67fdb1c96f..00a225635fb8 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 f6c0fb6f3837..1b8836fc2438 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 2697e4a571ad..36f386e49c5c 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 db83d9e1bcfe..fc36a0c4337c 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 5a40c79d6e5e..a339676ac361 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 8f624dabd64d..000000000000 --- 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 fa7ac50c0876..000000000000 --- 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 a50e3e91ab9e..000000000000 --- 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 f78451e205b1..000000000000 --- 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 dfd9c6c9a7fc..71d6d5eb0c58 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 5348c12fb6c8..c89199c2cb0c 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 1d02acaf65f4..c4cf876cbdc7 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 189f230c02c8..3697d27015fd 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 4a64462b27ad..e112942b0974 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 000000000000..de710c2f1b17 --- /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 000000000000..ad737ac410e3 --- /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 9ac61cc9487e..000000000000 --- 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 a546d20e4958..b1310e363eef 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 000000000000..5686be269121 --- /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 c8076ab6a448..000000000000 --- 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 0dc0e8a3fe1f..000000000000 --- 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 a1bf4d9d35f6..000000000000 --- 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 000000000000..865af2266f2e --- /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'; +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 000000000000..24bdc191b3d6 --- /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 2539490be16f..b38072464c65 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 @@ -23,12 +23,9 @@ import { } 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 { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; @@ -73,18 +70,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; @@ -303,8 +299,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( @@ -312,7 +308,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 67ee6c55ac06..1df49fed0735 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 5857a0417239..e43af97e84af 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 000000000000..0951503b04cd --- /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'; +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 000000000000..832006930a32 --- /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'; + +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 000000000000..08463146a683 --- /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'; + +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 76d3f07facf0..58d04788e98e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23438,7 +23438,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": "このガイドに従い、ダッシュボードを有効にして、ビジュアライゼーションにソースを表示できるようにしてください。", @@ -23460,7 +23459,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 01997e32f243..da71c1796066 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23828,7 +23828,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": "按照此指南启用您的仪表板,以便可以在可视化中查看您的源。", @@ -23851,7 +23850,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 a2e0c2d2921d..ec5e2aae6e2e 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 8840cd4bee0d..bc5f6e3db916 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" + } + } } } } From 4ad3044daedb66319d2231d4b62b332880fb54d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20S=C3=A1nchez?= Date: Fri, 3 Dec 2021 15:35:36 +0100 Subject: [PATCH 02/25] [Security solution] [Endpoint] Adds missing parenthesis to fix trusted apps query (#120326) * Initial commit to add search bar for trusted apps in policy view page * Retrieve all assigned trusted apps to ensure if there are something assigned without the search filters or not. Also fixes unit tests * remove useless if condition * Adds more unit tests and fixes some pr suggestions * Fix weird bug when loading empty state * Fix ts errors due changes in api mocks * Fixes unit test * Remove grid loader to use paginated results one. Fix selectors and tests * Remove unused imports due ts errors * remove unused import * Adds parenthesis to fix a query error. Also fixes related unit test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../policy_details/middleware/policy_trusted_apps_middleware.ts | 2 +- .../trusted_apps/layout/policy_trusted_apps_layout.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts index e9cbda1f487c..1630d63aee5c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_trusted_apps_middleware.ts @@ -157,7 +157,7 @@ const checkIfPolicyHasTrustedAppsAssigned = async ( } try { const policyId = policyIdFromParams(state); - const kuery = `exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all"`; + const kuery = `(exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all")`; const trustedApps = await trustedAppsService.getTrustedAppsList({ page: 1, per_page: 100, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx index 40997de054c7..089bbd4bcb4e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx @@ -136,7 +136,7 @@ describe('Policy trusted apps layout', () => { mockedApis.responseProvider.trustedAppsList.mockImplementation( (options: HttpFetchOptionsWithPath) => { const hasAnyQuery = - 'exception-list-agnostic.attributes.tags:"policy:1234" OR exception-list-agnostic.attributes.tags:"policy:all"'; + '(exception-list-agnostic.attributes.tags:"policy:1234" OR exception-list-agnostic.attributes.tags:"policy:all")'; if (options.query?.filter === hasAnyQuery) { const exceptionsGenerator = new ExceptionsListItemGenerator('seed'); return { From 3b516b6dd1e1fc38ca931f7c04b44e39fa679d24 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Fri, 3 Dec 2021 15:39:48 +0100 Subject: [PATCH 03/25] :bug: Keep the behindText color flag for treemap (#120228) --- .../plugins/lens/public/pie_visualization/render_function.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 539d69207f5f..3b9fdaf09482 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -186,7 +186,7 @@ export function PieComponent( const outputColor = paletteService.get(palette.name).getCategoricalColor( seriesLayers, { - behindText: categoryDisplay !== 'hide', + behindText: categoryDisplay !== 'hide' || isTreemapOrMosaicShape(shape), maxDepth: bucketColumns.length, totalSeries: totalSeriesCount, syncColors, From a0db43e07bfe611bc7d89a234c8511a2a49769fd Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Fri, 3 Dec 2021 10:50:17 -0500 Subject: [PATCH 04/25] Revert " [CTI] Threat Intel Card on Overview page needs to accommodate Fleet TI integrations (#115940)" This reverts commit 6640357eb6e2a4b5db39e468dc315bce74109259. --- .../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, 720 insertions(+), 731 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx create mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx delete mode 100644 x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts create mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts delete mode 100644 x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts delete mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts delete mode 100644 x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts delete 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 7a88b065d870..b33541c5057d 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -58,5 +58,14 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { export const DEFAULT_EVENT_ENRICHMENT_FROM = 'now-30d'; export const DEFAULT_EVENT_ENRICHMENT_TO = 'now'; -export const TI_INTEGRATION_PREFIX = 'ti'; -export const OTHER_TI_DATASET_KEY = '_others_ti_'; +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', +}; 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 a6e7eef88724..26bf4ce6740a 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,16 +5,13 @@ * 2.0. */ -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { IEsSearchResponse, IEsSearchRequest } from 'src/plugins/data/public'; -import { FactoryQueryTypes } from '../..'; +import type { IEsSearchResponse } from 'src/plugins/data/public'; import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../cti/constants'; -import { Inspect, Maybe, TimerangeInput } from '../../common'; +import { Inspect } from '../../common'; import { RequestBasicOptions } from '..'; export enum CtiQueries { eventEnrichment = 'eventEnrichment', - dataSource = 'dataSource', } export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions { @@ -43,33 +40,3 @@ 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 340093995b29..00cbdb941c11 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,8 +72,6 @@ import { CtiEventEnrichmentRequestOptions, CtiEventEnrichmentStrategyResponse, CtiQueries, - CtiDataSourceRequestOptions, - CtiDataSourceStrategyResponse, } from './cti'; import { HostRulesRequestOptions, @@ -87,7 +85,6 @@ import { UserRulesStrategyResponse, } from './ueba'; -export * from './cti'; export * from './hosts'; export * from './matrix_histogram'; export * from './network'; @@ -181,8 +178,6 @@ 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 @@ -243,8 +238,6 @@ 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 75ff13b66b29..095401ff3142 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,8 +10,9 @@ 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_ENABLE_INTEGRATIONS_BUTTON, + OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON, } from '../../screens/overview'; import { loginAndWaitForPage } from '../../tasks/login'; @@ -27,11 +28,12 @@ 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', /app\/integrations\/browse\?q=threat%20intelligence/); + .and('match', /filebeat-module-threatintel.html/); }); describe('enabled threat intel module', () => { @@ -47,16 +49,17 @@ 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_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_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); 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 bc335ff6680e..1945b7e3ce3e 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_ENABLE_INTEGRATIONS_BUTTON = - '[data-test-subj="cti-enable-integrations-button"]'; +export const OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON = '[data-test-subj="cti-view-dashboard-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 e2adaaae3554..45d26d9269f6 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,6 +5,13 @@ * 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 9a827b137ae7..9d404abcf222 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,5 +6,6 @@ */ 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 00a225635fb8..ed67fdb1c96f 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,16 +134,14 @@ 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 1b8836fc2438..f6c0fb6f3837 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,5 +21,4 @@ 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 36f386e49c5c..2697e4a571ad 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,21 +6,24 @@ */ 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 integrationsLink = useIntegrationsPageLink(); + const threatIntelDocLink = `${ + useKibana().services.docLinks.links.filebeat.base + }/filebeat-module-threatintel.html`; 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 fc36a0c4337c..db83d9e1bcfe 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,15 +19,20 @@ import { mockGlobalState, SUB_PLUGINS_REDUCER, } from '../../../common/mock'; -import { mockTheme, mockProps, mockTiDataSources, mockCtiLinksResponse } from './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'; -import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { useRequestEventCounts } from '../../containers/overview_cti_links/use_request_event_counts'; jest.mock('../../../common/lib/kibana'); -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/use_request_event_counts'); +const useRequestEventCountsMock = useRequestEventCounts as jest.Mock; +useRequestEventCountsMock.mockReturnValue([true, {}]); jest.mock('../../containers/overview_cti_links'); const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; @@ -49,12 +54,42 @@ 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(screen.getByText('Showing: 5 indicators')).toBeInTheDocument(); + expect(container.firstChild).toBeNull(); }); }); 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 a339676ac361..5a40c79d6e5e 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,28 +7,37 @@ import React from 'react'; import { ThreatIntelLinkPanelProps } from '.'; -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'; +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 { to, from, allIntegrationsInstalled, allTiDataSources, setQuery, deleteQuery } = props; - const { tiDataSources, totalCount } = useTiDataSources({ - to, - from, - allTiDataSources, - setQuery, - deleteQuery, - }); - const { listItems } = useCtiDashboardLinks({ to, from, tiDataSources }); +export type CtiEnabledModuleProps = Omit; - return ( - - ); +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 ( +
+ +
+ ); + } }; 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 new file mode 100644 index 000000000000..8f624dabd64d --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx @@ -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 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 new file mode 100644 index 000000000000..fa7ac50c0876 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx @@ -0,0 +1,42 @@ +/* + * 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 new file mode 100644 index 000000000000..a50e3e91ab9e --- /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} 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 new file mode 100644 index 000000000000..f78451e205b1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx @@ -0,0 +1,49 @@ +/* + * 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 71d6d5eb0c58..dfd9c6c9a7fc 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, mockTiDataSources, mockCtiLinksResponse } from './mock'; -import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; -import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; +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'; jest.mock('../../../common/lib/kibana'); -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_request_event_counts'); +const useRequestEventCountsMock = useRequestEventCounts as jest.Mock; +useRequestEventCountsMock.mockReturnValue([true, {}]); -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); describe('ThreatIntelLinkPanel', () => { const state: State = mockGlobalState; @@ -49,44 +49,40 @@ 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 Enable source buttons when not all integrations installed', () => { + it('renders CtiDisabledModule when Threat Intel module is disabled', () => { const wrapper = mount( - + ); - expect(wrapper.find('[data-test-subj="cti-enable-integrations-button"]').length).not.toBe(0); + + expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); }); - it('renders CtiDisabledModule when Threat Intel module is disabled', () => { + it('renders null while Threat Intel module state is loading', () => { const wrapper = mount( - + ); - expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); + 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 index c89199c2cb0c..5348c12fb6c8 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,7 +8,6 @@ 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'; @@ -16,26 +15,27 @@ export type ThreatIntelLinkPanelProps = Pick< GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'setQuery' > & { - allIntegrationsInstalled: boolean | undefined; - allTiDataSources: TiDataSources[]; + isThreatIntelModuleEnabled: boolean | undefined; }; const ThreatIntelLinkPanelComponent: React.FC = (props) => { - const { allIntegrationsInstalled, allTiDataSources } = props; - const isThreatIntelModuleEnabled = allTiDataSources.length > 0; - return isThreatIntelModuleEnabled ? ( -
- -
- ) : ( -
- -
- ); + switch (props.isThreatIntelModuleEnabled) { + case true: + return ( +
+ +
+ ); + case false: + return ( +
+ +
+ ); + case undefined: + default: + return null; + } }; 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 c4cf876cbdc7..1d02acaf65f4 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,13 +15,6 @@ 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, @@ -38,6 +31,8 @@ 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' }, @@ -68,10 +63,6 @@ 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 3697d27015fd..189f230c02c8 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_ti_data_sources'; +import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_cti_event_counts'; 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,43 +39,51 @@ const columns: Array> = [ ]; export const ThreatIntelPanelView: React.FC = ({ + buttonHref = '', + isPluginDisabled, isInspectEnabled = true, listItems, splitPanel, totalCount = 0, - allIntegrationsInstalled, }) => { - const integrationsLink = useIntegrationsPageLink(); + const threatIntelDashboardDocLink = `${ + useKibana().services.docLinks.links.filebeat.base + }/load-kibana-dashboards.html`; return ( ( + + {i18n.VIEW_DASHBOARD} + + ), + [buttonHref] + ), columns, dataTestSubj: 'cti-dashboard-links', infoPanel: useMemo( - () => ( - <> - {allIntegrationsInstalled === false ? ( - - {i18n.DANGER_BUTTON} - - } - /> - ) : null} - - ), - [allIntegrationsInstalled, integrationsLink] + () => + isPluginDisabled ? ( + + {i18n.INFO_BUTTON} + + } + /> + ) : null, + [isPluginDisabled, threatIntelDashboardDocLink] ), 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 e112942b0974..4a64462b27ad 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,14 +53,15 @@ export const DANGER_TITLE = i18n.translate( export const DANGER_BODY = i18n.translate( 'xpack.securitySolution.overview.ctiDashboardEnableThreatIntel', { - defaultMessage: 'You need to enable threat intel sources in order to view data.', + defaultMessage: + 'You need to enable the filebeat threatintel module in order to view data from different sources.', } ); export const DANGER_BUTTON = i18n.translate( - 'xpack.securitySolution.overview.ctiDashboardDangerButton', + 'xpack.securitySolution.overview.ctiDashboardDangerPanelButton', { - defaultMessage: 'Enable sources', + defaultMessage: 'Enable Module', } ); @@ -71,17 +72,3 @@ 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 deleted file mode 100644 index de710c2f1b17..000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx +++ /dev/null @@ -1,11 +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 { 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 deleted file mode 100644 index ad737ac410e3..000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts +++ /dev/null @@ -1,28 +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 { 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 new file mode 100644 index 000000000000..9ac61cc9487e --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts @@ -0,0 +1,60 @@ +/* + * 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 b1310e363eef..a546d20e4958 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,30 +6,35 @@ */ 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'; -const TAG_REQUEST_BODY_SEARCH = 'threat intel'; -export const TAG_REQUEST_BODY = { - type: 'tag', - search: TAG_REQUEST_BODY_SEARCH, - searchFields: ['name'], -}; - -export const useCtiDashboardLinks = ({ - to, - from, - tiDataSources = [], -}: { - to: string; - from: string; - tiDataSources?: TiDataSources[]; -}) => { - const [installedDashboardIds, setInstalledDashboardIds] = useState([]); - const dashboardLocator = useKibana().services.dashboard?.locator; +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 [isPluginDisabled, setIsDashboardPluginDisabled] = useState(false); + const handleDisabledPlugin = useCallback(() => { + if (!isPluginDisabled) { + setIsDashboardPluginDisabled(true); + } + setListItems(getCtiListItemsWithoutLinks(eventCountsByDataset)); + }, [setIsDashboardPluginDisabled, setListItems, eventCountsByDataset, isPluginDisabled]); + const handleTagsReceived = useCallback( (TagsSO?) => { if (TagsSO?.savedObjects?.length) { @@ -44,7 +49,9 @@ export const useCtiDashboardLinks = ({ ); useEffect(() => { - if (savedObjectsClient) { + if (!createDashboardUrl || !savedObjectsClient) { + handleDisabledPlugin(); + } else { savedObjectsClient .find(TAG_REQUEST_BODY) .then(handleTagsReceived) @@ -56,40 +63,53 @@ export const useCtiDashboardLinks = ({ }>; }) => { if (DashboardsSO?.savedObjects?.length) { - setInstalledDashboardIds( - DashboardsSO.savedObjects.map((SO) => SO.id ?? '').filter(Boolean) + const dashboardUrls = await Promise.all( + DashboardsSO.savedObjects.map((SO) => + createDashboardUrl({ + dashboardId: SO.id, + timeRange: { + to, + from, + }, + }) + ) ); + 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(); } } ); } - }, [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; - }); + }, [ + createDashboardUrl, + eventCountsByDataset, + from, + handleDisabledPlugin, + handleTagsReceived, + isPluginDisabled, + savedObjectsClient, + to, + ]); 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 deleted file mode 100644 index 5686be269121..000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts +++ /dev/null @@ -1,22 +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 { 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 new file mode 100644 index 000000000000..c8076ab6a448 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts @@ -0,0 +1,64 @@ +/* + * 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 new file mode 100644 index 000000000000..0dc0e8a3fe1f --- /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 000000000000..a1bf4d9d35f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts @@ -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 { 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 deleted file mode 100644 index 865af2266f2e..000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts +++ /dev/null @@ -1,174 +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 { 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'; -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 deleted file mode 100644 index 24bdc191b3d6..000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts +++ /dev/null @@ -1,55 +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 } 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 b38072464c65..2539490be16f 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 @@ -23,9 +23,12 @@ import { } from '../../common/components/user_privileges'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useFetchIndex } from '../../common/containers/source'; -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 { 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'; import { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; @@ -70,17 +73,18 @@ 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_all_ti_data_sources'); -const useAllTiDataSourcesMock = useAllTiDataSources as jest.Mock; -useAllTiDataSourcesMock.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/use_ti_integrations'); -const useTiIntegrationsMock = useTiIntegrations as jest.Mock; -useTiIntegrationsMock.mockReturnValue({}); +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_risky_host_links/use_hosts_risk_score'); const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; @@ -299,8 +303,8 @@ describe('Overview', () => { }); describe('Threat Intel Dashboard Links', () => { - it('invokes useAllTiDataSourcesMock hook only once', () => { - useAllTiDataSourcesMock.mockClear(); + it('invokes useIsThreatIntelModuleEnabled hook only once', () => { + useIsThreatIntelModuleEnabledMock.mockClear(); mount( @@ -308,7 +312,7 @@ describe('Overview', () => { ); - expect(useAllTiDataSourcesMock).toHaveBeenCalledTimes(1); + expect(useIsThreatIntelModuleEnabledMock).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 1df49fed0735..67ee6c55ac06 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -30,8 +30,7 @@ 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 { useAllTiDataSources } from '../containers/overview_cti_links/use_all_ti_data_sources'; -import { useTiIntegrations } from '../containers/overview_cti_links/use_ti_integrations'; +import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; 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'; @@ -76,10 +75,7 @@ const OverviewComponent = () => { endpointPrivileges: { canAccessFleet }, } = useUserPrivileges(); const { hasIndexRead, hasKibanaREAD } = useAlertsPrivileges(); - const { tiDataSources: allTiDataSources, isInitiallyLoaded: allTiDataSourcesLoaded } = - useAllTiDataSources(); - const tiIntegrationStatus = useTiIntegrations(); - const isTiLoaded = tiIntegrationStatus && allTiDataSourcesLoaded; + const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); const riskyHostsEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); @@ -154,16 +150,13 @@ 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 e43af97e84af..5857a0417239 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,9 +9,7 @@ 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 deleted file mode 100644 index 0951503b04cd..000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts +++ /dev/null @@ -1,33 +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 { SecuritySolutionFactory } from '../../types'; -import { - CtiDataSourceStrategyResponse, - CtiQueries, - CtiDataSourceRequestOptions, -} from '../../../../../../common'; -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 deleted file mode 100644 index 832006930a32..000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts +++ /dev/null @@ -1,71 +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 { buildTiDataSourceQuery } from './query.threat_intel_source.dsl'; -import { CtiQueries } from '../../../../../../common'; - -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 deleted file mode 100644 index 08463146a683..000000000000 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts +++ /dev/null @@ -1,59 +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 { CtiDataSourceRequestOptions } from '../../../../../../common'; - -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 58d04788e98e..76d3f07facf0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23438,6 +23438,7 @@ "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": "このガイドに従い、ダッシュボードを有効にして、ビジュアライゼーションにソースを表示できるようにしてください。", @@ -23459,6 +23460,7 @@ "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 da71c1796066..01997e32f243 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23828,6 +23828,7 @@ "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": "按照此指南启用您的仪表板,以便可以在可视化中查看您的源。", @@ -23850,6 +23851,7 @@ "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 ec5e2aae6e2e..a2e0c2d2921d 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,9 +31,6 @@ } }, "type": "file" - }, - "feed": { - "name": "AbuseCH malware" } }, "abusemalware": { @@ -75,4 +72,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 bc5f6e3db916..8840cd4bee0d 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,14 +796,6 @@ "type": "keyword" } } - }, - "feed":{ - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } } } } From 2c4b1ff37130af73093f304bc83c556150cb2ebe Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 3 Dec 2021 15:59:02 +0000 Subject: [PATCH 05/25] fix(NA): @kbn/utils build on windows native environment (#120317) * fix(NA): @kbn/utils build on windows native environment * chore(NA): remove circular dep from @kbn/utils --- packages/kbn-utils/src/path/index.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/kbn-utils/src/path/index.test.ts b/packages/kbn-utils/src/path/index.test.ts index 307d47af9ac5..e4c80a0783b5 100644 --- a/packages/kbn-utils/src/path/index.test.ts +++ b/packages/kbn-utils/src/path/index.test.ts @@ -7,10 +7,17 @@ */ import { accessSync, constants } from 'fs'; -import { createAbsolutePathSerializer } from '@kbn/dev-utils'; import { getConfigPath, getDataPath, getLogsPath, getConfigDirectory } from './'; - -expect.addSnapshotSerializer(createAbsolutePathSerializer()); +import { REPO_ROOT } from '../repo_root'; + +expect.addSnapshotSerializer( + ((rootPath: string = REPO_ROOT, replacement = '') => { + return { + test: (value: any) => typeof value === 'string' && value.startsWith(rootPath), + serialize: (value: string) => value.replace(rootPath, replacement).replace(/\\/g, '/'), + }; + })() +); describe('Default path finder', () => { it('should expose a path to the config directory', () => { From 7ba6e7f68811aed560cf1211e384dc2d57c0c43c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Fri, 3 Dec 2021 16:11:57 +0000 Subject: [PATCH 06/25] [ML] Fixing job selector time range charts (#120343) --- .../public/application/components/job_selector/job_selector.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx index f67a9df4a4a8..4b0d8cdc5509 100644 --- a/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/job_selector/job_selector.tsx @@ -10,6 +10,8 @@ import React, { useState, useEffect, useCallback } from 'react'; import { EuiButtonEmpty, EuiFlexItem, EuiFlexGroup, EuiFlyout } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import './_index.scss'; + import { Dictionary } from '../../../../common/types/common'; import { useUrlState } from '../../util/url_state'; // @ts-ignore From 63bbc45ec24a7441b4cac4441a75147e10cee6a8 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Fri, 3 Dec 2021 11:16:52 -0500 Subject: [PATCH 07/25] [Security Solution][Endpoint] Remove checks for `superuser` role and instead look at fleet kibana privileges (#120027) * Change endpoint privileges to use fleet authz instead of checking for superuser * split user privileges react context component from hook in order to better support mocking * remove `isPlatinumPlus` from endpoint privileges and refactor to use `useUserPrivileges()` hook instead * add `endpointAuthz` to the Server API route handler context * moved fleet's `createFleetAuthzMock` to `fleet/common` --- x-pack/plugins/fleet/common/index.ts | 1 + x-pack/plugins/fleet/common/mocks.ts | 27 ++++- x-pack/plugins/fleet/server/mocks/index.ts | 30 +----- .../server/routes/setup/handlers.test.ts | 4 +- .../endpoint/service/authz/authz.test.ts | 75 +++++++++++++ .../common/endpoint/service/authz/authz.ts | 43 ++++++++ .../common/endpoint/service/authz/index.ts | 9 ++ .../common/endpoint/service/authz/mocks.ts | 29 +++++ .../common/endpoint/types/authz.ts | 27 +++++ .../common/endpoint/types/index.ts | 2 + .../security_solution/public/app/app.tsx | 2 +- .../user_privileges/__mocks__/index.ts | 18 ++++ .../user_privileges/endpoint/index.ts | 2 +- .../user_privileges/endpoint/mocks.ts | 24 ++--- .../endpoint/use_endpoint_privileges.test.ts | 100 ++++-------------- .../endpoint/use_endpoint_privileges.ts | 79 ++++++-------- .../user_privileges/endpoint/utils.ts | 9 +- .../components/user_privileges/index.ts | 13 +++ ...{index.tsx => user_privileges_context.tsx} | 22 ++-- .../public/common/mock/test_providers.tsx | 2 +- .../components/user_info/index.test.tsx | 2 +- .../search_exceptions.test.tsx | 42 ++++---- .../search_exceptions/search_exceptions.tsx | 6 +- .../host_isolation_exceptions_list.test.tsx | 24 ++++- .../view/host_isolation_exceptions_list.tsx | 4 +- .../policy_trusted_apps_empty_unassigned.tsx | 6 +- .../policy_trusted_apps_layout.test.tsx | 4 +- .../layout/policy_trusted_apps_layout.tsx | 10 +- .../list/policy_trusted_apps_list.test.tsx | 28 ++--- .../list/policy_trusted_apps_list.tsx | 15 ++- .../public/overview/pages/overview.test.tsx | 8 +- .../server/endpoint/mocks.ts | 7 +- .../endpoint/routes/actions/isolation.test.ts | 46 +++++--- .../endpoint/routes/actions/isolation.ts | 20 ++-- .../routes/__mocks__/request_context.ts | 2 + .../server/request_context_factory.ts | 37 ++++++- .../plugins/security_solution/server/types.ts | 2 + 37 files changed, 490 insertions(+), 291 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/authz/index.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts create mode 100644 x-pack/plugins/security_solution/common/endpoint/types/authz.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/index.ts rename x-pack/plugins/security_solution/public/common/components/user_privileges/{index.tsx => user_privileges_context.tsx} (81%) diff --git a/x-pack/plugins/fleet/common/index.ts b/x-pack/plugins/fleet/common/index.ts index 611e15032385..46a8e2d01fc9 100644 --- a/x-pack/plugins/fleet/common/index.ts +++ b/x-pack/plugins/fleet/common/index.ts @@ -13,3 +13,4 @@ export * from './services'; export * from './types'; export type { FleetAuthz } from './authz'; export { calculateAuthz } from './authz'; +export { createFleetAuthzMock } from './mocks'; diff --git a/x-pack/plugins/fleet/common/mocks.ts b/x-pack/plugins/fleet/common/mocks.ts index eb81ea2d6a0a..5b71e9b15860 100644 --- a/x-pack/plugins/fleet/common/mocks.ts +++ b/x-pack/plugins/fleet/common/mocks.ts @@ -5,7 +5,8 @@ * 2.0. */ -import type { NewPackagePolicy, PackagePolicy, DeletePackagePoliciesResponse } from './types'; +import type { DeletePackagePoliciesResponse, NewPackagePolicy, PackagePolicy } from './types'; +import type { FleetAuthz } from './authz'; export const createNewPackagePolicyMock = (): NewPackagePolicy => { return { @@ -56,3 +57,27 @@ export const deletePackagePolicyMock = (): DeletePackagePoliciesResponse => { }, ]; }; + +/** + * Creates mock `authz` object + */ +export const createFleetAuthzMock = (): FleetAuthz => { + return { + fleet: { + all: true, + setup: true, + readEnrollmentTokens: true, + }, + integrations: { + readPackageInfo: true, + readInstalledPackages: true, + installPackages: true, + upgradePackages: true, + removePackages: true, + readPackageSettings: true, + writePackageSettings: true, + readIntegrationPolicies: true, + writeIntegrationPolicies: true, + }, + }; +}; diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 90a0addfae49..90c9181b5007 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -7,11 +7,11 @@ import { of } from 'rxjs'; import { + coreMock, elasticsearchServiceMock, loggingSystemMock, - savedObjectsServiceMock, - coreMock, savedObjectsClientMock, + savedObjectsServiceMock, } from '../../../../../src/core/server/mocks'; import { dataPluginMock } from '../../../../../src/plugins/data/server/mocks'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; @@ -21,7 +21,7 @@ import type { PackagePolicyServiceInterface } from '../services/package_policy'; import type { AgentPolicyServiceInterface, PackageService } from '../services'; import type { FleetAppContext } from '../plugin'; import { createMockTelemetryEventsSender } from '../telemetry/__mocks__'; -import type { FleetAuthz } from '../../common'; +import { createFleetAuthzMock } from '../../common'; import { agentServiceMock } from '../services/agents/agent_service.mock'; import type { FleetRequestHandlerContext } from '../types'; @@ -145,27 +145,3 @@ export const createMockPackageService = (): PackageService => { ensureInstalledPackage: jest.fn(), }; }; - -/** - * Creates mock `authz` object - */ -export const createFleetAuthzMock = (): FleetAuthz => { - return { - fleet: { - all: true, - setup: true, - readEnrollmentTokens: true, - }, - integrations: { - readPackageInfo: true, - readInstalledPackages: true, - installPackages: true, - upgradePackages: true, - removePackages: true, - readPackageSettings: true, - writePackageSettings: true, - readIntegrationPolicies: true, - writeIntegrationPolicies: true, - }, - }; -}; diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index d48d80add243..035659185955 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -9,12 +9,14 @@ import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks'; import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; -import { createAppContextStartContractMock, xpackMocks, createFleetAuthzMock } from '../../mocks'; +import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import { agentServiceMock } from '../../services/agents/agent_service.mock'; import { appContextService } from '../../services/app_context'; import { setupFleet } from '../../services/setup'; import type { FleetRequestHandlerContext } from '../../types'; +import { createFleetAuthzMock } from '../../../common'; + import { fleetSetupHandler } from './handlers'; jest.mock('../../services/setup', () => { diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts new file mode 100644 index 000000000000..588366036932 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.test.ts @@ -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 { calculateEndpointAuthz, getEndpointAuthzInitialState } from './authz'; +import { createFleetAuthzMock, FleetAuthz } from '../../../../../fleet/common'; +import { createLicenseServiceMock } from '../../../license/mocks'; +import type { EndpointAuthz } from '../../types/authz'; + +describe('Endpoint Authz service', () => { + let licenseService: ReturnType; + let fleetAuthz: FleetAuthz; + + beforeEach(() => { + licenseService = createLicenseServiceMock(); + fleetAuthz = createFleetAuthzMock(); + }); + + describe('calculateEndpointAuthz()', () => { + describe('and `fleet.all` access is true', () => { + it.each>([ + ['canAccessFleet'], + ['canAccessEndpointManagement'], + ['canIsolateHost'], + ])('should set `%s` to `true`', (authProperty) => { + expect(calculateEndpointAuthz(licenseService, fleetAuthz)[authProperty]).toBe(true); + }); + + it('should set `canIsolateHost` to false if not proper license', () => { + licenseService.isPlatinumPlus.mockReturnValue(false); + + expect(calculateEndpointAuthz(licenseService, fleetAuthz).canIsolateHost).toBe(false); + }); + + it('should set `canUnIsolateHost` to true even if not proper license', () => { + licenseService.isPlatinumPlus.mockReturnValue(false); + + expect(calculateEndpointAuthz(licenseService, fleetAuthz).canUnIsolateHost).toBe(true); + }); + }); + + describe('and `fleet.all` access is false', () => { + beforeEach(() => (fleetAuthz.fleet.all = false)); + + it.each>([ + ['canAccessFleet'], + ['canAccessEndpointManagement'], + ['canIsolateHost'], + ])('should set `%s` to `false`', (authProperty) => { + expect(calculateEndpointAuthz(licenseService, fleetAuthz)[authProperty]).toBe(false); + }); + + it('should set `canUnIsolateHost` to true even if not proper license', () => { + licenseService.isPlatinumPlus.mockReturnValue(false); + + expect(calculateEndpointAuthz(licenseService, fleetAuthz).canUnIsolateHost).toBe(true); + }); + }); + }); + + describe('getEndpointAuthzInitialState()', () => { + it('returns expected initial state', () => { + expect(getEndpointAuthzInitialState()).toEqual({ + canAccessFleet: false, + canAccessEndpointManagement: false, + canIsolateHost: false, + canUnIsolateHost: true, + canCreateArtifactsByPolicy: false, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts new file mode 100644 index 000000000000..766843311cfd --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/authz.ts @@ -0,0 +1,43 @@ +/* + * 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 { LicenseService } from '../../../license'; +import { FleetAuthz } from '../../../../../fleet/common'; +import { EndpointAuthz } from '../../types/authz'; + +/** + * Used by both the server and the UI to generate the Authorization for access to Endpoint related + * functionality + * + * @param licenseService + * @param fleetAuthz + */ +export const calculateEndpointAuthz = ( + licenseService: LicenseService, + fleetAuthz: FleetAuthz +): EndpointAuthz => { + const isPlatinumPlusLicense = licenseService.isPlatinumPlus(); + const hasAllAccessToFleet = fleetAuthz.fleet.all; + + return { + canAccessFleet: hasAllAccessToFleet, + canAccessEndpointManagement: hasAllAccessToFleet, + canCreateArtifactsByPolicy: isPlatinumPlusLicense, + canIsolateHost: isPlatinumPlusLicense && hasAllAccessToFleet, + canUnIsolateHost: true, + }; +}; + +export const getEndpointAuthzInitialState = (): EndpointAuthz => { + return { + canAccessFleet: false, + canAccessEndpointManagement: false, + canCreateArtifactsByPolicy: false, + canIsolateHost: false, + canUnIsolateHost: true, + }; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/index.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/index.ts new file mode 100644 index 000000000000..975d28eb9dcb --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export { getEndpointAuthzInitialState, calculateEndpointAuthz } from './authz'; +export { getEndpointAuthzInitialStateMock } from './mocks'; diff --git a/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts b/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts new file mode 100644 index 000000000000..7f1a6f969272 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/service/authz/mocks.ts @@ -0,0 +1,29 @@ +/* + * 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 { EndpointAuthz } from '../../types/authz'; +import { getEndpointAuthzInitialState } from './authz'; + +export const getEndpointAuthzInitialStateMock = ( + overrides: Partial = {} +): EndpointAuthz => { + const authz: EndpointAuthz = { + ...( + Object.entries(getEndpointAuthzInitialState()) as Array<[keyof EndpointAuthz, boolean]> + ).reduce((mockPrivileges, [key, value]) => { + // Invert the initial values (from `false` to `true`) so that everything is authorized + mockPrivileges[key] = !value; + + return mockPrivileges; + }, {} as EndpointAuthz), + // this one is currently treated special in that everyone can un-isolate + canUnIsolateHost: true, + ...overrides, + }; + + return authz; +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/authz.ts b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts new file mode 100644 index 000000000000..da0a372db8aa --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/types/authz.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Set of Endpoint Specific privileges that control application authorization. This interface is + * used both on the client and server for consistency + */ +export interface EndpointAuthz { + /** If user has permissions to access Fleet */ + canAccessFleet: boolean; + /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ + canAccessEndpointManagement: boolean; + /** if user has permissions to create Artifacts by Policy */ + canCreateArtifactsByPolicy: boolean; + /** If user has permissions to isolate hosts */ + canIsolateHost: boolean; + /** If user has permissions to un-isolate (release) hosts */ + canUnIsolateHost: boolean; +} + +export interface EndpointPrivileges extends EndpointAuthz { + loading: boolean; +} diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index c869c9c780bd..1fce6f17bdea 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -1246,3 +1246,5 @@ interface BaseListResponse { * Returned by the server via GET /api/endpoint/metadata */ export type MetadataListResponse = BaseListResponse; + +export type { EndpointPrivileges } from './authz'; diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index 78a340d6bbca..6d5f81b07656 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -25,7 +25,7 @@ import { State } from '../common/store'; import { StartServices } from '../types'; import { PageRouter } from './routes'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { UserPrivilegesProvider } from '../common/components/user_privileges'; +import { UserPrivilegesProvider } from '../common/components/user_privileges/user_privileges_context'; interface StartAppComponent { children: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/index.ts new file mode 100644 index 000000000000..dc77a6b9eea8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { initialUserPrivilegesState, UserPrivilegesState } from '../user_privileges_context'; +import { getEndpointPrivilegesInitialStateMock } from '../endpoint/mocks'; + +export const useUserPrivileges = jest.fn(() => { + const mockedPrivileges: UserPrivilegesState = { + ...initialUserPrivilegesState(), + endpointPrivileges: getEndpointPrivilegesInitialStateMock(), + }; + + return mockedPrivileges; +}); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts index adea89ce1a05..83443dc20b9b 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export * from './use_endpoint_privileges'; +export { useEndpointPrivileges } from './use_endpoint_privileges'; export { getEndpointPrivilegesInitialState } from './utils'; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts index 2851c92816ce..2348fdf017c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts @@ -5,24 +5,16 @@ * 2.0. */ -import type { EndpointPrivileges } from './use_endpoint_privileges'; -import { getEndpointPrivilegesInitialState } from './utils'; +import { EndpointPrivileges } from '../../../../../common/endpoint/types'; +import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz/mocks'; -export const getEndpointPrivilegesInitialStateMock = ( - overrides: Partial = {} -): EndpointPrivileges => { - // Get the initial state and set all permissions to `true` (enabled) for testing +export const getEndpointPrivilegesInitialStateMock = ({ + loading = false, + ...overrides +}: Partial = {}): EndpointPrivileges => { const endpointPrivilegesMock: EndpointPrivileges = { - ...( - Object.entries(getEndpointPrivilegesInitialState()) as Array< - [keyof EndpointPrivileges, boolean] - > - ).reduce((mockPrivileges, [key, value]) => { - mockPrivileges[key] = !value; - - return mockPrivileges; - }, {} as EndpointPrivileges), - ...overrides, + ...getEndpointAuthzInitialStateMock(overrides), + loading, }; return endpointPrivilegesMock; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index d4ba29a4ef95..4daef6cca45b 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -6,14 +6,14 @@ */ import { act, renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; -import { useHttp, useCurrentUser } from '../../../lib/kibana'; -import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +import { useCurrentUser, useKibana } from '../../../lib/kibana'; +import { useEndpointPrivileges } from './use_endpoint_privileges'; import { securityMock } from '../../../../../../security/public/mocks'; -import { appRoutesService } from '../../../../../../fleet/common'; import { AuthenticatedUser } from '../../../../../../security/common'; import { licenseService } from '../../../hooks/use_license'; -import { fleetGetCheckPermissionsHttpMock } from '../../../../management/pages/mocks'; import { getEndpointPrivilegesInitialStateMock } from './mocks'; +import { EndpointPrivileges } from '../../../../../common/endpoint/types'; +import { getEndpointPrivilegesInitialState } from './utils'; jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_license', () => { @@ -32,10 +32,9 @@ const licenseServiceMock = licenseService as jest.Mocked; describe('When using useEndpointPrivileges hook', () => { let authenticatedUser: AuthenticatedUser; - let fleetApiMock: ReturnType; let result: RenderResult; let unmount: ReturnType['unmount']; - let waitForNextUpdate: ReturnType['waitForNextUpdate']; + let releaseFleetAuthz: () => void; let render: () => RenderHookResult; beforeEach(() => { @@ -45,14 +44,19 @@ describe('When using useEndpointPrivileges hook', () => { (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); - fleetApiMock = fleetGetCheckPermissionsHttpMock( - useHttp() as Parameters[0] - ); licenseServiceMock.isPlatinumPlus.mockReturnValue(true); + // Add a daly to fleet service that provides authz information + const fleetAuthz = useKibana().services.fleet!.authz; + + // Add a delay to the fleet Authz promise to test out the `loading` property + useKibana().services.fleet!.authz = new Promise((resolve) => { + releaseFleetAuthz = () => resolve(fleetAuthz); + }); + render = () => { const hookRenderResponse = renderHook(() => useEndpointPrivileges()); - ({ result, unmount, waitForNextUpdate } = hookRenderResponse); + ({ result, unmount } = hookRenderResponse); return hookRenderResponse; }; }); @@ -62,88 +66,22 @@ describe('When using useEndpointPrivileges hook', () => { }); it('should return `loading: true` while retrieving privileges', async () => { - // Add a daly to the API response that we can control from the test - let releaseApiResponse: () => void; - fleetApiMock.responseProvider.checkPermissions.mockDelay.mockReturnValue( - new Promise((resolve) => { - releaseApiResponse = () => resolve(); - }) - ); (useCurrentUser as jest.Mock).mockReturnValue(null); const { rerender } = render(); - expect(result.current).toEqual( - getEndpointPrivilegesInitialStateMock({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - }) - ); + expect(result.current).toEqual(getEndpointPrivilegesInitialState()); // Make user service available (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); rerender(); - expect(result.current).toEqual( - getEndpointPrivilegesInitialStateMock({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - }) - ); + expect(result.current).toEqual(getEndpointPrivilegesInitialState()); // Release the API response await act(async () => { - fleetApiMock.waitForApi(); - releaseApiResponse!(); + releaseFleetAuthz(); + await useKibana().services.fleet!.authz; }); - expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock()); - }); - - it('should call Fleet permissions api to determine user privilege to fleet', async () => { - render(); - await waitForNextUpdate(); - await fleetApiMock.waitForApi(); - expect(useHttp().get as jest.Mock).toHaveBeenCalledWith( - appRoutesService.getCheckPermissionsPath() - ); - }); - it('should set privileges to false if user does not have superuser role', async () => { - authenticatedUser.roles = []; - render(); - await waitForNextUpdate(); - await fleetApiMock.waitForApi(); - expect(result.current).toEqual( - getEndpointPrivilegesInitialStateMock({ - canAccessEndpointManagement: false, - }) - ); - }); - - it('should set privileges to false if fleet api check returns failure', async () => { - fleetApiMock.responseProvider.checkPermissions.mockReturnValue({ - error: 'MISSING_SECURITY', - success: false, - }); - - render(); - await waitForNextUpdate(); - await fleetApiMock.waitForApi(); - expect(result.current).toEqual( - getEndpointPrivilegesInitialStateMock({ - canAccessEndpointManagement: false, - canAccessFleet: false, - }) - ); + expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock()); }); - - it.each([['canIsolateHost'], ['canCreateArtifactsByPolicy']])( - 'should set %s to false if license is not PlatinumPlus', - async (privilege) => { - licenseServiceMock.isPlatinumPlus.mockReturnValue(false); - render(); - await waitForNextUpdate(); - expect(result.current).toEqual(expect.objectContaining({ [privilege]: false })); - } - ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts index 448cb215941d..6fa0c51f500d 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts @@ -6,24 +6,14 @@ */ import { useEffect, useMemo, useRef, useState } from 'react'; -import { useCurrentUser, useHttp } from '../../../lib/kibana'; -import { appRoutesService, CheckPermissionsResponse } from '../../../../../../fleet/common'; +import { useCurrentUser, useKibana } from '../../../lib/kibana'; import { useLicense } from '../../../hooks/use_license'; -import { Immutable } from '../../../../../common/endpoint/types'; - -export interface EndpointPrivileges { - loading: boolean; - /** If user has permissions to access Fleet */ - canAccessFleet: boolean; - /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ - canAccessEndpointManagement: boolean; - /** if user has permissions to create Artifacts by Policy */ - canCreateArtifactsByPolicy: boolean; - /** If user has permissions to use the Host isolation feature */ - canIsolateHost: boolean; - /** @deprecated do not use. instead, use one of the other privileges defined */ - isPlatinumPlus: boolean; -} +import { EndpointPrivileges, Immutable } from '../../../../../common/endpoint/types'; +import { + calculateEndpointAuthz, + getEndpointAuthzInitialState, +} from '../../../../../common/endpoint/service/authz'; +import { FleetAuthz } from '../../../../../../fleet/common'; /** * Retrieve the endpoint privileges for the current user. @@ -32,23 +22,39 @@ export interface EndpointPrivileges { * to keep API calls to a minimum. */ export const useEndpointPrivileges = (): Immutable => { - const http = useHttp(); const user = useCurrentUser(); + const fleetServices = useKibana().services.fleet; const isMounted = useRef(true); - const isPlatinumPlusLicense = useLicense().isPlatinumPlus(); - const [canAccessFleet, setCanAccessFleet] = useState(false); + const licenseService = useLicense(); const [fleetCheckDone, setFleetCheckDone] = useState(false); + const [fleetAuthz, setFleetAuthz] = useState(null); + + const privileges = useMemo(() => { + const privilegeList: EndpointPrivileges = Object.freeze({ + loading: !fleetCheckDone || !user, + ...(fleetAuthz + ? calculateEndpointAuthz(licenseService, fleetAuthz) + : getEndpointAuthzInitialState()), + }); + + return privilegeList; + }, [fleetCheckDone, user, fleetAuthz, licenseService]); // Check if user can access fleet useEffect(() => { + if (!fleetServices) { + setFleetCheckDone(true); + return; + } + + setFleetCheckDone(false); + (async () => { try { - const fleetPermissionsResponse = await http.get( - appRoutesService.getCheckPermissionsPath() - ); + const fleetAuthzForCurrentUser = await fleetServices.authz; if (isMounted.current) { - setCanAccessFleet(fleetPermissionsResponse.success); + setFleetAuthz(fleetAuthzForCurrentUser); } } finally { if (isMounted.current) { @@ -56,30 +62,7 @@ export const useEndpointPrivileges = (): Immutable => { } } })(); - }, [http]); - - // Check if user has `superuser` role - const isSuperUser = useMemo(() => { - if (user?.roles) { - return user.roles.includes('superuser'); - } - return false; - }, [user?.roles]); - - const privileges = useMemo(() => { - const privilegeList: EndpointPrivileges = Object.freeze({ - loading: !fleetCheckDone || !user, - canAccessFleet, - canAccessEndpointManagement: canAccessFleet && isSuperUser, - canCreateArtifactsByPolicy: isPlatinumPlusLicense, - canIsolateHost: isPlatinumPlusLicense, - // FIXME: Remove usages of the property below - /** @deprecated */ - isPlatinumPlus: isPlatinumPlusLicense, - }); - - return privilegeList; - }, [canAccessFleet, fleetCheckDone, isSuperUser, user, isPlatinumPlusLicense]); + }, [fleetServices]); // Capture if component is unmounted useEffect( diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts index df91314479f1..0c314ba5573c 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts @@ -5,15 +5,12 @@ * 2.0. */ -import { EndpointPrivileges } from './use_endpoint_privileges'; +import { EndpointPrivileges } from '../../../../../common/endpoint/types'; +import { getEndpointAuthzInitialState } from '../../../../../common/endpoint/service/authz'; export const getEndpointPrivilegesInitialState = (): EndpointPrivileges => { return { loading: true, - canAccessFleet: false, - canAccessEndpointManagement: false, - canIsolateHost: false, - canCreateArtifactsByPolicy: false, - isPlatinumPlus: false, + ...getEndpointAuthzInitialState(), }; }; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.ts new file mode 100644 index 000000000000..3a5d942d3b53 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.ts @@ -0,0 +1,13 @@ +/* + * 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 { useContext } from 'react'; +import { DeepReadonly } from 'utility-types'; +import { UserPrivilegesContext, UserPrivilegesState } from './user_privileges_context'; + +export const useUserPrivileges = (): DeepReadonly => + useContext(UserPrivilegesContext); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx similarity index 81% rename from x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx rename to x-pack/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx index 05ccadeaf67a..5c681e5dbbae 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/user_privileges_context.tsx @@ -5,16 +5,14 @@ * 2.0. */ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { DeepReadonly } from 'utility-types'; - -import { Capabilities } from '../../../../../../../src/core/public'; -import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; +import React, { createContext, useEffect, useState } from 'react'; +import { Capabilities } from '../../../../../../../src/core/types'; +import { SERVER_APP_ID } from '../../../../common/constants'; import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; -import { EndpointPrivileges, useEndpointPrivileges } from './endpoint'; +import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; +import { getEndpointPrivilegesInitialState, useEndpointPrivileges } from './endpoint'; +import { EndpointPrivileges } from '../../../../common/endpoint/types'; -import { SERVER_APP_ID } from '../../../../common/constants'; -import { getEndpointPrivilegesInitialState } from './endpoint/utils'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; @@ -28,8 +26,9 @@ export const initialUserPrivilegesState = (): UserPrivilegesState => ({ endpointPrivileges: getEndpointPrivilegesInitialState(), kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); - -const UserPrivilegesContext = createContext(initialUserPrivilegesState()); +export const UserPrivilegesContext = createContext( + initialUserPrivilegesState() +); interface UserPrivilegesProviderProps { kibanaCapabilities: Capabilities; @@ -73,6 +72,3 @@ export const UserPrivilegesProvider = ({ ); }; - -export const useUserPrivileges = (): DeepReadonly => - useContext(UserPrivilegesContext); diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 528592051ccc..9ad5abc1c7ed 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -25,8 +25,8 @@ import { import { FieldHook } from '../../shared_imports'; import { SUB_PLUGINS_REDUCER } from './utils'; import { createSecuritySolutionStorageMock, localStorageMock } from './mock_local_storage'; -import { UserPrivilegesProvider } from '../components/user_privileges'; import { CASES_FEATURE_ID } from '../../../common/constants'; +import { UserPrivilegesProvider } from '../components/user_privileges/user_privileges_context'; const state: State = mockGlobalState; diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 0447130e1bd1..32911a2c8e4a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -13,7 +13,7 @@ import { Capabilities } from 'src/core/public'; import { useKibana } from '../../../common/lib/kibana'; import * as api from '../../containers/detection_engine/alerts/api'; import { TestProviders } from '../../../common/mock/test_providers'; -import { UserPrivilegesProvider } from '../../../common/components/user_privileges'; +import { UserPrivilegesProvider } from '../../../common/components/user_privileges/user_privileges_context'; jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/detection_engine/alerts/api'); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index 3b987a721141..493b41bc0165 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -8,18 +8,21 @@ import React from 'react'; import { act, fireEvent } from '@testing-library/react'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; -import { - EndpointPrivileges, - useEndpointPrivileges, -} from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; import { SearchExceptions, SearchExceptionsProps } from '.'; import { getEndpointPrivilegesInitialStateMock } from '../../../common/components/user_privileges/endpoint/mocks'; -jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); +import { + initialUserPrivilegesState, + UserPrivilegesState, +} from '../../../common/components/user_privileges/user_privileges_context'; +import { EndpointPrivileges } from '../../../../common/endpoint/types'; + +jest.mock('../../../common/components/user_privileges'); let onSearchMock: jest.Mock; -const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; +const mockUseUserPrivileges = useUserPrivileges as jest.Mock; describe('Search exceptions', () => { let appTestContext: AppContextTestRender; @@ -28,13 +31,16 @@ describe('Search exceptions', () => { props?: Partial ) => ReturnType; - const loadedUserEndpointPrivilegesState = ( + const loadedUserPrivilegesState = ( endpointOverrides: Partial = {} - ): EndpointPrivileges => - getEndpointPrivilegesInitialStateMock({ - isPlatinumPlus: false, - ...endpointOverrides, - }); + ): UserPrivilegesState => { + return { + ...initialUserPrivilegesState(), + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ + ...endpointOverrides, + }), + }; + }; beforeEach(() => { onSearchMock = jest.fn(); @@ -51,11 +57,11 @@ describe('Search exceptions', () => { return renderResult; }; - mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState()); + mockUseUserPrivileges.mockReturnValue(loadedUserPrivilegesState()); }); afterAll(() => { - mockUseEndpointPrivileges.mockReset(); + mockUseUserPrivileges.mockReset(); }); it('should have a default value', () => { @@ -102,8 +108,8 @@ describe('Search exceptions', () => { it('should hide policies selector when no license', () => { const generator = new EndpointDocGenerator('policy-list'); const policy = generator.generatePolicyPackagePolicy(); - mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ isPlatinumPlus: false }) + mockUseUserPrivileges.mockReturnValue( + loadedUserPrivilegesState({ canCreateArtifactsByPolicy: false }) ); const element = render({ policyList: [policy], hasPolicyFilter: true }); @@ -113,8 +119,8 @@ describe('Search exceptions', () => { it('should display policies selector when right license', () => { const generator = new EndpointDocGenerator('policy-list'); const policy = generator.generatePolicyPackagePolicy(); - mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ isPlatinumPlus: true }) + mockUseUserPrivileges.mockReturnValue( + loadedUserPrivilegesState({ canCreateArtifactsByPolicy: true }) ); const element = render({ policyList: [policy], hasPolicyFilter: true }); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx index 569916ac2031..5489f7a394c9 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/e import { i18n } from '@kbn/i18n'; import { PolicySelectionItem, PoliciesSelector } from '../policies_selector'; import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types'; -import { useEndpointPrivileges } from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { useUserPrivileges } from '../../../common/components/user_privileges'; export interface SearchExceptionsProps { defaultValue?: string; @@ -34,7 +34,7 @@ export const SearchExceptions = memo( defaultExcludedPolicies, hideRefreshButton = false, }) => { - const { isPlatinumPlus } = useEndpointPrivileges(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const [query, setQuery] = useState(defaultValue); const [includedPolicies, setIncludedPolicies] = useState(defaultIncludedPolicies || ''); const [excludedPolicies, setExcludedPolicies] = useState(defaultExcludedPolicies || ''); @@ -92,7 +92,7 @@ export const SearchExceptions = memo( data-test-subj="searchField" /> - {isPlatinumPlus && hasPolicyFilter && policyList ? ( + {canCreateArtifactsByPolicy && hasPolicyFilter && policyList ? ( { let history: AppContextTestRender['history']; let mockedContext: AppContextTestRender; - const useEndpointPrivilegesMock = useEndpointPrivileges as jest.Mock; + const useUserPrivilegesMock = _useUserPrivileges as jest.Mock; + + const setEndpointPrivileges = (overrides: Partial = {}) => { + const newPrivileges = _useUserPrivileges(); + + useUserPrivilegesMock.mockReturnValue({ + ...newPrivileges, + endpointPrivileges: { + ...newPrivileges.endpointPrivileges, + ...overrides, + }, + }); + }; + const waitForApiCall = () => { return waitFor(() => expect(getHostIsolationExceptionItemsMock).toHaveBeenCalled()); }; @@ -162,7 +176,7 @@ describe('When on the host isolation exceptions page', () => { describe('has canIsolateHost privileges', () => { beforeEach(async () => { - useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: true }); + setEndpointPrivileges({ canIsolateHost: true }); getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); }); @@ -185,7 +199,7 @@ describe('When on the host isolation exceptions page', () => { describe('does not have canIsolateHost privileges', () => { beforeEach(() => { - useEndpointPrivilegesMock.mockReturnValue({ canIsolateHost: false }); + setEndpointPrivileges({ canIsolateHost: false }); }); it('should not show the create flyout if the user navigates to the create url', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index a9da5c6d135a..816aef5ca2dc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -31,11 +31,11 @@ import { EDIT_HOST_ISOLATION_EXCEPTION_LABEL, } from './components/translations'; import { getEndpointListPath } from '../../../common/routing'; -import { useEndpointPrivileges } from '../../../../common/components/user_privileges/endpoint'; import { MANAGEMENT_DEFAULT_PAGE_SIZE, MANAGEMENT_PAGE_SIZE_OPTIONS, } from '../../../common/constants'; +import { useUserPrivileges } from '../../../../common/components/user_privileges'; type HostIsolationExceptionPaginatedContent = PaginatedContentProps< Immutable, @@ -44,7 +44,7 @@ type HostIsolationExceptionPaginatedContent = PaginatedContentProps< export const HostIsolationExceptionsList = () => { const history = useHistory(); - const privileges = useEndpointPrivileges(); + const privileges = useUserPrivileges().endpointPrivileges; const location = useHostIsolationExceptionsSelector(getCurrentLocation); const navigateCallback = useHostIsolationExceptionsNavigateCallback(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx index 3252c5a27d85..3a7308fef75f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n-react'; import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; interface CommonProps { policyId: string; @@ -18,7 +18,7 @@ interface CommonProps { } export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, policyName }) => { - const { isPlatinumPlus } = useEndpointPrivileges(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const navigateCallback = usePolicyDetailsNavigateCallback(); const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); const onClickPrimaryButtonHandler = useCallback( @@ -49,7 +49,7 @@ export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, p /> } actions={[ - ...(isPlatinumPlus + ...(canCreateArtifactsByPolicy ? [ { it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { mockUseEndpointPrivileges.mockReturnValue( getEndpointPrivilegesInitialStateMock({ - isPlatinumPlus: false, + canCreateArtifactsByPolicy: false, }) ); const component = render(); @@ -184,7 +184,7 @@ describe('Policy trusted apps layout', () => { it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => { mockUseEndpointPrivileges.mockReturnValue( getEndpointPrivilegesInitialStateMock({ - isPlatinumPlus: false, + canCreateArtifactsByPolicy: false, }) ); const component = render(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx index f39b080e56e3..3cf8e60c5e16 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -32,10 +32,10 @@ import { import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyTrustedAppsFlyout } from '../flyout'; import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_UI_ID } from '../../../../../../../common/constants'; import { getTrustedAppsListPath } from '../../../../../common/routing'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; export const PolicyTrustedAppsLayout = React.memo(() => { const { getAppUrl } = useAppUrl(); @@ -44,7 +44,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { const isDoesTrustedAppExistsLoading = usePolicyDetailsSelector(doesTrustedAppExistsLoading); const policyItem = usePolicyDetailsSelector(policyDetails); const navigateCallback = usePolicyDetailsNavigateCallback(); - const { isPlatinumPlus } = useEndpointPrivileges(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const totalAssignedCount = usePolicyDetailsSelector(getTotalPolicyTrustedAppsListPagination); const hasTrustedApps = usePolicyDetailsSelector(getHasTrustedApps); const isLoadedHasTrustedApps = usePolicyDetailsSelector(getIsLoadedHasTrustedApps); @@ -138,7 +138,9 @@ export const PolicyTrustedAppsLayout = React.memo(() => { - {isPlatinumPlus && assignTrustedAppButton} + + {canCreateArtifactsByPolicy && assignTrustedAppButton} + @@ -169,7 +171,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { )} - {isPlatinumPlus && showListFlyout ? : null} + {canCreateArtifactsByPolicy && showListFlyout ? : null} ) : null; }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index 7410dd20d928..32568ec2b48e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -16,14 +16,12 @@ import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils'; import { isFailedResourceState, isLoadedResourceState } from '../../../../../state'; import { fireEvent, within, act, waitFor } from '@testing-library/react'; import { APP_UI_ID } from '../../../../../../../common/constants'; -import { - EndpointPrivileges, - useEndpointPrivileges, -} from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; +import { EndpointPrivileges } from '../../../../../../../common/endpoint/types'; -jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); -const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; +jest.mock('../../../../../../common/components/user_privileges'); +const mockUseUserPrivileges = useUserPrivileges as jest.Mock; describe('when rendering the PolicyTrustedAppsList', () => { // The index (zero based) of the card created by the generator that is policy specific @@ -78,11 +76,14 @@ describe('when rendering the PolicyTrustedAppsList', () => { }; afterAll(() => { - mockUseEndpointPrivileges.mockReset(); + mockUseUserPrivileges.mockReset(); }); beforeEach(() => { appTestContext = createAppRootMockRenderer(); - mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState()); + mockUseUserPrivileges.mockReturnValue({ + ...mockUseUserPrivileges(), + endpointPrivileges: loadedUserEndpointPrivilegesState(), + }); mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http); appTestContext.setExperimentalFlag({ trustedAppsByPolicyEnabled: true }); @@ -317,11 +318,12 @@ describe('when rendering the PolicyTrustedAppsList', () => { }); it('does not show remove option in actions menu if license is downgraded to gold or below', async () => { - mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ - isPlatinumPlus: false, - }) - ); + mockUseUserPrivileges.mockReturnValue({ + ...mockUseUserPrivileges(), + endpointPrivileges: loadedUserEndpointPrivilegesState({ + canCreateArtifactsByPolicy: false, + }), + }); await render(); await toggleCardActionMenu(POLICY_SPECIFIC_CARD_INDEX); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index 3453bc529b27..fa4d4e40b3e5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -38,7 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { useUserPrivileges } from '../../../../../../common/components/user_privileges'; const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; @@ -52,7 +52,7 @@ export const PolicyTrustedAppsList = memo( const toasts = useToasts(); const history = useHistory(); const { getAppUrl } = useAppUrl(); - const { isPlatinumPlus } = useEndpointPrivileges(); + const { canCreateArtifactsByPolicy } = useUserPrivileges().endpointPrivileges; const policyId = usePolicyDetailsSelector(policyIdFromParams); const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading); const defaultFilter = usePolicyDetailsSelector(getCurrentPolicyArtifactsFilter); @@ -158,7 +158,7 @@ export const PolicyTrustedAppsList = memo( ]; const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = { expanded: Boolean(isCardExpanded[trustedApp.id]), - actions: isPlatinumPlus + actions: canCreateArtifactsByPolicy ? [ ...fullDetailsAction, { @@ -194,7 +194,14 @@ export const PolicyTrustedAppsList = memo( } return newCardProps; - }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems, isPlatinumPlus]); + }, [ + allPoliciesById, + getAppUrl, + getTestId, + isCardExpanded, + trustedAppItems, + canCreateArtifactsByPolicy, + ]); const provideCardProps = useCallback['cardComponentProps']>( (item) => { 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 2539490be16f..33fd1918dad5 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 @@ -17,10 +17,7 @@ import { UseMessagesStorage, } from '../../common/containers/local_storage/use_messages_storage'; import { Overview } from './index'; -import { - initialUserPrivilegesState, - useUserPrivileges, -} from '../../common/components/user_privileges'; +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'; @@ -30,9 +27,10 @@ import { mockCtiLinksResponse, } from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; -import { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score'; +import { initialUserPrivilegesState } from '../../common/components/user_privileges/user_privileges_context'; +import { EndpointPrivileges } from '../../../common/endpoint/types'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 9b9d72805425..dce08e2522be 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -17,9 +17,8 @@ import { createMockAgentPolicyService, createMockAgentService, createArtifactsClientMock, - createFleetAuthzMock, } from '../../../fleet/server/mocks'; -import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; +import { createMockConfig, requestContextMock } from '../lib/detection_engine/routes/__mocks__'; import { EndpointAppContextService, EndpointAppContextServiceSetupContract, @@ -40,6 +39,7 @@ import { parseExperimentalConfigValue } from '../../common/experimental_features import { createCasesClientMock } from '../../../cases/server/client/mocks'; import { requestContextFactoryMock } from '../request_context_factory.mock'; import { EndpointMetadataService } from './services/metadata'; +import { createFleetAuthzMock } from '../../../fleet/common'; /** * Creates a mocked EndpointAppContext. @@ -183,8 +183,7 @@ export function createRouteHandlerContext( dataClient: jest.Mocked, savedObjectsClient: jest.Mocked ) { - const context = - xpackMocks.createRequestHandlerContext() as unknown as jest.Mocked; + const context = requestContextMock.create() as jest.Mocked; context.core.elasticsearch.client = dataClient; context.core.savedObjects.client = savedObjectsClient; return context; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts index 29a4e5ce0b29..bd72c5a4044e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts @@ -48,6 +48,7 @@ import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data' import { legacyMetadataSearchResponseMock } from '../metadata/support/test_support'; import { AGENT_ACTIONS_INDEX, ElasticsearchAssetType } from '../../../../../fleet/common'; import { CasesClientMock } from '../../../../../cases/server/client/mocks'; +import { EndpointAuthz } from '../../../../common/endpoint/types/authz'; interface CallRouteInterface { body?: HostIsolationRequestBody; @@ -55,6 +56,7 @@ interface CallRouteInterface { searchResponse?: HostMetadata; mockUser?: any; license?: License; + authz?: Partial; } const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); @@ -182,7 +184,7 @@ describe('Host Isolation', () => { // it returns the requestContext mock used in the call, to assert internal calls (e.g. the indexed document) callRoute = async ( routePrefix: string, - { body, idxResponse, searchResponse, mockUser, license }: CallRouteInterface, + { body, idxResponse, searchResponse, mockUser, license, authz = {} }: CallRouteInterface, indexExists?: { endpointDsExists: boolean } ): Promise> => { const asUser = mockUser ? mockUser : superUser; @@ -191,6 +193,12 @@ describe('Host Isolation', () => { ); const ctx = createRouteHandlerContext(mockScopedClient, mockSavedObjectClient); + + ctx.securitySolution.endpointAuthz = { + ...ctx.securitySolution.endpointAuthz, + ...authz, + }; + // mock _index_template ctx.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = jest .fn() @@ -206,6 +214,7 @@ describe('Host Isolation', () => { statusCode: 404, }); }); + const withIdxResp = idxResponse ? idxResponse : { statusCode: 201 }; const mockIndexResponse = jest.fn().mockImplementation(() => Promise.resolve(withIdxResp)); const mockSearchResponse = jest @@ -213,19 +222,25 @@ describe('Host Isolation', () => { .mockImplementation(() => Promise.resolve({ body: legacyMetadataSearchResponseMock(searchResponse) }) ); + if (indexExists) { ctx.core.elasticsearch.client.asInternalUser.index = mockIndexResponse; } + ctx.core.elasticsearch.client.asCurrentUser.index = mockIndexResponse; ctx.core.elasticsearch.client.asCurrentUser.search = mockSearchResponse; + const withLicense = license ? license : Platinum; licenseEmitter.next(withLicense); + const mockRequest = httpServerMock.createKibanaRequest({ body }); const [, routeHandler]: [ RouteConfig, RequestHandler ] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(routePrefix))!; + await routeHandler(ctx, mockRequest, mockResponse); + return ctx as unknown as jest.Mocked; }; }); @@ -424,14 +439,17 @@ describe('Host Isolation', () => { }); expect(mockResponse.ok).toBeCalled(); }); - it('prohibits license levels less than platinum from isolating hosts', async () => { - licenseEmitter.next(Gold); + + it('prohibits isolating hosts if no authz for it', async () => { await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, + authz: { canIsolateHost: false }, license: Gold, }); + expect(mockResponse.forbidden).toBeCalled(); }); + it('allows any license level to unisolate', async () => { licenseEmitter.next(Gold); await callRoute(UNISOLATE_HOST_ROUTE, { @@ -442,37 +460,33 @@ describe('Host Isolation', () => { }); }); - describe('User Level', () => { - it('allows superuser to perform isolation', async () => { - const superU = { username: 'foo', roles: ['superuser'] }; + describe('User Authorization Level', () => { + it('allows user to perform isolation when canIsolateHost is true', async () => { await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, - mockUser: superU, }); expect(mockResponse.ok).toBeCalled(); }); - it('allows superuser to perform unisolation', async () => { - const superU = { username: 'foo', roles: ['superuser'] }; + + it('allows user to perform unisolation when canUnIsolateHost is true', async () => { await callRoute(UNISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, - mockUser: superU, }); expect(mockResponse.ok).toBeCalled(); }); - it('prohibits non-admin user from performing isolation', async () => { - const superU = { username: 'foo', roles: ['user'] }; + it('prohibits user from performing isolation if canIsolateHost is false', async () => { await callRoute(ISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, - mockUser: superU, + authz: { canIsolateHost: false }, }); expect(mockResponse.forbidden).toBeCalled(); }); - it('prohibits non-admin user from performing unisolation', async () => { - const superU = { username: 'foo', roles: ['user'] }; + + it('prohibits user from performing un-isolation if canUnIsolateHost is false', async () => { await callRoute(UNISOLATE_HOST_ROUTE, { body: { endpoint_ids: ['XYZ'] }, - mockUser: superU, + authz: { canUnIsolateHost: false }, }); expect(mockResponse.forbidden).toBeCalled(); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 02f0cb486764..51f88730eb6f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -33,7 +33,6 @@ import { import { getMetadataForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; import { APP_ID } from '../../../../common/constants'; -import { userCanIsolate } from '../../../../common/endpoint/actions'; import { doLogsEndpointActionDsExists } from '../../utils'; /** @@ -100,25 +99,20 @@ export const isolationRequestHandler = function ( SecuritySolutionRequestHandlerContext > { return async (context, req, res) => { - // only allow admin users - const user = endpointContext.service.security?.authc.getCurrentUser(req); - if (!userCanIsolate(user?.roles)) { - return res.forbidden({ - body: { - message: 'You do not have permission to perform this action', - }, - }); - } + const { canIsolateHost, canUnIsolateHost } = context.securitySolution.endpointAuthz; - // isolation requires plat+ - if (isolate && !endpointContext.service.getLicenseService()?.isPlatinumPlus()) { + // Ensure user has authorization to use this api + if ((!canIsolateHost && isolate) || (!canUnIsolateHost && !isolate)) { return res.forbidden({ body: { - message: 'Your license level does not allow for this action', + message: + 'You do not have permission to perform this action or license level does not allow for this action', }, }); } + const user = endpointContext.service.security?.authc.getCurrentUser(req); + // fetch the Agent IDs to send the commands to const endpointIDs = [...new Set(req.body.endpoint_ids)]; // dedupe const endpointData = await getMetadataForEndpoints(endpointIDs, context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 86bba6969919..8abe054daeaf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -30,6 +30,7 @@ import type { SecuritySolutionApiRequestHandlerContext, SecuritySolutionRequestHandlerContext, } from '../../../../types'; +import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz'; const createMockClients = () => { const core = coreMock.createRequestHandlerContext(); @@ -93,6 +94,7 @@ const createSecuritySolutionRequestContextMock = ( return { core, + endpointAuthz: getEndpointAuthzInitialStateMock(), getConfig: jest.fn(() => clients.config), getFrameworkRequest: jest.fn(() => { return { diff --git a/x-pack/plugins/security_solution/server/request_context_factory.ts b/x-pack/plugins/security_solution/server/request_context_factory.ts index f6c1d6b44eca..d4adf5500438 100644 --- a/x-pack/plugins/security_solution/server/request_context_factory.ts +++ b/x-pack/plugins/security_solution/server/request_context_factory.ts @@ -17,7 +17,18 @@ import { SecuritySolutionPluginCoreSetupDependencies, SecuritySolutionPluginSetupDependencies, } from './plugin_contract'; -import { SecuritySolutionApiRequestHandlerContext } from './types'; +import { + SecuritySolutionApiRequestHandlerContext, + SecuritySolutionRequestHandlerContext, +} from './types'; +import { Immutable } from '../common/endpoint/types'; +import { EndpointAuthz } from '../common/endpoint/types/authz'; +import { + calculateEndpointAuthz, + getEndpointAuthzInitialState, +} from '../common/endpoint/service/authz'; +import { licenseService } from './lib/license'; +import { FleetAuthz } from '../../fleet/common'; export interface IRequestContextFactory { create( @@ -41,7 +52,7 @@ export class RequestContextFactory implements IRequestContextFactory { } public async create( - context: RequestHandlerContext, + context: Omit, request: KibanaRequest ): Promise { const { options, appClientFactory } = this; @@ -55,9 +66,31 @@ export class RequestContextFactory implements IRequestContextFactory { config, }); + let endpointAuthz: Immutable; + let fleetAuthz: FleetAuthz; + + // If Fleet is enabled, then get its Authz + if (startPlugins.fleet) { + fleetAuthz = context.fleet?.authz ?? (await startPlugins.fleet?.authz.fromRequest(request)); + } + return { core: context.core, + get endpointAuthz(): Immutable { + // Lazy getter of endpoint Authz. No point in defining it if it is never used. + if (!endpointAuthz) { + // If no fleet (fleet plugin is optional in the configuration), then just turn off all permissions + if (!startPlugins.fleet) { + endpointAuthz = getEndpointAuthzInitialState(); + } else { + endpointAuthz = calculateEndpointAuthz(licenseService, fleetAuthz); + } + } + + return endpointAuthz; + }, + getConfig: () => config, getFrameworkRequest: () => frameworkRequest, diff --git a/x-pack/plugins/security_solution/server/types.ts b/x-pack/plugins/security_solution/server/types.ts index 82616aa36d27..75686d783407 100644 --- a/x-pack/plugins/security_solution/server/types.ts +++ b/x-pack/plugins/security_solution/server/types.ts @@ -17,10 +17,12 @@ import { AppClient } from './client'; import { ConfigType } from './config'; import { IRuleExecutionLogClient } from './lib/detection_engine/rule_execution_log/types'; import { FrameworkRequest } from './lib/framework'; +import { EndpointAuthz } from '../common/endpoint/types/authz'; export { AppClient }; export interface SecuritySolutionApiRequestHandlerContext extends RequestHandlerContext { + endpointAuthz: EndpointAuthz; getConfig: () => ConfigType; getFrameworkRequest: () => FrameworkRequest; getAppClient: () => AppClient; From 7fb9dee206e045520a1f271f48f498b010340639 Mon Sep 17 00:00:00 2001 From: Miriam <31922082+MiriamAparicio@users.noreply.github.com> Date: Fri, 3 Dec 2021 16:24:16 +0000 Subject: [PATCH 08/25] Rename error rate to failed transactions rate (#120255) * Rename error rate to failed transactions rate * Fix conflict --- ..._error_rate.ts => get_failed_transaction_rate.ts} | 12 ++++++++---- .../service_map/get_service_map_service_node_info.ts | 4 ++-- .../plugins/apm/server/routes/transactions/route.ts | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) rename x-pack/plugins/apm/server/lib/transaction_groups/{get_error_rate.ts => get_failed_transaction_rate.ts} (94%) diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_failed_transaction_rate.ts similarity index 94% rename from x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts rename to x-pack/plugins/apm/server/lib/transaction_groups/get_failed_transaction_rate.ts index e1dde61bfc3f..b4f2c4b4bee1 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_failed_transaction_rate.ts @@ -32,7 +32,7 @@ import { getFailedTransactionRateTimeSeries, } from '../helpers/transaction_error_rate'; -export async function getErrorRate({ +export async function getFailedTransactionRate({ environment, kuery, serviceName, @@ -122,7 +122,7 @@ export async function getErrorRate({ return { timeseries, average }; } -export async function getErrorRatePeriods({ +export async function getFailedTransactionRatePeriods({ environment, kuery, serviceName, @@ -157,11 +157,15 @@ export async function getErrorRatePeriods({ searchAggregatedTransactions, }; - const currentPeriodPromise = getErrorRate({ ...commonProps, start, end }); + const currentPeriodPromise = getFailedTransactionRate({ + ...commonProps, + start, + end, + }); const previousPeriodPromise = comparisonStart && comparisonEnd - ? getErrorRate({ + ? getFailedTransactionRate({ ...commonProps, start: comparisonStart, end: comparisonEnd, diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts index ad2ab74098c2..545fb4dbc460 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_map_service_node_info.ts @@ -29,7 +29,7 @@ import { getDurationFieldForTransactions, getProcessorEventForTransactions, } from '../../lib/helpers/transactions'; -import { getErrorRate } from '../../lib/transaction_groups/get_error_rate'; +import { getFailedTransactionRate } from '../../lib/transaction_groups/get_failed_transaction_rate'; import { withApmSpan } from '../../utils/with_apm_span'; import { percentCgroupMemoryUsedScript, @@ -123,7 +123,7 @@ async function getFailedTransactionsRateStats({ numBuckets, }: TaskParameters): Promise { return withApmSpan('get_error_rate_for_service_map_node', async () => { - const { average, timeseries } = await getErrorRate({ + const { average, timeseries } = await getFailedTransactionRate({ environment, setup, serviceName, diff --git a/x-pack/plugins/apm/server/routes/transactions/route.ts b/x-pack/plugins/apm/server/routes/transactions/route.ts index fb73fe155596..b9db2762bce9 100644 --- a/x-pack/plugins/apm/server/routes/transactions/route.ts +++ b/x-pack/plugins/apm/server/routes/transactions/route.ts @@ -19,7 +19,7 @@ import { getServiceTransactionGroupDetailedStatisticsPeriods } from '../services import { getTransactionBreakdown } from './breakdown'; import { getTransactionTraceSamples } from './trace_samples'; import { getLatencyPeriods } from './get_latency_charts'; -import { getErrorRatePeriods } from '../../lib/transaction_groups/get_error_rate'; +import { getFailedTransactionRatePeriods } from '../../lib/transaction_groups/get_failed_transaction_rate'; import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { createApmServerRouteRepository } from '../apm_routes/create_apm_server_route_repository'; import { @@ -349,7 +349,7 @@ const transactionChartsErrorRateRoute = createApmServerRoute({ end, }); - return getErrorRatePeriods({ + return getFailedTransactionRatePeriods({ environment, kuery, serviceName, From 80660f168676604791870f91fe8eaf691ed75808 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Fri, 3 Dec 2021 08:27:41 -0800 Subject: [PATCH 09/25] [Fleet] Renable skipped test for limited packages (#120293) * Renable skipped test for limited packages * Try with newer endpoint package version. Test pass locally... --- .../fleet_api_integration/apis/package_policy/create.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index d568e7224fd2..1815ab91b531 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -199,8 +199,7 @@ export default function (providerContext: FtrProviderContext) { .expect(400); }); - // https://github.com/elastic/kibana/issues/118257 - it.skip('should not allow multiple limited packages on the same agent policy', async function () { + it('should not allow multiple limited packages on the same agent policy', async function () { await supertest .post(`/api/fleet/package_policies`) .set('kbn-xsrf', 'xxxx') @@ -215,7 +214,7 @@ export default function (providerContext: FtrProviderContext) { package: { name: 'endpoint', title: 'Endpoint', - version: '0.13.0', + version: '1.3.0-dev.0', }, }) .expect(200); @@ -233,7 +232,7 @@ export default function (providerContext: FtrProviderContext) { package: { name: 'endpoint', title: 'Endpoint', - version: '0.13.0', + version: '1.3.0-dev.0', }, }) .expect(400); From c7a06cdcbf844cdfecdbf9856c891915e124f3e3 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Fri, 3 Dec 2021 11:58:56 -0500 Subject: [PATCH 10/25] [Fleet] Wire Fleet setup status to core Kibana status API (#120020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Wire Fleet setup status to core Kibana status API * Remove fake error from testing 🙃 * Apply suggestion for PR review Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> * Add error message to meta upon Fleet setup failure * Mark fleet as available if setup fails - for now * Fix failing API key tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Josh Dover <1813008+joshdover@users.noreply.github.com> --- x-pack/plugins/fleet/server/plugin.ts | 37 ++++++++++++++++++- .../functional/apps/api_keys/home_page.ts | 3 ++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 4b45cf645201..1e421fefce83 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -6,6 +6,7 @@ */ import type { Observable } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import type { CoreSetup, CoreStart, @@ -16,13 +17,18 @@ import type { SavedObjectsServiceStart, HttpServiceSetup, KibanaRequest, + ServiceStatus, ElasticsearchClient, } from 'kibana/server'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import type { TelemetryPluginSetup, TelemetryPluginStart } from 'src/plugins/telemetry/server'; -import { DEFAULT_APP_CATEGORIES, SavedObjectsClient } from '../../../../src/core/server'; +import { + DEFAULT_APP_CATEGORIES, + SavedObjectsClient, + ServiceStatusLevels, +} from '../../../../src/core/server'; import type { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; import type { LicensingPluginSetup, ILicense } from '../../licensing/server'; import type { @@ -182,6 +188,7 @@ export class FleetPlugin private securitySetup?: SecurityPluginSetup; private encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup; private readonly telemetryEventsSender: TelemetryEventsSender; + private readonly fleetStatus$: BehaviorSubject; private agentService?: AgentService; @@ -193,6 +200,11 @@ export class FleetPlugin this.logger = this.initializerContext.logger.get(); this.configInitialValue = this.initializerContext.config.get(); this.telemetryEventsSender = new TelemetryEventsSender(this.logger.get('telemetry_events')); + + this.fleetStatus$ = new BehaviorSubject({ + level: ServiceStatusLevels.unavailable, + summary: 'Fleet is unavailable', + }); } public setup(core: CoreSetup, deps: FleetSetupDeps) { @@ -203,6 +215,8 @@ export class FleetPlugin this.securitySetup = deps.security; const config = this.configInitialValue; + core.status.set(this.fleetStatus$.asObservable()); + registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects); registerEncryptedSavedObjects(deps.encryptedSavedObjects); @@ -357,13 +371,33 @@ export class FleetPlugin const fleetSetupPromise = (async () => { try { + this.fleetStatus$.next({ + level: ServiceStatusLevels.degraded, + summary: 'Fleet is setting up', + }); + await setupFleet( new SavedObjectsClient(core.savedObjects.createInternalRepository()), core.elasticsearch.client.asInternalUser ); + + this.fleetStatus$.next({ + level: ServiceStatusLevels.available, + summary: 'Fleet is available', + }); } catch (error) { logger.warn('Fleet setup failed'); logger.warn(error); + + this.fleetStatus$.next({ + // As long as Fleet has a dependency on EPR, we can't reliably set Kibana status to `unavailable` here. + // See https://github.com/elastic/kibana/issues/120237 + level: ServiceStatusLevels.available, + summary: 'Fleet setup failed', + meta: { + error: error.message, + }, + }); } })(); @@ -400,6 +434,7 @@ export class FleetPlugin appContextService.stop(); licenseService.stop(); this.telemetryEventsSender.stop(); + this.fleetStatus$.complete(); } private setupAgentService(internalEsClient: ElasticsearchClient): AgentService { diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 590724752758..c2dbcc1046f5 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -42,6 +42,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await security.testUser.setRoles(['kibana_admin']); await security.testUser.setRoles(['test_api_keys']); await pageObjects.common.navigateToApp('apiKeys'); + + // Delete any API keys created outside of these tests + await pageObjects.apiKeys.bulkDeleteApiKeys(); }); afterEach(async () => { From ffbe19cf4658ad64f274bd913a43465162da70a5 Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 3 Dec 2021 19:26:03 +0200 Subject: [PATCH 11/25] [Canvas] Added KibanaThemeProvider to expression_reveal_image. (#120094) * Added kibanaThemeProvider to expression_reveal_image. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../expression_reveal_image/kibana.json | 2 +- .../reveal_image_renderer.stories.tsx | 4 +- .../public/expression_renderers/index.ts | 6 +- .../reveal_image_renderer.tsx | 55 +++++++++++-------- .../expression_reveal_image/public/index.ts | 5 +- .../expression_reveal_image/public/plugin.ts | 4 +- .../canvas_plugin_src/renderers/external.ts | 10 +--- .../shareable_runtime/supported_renderers.js | 4 +- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/plugins/expression_reveal_image/kibana.json b/src/plugins/expression_reveal_image/kibana.json index dad7fdfe2bc5..5fb13ce31247 100755 --- a/src/plugins/expression_reveal_image/kibana.json +++ b/src/plugins/expression_reveal_image/kibana.json @@ -11,5 +11,5 @@ "ui": true, "requiredPlugins": ["expressions", "presentationUtil"], "optionalPlugins": [], - "requiredBundles": [] + "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx b/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx index 863d8d1000f3..22dd2ef4156d 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx +++ b/src/plugins/expression_reveal_image/public/expression_renderers/__stories__/reveal_image_renderer.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { revealImageRenderer } from '../'; +import { getRevealImageRenderer } from '../'; import { getElasticOutline, getElasticLogo } from '../../../../presentation_util/public'; import { Render, waitFor } from '../../../../presentation_util/public/__stories__'; import { Origin } from '../../../common/types/expression_functions'; @@ -26,7 +26,7 @@ const Renderer = ({ origin: Origin.LEFT, percent: 0.45, }; - return ; + return ; }; storiesOf('renderers/revealImage', module).add( diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/index.ts b/src/plugins/expression_reveal_image/public/expression_renderers/index.ts index 433a81884f15..959a630b08b5 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/index.ts +++ b/src/plugins/expression_reveal_image/public/expression_renderers/index.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -import { revealImageRenderer } from './reveal_image_renderer'; - -export const renderers = [revealImageRenderer]; - -export { revealImageRenderer }; +export { revealImageRendererFactory, getRevealImageRenderer } from './reveal_image_renderer'; diff --git a/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx b/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx index d4dec3a8a582..6bdd01429641 100644 --- a/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx +++ b/src/plugins/expression_reveal_image/public/expression_renderers/reveal_image_renderer.tsx @@ -7,10 +7,14 @@ */ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { Observable } from 'rxjs'; +import { CoreTheme } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n-react'; import { ExpressionRenderDefinition, IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { i18n } from '@kbn/i18n'; -import { withSuspense } from '../../../presentation_util/public'; +import { CoreSetup } from '../../../../core/public'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; +import { withSuspense, defaultTheme$ } from '../../../presentation_util/public'; import { RevealImageRendererConfig } from '../../common/types'; export const strings = { @@ -27,25 +31,32 @@ export const strings = { const LazyRevealImageComponent = lazy(() => import('../components/reveal_image_component')); const RevealImageComponent = withSuspense(LazyRevealImageComponent, null); -export const revealImageRenderer = (): ExpressionRenderDefinition => ({ - name: 'revealImage', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, - render: ( - domNode: HTMLElement, - config: RevealImageRendererConfig, - handlers: IInterpreterRenderHandlers - ) => { - handlers.onDestroy(() => { - unmountComponentAtNode(domNode); - }); +export const getRevealImageRenderer = + (theme$: Observable = defaultTheme$) => + (): ExpressionRenderDefinition => ({ + name: 'revealImage', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, + render: ( + domNode: HTMLElement, + config: RevealImageRendererConfig, + handlers: IInterpreterRenderHandlers + ) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); - render( - - - , - domNode - ); - }, -}); + render( + + + + + , + domNode + ); + }, + }); + +export const revealImageRendererFactory = (core: CoreSetup) => + getRevealImageRenderer(core.theme.theme$); diff --git a/src/plugins/expression_reveal_image/public/index.ts b/src/plugins/expression_reveal_image/public/index.ts index 66512a1126b0..736e062475e6 100755 --- a/src/plugins/expression_reveal_image/public/index.ts +++ b/src/plugins/expression_reveal_image/public/index.ts @@ -6,9 +6,6 @@ * Side Public License, v 1. */ -// TODO: https://github.com/elastic/kibana/issues/110893 -/* eslint-disable @kbn/eslint/no_export_all */ - import { ExpressionRevealImagePlugin } from './plugin'; export type { ExpressionRevealImagePluginSetup, ExpressionRevealImagePluginStart } from './plugin'; @@ -17,4 +14,4 @@ export function plugin() { return new ExpressionRevealImagePlugin(); } -export * from './expression_renderers'; +export { revealImageRendererFactory, getRevealImageRenderer } from './expression_renderers'; diff --git a/src/plugins/expression_reveal_image/public/plugin.ts b/src/plugins/expression_reveal_image/public/plugin.ts index c5e1b5c8d916..17bff3f33e8a 100755 --- a/src/plugins/expression_reveal_image/public/plugin.ts +++ b/src/plugins/expression_reveal_image/public/plugin.ts @@ -8,7 +8,7 @@ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { ExpressionsStart, ExpressionsSetup } from '../../expressions/public'; -import { revealImageRenderer } from './expression_renderers'; +import { revealImageRendererFactory } from './expression_renderers'; import { revealImageFunction } from '../common/expression_functions'; interface SetupDeps { @@ -33,7 +33,7 @@ export class ExpressionRevealImagePlugin { public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionRevealImagePluginSetup { expressions.registerFunction(revealImageFunction); - expressions.registerRenderer(revealImageRenderer); + expressions.registerRenderer(revealImageRendererFactory(core)); } public start(core: CoreStart): ExpressionRevealImagePluginStart {} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts index 9e2a51065eb6..569669032cb0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts @@ -11,23 +11,19 @@ import { errorRendererFactory, debugRendererFactory, } from '../../../../../src/plugins/expression_error/public'; +import { revealImageRendererFactory } from '../../../../../src/plugins/expression_reveal_image/public'; import { repeatImageRendererFactory } from '../../../../../src/plugins/expression_repeat_image/public'; -import { revealImageRenderer } from '../../../../../src/plugins/expression_reveal_image/public'; import { shapeRenderer, progressRenderer, } from '../../../../../src/plugins/expression_shape/public'; -export const renderFunctions = [ - imageRenderer, - revealImageRenderer, - shapeRenderer, - progressRenderer, -]; +export const renderFunctions = [imageRenderer, shapeRenderer, progressRenderer]; export const renderFunctionFactories = [ debugRendererFactory, errorRendererFactory, + revealImageRendererFactory, repeatImageRendererFactory, metricRendererFactory, ]; diff --git a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js index 84150a6a9e82..01b8cc98ba5e 100644 --- a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js +++ b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js @@ -15,8 +15,8 @@ import { getErrorRenderer, getDebugRenderer, } from '../../../../src/plugins/expression_error/public'; +import { getRevealImageRenderer } from '../../../../src/plugins/expression_reveal_image/public'; import { getRepeatImageRenderer } from '../../../../src/plugins/expression_repeat_image/public'; -import { revealImageRenderer as revealImage } from '../../../../src/plugins/expression_reveal_image/public'; import { shapeRenderer as shape, progressRenderer as progress, @@ -31,6 +31,7 @@ const renderFunctionsFactories = [ getTableRenderer, getErrorRenderer, getDebugRenderer, + getRevealImageRenderer, getRepeatImageRenderer, getMetricRenderer, ]; @@ -42,7 +43,6 @@ const renderFunctionsFactories = [ */ export const renderFunctions = [ image, - revealImage, pie, plot, progress, From 72765c9de4247161e2672abae4e4bfc0282cb5aa Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Fri, 3 Dec 2021 11:33:28 -0700 Subject: [PATCH 12/25] [maps] rename layer types provide more distinguishable names that better reflect layer (#118617) * [maps] rename layer types provide more distinguishable names that better refect layer * fix migration rename * revert extends change for EmsVectorTileLayer * tsling * fix regression and jest test * extend from Abstract instead of RasterTile * better fix * fix tests * more test fixes * update path for newly merged code Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__mocks__/regions_layer.mock.ts | 4 +- .../VisitorBreakdownMap/useLayerList.ts | 4 +- .../choropleth_map.tsx | 2 +- x-pack/plugins/maps/common/constants.ts | 8 +- .../layer_descriptor_types.ts | 2 +- .../migrations/add_field_meta_options.js | 5 +- .../migrations/add_field_meta_options.test.js | 6 +- .../migrations/add_type_to_termjoin.test.ts | 4 +- .../ems_raster_tile_to_ems_vector_tile.js | 8 +- .../common/migrations/join_agg_key.test.ts | 3 +- .../maps/common/migrations/join_agg_key.ts | 3 +- .../migrate_symbol_style_descriptor.js | 5 +- .../migrate_symbol_style_descriptor.test.js | 8 +- .../migrations/rename_layer_types.test.ts | 83 +++++++++++++++++++ .../common/migrations/rename_layer_types.ts | 49 +++++++++++ .../public/actions/data_request_actions.ts | 2 +- .../create_basemap_layer_descriptor.test.ts | 4 +- .../layers/create_basemap_layer_descriptor.ts | 8 +- .../ems_vector_tile_layer.test.ts | 53 ++++++++++++ .../ems_vector_tile_layer.tsx} | 43 ++++++++-- .../raster_tile_layer.test.ts} | 12 ++- .../raster_tile_layer.ts} | 18 ++-- .../geojson_vector_layer.tsx | 2 +- .../mvt_vector_layer/mvt_vector_layer.tsx | 2 +- .../layers/vector_layer/vector_layer.tsx | 2 +- .../vector_tile_layer.test.ts | 75 ----------------- .../create_layer_descriptor.test.ts | 4 +- .../security/create_layer_descriptors.test.ts | 18 ++-- .../ems_base_map_layer_wizard.tsx | 6 +- .../update_source_editor.test.tsx | 2 +- .../update_source_editor.tsx | 8 +- .../es_search_source/util/scaling_form.tsx | 4 +- .../kibana_base_map_layer_wizard.tsx | 4 +- .../sources/wms_source/wms_layer_wizard.tsx | 4 +- .../sources/xyz_tms_source/layer_wizard.tsx | 4 +- .../edit_layer_panel.test.tsx | 2 +- .../connected_components/mb_map/utils.ts | 2 +- x-pack/plugins/maps/public/locators.test.ts | 4 +- .../public/selectors/map_selectors.test.ts | 4 +- .../maps/public/selectors/map_selectors.ts | 18 ++-- .../visualize_geo_field_action.ts | 2 +- .../maps/server/embeddable_migrations.ts | 13 +++ .../maps_telemetry/maps_telemetry.test.js | 2 +- .../server/maps_telemetry/maps_telemetry.ts | 2 +- .../sample_map_saved_objects.json | 8 +- .../maps/server/maps_telemetry/util.ts | 2 +- .../saved_objects/saved_object_migrations.js | 14 ++++ .../application/explorer/anomalies_map.tsx | 2 +- .../api_integration/apis/maps/migrations.js | 4 +- 49 files changed, 359 insertions(+), 189 deletions(-) create mode 100644 x-pack/plugins/maps/common/migrations/rename_layer_types.test.ts create mode 100644 x-pack/plugins/maps/common/migrations/rename_layer_types.ts create mode 100644 x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts rename x-pack/plugins/maps/public/classes/layers/{vector_tile_layer/vector_tile_layer.tsx => ems_vector_tile_layer/ems_vector_tile_layer.tsx} (93%) rename x-pack/plugins/maps/public/classes/layers/{tile_layer/tile_layer.test.ts => raster_tile_layer/raster_tile_layer.test.ts} (88%) rename x-pack/plugins/maps/public/classes/layers/{tile_layer/tile_layer.ts => raster_tile_layer/raster_tile_layer.ts} (92%) delete mode 100644 x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.test.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts index 52bd024d8116..81b0a71e8d94 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/__mocks__/regions_layer.mock.ts @@ -79,7 +79,7 @@ export const mockLayerList = [ maxZoom: 24, alpha: 0.75, visible: true, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', }, { joins: [ @@ -148,6 +148,6 @@ export const mockLayerList = [ maxZoom: 24, alpha: 0.75, visible: true, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', }, ]; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts index 7f81e9d10518..f573a2641b86 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdownMap/useLayerList.ts @@ -155,7 +155,7 @@ export function useLayerList() { maxZoom: 24, alpha: 0.75, visible: true, - type: LAYER_TYPE.VECTOR, + type: LAYER_TYPE.GEOJSON_VECTOR, }; ES_TERM_SOURCE_REGION.whereQuery = getWhereQuery(serviceName!); @@ -179,7 +179,7 @@ export function useLayerList() { maxZoom: 24, alpha: 0.75, visible: true, - type: LAYER_TYPE.VECTOR, + type: LAYER_TYPE.GEOJSON_VECTOR, }; return [ diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx index 507bfc36d47f..bf767c7e6520 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/choropleth_map.tsx @@ -87,7 +87,7 @@ export const getChoroplethTopValuesLayer = ( }, isTimeAware: true, }, - type: LAYER_TYPE.VECTOR, + type: LAYER_TYPE.GEOJSON_VECTOR, }; }; diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 0f2ce2c91773..43cca5f0c6a0 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -49,12 +49,12 @@ export function getEditPath(id: string | undefined) { } export enum LAYER_TYPE { - TILE = 'TILE', - VECTOR = 'VECTOR', - VECTOR_TILE = 'VECTOR_TILE', // for static display of mvt vector tiles with a mapbox stylesheet. Does not support any ad-hoc configurations. Used for consuming EMS vector tiles. + RASTER_TILE = 'RASTER_TILE', + GEOJSON_VECTOR = 'GEOJSON_VECTOR', + EMS_VECTOR_TILE = 'EMS_VECTOR_TILE', HEATMAP = 'HEATMAP', BLENDED_VECTOR = 'BLENDED_VECTOR', - TILED_VECTOR = 'TILED_VECTOR', // similar to a regular vector-layer, but it consumes the data as .mvt tilea iso GeoJson. It supports similar ad-hoc configurations like a regular vector layer (E.g. using IVectorStyle), although there is some loss of functionality e.g. does not support term joining + MVT_VECTOR = 'MVT_VECTOR', } export enum SOURCE_TYPES { diff --git a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts index 4d687969308b..044678b918fd 100644 --- a/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/layer_descriptor_types.ts @@ -66,7 +66,7 @@ export type LayerDescriptor = { }; export type VectorLayerDescriptor = LayerDescriptor & { - type: LAYER_TYPE.VECTOR | LAYER_TYPE.TILED_VECTOR | LAYER_TYPE.BLENDED_VECTOR; + type: LAYER_TYPE.GEOJSON_VECTOR | LAYER_TYPE.MVT_VECTOR | LAYER_TYPE.BLENDED_VECTOR; joins?: JoinDescriptor[]; style: VectorStyleDescriptor; }; diff --git a/x-pack/plugins/maps/common/migrations/add_field_meta_options.js b/x-pack/plugins/maps/common/migrations/add_field_meta_options.js index 33a98c7dbf33..736fd30bfbcb 100644 --- a/x-pack/plugins/maps/common/migrations/add_field_meta_options.js +++ b/x-pack/plugins/maps/common/migrations/add_field_meta_options.js @@ -6,11 +6,12 @@ */ import _ from 'lodash'; -import { LAYER_TYPE, STYLE_TYPE } from '../constants'; +import { STYLE_TYPE } from '../constants'; function isVectorLayer(layerDescriptor) { const layerType = _.get(layerDescriptor, 'type'); - return layerType === LAYER_TYPE.VECTOR; + // can not use LAYER_TYPE because LAYER_TYPE.VECTOR does not exist >8.1 + return layerType === 'VECTOR'; } export function addFieldMetaOptions({ attributes }) { diff --git a/x-pack/plugins/maps/common/migrations/add_field_meta_options.test.js b/x-pack/plugins/maps/common/migrations/add_field_meta_options.test.js index 31300707d147..60587e56bdba 100644 --- a/x-pack/plugins/maps/common/migrations/add_field_meta_options.test.js +++ b/x-pack/plugins/maps/common/migrations/add_field_meta_options.test.js @@ -41,7 +41,7 @@ describe('addFieldMetaOptions', () => { test('Should ignore static style properties', () => { const layerListJSON = JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', style: { type: 'VECTOR', properties: { @@ -68,7 +68,7 @@ describe('addFieldMetaOptions', () => { test('Should add field meta options to dynamic style properties', () => { const layerListJSON = JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', style: { type: 'VECTOR', properties: { @@ -94,7 +94,7 @@ describe('addFieldMetaOptions', () => { title: 'my map', layerListJSON: JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', style: { type: 'VECTOR', properties: { diff --git a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.test.ts b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.test.ts index d795e63bf81d..5229380ac55f 100644 --- a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.test.ts +++ b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.test.ts @@ -6,14 +6,14 @@ */ import { addTypeToTermJoin } from './add_type_to_termjoin'; -import { LAYER_TYPE, SOURCE_TYPES } from '../constants'; +import { SOURCE_TYPES } from '../constants'; import { LayerDescriptor } from '../descriptor_types'; describe('addTypeToTermJoin', () => { test('Should handle missing type attribute', () => { const layerListJSON = JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', joins: [ { right: {}, diff --git a/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js index 2945b9efed95..4cf5407d7969 100644 --- a/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js +++ b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js @@ -6,7 +6,7 @@ */ import _ from 'lodash'; -import { SOURCE_TYPES, LAYER_TYPE } from '../constants'; +import { SOURCE_TYPES } from '../constants'; function isEmsTileSource(layerDescriptor) { const sourceType = _.get(layerDescriptor, 'sourceDescriptor.type'); @@ -15,7 +15,8 @@ function isEmsTileSource(layerDescriptor) { function isTileLayer(layerDescriptor) { const layerType = _.get(layerDescriptor, 'type'); - return layerType === LAYER_TYPE.TILE; + // can not use LAYER_TYPE because LAYER_TYPE.TILE does not exist >8.1 + return layerType === 'TILE'; } export function emsRasterTileToEmsVectorTile({ attributes }) { @@ -33,7 +34,8 @@ export function emsRasterTileToEmsVectorTile({ attributes }) { layerList.forEach((layer) => { if (isTileLayer(layer) && isEmsTileSource(layer)) { // Just need to switch layer type to migrate TILE layer to VECTOR_TILE layer - layer.type = LAYER_TYPE.VECTOR_TILE; + // can not use LAYER_TYPE because LAYER_TYPE.VECTOR_TILE does not exist >8.1 + layer.type = 'VECTOR_TILE'; } }); diff --git a/x-pack/plugins/maps/common/migrations/join_agg_key.test.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.test.ts index 96d60e2f9238..3926c017a950 100644 --- a/x-pack/plugins/maps/common/migrations/join_agg_key.test.ts +++ b/x-pack/plugins/maps/common/migrations/join_agg_key.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { LAYER_TYPE } from '../constants'; import { migrateJoinAggKey } from './join_agg_key'; describe('migrateJoinAggKey', () => { @@ -65,7 +64,7 @@ describe('migrateJoinAggKey', () => { test('Should migrate vector styles from legacy join agg key to new join agg key', () => { const layerListJSON = JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', joins, style: { properties: { diff --git a/x-pack/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.ts index 726855783be6..ae102f2ed540 100644 --- a/x-pack/plugins/maps/common/migrations/join_agg_key.ts +++ b/x-pack/plugins/maps/common/migrations/join_agg_key.ts @@ -71,7 +71,8 @@ export function migrateJoinAggKey({ layerList.forEach((layerDescriptor: LayerDescriptor) => { if ( - layerDescriptor.type === LAYER_TYPE.VECTOR || + // can not use LAYER_TYPE because LAYER_TYPE.VECTOR does not exist >8.1 + layerDescriptor.type === 'VECTOR' || layerDescriptor.type === LAYER_TYPE.BLENDED_VECTOR ) { const vectorLayerDescriptor = layerDescriptor as VectorLayerDescriptor; diff --git a/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js index 6dab8595663e..4aa0b7414843 100644 --- a/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js +++ b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js @@ -6,11 +6,12 @@ */ import _ from 'lodash'; -import { DEFAULT_ICON, LAYER_TYPE, STYLE_TYPE, SYMBOLIZE_AS_TYPES } from '../constants'; +import { DEFAULT_ICON, STYLE_TYPE, SYMBOLIZE_AS_TYPES } from '../constants'; function isVectorLayer(layerDescriptor) { const layerType = _.get(layerDescriptor, 'type'); - return layerType === LAYER_TYPE.VECTOR; + // can not use LAYER_TYPE because LAYER_TYPE.VECTOR does not exist >8.1 + return layerType === 'VECTOR'; } export function migrateSymbolStyleDescriptor({ attributes }) { diff --git a/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js index 89f3c05e621e..9932fe1625c9 100644 --- a/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js +++ b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.test.js @@ -41,7 +41,7 @@ describe('migrateSymbolStyleDescriptor', () => { test('Should migrate "symbol" style descriptor', () => { const layerListJSON = JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', style: { properties: { fillColor: { @@ -66,7 +66,7 @@ describe('migrateSymbolStyleDescriptor', () => { title: 'my map', layerListJSON: JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', style: { properties: { fillColor: { @@ -90,7 +90,7 @@ describe('migrateSymbolStyleDescriptor', () => { test('Should migrate style descriptor without "symbol"', () => { const layerListJSON = JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', style: { properties: { fillColor: { @@ -109,7 +109,7 @@ describe('migrateSymbolStyleDescriptor', () => { title: 'my map', layerListJSON: JSON.stringify([ { - type: LAYER_TYPE.VECTOR, + type: 'VECTOR', style: { properties: { fillColor: { diff --git a/x-pack/plugins/maps/common/migrations/rename_layer_types.test.ts b/x-pack/plugins/maps/common/migrations/rename_layer_types.test.ts new file mode 100644 index 000000000000..2504a2adb31a --- /dev/null +++ b/x-pack/plugins/maps/common/migrations/rename_layer_types.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { renameLayerTypes } from './rename_layer_types'; + +describe('renameLayerTypes', () => { + test('Should handle missing layerListJSON attribute', () => { + const attributes = { + title: 'my map', + }; + expect(renameLayerTypes({ attributes })).toEqual({ + title: 'my map', + }); + }); + + test('Should rename TILED_VECTOR to MVT_VECTOR', () => { + const layerListJSON = JSON.stringify([ + { + type: 'TILED_VECTOR', + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(renameLayerTypes({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"type":"MVT_VECTOR"}]', + }); + }); + + test('Should rename VECTOR_TILE to EMS_VECTOR_TILE', () => { + const layerListJSON = JSON.stringify([ + { + type: 'VECTOR_TILE', + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(renameLayerTypes({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"type":"EMS_VECTOR_TILE"}]', + }); + }); + + test('Should rename VECTOR to GEOJSON_VECTOR', () => { + const layerListJSON = JSON.stringify([ + { + type: 'VECTOR', + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(renameLayerTypes({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"type":"GEOJSON_VECTOR"}]', + }); + }); + + test('Should rename TILE to RASTER_TILE', () => { + const layerListJSON = JSON.stringify([ + { + type: 'TILE', + }, + ]); + const attributes = { + title: 'my map', + layerListJSON, + }; + expect(renameLayerTypes({ attributes })).toEqual({ + title: 'my map', + layerListJSON: '[{"type":"RASTER_TILE"}]', + }); + }); +}); diff --git a/x-pack/plugins/maps/common/migrations/rename_layer_types.ts b/x-pack/plugins/maps/common/migrations/rename_layer_types.ts new file mode 100644 index 000000000000..6ba924ff3226 --- /dev/null +++ b/x-pack/plugins/maps/common/migrations/rename_layer_types.ts @@ -0,0 +1,49 @@ +/* + * 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 { LAYER_TYPE } from '../constants'; +import { LayerDescriptor } from '../descriptor_types'; +import { MapSavedObjectAttributes } from '../map_saved_object_type'; + +// LAYER_TYPE constants renamed in 8.1 to provide more distinguishable names that better refect layer. +// TILED_VECTOR replaced with MVT_VECTOR +// VECTOR_TILE replaced with EMS_VECTOR_TILE +// VECTOR replaced with GEOJSON_VECTOR +// TILE replaced with RASTER_TILE +export function renameLayerTypes({ + attributes, +}: { + attributes: MapSavedObjectAttributes; +}): MapSavedObjectAttributes { + if (!attributes || !attributes.layerListJSON) { + return attributes; + } + + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + + layerList.forEach((layerDescriptor: LayerDescriptor) => { + if (layerDescriptor.type === 'TILED_VECTOR') { + layerDescriptor.type = LAYER_TYPE.MVT_VECTOR; + } else if (layerDescriptor.type === 'VECTOR_TILE') { + layerDescriptor.type = LAYER_TYPE.EMS_VECTOR_TILE; + } else if (layerDescriptor.type === 'VECTOR') { + layerDescriptor.type = LAYER_TYPE.GEOJSON_VECTOR; + } else if (layerDescriptor.type === 'TILE') { + layerDescriptor.type = LAYER_TYPE.RASTER_TILE; + } + }); + + return { + ...attributes, + layerListJSON: JSON.stringify(layerList), + }; +} diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index c1a6d05cc057..730135424a4d 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -287,7 +287,7 @@ function endDataLoad( const eventHandlers = getEventHandlers(getState()); if (eventHandlers && eventHandlers.onDataLoadEnd) { const resultMeta: ResultMeta = {}; - if (layer && layer.getType() === LAYER_TYPE.VECTOR) { + if (layer && layer.getType() === LAYER_TYPE.GEOJSON_VECTOR) { const featuresWithoutCentroids = features.filter((feature) => { return feature.properties ? !feature.properties[KBN_IS_CENTROID_FEATURE] : true; }); diff --git a/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.test.ts index cf3d87053800..eded70a75e4a 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.test.ts @@ -53,7 +53,7 @@ describe('kibana.yml configured with map.tilemap.url', () => { type: 'KIBANA_TILEMAP', }, style: { type: 'TILE' }, - type: 'TILE', + type: 'RASTER_TILE', visible: true, }); }); @@ -89,7 +89,7 @@ describe('EMS is enabled', () => { type: 'EMS_TMS', }, style: { type: 'TILE' }, - type: 'VECTOR_TILE', + type: 'EMS_VECTOR_TILE', visible: true, }); }); diff --git a/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.ts index d285d3a36f66..e104261f9084 100644 --- a/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/create_basemap_layer_descriptor.ts @@ -11,14 +11,14 @@ import { getKibanaTileMap } from '../../util'; import { getEMSSettings } from '../../kibana_services'; // @ts-expect-error import { KibanaTilemapSource } from '../sources/kibana_tilemap_source'; -import { TileLayer } from './tile_layer/tile_layer'; -import { VectorTileLayer } from './vector_tile_layer/vector_tile_layer'; +import { RasterTileLayer } from './raster_tile_layer/raster_tile_layer'; +import { EmsVectorTileLayer } from './ems_vector_tile_layer/ems_vector_tile_layer'; import { EMSTMSSource } from '../sources/ems_tms_source'; export function createBasemapLayerDescriptor(): LayerDescriptor | null { const tilemapSourceFromKibana = getKibanaTileMap(); if (_.get(tilemapSourceFromKibana, 'url')) { - const layerDescriptor = TileLayer.createDescriptor({ + const layerDescriptor = RasterTileLayer.createDescriptor({ sourceDescriptor: KibanaTilemapSource.createDescriptor(), }); return layerDescriptor; @@ -26,7 +26,7 @@ export function createBasemapLayerDescriptor(): LayerDescriptor | null { const isEmsEnabled = getEMSSettings()!.isEMSEnabled(); if (isEmsEnabled) { - const layerDescriptor = VectorTileLayer.createDescriptor({ + const layerDescriptor = EmsVectorTileLayer.createDescriptor({ sourceDescriptor: EMSTMSSource.createDescriptor({ isAutoSelect: true }), }); return layerDescriptor; diff --git a/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.ts new file mode 100644 index 000000000000..8b27bacff8ec --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.test.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 { SOURCE_TYPES } from '../../../../common/constants'; +import { DataFilters, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; +import { ILayer } from '../layer'; +import { EmsVectorTileLayer } from './ems_vector_tile_layer'; +import { DataRequestContext } from '../../../actions'; +import { EMSTMSSource } from '../../sources/ems_tms_source'; + +describe('EmsVectorTileLayer', () => { + it('should correctly inject tileLayerId in meta', async () => { + const layer: ILayer = new EmsVectorTileLayer({ + source: { + getTileLayerId: () => { + return 'myTileLayerId'; + }, + getVectorStyleSheetAndSpriteMeta: () => { + throw new Error('network error'); + }, + } as unknown as EMSTMSSource, + layerDescriptor: { + id: 'layerid', + sourceDescriptor: { + type: SOURCE_TYPES.EMS_XYZ, + urlTemplate: 'https://example.com/{x}/{y}/{z}.png', + id: 'mockSourceId', + } as XYZTMSSourceDescriptor, + }, + }); + + let actualMeta; + let actualErrorMessage; + const mockContext = { + startLoading: (requestId: string, token: string, meta: unknown) => { + actualMeta = meta; + }, + onLoadError: (requestId: string, token: string, message: string) => { + actualErrorMessage = message; + }, + dataFilters: { foo: 'bar' } as unknown as DataFilters, + } as unknown as DataRequestContext; + + await layer.syncData(mockContext); + + expect(actualMeta).toStrictEqual({ tileLayerId: 'myTileLayerId' }); + expect(actualErrorMessage).toStrictEqual('network error'); + }); +}); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx similarity index 93% rename from x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx rename to x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx index f6c4e3fd057c..8f7471f255a5 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/ems_vector_tile_layer/ems_vector_tile_layer.tsx @@ -7,7 +7,7 @@ import type { Map as MbMap, Layer as MbLayer, Style as MbStyle } from '@kbn/mapbox-gl'; import _ from 'lodash'; -import { TileLayer } from '../tile_layer/tile_layer'; +import { AbstractLayer } from '../layer'; import { SOURCE_DATA_REQUEST_ID, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; import { LayerDescriptor } from '../../../../common/descriptor_types'; import { DataRequest } from '../../util/data_request'; @@ -18,6 +18,7 @@ import { } from '../../../connected_components/mb_map/utils'; import { DataRequestContext } from '../../../actions'; import { EMSTMSSource } from '../../sources/ems_tms_source'; +import { TileStyle } from '../../styles/tile/tile_style'; interface SourceRequestMeta { tileLayerId: string; @@ -46,22 +47,44 @@ interface SourceRequestData { }; } -// TODO - rename to EmsVectorTileLayer -export class VectorTileLayer extends TileLayer { - static type = LAYER_TYPE.VECTOR_TILE; - +export class EmsVectorTileLayer extends AbstractLayer { static createDescriptor(options: Partial) { const tileLayerDescriptor = super.createDescriptor(options); - tileLayerDescriptor.type = VectorTileLayer.type; + tileLayerDescriptor.type = LAYER_TYPE.EMS_VECTOR_TILE; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; } + private readonly _style: TileStyle; + + constructor({ + source, + layerDescriptor, + }: { + source: EMSTMSSource; + layerDescriptor: LayerDescriptor; + }) { + super({ source, layerDescriptor }); + this._style = new TileStyle(); + } + getSource(): EMSTMSSource { return super.getSource() as EMSTMSSource; } + getStyleForEditing() { + return this._style; + } + + getStyle() { + return this._style; + } + + getCurrentStyle() { + return this._style; + } + _canSkipSync({ prevDataRequest, nextMeta, @@ -349,4 +372,12 @@ export class VectorTileLayer extends TileLayer { async getLicensedFeatures() { return this._source.getLicensedFeatures(); } + + getLayerTypeIconName() { + return 'grid'; + } + + isBasemap(order: number) { + return order === 0; + } } diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.test.ts b/x-pack/plugins/maps/public/classes/layers/raster_tile_layer/raster_tile_layer.test.ts similarity index 88% rename from x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.test.ts rename to x-pack/plugins/maps/public/classes/layers/raster_tile_layer/raster_tile_layer.test.ts index 43cc69687ffb..66c5b8da0591 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/raster_tile_layer/raster_tile_layer.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ITileLayerArguments, TileLayer } from './tile_layer'; +import { RasterTileLayer } from './raster_tile_layer'; import { SOURCE_TYPES } from '../../../../common/constants'; import { XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; import { AbstractSource } from '../../sources/source'; @@ -34,22 +34,20 @@ class MockTileSource extends AbstractSource implements ITMSSource { } } -describe('TileLayer', () => { +describe('RasterTileLayer', () => { it('should use display-label from source', async () => { const source = new MockTileSource(sourceDescriptor); - const args: ITileLayerArguments = { + const layer: ILayer = new RasterTileLayer({ source, layerDescriptor: { id: 'layerid', sourceDescriptor }, - }; - - const layer: ILayer = new TileLayer(args); + }); expect(await source.getDisplayName()).toEqual(await layer.getDisplayName()); }); it('should override with custom display-label if present', async () => { const source = new MockTileSource(sourceDescriptor); - const layer: ILayer = new TileLayer({ + const layer: ILayer = new RasterTileLayer({ source, layerDescriptor: { id: 'layerid', sourceDescriptor, label: 'custom' }, }); diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.ts b/x-pack/plugins/maps/public/classes/layers/raster_tile_layer/raster_tile_layer.ts similarity index 92% rename from x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.ts rename to x-pack/plugins/maps/public/classes/layers/raster_tile_layer/raster_tile_layer.ts index 009b447402f9..40a7276bc10d 100644 --- a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/raster_tile_layer/raster_tile_layer.ts @@ -14,16 +14,10 @@ import { TileStyle } from '../../styles/tile/tile_style'; import { ITMSSource } from '../../sources/tms_source'; import { DataRequestContext } from '../../../actions'; -export interface ITileLayerArguments { - source: ITMSSource; - layerDescriptor: LayerDescriptor; -} - -// TODO - rename to RasterTileLayer -export class TileLayer extends AbstractLayer { +export class RasterTileLayer extends AbstractLayer { static createDescriptor(options: Partial) { const tileLayerDescriptor = super.createDescriptor(options); - tileLayerDescriptor.type = LAYER_TYPE.TILE; + tileLayerDescriptor.type = LAYER_TYPE.RASTER_TILE; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; @@ -31,7 +25,13 @@ export class TileLayer extends AbstractLayer { private readonly _style: TileStyle; - constructor({ source, layerDescriptor }: ITileLayerArguments) { + constructor({ + source, + layerDescriptor, + }: { + source: ITMSSource; + layerDescriptor: LayerDescriptor; + }) { super({ source, layerDescriptor }); this._style = new TileStyle(); } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx index 3152ac27189b..e7fa87435cf0 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/geojson_vector_layer/geojson_vector_layer.tsx @@ -51,7 +51,7 @@ export class GeoJsonVectorLayer extends AbstractVectorLayer { mapColors?: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(options) as VectorLayerDescriptor; - layerDescriptor.type = LAYER_TYPE.VECTOR; + layerDescriptor.type = LAYER_TYPE.GEOJSON_VECTOR; if (!options.style) { const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx index e266c729f26f..5ac95c9a91f6 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.tsx @@ -42,7 +42,7 @@ export class MvtVectorLayer extends AbstractVectorLayer { mapColors?: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(descriptor, mapColors); - layerDescriptor.type = LAYER_TYPE.TILED_VECTOR; + layerDescriptor.type = LAYER_TYPE.MVT_VECTOR; if (!layerDescriptor.style) { const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 71a960fc1919..0384517bf983 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -113,7 +113,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { mapColors?: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(options) as VectorLayerDescriptor; - layerDescriptor.type = LAYER_TYPE.VECTOR; + layerDescriptor.type = LAYER_TYPE.GEOJSON_VECTOR; if (!options.style) { const styleProperties = VectorStyle.createDefaultStyleProperties(mapColors ? mapColors : []); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.test.ts b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.test.ts deleted file mode 100644 index d094e53c59a9..000000000000 --- a/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.test.ts +++ /dev/null @@ -1,75 +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 { ITileLayerArguments } from '../tile_layer/tile_layer'; -import { SOURCE_TYPES } from '../../../../common/constants'; -import { DataFilters, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; -import { AbstractSource } from '../../sources/source'; -import { ITMSSource } from '../../sources/tms_source'; -import { ILayer } from '../layer'; -import { VectorTileLayer } from './vector_tile_layer'; -import { DataRequestContext } from '../../../actions'; - -const sourceDescriptor: XYZTMSSourceDescriptor = { - type: SOURCE_TYPES.EMS_XYZ, - urlTemplate: 'https://example.com/{x}/{y}/{z}.png', - id: 'mockSourceId', -}; - -class MockTileSource extends AbstractSource implements ITMSSource { - readonly _descriptor: XYZTMSSourceDescriptor; - constructor(descriptor: XYZTMSSourceDescriptor) { - super(descriptor, {}); - this._descriptor = descriptor; - } - - async getDisplayName(): Promise { - return this._descriptor.urlTemplate; - } - - async getUrlTemplate(): Promise { - return 'template/{x}/{y}/{z}.png'; - } - - getTileLayerId() { - return this._descriptor.id; - } - - getVectorStyleSheetAndSpriteMeta() { - throw new Error('network error'); - } -} - -describe('VectorTileLayer', () => { - it('should correctly inject tileLayerId in meta', async () => { - const source = new MockTileSource(sourceDescriptor); - - const args: ITileLayerArguments = { - source, - layerDescriptor: { id: 'layerid', sourceDescriptor }, - }; - - const layer: ILayer = new VectorTileLayer(args); - - let actualMeta; - let actualErrorMessage; - const mockContext = { - startLoading: (requestId: string, token: string, meta: unknown) => { - actualMeta = meta; - }, - onLoadError: (requestId: string, token: string, message: string) => { - actualErrorMessage = message; - }, - dataFilters: { foo: 'bar' } as unknown as DataFilters, - } as unknown as DataRequestContext; - - await layer.syncData(mockContext); - - expect(actualMeta).toStrictEqual({ tileLayerId: 'mockSourceId' }); - expect(actualErrorMessage).toStrictEqual('network error'); - }); -}); diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/create_layer_descriptor.test.ts index 7c762ac3409f..9e843c98cb64 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/create_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/observability/create_layer_descriptor.test.ts @@ -165,7 +165,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }); }); @@ -345,7 +345,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }); }); diff --git a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.test.ts b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.test.ts index b5d4edc8cb43..b7e07e6f8e27 100644 --- a/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/wizards/solution_layers/security/create_layer_descriptors.test.ts @@ -138,7 +138,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, { @@ -248,7 +248,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, { @@ -365,7 +365,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, ]); @@ -480,7 +480,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, { @@ -590,7 +590,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, { @@ -707,7 +707,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, ]); @@ -822,7 +822,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, { @@ -932,7 +932,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, { @@ -1049,7 +1049,7 @@ describe('createLayerDescriptor', () => { }, type: 'VECTOR', }, - type: 'VECTOR', + type: 'GEOJSON_VECTOR', visible: true, }, ]); diff --git a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 9138b199fd57..26afa65b9527 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -8,10 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { LayerWizard, RenderWizardArguments } from '../../layers'; -// @ts-ignore import { EMSTMSSource, getSourceTitle } from './ems_tms_source'; -// @ts-ignore -import { VectorTileLayer } from '../../layers/vector_tile_layer/vector_tile_layer'; +import { EmsVectorTileLayer } from '../../layers/ems_vector_tile_layer/ems_vector_tile_layer'; import { EmsTmsSourceConfig } from './tile_service_select'; import { CreateSourceEditor } from './create_source_editor'; import { getEMSSettings } from '../../../kibana_services'; @@ -45,7 +43,7 @@ export const emsBaseMapLayerWizardConfig: LayerWizard = { icon: WorldMapLayerIcon, renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = (sourceConfig: EmsTmsSourceConfig) => { - const layerDescriptor = VectorTileLayer.createDescriptor({ + const layerDescriptor = EmsVectorTileLayer.createDescriptor({ sourceDescriptor: EMSTMSSource.createDescriptor(sourceConfig), }); previewLayers([layerDescriptor]); diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx index 32658447acf2..3ddb804cac21 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.test.tsx @@ -18,7 +18,7 @@ jest.mock('uuid/v4', () => { }); const defaultProps = { - currentLayerType: LAYER_TYPE.VECTOR, + currentLayerType: LAYER_TYPE.GEOJSON_VECTOR, indexPatternId: 'foobar', onChange: async () => {}, metrics: [], diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx index ba10479a2bd2..fb748cdc63af 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx @@ -82,11 +82,13 @@ export class UpdateSourceEditor extends Component { _onResolutionChange = async (resolution: GRID_RESOLUTION, metrics: AggDescriptor[]) => { let newLayerType; if ( - this.props.currentLayerType === LAYER_TYPE.VECTOR || - this.props.currentLayerType === LAYER_TYPE.TILED_VECTOR + this.props.currentLayerType === LAYER_TYPE.GEOJSON_VECTOR || + this.props.currentLayerType === LAYER_TYPE.MVT_VECTOR ) { newLayerType = - resolution === GRID_RESOLUTION.SUPER_FINE ? LAYER_TYPE.TILED_VECTOR : LAYER_TYPE.VECTOR; + resolution === GRID_RESOLUTION.SUPER_FINE + ? LAYER_TYPE.MVT_VECTOR + : LAYER_TYPE.GEOJSON_VECTOR; } await this.props.onChange( diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx index ddf727842780..fcf915618a0b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/scaling_form.tsx @@ -86,9 +86,9 @@ export class ScalingForm extends Component { if (optionId === SCALING_TYPES.CLUSTERS) { layerType = LAYER_TYPE.BLENDED_VECTOR; } else if (optionId === SCALING_TYPES.MVT) { - layerType = LAYER_TYPE.TILED_VECTOR; + layerType = LAYER_TYPE.MVT_VECTOR; } else { - layerType = LAYER_TYPE.VECTOR; + layerType = LAYER_TYPE.GEOJSON_VECTOR; } this.props.onChange({ propName: 'scalingType', value: optionId, newLayerType: layerType }); diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index 70f56f8f4e6d..ec69989a8313 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -12,7 +12,7 @@ import { LayerWizard, RenderWizardArguments } from '../../layers'; import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; -import { TileLayer } from '../../layers/tile_layer/tile_layer'; +import { RasterTileLayer } from '../../layers/raster_tile_layer/raster_tile_layer'; import { getKibanaTileMap } from '../../../util'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; @@ -29,7 +29,7 @@ export const kibanaBasemapLayerWizardConfig: LayerWizard = { icon: 'logoKibana', renderWizard: ({ previewLayers }: RenderWizardArguments) => { const onSourceConfigChange = () => { - const layerDescriptor = TileLayer.createDescriptor({ + const layerDescriptor = RasterTileLayer.createDescriptor({ sourceDescriptor: KibanaTilemapSource.createDescriptor(), }); previewLayers([layerDescriptor]); diff --git a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx index adbb23b921d4..19f31d481f58 100644 --- a/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx @@ -12,7 +12,7 @@ import { WMSCreateSourceEditor } from './wms_create_source_editor'; // @ts-ignore import { sourceTitle, WMSSource } from './wms_source'; import { LayerWizard, RenderWizardArguments } from '../../layers'; -import { TileLayer } from '../../layers/tile_layer/tile_layer'; +import { RasterTileLayer } from '../../layers/raster_tile_layer/raster_tile_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { WebMapServiceLayerIcon } from '../../layers/wizards/icons/web_map_service_layer_icon'; @@ -29,7 +29,7 @@ export const wmsLayerWizardConfig: LayerWizard = { return; } - const layerDescriptor = TileLayer.createDescriptor({ + const layerDescriptor = RasterTileLayer.createDescriptor({ sourceDescriptor: WMSSource.createDescriptor(sourceConfig), }); previewLayers([layerDescriptor]); diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx index 95a2b104c7a1..82aab592a134 100644 --- a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { XYZTMSEditor, XYZTMSSourceConfig } from './xyz_tms_editor'; import { XYZTMSSource, sourceTitle } from './xyz_tms_source'; import { LayerWizard, RenderWizardArguments } from '../../layers'; -import { TileLayer } from '../../layers/tile_layer/tile_layer'; +import { RasterTileLayer } from '../../layers/raster_tile_layer/raster_tile_layer'; import { LAYER_WIZARD_CATEGORY } from '../../../../common/constants'; import { WorldMapLayerIcon } from '../../layers/wizards/icons/world_map_layer_icon'; @@ -32,7 +32,7 @@ export const tmsLayerWizardConfig: LayerWizard = { return; } - const layerDescriptor = TileLayer.createDescriptor({ + const layerDescriptor = RasterTileLayer.createDescriptor({ sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig), }); previewLayers([layerDescriptor]); diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx index 82795b8bd931..ebe0cc6a4178 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx @@ -57,7 +57,7 @@ const mockLayer = { return '1'; }, getType: () => { - return LAYER_TYPE.VECTOR; + return LAYER_TYPE.GEOJSON_VECTOR; }, getDisplayName: () => { return 'layer 1'; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts b/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts index 17994bf79948..f5de99d04c01 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/utils.ts @@ -11,7 +11,7 @@ import { TileMetaFeature } from '../../../common/descriptor_types'; import { RGBAImage } from './image_utils'; import { isGlDrawLayer } from './sort_layers'; import { ILayer } from '../../classes/layers/layer'; -import { EmsSpriteSheet } from '../../classes/layers/vector_tile_layer/vector_tile_layer'; +import { EmsSpriteSheet } from '../../classes/layers/ems_vector_tile_layer/ems_vector_tile_layer'; import { ES_MVT_META_LAYER_NAME } from '../../classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer'; export function removeOrphanedSourcesAndLayers( diff --git a/x-pack/plugins/maps/public/locators.test.ts b/x-pack/plugins/maps/public/locators.test.ts index bfcd34c4ae71..aabae1a26c1d 100644 --- a/x-pack/plugins/maps/public/locators.test.ts +++ b/x-pack/plugins/maps/public/locators.test.ts @@ -52,7 +52,7 @@ describe('visualize url generator', () => { { id: LAYER_ID, visible: true, - type: LAYER_TYPE.VECTOR, + type: LAYER_TYPE.GEOJSON_VECTOR, sourceDescriptor: { id: LAYER_ID, type: SOURCE_TYPES.ES_SEARCH, @@ -70,7 +70,7 @@ describe('visualize url generator', () => { expect(location).toMatchObject({ app: 'maps', - path: `/map#/?_g=()&_a=()&initialLayers=(id%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CsourceDescriptor%3A(geoField%3Atest%2Cid%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CindexPatternId%3A'90943e30-9a47-11e8-b64d-95841ca0b247'%2Clabel%3A'Sample%20Data'%2CscalingType%3ALIMIT%2CtooltipProperties%3A!()%2Ctype%3AES_SEARCH)%2Ctype%3AVECTOR%2Cvisible%3A!t)`, + path: `/map#/?_g=()&_a=()&initialLayers=(id%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CsourceDescriptor%3A(geoField%3Atest%2Cid%3A'13823000-99b9-11ea-9eb6-d9e8adceb647'%2CindexPatternId%3A'90943e30-9a47-11e8-b64d-95841ca0b247'%2Clabel%3A'Sample%20Data'%2CscalingType%3ALIMIT%2CtooltipProperties%3A!()%2Ctype%3AES_SEARCH)%2Ctype%3AGEOJSON_VECTOR%2Cvisible%3A!t)`, state: {}, }); }); diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index 4f336d9a8ad2..6b903214f8f9 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -8,7 +8,7 @@ import { LAYER_STYLE_TYPE, LAYER_TYPE, SOURCE_TYPES } from '../../common/constants'; jest.mock('../classes/layers/heatmap_layer', () => {}); -jest.mock('../classes/layers/vector_tile_layer/vector_tile_layer', () => {}); +jest.mock('../classes/layers/ems_vector_tile_layer/ems_vector_tile_layer', () => {}); jest.mock('../classes/joins/inner_join', () => {}); jest.mock('../kibana_services', () => ({ getTimeFilter: () => ({ @@ -232,7 +232,7 @@ describe('getQueryableUniqueIndexPatternIds', () => { indexPatternId: string; }) { return { - type: LAYER_TYPE.VECTOR, + type: LAYER_TYPE.GEOJSON_VECTOR, style: { type: LAYER_STYLE_TYPE.VECTOR, }, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index f58525ea6f97..5f308387e9d8 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -10,9 +10,8 @@ import { FeatureCollection } from 'geojson'; import _ from 'lodash'; import { Adapters } from 'src/plugins/inspector/public'; import type { Query } from 'src/plugins/data/common'; -import { TileLayer } from '../classes/layers/tile_layer/tile_layer'; -// @ts-ignore -import { VectorTileLayer } from '../classes/layers/vector_tile_layer/vector_tile_layer'; +import { RasterTileLayer } from '../classes/layers/raster_tile_layer/raster_tile_layer'; +import { EmsVectorTileLayer } from '../classes/layers/ems_vector_tile_layer/ems_vector_tile_layer'; import { BlendedVectorLayer, IVectorLayer, @@ -59,6 +58,7 @@ import { ISource } from '../classes/sources/source'; import { ITMSSource } from '../classes/sources/tms_source'; import { IVectorSource } from '../classes/sources/vector_source'; import { ESGeoGridSource } from '../classes/sources/es_geo_grid_source'; +import { EMSTMSSource } from '../classes/sources/ems_tms_source'; import { ILayer } from '../classes/layers/layer'; import { getIsReadOnly } from './ui_selectors'; @@ -70,9 +70,9 @@ export function createLayerInstance( const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); switch (layerDescriptor.type) { - case LAYER_TYPE.TILE: - return new TileLayer({ layerDescriptor, source: source as ITMSSource }); - case LAYER_TYPE.VECTOR: + case LAYER_TYPE.RASTER_TILE: + return new RasterTileLayer({ layerDescriptor, source: source as ITMSSource }); + case LAYER_TYPE.GEOJSON_VECTOR: const joins: InnerJoin[] = []; const vectorLayerDescriptor = layerDescriptor as VectorLayerDescriptor; if (vectorLayerDescriptor.joins) { @@ -87,8 +87,8 @@ export function createLayerInstance( joins, chartsPaletteServiceGetColor, }); - case LAYER_TYPE.VECTOR_TILE: - return new VectorTileLayer({ layerDescriptor, source: source as ITMSSource }); + case LAYER_TYPE.EMS_VECTOR_TILE: + return new EmsVectorTileLayer({ layerDescriptor, source: source as EMSTMSSource }); case LAYER_TYPE.HEATMAP: return new HeatmapLayer({ layerDescriptor: layerDescriptor as HeatmapLayerDescriptor, @@ -100,7 +100,7 @@ export function createLayerInstance( source: source as IVectorSource, chartsPaletteServiceGetColor, }); - case LAYER_TYPE.TILED_VECTOR: + case LAYER_TYPE.MVT_VECTOR: return new MvtVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, diff --git a/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts b/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts index 1cbb3e92134b..1da2ca14bd16 100644 --- a/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts +++ b/x-pack/plugins/maps/public/trigger_actions/visualize_geo_field_action.ts @@ -61,7 +61,7 @@ const getMapsLink = async (context: VisualizeFieldContext) => { { id: uuid(), visible: true, - type: supportsClustering ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.VECTOR, + type: supportsClustering ? LAYER_TYPE.BLENDED_VECTOR : LAYER_TYPE.GEOJSON_VECTOR, sourceDescriptor: { id: uuid(), type: SOURCE_TYPES.ES_SEARCH, diff --git a/x-pack/plugins/maps/server/embeddable_migrations.ts b/x-pack/plugins/maps/server/embeddable_migrations.ts index 962f5c4fb0d7..f5356e5eb29a 100644 --- a/x-pack/plugins/maps/server/embeddable_migrations.ts +++ b/x-pack/plugins/maps/server/embeddable_migrations.ts @@ -9,6 +9,7 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { MapSavedObjectAttributes } from '../common/map_saved_object_type'; import { moveAttribution } from '../common/migrations/move_attribution'; import { setEmsTmsDefaultModes } from '../common/migrations/set_ems_tms_default_modes'; +import { renameLayerTypes } from '../common/migrations/rename_layer_types'; /* * Embeddables such as Maps, Lens, and Visualize can be embedded by value or by reference on a dashboard. @@ -42,4 +43,16 @@ export const embeddableMigrations = { return state; } }, + '8.1.0': (state: SerializableRecord) => { + try { + return { + ...state, + attributes: renameLayerTypes(state as { attributes: MapSavedObjectAttributes }), + } as SerializableRecord; + } catch (e) { + // Do not fail migration for invalid layerListJSON + // Maps application can display invalid layerListJSON error when saved object is viewed + return state; + } + }, }; diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js index a415d181900d..796d641f3eff 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.test.js @@ -202,7 +202,7 @@ describe('buildMapsSavedObjectsTelemetry', () => { max: 1, min: 1, }, - VECTOR: { + GEOJSON_VECTOR: { avg: 1.2, max: 2, min: 1, diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index 93a9a118d23b..3b257fb812bf 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -160,7 +160,7 @@ async function isGeoShapeAggLayer(layer: LayerDescriptor): Promise { } if ( - layer.type !== LAYER_TYPE.VECTOR && + layer.type !== LAYER_TYPE.GEOJSON_VECTOR && layer.type !== LAYER_TYPE.BLENDED_VECTOR && layer.type !== LAYER_TYPE.HEATMAP ) { diff --git a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json b/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json index 3adaaaf091e0..e9427a996ad1 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json +++ b/x-pack/plugins/maps/server/maps_telemetry/test_resources/sample_map_saved_objects.json @@ -6,7 +6,7 @@ "title": "Italy Map", "description": "", "mapStateJSON": "{\"zoom\":4.82,\"center\":{\"lon\":11.41545,\"lat\":42.0865},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"language\":\"lucene\",\"query\":\"\"}}", - "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"italy_provinces\"},\"id\":\"0oye8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#0c1f70\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"053fe296-f5ae-4cb0-9e73-a5752cb9ba74\",\"indexPatternId\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"geoField\":\"DestLocation\",\"requestType\":\"point\",\"resolution\":\"COARSE\"},\"id\":\"1gx22\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Greens\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}}},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"italy_provinces\"},\"id\":\"0oye8\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#0c1f70\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"053fe296-f5ae-4cb0-9e73-a5752cb9ba74\",\"indexPatternId\":\"d3d7af60-4c81-11e8-b3d7-01146121b73d\",\"geoField\":\"DestLocation\",\"requestType\":\"point\",\"resolution\":\"COARSE\"},\"id\":\"1gx22\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"color\":\"Greens\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"label\":\"Count\",\"name\":\"doc_count\",\"origin\":\"source\"},\"minSize\":4,\"maxSize\":32}}}},\"type\":\"GEOJSON_VECTOR\"}]", "uiStateJSON": "{}" }, "references": [ @@ -21,7 +21,7 @@ "title": "France Map", "description": "", "mapStateJSON": "{\"zoom\":3.43,\"center\":{\"lon\":-16.30411,\"lat\":42.88411},\"timeFilters\":{\"from\":\"now-15w\",\"to\":\"now\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", - "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\"},\"id\":\"65xbw\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.25,\"visible\":true,\"joins\":[{\"leftField\":\"iso_3166_2\",\"right\":{\"id\":\"6a263f96-7a96-4f5a-a00e-c89178c1d017\"}}],\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#19c1e6\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"},{\"sourceDescriptor\":{\"id\":\"240125db-e612-4001-b853-50107e55d984\",\"type\":\"ES_SEARCH\",\"scalingType\":\"LIMIT\",\"indexPatternId\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"id\":\"mdae9\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#1ce619\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"france_departments\"},\"id\":\"65xbw\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.25,\"visible\":true,\"joins\":[{\"leftField\":\"iso_3166_2\",\"right\":{\"id\":\"6a263f96-7a96-4f5a-a00e-c89178c1d017\"}}],\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#19c1e6\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"GEOJSON_VECTOR\"},{\"sourceDescriptor\":{\"id\":\"240125db-e612-4001-b853-50107e55d984\",\"type\":\"ES_SEARCH\",\"scalingType\":\"LIMIT\",\"indexPatternId\":\"ff959d40-b880-11e8-a6d9-e546fe2bba5f\",\"geoField\":\"geoip.location\",\"limit\":2048,\"filterByMapBounds\":true,\"tooltipProperties\":[]},\"id\":\"mdae9\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#1ce619\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"GEOJSON_VECTOR\"}]", "uiStateJSON": "{}" }, "references": [ @@ -36,7 +36,7 @@ "title": "Canada Map", "description": "", "mapStateJSON": "{\"zoom\":2.12,\"center\":{\"lon\":-88.67592,\"lat\":34.23257},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", - "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"canada_provinces\"},\"id\":\"kt086\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#60895e\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"VECTOR\"}]", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"EMS_TMS\",\"id\":\"road_map\"},\"id\":\"csq5v\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.65,\"visible\":true,\"style\":{\"type\":\"TILE\",\"properties\":{}},\"type\":\"TILE\"},{\"sourceDescriptor\":{\"type\":\"EMS_FILE\",\"id\":\"canada_provinces\"},\"id\":\"kt086\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":0.75,\"visible\":true,\"style\":{\"type\":\"VECTOR\",\"properties\":{\"fillColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#60895e\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":1}},\"iconSize\":{\"type\":\"STATIC\",\"options\":{\"size\":10}}}},\"type\":\"GEOJSON_VECTOR\"}]", "uiStateJSON": "{}" }, "references": [ @@ -51,7 +51,7 @@ "title": "Single cluster layer with geo_shape field", "description": "", "mapStateJSON": "{\"zoom\":2.12,\"center\":{\"lon\":-88.67592,\"lat\":34.23257},\"timeFilters\":{\"from\":\"now-15m\",\"to\":\"now\",\"mode\":\"quick\"},\"refreshConfig\":{\"isPaused\":false,\"interval\":0},\"query\":{\"query\":\"\",\"language\":\"lucene\"}}", - "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"51afb7d0-c628-11ea-87d0-0242ac130003\",\"geoField\":\"geometry\",\"metrics\":[{\"type\":\"count\"}],\"requestType\":\"point\",\"resolution\":\"COARSE\",\"indexPatternId\":\"4a7f6010-0aed-11ea-9dd2-95afd7ad44d4\"},\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"doc_count\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":7,\"maxSize\":32,\"field\":{\"name\":\"doc_count\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"doc_count\",\"origin\":\"source\"}}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"id\":\"8d384d5d-6353-468f-b8f8-8eaa487358c4\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"type\":\"VECTOR\",\"joins\":[]}]", + "layerListJSON": "[{\"sourceDescriptor\":{\"type\":\"ES_GEO_GRID\",\"id\":\"51afb7d0-c628-11ea-87d0-0242ac130003\",\"geoField\":\"geometry\",\"metrics\":[{\"type\":\"count\"}],\"requestType\":\"point\",\"resolution\":\"COARSE\",\"indexPatternId\":\"4a7f6010-0aed-11ea-9dd2-95afd7ad44d4\"},\"style\":{\"type\":\"VECTOR\",\"properties\":{\"icon\":{\"type\":\"STATIC\",\"options\":{\"value\":\"marker\"}},\"fillColor\":{\"type\":\"DYNAMIC\",\"options\":{\"color\":\"Blues\",\"colorCategory\":\"palette_0\",\"field\":{\"name\":\"doc_count\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3},\"type\":\"ORDINAL\"}},\"lineColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFF\"}},\"lineWidth\":{\"type\":\"STATIC\",\"options\":{\"size\":0}},\"iconSize\":{\"type\":\"DYNAMIC\",\"options\":{\"minSize\":7,\"maxSize\":32,\"field\":{\"name\":\"doc_count\",\"origin\":\"source\"},\"fieldMetaOptions\":{\"isEnabled\":true,\"sigma\":3}}},\"iconOrientation\":{\"type\":\"STATIC\",\"options\":{\"orientation\":0}},\"labelText\":{\"type\":\"DYNAMIC\",\"options\":{\"field\":{\"name\":\"doc_count\",\"origin\":\"source\"}}},\"labelColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#000000\"}},\"labelSize\":{\"type\":\"STATIC\",\"options\":{\"size\":14}},\"labelBorderColor\":{\"type\":\"STATIC\",\"options\":{\"color\":\"#FFFFFF\"}},\"symbolizeAs\":{\"options\":{\"value\":\"circle\"}},\"labelBorderSize\":{\"options\":{\"size\":\"SMALL\"}}},\"isTimeAware\":true},\"id\":\"8d384d5d-6353-468f-b8f8-8eaa487358c4\",\"label\":null,\"minZoom\":0,\"maxZoom\":24,\"alpha\":1,\"visible\":true,\"type\":\"GEOJSON_VECTOR\",\"joins\":[]}]", "uiStateJSON": "{}" }, "references": [ diff --git a/x-pack/plugins/maps/server/maps_telemetry/util.ts b/x-pack/plugins/maps/server/maps_telemetry/util.ts index 27190c9b8214..defc2cb9aa9b 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/util.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/util.ts @@ -265,7 +265,7 @@ export function getTermJoinsPerCluster( layerLists: LayerDescriptor[][] ): TELEMETRY_TERM_JOIN_COUNTS_PER_CLUSTER { return getCountsByCluster(layerLists, (layerDescriptor: LayerDescriptor) => { - return layerDescriptor.type === LAYER_TYPE.VECTOR && + return layerDescriptor.type === LAYER_TYPE.GEOJSON_VECTOR && (layerDescriptor as VectorLayerDescriptor)?.joins?.length ? TELEMETRY_TERM_JOIN : null; diff --git a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js index 6d2324686042..986878e65eb8 100644 --- a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js +++ b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js @@ -18,6 +18,7 @@ import { setDefaultAutoFitToBounds } from '../../common/migrations/set_default_a import { addTypeToTermJoin } from '../../common/migrations/add_type_to_termjoin'; import { moveAttribution } from '../../common/migrations/move_attribution'; import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_default_modes'; +import { renameLayerTypes } from '../../common/migrations/rename_layer_types'; function logMigrationWarning(context, errorMsg, doc) { context.log.warning( @@ -172,6 +173,19 @@ export const savedObjectMigrations = { try { const attributes = setEmsTmsDefaultModes(doc); + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } + }, + '8.1.0': (doc, context) => { + try { + const attributes = renameLayerTypes(doc); + return { ...doc, attributes, diff --git a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx index 0eb6e356e139..cb3651e4a458 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx @@ -126,7 +126,7 @@ export const getChoroplethAnomaliesLayer = ( isTimeAware: true, }, visible: false, - type: LAYER_TYPE.VECTOR, + type: LAYER_TYPE.GEOJSON_VECTOR, }; }; diff --git a/x-pack/test/api_integration/apis/maps/migrations.js b/x-pack/test/api_integration/apis/maps/migrations.js index 19f79da94025..26de152f1473 100644 --- a/x-pack/test/api_integration/apis/maps/migrations.js +++ b/x-pack/test/api_integration/apis/maps/migrations.js @@ -44,7 +44,7 @@ export default function ({ getService }) { type: 'index-pattern', }, ]); - expect(resp.body.migrationVersion).to.eql({ map: '8.0.0' }); // migrtionVersion is derived from both "migrations" and "convertToMultiNamespaceVersion" fields when the object is registered + expect(resp.body.migrationVersion).to.eql({ map: '8.1.0' }); // migrtionVersion is derived from both "migrations" and "convertToMultiNamespaceVersion" fields when the object is registered expect(resp.body.attributes.layerListJSON.includes('indexPatternRefName')).to.be(true); }); @@ -90,7 +90,7 @@ export default function ({ getService }) { } expect(panels.length).to.be(1); expect(panels[0].type).to.be('map'); - expect(panels[0].version).to.be('8.0.0'); + expect(panels[0].version).to.be('8.1.0'); }); }); }); From ded75bdb7ba22c8bb3271594c80a5c88f2332a66 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 3 Dec 2021 18:48:25 +0000 Subject: [PATCH 13/25] chore(NA): splits types from code on @kbn/crypto (#120371) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 1 + packages/BUILD.bazel | 1 + packages/kbn-crypto/BUILD.bazel | 26 ++++++++++++++++++---- packages/kbn-crypto/package.json | 3 +-- packages/kbn-server-http-tools/BUILD.bazel | 2 +- yarn.lock | 4 ++++ 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 1e5a135b723e..b2299c239188 100644 --- a/package.json +++ b/package.json @@ -565,6 +565,7 @@ "@types/kbn__apm-utils": "link:bazel-bin/packages/kbn-apm-utils/npm_module_types", "@types/kbn__cli-dev-mode": "link:bazel-bin/packages/kbn-cli-dev-mode/npm_module_types", "@types/kbn__config": "link:bazel-bin/packages/kbn-config/npm_module_types", + "@types/kbn__crypto": "link:bazel-bin/packages/kbn-crypto/npm_module_types", "@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types", "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/license-checker": "15.0.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 8208496f7d80..1cf887290af7 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -84,6 +84,7 @@ filegroup( "//packages/kbn-apm-utils:build_types", "//packages/kbn-cli-dev-mode:build_types", "//packages/kbn-config:build_types", + "//packages/kbn-crypto:build_types", "//packages/kbn-i18n:build_types", "//packages/kbn-i18n-react:build_types", ], diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index 0f35aab46107..81ee6d770103 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -1,10 +1,11 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-crypto" PKG_REQUIRE_NAME = "@kbn/crypto" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__crypto" SOURCE_FILES = glob( [ @@ -72,7 +73,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -91,3 +92,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-crypto/package.json b/packages/kbn-crypto/package.json index 8fa6cd3c232f..96bf21906ed4 100644 --- a/packages/kbn-crypto/package.json +++ b/packages/kbn-crypto/package.json @@ -3,6 +3,5 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target_node/index.js", - "types": "./target_types/index.d.ts" + "main": "./target_node/index.js" } diff --git a/packages/kbn-server-http-tools/BUILD.bazel b/packages/kbn-server-http-tools/BUILD.bazel index 609fe6d00f17..b9eae3d02243 100644 --- a/packages/kbn-server-http-tools/BUILD.bazel +++ b/packages/kbn-server-http-tools/BUILD.bazel @@ -38,7 +38,7 @@ RUNTIME_DEPS = [ TYPES_DEPS = [ "//packages/kbn-config-schema", - "//packages/kbn-crypto", + "//packages/kbn-crypto:npm_module_types", "@npm//@hapi/hapi", "@npm//@hapi/hoek", "@npm//joi", diff --git a/yarn.lock b/yarn.lock index fc670fda132c..c4d2a31724d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5828,6 +5828,10 @@ version "0.0.0" uid "" +"@types/kbn__crypto@link:bazel-bin/packages/kbn-crypto/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__i18n-react@link:bazel-bin/packages/kbn-i18n-react/npm_module_types": version "0.0.0" uid "" From 3521a928d57adf531ac8331ed1008ac8e5e46ce8 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 3 Dec 2021 14:56:01 -0500 Subject: [PATCH 14/25] More descriptive audit integration test errors (#120354) --- .../security_api_integration/tests/audit/audit_log.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_api_integration/tests/audit/audit_log.ts b/x-pack/test/security_api_integration/tests/audit/audit_log.ts index fd7db8faadd5..bb4c27976c85 100644 --- a/x-pack/test/security_api_integration/tests/audit/audit_log.ts +++ b/x-pack/test/security_api_integration/tests/audit/audit_log.ts @@ -22,7 +22,14 @@ class FileWrapper { } async readJSON() { const content = await this.read(); - return content.map((l) => JSON.parse(l)); + try { + return content.map((l) => JSON.parse(l)); + } catch (err) { + const contentString = content.join('\n'); + throw new Error( + `Failed to parse audit log JSON, error: "${err.message}", audit.log contents:\n${contentString}` + ); + } } // writing in a file is an async operation. we use this method to make sure logs have been written. async isNotEmpty() { From b5895ec0b441befa5d0a4ada2ac602efd5ffe386 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Fri, 3 Dec 2021 15:05:24 -0500 Subject: [PATCH 15/25] [Alerting] Skip writing alerts when rule execution times out (#114518) * Added cancel() to alerting task runner and writing event log document * Updating rule saved object with timeout execution status * Skip scheduling actions and logging event log for alerts if rule execution is cancelled * Adding config for disabling skipping actions * Fixing types * Adding flag for rule types to opt out of skipping acitons * Passing function into rule executor to determine whether to write AAD * Using task runner uuid to differentiate between task instances * Adding functional test * Fixing types * Updating service function name and adding logic to persistence rule type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/alerting/server/mocks.ts | 1 + .../server/task_runner/task_runner.ts | 1 + x-pack/plugins/alerting/server/types.ts | 1 + .../server/routes/alerts/test_utils/index.ts | 1 + .../utils/create_lifecycle_executor.test.ts | 27 +++++++++++++++++++ .../server/utils/create_lifecycle_executor.ts | 14 +++++++--- .../utils/create_lifecycle_rule_type.test.ts | 23 +++++++++++++++- .../create_persistence_rule_type_wrapper.ts | 11 +++++++- .../server/utils/rule_executor_test_utils.ts | 3 +++ .../routes/rules/preview_rules_route.ts | 3 +++ .../rule_types/__mocks__/rule_type.ts | 1 + 11 files changed, 81 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index 7fb748a30503..f23dbf05449a 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -74,6 +74,7 @@ const createAlertServicesMock = < .mockReturnValue(alertInstanceFactoryMock), savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + shouldWriteAlerts: () => true, }; }; export type AlertServicesMock = ReturnType; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index fb7268ef529d..0cf520278739 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -323,6 +323,7 @@ export class TaskRunner< InstanceContext, WithoutReservedActionGroups >(alertInstances), + shouldWriteAlerts: () => this.shouldLogAndScheduleActionsForAlerts(), }, params, state: alertTypeState as State, diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index c1645936c06e..343b717dcb1a 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -75,6 +75,7 @@ export interface AlertServices< alertInstanceFactory: ( id: string ) => PublicAlertInstance; + shouldWriteAlerts: () => boolean; } export interface AlertExecutorOptions< diff --git a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts index 22649a701046..a8610bbcc8d3 100644 --- a/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts +++ b/x-pack/plugins/apm/server/routes/alerts/test_utils/index.ts @@ -45,6 +45,7 @@ export const createRuleTypeMocks = () => { alertInstanceFactory: jest.fn(() => ({ scheduleActions })), alertWithLifecycle: jest.fn(), logger: loggerMock, + shouldWriteAlerts: () => true, }; return { diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index fcddcab378bc..2c5fe09d8056 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -350,6 +350,33 @@ describe('createLifecycleExecutor', () => { }) ); }); + + it('does not write alert documents when rule execution is cancelled and feature flags indicate to skip', async () => { + const logger = loggerMock.create(); + const ruleDataClientMock = createRuleDataClientMock(); + const executor = createLifecycleExecutor( + logger, + ruleDataClientMock + )<{}, TestRuleState, never, never, never>(async (options) => { + expect(options.state).toEqual(initialRuleState); + + const nextRuleState: TestRuleState = { + aRuleStateKey: 'NEXT_RULE_STATE_VALUE', + }; + + return nextRuleState; + }); + + await executor( + createDefaultAlertExecutorOptions({ + params: {}, + state: { wrapped: initialRuleState, trackedAlerts: {} }, + shouldWriteAlerts: false, + }) + ); + + expect(ruleDataClientMock.getWriter).not.toHaveBeenCalled(); + }); }); type TestRuleState = Record & { diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 1acbc0c3f43b..c30b1654a358 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -140,7 +140,7 @@ export const createLifecycleExecutor = > ): Promise> => { const { - services: { alertInstanceFactory }, + services: { alertInstanceFactory, shouldWriteAlerts }, state: previousState, } = options; @@ -281,7 +281,15 @@ export const createLifecycleExecutor = const newEventsToIndex = makeEventsDataMapFor(newAlertIds); const allEventsToIndex = [...trackedEventsToIndex, ...newEventsToIndex]; - if (allEventsToIndex.length > 0 && ruleDataClient.isWriteEnabled()) { + // Only write alerts if: + // - writing is enabled + // AND + // - rule execution has not been cancelled due to timeout + // OR + // - if execution has been cancelled due to timeout, if feature flags are configured to write alerts anyway + const writeAlerts = ruleDataClient.isWriteEnabled() && shouldWriteAlerts(); + + if (allEventsToIndex.length > 0 && writeAlerts) { logger.debug(`Preparing to index ${allEventsToIndex.length} alerts.`); await ruleDataClient.getWriter().bulk({ @@ -307,6 +315,6 @@ export const createLifecycleExecutor = return { wrapped: nextWrappedState ?? ({} as State), - trackedAlerts: ruleDataClient.isWriteEnabled() ? nextTrackedAlerts : {}, + trackedAlerts: writeAlerts ? nextTrackedAlerts : {}, }; }; diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 2284ad5e796e..f0e2412629bb 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -21,7 +21,7 @@ import { createLifecycleRuleTypeFactory } from './create_lifecycle_rule_type_fac type RuleTestHelpers = ReturnType; -function createRule() { +function createRule(shouldWriteAlerts: boolean = true) { const ruleDataClientMock = createRuleDataClientMock(); const factory = createLifecycleRuleTypeFactory({ @@ -110,6 +110,7 @@ function createRule() { alertInstanceFactory, savedObjectsClient: {} as any, scopedClusterClient: {} as any, + shouldWriteAlerts: () => shouldWriteAlerts, }, spaceId: 'spaceId', state, @@ -152,6 +153,26 @@ describe('createLifecycleRuleTypeFactory', () => { }); }); + describe('when rule is cancelled due to timeout and config flags indicate to skip actions', () => { + beforeEach(() => { + helpers = createRule(false); + helpers.ruleDataClientMock.isWriteEnabled.mockReturnValue(true); + }); + + it("doesn't persist anything", async () => { + await helpers.alertWithLifecycle([ + { + id: 'opbeans-java', + fields: { + 'service.name': 'opbeans-java', + }, + }, + ]); + + expect(helpers.ruleDataClientMock.getWriter().bulk).toHaveBeenCalledTimes(0); + }); + }); + describe('when alerts are new', () => { beforeEach(async () => { await helpers.alertWithLifecycle([ diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts index afdcf856a872..de1193771dd9 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts @@ -25,7 +25,16 @@ export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper const numAlerts = alerts.length; logger.debug(`Found ${numAlerts} alerts.`); - if (ruleDataClient.isWriteEnabled() && numAlerts) { + // Only write alerts if: + // - writing is enabled + // AND + // - rule execution has not been cancelled due to timeout + // OR + // - if execution has been cancelled due to timeout, if feature flags are configured to write alerts anyway + const writeAlerts = + ruleDataClient.isWriteEnabled() && options.services.shouldWriteAlerts(); + + if (writeAlerts && numAlerts) { const commonRuleFields = getCommonAlertFields(options); const CHUNK_SIZE = 10000; diff --git a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts index 95a676115237..288830f4b380 100644 --- a/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts +++ b/x-pack/plugins/rule_registry/server/utils/rule_executor_test_utils.ts @@ -31,6 +31,7 @@ export const createDefaultAlertExecutorOptions = < createdAt = new Date(), startedAt = new Date(), updatedAt = new Date(), + shouldWriteAlerts = true, }: { alertId?: string; ruleName?: string; @@ -39,6 +40,7 @@ export const createDefaultAlertExecutorOptions = < createdAt?: Date; startedAt?: Date; updatedAt?: Date; + shouldWriteAlerts?: boolean; }): AlertExecutorOptions => ({ alertId, createdBy: 'CREATED_BY', @@ -69,6 +71,7 @@ export const createDefaultAlertExecutorOptions = < .alertInstanceFactory, savedObjectsClient: savedObjectsClientMock.create(), scopedClusterClient: elasticsearchServiceMock.createScopedClusterClient(), + shouldWriteAlerts: () => shouldWriteAlerts, }, state, updatedBy: null, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts index 882732544dcb..3949e7158202 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/preview_rules_route.ts @@ -122,6 +122,7 @@ export const previewRulesRoute = async ( ruleTypeId: string, ruleTypeName: string, params: TParams, + shouldWriteAlerts: () => boolean, alertInstanceFactory: ( id: string ) => Pick< @@ -157,6 +158,7 @@ export const previewRulesRoute = async ( previousStartedAt, rule, services: { + shouldWriteAlerts, alertInstanceFactory, savedObjectsClient: context.core.savedObjects.client, scopedClusterClient: context.core.elasticsearch.client, @@ -191,6 +193,7 @@ export const previewRulesRoute = async ( signalRuleAlertType.id, signalRuleAlertType.name, previewRuleParams, + () => true, alertInstanceFactoryStub ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index c6f818f04fc5..aaab81a4c66f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -78,6 +78,7 @@ export const createRuleTypeMocks = ( findAlerts: jest.fn(), // TODO: does this stay? alertWithPersistence: jest.fn(), logger: loggerMock, + shouldWriteAlerts: () => true, }; return { From dbde0a75f92303b525d7845735085d08329006a1 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Fri, 3 Dec 2021 15:43:32 -0500 Subject: [PATCH 16/25] Update performance docs to touch on server-side considerations (#120356) * Update performance docs to touch on server-side considerations * update edited date --- dev_docs/key_concepts/performance.mdx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/dev_docs/key_concepts/performance.mdx b/dev_docs/key_concepts/performance.mdx index 0201c7774f85..5d955c789dde 100644 --- a/dev_docs/key_concepts/performance.mdx +++ b/dev_docs/key_concepts/performance.mdx @@ -3,11 +3,13 @@ id: kibDevPerformance slug: /kibana-dev-docs/key-concepts/performance title: Performance summary: Performance tips for Kibana development. -date: 2021-09-02 +date: 2021-12-03 tags: ['kibana', 'onboarding', 'dev', 'performance'] --- -## Keep Kibana fast +## Client-side considerations + +### Lazy load code _tl;dr_: Load as much code lazily as possible. Everyone loves snappy applications with a responsive UI and hates spinners. Users deserve the @@ -105,3 +107,15 @@ Many OSS tools allow you to analyze the generated stats file: Webpack authors - [webpack-visualizer](https://chrisbateman.github.io/webpack-visualizer/) - [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) + +## Server-side considerations + +### Don't block the event loop + +[Node.js is single threaded](https://nodejs.dev/learn/introduction-to-nodejs) which means a single CPU-intensive server-side, synchronous operation will block any other functionality waiting to execute on the Kibana server. The affects background tasks, like alerts, and search sessions, as well as search requests and page loads. + +**When writing code that will run on the server, [don't block the event loop](https://nodejs.org/en/docs/guides/dont-block-the-event-loop/)**. Instead consider: + +- Writing async code. For example, leverage [setImmediate](https://nodejs.dev/learn/understanding-setimmediate) inside for loops. +- Executing logic on the client instead. This may not be a good option if you require a lot of data going back and forth between the server and the client, as that can also slow down the user's experience, especially over slower bandwidth internet connections. +- Worker threads are also an option if the code doesn't rely on stateful Kibana services. If you are interested in using worker threads, please reach out to a tech-lead before doing so. We will likely want to implement a worker threads pool to ensure worker threads cooperate appropriately. \ No newline at end of file From d21d721bd1104a3e231baa5775fcc6749423aafa Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Fri, 3 Dec 2021 12:44:50 -0800 Subject: [PATCH 17/25] [presentation] Create Expression Input (#119411) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/presentation_util/common/index.ts | 6 + src/plugins/presentation_util/kibana.json | 13 +- .../expression_input}/autocomplete.ts | 24 +- .../components/expression_input/constants.ts | 28 ++ .../expression_input.stories.tsx | 94 +++++ .../expression_input/expression_input.tsx | 82 +++++ .../components/expression_input/index.tsx | 16 + .../components/expression_input/language.ts | 23 +- .../components/expression_input/providers.ts | 189 ++++++++++ .../components/expression_input/reference.ts | 24 +- .../public/components/index.tsx | 5 + .../public/components/types.ts | 43 +++ src/plugins/presentation_util/public/index.ts | 19 +- src/plugins/presentation_util/public/mocks.ts | 2 + .../presentation_util/public/plugin.ts | 3 + .../public/services/index.ts | 3 + src/plugins/presentation_util/public/types.ts | 2 + .../presentation_util/storybook/decorator.tsx | 20 +- .../presentation_util/storybook/main.ts | 4 +- .../presentation_util/storybook/manager.ts | 5 + .../canvas/common/lib/autocomplete.test.ts | 198 ---------- x-pack/plugins/canvas/common/lib/index.ts | 1 - x-pack/plugins/canvas/public/application.tsx | 3 - .../components/expression/expression.tsx | 19 +- .../public/components/expression/index.tsx | 7 +- .../expression_input.stories.storyshot | 33 +- .../__stories__/expression_input.stories.tsx | 76 ++-- .../expression_input/expression_input.tsx | 344 ++---------------- x-pack/plugins/canvas/public/plugin.tsx | 5 + .../canvas/storybook/storyshots.test.tsx | 5 + .../translations/translations/ja-JP.json | 12 +- .../translations/translations/zh-CN.json | 12 +- 32 files changed, 663 insertions(+), 657 deletions(-) rename {x-pack/plugins/canvas/common/lib => src/plugins/presentation_util/public/components/expression_input}/autocomplete.ts (98%) create mode 100644 src/plugins/presentation_util/public/components/expression_input/constants.ts create mode 100644 src/plugins/presentation_util/public/components/expression_input/expression_input.stories.tsx create mode 100644 src/plugins/presentation_util/public/components/expression_input/expression_input.tsx create mode 100644 src/plugins/presentation_util/public/components/expression_input/index.tsx rename x-pack/plugins/canvas/public/lib/monaco_language_def.ts => src/plugins/presentation_util/public/components/expression_input/language.ts (74%) create mode 100644 src/plugins/presentation_util/public/components/expression_input/providers.ts rename {x-pack/plugins/canvas => src/plugins/presentation_util}/public/components/expression_input/reference.ts (79%) delete mode 100644 x-pack/plugins/canvas/common/lib/autocomplete.test.ts diff --git a/src/plugins/presentation_util/common/index.ts b/src/plugins/presentation_util/common/index.ts index 4510a0aac5a0..a84a78c823a5 100644 --- a/src/plugins/presentation_util/common/index.ts +++ b/src/plugins/presentation_util/common/index.ts @@ -12,4 +12,10 @@ export const PLUGIN_ID = 'presentationUtil'; export const PLUGIN_NAME = 'presentationUtil'; +/** + * The unique identifier for the Expressions Language for use in the ExpressionInput + * and CodeEditor components. + */ +export const EXPRESSIONS_LANGUAGE_ID = 'kibana-expressions'; + export * from './labs'; diff --git a/src/plugins/presentation_util/kibana.json b/src/plugins/presentation_util/kibana.json index 210937b335e5..32460a845515 100644 --- a/src/plugins/presentation_util/kibana.json +++ b/src/plugins/presentation_util/kibana.json @@ -9,7 +9,16 @@ "kibanaVersion": "kibana", "server": true, "ui": true, - "extraPublicDirs": ["common/lib"], - "requiredPlugins": ["savedObjects", "data", "dataViews", "embeddable", "kibanaReact"], + "extraPublicDirs": [ + "common/lib" + ], + "requiredPlugins": [ + "savedObjects", + "data", + "dataViews", + "embeddable", + "kibanaReact", + "expressions" + ], "optionalPlugins": [] } diff --git a/x-pack/plugins/canvas/common/lib/autocomplete.ts b/src/plugins/presentation_util/public/components/expression_input/autocomplete.ts similarity index 98% rename from x-pack/plugins/canvas/common/lib/autocomplete.ts rename to src/plugins/presentation_util/public/components/expression_input/autocomplete.ts index 88fb6b052b95..5f0c9cab6215 100644 --- a/x-pack/plugins/canvas/common/lib/autocomplete.ts +++ b/src/plugins/presentation_util/public/components/expression_input/autocomplete.ts @@ -1,8 +1,9 @@ /* * 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. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import { uniq } from 'lodash'; @@ -15,9 +16,9 @@ import { ExpressionFunction, ExpressionFunctionParameter, getByAlias, -} from '../../../../../src/plugins/expressions/common'; +} from '../../../../expressions/common'; -const MARKER = 'CANVAS_SUGGESTION_MARKER'; +const MARKER = 'EXPRESSIONS_SUGGESTION_MARKER'; interface BaseSuggestion { text: string; @@ -25,11 +26,6 @@ interface BaseSuggestion { end: number; } -export interface FunctionSuggestion extends BaseSuggestion { - type: 'function'; - fnDef: ExpressionFunction; -} - interface ArgSuggestionValue extends Omit { name: string; } @@ -43,8 +39,6 @@ interface ValueSuggestion extends BaseSuggestion { type: 'value'; } -export type AutocompleteSuggestion = FunctionSuggestion | ArgSuggestion | ValueSuggestion; - interface FnArgAtPosition { ast: ExpressionASTWithMeta; fnIndex: number; @@ -57,6 +51,7 @@ interface FnArgAtPosition { // If this function is a sub-expression function, we need the parent function and argument // name to determine the return type of the function parentFn?: string; + // If this function is a sub-expression function, the context could either be local or it // could be the parent's previous function. contextFn?: string | null; @@ -101,6 +96,13 @@ type ExpressionASTWithMeta = ASTMetaInformation< > >; +export interface FunctionSuggestion extends BaseSuggestion { + type: 'function'; + fnDef: ExpressionFunction; +} + +export type AutocompleteSuggestion = FunctionSuggestion | ArgSuggestion | ValueSuggestion; + // Typeguard for checking if ExpressionArg is a new expression function isExpression( maybeExpression: ExpressionArgASTWithMeta diff --git a/src/plugins/presentation_util/public/components/expression_input/constants.ts b/src/plugins/presentation_util/public/components/expression_input/constants.ts new file mode 100644 index 000000000000..f937d55cbf9b --- /dev/null +++ b/src/plugins/presentation_util/public/components/expression_input/constants.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { CodeEditorProps } from '../../../../kibana_react/public'; + +export const LANGUAGE_CONFIGURATION = { + autoClosingPairs: [ + { + open: '{', + close: '}', + }, + ], +}; + +export const CODE_EDITOR_OPTIONS: CodeEditorProps['options'] = { + scrollBeyondLastLine: false, + quickSuggestions: true, + minimap: { + enabled: false, + }, + wordWrap: 'on', + wrappingIndent: 'indent', +}; diff --git a/src/plugins/presentation_util/public/components/expression_input/expression_input.stories.tsx b/src/plugins/presentation_util/public/components/expression_input/expression_input.stories.tsx new file mode 100644 index 000000000000..648171959791 --- /dev/null +++ b/src/plugins/presentation_util/public/components/expression_input/expression_input.stories.tsx @@ -0,0 +1,94 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { action } from '@storybook/addon-actions'; +import { Meta } from '@storybook/react'; + +import { ExpressionFunction, ExpressionFunctionParameter, Style } from 'src/plugins/expressions'; +import { ExpressionInput } from '../expression_input'; +import { registerExpressionsLanguage } from './language'; + +const content: ExpressionFunctionParameter<'string'> = { + name: 'content', + required: false, + help: 'A string of text that contains Markdown. To concatenate, pass the `string` function multiple times.', + types: ['string'], + default: '', + aliases: ['_', 'expression'], + multi: true, + resolve: false, + options: [], + accepts: () => true, +}; + +const font: ExpressionFunctionParameter