From bfa8b3c9b6f3e7d58335cbfe991de519e2f087ec Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Fri, 3 Dec 2021 13:34:00 +0200 Subject: [PATCH 01/31] [Canvas] Added KibanaThemeProvider to expression_repeat_image. (#120089) * Added kibanaThemeProvider to expression_repeat_image. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../expression_repeat_image/kibana.json | 2 +- .../components/repeat_image_component.tsx | 4 +- .../repeat_image_renderer.stories.tsx | 4 +- .../public/expression_renderers/index.ts | 6 +- .../repeat_image_renderer.tsx | 74 +++++++++++-------- .../expression_repeat_image/public/index.ts | 5 +- .../expression_repeat_image/public/plugin.ts | 4 +- .../canvas_plugin_src/renderers/external.ts | 4 +- .../shareable_runtime/supported_renderers.js | 4 +- 9 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/plugins/expression_repeat_image/kibana.json b/src/plugins/expression_repeat_image/kibana.json index 5694e0160042ca..0df2eb9842312a 100755 --- a/src/plugins/expression_repeat_image/kibana.json +++ b/src/plugins/expression_repeat_image/kibana.json @@ -11,5 +11,5 @@ "ui": true, "requiredPlugins": ["expressions", "presentationUtil"], "optionalPlugins": [], - "requiredBundles": [] + "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/expression_repeat_image/public/components/repeat_image_component.tsx b/src/plugins/expression_repeat_image/public/components/repeat_image_component.tsx index 7a136b470e9439..7da6735c6ce861 100644 --- a/src/plugins/expression_repeat_image/public/components/repeat_image_component.tsx +++ b/src/plugins/expression_repeat_image/public/components/repeat_image_component.tsx @@ -46,7 +46,9 @@ function setImageSize(img: HTMLImageElement, size: number) { } function createImageJSX(img: HTMLImageElement | null) { - if (!img) return null; + if (!img) { + return null; + } const params = img.width > img.height ? { heigth: img.height } : { width: img.width }; return ; } diff --git a/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx b/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx index 42f008b2570ea9..c727ca9562fadc 100644 --- a/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx +++ b/src/plugins/expression_repeat_image/public/expression_renderers/__stories__/repeat_image_renderer.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { Render } from '../../../../presentation_util/public/__stories__'; -import { repeatImageRenderer } from '../repeat_image_renderer'; +import { getRepeatImageRenderer } from '../repeat_image_renderer'; import { getElasticLogo, getElasticOutline, @@ -31,7 +31,7 @@ const Renderer = ({ emptyImage: elasticOutline, }; - return ; + return ; }; storiesOf('enderers/repeatImage', module).add( diff --git a/src/plugins/expression_repeat_image/public/expression_renderers/index.ts b/src/plugins/expression_repeat_image/public/expression_renderers/index.ts index 5c5625f8c7730a..eb161e6e0f2a39 100644 --- a/src/plugins/expression_repeat_image/public/expression_renderers/index.ts +++ b/src/plugins/expression_repeat_image/public/expression_renderers/index.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -import { repeatImageRenderer } from './repeat_image_renderer'; - -export const renderers = [repeatImageRenderer]; - -export { repeatImageRenderer }; +export { getRepeatImageRenderer, repeatImageRendererFactory } from './repeat_image_renderer'; diff --git a/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx b/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx index 330bf16e038fa0..5e5bc04f317d24 100644 --- a/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx +++ b/src/plugins/expression_repeat_image/public/expression_renderers/repeat_image_renderer.tsx @@ -7,10 +7,19 @@ */ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nProvider } from '@kbn/i18n-react'; +import { Observable } from 'rxjs'; +import { CoreTheme } from 'kibana/public'; import { ExpressionRenderDefinition, IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { i18n } from '@kbn/i18n'; -import { getElasticOutline, isValidUrl, withSuspense } from '../../../presentation_util/public'; +import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; +import { CoreSetup } from '../../../../core/public'; +import { + defaultTheme$, + getElasticOutline, + isValidUrl, + withSuspense, +} from '../../../presentation_util/public'; import { RepeatImageRendererConfig } from '../../common/types'; const strings = { @@ -27,32 +36,39 @@ const strings = { const LazyRepeatImageComponent = lazy(() => import('../components/repeat_image_component')); const RepeatImageComponent = withSuspense(LazyRepeatImageComponent, null); -export const repeatImageRenderer = (): ExpressionRenderDefinition => ({ - name: 'repeatImage', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, - render: async ( - domNode: HTMLElement, - config: RepeatImageRendererConfig, - handlers: IInterpreterRenderHandlers - ) => { - const { elasticOutline } = await getElasticOutline(); - const settings = { - ...config, - image: isValidUrl(config.image) ? config.image : elasticOutline, - emptyImage: config.emptyImage || '', - }; +export const getRepeatImageRenderer = + (theme$: Observable = defaultTheme$) => + (): ExpressionRenderDefinition => ({ + name: 'repeatImage', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, + render: async ( + domNode: HTMLElement, + config: RepeatImageRendererConfig, + handlers: IInterpreterRenderHandlers + ) => { + const { elasticOutline } = await getElasticOutline(); + const settings = { + ...config, + image: isValidUrl(config.image) ? config.image : elasticOutline, + emptyImage: config.emptyImage || '', + }; + + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); - handlers.onDestroy(() => { - unmountComponentAtNode(domNode); - }); + render( + + + + + , + domNode + ); + }, + }); - render( - - - , - domNode - ); - }, -}); +export const repeatImageRendererFactory = (core: CoreSetup) => + getRepeatImageRenderer(core.theme.theme$); diff --git a/src/plugins/expression_repeat_image/public/index.ts b/src/plugins/expression_repeat_image/public/index.ts index 21e8f449dcc701..4080ad4f1359f2 100755 --- a/src/plugins/expression_repeat_image/public/index.ts +++ b/src/plugins/expression_repeat_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 { ExpressionRepeatImagePlugin } from './plugin'; export type { ExpressionRepeatImagePluginSetup, ExpressionRepeatImagePluginStart } from './plugin'; @@ -17,4 +14,4 @@ export function plugin() { return new ExpressionRepeatImagePlugin(); } -export * from './expression_renderers'; +export { getRepeatImageRenderer, repeatImageRendererFactory } from './expression_renderers'; diff --git a/src/plugins/expression_repeat_image/public/plugin.ts b/src/plugins/expression_repeat_image/public/plugin.ts index d71ce99eb1bd14..2f275f9218d504 100755 --- a/src/plugins/expression_repeat_image/public/plugin.ts +++ b/src/plugins/expression_repeat_image/public/plugin.ts @@ -9,7 +9,7 @@ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { ExpressionsStart, ExpressionsSetup } from '../../expressions/public'; import { repeatImageFunction } from '../common/expression_functions'; -import { repeatImageRenderer } from './expression_renderers'; +import { repeatImageRendererFactory } from './expression_renderers'; interface SetupDeps { expressions: ExpressionsSetup; @@ -33,7 +33,7 @@ export class ExpressionRepeatImagePlugin { public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionRepeatImagePluginSetup { expressions.registerFunction(repeatImageFunction); - expressions.registerRenderer(repeatImageRenderer); + expressions.registerRenderer(repeatImageRendererFactory(core)); } public start(core: CoreStart): ExpressionRepeatImagePluginStart {} 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 f97ac6e5385756..9e2a51065eb6c6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts @@ -11,7 +11,7 @@ import { errorRendererFactory, debugRendererFactory, } from '../../../../../src/plugins/expression_error/public'; -import { repeatImageRenderer } from '../../../../../src/plugins/expression_repeat_image/public'; +import { repeatImageRendererFactory } from '../../../../../src/plugins/expression_repeat_image/public'; import { revealImageRenderer } from '../../../../../src/plugins/expression_reveal_image/public'; import { shapeRenderer, @@ -22,12 +22,12 @@ export const renderFunctions = [ imageRenderer, revealImageRenderer, shapeRenderer, - repeatImageRenderer, progressRenderer, ]; export const renderFunctionFactories = [ debugRendererFactory, errorRendererFactory, + 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 b71e60e5ef0be6..84150a6a9e82e8 100644 --- a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js +++ b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js @@ -15,7 +15,7 @@ import { getErrorRenderer, getDebugRenderer, } from '../../../../src/plugins/expression_error/public'; -import { repeatImageRenderer as repeatImage } from '../../../../src/plugins/expression_repeat_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, @@ -31,6 +31,7 @@ const renderFunctionsFactories = [ getTableRenderer, getErrorRenderer, getDebugRenderer, + getRepeatImageRenderer, getMetricRenderer, ]; @@ -41,7 +42,6 @@ const renderFunctionsFactories = [ */ export const renderFunctions = [ image, - repeatImage, revealImage, pie, plot, From 5bfaf51816c733df0dba4a6d61e7e8237d10ec57 Mon Sep 17 00:00:00 2001 From: Claudio Procida Date: Fri, 3 Dec 2021 13:09:47 +0100 Subject: [PATCH 02/31] [RAC] Fixes terminology and splits tests to new case (#120335) * Includes errors in the total number of rules * Fixes terminology and splits tests to new case * Removes unused vars * Review feedback --- .../containers/alerts_page/alerts_page.tsx | 11 +- .../services/observability/alerts/common.ts | 4 +- .../apps/observability/alerts/index.ts | 75 +----------- .../apps/observability/alerts/rule_stats.ts | 107 ++++++++++++++++++ .../apps/observability/helpers.ts | 12 ++ .../apps/observability/index.ts | 13 ++- 6 files changed, 133 insertions(+), 89 deletions(-) create mode 100644 x-pack/test/observability_functional/apps/observability/alerts/rule_stats.ts create mode 100644 x-pack/test/observability_functional/apps/observability/helpers.ts diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx index 06040d9a186fff..4462daa1cbf288 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/alerts_page.tsx @@ -107,15 +107,10 @@ function AlertsPage() { // Note that the API uses the semantics of 'alerts' instead of 'rules' const { alertExecutionStatus, ruleMutedStatus, ruleEnabledStatus } = response; if (alertExecutionStatus && ruleMutedStatus && ruleEnabledStatus) { - const total = Object.entries(alertExecutionStatus).reduce((acc, [key, value]) => { - if (key !== 'error') { - acc = acc + value; - } - return acc; - }, 0); - const { error } = alertExecutionStatus; - const { muted } = ruleMutedStatus; + const total = Object.values(alertExecutionStatus).reduce((acc, value) => acc + value, 0); const { disabled } = ruleEnabledStatus; + const { muted } = ruleMutedStatus; + const { error } = alertExecutionStatus; setRuleStats({ ...ruleStats, total, diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 2f888d3d733c09..270ab84f01d7ae 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -271,7 +271,7 @@ export function ObservabilityAlertsCommonProvider({ return actionsOverflowButtons[index] || null; }; - const getAlertStatValue = async (testSubj: string) => { + const getRuleStatValue = async (testSubj: string) => { const stat = await testSubjects.find(testSubj); const title = await stat.findByCssSelector('.euiStat__title'); const count = await title.getVisibleText(); @@ -317,6 +317,6 @@ export function ObservabilityAlertsCommonProvider({ viewRuleDetailsButtonClick, viewRuleDetailsLinkClick, getAlertsFlyoutViewRuleDetailsLinkOrFail, - getAlertStatValue, + getRuleStatValue, }; } diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts index 12a83f19ca2580..40c4e53ba869b2 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -7,19 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { ObjectRemover } from '../../../../functional_with_es_ssl/lib/object_remover'; -import { - createAlert, - disableAlert, - muteAlert, -} from '../../../../functional_with_es_ssl/lib/alert_api_actions'; -import { generateUniqueKey } from '../../../../functional_with_es_ssl/lib/get_test_data'; - -async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { - for (let index = 0; index < array.length; index++) { - await callback(array[index], index); - } -} +import { asyncForEach } from '../helpers'; const ACTIVE_ALERTS_CELL_COUNT = 78; const RECOVERED_ALERTS_CELL_COUNT = 120; @@ -28,8 +16,6 @@ const TOTAL_ALERTS_CELL_COUNT = 165; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const find = getService('find'); - const supertest = getService('supertest'); - const objectRemover = new ObjectRemover(supertest); describe('Observability alerts', function () { this.tags('includeFirefox'); @@ -245,65 +231,6 @@ export default ({ getService }: FtrProviderContext) => { expect(await find.existsByCssSelector('[title="Rules and Connectors"]')).to.eql(true); }); }); - - describe('Stat counters', () => { - beforeEach(async () => { - const uniqueKey = generateUniqueKey(); - - const alertToDisable = await createAlert({ - supertest, - objectRemover, - overwrites: { name: 'b', tags: [uniqueKey] }, - }); - await createAlert({ - supertest, - objectRemover, - overwrites: { name: 'c', tags: [uniqueKey] }, - }); - await createAlert({ - supertest, - objectRemover, - overwrites: { name: 'a', tags: [uniqueKey] }, - }); - await createAlert({ - supertest, - objectRemover, - overwrites: { name: 'd', tags: [uniqueKey] }, - }); - await createAlert({ - supertest, - objectRemover, - overwrites: { name: 'e', tags: [uniqueKey] }, - }); - const alertToMute = await createAlert({ - supertest, - objectRemover, - overwrites: { name: 'f', tags: [uniqueKey] }, - }); - - await disableAlert({ supertest, alertId: alertToDisable.id }); - await muteAlert({ supertest, alertId: alertToMute.id }); - - await observability.alerts.common.navigateToTimeWithData(); - }); - - afterEach(async () => { - await objectRemover.removeAll(); - }); - - it('Exist and display expected values', async () => { - const subjToValueMap: { [key: string]: number } = { - statRuleCount: 6, - statDisabled: 1, - statMuted: 1, - statErrors: 0, - }; - await asyncForEach(Object.keys(subjToValueMap), async (subject: string) => { - const value = await observability.alerts.common.getAlertStatValue(subject); - expect(value).to.be(subjToValueMap[subject]); - }); - }); - }); }); }); }; diff --git a/x-pack/test/observability_functional/apps/observability/alerts/rule_stats.ts b/x-pack/test/observability_functional/apps/observability/alerts/rule_stats.ts new file mode 100644 index 00000000000000..41ff88be079d4f --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/alerts/rule_stats.ts @@ -0,0 +1,107 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { ObjectRemover } from '../../../../functional_with_es_ssl/lib/object_remover'; +import { + createAlert as createRule, + disableAlert as disableRule, + muteAlert as muteRule, +} from '../../../../functional_with_es_ssl/lib/alert_api_actions'; +import { generateUniqueKey } from '../../../../functional_with_es_ssl/lib/get_test_data'; +import { asyncForEach } from '../helpers'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + const objectRemover = new ObjectRemover(supertest); + + describe('Observability rules', function () { + this.tags('includeFirefox'); + + const observability = getService('observability'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + const setup = async () => { + await observability.alerts.common.setKibanaTimeZoneToUTC(); + await observability.alerts.common.navigateToTimeWithData(); + }; + await setup(); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + describe('With no data', () => { + it('Shows the no data screen', async () => { + await observability.alerts.common.getNoDataPageOrFail(); + }); + }); + + describe('Stat counters', () => { + beforeEach(async () => { + const uniqueKey = generateUniqueKey(); + + const ruleToDisable = await createRule({ + supertest, + objectRemover, + overwrites: { name: 'b', tags: [uniqueKey] }, + }); + await createRule({ + supertest, + objectRemover, + overwrites: { name: 'c', tags: [uniqueKey] }, + }); + await createRule({ + supertest, + objectRemover, + overwrites: { name: 'a', tags: [uniqueKey] }, + }); + await createRule({ + supertest, + objectRemover, + overwrites: { name: 'd', tags: [uniqueKey] }, + }); + await createRule({ + supertest, + objectRemover, + overwrites: { name: 'e', tags: [uniqueKey] }, + }); + const ruleToMute = await createRule({ + supertest, + objectRemover, + overwrites: { name: 'f', tags: [uniqueKey] }, + }); + + await disableRule({ supertest, alertId: ruleToDisable.id }); + await muteRule({ supertest, alertId: ruleToMute.id }); + + await observability.alerts.common.navigateToTimeWithData(); + }); + + afterEach(async () => { + await objectRemover.removeAll(); + }); + + it('Exist and display expected values', async () => { + const subjToValueMap: { [key: string]: number } = { + statRuleCount: 6, + statDisabled: 1, + statMuted: 1, + statErrors: 0, + }; + await asyncForEach(Object.keys(subjToValueMap), async (subject: string) => { + const value = await observability.alerts.common.getRuleStatValue(subject); + expect(value).to.be(subjToValueMap[subject]); + }); + }); + }); + }); +}; diff --git a/x-pack/test/observability_functional/apps/observability/helpers.ts b/x-pack/test/observability_functional/apps/observability/helpers.ts new file mode 100644 index 00000000000000..72a91881ab97aa --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/helpers.ts @@ -0,0 +1,12 @@ +/* + * 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 async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index); + } +} diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index bc78fa138c39b2..de681d5c17f2c2 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -10,15 +10,18 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('ObservabilityApp', function () { this.tags('ciGroup6'); - loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./exploratory_view')); + loadTestFile(require.resolve('./alerts')); + loadTestFile(require.resolve('./alerts/add_to_case')); loadTestFile(require.resolve('./alerts/alert_disclaimer')); - loadTestFile(require.resolve('./alerts/workflow_status')); + loadTestFile(require.resolve('./alerts/bulk_actions')); loadTestFile(require.resolve('./alerts/pagination')); - loadTestFile(require.resolve('./alerts/add_to_case')); + loadTestFile(require.resolve('./alerts/rule_stats')); loadTestFile(require.resolve('./alerts/state_synchronization')); - loadTestFile(require.resolve('./alerts/bulk_actions')); loadTestFile(require.resolve('./alerts/table_storage')); + loadTestFile(require.resolve('./alerts/workflow_status')); + + loadTestFile(require.resolve('./exploratory_view')); + loadTestFile(require.resolve('./feature_controls')); }); } From 4436b267b74193e397bc7d3907638248af03b6b4 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 3 Dec 2021 14:31:43 +0100 Subject: [PATCH 03/31] Fix globalsearchg functional tests (#120333) --- .../integration_tests/get_searchable_types.test.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts b/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts index b8224d9a30d086..e3b237bffaf952 100644 --- a/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts +++ b/x-pack/plugins/global_search/server/routes/integration_tests/get_searchable_types.test.ts @@ -14,8 +14,7 @@ import { registerInternalSearchableTypesRoute } from '../get_searchable_types'; type SetupServerReturn = UnwrapPromise>; const pluginId = Symbol('globalSearch'); -// FAILING: https://github.com/elastic/kibana/issues/120268 -describe.skip('GET /internal/global_search/searchable_types', () => { +describe('GET /internal/global_search/searchable_types', () => { let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; let globalSearchHandlerContext: ReturnType< @@ -47,7 +46,7 @@ describe.skip('GET /internal/global_search/searchable_types', () => { it('calls the handler context with correct parameters', async () => { await supertest(httpSetup.server.listener) - .post('/internal/global_search/searchable_types') + .get('/internal/global_search/searchable_types') .expect(200); expect(globalSearchHandlerContext.getSearchableTypes).toHaveBeenCalledTimes(1); @@ -57,7 +56,7 @@ describe.skip('GET /internal/global_search/searchable_types', () => { globalSearchHandlerContext.getSearchableTypes.mockResolvedValue(['type-a', 'type-b']); const response = await supertest(httpSetup.server.listener) - .post('/internal/global_search/searchable_types') + .get('/internal/global_search/searchable_types') .expect(200); expect(response.body).toEqual({ @@ -69,8 +68,8 @@ describe.skip('GET /internal/global_search/searchable_types', () => { globalSearchHandlerContext.getSearchableTypes.mockRejectedValue(new Error()); const response = await supertest(httpSetup.server.listener) - .post('/internal/global_search/searchable_types') - .expect(200); + .get('/internal/global_search/searchable_types') + .expect(500); expect(response.body).toEqual( expect.objectContaining({ From 265c8dcd434369dbc47fe844a29855ce79e2e398 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Fri, 3 Dec 2021 08:33:09 -0500 Subject: [PATCH 04/31] [Fleet] Configure fleet default output on prem with ES host and CA fingerprint (#120276) --- .../server/kibana_config_writer.test.ts | 27 ++++++++++ .../server/kibana_config_writer.ts | 52 ++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts index 005e280fcc7447..82d02882698d47 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts @@ -7,6 +7,7 @@ */ jest.mock('fs/promises'); +jest.mock('crypto'); import { constants } from 'fs'; import { loggingSystemMock } from 'src/core/server/mocks'; @@ -28,6 +29,16 @@ describe('KibanaConfigWriter', () => { mockReadFile.mockResolvedValue(''); + const mockCrypto = jest.requireMock('crypto'); + mockCrypto.X509Certificate = function (cert: string) { + if (cert === 'invalid-cert') { + throw new Error('Invalid certificate'); + } + return { + fingerprint256: 'fingerprint256', + }; + }; + kibanaConfigWriter = new KibanaConfigWriter( '/some/path/kibana.yml', '/data', @@ -120,6 +131,7 @@ describe('KibanaConfigWriter', () => { elasticsearch.hosts: [some-host] elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] + xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_sha256: fingerprint256}] ", ], @@ -186,6 +198,7 @@ describe('KibanaConfigWriter', () => { elasticsearch.username: username elasticsearch.password: password elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] + xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_sha256: fingerprint256}] ", ], @@ -193,6 +206,18 @@ describe('KibanaConfigWriter', () => { `); }); + it('throws if it cannot parse CA certificate', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'invalid-cert', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Invalid certificate]`); + + expect(mockWriteFile).not.toHaveBeenCalled(); + }); + it('can successfully write elasticsearch config without CA certificate', async () => { await expect( kibanaConfigWriter.writeConfig({ @@ -250,6 +275,7 @@ describe('KibanaConfigWriter', () => { elasticsearch.hosts: [some-host] elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] + xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_sha256: fingerprint256}] ", ], @@ -303,6 +329,7 @@ describe('KibanaConfigWriter', () => { monitoring.ui.container.elasticsearch.enabled: true elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/data/ca_1234.crt] + xpack.fleet.outputs: [{id: fleet-default-output, name: default, is_default: true, is_default_monitoring: true, type: elasticsearch, hosts: [some-host], ca_sha256: fingerprint256}] ", ], diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.ts b/src/plugins/interactive_setup/server/kibana_config_writer.ts index 949bc25ddd2538..af177fee33bcea 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { X509Certificate } from 'crypto'; import { constants } from 'fs'; import fs from 'fs/promises'; import yaml from 'js-yaml'; @@ -30,6 +31,16 @@ export type WriteConfigParameters = { | {} ); +interface FleetOutputConfig { + id: string; + name: string; + is_default: boolean; + is_default_monitoring: boolean; + type: 'elasticsearch'; + hosts: string[]; + ca_sha256: string; +} + export class KibanaConfigWriter { constructor( private readonly configPath: string, @@ -61,7 +72,9 @@ export class KibanaConfigWriter { */ public async writeConfig(params: WriteConfigParameters) { const caPath = path.join(this.dataDirectoryPath, `ca_${Date.now()}.crt`); - const config: Record = { 'elasticsearch.hosts': [params.host] }; + const config: Record = { + 'elasticsearch.hosts': [params.host], + }; if ('serviceAccountToken' in params && params.serviceAccountToken) { config['elasticsearch.serviceAccountToken'] = params.serviceAccountToken.value; } else if ('username' in params && params.username) { @@ -72,6 +85,21 @@ export class KibanaConfigWriter { config['elasticsearch.ssl.certificateAuthorities'] = [caPath]; } + // If a certificate is passed configure Fleet default output + if (params.caCert) { + try { + config['xpack.fleet.outputs'] = KibanaConfigWriter.getFleetDefaultOutputConfig( + params.caCert, + params.host + ); + } catch (err) { + this.logger.error( + `Failed to generate Fleet default output: ${getDetailedErrorMessage(err)}.` + ); + throw err; + } + } + // Load and parse existing configuration file to check if it already has values for the config // entries we want to write. const existingConfig = await this.loadAndParseKibanaConfig(); @@ -152,6 +180,28 @@ export class KibanaConfigWriter { return { raw: rawConfig, parsed: parsedConfig }; } + /** + * Build config for Fleet outputs + * @param caCert + * @param host + */ + private static getFleetDefaultOutputConfig(caCert: string, host: string): FleetOutputConfig[] { + const cert = new X509Certificate(caCert); + const certFingerprint = cert.fingerprint256; + + return [ + { + id: 'fleet-default-output', + name: 'default', + is_default: true, + is_default_monitoring: true, + type: 'elasticsearch', + hosts: [host], + ca_sha256: certFingerprint, + }, + ]; + } + /** * Comments out all non-commented entries in the Kibana configuration file. * @param rawConfig Content of the Kibana configuration file. From 89b1f9056c29ac9c35e29fde14d5a7e27b7d731f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:42:54 -0500 Subject: [PATCH 05/31] Prevent endless loop for saved object migrations (#120146) --- dev_docs/tutorials/saved_objects.mdx | 2 + .../core/saved-objects-service.asciidoc | 3 ++ .../migrations/core/document_migrator.test.ts | 45 +++---------------- .../migrations/core/document_migrator.ts | 16 +------ .../service/lib/repository.test.ts | 23 ++++++++-- .../saved_objects/service/lib/repository.ts | 3 ++ 6 files changed, 35 insertions(+), 57 deletions(-) diff --git a/dev_docs/tutorials/saved_objects.mdx b/dev_docs/tutorials/saved_objects.mdx index 9583e195d1c828..a9d8cd7c6ec1c4 100644 --- a/dev_docs/tutorials/saved_objects.mdx +++ b/dev_docs/tutorials/saved_objects.mdx @@ -252,6 +252,8 @@ Having said that, if a document is encountered that is not in the expected shape fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. When such a scenario is encountered, the error should be verbose and informative so that the corrupt document can be corrected, if possible. +**WARNING:** Do not attempt to change the `migrationVersion`, `id`, or `type` fields within a migration function, this is not supported. + ### Testing Migrations Bugs in a migration function cause downtime for our users and therefore have a very high impact. Follow the . diff --git a/docs/developer/architecture/core/saved-objects-service.asciidoc b/docs/developer/architecture/core/saved-objects-service.asciidoc index a7ce86ea463593..54a5c319c62220 100644 --- a/docs/developer/architecture/core/saved-objects-service.asciidoc +++ b/docs/developer/architecture/core/saved-objects-service.asciidoc @@ -259,6 +259,9 @@ upgrade. In most scenarios, it is better to fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time. +WARNING: Do not attempt to change the `migrationVersion`, `id`, or `type` fields +within a migration function, this is not supported. + It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input documents. Given how simple it is to test all the branch conditions in a migration function and the high impact of a bug diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 64c1c4ce2fa9fb..f92d505c058edd 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -664,39 +664,6 @@ describe('DocumentMigrator', () => { ); }); - it('allows updating a migrationVersion prop to a later version', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'cat', - migrations: { - '1.0.0': setAttr('migrationVersion.cat', '2.9.1'), - '2.0.0': () => { - throw new Error('POW!'); - }, - '2.9.1': () => { - throw new Error('BANG!'); - }, - '3.0.0': setAttr('attributes.name', 'Shiny'), - }, - }), - }); - migrator.prepareMigrations(); - const actual = migrator.migrate({ - id: 'smelly', - type: 'cat', - attributes: { name: 'Boo' }, - migrationVersion: { cat: '0.5.6' }, - }); - expect(actual).toEqual({ - id: 'smelly', - type: 'cat', - attributes: { name: 'Shiny' }, - migrationVersion: { cat: '3.0.0' }, - coreMigrationVersion: kibanaVersion, - }); - }); - it('allows adding props to migrationVersion', () => { const migrator = new DocumentMigrator({ ...testOpts(), @@ -1072,7 +1039,8 @@ describe('DocumentMigrator', () => { name: 'dog', namespaceType: 'single', migrations: { - '1.0.0': setAttr('migrationVersion.dog', '2.0.0'), + '1.1.0': setAttr('attributes.age', '12'), + '1.5.0': setAttr('attributes.color', 'tri-color'), '2.0.0': (doc) => doc, // noop }, }, @@ -1083,9 +1051,10 @@ describe('DocumentMigrator', () => { const obj = { id: 'sleepy', type: 'dog', - attributes: { name: 'Patches' }, - migrationVersion: {}, + attributes: { name: 'Patches', age: '11' }, + migrationVersion: { dog: '1.1.0' }, // skip the first migration transform, only apply the second and third references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], + coreMigrationVersion: undefined, // this is intentional }; it('in the default space', () => { @@ -1095,7 +1064,7 @@ describe('DocumentMigrator', () => { { id: 'sleepy', type: 'dog', - attributes: { name: 'Patches' }, + attributes: { name: 'Patches', age: '11', color: 'tri-color' }, migrationVersion: { dog: '2.0.0' }, references: [{ id: 'favorite', type: 'toy', name: 'BALL!' }], // no change coreMigrationVersion: kibanaVersion, @@ -1111,7 +1080,7 @@ describe('DocumentMigrator', () => { { id: 'sleepy', type: 'dog', - attributes: { name: 'Patches' }, + attributes: { name: 'Patches', age: '11', color: 'tri-color' }, migrationVersion: { dog: '2.0.0' }, references: [{ id: 'uuidv5', type: 'toy', name: 'BALL!' }], // changed coreMigrationVersion: kibanaVersion, diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index da16dbc5e69e8d..5f2870fb6e244e 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -27,15 +27,7 @@ * handle property addition / deletion / renaming. * * A caveat is that this means we must restrict what a migration can do to the doc's - * migrationVersion itself. We allow only these kinds of changes: - * - * - Add a new property to migrationVersion - * - Move a migrationVersion property forward to a later version - * - * Migrations *cannot* move a migrationVersion property backwards (e.g. from 2.0.0 to 1.0.0), and they - * cannot clear a migrationVersion property, as allowing either of these could produce infinite loops. - * However, we do wish to allow migrations to modify migrationVersion if they wish, so that - * they could transform a type from "foo 1.0.0" to "bar 3.0.0". + * migrationVersion itself. Migrations should *not* make any changes to the migrationVersion property. * * One last gotcha is that any docs which have no migrationVersion are assumed to be up-to-date. * This is because Kibana UI and other clients really can't be expected build the migrationVersion @@ -753,12 +745,6 @@ function migrateProp( let additionalDocs: SavedObjectUnsanitizedDoc[] = []; for (const { version, transform, transformType } of applicableTransforms(migrations, doc, prop)) { - const currentVersion = propVersion(doc, prop); - if (currentVersion && Semver.gt(currentVersion, version)) { - // the previous transform function increased the object's migrationVersion; break out of the loop - break; - } - if (convertNamespaceTypes || (transformType !== 'convert' && transformType !== 'reference')) { // migrate transforms are always applied, but conversion transforms and reference transforms are only applied during index migrations const result = transform(doc); diff --git a/src/core/server/saved_objects/service/lib/repository.test.ts b/src/core/server/saved_objects/service/lib/repository.test.ts index 46a532cdefef4c..ab692b146e7f67 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.ts +++ b/src/core/server/saved_objects/service/lib/repository.test.ts @@ -976,8 +976,9 @@ describe('SavedObjectsRepository', () => { describe('migration', () => { it(`migrates the docs and serializes the migrated docs`, async () => { migrator.migrateDocument.mockImplementation(mockMigrateDocument); - await bulkCreateSuccess([obj1, obj2]); - const docs = [obj1, obj2].map((x) => ({ ...x, ...mockTimestampFields })); + const modifiedObj1 = { ...obj1, coreMigrationVersion: '8.0.0' }; + await bulkCreateSuccess([modifiedObj1, obj2]); + const docs = [modifiedObj1, obj2].map((x) => ({ ...x, ...mockTimestampFields })); expectMigrationArgs(docs[0], true, 1); expectMigrationArgs(docs[1], true, 2); @@ -2556,8 +2557,22 @@ describe('SavedObjectsRepository', () => { it(`migrates a document and serializes the migrated doc`, async () => { const migrationVersion = mockMigrationVersion; - await createSuccess(type, attributes, { id, references, migrationVersion }); - const doc = { type, id, attributes, references, migrationVersion, ...mockTimestampFields }; + const coreMigrationVersion = '8.0.0'; + await createSuccess(type, attributes, { + id, + references, + migrationVersion, + coreMigrationVersion, + }); + const doc = { + type, + id, + attributes, + references, + migrationVersion, + coreMigrationVersion, + ...mockTimestampFields, + }; expectMigrationArgs(doc); const migratedDoc = migrator.migrateDocument(doc); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 9be58f1b718613..0d175250160435 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -305,6 +305,7 @@ export class SavedObjectsRepository { const { id = SavedObjectsUtils.generateId(), migrationVersion, + coreMigrationVersion, overwrite = false, references = [], refresh = DEFAULT_REFRESH_SETTING, @@ -359,6 +360,7 @@ export class SavedObjectsRepository { originId, attributes, migrationVersion, + coreMigrationVersion, updated_at: time, ...(Array.isArray(references) && { references }), }); @@ -523,6 +525,7 @@ export class SavedObjectsRepository { type: object.type, attributes: object.attributes, migrationVersion: object.migrationVersion, + coreMigrationVersion: object.coreMigrationVersion, ...(savedObjectNamespace && { namespace: savedObjectNamespace }), ...(savedObjectNamespaces && { namespaces: savedObjectNamespaces }), updated_at: time, From 6640357eb6e2a4b5db39e468dc315bce74109259 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Fri, 3 Dec 2021 14:54:44 +0100 Subject: [PATCH 06/31] [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 b33541c5057d8e..7a88b065d8701f 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 26bf4ce6740a98..a6e7eef88724bd 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 00cbdb941c11be..340093995b297c 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 095401ff314226..75ff13b66b29cf 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 1945b7e3ce3e7f..bc335ff6680ee1 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 45d26d9269f6e3..e2adaaae35547d 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 9d404abcf2223c..9a827b137ae78d 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 ed67fdb1c96f67..00a225635fb8b5 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 f6c0fb6f3837f4..1b8836fc2438df 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 2697e4a571ad89..36f386e49c5c7d 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 db83d9e1bcfe52..fc36a0c4337cf7 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 5a40c79d6e5ecd..a339676ac361f0 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 8f624dabd64d16..00000000000000 --- 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 fa7ac50c087658..00000000000000 --- 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 a50e3e91ab9e52..00000000000000 --- 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 f78451e205b1ea..00000000000000 --- 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 dfd9c6c9a7fcd4..71d6d5eb0c5831 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 5348c12fb6c8ef..c89199c2cb0c58 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 1d02acaf65f486..c4cf876cbdc7de 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 189f230c02c8de..3697d27015fdc8 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 4a64462b27ad5a..e112942b097492 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 00000000000000..de710c2f1b17cb --- /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 00000000000000..ad737ac410e3b1 --- /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 9ac61cc9487ee0..00000000000000 --- 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 a546d20e495838..b1310e363eef03 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 00000000000000..5686be269121a3 --- /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 c8076ab6a4484e..00000000000000 --- 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 0dc0e8a3fe1f28..00000000000000 --- 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 a1bf4d9d35f65c..00000000000000 --- 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 00000000000000..865af2266f2e0a --- /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 00000000000000..24bdc191b3d66d --- /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 2539490be16fb5..b38072464c6530 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 67ee6c55ac06f8..1df49fed073589 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 5857a0417239c8..e43af97e84af05 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 00000000000000..0951503b04cd41 --- /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 00000000000000..832006930a326f --- /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 00000000000000..08463146a683ee --- /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 76d3f07facf05b..58d04788e98eb3 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 01997e32f243e6..da71c1796066f6 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 a2e0c2d2921dc8..ec5e2aae6e2e2d 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 8840cd4bee0dd8..bc5f6e3db91690 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 07/31] [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 e9cbda1f487cbe..1630d63aee5c67 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 40997de054c7f4..089bbd4bcb4e8b 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 08/31] :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 539d69207f5f92..3b9fdaf094822e 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 09/31] 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 7a88b065d8701f..b33541c5057d8e 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 a6e7eef88724bd..26bf4ce6740a98 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 340093995b297c..00cbdb941c11be 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 75ff13b66b29cf..095401ff314226 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 bc335ff6680ee1..1945b7e3ce3e7f 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 e2adaaae35547d..45d26d9269f6e3 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 9a827b137ae78d..9d404abcf2223c 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 00a225635fb8b5..ed67fdb1c96f67 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 1b8836fc2438df..f6c0fb6f3837f4 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 36f386e49c5c7d..2697e4a571ad89 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 fc36a0c4337cf7..db83d9e1bcfe52 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 a339676ac361f0..5a40c79d6e5ecd 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 00000000000000..8f624dabd64d16 --- /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 00000000000000..fa7ac50c087658 --- /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 00000000000000..a50e3e91ab9e52 --- /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 00000000000000..f78451e205b1ea --- /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 71d6d5eb0c5831..dfd9c6c9a7fcd4 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 c89199c2cb0c58..5348c12fb6c8ef 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 c4cf876cbdc7de..1d02acaf65f486 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 3697d27015fdc8..189f230c02c8de 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 e112942b097492..4a64462b27ad5a 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 de710c2f1b17cb..00000000000000 --- 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 ad737ac410e3b1..00000000000000 --- 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 00000000000000..9ac61cc9487ee0 --- /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 b1310e363eef03..a546d20e495838 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 5686be269121a3..00000000000000 --- 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 00000000000000..c8076ab6a4484e --- /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 00000000000000..0dc0e8a3fe1f28 --- /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 00000000000000..a1bf4d9d35f65c --- /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 865af2266f2e0a..00000000000000 --- 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 24bdc191b3d66d..00000000000000 --- 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 b38072464c6530..2539490be16fb5 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 1df49fed073589..67ee6c55ac06f8 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 e43af97e84af05..5857a0417239c8 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 0951503b04cd41..00000000000000 --- 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 832006930a326f..00000000000000 --- 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 08463146a683ee..00000000000000 --- 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 58d04788e98eb3..76d3f07facf05b 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 da71c1796066f6..01997e32f243e6 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 ec5e2aae6e2e2d..a2e0c2d2921dc8 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 bc5f6e3db91690..8840cd4bee0dd8 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 10/31] 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 307d47af9ac50f..e4c80a0783b5d8 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 11/31] [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 f67a9df4a4a856..4b0d8cdc550946 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 12/31] [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 611e150323855a..46a8e2d01fc967 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 eb81ea2d6a0ac3..5b71e9b15860ee 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 90a0addfae4904..90c9181b5007a0 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 d48d80add24358..035659185955d1 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 00000000000000..588366036932f9 --- /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 00000000000000..766843311cfdcf --- /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 00000000000000..975d28eb9dcbfe --- /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 00000000000000..7f1a6f969272be --- /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 00000000000000..da0a372db8aa2a --- /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 c869c9c780bd98..1fce6f17bdea6b 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 78a340d6bbca05..6d5f81b076560b 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 00000000000000..dc77a6b9eea8d7 --- /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 adea89ce1a0511..83443dc20b9b8c 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 2851c92816ceaf..2348fdf017c866 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 d4ba29a4ef950c..4daef6cca45bdf 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 448cb215941deb..6fa0c51f500da0 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 df91314479f189..0c314ba5573c84 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 00000000000000..3a5d942d3b5328 --- /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 05ccadeaf67ace..5c681e5dbbaecc 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 528592051ccce9..9ad5abc1c7ed23 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 0447130e1bd144..32911a2c8e4ab6 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 3b987a72114114..493b41bc0165c5 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 569916ac203156..5489f7a394c993 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 a9da5c6d135a34..816aef5ca2dce5 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 3252c5a27d85dc..3a7308fef75f15 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 f39b080e56e300..3cf8e60c5e1688 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 7410dd20d92863..32568ec2b48ee3 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 3453bc529b272c..fa4d4e40b3e52f 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 2539490be16fb5..33fd1918dad595 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 9b9d72805425a0..dce08e2522beb0 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 29a4e5ce0b2996..bd72c5a4044ee1 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 02f0cb4867646a..51f88730eb6fd1 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 86bba696991951..8abe054daeaf56 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 f6c1d6b44eca62..d4adf55004389f 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 82616aa36d27e9..75686d78340702 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 13/31] 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 e1dde61bfc3ffa..b4f2c4b4bee118 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 ad2ab74098c228..545fb4dbc46060 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 fb73fe1555965b..b9db2762bce934 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 14/31] [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 d568e7224fd200..1815ab91b53165 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 15/31] [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 4b45cf645201c1..1e421fefce835d 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 59072475275850..c2dbcc1046f54a 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 16/31] [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 dad7fdfe2bc5f8..5fb13ce31247b7 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 863d8d1000f385..22dd2ef4156df7 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 433a81884f157e..959a630b08b515 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 d4dec3a8a58257..6bdd0142964199 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 66512a1126b062..736e062475e6a3 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 c5e1b5c8d916f1..17bff3f33e8ac9 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 9e2a51065eb6c6..569669032cb0b1 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 84150a6a9e82e8..01b8cc98ba5ecd 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 17/31] [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 52bd024d8116b8..81b0a71e8d9436 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 7f81e9d1051867..f573a2641b8641 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 507bfc36d47f04..bf767c7e652077 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 0f2ce2c9177384..43cca5f0c6a075 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 4d687969308bbe..044678b918fde3 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 33a98c7dbf33ce..736fd30bfbcbbe 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 31300707d147d5..60587e56bdbae1 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 d795e63bf81d1d..5229380ac55f4e 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 2945b9efed958d..4cf5407d796912 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 96d60e2f92385a..3926c017a95027 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 726855783be63f..ae102f2ed540f3 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 6dab8595663ed3..4aa0b741484382 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 89f3c05e621ec0..9932fe1625c9df 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 00000000000000..2504a2adb31aa7 --- /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 00000000000000..6ba924ff322682 --- /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 c1a6d05cc05779..730135424a4dda 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 cf3d8705380043..eded70a75e4ac9 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 d285d3a36f66ae..e104261f908478 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 00000000000000..8b27bacff8ecb7 --- /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 f6c4e3fd057cf7..8f7471f255a5d9 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 43cc69687ffb71..66c5b8da0591c6 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 009b447402f9e2..40a7276bc10d01 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 3152ac27189b3a..e7fa87435cf092 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 e266c729f26fa9..5ac95c9a91f64f 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 71a960fc1919b7..0384517bf98344 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 d094e53c59a922..00000000000000 --- 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 7c762ac3409f80..9e843c98cb649e 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 b5d4edc8cb43ba..b7e07e6f8e2748 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 9138b199fd5780..26afa65b9527cb 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 32658447acf2b1..3ddb804cac2131 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 ba10479a2bd2cf..fb748cdc63aff2 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 ddf727842780de..fcf915618a0b78 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 70f56f8f4e6d24..ec69989a8313d1 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 adbb23b921d4b9..19f31d481f58ed 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 95a2b104c7a1b4..82aab592a13443 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 82795b8bd93179..ebe0cc6a4178d8 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 17994bf7994870..f5de99d04c01c3 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 bfcd34c4ae7107..aabae1a26c1df7 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 4f336d9a8ad275..6b903214f8f976 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 f58525ea6f9741..5f308387e9d8b6 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 1cbb3e92134b82..1da2ca14bd16f3 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 962f5c4fb0d7af..f5356e5eb29a5b 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 a415d181900d7f..796d641f3eff77 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 93a9a118d23bcb..3b257fb812bf9d 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 3adaaaf091e080..e9427a996ad1ef 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 27190c9b821420..defc2cb9aa9b36 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 6d232468604233..986878e65eb8be 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 0eb6e356e1397f..cb3651e4a458ab 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 19f79da9402534..26de152f1473e3 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 18/31] 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 1e5a135b723ed7..b2299c2391886e 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 8208496f7d8000..1cf887290af77c 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 0f35aab4610788..81ee6d770103cd 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 8fa6cd3c232fad..96bf21906ed4a7 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 609fe6d00f173a..b9eae3d022439d 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 fc670fda132ca1..c4d2a31724d480 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 19/31] 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 fd7db8faadd548..bb4c27976c8573 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 20/31] [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 7fb748a305037a..f23dbf05449adb 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 fb7268ef529dad..0cf52027873926 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 c1645936c06e96..343b717dcb1aa8 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 22649a70104610..a8610bbcc8d37c 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 fcddcab378bc6b..2c5fe09d80563f 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 1acbc0c3f43bdc..c30b1654a3587e 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 2284ad5e796eec..f0e2412629bb18 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 afdcf856a872f8..de1193771dd954 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 95a67611523714..288830f4b3804e 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 882732544dcbb6..3949e715820201 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 c6f818f04fc5d2..aaab81a4c66fff 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 21/31] 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 0201c7774f8540..5d955c789ddeb0 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 22/31] [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 4510a0aac5a0b3..a84a78c823a5f3 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 210937b335e50b..32460a84551529 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 88fb6b052b9572..5f0c9cab6215c6 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 00000000000000..f937d55cbf9bb5 --- /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 00000000000000..648171959791f8 --- /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