From 6da7c00b5db79df491cea9fdb5a17f79f167a2ea Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Mon, 6 Apr 2020 11:49:34 -0400 Subject: [PATCH 01/15] Remove the action_value_click action in canvas (#62215) Co-authored-by: Elastic Machine --- .../plugins/canvas/public/application.tsx | 34 ++++++++++++++++++- x-pack/legacy/plugins/canvas/public/legacy.ts | 1 + .../legacy/plugins/canvas/public/plugin.tsx | 4 ++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/public/application.tsx b/x-pack/legacy/plugins/canvas/public/application.tsx index e26157aadebcb1c..79b3918fef99be8 100644 --- a/x-pack/legacy/plugins/canvas/public/application.tsx +++ b/x-pack/legacy/plugins/canvas/public/application.tsx @@ -26,9 +26,24 @@ import { getDocumentationLinks } from './lib/documentation_links'; import { HelpMenu } from './components/help_menu/help_menu'; import { createStore } from './store'; +import { VALUE_CLICK_TRIGGER, ActionByType } from '../../../../../src/plugins/ui_actions/public'; +/* eslint-disable */ +import { ACTION_VALUE_CLICK } from '../../../../../src/plugins/data/public/actions/value_click_action'; +/* eslint-enable */ + import { CapabilitiesStrings } from '../i18n'; const { ReadOnlyBadge: strings } = CapabilitiesStrings; +let restoreAction: ActionByType | undefined; +const emptyAction = { + id: 'empty-action', + type: '', + getDisplayName: () => 'empty action', + getIconType: () => undefined, + isCompatible: async () => true, + execute: async () => undefined, +} as ActionByType; + export const renderApp = ( coreStart: CoreStart, plugins: CanvasStartDeps, @@ -94,13 +109,30 @@ export const initializeCanvas = async ( }, }); + // TODO: We need this to disable the filtering modal from popping up in lens embeds until + // they honor the disableTriggers parameter + const action = startPlugins.uiActions.getAction(ACTION_VALUE_CLICK); + + if (action) { + restoreAction = action; + + startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, action.id); + startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, emptyAction); + } + return canvasStore; }; -export const teardownCanvas = (coreStart: CoreStart) => { +export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => { destroyRegistries(); resetInterpreter(); + startPlugins.uiActions.detachAction(VALUE_CLICK_TRIGGER, emptyAction.id); + if (restoreAction) { + startPlugins.uiActions.attachAction(VALUE_CLICK_TRIGGER, restoreAction); + restoreAction = undefined; + } + coreStart.chrome.setBadge(undefined); coreStart.chrome.setHelpExtension(undefined); }; diff --git a/x-pack/legacy/plugins/canvas/public/legacy.ts b/x-pack/legacy/plugins/canvas/public/legacy.ts index 9bccc958f726310..a6caa1985325ef1 100644 --- a/x-pack/legacy/plugins/canvas/public/legacy.ts +++ b/x-pack/legacy/plugins/canvas/public/legacy.ts @@ -27,6 +27,7 @@ const shimSetupPlugins: CanvasSetupDeps = { const shimStartPlugins: CanvasStartDeps = { ...npStart.plugins, expressions: npStart.plugins.expressions, + uiActions: npStart.plugins.uiActions, __LEGACY: { // ToDo: Copy directly into canvas absoluteToParsedUrl, diff --git a/x-pack/legacy/plugins/canvas/public/plugin.tsx b/x-pack/legacy/plugins/canvas/public/plugin.tsx index f4a3aed28a0a459..d9e5e6b4b084bff 100644 --- a/x-pack/legacy/plugins/canvas/public/plugin.tsx +++ b/x-pack/legacy/plugins/canvas/public/plugin.tsx @@ -10,6 +10,7 @@ import { HomePublicPluginSetup } from '../../../../../src/plugins/home/public'; import { initLoadingIndicator } from './lib/loading_indicator'; import { featureCatalogueEntry } from './feature_catalogue_entry'; import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; +import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; // @ts-ignore untyped local import { argTypeSpecs } from './expression_types/arg_types'; import { transitions } from './transitions'; @@ -31,6 +32,7 @@ export interface CanvasSetupDeps { export interface CanvasStartDeps { expressions: ExpressionsStart; + uiActions: UiActionsStart; __LEGACY: { absoluteToParsedUrl: (url: string, basePath: string) => any; formatMsg: any; @@ -70,7 +72,7 @@ export class CanvasPlugin return () => { unmount(); - teardownCanvas(coreStart); + teardownCanvas(coreStart, depsStart); }; }, }); From 1e92dcf1f33ddb9af5ea794a4612a0c940edee49 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 6 Apr 2020 17:55:27 +0200 Subject: [PATCH 02/15] migrate saved object, reset validation counter and fix typo attribute (#62442) --- .../core_plugins/vis_type_timeseries/index.ts | 14 - .../validation_telemetry/saved_object_type.ts | 45 ++ .../validation_telemetry_service.ts | 2 + .../visualization_migrations.test.ts | 539 ++++++++++-------- .../saved_objects/visualization_migrations.ts | 33 ++ 5 files changed, 390 insertions(+), 243 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts diff --git a/src/legacy/core_plugins/vis_type_timeseries/index.ts b/src/legacy/core_plugins/vis_type_timeseries/index.ts index 3ad8ba3a31c17ea..596fd5b581a7193 100644 --- a/src/legacy/core_plugins/vis_type_timeseries/index.ts +++ b/src/legacy/core_plugins/vis_type_timeseries/index.ts @@ -31,20 +31,6 @@ const metricsPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPlu styleSheetPaths: resolve(__dirname, 'public/index.scss'), hacks: [resolve(__dirname, 'public/legacy')], injectDefaultVars: server => ({}), - mappings: { - 'tsvb-validation-telemetry': { - properties: { - failedRequests: { - type: 'long', - }, - }, - }, - }, - savedObjectSchemas: { - 'tsvb-validation-telemetry': { - isNamespaceAgnostic: true, - }, - }, }, config(Joi: any) { return Joi.object({ diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts new file mode 100644 index 000000000000000..77b49e824334fd8 --- /dev/null +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/saved_object_type.ts @@ -0,0 +1,45 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { flow } from 'lodash'; +import { SavedObjectMigrationFn, SavedObjectsType } from 'kibana/server'; + +const resetCount: SavedObjectMigrationFn = doc => ({ + ...doc, + attributes: { + ...doc.attributes, + failedRequests: 0, + }, +}); + +export const tsvbTelemetrySavedObjectType: SavedObjectsType = { + name: 'tsvb-validation-telemetry', + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: { + failedRequests: { + type: 'long', + }, + }, + }, + migrations: { + '7.7.0': flow(resetCount), + }, +}; diff --git a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts index e49664265b8bbb6..779d9441df2fd97 100644 --- a/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts +++ b/src/plugins/vis_type_timeseries/server/validation_telemetry/validation_telemetry_service.ts @@ -19,6 +19,7 @@ import { APICaller, CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; import { UsageCollectionSetup } from '../../../usage_collection/server'; +import { tsvbTelemetrySavedObjectType } from './saved_object_type'; export interface ValidationTelemetryServiceSetup { logFailedValidation: () => void; @@ -36,6 +37,7 @@ export class ValidationTelemetryService implements Plugin { this.kibanaIndex = config.kibana.index; }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts index c7f245e59551ff8..26f8278cd3d434d 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts @@ -207,13 +207,13 @@ describe('migration visualization', () => { }, }); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{}", - }, - "references": Array [], -} -`); + Object { + "attributes": Object { + "visState": "{}", + }, + "references": Array [], + } + `); }); it('skips errors when searchSourceJSON is null', () => { @@ -231,25 +231,25 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": null, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": null, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", + } + `); }); it('skips errors when searchSourceJSON is undefined', () => { @@ -267,25 +267,25 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": undefined, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": undefined, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", + } + `); }); it('skips error when searchSourceJSON is not a string', () => { @@ -302,25 +302,25 @@ Object { }; expect(migrate(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": 123, - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": 123, + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", + } + `); }); it('skips error when searchSourceJSON is invalid json', () => { @@ -337,25 +337,25 @@ Object { }; expect(migrate(doc)).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{abc123}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{abc123}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", + } + `); }); it('skips error when "index" and "filter" is missing from searchSourceJSON', () => { @@ -373,25 +373,25 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", + } + `); }); it('extracts "index" attribute from doc', () => { @@ -409,30 +409,30 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "kibanaSavedObjectMeta.searchSourceJSON.index", - "type": "index-pattern", - }, - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.index\\"}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern", + }, + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", + } + `); }); it('extracts index patterns from the filter', () => { @@ -457,30 +457,30 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "my-index", - "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", - "type": "index-pattern", - }, - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], - "type": "visualization", -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{\\"bar\\":true,\\"filter\\":[{\\"meta\\":{\\"foo\\":true,\\"indexRefName\\":\\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\\"}}]}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "my-index", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern", + }, + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + "type": "visualization", + } + `); }); it('extracts index patterns from controls', () => { @@ -508,22 +508,22 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "foo": true, - "visState": "{\\"bar\\":false,\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"foo\\":true}]}}", - }, - "id": "1", - "references": Array [ - Object { - "id": "pattern*", - "name": "control_0_index_pattern", - "type": "index-pattern", - }, - ], - "type": "visualization", -} -`); + Object { + "attributes": Object { + "foo": true, + "visState": "{\\"bar\\":false,\\"params\\":{\\"controls\\":[{\\"bar\\":true,\\"indexPatternRefName\\":\\"control_0_index_pattern\\"},{\\"foo\\":true}]}}", + }, + "id": "1", + "references": Array [ + Object { + "id": "pattern*", + "name": "control_0_index_pattern", + "type": "index-pattern", + }, + ], + "type": "visualization", + } + `); }); it('skips extracting savedSearchId when missing', () => { @@ -539,17 +539,17 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "visState": "{}", - }, - "id": "1", - "references": Array [], -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "visState": "{}", + }, + "id": "1", + "references": Array [], + } + `); }); it('extract savedSearchId from doc', () => { @@ -566,24 +566,24 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "savedSearchRefName": "search_0", - "visState": "{}", - }, - "id": "1", - "references": Array [ - Object { - "id": "123", - "name": "search_0", - "type": "search", - }, - ], -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "savedSearchRefName": "search_0", + "visState": "{}", + }, + "id": "1", + "references": Array [ + Object { + "id": "123", + "name": "search_0", + "type": "search", + }, + ], + } + `); }); it('delete savedSearchId when empty string in doc', () => { @@ -600,17 +600,17 @@ Object { const migratedDoc = migrate(doc); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "kibanaSavedObjectMeta": Object { - "searchSourceJSON": "{}", - }, - "visState": "{}", - }, - "id": "1", - "references": Array [], -} -`); + Object { + "attributes": Object { + "kibanaSavedObjectMeta": Object { + "searchSourceJSON": "{}", + }, + "visState": "{}", + }, + "id": "1", + "references": Array [], + } + `); }); it('should return a new object if vis is table and has multiple split aggs', () => { @@ -930,12 +930,12 @@ Object { }, }); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}", - }, -} -`); + Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}", + }, + } + `); }); it('migrates type = gauge verticalSplit: false to alignment: horizontal', () => { @@ -946,12 +946,12 @@ Object { }); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}", - }, -} -`); + Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}", + }, + } + `); }); it('doesnt migrate type = gauge containing invalid visState object, adds message to log', () => { @@ -962,18 +962,18 @@ Object { }); expect(migratedDoc).toMatchInlineSnapshot(` -Object { - "attributes": Object { - "visState": "{\\"type\\":\\"gauge\\"}", - }, -} -`); + Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\"}", + }, + } + `); expect(logMsgArr).toMatchInlineSnapshot(` -Array [ - "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined", - "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}", -] -`); + Array [ + "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined", + "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}", + ] + `); }); describe('filters agg query migration', () => { @@ -1379,4 +1379,85 @@ Array [ expect(timeSeriesParams.series[0].split_filters[0].filter.language).toEqual('lucene'); }); }); + + describe('7.7.0 tsvb opperator typo migration', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.7.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + const generateDoc = (params: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: JSON.stringify({ params }), + uiStateJSON: '{}', + version: 1, + kibanaSavedObjectMeta: { + searchSourceJSON: '{}', + }, + }, + }); + + it('should remove the misspelled opperator key if it exists', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [], + gauge_color_rules: [ + { + value: 0, + id: '020e3d50-75a6-11ea-8f61-71579ff7f64d', + gauge: 'rgba(69,39,217,1)', + opperator: 'lt', + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const migratedParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(migratedParams.gauge_color_rules[0]).toMatchInlineSnapshot(` + Object { + "gauge": "rgba(69,39,217,1)", + "id": "020e3d50-75a6-11ea-8f61-71579ff7f64d", + "opperator": "lt", + "value": 0, + } + `); + }); + + it('should not change color rules with the correct spelling', () => { + const params = { + type: 'timeseries', + filter: { + query: 'bytes:>1000', + language: 'lucene', + }, + series: [], + gauge_color_rules: [ + { + value: 0, + id: '020e3d50-75a6-11ea-8f61-71579ff7f64d', + gauge: 'rgba(69,39,217,1)', + opperator: 'lt', + }, + { + value: 0, + id: '020e3d50-75a6-11ea-8f61-71579ff7f64d', + gauge: 'rgba(69,39,217,1)', + operator: 'lt', + }, + ], + }; + const timeSeriesDoc = generateDoc(params); + const migratedtimeSeriesDoc = migrate(timeSeriesDoc); + const migratedParams = JSON.parse(migratedtimeSeriesDoc.attributes.visState).params; + + expect(migratedParams.gauge_color_rules[1]).toEqual(params.gauge_color_rules[1]); + }); + }); }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts index db87006dde3eec8..80783e41863eafd 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts @@ -99,6 +99,38 @@ const migratePercentileRankAggregation: SavedObjectMigrationFn = doc => { return doc; }; +// [TSVB] Remove stale opperator key +const migrateOperatorKeyTypo: SavedObjectMigrationFn = doc => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + if (visState && visState.type === 'metrics') { + const gaugeColorRules: any[] = get(visState, 'params.gauge_color_rules') || []; + + gaugeColorRules.forEach(colorRule => { + if (colorRule.opperator) { + delete colorRule.opperator; + } + }); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } + return doc; +}; + // Migrate date histogram aggregation (remove customInterval) const migrateDateHistogramAggregation: SavedObjectMigrationFn = doc => { const visStateJSON = get(doc, 'attributes.visState'); @@ -606,4 +638,5 @@ export const visualizationSavedObjectTypeMigrations = { ), '7.3.1': flow(migrateFiltersAggQueryStringQueries), '7.4.2': flow(transformSplitFiltersStringToQueryObject), + '7.7.0': flow(migrateOperatorKeyTypo), }; From 70ae8aa6f1418beff1aeb853dce0415814d0a890 Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Mon, 6 Apr 2020 12:10:20 -0400 Subject: [PATCH 03/15] add ownFocus prop to help menu (#62492) --- src/core/public/chrome/ui/header/header_help_menu.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index 80a45b4c61f077e..1023a561a0fe331 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -314,13 +314,14 @@ class HeaderHelpMenuUI extends Component { return ( // @ts-ignore repositionOnScroll doesn't exist in EuiPopover From e86cb4208058bc68eb49b3780ea304521c4106b7 Mon Sep 17 00:00:00 2001 From: Michael Marcialis Date: Mon, 6 Apr 2020 12:15:13 -0400 Subject: [PATCH 04/15] [Core UI] Add missing alt attributes to add data tutorial logos (#62393) * add empty title prop for data tutorial page icons * switch alt attribute to title prop * update snapshots * enabling skipped a11y tests --- .../components/__snapshots__/synopsis.test.js.snap | 2 +- .../home/public/application/components/synopsis.js | 4 ++-- .../tutorial/__snapshots__/introduction.test.js.snap | 7 ++----- .../application/components/tutorial/introduction.js | 11 ++++++++--- x-pack/test/accessibility/apps/home.ts | 6 ++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap b/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap index 594d67d9c8eb01c..d757d6a8b73051b 100644 --- a/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap +++ b/src/plugins/home/public/application/components/__snapshots__/synopsis.test.js.snap @@ -9,8 +9,8 @@ exports[`props iconType 1`] = ` href="link_to_item" icon={ } diff --git a/src/plugins/home/public/application/components/synopsis.js b/src/plugins/home/public/application/components/synopsis.js index f43c377b4e5b902..b6fa85db2bfe903 100644 --- a/src/plugins/home/public/application/components/synopsis.js +++ b/src/plugins/home/public/application/components/synopsis.js @@ -35,9 +35,9 @@ export function Synopsis({ }) { let optionalImg; if (iconUrl) { - optionalImg = ; + optionalImg = ; } else if (iconType) { - optionalImg = ; + optionalImg = ; } const classes = classNames('homSynopsis__card', { diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/introduction.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/introduction.test.js.snap index b35545787e4a41b..410d29a42cac967 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/introduction.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/introduction.test.js.snap @@ -15,7 +15,6 @@ exports[`props exportedFieldsUrl 1`] = ` >

Great tutorial -  

@@ -56,6 +55,7 @@ exports[`props iconType 1`] = ` > @@ -67,7 +67,6 @@ exports[`props iconType 1`] = ` >

Great tutorial -  

@@ -97,7 +96,7 @@ exports[`props isBeta 1`] = ` >

Great tutorial -   +   @@ -130,7 +129,6 @@ exports[`props previewUrl 1`] = ` >

Great tutorial -  

@@ -169,7 +167,6 @@ exports[`render 1`] = ` >

Great tutorial -  

diff --git a/src/plugins/home/public/application/components/tutorial/introduction.js b/src/plugins/home/public/application/components/tutorial/introduction.js index bc5f30622f1a51c..c36d150c42d3db9 100644 --- a/src/plugins/home/public/application/components/tutorial/introduction.js +++ b/src/plugins/home/public/application/components/tutorial/introduction.js @@ -76,7 +76,7 @@ function IntroductionUI({ if (iconType) { icon = ( - + ); } @@ -99,8 +99,13 @@ function IntroductionUI({

- {title}   - {betaBadge} + {title} + {betaBadge && ( + <> +   + {betaBadge} + + )}

diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index f40976f09f9c883..463673d966c2e51 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -52,14 +52,12 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // issue - logo images are missing alt -text https://github.com/elastic/kibana/issues/62239 - it.skip('click on ActiveMQ logs panel to open tutorial meets a11y requirements', async () => { + it('click on ActiveMQ logs panel to open tutorial meets a11y requirements', async () => { await PageObjects.home.clickOnLogsTutorial(); await a11y.testAppSnapshot(); }); - // https://github.com/elastic/kibana/issues/62239 - it.skip('click on cloud tutorial meets a11y requirements', async () => { + it('click on cloud tutorial meets a11y requirements', async () => { await PageObjects.home.clickOnCloudTutorial(); await a11y.testAppSnapshot(); }); From 23755f95fa82e9f910b0646330fb222a142167bf Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Mon, 6 Apr 2020 11:15:52 -0500 Subject: [PATCH 05/15] [DOCS] Collapses content in Kibana and APM APIs (#62201) * Collapses content in Kibana and APM APIs * Update docs/api/role-management/put.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/copy_saved_objects.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/copy_saved_objects.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/copy_saved_objects.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/copy_saved_objects.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/copy_saved_objects.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/copy_saved_objects.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc Co-Authored-By: Lisa Cawley * Update docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc Co-Authored-By: Lisa Cawley Co-authored-by: Lisa Cawley --- docs/api/role-management/put.asciidoc | 11 +++-- .../copy_saved_objects.asciidoc | 29 +++++++++++-- ...olve_copy_saved_objects_conflicts.asciidoc | 42 ++++++++++++++++++- docs/apm/api.asciidoc | 15 +++++++ 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/docs/api/role-management/put.asciidoc b/docs/api/role-management/put.asciidoc index 59e6bc8d37eec7d..993d75780c87c6a 100644 --- a/docs/api/role-management/put.asciidoc +++ b/docs/api/role-management/put.asciidoc @@ -17,6 +17,7 @@ experimental[] Create a new {kib} role, or update the attributes of an existing To use the create or update role API, you must have the `manage_security` cluster privilege. +[role="child_attributes"] [[role-management-api-response-body]] ==== Request body @@ -29,8 +30,11 @@ To use the create or update role API, you must have the `manage_security` cluste {ref}/defining-roles.html[Defining roles]. `kibana`:: - (list) Objects that specify the <> for the role: - + (list) Objects that specify the <> for the role. ++ +.Properties of `kibana` +[%collapsible%open] +===== `base` ::: (Optional, list) A base privilege. When specified, the base must be `["all"]` or `["read"]`. When the `base` privilege is specified, you are unable to use the `feature` section. @@ -45,6 +49,7 @@ To use the create or update role API, you must have the `manage_security` cluste `spaces` ::: (list) The spaces to apply the privileges to. To grant access to all spaces, set to `["*"]`, or omit the value. +===== [[role-management-api-put-response-codes]] ==== Response code @@ -52,7 +57,7 @@ To use the create or update role API, you must have the `manage_security` cluste `204`:: Indicates a successful call. -===== Examples +==== Examples Grant access to various features in all spaces: diff --git a/docs/api/spaces-management/copy_saved_objects.asciidoc b/docs/api/spaces-management/copy_saved_objects.asciidoc index e23a137485b2d53..4822e7f624302d6 100644 --- a/docs/api/spaces-management/copy_saved_objects.asciidoc +++ b/docs/api/spaces-management/copy_saved_objects.asciidoc @@ -26,6 +26,7 @@ You can request to overwrite any objects that already exist in the target space `space_id`:: (Optional, string) The ID of the space that contains the saved objects you want to copy. When `space_id` is unspecified in the URL, the default space is used. +[role="child_attributes"] [[spaces-api-copy-saved-objects-request-body]] ==== {api-request-body-title} @@ -34,10 +35,16 @@ You can request to overwrite any objects that already exist in the target space `objects`:: (Required, object array) The saved objects to copy. ++ +.Properties of `objects` +[%collapsible%open] +===== `type`::: (Required, string) The saved object type. + `id`::: (Required, string) The saved object ID. +===== `includeReferences`:: (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects will also be copied into the target spaces. The default value is `false`. @@ -45,27 +52,43 @@ You can request to overwrite any objects that already exist in the target space `overwrite`:: (Optional, boolean) When set to `true`, all conflicts are automatically overidden. When a saved object with a matching `type` and `id` exists in the target space, that version is replaced with the version from the source space. The default value is `false`. - +[role="child_attributes"] [[spaces-api-copy-saved-objects-response-body]] ==== {api-response-body-title} ``:: (object) An object that describes the result of the copy operation for the space. Includes the dynamic keys in the response. ++ +.Properties of `` +[%collapsible%open] +===== `success`::: (boolean) The copy operation was successful. When set to `false`, some objects may have been copied. For additional information, refer to the `successCount` and `errors` properties. + `successCount`::: (number) The number of objects that successfully copied. + `errors`::: - (Optional, array) The errors that occurred during the copy operation. When errors are reported, the `success` flag is set to `false`.v + (Optional, array) The errors that occurred during the copy operation. When errors are reported, the `success` flag is set to `false`. ++ +.Properties of `errors` +[%collapsible%open] +====== `id`:::: (string) The saved object ID that failed to copy. `type`:::: (string) The type of saved object that failed to copy. `error`:::: (object) The error that caused the copy operation to fail. ++ +.Properties of `error` +[%collapsible%open] +======= `type`::::: (string) The type of error. For example, `unsupported_type`, `missing_references`, or `unknown`. Errors marked as `conflict` may be resolved by using the <>. - +======= +====== +===== [[spaces-api-copy-saved-objects-example]] ==== {api-examples-title} diff --git a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc index 8e874bb9f94e5d6..7f35dc3834f002e 100644 --- a/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc +++ b/docs/api/spaces-management/resolve_copy_saved_objects_conflicts.asciidoc @@ -25,51 +25,89 @@ Execute the <>, w `space_id`:: (Optional, string) The ID of the space that contains the saved objects you want to copy. When `space_id` is unspecified in the URL, the default space is used. The `space_id` must be the same value used during the failed <> operation. +[role="child_attributes"] [[spaces-api-resolve-copy-saved-objects-conflicts-request-body]] ==== {api-request-body-title} `objects`:: (Required, object array) The saved objects to copy. The `objects` must be the same values used during the failed <> operation. ++ +.Properties of `objects` +[%collapsible%open] +===== `type`::: (Required, string) The saved object type. + `id`::: (Required, string) The saved object ID. +===== `includeReferences`:: (Optional, boolean) When set to `true`, all saved objects related to the specified saved objects are copied into the target spaces. The `includeReferences` must be the same values used during the failed <> operation. The default value is `false`. `retries`:: (Required, object) The retry operations to attempt. Object keys represent the target space IDs. ++ +.Properties of `retries` +[%collapsible%open] +===== ``::: (Required, array) The errors to resolve for the specified ``. ++ + +.Properties of `` +[%collapsible%open] +====== `type`:::: (Required, string) The saved object type. `id`:::: (Required, string) The saved object ID. `overwrite`:::: (Required, boolean) When set to `true`, the saved object from the source space (desigated by the <>) overwrites the conflicting object in the destination space. When set to `false`, this does nothing. +====== +===== - +[role="child_attributes"] [[spaces-api-resolve-copy-saved-objects-conflicts-response-body]] ==== {api-response-body-title} ``:: (object) An object that describes the result of the copy operation for the space. Includes the dynamic keys in the response. ++ +.Properties of `` +[%collapsible%open] +===== `success`::: (boolean) The copy operation was successful. When set to `false`, some objects may have been copied. For additional information, refer to the `successCount` and `errors` properties. + `successCount`::: (number) The number of objects that successfully copied. + `errors`::: (Optional, array) The errors that occurred during the copy operation. When errors are reported, the `success` flag is set to `false`. ++ + +.Properties of `errors` +[%collapsible%open] +====== `id`:::: (string) The saved object ID that failed to copy. + `type`:::: (string) The type of saved object that failed to copy. + `error`:::: (object) The error that caused the copy operation to fail. ++ + +.Properties of `error` +[%collapsible%open] +======= `type`::::: (string) The type of error. For example, `unsupported_type`, `missing_references`, or `unknown`. - +======= +====== +===== [[spaces-api-resolve-copy-saved-objects-conflicts-example]] ==== {api-examples-title} diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index b520cc46bef8d3f..76d898ba0cb113f 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -38,17 +38,22 @@ The following Agent configuration APIs are available: `PUT /api/apm/settings/agent-configuration` +[role="child_attributes"] [[apm-update-config-req-body]] ===== Request body `service`:: (required, object) Service identifying the configuration to create or update. +.Properties of `service` +[%collapsible%open] +====== `name` ::: (required, string) Name of service `environment` ::: (optional, string) Environment of service +====== `settings`:: (required) Key/value object with settings and their corresponding value. @@ -90,16 +95,21 @@ PUT /api/apm/settings/agent-configuration `DELETE /api/apm/settings/agent-configuration` +[role="child_attributes"] [[apm-delete-config-req-body]] ===== Request body `service`:: (required, object) Service identifying the configuration to delete +.Properties of `service` +[%collapsible%open] +====== `name` ::: (required, string) Name of service `environment` ::: (optional, string) Environment of service +====== [[apm-delete-config-example]] @@ -201,17 +211,22 @@ GET /api/apm/settings/agent-configuration `POST /api/apm/settings/agent-configuration/search` +[role="child_attributes"] [[apm-search-config-req-body]] ===== Request body `service`:: (required, object) Service identifying the configuration. +.Properties of `service` +[%collapsible%open] +====== `name` ::: (required, string) Name of service `environment` ::: (optional, string) Environment of service +====== `etag`:: (required) etag is sent by the agent to indicate the etag of the last successfully applied configuration. If the etag matches an existing configuration its `applied_by_agent` property will be set to `true`. Every time a configuration is edited `applied_by_agent` is reset to `false`. From 0abd7aa43fac41f8541d5337a5f793ab2f34fb6b Mon Sep 17 00:00:00 2001 From: Lee Drengenberg Date: Mon, 6 Apr 2020 16:18:38 +0000 Subject: [PATCH 06/15] simplify new index pattern button click method (#62451) * simplify new index pattern button click method * replace method name to match previous commit Co-authored-by: Elastic Machine --- .../apps/management/_index_pattern_create_delete.js | 2 +- test/functional/page_objects/settings_page.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/test/functional/apps/management/_index_pattern_create_delete.js b/test/functional/apps/management/_index_pattern_create_delete.js index a74620b696d1bef..616e2297b2f5101 100644 --- a/test/functional/apps/management/_index_pattern_create_delete.js +++ b/test/functional/apps/management/_index_pattern_create_delete.js @@ -42,7 +42,7 @@ export default function({ getService, getPageObjects }) { describe('special character handling', () => { it('should handle special charaters in template input', async () => { - await PageObjects.settings.clickOptionalAddNewButton(); + await PageObjects.settings.clickAddNewIndexPatternButton(); await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.settings.setIndexPatternField({ indexPatternName: '❤️', diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 3f6036f58f0a914..6dcd017335c8548 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -326,7 +326,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider await PageObjects.header.waitUntilLoadingHasFinished(); await this.clickKibanaIndexPatterns(); await PageObjects.header.waitUntilLoadingHasFinished(); - await this.clickOptionalAddNewButton(); + await this.clickAddNewIndexPatternButton(); if (!isStandardIndexPattern) { await this.clickCreateNewRollupButton(); } @@ -356,11 +356,8 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider return await this.getIndexPatternIdFromUrl(); } - // adding a method to check if the create index pattern button is visible when more than 1 index pattern is present - async clickOptionalAddNewButton() { - if (await testSubjects.isDisplayed('createIndexPatternButton')) { - await testSubjects.click('createIndexPatternButton'); - } + async clickAddNewIndexPatternButton() { + await testSubjects.click('createIndexPatternButton'); } async clickCreateNewRollupButton() { From 9bab4ef746a380ca13d7ba0e611a6a30116c21aa Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 6 Apr 2020 18:36:22 +0200 Subject: [PATCH 07/15] Move ownerships of home, dev tools and discover (#62612) * move ownership * fix es-ui ownership --- .github/CODEOWNERS | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index da85fb986ae0186..3ae01b079d37c4a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,18 +15,20 @@ /src/legacy/core_plugins/metrics/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_vislib/ @elastic/kibana-app /src/legacy/core_plugins/vis_type_xy/ @elastic/kibana-app -# Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon -/src/plugins/home/public @elastic/kibana-app -/src/plugins/home/server/*.ts @elastic/kibana-app -/src/plugins/home/server/services/ @elastic/kibana-app -# Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon -/src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-app -/src/legacy/core_plugins/kibana/public/home/*.scss @elastic/kibana-app -/src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-app /src/plugins/kibana_legacy/ @elastic/kibana-app /src/plugins/timelion/ @elastic/kibana-app -/src/plugins/dev_tools/ @elastic/kibana-app /src/plugins/dashboard/ @elastic/kibana-app +/src/plugins/discover/ @elastic/kibana-app + +# Core UI +# Exclude tutorials folder for now because they are not owned by Kibana app and most will move out soon +/src/plugins/home/public @elastic/kibana-core-ui +/src/plugins/home/server/*.ts @elastic/kibana-core-ui +/src/plugins/home/server/services/ @elastic/kibana-core-ui +# Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon +/src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui +/src/legacy/core_plugins/kibana/public/home/*.scss @elastic/kibana-core-ui +/src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui # App Architecture /examples/url_generators_examples/ @elastic/kibana-app-arch @@ -175,6 +177,7 @@ **/*.scss @elastic/kibana-design # Elasticsearch UI +/src/plugins/dev_tools/ @elastic/es-ui /src/plugins/console/ @elastic/es-ui /src/plugins/es_ui_shared/ @elastic/es-ui /x-pack/legacy/plugins/cross_cluster_replication/ @elastic/es-ui From 5c8dda265636529c2ba54ed915dac88ff21fbd05 Mon Sep 17 00:00:00 2001 From: marshallmain <55718608+marshallmain@users.noreply.github.com> Date: Mon, 6 Apr 2020 12:41:49 -0400 Subject: [PATCH 08/15] [Endpoint] Add pipeline to generator that redirect alerts to alert index (#62512) * add ingest pipeline to generator script * make alert index name configurable * move pipeline name to constant * update setupOnly flag help text Co-authored-by: Elastic Machine --- .../endpoint/scripts/alert_mapping.json | 2375 +++++++++++++++++ .../{mapping.json => event_mapping.json} | 3 +- .../endpoint/scripts/resolver_generator.ts | 81 +- 3 files changed, 2445 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/endpoint/scripts/alert_mapping.json rename x-pack/plugins/endpoint/scripts/{mapping.json => event_mapping.json} (99%) diff --git a/x-pack/plugins/endpoint/scripts/alert_mapping.json b/x-pack/plugins/endpoint/scripts/alert_mapping.json new file mode 100644 index 000000000000000..a21e48b4bc95fc5 --- /dev/null +++ b/x-pack/plugins/endpoint/scripts/alert_mapping.json @@ -0,0 +1,2375 @@ +{ + "mappings": { + "_meta": { + "version": "1.5.0-dev" + }, + "date_detection": false, + "dynamic": false, + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "mutable_state": { + "properties": { + "triage_status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "endpoint": { + "properties": { + "artifact": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "policy": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "entry_modified": { + "type": "double" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "macro": { + "properties": { + "code_page": { + "type": "long" + }, + "collection": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "object" + }, + "errors": { + "properties": { + "count": { + "type": "long" + }, + "error_type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "file_extension": { + "type": "long" + }, + "project_file": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "object" + }, + "stream": { + "properties": { + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "raw_code_size": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "temp_file_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_percent": { + "type": "double" + }, + "cwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "env_variables": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "handles": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "memory_percent": { + "type": "double" + }, + "memory_region": { + "properties": { + "allocation_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocation_protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram": { + "properties": { + "histogram_array": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_flavor": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_resolution": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "length": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "permission": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_tag": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "unbacked_on_disk": { + "type": "boolean" + } + }, + "type": "nested" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "num_threads": { + "type": "long" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "phys_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "session_id": { + "type": "long" + }, + "short_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tty_device": { + "properties": { + "major_number": { + "type": "integer" + }, + "minor_number": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rule": { + "properties": { + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "ruleset": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "target": { + "properties": { + "dll": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "compile_time": { + "type": "date" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mapped_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "mapped_size": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_percent": { + "type": "double" + }, + "cwd": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "env_variables": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "handles": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "malware_classification": { + "properties": { + "features": { + "properties": { + "data": { + "properties": { + "buffer": { + "ignore_above": 1024, + "type": "keyword" + }, + "decompressed_size": { + "type": "integer" + }, + "encoding": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "identifier": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "double" + }, + "threshold": { + "type": "double" + }, + "upx_packed": { + "type": "boolean" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "memory_percent": { + "type": "double" + }, + "memory_region": { + "properties": { + "allocation_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "allocation_protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram": { + "properties": { + "histogram_array": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_flavor": { + "ignore_above": 1024, + "type": "keyword" + }, + "histogram_resolution": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "length": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "permission": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_base": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_tag": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "unbacked_on_disk": { + "type": "boolean" + } + }, + "type": "nested" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "num_threads": { + "type": "long" + }, + "parent": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "phys_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "services": { + "ignore_above": 1024, + "type": "keyword" + }, + "session_id": { + "type": "long" + }, + "short_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "call_stack": { + "properties": { + "instruction_pointer": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_section": { + "properties": { + "memory_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "memory_size": { + "ignore_above": 1024, + "type": "keyword" + }, + "protection": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "module_path": { + "ignore_above": 1024, + "type": "keyword" + }, + "rva": { + "ignore_above": 1024, + "type": "keyword" + }, + "symbol_info": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entrypoint": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "service": { + "ignore_above": 1024, + "type": "keyword" + }, + "start": { + "type": "date" + }, + "start_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "start_address_module": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + } + } + }, + "title": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "impersonation_level": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level": { + "type": "long" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_appcontainer": { + "type": "boolean" + }, + "privileges": { + "properties": { + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "sid": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tty_device": { + "properties": { + "major_number": { + "type": "integer" + }, + "minor_number": { + "type": "integer" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "uptime": { + "type": "long" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + }, + "virt_memory_bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "working_directory": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "threat": { + "properties": { + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "tactic": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "technique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": 10000 + } + }, + "refresh_interval": "5s" + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/endpoint/scripts/mapping.json b/x-pack/plugins/endpoint/scripts/event_mapping.json similarity index 99% rename from x-pack/plugins/endpoint/scripts/mapping.json rename to x-pack/plugins/endpoint/scripts/event_mapping.json index 5878e01b52a47dc..59d1ed17852b16a 100644 --- a/x-pack/plugins/endpoint/scripts/mapping.json +++ b/x-pack/plugins/endpoint/scripts/event_mapping.json @@ -2361,7 +2361,8 @@ "limit": 10000 } }, - "refresh_interval": "5s" + "refresh_interval": "5s", + "default_pipeline": "endpoint-event-pipeline" } } } \ No newline at end of file diff --git a/x-pack/plugins/endpoint/scripts/resolver_generator.ts b/x-pack/plugins/endpoint/scripts/resolver_generator.ts index aebf92eff6cb814..333846bde6ce4f7 100644 --- a/x-pack/plugins/endpoint/scripts/resolver_generator.ts +++ b/x-pack/plugins/endpoint/scripts/resolver_generator.ts @@ -8,7 +8,8 @@ import seedrandom from 'seedrandom'; import { Client, ClientOptions } from '@elastic/elasticsearch'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { EndpointDocGenerator, Event } from '../common/generate_data'; -import { default as mapping } from './mapping.json'; +import { default as eventMapping } from './event_mapping.json'; +import { default as alertMapping } from './alert_mapping.json'; main(); @@ -25,6 +26,12 @@ async function main() { default: 'http://localhost:9200', type: 'string', }, + alertIndex: { + alias: 'ai', + describe: 'index to store alerts in', + default: '.alerts-endpoint-000001', + type: 'string', + }, eventIndex: { alias: 'ei', describe: 'index to store events in', @@ -95,7 +102,16 @@ async function main() { type: 'boolean', default: false, }, + setupOnly: { + alias: 'so', + describe: + 'Run only the index and pipeline creation then exit. This is intended to be used to set up the Endpoint App for use with the real Elastic Endpoint.', + type: 'boolean', + default: false, + }, }).argv; + const pipelineName = 'endpoint-event-pipeline'; + eventMapping.settings.index.default_pipeline = pipelineName; const clientOptions: ClientOptions = { node: argv.node, }; @@ -107,7 +123,7 @@ async function main() { if (argv.delete) { try { await client.indices.delete({ - index: [argv.eventIndex, argv.metadataIndex], + index: [argv.eventIndex, argv.metadataIndex, argv.alertIndex], }); } catch (err) { if (err instanceof ResponseError && err.statusCode !== 404) { @@ -117,21 +133,42 @@ async function main() { } } } + + const pipeline = { + description: 'redirects alerts to their own index', + processors: [ + { + set: { + field: '_index', + value: argv.alertIndex, + if: "ctx.event.kind == 'alert'", + }, + }, + { + set: { + field: 'mutable_state.triage_status', + value: 'open', + }, + }, + ], + }; try { - await client.indices.create({ - index: argv.eventIndex, - body: mapping, + await client.ingest.putPipeline({ + id: pipelineName, + body: pipeline, }); } catch (err) { - if ( - err instanceof ResponseError && - err.body.error.type !== 'resource_already_exists_exception' - ) { - // eslint-disable-next-line no-console - console.log(err.body); - process.exit(1); - } + // eslint-disable-next-line no-console + console.log(err); + process.exit(1); } + + await createIndex(client, argv.alertIndex, alertMapping); + await createIndex(client, argv.eventIndex, eventMapping); + if (argv.setupOnly) { + process.exit(0); + } + let seed = argv.seed; if (!seed) { seed = Math.random().toString(); @@ -183,3 +220,21 @@ async function main() { } } } + +async function createIndex(client: Client, index: string, mapping: any) { + try { + await client.indices.create({ + index, + body: mapping, + }); + } catch (err) { + if ( + err instanceof ResponseError && + err.body.error.type !== 'resource_already_exists_exception' + ) { + // eslint-disable-next-line no-console + console.log(err.body); + process.exit(1); + } + } +} From 2cd86a4c838c81c17a7c66e5b4379b9ffa72f7fe Mon Sep 17 00:00:00 2001 From: Sonja Krause-Harder Date: Mon, 6 Apr 2020 18:48:18 +0200 Subject: [PATCH 09/15] [EPM] Refactor expandFields() (#62180) * Do not modify input array in expandFields() * Add unit tests for processFields() Co-authored-by: Elastic Machine --- .../server/services/epm/fields/field.test.ts | 100 ++++++++++++++++++ .../server/services/epm/fields/field.ts | 30 +++--- 2 files changed, 114 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts index 929f2518ee748e5..e3aef6077dbc301 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.test.ts @@ -80,3 +80,103 @@ describe('getField searches recursively for nested field in fields given an arra expect(getField(searchFields, ['2', '2-2', '2-2-1'])?.name).toBe('2-2-1'); }); }); + +describe('processFields', () => { + const flattenedFields = [ + { + name: 'a.a', + type: 'text', + }, + { + name: 'a.b', + type: 'text', + }, + ]; + const expandedFields = [ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'a', + type: 'text', + }, + { + name: 'b', + type: 'text', + }, + ], + }, + ]; + test('correctly expands flattened fields', () => { + expect(JSON.stringify(processFields(flattenedFields))).toEqual(JSON.stringify(expandedFields)); + }); + test('leaves expanded fields unchanged', () => { + expect(JSON.stringify(processFields(expandedFields))).toEqual(JSON.stringify(expandedFields)); + }); + + const mixedFieldsA = [ + { + name: 'a.a', + type: 'group', + fields: [ + { + name: 'a', + type: 'text', + }, + { + name: 'b', + type: 'text', + }, + ], + }, + ]; + + const mixedFieldsB = [ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'a.a', + type: 'text', + }, + { + name: 'a.b', + type: 'text', + }, + ], + }, + ]; + + const mixedFieldsExpanded = [ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'a', + type: 'text', + }, + { + name: 'b', + type: 'text', + }, + ], + }, + ], + }, + ]; + test('correctly expands a mix of expanded and flattened fields', () => { + expect(JSON.stringify(processFields(mixedFieldsA))).toEqual( + JSON.stringify(mixedFieldsExpanded) + ); + expect(JSON.stringify(processFields(mixedFieldsB))).toEqual( + JSON.stringify(mixedFieldsExpanded) + ); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts index 4a1a84baf6599e7..810896bb50389f1 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/fields/field.ts @@ -52,13 +52,12 @@ export type Fields = Field[]; * expandFields takes the given fields read from yaml and expands them. * There are dotted fields in the field.yml like `foo.bar`. These should * be stored as an field within a 'group' field. - * - * Note: This function modifies the passed fields array. */ -export function expandFields(fields: Fields) { +export function expandFields(fields: Fields): Fields { + const newFields: Fields = []; + fields.forEach((field, key) => { const fieldName = field.name; - // If the field name contains a dot, it means we need to // - take the first part of the name // - create a field of type 'group' with this first part @@ -71,30 +70,29 @@ export function expandFields(fields: Fields) { const groupFieldName = nameParts[0]; // Put back together the parts again for the new field name - const restFieldName = nameParts.slice(1).join('.'); + const nestedFieldName = nameParts.slice(1).join('.'); // keep all properties of the original field, but give it the shortened name - field.name = restFieldName; + const nestedField = { ...field, name: nestedFieldName }; // create a new field of type group with the original field in the fields array const groupField: Field = { name: groupFieldName, type: 'group', - fields: [field], + fields: expandFields([nestedField]), }; - // check child fields further down the tree - if (groupField.fields) { - expandFields(groupField.fields); - } // Replace the original field in the array with the new one - fields[key] = groupField; + newFields.push(groupField); } else { // even if this field doesn't have dots to expand, its child fields further down the tree might - if (field.fields) { - expandFields(field.fields); + const newField = { ...field }; + if (newField.fields) { + newField.fields = expandFields(newField.fields); } + newFields.push(newField); } }); + return newFields; } /** * dedupFields takes the given fields and merges sibling fields with the @@ -180,8 +178,8 @@ export const getField = (fields: Fields, pathNames: string[]): Field | undefined }; export function processFields(fields: Fields): Fields { - expandFields(fields); - const dedupedFields = dedupFields(fields); + const expandedFields = expandFields(fields); + const dedupedFields = dedupFields(expandedFields); return validateFields(dedupedFields, dedupedFields); } From 4bdbe7356d10d87fb7cf38b8fe529c63cb2a316f Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 6 Apr 2020 09:49:23 -0700 Subject: [PATCH 10/15] Remove ES-UI as code owner of Transform app. (#62556) --- .github/CODEOWNERS | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3ae01b079d37c4a..feaf47e45fd69ce 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -87,9 +87,8 @@ /x-pack/test/functional/apps/machine_learning/ @elastic/ml-ui /x-pack/test/functional/services/machine_learning/ @elastic/ml-ui /x-pack/test/functional/services/ml.ts @elastic/ml-ui -# ML team owns the transform plugin, ES team added here for visibility -# because the plugin lives in Kibana's Elasticsearch management section. -/x-pack/plugins/transform/ @elastic/ml-ui @elastic/es-ui +# ML team owns and maintains the transform plugin despite it living in the Elasticsearch management section. +/x-pack/plugins/transform/ @elastic/ml-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui /x-pack/test/functional/services/transform_ui/ @elastic/ml-ui /x-pack/test/functional/services/transform.ts @elastic/ml-ui From e7a4ca261b17418e32567b0f69fd842ffc989318 Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Mon, 6 Apr 2020 18:02:58 +0100 Subject: [PATCH 11/15] [Event Log] adds query support to the Event Log (#62015) * added Start api on Event Log plugin * added empty skeleton for Event Log FTs * added functional test to public find events api * added test for pagination * fixed unit tests * added support for date ranges * removed unused code * replaces valdiation typing * Revert "replaces valdiation typing" This reverts commit 711c098e9b2a8329c58c9674fc99de23842884d1. * replaces match with term * added sorting * fixed saved objects nested query * updated plugin FTs path * Update x-pack/plugins/encrypted_saved_objects/README.md Co-Authored-By: Aleh Zasypkin * Update x-pack/plugins/encrypted_saved_objects/README.md Co-Authored-By: Aleh Zasypkin * remofed validation from tests * fixed typos Co-authored-by: Elastic Machine Co-authored-by: Aleh Zasypkin --- package.json | 3 +- test/scripts/jenkins_xpack_build_kibana.sh | 1 + .../plugins/encrypted_saved_objects/README.md | 4 +- x-pack/plugins/event_log/common/index.ts | 7 + .../server/es/cluster_client_adapter.mock.ts | 1 + .../server/es/cluster_client_adapter.test.ts | 229 ++++++++++++++ .../server/es/cluster_client_adapter.ts | 91 ++++++ .../event_log/server/event_log_client.mock.ts | 18 ++ .../event_log/server/event_log_client.test.ts | 292 ++++++++++++++++++ .../event_log/server/event_log_client.ts | 86 ++++++ .../server/event_log_start_service.mock.ts | 18 ++ .../server/event_log_start_service.test.ts | 59 ++++ .../server/event_log_start_service.ts | 48 +++ x-pack/plugins/event_log/server/index.ts | 2 +- x-pack/plugins/event_log/server/mocks.ts | 5 +- x-pack/plugins/event_log/server/plugin.ts | 42 ++- .../server/routes/_mock_handler_arguments.ts | 70 +++++ .../event_log/server/routes/find.test.ts | 98 ++++++ .../plugins/event_log/server/routes/find.ts | 50 +++ .../plugins/event_log/server/routes/index.ts | 7 + x-pack/plugins/event_log/server/types.ts | 23 ++ x-pack/plugins/task_manager/server/README.md | 4 +- x-pack/scripts/functional_tests.js | 2 +- .../{config.js => config.ts} | 11 +- .../plugins/event_log/kibana.json | 9 + .../plugins/event_log/package.json | 15 + .../plugins/event_log/server/index.ts | 11 + .../plugins/event_log/server/plugin.ts | 98 ++++++ .../test_suites/event_log/index.ts | 15 + .../event_log/public_api_integration.ts | 236 ++++++++++++++ .../event_log/service_api_integration.ts | 11 + 31 files changed, 1552 insertions(+), 14 deletions(-) create mode 100644 x-pack/plugins/event_log/common/index.ts create mode 100644 x-pack/plugins/event_log/server/event_log_client.mock.ts create mode 100644 x-pack/plugins/event_log/server/event_log_client.test.ts create mode 100644 x-pack/plugins/event_log/server/event_log_client.ts create mode 100644 x-pack/plugins/event_log/server/event_log_start_service.mock.ts create mode 100644 x-pack/plugins/event_log/server/event_log_start_service.test.ts create mode 100644 x-pack/plugins/event_log/server/event_log_start_service.ts create mode 100644 x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts create mode 100644 x-pack/plugins/event_log/server/routes/find.test.ts create mode 100644 x-pack/plugins/event_log/server/routes/find.ts create mode 100644 x-pack/plugins/event_log/server/routes/index.ts rename x-pack/test/plugin_api_integration/{config.js => config.ts} (77%) create mode 100644 x-pack/test/plugin_api_integration/plugins/event_log/kibana.json create mode 100644 x-pack/test/plugin_api_integration/plugins/event_log/package.json create mode 100644 x-pack/test/plugin_api_integration/plugins/event_log/server/index.ts create mode 100644 x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts create mode 100644 x-pack/test/plugin_api_integration/test_suites/event_log/index.ts create mode 100644 x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts create mode 100644 x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts diff --git a/package.json b/package.json index 46e0b9adfea2513..e807cd4d9519891 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,8 @@ "examples/*", "test/plugin_functional/plugins/*", "test/interpreter_functional/plugins/*", - "x-pack/test/functional_with_es_ssl/fixtures/plugins/*" + "x-pack/test/functional_with_es_ssl/fixtures/plugins/*", + "x-pack/test/plugin_api_integration/plugins/*" ], "nohoist": [ "**/@types/*", diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh index 777d98080e407e7..962d2794f712f4b 100755 --- a/test/scripts/jenkins_xpack_build_kibana.sh +++ b/test/scripts/jenkins_xpack_build_kibana.sh @@ -7,6 +7,7 @@ echo " -> building kibana platform plugins" node scripts/build_kibana_platform_plugins \ --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \ --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \ + --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \ --verbose; # doesn't persist, also set in kibanaPipeline.groovy diff --git a/x-pack/plugins/encrypted_saved_objects/README.md b/x-pack/plugins/encrypted_saved_objects/README.md index a352989870079e8..6085b52d392a4d1 100644 --- a/x-pack/plugins/encrypted_saved_objects/README.md +++ b/x-pack/plugins/encrypted_saved_objects/README.md @@ -100,10 +100,10 @@ $ node scripts/jest.js In one shell, from `kibana-root-folder/x-pack`: ```bash -$ node scripts/functional_tests_server.js --config test/plugin_api_integration/config.js +$ node scripts/functional_tests_server.js --config test/encrypted_saved_objects_api_integration/config.ts ``` In another shell, from `kibana-root-folder/x-pack`: ```bash -$ node ../scripts/functional_test_runner.js --config test/plugin_api_integration/config.js --grep="{TEST_NAME}" +$ node ../scripts/functional_test_runner.js --config test/encrypted_saved_objects_api_integration/config.ts --grep="{TEST_NAME}" ``` diff --git a/x-pack/plugins/event_log/common/index.ts b/x-pack/plugins/event_log/common/index.ts new file mode 100644 index 000000000000000..3ee274916c127cc --- /dev/null +++ b/x-pack/plugins/event_log/common/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const BASE_EVENT_LOG_API_PATH = '/api/event_log'; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts index 87e8fb0f521a9e4..bd57958b0cb88a2 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts @@ -15,6 +15,7 @@ const createClusterClientMock = () => { createIndexTemplate: jest.fn(), doesAliasExist: jest.fn(), createIndex: jest.fn(), + queryEventsBySavedObject: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index b61196439ee4fac..ae26d7a7ece07d2 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -7,6 +7,8 @@ import { ClusterClient, Logger } from '../../../../../src/core/server'; import { elasticsearchServiceMock, loggingServiceMock } from '../../../../../src/core/server/mocks'; import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter'; +import moment from 'moment'; +import { findOptionsSchema } from '../event_log_client'; type EsClusterClient = Pick, 'callAsInternalUser' | 'asScoped'>; @@ -195,3 +197,230 @@ describe('createIndex', () => { await clusterClientAdapter.createIndex('foo'); }); }); + +describe('queryEventsBySavedObject', () => { + const DEFAULT_OPTIONS = findOptionsSchema.validate({}); + + test('should call cluster with proper arguments', async () => { + clusterClient.callAsInternalUser.mockResolvedValue({ + hits: { + hits: [], + total: { value: 0 }, + }, + }); + await clusterClientAdapter.queryEventsBySavedObject( + 'index-name', + 'saved-object-type', + 'saved-object-id', + DEFAULT_OPTIONS + ); + + const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; + expect(method).toEqual('search'); + expect(query).toMatchObject({ + index: 'index-name', + body: { + from: 0, + size: 10, + sort: { 'event.start': { order: 'asc' } }, + query: { + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.type': { + value: 'saved-object-type', + }, + }, + }, + { + term: { + 'kibana.saved_objects.id': { + value: 'saved-object-id', + }, + }, + }, + ], + }, + }, + }, + }, + ], + }, + }, + }, + }); + }); + + test('should call cluster with sort', async () => { + clusterClient.callAsInternalUser.mockResolvedValue({ + hits: { + hits: [], + total: { value: 0 }, + }, + }); + await clusterClientAdapter.queryEventsBySavedObject( + 'index-name', + 'saved-object-type', + 'saved-object-id', + { ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' } + ); + + const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; + expect(method).toEqual('search'); + expect(query).toMatchObject({ + index: 'index-name', + body: { + sort: { 'event.end': { order: 'desc' } }, + }, + }); + }); + + test('supports open ended date', async () => { + clusterClient.callAsInternalUser.mockResolvedValue({ + hits: { + hits: [], + total: { value: 0 }, + }, + }); + + const start = moment() + .subtract(1, 'days') + .toISOString(); + + await clusterClientAdapter.queryEventsBySavedObject( + 'index-name', + 'saved-object-type', + 'saved-object-id', + { ...DEFAULT_OPTIONS, start } + ); + + const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; + expect(method).toEqual('search'); + expect(query).toMatchObject({ + index: 'index-name', + body: { + query: { + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.type': { + value: 'saved-object-type', + }, + }, + }, + { + term: { + 'kibana.saved_objects.id': { + value: 'saved-object-id', + }, + }, + }, + ], + }, + }, + }, + }, + { + range: { + 'event.start': { + gte: start, + }, + }, + }, + ], + }, + }, + }, + }); + }); + + test('supports optional date range', async () => { + clusterClient.callAsInternalUser.mockResolvedValue({ + hits: { + hits: [], + total: { value: 0 }, + }, + }); + + const start = moment() + .subtract(1, 'days') + .toISOString(); + const end = moment() + .add(1, 'days') + .toISOString(); + + await clusterClientAdapter.queryEventsBySavedObject( + 'index-name', + 'saved-object-type', + 'saved-object-id', + { ...DEFAULT_OPTIONS, start, end } + ); + + const [method, query] = clusterClient.callAsInternalUser.mock.calls[0]; + expect(method).toEqual('search'); + expect(query).toMatchObject({ + index: 'index-name', + body: { + query: { + bool: { + must: [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.type': { + value: 'saved-object-type', + }, + }, + }, + { + term: { + 'kibana.saved_objects.id': { + value: 'saved-object-id', + }, + }, + }, + ], + }, + }, + }, + }, + { + range: { + 'event.start': { + gte: start, + }, + }, + }, + { + range: { + 'event.end': { + lte: end, + }, + }, + }, + ], + }, + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index d585fd4f539b5c3..36bc94edfca4ee3 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { reject, isUndefined } from 'lodash'; import { Logger, ClusterClient } from '../../../../../src/core/server'; +import { IEvent } from '../types'; +import { FindOptionsType } from '../event_log_client'; export type EsClusterClient = Pick; export type IClusterClientAdapter = PublicMethodsOf; @@ -14,6 +17,13 @@ export interface ConstructorOpts { clusterClient: EsClusterClient; } +export interface QueryEventsBySavedObjectResult { + page: number; + per_page: number; + total: number; + data: IEvent[]; +} + export class ClusterClientAdapter { private readonly logger: Logger; private readonly clusterClient: EsClusterClient; @@ -107,6 +117,87 @@ export class ClusterClientAdapter { } } + public async queryEventsBySavedObject( + index: string, + type: string, + id: string, + { page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType + ): Promise { + try { + const { + hits: { + hits, + total: { value: total }, + }, + } = await this.callEs('search', { + index, + body: { + size: perPage, + from: (page - 1) * perPage, + sort: { [sort_field]: { order: sort_order } }, + query: { + bool: { + must: reject( + [ + { + nested: { + path: 'kibana.saved_objects', + query: { + bool: { + must: [ + { + term: { + 'kibana.saved_objects.type': { + value: type, + }, + }, + }, + { + term: { + 'kibana.saved_objects.id': { + value: id, + }, + }, + }, + ], + }, + }, + }, + }, + start && { + range: { + 'event.start': { + gte: start, + }, + }, + }, + end && { + range: { + 'event.end': { + lte: end, + }, + }, + }, + ], + isUndefined + ), + }, + }, + }, + }); + return { + page, + per_page: perPage, + total, + data: hits.map((hit: any) => hit._source) as IEvent[], + }; + } catch (err) { + throw new Error( + `querying for Event Log by for type "${type}" and id "${id}" failed with: ${err.message}` + ); + } + } + private async callEs(operation: string, body?: any): Promise { try { this.debug(`callEs(${operation}) calls:`, body); diff --git a/x-pack/plugins/event_log/server/event_log_client.mock.ts b/x-pack/plugins/event_log/server/event_log_client.mock.ts new file mode 100644 index 000000000000000..31cab802555d068 --- /dev/null +++ b/x-pack/plugins/event_log/server/event_log_client.mock.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEventLogClient } from './types'; + +const createEventLogClientMock = () => { + const mock: jest.Mocked = { + findEventsBySavedObject: jest.fn(), + }; + return mock; +}; + +export const eventLogClientMock = { + create: createEventLogClientMock, +}; diff --git a/x-pack/plugins/event_log/server/event_log_client.test.ts b/x-pack/plugins/event_log/server/event_log_client.test.ts new file mode 100644 index 000000000000000..6d4c9b67abc1bae --- /dev/null +++ b/x-pack/plugins/event_log/server/event_log_client.test.ts @@ -0,0 +1,292 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EventLogClient } from './event_log_client'; +import { contextMock } from './es/context.mock'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { merge } from 'lodash'; +import moment from 'moment'; + +describe('EventLogStart', () => { + describe('findEventsBySavedObject', () => { + test('verifies that the user can access the specified saved object', async () => { + const esContext = contextMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); + const eventLogClient = new EventLogClient({ + esContext, + savedObjectsClient, + }); + + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'saved-object-id', + type: 'saved-object-type', + attributes: {}, + references: [], + }); + + await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id'); + + expect(savedObjectsClient.get).toHaveBeenCalledWith('saved-object-type', 'saved-object-id'); + }); + + test('throws when the user doesnt have permission to access the specified saved object', async () => { + const esContext = contextMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); + const eventLogClient = new EventLogClient({ + esContext, + savedObjectsClient, + }); + + savedObjectsClient.get.mockRejectedValue(new Error('Fail')); + + expect( + eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id') + ).rejects.toMatchInlineSnapshot(`[Error: Fail]`); + }); + + test('fetches all event that reference the saved object', async () => { + const esContext = contextMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); + const eventLogClient = new EventLogClient({ + esContext, + savedObjectsClient, + }); + + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'saved-object-id', + type: 'saved-object-type', + attributes: {}, + references: [], + }); + + const expectedEvents = [ + fakeEvent({ + kibana: { + saved_objects: [ + { + id: 'saved-object-id', + type: 'saved-object-type', + }, + { + type: 'action', + id: '1', + }, + ], + }, + }), + fakeEvent({ + kibana: { + saved_objects: [ + { + id: 'saved-object-id', + type: 'saved-object-type', + }, + { + type: 'action', + id: '2', + }, + ], + }, + }), + ]; + + const result = { + page: 0, + per_page: 10, + total: expectedEvents.length, + data: expectedEvents, + }; + esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue(result); + + expect( + await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id') + ).toEqual(result); + + expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( + esContext.esNames.alias, + 'saved-object-type', + 'saved-object-id', + { + page: 1, + per_page: 10, + sort_field: 'event.start', + sort_order: 'asc', + } + ); + }); + + test('fetches all events in time frame that reference the saved object', async () => { + const esContext = contextMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); + const eventLogClient = new EventLogClient({ + esContext, + savedObjectsClient, + }); + + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'saved-object-id', + type: 'saved-object-type', + attributes: {}, + references: [], + }); + + const expectedEvents = [ + fakeEvent({ + kibana: { + saved_objects: [ + { + id: 'saved-object-id', + type: 'saved-object-type', + }, + { + type: 'action', + id: '1', + }, + ], + }, + }), + fakeEvent({ + kibana: { + saved_objects: [ + { + id: 'saved-object-id', + type: 'saved-object-type', + }, + { + type: 'action', + id: '2', + }, + ], + }, + }), + ]; + + const result = { + page: 0, + per_page: 10, + total: expectedEvents.length, + data: expectedEvents, + }; + esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue(result); + + const start = moment() + .subtract(1, 'days') + .toISOString(); + const end = moment() + .add(1, 'days') + .toISOString(); + + expect( + await eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', { + start, + end, + }) + ).toEqual(result); + + expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith( + esContext.esNames.alias, + 'saved-object-type', + 'saved-object-id', + { + page: 1, + per_page: 10, + sort_field: 'event.start', + sort_order: 'asc', + start, + end, + } + ); + }); + + test('validates that the start date is valid', async () => { + const esContext = contextMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); + const eventLogClient = new EventLogClient({ + esContext, + savedObjectsClient, + }); + + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'saved-object-id', + type: 'saved-object-type', + attributes: {}, + references: [], + }); + + esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue({ + page: 0, + per_page: 0, + total: 0, + data: [], + }); + + expect( + eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', { + start: 'not a date string', + }) + ).rejects.toMatchInlineSnapshot(`[Error: [start]: Invalid Date]`); + }); + + test('validates that the end date is valid', async () => { + const esContext = contextMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); + const eventLogClient = new EventLogClient({ + esContext, + savedObjectsClient, + }); + + savedObjectsClient.get.mockResolvedValueOnce({ + id: 'saved-object-id', + type: 'saved-object-type', + attributes: {}, + references: [], + }); + + esContext.esAdapter.queryEventsBySavedObject.mockResolvedValue({ + page: 0, + per_page: 0, + total: 0, + data: [], + }); + + expect( + eventLogClient.findEventsBySavedObject('saved-object-type', 'saved-object-id', { + end: 'not a date string', + }) + ).rejects.toMatchInlineSnapshot(`[Error: [end]: Invalid Date]`); + }); + }); +}); + +function fakeEvent(overrides = {}) { + return merge( + { + event: { + provider: 'actions', + action: 'execute', + start: '2020-03-30T14:55:47.054Z', + end: '2020-03-30T14:55:47.055Z', + duration: 1000000, + }, + kibana: { + namespace: 'default', + saved_objects: [ + { + type: 'action', + id: '968f1b82-0414-4a10-becc-56b6473e4a29', + }, + ], + server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', + }, + message: 'action executed: .server-log:968f1b82-0414-4a10-becc-56b6473e4a29: logger', + '@timestamp': '2020-03-30T14:55:47.055Z', + ecs: { + version: '1.3.1', + }, + }, + overrides + ); +} diff --git a/x-pack/plugins/event_log/server/event_log_client.ts b/x-pack/plugins/event_log/server/event_log_client.ts new file mode 100644 index 000000000000000..765f0895f8e0d1d --- /dev/null +++ b/x-pack/plugins/event_log/server/event_log_client.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { ClusterClient, SavedObjectsClientContract } from 'src/core/server'; + +import { schema, TypeOf } from '@kbn/config-schema'; +import { EsContext } from './es'; +import { IEventLogClient } from './types'; +import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; +export type PluginClusterClient = Pick; +export type AdminClusterClient$ = Observable; + +interface EventLogServiceCtorParams { + esContext: EsContext; + savedObjectsClient: SavedObjectsClientContract; +} + +const optionalDateFieldSchema = schema.maybe( + schema.string({ + validate(value) { + if (isNaN(Date.parse(value))) { + return 'Invalid Date'; + } + }, + }) +); + +export const findOptionsSchema = schema.object({ + per_page: schema.number({ defaultValue: 10, min: 0 }), + page: schema.number({ defaultValue: 1, min: 1 }), + start: optionalDateFieldSchema, + end: optionalDateFieldSchema, + sort_field: schema.oneOf( + [ + schema.literal('event.start'), + schema.literal('event.end'), + schema.literal('event.provider'), + schema.literal('event.duration'), + schema.literal('event.action'), + schema.literal('message'), + ], + { + defaultValue: 'event.start', + } + ), + sort_order: schema.oneOf([schema.literal('asc'), schema.literal('desc')], { + defaultValue: 'asc', + }), +}); +// page & perPage are required, other fields are optional +// using schema.maybe allows us to set undefined, but not to make the field optional +export type FindOptionsType = Pick< + TypeOf, + 'page' | 'per_page' | 'sort_field' | 'sort_order' +> & + Partial>; + +// note that clusterClient may be null, indicating we can't write to ES +export class EventLogClient implements IEventLogClient { + private esContext: EsContext; + private savedObjectsClient: SavedObjectsClientContract; + + constructor({ esContext, savedObjectsClient }: EventLogServiceCtorParams) { + this.esContext = esContext; + this.savedObjectsClient = savedObjectsClient; + } + + async findEventsBySavedObject( + type: string, + id: string, + options?: Partial + ): Promise { + // verify the user has the required permissions to view this saved object + await this.savedObjectsClient.get(type, id); + return await this.esContext.esAdapter.queryEventsBySavedObject( + this.esContext.esNames.alias, + type, + id, + findOptionsSchema.validate(options ?? {}) + ); + } +} diff --git a/x-pack/plugins/event_log/server/event_log_start_service.mock.ts b/x-pack/plugins/event_log/server/event_log_start_service.mock.ts new file mode 100644 index 000000000000000..e99ec777b473b15 --- /dev/null +++ b/x-pack/plugins/event_log/server/event_log_start_service.mock.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEventLogClientService } from './types'; + +const createEventLogServiceMock = () => { + const mock: jest.Mocked = { + getClient: jest.fn(), + }; + return mock; +}; + +export const eventLogStartServiceMock = { + create: createEventLogServiceMock, +}; diff --git a/x-pack/plugins/event_log/server/event_log_start_service.test.ts b/x-pack/plugins/event_log/server/event_log_start_service.test.ts new file mode 100644 index 000000000000000..a8d75bc6c2e5a27 --- /dev/null +++ b/x-pack/plugins/event_log/server/event_log_start_service.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EventLogClientService } from './event_log_start_service'; +import { contextMock } from './es/context.mock'; +import { KibanaRequest } from 'kibana/server'; +import { savedObjectsServiceMock } from 'src/core/server/saved_objects/saved_objects_service.mock'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +jest.mock('./event_log_client'); + +describe('EventLogClientService', () => { + const esContext = contextMock.create(); + + describe('getClient', () => { + test('creates a client with a scoped SavedObjects client', () => { + const savedObjectsService = savedObjectsServiceMock.createStartContract(); + const request = fakeRequest(); + + const eventLogStartService = new EventLogClientService({ + esContext, + savedObjectsService, + }); + + eventLogStartService.getClient(request); + + expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request); + + const [{ value: savedObjectsClient }] = savedObjectsService.getScopedClient.mock.results; + + expect(jest.requireMock('./event_log_client').EventLogClient).toHaveBeenCalledWith({ + esContext, + savedObjectsClient, + }); + }); + }); +}); + +function fakeRequest(): KibanaRequest { + const savedObjectsClient = savedObjectsClientMock.create(); + return { + headers: {}, + getBasePath: () => '', + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + getSavedObjectsClient: () => savedObjectsClient, + } as any; +} diff --git a/x-pack/plugins/event_log/server/event_log_start_service.ts b/x-pack/plugins/event_log/server/event_log_start_service.ts new file mode 100644 index 000000000000000..5938f7a2e614ecb --- /dev/null +++ b/x-pack/plugins/event_log/server/event_log_start_service.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _ from 'lodash'; +import { Observable } from 'rxjs'; +import { + ClusterClient, + KibanaRequest, + SavedObjectsServiceStart, + SavedObjectsClientContract, +} from 'src/core/server'; + +import { EsContext } from './es'; +import { IEventLogClientService } from './types'; +import { EventLogClient } from './event_log_client'; +export type PluginClusterClient = Pick; +export type AdminClusterClient$ = Observable; + +interface EventLogServiceCtorParams { + esContext: EsContext; + savedObjectsService: SavedObjectsServiceStart; +} + +// note that clusterClient may be null, indicating we can't write to ES +export class EventLogClientService implements IEventLogClientService { + private esContext: EsContext; + private savedObjectsService: SavedObjectsServiceStart; + + constructor({ esContext, savedObjectsService }: EventLogServiceCtorParams) { + this.esContext = esContext; + this.savedObjectsService = savedObjectsService; + } + + getClient( + request: KibanaRequest, + savedObjectsClient: SavedObjectsClientContract = this.savedObjectsService.getScopedClient( + request + ) + ) { + return new EventLogClient({ + esContext: this.esContext, + savedObjectsClient, + }); + } +} diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index 81a56faa49964c9..b7fa25cb6eb9cc9 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -8,6 +8,6 @@ import { PluginInitializerContext } from 'src/core/server'; import { ConfigSchema } from './types'; import { Plugin } from './plugin'; -export { IEventLogService, IEventLogger, IEvent } from './types'; +export { IEventLogService, IEventLogger, IEventLogClientService, IEvent } from './types'; export const config = { schema: ConfigSchema }; export const plugin = (context: PluginInitializerContext) => new Plugin(context); diff --git a/x-pack/plugins/event_log/server/mocks.ts b/x-pack/plugins/event_log/server/mocks.ts index aad6cf3e2456164..2f632a52d2f360b 100644 --- a/x-pack/plugins/event_log/server/mocks.ts +++ b/x-pack/plugins/event_log/server/mocks.ts @@ -5,8 +5,9 @@ */ import { eventLogServiceMock } from './event_log_service.mock'; +import { eventLogStartServiceMock } from './event_log_start_service.mock'; -export { eventLogServiceMock }; +export { eventLogServiceMock, eventLogStartServiceMock }; export { eventLoggerMock } from './event_logger.mock'; const createSetupMock = () => { @@ -14,7 +15,7 @@ const createSetupMock = () => { }; const createStartMock = () => { - return undefined; + return eventLogStartServiceMock.create(); }; export const eventLogMock = { diff --git a/x-pack/plugins/event_log/server/plugin.ts b/x-pack/plugins/event_log/server/plugin.ts index fdb08b2d090a627..2cc41354b4fbc5d 100644 --- a/x-pack/plugins/event_log/server/plugin.ts +++ b/x-pack/plugins/event_log/server/plugin.ts @@ -14,11 +14,21 @@ import { PluginInitializerContext, ClusterClient, SharedGlobalConfig, + IContextProvider, + RequestHandler, } from 'src/core/server'; -import { IEventLogConfig, IEventLogService, IEventLogger, IEventLogConfig$ } from './types'; +import { + IEventLogConfig, + IEventLogService, + IEventLogger, + IEventLogConfig$, + IEventLogClientService, +} from './types'; +import { findRoute } from './routes'; import { EventLogService } from './event_log_service'; import { createEsContext, EsContext } from './es'; +import { EventLogClientService } from './event_log_start_service'; export type PluginClusterClient = Pick; @@ -29,13 +39,14 @@ const ACTIONS = { stopping: 'stopping', }; -export class Plugin implements CorePlugin { +export class Plugin implements CorePlugin { private readonly config$: IEventLogConfig$; private systemLogger: Logger; private eventLogService?: IEventLogService; private esContext?: EsContext; private eventLogger?: IEventLogger; private globalConfig$: Observable; + private eventLogClientService?: EventLogClientService; constructor(private readonly context: PluginInitializerContext) { this.systemLogger = this.context.logger.get(); @@ -71,10 +82,17 @@ export class Plugin implements CorePlugin { event: { provider: PROVIDER }, }); + core.http.registerRouteHandlerContext('eventLog', this.createRouteHandlerContext()); + + // Routes + const router = core.http.createRouter(); + // Register routes + findRoute(router); + return this.eventLogService; } - async start(core: CoreStart) { + async start(core: CoreStart): Promise { this.systemLogger.debug('starting plugin'); if (!this.esContext) throw new Error('esContext not initialized'); @@ -91,8 +109,26 @@ export class Plugin implements CorePlugin { event: { action: ACTIONS.starting }, message: 'eventLog starting', }); + + this.eventLogClientService = new EventLogClientService({ + esContext: this.esContext, + savedObjectsService: core.savedObjects, + }); + return this.eventLogClientService; } + private createRouteHandlerContext = (): IContextProvider< + RequestHandler, + 'eventLog' + > => { + return async (context, request) => { + return { + getEventLogClient: () => + this.eventLogClientService!.getClient(request, context.core.savedObjects.client), + }; + }; + }; + stop() { this.systemLogger.debug('stopping plugin'); diff --git a/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts new file mode 100644 index 000000000000000..6640683bf6005e5 --- /dev/null +++ b/x-pack/plugins/event_log/server/routes/_mock_handler_arguments.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server'; +import { identity, merge } from 'lodash'; +import { httpServerMock } from '../../../../../src/core/server/mocks'; +import { IEventLogClient } from '../types'; + +export function mockHandlerArguments( + eventLogClient: IEventLogClient, + req: any, + res?: Array> +): [RequestHandlerContext, KibanaRequest, KibanaResponseFactory] { + return [ + ({ + eventLog: { + getEventLogClient() { + return eventLogClient; + }, + }, + } as unknown) as RequestHandlerContext, + req as KibanaRequest, + mockResponseFactory(res), + ]; +} + +export const mockResponseFactory = (resToMock: Array> = []) => { + const factory: jest.Mocked = httpServerMock.createResponseFactory(); + resToMock.forEach((key: string) => { + if (key in factory) { + Object.defineProperty(factory, key, { + value: jest.fn(identity), + }); + } + }); + return (factory as unknown) as KibanaResponseFactory; +}; + +export function fakeEvent(overrides = {}) { + return merge( + { + event: { + provider: 'actions', + action: 'execute', + start: '2020-03-30T14:55:47.054Z', + end: '2020-03-30T14:55:47.055Z', + duration: 1000000, + }, + kibana: { + namespace: 'default', + saved_objects: [ + { + type: 'action', + id: '968f1b82-0414-4a10-becc-56b6473e4a29', + }, + ], + server_uuid: '5b2de169-2785-441b-ae8c-186a1936b17d', + }, + message: 'action executed: .server-log:968f1b82-0414-4a10-becc-56b6473e4a29: logger', + '@timestamp': '2020-03-30T14:55:47.055Z', + ecs: { + version: '1.3.1', + }, + }, + overrides + ); +} diff --git a/x-pack/plugins/event_log/server/routes/find.test.ts b/x-pack/plugins/event_log/server/routes/find.test.ts new file mode 100644 index 000000000000000..844a84dc117a9c2 --- /dev/null +++ b/x-pack/plugins/event_log/server/routes/find.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { findRoute } from './find'; +import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock'; +import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments'; +import { eventLogClientMock } from '../event_log_client.mock'; + +const eventLogClient = eventLogClientMock.create(); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('find', () => { + it('finds events with proper parameters', async () => { + const router: RouterMock = mockRouter.create(); + + findRoute(router); + + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/api/event_log/{type}/{id}/_find"`); + + const events = [fakeEvent(), fakeEvent()]; + const result = { + page: 0, + per_page: 10, + total: events.length, + data: events, + }; + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce(result); + + const [context, req, res] = mockHandlerArguments( + eventLogClient, + { + params: { id: '1', type: 'action' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + + const [type, id] = eventLogClient.findEventsBySavedObject.mock.calls[0]; + expect(type).toEqual(`action`); + expect(id).toEqual(`1`); + + expect(res.ok).toHaveBeenCalledWith({ + body: result, + }); + }); + + it('supports optional pagination parameters', async () => { + const router: RouterMock = mockRouter.create(); + + findRoute(router); + + const [, handler] = router.get.mock.calls[0]; + eventLogClient.findEventsBySavedObject.mockResolvedValueOnce({ + page: 0, + per_page: 10, + total: 0, + data: [], + }); + + const [context, req, res] = mockHandlerArguments( + eventLogClient, + { + params: { id: '1', type: 'action' }, + query: { page: 3, per_page: 10 }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(eventLogClient.findEventsBySavedObject).toHaveBeenCalledTimes(1); + + const [type, id, options] = eventLogClient.findEventsBySavedObject.mock.calls[0]; + expect(type).toEqual(`action`); + expect(id).toEqual(`1`); + expect(options).toMatchObject({}); + + expect(res.ok).toHaveBeenCalledWith({ + body: { + page: 0, + per_page: 10, + total: 0, + data: [], + }, + }); + }); +}); diff --git a/x-pack/plugins/event_log/server/routes/find.ts b/x-pack/plugins/event_log/server/routes/find.ts new file mode 100644 index 000000000000000..cb170e50fb44773 --- /dev/null +++ b/x-pack/plugins/event_log/server/routes/find.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; +import { + IRouter, + RequestHandlerContext, + KibanaRequest, + IKibanaResponse, + KibanaResponseFactory, +} from 'kibana/server'; +import { BASE_EVENT_LOG_API_PATH } from '../../common'; +import { findOptionsSchema, FindOptionsType } from '../event_log_client'; + +const paramSchema = schema.object({ + type: schema.string(), + id: schema.string(), +}); + +export const findRoute = (router: IRouter) => { + router.get( + { + path: `${BASE_EVENT_LOG_API_PATH}/{type}/{id}/_find`, + validate: { + params: paramSchema, + query: findOptionsSchema, + }, + }, + router.handleLegacyErrors(async function( + context: RequestHandlerContext, + req: KibanaRequest, FindOptionsType, any, any>, + res: KibanaResponseFactory + ): Promise> { + if (!context.eventLog) { + return res.badRequest({ body: 'RouteHandlerContext is not registered for eventLog' }); + } + const eventLogClient = context.eventLog.getEventLogClient(); + const { + params: { id, type }, + query, + } = req; + return res.ok({ + body: await eventLogClient.findEventsBySavedObject(type, id, query), + }); + }) + ); +}; diff --git a/x-pack/plugins/event_log/server/routes/index.ts b/x-pack/plugins/event_log/server/routes/index.ts new file mode 100644 index 000000000000000..85d9b3e0db8cd6b --- /dev/null +++ b/x-pack/plugins/event_log/server/routes/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { findRoute } from './find'; diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index f606bb2be6c6c14..baf53ef4479141f 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -8,7 +8,10 @@ import { Observable } from 'rxjs'; import { schema, TypeOf } from '@kbn/config-schema'; export { IEvent, IValidatedEvent, EventSchema, ECS_VERSION } from '../generated/schemas'; +import { KibanaRequest } from 'kibana/server'; import { IEvent } from '../generated/schemas'; +import { FindOptionsType } from './event_log_client'; +import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter'; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -19,6 +22,14 @@ export const ConfigSchema = schema.object({ export type IEventLogConfig = TypeOf; export type IEventLogConfig$ = Observable>; +declare module 'src/core/server' { + interface RequestHandlerContext { + eventLog?: { + getEventLogClient: () => IEventLogClient; + }; + } +} + // the object exposed by plugin.setup() export interface IEventLogService { isEnabled(): boolean; @@ -31,6 +42,18 @@ export interface IEventLogService { getLogger(properties: IEvent): IEventLogger; } +export interface IEventLogClientService { + getClient(request: KibanaRequest): IEventLogClient; +} + +export interface IEventLogClient { + findEventsBySavedObject( + type: string, + id: string, + options?: Partial + ): Promise; +} + export interface IEventLogger { logEvent(properties: IEvent): void; startTiming(event: IEvent): void; diff --git a/x-pack/plugins/task_manager/server/README.md b/x-pack/plugins/task_manager/server/README.md index a4154f3ecf21243..c3d45be5d8f22ed 100644 --- a/x-pack/plugins/task_manager/server/README.md +++ b/x-pack/plugins/task_manager/server/README.md @@ -456,6 +456,6 @@ The task manager's public API is create / delete / list. Updates aren't directly ``` - Integration tests: ``` - node scripts/functional_tests_server.js --config x-pack/test/plugin_api_integration/config.js - node scripts/functional_test_runner --config x-pack/test/plugin_api_integration/config.js + node scripts/functional_tests_server.js --config x-pack/test/plugin_api_integration/config.ts + node scripts/functional_test_runner --config x-pack/test/plugin_api_integration/config.ts ``` diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index b0ca33b00fde835..7943da07716a13f 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -17,7 +17,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/plugin_api_integration/config.js'), + require.resolve('../test/plugin_api_integration/config.ts'), require.resolve('../test/plugin_functional/config.ts'), require.resolve('../test/kerberos_api_integration/config.ts'), require.resolve('../test/kerberos_api_integration/anonymous_access.config.ts'), diff --git a/x-pack/test/plugin_api_integration/config.js b/x-pack/test/plugin_api_integration/config.ts similarity index 77% rename from x-pack/test/plugin_api_integration/config.js rename to x-pack/test/plugin_api_integration/config.ts index 83e8b1f84a9e02f..c581e0c246e132b 100644 --- a/x-pack/test/plugin_api_integration/config.js +++ b/x-pack/test/plugin_api_integration/config.ts @@ -6,9 +6,10 @@ import path from 'path'; import fs from 'fs'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; -export default async function({ readConfigFile }) { +export default async function({ readConfigFile }: FtrConfigProviderContext) { const integrationConfig = await readConfigFile(require.resolve('../api_integration/config')); // Find all folders in ./plugins since we treat all them as plugin folder @@ -18,7 +19,10 @@ export default async function({ readConfigFile }) { ); return { - testFiles: [require.resolve('./test_suites/task_manager')], + testFiles: [ + require.resolve('./test_suites/task_manager'), + require.resolve('./test_suites/event_log'), + ], services, servers: integrationConfig.get('servers'), esTestCluster: integrationConfig.get('esTestCluster'), @@ -34,6 +38,9 @@ export default async function({ readConfigFile }) { ...integrationConfig.get('kbnTestServer'), serverArgs: [ ...integrationConfig.get('kbnTestServer.serverArgs'), + '--xpack.eventLog.enabled=true', + '--xpack.eventLog.logEntries=true', + '--xpack.eventLog.indexEntries=true', ...plugins.map( pluginDir => `--plugin-path=${path.resolve(__dirname, 'plugins', pluginDir)}` ), diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/kibana.json b/x-pack/test/plugin_api_integration/plugins/event_log/kibana.json new file mode 100644 index 000000000000000..4b467ce97501226 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/event_log/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "event_log_fixture", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["eventLog"], + "server": true, + "ui": false +} diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/package.json b/x-pack/test/plugin_api_integration/plugins/event_log/package.json new file mode 100644 index 000000000000000..222dfb2338e20ca --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/event_log/package.json @@ -0,0 +1,15 @@ +{ + "name": "event_log_fixture", + "version": "0.0.0", + "kibana": { + "version": "kibana" + }, + "main": "target/test/plugin_api_integration/plugins/event_log", + "scripts": { + "kbn": "node ../../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/index.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/index.ts new file mode 100644 index 000000000000000..3d794a7c80fa38a --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PluginInitializerContext } from 'kibana/server'; +import { EventLogFixturePlugin } from './plugin'; + +export const plugin = (initContext: PluginInitializerContext) => + new EventLogFixturePlugin(initContext); diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts new file mode 100644 index 000000000000000..eccbd4fb7f90be5 --- /dev/null +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/plugin.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Plugin, + CoreSetup, + RequestHandlerContext, + KibanaRequest, + KibanaResponseFactory, + IKibanaResponse, + IRouter, + Logger, + PluginInitializerContext, + RouteValidationResultFactory, +} from 'kibana/server'; +import { + IEventLogService, + IEventLogClientService, + IEventLogger, +} from '../../../../../plugins/event_log/server'; +import { IValidatedEvent } from '../../../../../plugins/event_log/server/types'; + +// this plugin's dependendencies +export interface EventLogFixtureSetupDeps { + eventLog: IEventLogService; +} +export interface EventLogFixtureStartDeps { + eventLog: IEventLogClientService; +} + +export class EventLogFixturePlugin + implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get('plugins', 'eventLogFixture'); + } + + public setup(core: CoreSetup, { eventLog }: EventLogFixtureSetupDeps) { + const router = core.http.createRouter(); + + eventLog.registerProviderActions('event_log_fixture', ['test']); + const eventLogger = eventLog.getLogger({ + event: { provider: 'event_log_fixture' }, + }); + + core.savedObjects.registerType({ + name: 'event_log_test', + hidden: false, + namespaceAgnostic: true, + mappings: { + properties: {}, + }, + }); + + logEventRoute(router, eventLogger, this.logger); + } + + public start() {} + public stop() {} +} + +const logEventRoute = (router: IRouter, eventLogger: IEventLogger, logger: Logger) => { + router.post( + { + path: `/api/log_event_fixture/{id}/_log`, + validate: { + // removed validation as schema is currently broken in tests + // blocked by: https://github.com/elastic/kibana/issues/61652 + params: (value: any, { ok }: RouteValidationResultFactory) => ok(value), + body: (value: any, { ok }: RouteValidationResultFactory) => ok(value), + }, + }, + async function( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + const { id } = req.params as { id: string }; + const event: IValidatedEvent = req.body; + logger.info(`test fixture: log event: ${id} ${JSON.stringify(event)}`); + try { + await context.core.savedObjects.client.get('event_log_test', id); + logger.info(`found existing saved object`); + } catch (ex) { + logger.info(`log event error: ${ex}`); + await context.core.savedObjects.client.create('event_log_test', {}, { id }); + logger.info(`created saved object`); + } + eventLogger.logEvent(event); + logger.info(`logged`); + return res.ok({}); + } + ); +}; diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts new file mode 100644 index 000000000000000..a68378decb1fda1 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function({ loadTestFile }: FtrProviderContext) { + describe('event_log', function taskManagerSuite() { + this.tags('ciGroup2'); + loadTestFile(require.resolve('./public_api_integration')); + loadTestFile(require.resolve('./service_api_integration')); + }); +} diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts new file mode 100644 index 000000000000000..c440971225d78fb --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/public_api_integration.ts @@ -0,0 +1,236 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { merge, omit, times, chunk, isEmpty } from 'lodash'; +import uuid from 'uuid'; +import expect from '@kbn/expect/expect.js'; +import moment, { Moment } from 'moment'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { IEvent } from '../../../../plugins/event_log/server'; +import { IValidatedEvent } from '../../../../plugins/event_log/server/types'; + +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +export default function({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + const retry = getService('retry'); + + describe('Event Log public API', () => { + it('should allow querying for events by Saved Object', async () => { + const id = uuid.v4(); + + const expectedEvents = [fakeEvent(id), fakeEvent(id)]; + + await logTestEvent(id, expectedEvents[0]); + await logTestEvent(id, expectedEvents[1]); + + await retry.try(async () => { + const { + body: { data, total }, + } = await findEvents(id, {}); + + expect(data.length).to.be(2); + expect(total).to.be(2); + + assertEventsFromApiMatchCreatedEvents(data, expectedEvents); + }); + }); + + it('should support pagination for events', async () => { + const id = uuid.v4(); + + const timestamp = moment(); + const [firstExpectedEvent, ...expectedEvents] = times(6, () => + fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))) + ); + // run one first to create the SO and avoid clashes + await logTestEvent(id, firstExpectedEvent); + await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); + + await retry.try(async () => { + const { + body: { data: foundEvents }, + } = await findEvents(id, {}); + + expect(foundEvents.length).to.be(6); + }); + + const [expectedFirstPage, expectedSecondPage] = chunk( + [firstExpectedEvent, ...expectedEvents], + 3 + ); + + const { + body: { data: firstPage }, + } = await findEvents(id, { per_page: 3 }); + + expect(firstPage.length).to.be(3); + assertEventsFromApiMatchCreatedEvents(firstPage, expectedFirstPage); + + const { + body: { data: secondPage }, + } = await findEvents(id, { per_page: 3, page: 2 }); + + expect(secondPage.length).to.be(3); + assertEventsFromApiMatchCreatedEvents(secondPage, expectedSecondPage); + }); + + it('should support sorting by event end', async () => { + const id = uuid.v4(); + + const timestamp = moment(); + const [firstExpectedEvent, ...expectedEvents] = times(6, () => + fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))) + ); + // run one first to create the SO and avoid clashes + await logTestEvent(id, firstExpectedEvent); + await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); + + await retry.try(async () => { + const { + body: { data: foundEvents }, + } = await findEvents(id, { sort_field: 'event.end', sort_order: 'desc' }); + + expect(foundEvents.length).to.be(6); + assertEventsFromApiMatchCreatedEvents( + foundEvents, + [firstExpectedEvent, ...expectedEvents].reverse() + ); + }); + }); + + it('should support date ranges for events', async () => { + const id = uuid.v4(); + + const timestamp = moment(); + + const firstEvent = fakeEvent(id, fakeEventTiming(timestamp)); + await logTestEvent(id, firstEvent); + await delay(100); + + const start = timestamp.add(1, 's').toISOString(); + + const expectedEvents = times(6, () => fakeEvent(id, fakeEventTiming(timestamp.add(1, 's')))); + await Promise.all(expectedEvents.map(event => logTestEvent(id, event))); + + const end = timestamp.add(1, 's').toISOString(); + + await delay(100); + const lastEvent = fakeEvent(id, fakeEventTiming(timestamp.add(1, 's'))); + await logTestEvent(id, lastEvent); + + await retry.try(async () => { + const { + body: { data: foundEvents, total }, + } = await findEvents(id, {}); + + expect(foundEvents.length).to.be(8); + expect(total).to.be(8); + }); + + const { + body: { data: eventsWithinRange }, + } = await findEvents(id, { start, end }); + + expect(eventsWithinRange.length).to.be(expectedEvents.length); + assertEventsFromApiMatchCreatedEvents(eventsWithinRange, expectedEvents); + + const { + body: { data: eventsFrom }, + } = await findEvents(id, { start }); + + expect(eventsFrom.length).to.be(expectedEvents.length + 1); + assertEventsFromApiMatchCreatedEvents(eventsFrom, [...expectedEvents, lastEvent]); + + const { + body: { data: eventsUntil }, + } = await findEvents(id, { end }); + + expect(eventsUntil.length).to.be(expectedEvents.length + 1); + assertEventsFromApiMatchCreatedEvents(eventsUntil, [firstEvent, ...expectedEvents]); + }); + }); + + async function findEvents(id: string, query: Record = {}) { + const uri = `/api/event_log/event_log_test/${id}/_find${ + isEmpty(query) + ? '' + : `?${Object.entries(query) + .map(([key, val]) => `${key}=${val}`) + .join('&')}` + }`; + log.debug(`calling ${uri}`); + return await supertest + .get(uri) + .set('kbn-xsrf', 'foo') + .expect(200); + } + + function assertEventsFromApiMatchCreatedEvents( + foundEvents: IValidatedEvent[], + expectedEvents: IEvent[] + ) { + try { + foundEvents.forEach((foundEvent: IValidatedEvent, index: number) => { + expect(foundEvent!.event).to.eql(expectedEvents[index]!.event); + expect(omit(foundEvent!.kibana ?? {}, 'server_uuid')).to.eql(expectedEvents[index]!.kibana); + expect(foundEvent!.message).to.eql(expectedEvents[index]!.message); + }); + } catch (ex) { + log.debug(`failed to match ${JSON.stringify({ foundEvents, expectedEvents })}`); + throw ex; + } + } + + async function logTestEvent(id: string, event: IEvent) { + log.debug(`Logging Event for Saved Object ${id}`); + return await supertest + .post(`/api/log_event_fixture/${id}/_log`) + .set('kbn-xsrf', 'foo') + .send(event) + .expect(200); + } + + function fakeEventTiming(start: Moment): Partial { + return { + event: { + start: start.toISOString(), + end: start + .clone() + .add(500, 'milliseconds') + .toISOString(), + }, + }; + } + + function fakeEvent(id: string, overrides: Partial = {}): IEvent { + const start = moment().toISOString(); + const end = moment().toISOString(); + return merge( + { + event: { + provider: 'event_log_fixture', + action: 'test', + start, + end, + duration: 1000000, + }, + kibana: { + namespace: 'default', + saved_objects: [ + { + type: 'event_log_test', + id, + }, + ], + }, + message: `test ${moment().toISOString()}`, + }, + overrides + ); + } +} diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts new file mode 100644 index 000000000000000..b055b22879bf919 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export default function() { + describe('Event Log service API', () => { + it('should allow logging an event', async () => {}); + }); +} From d67f2220b34a887a0c7564d64bd1472de87dcc4e Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 6 Apr 2020 20:22:46 +0300 Subject: [PATCH 12/15] [SIEM][CASE] Configuration page tests (#61093) * Test ClosureOptionsRadio component * Test ClosureOptions component * Test ConnectorsDropdown component * Test Connectors * Test FieldMappingRow * Test FieldMapping * Create utils functions and refactor to be able to test * Test Mapping * Improve tests * Test ConfigureCases * Refactor tests * Fix flacky tests * Remove snapshots * Refactor tests * Test button * Test reducer * Move test * Better structure Co-authored-by: Elastic Machine --- .../configure_cases/__mock__/index.tsx | 122 +++ .../configure_cases/button.test.tsx | 114 +++ .../components/configure_cases/button.tsx | 10 +- .../configure_cases/closure_options.test.tsx | 67 ++ .../configure_cases/closure_options.tsx | 10 +- .../closure_options_radio.test.tsx | 79 ++ .../configure_cases/closure_options_radio.tsx | 3 +- .../configure_cases/connectors.test.tsx | 90 +++ .../components/configure_cases/connectors.tsx | 16 +- .../connectors_dropdown.test.tsx | 86 ++ .../configure_cases/connectors_dropdown.tsx | 7 +- .../configure_cases/field_mapping.test.tsx | 84 ++ .../configure_cases/field_mapping.tsx | 31 +- .../field_mapping_row.test.tsx | 106 +++ .../configure_cases/field_mapping_row.tsx | 4 +- .../components/configure_cases/index.test.tsx | 748 ++++++++++++++++++ .../case/components/configure_cases/index.tsx | 16 +- .../configure_cases/mapping.test.tsx | 65 ++ .../components/configure_cases/mapping.tsx | 13 +- .../configure_cases/reducer.test.ts | 68 ++ .../components/configure_cases/utils.test.tsx | 63 ++ .../case/components/configure_cases/utils.ts | 44 ++ 22 files changed, 1807 insertions(+), 39 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx new file mode 100644 index 000000000000000..a3df3664398ad29 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Connector, + CasesConfigurationMapping, +} from '../../../../../containers/case/configure/types'; +import { State } from '../reducer'; +import { ReturnConnectors } from '../../../../../containers/case/configure/use_connectors'; +import { ReturnUseCaseConfigure } from '../../../../../containers/case/configure/use_configure'; +import { createUseKibanaMock } from '../../../../../mock/kibana_react'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { actionTypeRegistryMock } from '../../../../../../../../../plugins/triggers_actions_ui/public/application/action_type_registry.mock'; + +export const connectors: Connector[] = [ + { + id: '123', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + }, + { + id: '456', + actionTypeId: '.servicenow', + name: 'My Connector 2', + config: { + apiUrl: 'https://instance2.service-now.com', + casesConfiguration: { + mapping: [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ], + }, + }, + }, +]; + +export const mapping: CasesConfigurationMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'description', + target: 'description', + actionType: 'append', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, +]; + +export const searchURL = + '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; + +export const initialState: State = { + connectorId: 'none', + closureType: 'close-by-user', + mapping: null, + currentConfiguration: { connectorId: 'none', closureType: 'close-by-user' }, +}; + +export const useCaseConfigureResponse: ReturnUseCaseConfigure = { + loading: false, + persistLoading: false, + refetchCaseConfigure: jest.fn(), + persistCaseConfigure: jest.fn(), +}; + +export const useConnectorsResponse: ReturnConnectors = { + loading: false, + connectors, + refetchConnectors: jest.fn(), +}; + +export const kibanaMockImplementationArgs = { + services: { + ...createUseKibanaMock()().services, + triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx new file mode 100644 index 000000000000000..cf52fef94ed17e3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ReactWrapper, mount } from 'enzyme'; +import { EuiText } from '@elastic/eui'; + +import { ConfigureCaseButton, ConfigureCaseButtonProps } from './button'; +import { TestProviders } from '../../../../mock'; +import { searchURL } from './__mock__'; + +describe('Configuration button', () => { + let wrapper: ReactWrapper; + const props: ConfigureCaseButtonProps = { + isDisabled: false, + label: 'My label', + msgTooltip: <>, + showToolTip: false, + titleTooltip: '', + urlSearch: searchURL, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders without the tooltip', () => { + expect( + wrapper + .find('[data-test-subj="configure-case-button"]') + .first() + .exists() + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="configure-case-tooltip"]') + .first() + .exists() + ).toBe(false); + }); + + test('it pass the correct props to the button', () => { + expect( + wrapper + .find('[data-test-subj="configure-case-button"]') + .first() + .props() + ).toMatchObject({ + href: `#/link-to/case/configure${searchURL}`, + iconType: 'controlsHorizontal', + isDisabled: false, + 'aria-label': 'My label', + children: 'My label', + }); + }); + + test('it renders the tooltip', () => { + const msgTooltip = {'My message tooltip'}; + + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + + expect( + newWrapper + .find('[data-test-subj="configure-case-tooltip"]') + .first() + .exists() + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="configure-case-button"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the tooltip when hovering the button', () => { + const msgTooltip = 'My message tooltip'; + const titleTooltip = 'My title'; + + const newWrapper = mount( + {msgTooltip}} + />, + { + wrappingComponent: TestProviders, + } + ); + + newWrapper + .find('[data-test-subj="configure-case-button"]') + .first() + .simulate('mouseOver'); + + expect(newWrapper.find('.euiToolTipPopover').text()).toBe(`${titleTooltip}${msgTooltip}`); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx index b0bea83148bda22..844ffea28415f65 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiToolTip } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; import { getConfigureCasesUrl } from '../../../../components/link_to'; -interface ConfigureCaseButtonProps { +export interface ConfigureCaseButtonProps { label: string; isDisabled: boolean; msgTooltip: JSX.Element; @@ -32,6 +32,7 @@ const ConfigureCaseButtonComponent: React.FC = ({ iconType="controlsHorizontal" isDisabled={isDisabled} aria-label={label} + data-test-subj="configure-case-button" > {label} @@ -39,7 +40,12 @@ const ConfigureCaseButtonComponent: React.FC = ({ [label, isDisabled, urlSearch] ); return showToolTip ? ( - {msgTooltip}

}> + {msgTooltip}

} + data-test-subj="configure-case-tooltip" + > {configureCaseButton}
) : ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx new file mode 100644 index 000000000000000..209dce9aedffc57 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { ClosureOptions, ClosureOptionsProps } from './closure_options'; +import { TestProviders } from '../../../../mock'; +import { ClosureOptionsRadio } from './closure_options_radio'; + +describe('ClosureOptions', () => { + let wrapper: ReactWrapper; + const onChangeClosureType = jest.fn(); + const props: ClosureOptionsProps = { + disabled: false, + closureTypeSelected: 'close-by-user', + onChangeClosureType, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows the closure options form group', () => { + expect( + wrapper + .find('[data-test-subj="case-closure-options-form-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the closure options form row', () => { + expect( + wrapper + .find('[data-test-subj="case-closure-options-form-row"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows closure options', () => { + expect( + wrapper + .find('[data-test-subj="case-closure-options-radio"]') + .first() + .exists() + ).toBe(true); + }); + + test('it pass the correct props to child', () => { + const closureOptionsRadioComponent = wrapper.find(ClosureOptionsRadio); + expect(closureOptionsRadioComponent.props().disabled).toEqual(false); + expect(closureOptionsRadioComponent.props().closureTypeSelected).toEqual('close-by-user'); + expect(closureOptionsRadioComponent.props().onChangeClosureType).toEqual(onChangeClosureType); + }); + + test('the closure type is changed successfully', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + + expect(onChangeClosureType).toHaveBeenCalled(); + expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx index 9879b9149059ae9..6fa97818dd0ce38 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx @@ -11,7 +11,7 @@ import { ClosureType } from '../../../../containers/case/configure/types'; import { ClosureOptionsRadio } from './closure_options_radio'; import * as i18n from './translations'; -interface ClosureOptionsProps { +export interface ClosureOptionsProps { closureTypeSelected: ClosureType; disabled: boolean; onChangeClosureType: (newClosureType: ClosureType) => void; @@ -27,12 +27,18 @@ const ClosureOptionsComponent: React.FC = ({ fullWidth title={

{i18n.CASE_CLOSURE_OPTIONS_TITLE}

} description={i18n.CASE_CLOSURE_OPTIONS_DESC} + data-test-subj="case-closure-options-form-group" > - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx new file mode 100644 index 000000000000000..f2ef2c2d55c2880 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ReactWrapper, mount } from 'enzyme'; + +import { ClosureOptionsRadio, ClosureOptionsRadioComponentProps } from './closure_options_radio'; +import { TestProviders } from '../../../../mock'; + +describe('ClosureOptionsRadio', () => { + let wrapper: ReactWrapper; + const onChangeClosureType = jest.fn(); + const props: ClosureOptionsRadioComponentProps = { + disabled: false, + closureTypeSelected: 'close-by-user', + onChangeClosureType, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect( + wrapper + .find('[data-test-subj="closure-options-radio-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the correct number of radio buttons', () => { + expect(wrapper.find('input[name="closure_options"]')).toHaveLength(2); + }); + + test('it renders close by user radio button', () => { + expect(wrapper.find('input[id="close-by-user"]').exists()).toBeTruthy(); + }); + + test('it renders close by pushing radio button', () => { + expect(wrapper.find('input[id="close-by-pushing"]').exists()).toBeTruthy(); + }); + + test('it disables the close by user radio button', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('input[id="close-by-user"]').prop('disabled')).toEqual(true); + }); + + test('it disables correctly the close by pushing radio button', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('input[id="close-by-pushing"]').prop('disabled')).toEqual(true); + }); + + test('it selects the correct radio button', () => { + const newWrapper = mount( + , + { + wrappingComponent: TestProviders, + } + ); + expect(newWrapper.find('input[id="close-by-pushing"]').prop('checked')).toEqual(true); + }); + + test('it calls the onChangeClosureType function', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + expect(onChangeClosureType).toHaveBeenCalled(); + expect(onChangeClosureType).toHaveBeenCalledWith('close-by-pushing'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx index f32f867b2471d0f..d2cdb7ecda7ba41 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx @@ -26,7 +26,7 @@ const radios: ClosureRadios[] = [ }, ]; -interface ClosureOptionsRadioComponentProps { +export interface ClosureOptionsRadioComponentProps { closureTypeSelected: ClosureType; disabled: boolean; onChangeClosureType: (newClosureType: ClosureType) => void; @@ -51,6 +51,7 @@ const ClosureOptionsRadioComponent: React.FC idSelected={closureTypeSelected} onChange={onChangeLocal} name="closure_options" + data-test-subj="closure-options-radio-group" /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx new file mode 100644 index 000000000000000..5fb52c374b482b4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { Connectors, Props } from './connectors'; +import { TestProviders } from '../../../../mock'; +import { ConnectorsDropdown } from './connectors_dropdown'; +import { connectors } from './__mock__'; + +describe('Connectors', () => { + let wrapper: ReactWrapper; + const onChangeConnector = jest.fn(); + const handleShowAddFlyout = jest.fn(); + const props: Props = { + disabled: false, + connectors, + selectedConnector: 'none', + isLoading: false, + onChangeConnector, + handleShowAddFlyout, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows the connectors from group', () => { + expect( + wrapper + .find('[data-test-subj="case-connectors-form-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the connectors form row', () => { + expect( + wrapper + .find('[data-test-subj="case-connectors-form-row"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the connectors dropdown', () => { + expect( + wrapper + .find('[data-test-subj="case-connectors-dropdown"]') + .first() + .exists() + ).toBe(true); + }); + + test('it pass the correct props to child', () => { + const connectorsDropdownProps = wrapper.find(ConnectorsDropdown).props(); + expect(connectorsDropdownProps).toMatchObject({ + disabled: false, + isLoading: false, + connectors, + selectedConnector: 'none', + onChange: props.onChangeConnector, + }); + }); + + test('the connector is changed successfully', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + + expect(onChangeConnector).toHaveBeenCalled(); + expect(onChangeConnector).toHaveBeenCalledWith('456'); + }); + + test('the connector is changed successfully to none', () => { + onChangeConnector.mockClear(); + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + newWrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + newWrapper.find('button[data-test-subj="dropdown-connector-no-connector"]').simulate('click'); + + expect(onChangeConnector).toHaveBeenCalled(); + expect(onChangeConnector).toHaveBeenCalledWith('none'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx index 8fb1cfb1aa6cc48..de6d5f76cfad070 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx @@ -28,7 +28,7 @@ const EuiFormRowExtended = styled(EuiFormRow)` } `; -interface Props { +export interface Props { connectors: Connector[]; disabled: boolean; isLoading: boolean; @@ -48,7 +48,11 @@ const ConnectorsComponent: React.FC = ({ {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} - + {i18n.ADD_NEW_CONNECTOR} @@ -61,14 +65,20 @@ const ConnectorsComponent: React.FC = ({ fullWidth title={

{i18n.INCIDENT_MANAGEMENT_SYSTEM_TITLE}

} description={i18n.INCIDENT_MANAGEMENT_SYSTEM_DESC} + data-test-subj="case-connectors-form-group" > - + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx new file mode 100644 index 000000000000000..044108962efc7bd --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { EuiSuperSelect } from '@elastic/eui'; + +import { ConnectorsDropdown, Props } from './connectors_dropdown'; +import { TestProviders } from '../../../../mock'; +import { connectors } from './__mock__'; + +describe('ConnectorsDropdown', () => { + let wrapper: ReactWrapper; + const props: Props = { + disabled: false, + connectors, + isLoading: false, + onChange: jest.fn(), + selectedConnector: 'none', + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect( + wrapper + .find('[data-test-subj="dropdown-connectors"]') + .first() + .exists() + ).toBe(true); + }); + + test('it formats the connectors correctly', () => { + const selectProps = wrapper.find(EuiSuperSelect).props(); + + expect(selectProps.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: 'none', + 'data-test-subj': 'dropdown-connector-no-connector', + }), + expect.objectContaining({ value: '123', 'data-test-subj': 'dropdown-connector-123' }), + expect.objectContaining({ value: '456', 'data-test-subj': 'dropdown-connector-456' }), + ]) + ); + }); + + test('it disables the dropdown', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="dropdown-connectors"]') + .first() + .prop('disabled') + ).toEqual(true); + }); + + test('it loading correctly', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="dropdown-connectors"]') + .first() + .prop('isLoading') + ).toEqual(true); + }); + + test('it selects the correct connector', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('button span').text()).toEqual('My Connector'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx index a0a0ad6cd3e7ff0..15066e73eee829f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx @@ -12,7 +12,7 @@ import { Connector } from '../../../../containers/case/configure/types'; import { connectors as connectorsDefinition } from '../../../../lib/connectors/config'; import * as i18n from './translations'; -interface Props { +export interface Props { connectors: Connector[]; disabled: boolean; isLoading: boolean; @@ -34,7 +34,7 @@ const noConnectorOption = { {i18n.NO_CONNECTOR} ), - 'data-test-subj': 'no-connector', + 'data-test-subj': 'dropdown-connector-no-connector', }; const ConnectorsDropdownComponent: React.FC = ({ @@ -60,7 +60,7 @@ const ConnectorsDropdownComponent: React.FC = ({ {connector.name} ), - 'data-test-subj': connector.id, + 'data-test-subj': `dropdown-connector-${connector.id}`, }, ], [noConnectorOption] @@ -76,6 +76,7 @@ const ConnectorsDropdownComponent: React.FC = ({ valueOfSelected={selectedConnector} fullWidth onChange={onChange} + data-test-subj="dropdown-connectors" /> ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx new file mode 100644 index 000000000000000..9ab752bb589c0a8 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { FieldMapping, FieldMappingProps } from './field_mapping'; +import { mapping } from './__mock__'; +import { FieldMappingRow } from './field_mapping_row'; +import { defaultMapping } from '../../../../lib/connectors/config'; +import { TestProviders } from '../../../../mock'; + +describe('FieldMappingRow', () => { + let wrapper: ReactWrapper; + const onChangeMapping = jest.fn(); + const props: FieldMappingProps = { + disabled: false, + mapping, + onChangeMapping, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect( + wrapper + .find('[data-test-subj="case-configure-field-mapping-cols"]') + .first() + .exists() + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="case-configure-field-mapping-row-wrapper"]') + .first() + .exists() + ).toBe(true); + + expect(wrapper.find(FieldMappingRow).length).toEqual(3); + }); + + test('it shows the correct number of FieldMappingRow with default mapping', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find(FieldMappingRow).length).toEqual(3); + }); + + test('it pass the corrects props to mapping row', () => { + const rows = wrapper.find(FieldMappingRow); + rows.forEach((row, index) => { + expect(row.prop('siemField')).toEqual(mapping[index].source); + expect(row.prop('selectedActionType')).toEqual(mapping[index].actionType); + expect(row.prop('selectedThirdParty')).toEqual(mapping[index].target); + }); + }); + + test('it pass the default mapping when mapping is null', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + const rows = newWrapper.find(FieldMappingRow); + rows.forEach((row, index) => { + expect(row.prop('siemField')).toEqual(defaultMapping[index].source); + expect(row.prop('selectedActionType')).toEqual(defaultMapping[index].actionType); + expect(row.prop('selectedThirdParty')).toEqual(defaultMapping[index].target); + }); + }); + + test('it should show zero rows on empty array', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find(FieldMappingRow).length).toEqual(0); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx index 0c0dc14f1c218da..2934b1056e29cd7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx @@ -18,6 +18,7 @@ import { FieldMappingRow } from './field_mapping_row'; import * as i18n from './translations'; import { defaultMapping } from '../../../../lib/connectors/config'; +import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; const FieldRowWrapper = styled.div` margin-top: 8px; @@ -28,22 +29,26 @@ const supportedThirdPartyFields: Array> = { value: 'not_mapped', inputDisplay: {i18n.FIELD_MAPPING_FIELD_NOT_MAPPED}, + 'data-test-subj': 'third-party-field-not-mapped', }, { value: 'short_description', inputDisplay: {i18n.FIELD_MAPPING_FIELD_SHORT_DESC}, + 'data-test-subj': 'third-party-field-short-description', }, { value: 'comments', inputDisplay: {i18n.FIELD_MAPPING_FIELD_COMMENTS}, + 'data-test-subj': 'third-party-field-comments', }, { value: 'description', inputDisplay: {i18n.FIELD_MAPPING_FIELD_DESC}, + 'data-test-subj': 'third-party-field-description', }, ]; -interface FieldMappingProps { +export interface FieldMappingProps { disabled: boolean; mapping: CasesConfigurationMapping[] | null; onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; @@ -57,14 +62,7 @@ const FieldMappingComponent: React.FC = ({ const onChangeActionType = useCallback( (caseField: CaseField, newActionType: ActionType) => { const myMapping = mapping ?? defaultMapping; - const findItemIndex = myMapping.findIndex(item => item.source === caseField); - if (findItemIndex >= 0) { - onChangeMapping([ - ...myMapping.slice(0, findItemIndex), - { ...myMapping[findItemIndex], actionType: newActionType }, - ...myMapping.slice(findItemIndex + 1), - ]); - } + onChangeMapping(setActionTypeToMapping(caseField, newActionType, myMapping)); }, [mapping] ); @@ -72,22 +70,13 @@ const FieldMappingComponent: React.FC = ({ const onChangeThirdParty = useCallback( (caseField: CaseField, newThirdPartyField: ThirdPartyField) => { const myMapping = mapping ?? defaultMapping; - onChangeMapping( - myMapping.map(item => { - if (item.source !== caseField && item.target === newThirdPartyField) { - return { ...item, target: 'not_mapped' }; - } else if (item.source === caseField) { - return { ...item, target: newThirdPartyField }; - } - return item; - }) - ); + onChangeMapping(setThirdPartyToMapping(caseField, newThirdPartyField, myMapping)); }, [mapping] ); return ( <> - + {i18n.FIELD_MAPPING_FIRST_COL} @@ -100,7 +89,7 @@ const FieldMappingComponent: React.FC = ({ - + {(mapping ?? defaultMapping).map(item => ( > = [ + { + value: 'short_description', + inputDisplay: {'Short Description'}, + 'data-test-subj': 'third-party-short-desc', + }, + { + value: 'description', + inputDisplay: {'Description'}, + 'data-test-subj': 'third-party-desc', + }, +]; + +describe('FieldMappingRow', () => { + let wrapper: ReactWrapper; + const onChangeActionType = jest.fn(); + const onChangeThirdParty = jest.fn(); + + const props: RowProps = { + disabled: false, + siemField: 'title', + thirdPartyOptions, + onChangeActionType, + onChangeThirdParty, + selectedActionType: 'nothing', + selectedThirdParty: 'short_description', + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders', () => { + expect( + wrapper + .find('[data-test-subj="case-configure-third-party-select"]') + .first() + .exists() + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-type-select"]') + .first() + .exists() + ).toBe(true); + }); + + test('it passes thirdPartyOptions correctly', () => { + const selectProps = wrapper + .find(EuiSuperSelect) + .first() + .props(); + + expect(selectProps.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: 'short_description', + 'data-test-subj': 'third-party-short-desc', + }), + expect.objectContaining({ + value: 'description', + 'data-test-subj': 'third-party-desc', + }), + ]) + ); + }); + + test('it passes the correct actionTypeOptions', () => { + const selectProps = wrapper + .find(EuiSuperSelect) + .at(1) + .props(); + + expect(selectProps.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: 'nothing', + 'data-test-subj': 'edit-update-option-nothing', + }), + expect.objectContaining({ + value: 'overwrite', + 'data-test-subj': 'edit-update-option-overwrite', + }), + expect.objectContaining({ + value: 'append', + 'data-test-subj': 'edit-update-option-append', + }), + ]) + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx index 62e43c86af8d9e7..732a11a58d35a71 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx @@ -21,7 +21,7 @@ import { ThirdPartyField, } from '../../../../containers/case/configure/types'; -interface RowProps { +export interface RowProps { disabled: boolean; siemField: CaseField; thirdPartyOptions: Array>; @@ -77,6 +77,7 @@ const FieldMappingRowComponent: React.FC = ({ options={thirdPartyOptions} valueOfSelected={selectedThirdParty} onChange={onChangeThirdParty.bind(null, siemField)} + data-test-subj={'case-configure-third-party-select'} /> @@ -85,6 +86,7 @@ const FieldMappingRowComponent: React.FC = ({ options={actionTypeOptions} valueOfSelected={selectedActionType} onChange={onChangeActionType.bind(null, siemField)} + data-test-subj={'case-configure-action-type-select'} />
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx new file mode 100644 index 000000000000000..5ea3f500c0349f5 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx @@ -0,0 +1,748 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useEffect } from 'react'; +import { ReactWrapper, mount } from 'enzyme'; + +import { useKibana } from '../../../../lib/kibana'; +import { useConnectors } from '../../../../containers/case/configure/use_connectors'; +import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; +import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; + +import { + connectors, + searchURL, + useCaseConfigureResponse, + useConnectorsResponse, + kibanaMockImplementationArgs, +} from './__mock__'; + +jest.mock('../../../../lib/kibana'); +jest.mock('../../../../containers/case/configure/use_connectors'); +jest.mock('../../../../containers/case/configure/use_configure'); +jest.mock('../../../../components/navigation/use_get_url_search'); + +const useKibanaMock = useKibana as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; +const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; + +import { ConfigureCases } from './'; +import { TestProviders } from '../../../../mock'; +import { Connectors } from './connectors'; +import { ClosureOptions } from './closure_options'; +import { Mapping } from './mapping'; +import { + ActionsConnectorsContextProvider, + ConnectorAddFlyout, + ConnectorEditFlyout, +} from '../../../../../../../../plugins/triggers_actions_ui/public'; +import { EuiBottomBar } from '@elastic/eui'; + +describe('rendering', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders the Connectors', () => { + expect(wrapper.find('[data-test-subj="case-connectors-form-group"]').exists()).toBeTruthy(); + }); + + test('it renders the ClosureType', () => { + expect( + wrapper.find('[data-test-subj="case-closure-options-form-group"]').exists() + ).toBeTruthy(); + }); + + test('it renders the Mapping', () => { + expect(wrapper.find('[data-test-subj="case-mapping-form-group"]').exists()).toBeTruthy(); + }); + + test('it renders the ActionsConnectorsContextProvider', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ActionsConnectorsContextProvider).exists()).toBeTruthy(); + }); + + test('it renders the ConnectorAddFlyout', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ConnectorAddFlyout).exists()).toBeTruthy(); + }); + + test('it does NOT render the ConnectorEditFlyout', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); + }); + + test('it does NOT render the EuiCallOut', () => { + expect(wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists()).toBeFalsy(); + }); + + test('it does NOT render the EuiBottomBar', () => { + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); +}); + +describe('ConfigureCases - Unhappy path', () => { + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + }); + + test('it shows the warning callout when configuration is invalid', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('not-id'), []); + return useCaseConfigureResponse; + } + ); + + const wrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() + ).toBeTruthy(); + }); +}); + +describe('ConfigureCases - Happy path', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('123'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return useCaseConfigureResponse; + } + ); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders the ConnectorEditFlyout', () => { + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeTruthy(); + }); + + test('it renders with correct props', () => { + // Connector + expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); + expect(wrapper.find(Connectors).prop('disabled')).toBe(false); + expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); + expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('123'); + + // ClosureOptions + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); + expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); + + // Mapping + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + expect(wrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(false); + expect(wrapper.find(Mapping).prop('mapping')).toEqual( + connectors[0].config.casesConfiguration.mapping + ); + + // Flyouts + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); + expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ + { + id: '.servicenow', + name: 'ServiceNow', + enabled: true, + enabledInConfig: true, + enabledInLicense: true, + minimumLicenseRequired: 'platinum', + }, + ]); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); + expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]); + }); + + test('it disables correctly when the user cannot crud', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + expect(newWrapper.find(Mapping).prop('updateConnectorDisabled')).toBe(true); + }); + + test('it disables correctly Connector when loading connectors', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); + }); + + test('it disables correctly Connector when saving configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + persistLoading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Connectors).prop('disabled')).toBe(true); + }); + + test('it pass the correct value to isLoading attribute on Connector', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Connectors).prop('isLoading')).toBe(true); + }); + + test('it set correctly the selected connector', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Connectors).prop('selectedConnector')).toBe('456'); + }); + + test('it show the add flyout when pressing the add connector button', () => { + wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); + }); + + test('it disables correctly ClosureOptions when loading connectors', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it disables correctly ClosureOptions when saving configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + persistLoading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it disables correctly ClosureOptions when the connector is set to none', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('none'), []); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it disables the mapping permanently', () => { + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when loading the connectors', () => { + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + expect(wrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when loading the configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when saving the configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + persistLoading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when the connectorId is invalid', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('not-id'), []); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it disables the update connector button when the connectorId is set to none', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('none'), []); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect(newWrapper.find(Mapping).prop('disabled')).toBe(true); + }); + + test('it show the edit flyout when pressing the update connector button', () => { + wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); + expect(wrapper.find(EuiBottomBar).exists()).toBeFalsy(); + }); + + test('it sets the mapping of a connector correctly', () => { + expect(wrapper.find(Mapping).prop('mapping')).toEqual( + connectors[0].config.casesConfiguration.mapping + ); + }); + + // TODO: When mapping is enabled the test.todo should be implemented. + test.todo('the mapping is changed successfully when changing the third party'); + test.todo('the mapping is changed successfully when changing the action type'); + + test('it does not shows the action bar when there is no change', () => { + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it shows the action bar when the connector is changed', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it shows the action bar when the closure type is changed', () => { + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it tracks the changes successfully', () => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('2 unsaved changes'); + }); + + test('it tracks and reverts the changes successfully ', () => { + // change settings + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-456"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // revert back to initial settings + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-123"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-user"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it close and restores the action bar when the add connector button is pressed', () => { + // Change closure type + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // Press add connector button + wrapper.find('button[data-test-subj="case-configure-add-connector-button"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + + // Close the add flyout + wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it close and restores the action bar when the update connector button is pressed', () => { + // Change closure type + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // Press update connector button + wrapper.find('button[data-test-subj="case-mapping-update-connector-button"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); + + // Close the edit flyout + wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it disables the buttons of action bar when loading connectors', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return useCaseConfigureResponse; + } + ); + + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + test('it disables the buttons of action bar when loading configuration', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return { ...useCaseConfigureResponse, loading: true }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + test('it disables the buttons of action bar when saving configuration', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return { ...useCaseConfigureResponse, persistLoading: true }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + test('it shows the loading spinner when saving configuration', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return { ...useCaseConfigureResponse, persistLoading: true }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isLoading') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isLoading') + ).toBe(true); + }); + + test('it closes the action bar when pressing save', () => { + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return useCaseConfigureResponse; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .simulate('click'); + + newWrapper.update(); + + expect( + newWrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it submits the configuration correctly', () => { + const persistCaseConfigure = jest.fn(); + + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-pushing', + }), + [] + ); + return { ...useCaseConfigureResponse, persistCaseConfigure }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .simulate('click'); + + newWrapper.update(); + + expect(persistCaseConfigure).toHaveBeenCalled(); + expect(persistCaseConfigure).toHaveBeenCalledWith({ + connectorId: '456', + connectorName: 'My Connector 2', + closureType: 'close-by-user', + }); + }); + + test('it has the correct url on cancel button', () => { + const persistCaseConfigure = jest.fn(); + + useCaseConfigureMock.mockImplementation( + ({ setConnector, setClosureType, setCurrentConfiguration }) => { + useEffect(() => setConnector('456'), []); + useEffect(() => setClosureType('close-by-user'), []); + useEffect( + () => + setCurrentConfiguration({ + connectorId: '123', + closureType: 'close-by-user', + }), + [] + ); + return { ...useCaseConfigureResponse, persistCaseConfigure }; + } + ); + + const newWrapper = mount(, { wrappingComponent: TestProviders }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('href') + ).toBe(`#/link-to/case${searchURL}`); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx index b8cf5a388080119..241dcef14a145d7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/index.tsx @@ -140,6 +140,7 @@ const ConfigureCasesComponent: React.FC = ({ userC setClosureType, setCurrentConfiguration, }); + const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise. @@ -251,7 +252,12 @@ const ConfigureCasesComponent: React.FC = ({ userC {!connectorIsValid && ( - + {i18n.WARNING_NO_CONNECTOR_MESSAGE} @@ -283,11 +289,13 @@ const ConfigureCasesComponent: React.FC = ({ userC /> {actionBarVisible && ( - + - {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} + + {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} + @@ -300,6 +308,7 @@ const ConfigureCasesComponent: React.FC = ({ userC isLoading={persistLoading} aria-label={i18n.CANCEL} href={getCaseUrl(search)} + data-test-subj="case-configure-action-bottom-bar-cancel-button" > {i18n.CANCEL} @@ -313,6 +322,7 @@ const ConfigureCasesComponent: React.FC = ({ userC isDisabled={isLoadingAny} isLoading={persistLoading} onClick={handleSubmit} + data-test-subj="case-configure-action-bottom-bar-save-button" > {i18n.SAVE_CHANGES} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx new file mode 100644 index 000000000000000..fefcb2ca8cf6a39 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; + +import { TestProviders } from '../../../../mock'; +import { Mapping, MappingProps } from './mapping'; +import { mapping } from './__mock__'; + +describe('Mapping', () => { + let wrapper: ReactWrapper; + const onChangeMapping = jest.fn(); + const setEditFlyoutVisibility = jest.fn(); + const props: MappingProps = { + disabled: false, + mapping, + updateConnectorDisabled: false, + onChangeMapping, + setEditFlyoutVisibility, + }; + + beforeAll(() => { + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows mapping form group', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-form-group"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows mapping form row', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-form-row"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the update button', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-update-connector-button"]') + .first() + .exists() + ).toBe(true); + }); + + test('it shows the field mapping', () => { + expect( + wrapper + .find('[data-test-subj="case-mapping-field"]') + .first() + .exists() + ).toBe(true); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx index 8cba73d1249df1f..7340a49f6d0bbcf 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx @@ -20,7 +20,7 @@ import * as i18n from './translations'; import { FieldMapping } from './field_mapping'; import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; -interface MappingProps { +export interface MappingProps { disabled: boolean; updateConnectorDisabled: boolean; mapping: CasesConfigurationMapping[] | null; @@ -45,20 +45,27 @@ const MappingComponent: React.FC = ({ fullWidth title={

{i18n.FIELD_MAPPING_TITLE}

} description={i18n.FIELD_MAPPING_DESC} + data-test-subj="case-mapping-form-group" > - + {i18n.UPDATE_CONNECTOR} - + ); }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts new file mode 100644 index 000000000000000..df958b75dc6b890 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/reducer.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { configureCasesReducer, Action, State } from './reducer'; +import { initialState, mapping } from './__mock__'; + +describe('Reducer', () => { + let reducer: (state: State, action: Action) => State; + + beforeAll(() => { + reducer = configureCasesReducer(); + }); + + test('it should set the correct configuration', () => { + const action: Action = { + type: 'setCurrentConfiguration', + currentConfiguration: { connectorId: '123', closureType: 'close-by-user' }, + }; + const state = reducer(initialState, action); + + expect(state).toEqual({ + ...state, + currentConfiguration: action.currentConfiguration, + }); + }); + + test('it should set the correct connector id', () => { + const action: Action = { + type: 'setConnectorId', + connectorId: '456', + }; + const state = reducer(initialState, action); + + expect(state).toEqual({ + ...state, + connectorId: action.connectorId, + }); + }); + + test('it should set the closure type', () => { + const action: Action = { + type: 'setClosureType', + closureType: 'close-by-pushing', + }; + const state = reducer(initialState, action); + + expect(state).toEqual({ + ...state, + closureType: action.closureType, + }); + }); + + test('it should set the mapping', () => { + const action: Action = { + type: 'setMapping', + mapping, + }; + const state = reducer(initialState, action); + + expect(state).toEqual({ + ...state, + mapping: action.mapping, + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx new file mode 100644 index 000000000000000..1c6fc9b2d405f9a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mapping } from './__mock__'; +import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; +import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; + +describe('FieldMappingRow', () => { + test('it should change the action type', () => { + const newMapping = setActionTypeToMapping('title', 'nothing', mapping); + expect(newMapping[0].actionType).toBe('nothing'); + }); + + test('it should not change other fields', () => { + const [newTitle, description, comments] = setActionTypeToMapping('title', 'nothing', mapping); + expect(newTitle).not.toEqual(mapping[0]); + expect(description).toEqual(mapping[1]); + expect(comments).toEqual(mapping[2]); + }); + + test('it should return a new array when changing action type', () => { + const newMapping = setActionTypeToMapping('title', 'nothing', mapping); + expect(newMapping).not.toBe(mapping); + }); + + test('it should change the third party', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mapping); + expect(newMapping[0].target).toBe('description'); + }); + + test('it should not change other fields when there is not a conflict', () => { + const tempMapping: CasesConfigurationMapping[] = [ + { + source: 'title', + target: 'short_description', + actionType: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + actionType: 'append', + }, + ]; + + const [newTitle, comments] = setThirdPartyToMapping('title', 'description', tempMapping); + + expect(newTitle).not.toEqual(mapping[0]); + expect(comments).toEqual(tempMapping[1]); + }); + + test('it should return a new array when changing third party', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mapping); + expect(newMapping).not.toBe(mapping); + }); + + test('it should change the target of the conflicting third party field to not_mapped', () => { + const newMapping = setThirdPartyToMapping('title', 'description', mapping); + expect(newMapping[1].target).toBe('not_mapped'); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts new file mode 100644 index 000000000000000..2ac6cc1a385872a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/utils.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CaseField, + ActionType, + CasesConfigurationMapping, + ThirdPartyField, +} from '../../../../containers/case/configure/types'; + +export const setActionTypeToMapping = ( + caseField: CaseField, + newActionType: ActionType, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => { + const findItemIndex = mapping.findIndex(item => item.source === caseField); + + if (findItemIndex >= 0) { + return [ + ...mapping.slice(0, findItemIndex), + { ...mapping[findItemIndex], actionType: newActionType }, + ...mapping.slice(findItemIndex + 1), + ]; + } + + return [...mapping]; +}; + +export const setThirdPartyToMapping = ( + caseField: CaseField, + newThirdPartyField: ThirdPartyField, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => + mapping.map(item => { + if (item.source !== caseField && item.target === newThirdPartyField) { + return { ...item, target: 'not_mapped' }; + } else if (item.source === caseField) { + return { ...item, target: newThirdPartyField }; + } + return item; + }); From 0ebfe76b3fa0cb104c6accf8469fe390ba239b40 Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Mon, 6 Apr 2020 19:26:40 +0200 Subject: [PATCH 13/15] [SIEM][Detection Engine] Fix signals count in Rule notifications (#62311) --- .../notifications/get_signals_count.ts | 53 +-- .../rules_notification_alert_type.ts | 22 +- .../schedule_notification_actions.ts | 4 +- .../detection_engine/notifications/utils.ts | 13 +- .../signals/search_after_bulk_create.test.ts | 64 ++- .../signals/search_after_bulk_create.ts | 13 +- .../signals/signal_rule_alert_type.test.ts | 399 ++++++++++++++++++ .../signals/signal_rule_alert_type.ts | 31 +- .../signals/single_bulk_create.test.ts | 40 +- .../signals/single_bulk_create.ts | 8 +- 10 files changed, 567 insertions(+), 80 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts index 33cee6d074b7069..7ff6a4e5164bd74 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/get_signals_count.ts @@ -4,63 +4,40 @@ * you may not use this file except in compliance with the Elastic License. */ -import moment from 'moment'; -import { getNotificationResultsLink } from './utils'; -import { NotificationExecutorOptions } from './types'; -import { parseScheduleDates } from '../signals/utils'; +import { AlertServices } from '../../../../../../../plugins/alerting/server'; import { buildSignalsSearchQuery } from './build_signals_query'; -interface SignalsCountResults { - signalsCount: string; - resultsLink: string; -} - interface GetSignalsCount { - from: Date | string; - to: Date | string; - ruleAlertId: string; + from?: string; + to?: string; ruleId: string; index: string; - kibanaSiemAppUrl: string | undefined; - callCluster: NotificationExecutorOptions['services']['callCluster']; + callCluster: AlertServices['callCluster']; +} + +interface CountResult { + count: number; } export const getSignalsCount = async ({ from, to, - ruleAlertId, ruleId, index, callCluster, - kibanaSiemAppUrl = '', -}: GetSignalsCount): Promise => { - const fromMoment = moment.isDate(from) ? moment(from) : parseScheduleDates(from); - const toMoment = moment.isDate(to) ? moment(to) : parseScheduleDates(to); - - if (!fromMoment || !toMoment) { - throw new Error(`There was an issue with parsing ${from} or ${to} into Moment object`); +}: GetSignalsCount): Promise => { + if (from == null || to == null) { + throw Error('"from" or "to" was not provided to signals count query'); } - const fromInMs = fromMoment.format('x'); - const toInMs = toMoment.format('x'); - const query = buildSignalsSearchQuery({ index, ruleId, - to: toInMs, - from: fromInMs, + to, + from, }); - const result = await callCluster('count', query); - const resultsLink = getNotificationResultsLink({ - kibanaSiemAppUrl: `${kibanaSiemAppUrl}`, - id: ruleAlertId, - from: fromInMs, - to: toInMs, - }); + const result: CountResult = await callCluster('count', query); - return { - signalsCount: result.count, - resultsLink, - }; + return result.count; }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts index e74da583e919325..546488caa5ee78b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/rules_notification_alert_type.ts @@ -13,6 +13,8 @@ import { getSignalsCount } from './get_signals_count'; import { RuleAlertAttributes } from '../signals/types'; import { siemRuleActionGroups } from '../signals/siem_rule_action_groups'; import { scheduleNotificationActions } from './schedule_notification_actions'; +import { getNotificationResultsLink } from './utils'; +import { parseScheduleDates } from '../signals/utils'; export const rulesNotificationAlertType = ({ logger, @@ -42,16 +44,26 @@ export const rulesNotificationAlertType = ({ const { params: ruleAlertParams, name: ruleName } = ruleAlertSavedObject.attributes; const ruleParams = { ...ruleAlertParams, name: ruleName, id: ruleAlertSavedObject.id }; - const { signalsCount, resultsLink } = await getSignalsCount({ - from: previousStartedAt ?? `now-${ruleParams.interval}`, - to: startedAt, + const fromInMs = parseScheduleDates( + previousStartedAt ? previousStartedAt.toISOString() : `now-${ruleParams.interval}` + )?.format('x'); + const toInMs = parseScheduleDates(startedAt.toISOString())?.format('x'); + + const signalsCount = await getSignalsCount({ + from: fromInMs, + to: toInMs, index: ruleParams.outputIndex, ruleId: ruleParams.ruleId!, - kibanaSiemAppUrl: ruleAlertParams.meta?.kibanaSiemAppUrl as string, - ruleAlertId: ruleAlertSavedObject.id, callCluster: services.callCluster, }); + const resultsLink = getNotificationResultsLink({ + from: fromInMs, + to: toInMs, + id: ruleAlertSavedObject.id, + kibanaSiemAppUrl: ruleAlertParams.meta?.kibanaSiemAppUrl as string, + }); + logger.info( `Found ${signalsCount} signals using signal rule name: "${ruleParams.name}", id: "${params.ruleAlertId}", rule_id: "${ruleParams.ruleId}" in "${ruleParams.outputIndex}" index` ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts index b858b25377ffe91..749b892ef506f3b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/schedule_notification_actions.ts @@ -15,7 +15,7 @@ type NotificationRuleTypeParams = RuleTypeParams & { interface ScheduleNotificationActions { alertInstance: AlertInstance; - signalsCount: string; + signalsCount: number; resultsLink: string; ruleParams: NotificationRuleTypeParams; } @@ -23,7 +23,7 @@ interface ScheduleNotificationActions { export const scheduleNotificationActions = ({ alertInstance, signalsCount, - resultsLink, + resultsLink = '', ruleParams, }: ScheduleNotificationActions): AlertInstance => alertInstance diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts index b8a3c4199c4f0c6..5dc7e7fc30b7fa6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/utils.ts @@ -5,14 +5,17 @@ */ export const getNotificationResultsLink = ({ - kibanaSiemAppUrl, + kibanaSiemAppUrl = '/app/siem', id, from, to, }: { kibanaSiemAppUrl: string; id: string; - from: string; - to: string; -}) => - `${kibanaSiemAppUrl}#/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`; + from?: string; + to?: string; +}) => { + if (from == null || to == null) return ''; + + return `${kibanaSiemAppUrl}#/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 06652028b374109..414270ffcdd5c8c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -34,7 +34,7 @@ describe('searchAfterAndBulkCreate', () => { test('if successful with empty search results', async () => { const sampleParams = sampleRuleAlertParams(); - const { success } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: sampleEmptyDocSearchResults(), ruleParams: sampleParams, services: mockService, @@ -57,6 +57,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(success).toEqual(true); + expect(createdSignalsCount).toEqual(0); }); test('if successful iteration of while loop with maxDocs', async () => { @@ -70,6 +71,11 @@ describe('searchAfterAndBulkCreate', () => { { fakeItemValue: 'fakeItemKey', }, + { + create: { + status: 201, + }, + }, ], }) .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(0, 3))) @@ -80,6 +86,11 @@ describe('searchAfterAndBulkCreate', () => { { fakeItemValue: 'fakeItemKey', }, + { + create: { + status: 201, + }, + }, ], }) .mockReturnValueOnce(repeatedSearchResultsWithSortId(3, 1, someGuids.slice(3, 6))) @@ -90,9 +101,14 @@ describe('searchAfterAndBulkCreate', () => { { fakeItemValue: 'fakeItemKey', }, + { + create: { + status: 201, + }, + }, ], }); - const { success } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(3, 1, someGuids.slice(6, 9)), ruleParams: sampleParams, services: mockService, @@ -115,13 +131,14 @@ describe('searchAfterAndBulkCreate', () => { }); expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(success).toEqual(true); + expect(createdSignalsCount).toEqual(3); }); test('if unsuccessful first bulk create', async () => { const someGuids = Array.from({ length: 4 }).map(x => uuid.v4()); const sampleParams = sampleRuleAlertParams(10); mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); - const { success } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -144,6 +161,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); + expect(createdSignalsCount).toEqual(1); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids', async () => { @@ -155,9 +173,14 @@ describe('searchAfterAndBulkCreate', () => { { fakeItemValue: 'fakeItemKey', }, + { + create: { + status: 201, + }, + }, ], }); - const { success } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, services: mockService, @@ -180,6 +203,7 @@ describe('searchAfterAndBulkCreate', () => { }); expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); + expect(createdSignalsCount).toEqual(1); }); test('if unsuccessful iteration of searchAfterAndBulkCreate due to empty sort ids and 0 total hits', async () => { @@ -191,9 +215,14 @@ describe('searchAfterAndBulkCreate', () => { { fakeItemValue: 'fakeItemKey', }, + { + create: { + status: 201, + }, + }, ], }); - const { success } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: sampleDocSearchResultsNoSortIdNoHits(), ruleParams: sampleParams, services: mockService, @@ -215,6 +244,7 @@ describe('searchAfterAndBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(true); + expect(createdSignalsCount).toEqual(1); }); test('if successful iteration of while loop with maxDocs and search after returns results with no sort ids', async () => { @@ -228,10 +258,15 @@ describe('searchAfterAndBulkCreate', () => { { fakeItemValue: 'fakeItemKey', }, + { + create: { + status: 201, + }, + }, ], }) .mockReturnValueOnce(sampleDocSearchResultsNoSortId()); - const { success } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -253,6 +288,7 @@ describe('searchAfterAndBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(true); + expect(createdSignalsCount).toEqual(1); }); test('if successful iteration of while loop with maxDocs and search after returns empty results with no sort ids', async () => { @@ -266,10 +302,15 @@ describe('searchAfterAndBulkCreate', () => { { fakeItemValue: 'fakeItemKey', }, + { + create: { + status: 201, + }, + }, ], }) .mockReturnValueOnce(sampleEmptyDocSearchResults()); - const { success } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -291,6 +332,7 @@ describe('searchAfterAndBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(true); + expect(createdSignalsCount).toEqual(1); }); test('if returns false when singleSearchAfter throws an exception', async () => { @@ -304,12 +346,17 @@ describe('searchAfterAndBulkCreate', () => { { fakeItemValue: 'fakeItemKey', }, + { + create: { + status: 201, + }, + }, ], }) .mockImplementation(() => { throw Error('Fake Error'); }); - const { success } = await searchAfterAndBulkCreate({ + const { success, createdSignalsCount } = await searchAfterAndBulkCreate({ someResult: repeatedSearchResultsWithSortId(4, 1, someGuids), ruleParams: sampleParams, services: mockService, @@ -331,5 +378,6 @@ describe('searchAfterAndBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(false); + expect(createdSignalsCount).toEqual(1); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index a5d5dd0a7b71096..ff81730bc4a72bf 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -39,6 +39,7 @@ export interface SearchAfterAndBulkCreateReturnType { searchAfterTimes: string[]; bulkCreateTimes: string[]; lastLookBackDate: Date | null | undefined; + createdSignalsCount: number; } // search_after through documents and re-index using bulk endpoint. @@ -68,6 +69,7 @@ export const searchAfterAndBulkCreate = async ({ searchAfterTimes: [], bulkCreateTimes: [], lastLookBackDate: null, + createdSignalsCount: 0, }; if (someResult.hits.hits.length === 0) { toReturn.success = true; @@ -75,7 +77,7 @@ export const searchAfterAndBulkCreate = async ({ } logger.debug('[+] starting bulk insertion'); - const { bulkCreateDuration } = await singleBulkCreate({ + const { bulkCreateDuration, createdItemsCount } = await singleBulkCreate({ someResult, ruleParams, services, @@ -97,6 +99,9 @@ export const searchAfterAndBulkCreate = async ({ someResult.hits.hits.length > 0 ? new Date(someResult.hits.hits[someResult.hits.hits.length - 1]?._source['@timestamp']) : null; + if (createdItemsCount) { + toReturn.createdSignalsCount = createdItemsCount; + } if (bulkCreateDuration) { toReturn.bulkCreateTimes.push(bulkCreateDuration); } @@ -156,7 +161,10 @@ export const searchAfterAndBulkCreate = async ({ } sortId = sortIds[0]; logger.debug('next bulk index'); - const { bulkCreateDuration: bulkDuration } = await singleBulkCreate({ + const { + bulkCreateDuration: bulkDuration, + createdItemsCount: createdCount, + } = await singleBulkCreate({ someResult: searchResult, ruleParams, services, @@ -175,6 +183,7 @@ export const searchAfterAndBulkCreate = async ({ throttle, }); logger.debug('finished next bulk index'); + toReturn.createdSignalsCount += createdCount; if (bulkDuration) { toReturn.bulkCreateTimes.push(bulkDuration); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts new file mode 100644 index 000000000000000..11d31f1805440f7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -0,0 +1,399 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { loggerMock } from 'src/core/server/logging/logger.mock'; +import { getResult, getMlResult } from '../routes/__mocks__/request_responses'; +import { signalRulesAlertType } from './signal_rule_alert_type'; +import { AlertInstance } from '../../../../../../../plugins/alerting/server'; +import { ruleStatusServiceFactory } from './rule_status_service'; +import { getGapBetweenRuns } from './utils'; +import { RuleExecutorOptions } from './types'; +import { searchAfterAndBulkCreate } from './search_after_bulk_create'; +import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; +import { RuleAlertType } from '../rules/types'; +import { findMlSignals } from './find_ml_signals'; +import { bulkCreateMlSignals } from './bulk_create_ml_signals'; + +jest.mock('./rule_status_saved_objects_client'); +jest.mock('./rule_status_service'); +jest.mock('./search_after_bulk_create'); +jest.mock('./get_filter'); +jest.mock('./utils'); +jest.mock('../notifications/schedule_notification_actions'); +jest.mock('./find_ml_signals'); +jest.mock('./bulk_create_ml_signals'); + +const getPayload = ( + ruleAlert: RuleAlertType, + alertInstanceFactoryMock: () => AlertInstance, + savedObjectsClient: ReturnType, + callClusterMock: jest.Mock +) => ({ + alertId: ruleAlert.id, + services: { + savedObjectsClient, + alertInstanceFactory: alertInstanceFactoryMock, + callCluster: callClusterMock, + }, + params: { + ...ruleAlert.params, + actions: [], + enabled: ruleAlert.enabled, + interval: ruleAlert.schedule.interval, + name: ruleAlert.name, + tags: ruleAlert.tags, + throttle: ruleAlert.throttle!, + scrollSize: 10, + scrollLock: '0', + }, + state: {}, + spaceId: '', + name: 'name', + tags: [], + startedAt: new Date('2019-12-13T16:50:33.400Z'), + previousStartedAt: new Date('2019-12-13T16:40:33.400Z'), + createdBy: 'elastic', + updatedBy: 'elastic', +}); + +describe('rules_notification_alert_type', () => { + const version = '8.0.0'; + const jobsSummaryMock = jest.fn(); + const mlMock = { + mlClient: { + callAsInternalUser: jest.fn(), + close: jest.fn(), + asScoped: jest.fn(), + }, + jobServiceProvider: jest.fn().mockReturnValue({ + jobsSummary: jobsSummaryMock, + }), + anomalyDetectorsProvider: jest.fn(), + mlSystemProvider: jest.fn(), + modulesProvider: jest.fn(), + resultsServiceProvider: jest.fn(), + }; + let payload: RuleExecutorOptions; + let alert: ReturnType; + let alertInstanceMock: Record; + let alertInstanceFactoryMock: () => AlertInstance; + let savedObjectsClient: ReturnType; + let logger: ReturnType; + let callClusterMock: jest.Mock; + let ruleStatusService: Record; + + beforeEach(() => { + alertInstanceMock = { + scheduleActions: jest.fn(), + replaceState: jest.fn(), + }; + alertInstanceMock.replaceState.mockReturnValue(alertInstanceMock); + alertInstanceFactoryMock = jest.fn().mockReturnValue(alertInstanceMock); + callClusterMock = jest.fn(); + savedObjectsClient = savedObjectsClientMock.create(); + logger = loggerMock.create(); + ruleStatusService = { + success: jest.fn(), + find: jest.fn(), + goingToRun: jest.fn(), + error: jest.fn(), + }; + (ruleStatusServiceFactory as jest.Mock).mockReturnValue(ruleStatusService); + (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(0)); + (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ + success: true, + searchAfterTimes: [], + createdSignalsCount: 10, + }); + callClusterMock.mockResolvedValue({ + hits: { + total: { value: 10 }, + }, + }); + const ruleAlert = getResult(); + savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + + payload = getPayload(ruleAlert, alertInstanceFactoryMock, savedObjectsClient, callClusterMock); + + alert = signalRulesAlertType({ + logger, + version, + ml: mlMock, + }); + }); + + describe('executor', () => { + it('should warn about the gap between runs', async () => { + (getGapBetweenRuns as jest.Mock).mockReturnValue(moment.duration(1000)); + await alert.executor(payload); + expect(logger.warn).toHaveBeenCalled(); + expect(logger.warn.mock.calls[0][0]).toContain( + 'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.' + ); + expect(ruleStatusService.error).toHaveBeenCalled(); + expect(ruleStatusService.error.mock.calls[0][0]).toContain( + 'a few seconds (1000ms) has passed since last rule execution, and signals may have been missed.' + ); + expect(ruleStatusService.error.mock.calls[0][1]).toEqual({ + gap: 'a few seconds', + }); + }); + + it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => { + const ruleAlert = getResult(); + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + + savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + + await alert.executor(payload); + + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + signalsCount: 10, + }) + ); + }); + + describe('ML rule', () => { + it('should throw an error if ML plugin was not available', async () => { + const ruleAlert = getMlResult(); + payload = getPayload( + ruleAlert, + alertInstanceFactoryMock, + savedObjectsClient, + callClusterMock + ); + alert = signalRulesAlertType({ + logger, + version, + ml: undefined, + }); + await alert.executor(payload); + expect(logger.error).toHaveBeenCalled(); + expect(logger.error.mock.calls[0][0]).toContain( + 'ML plugin unavailable during rule execution' + ); + }); + + it('should throw an error if machineLearningJobId or anomalyThreshold was not null', async () => { + const ruleAlert = getMlResult(); + ruleAlert.params.anomalyThreshold = undefined; + payload = getPayload( + ruleAlert, + alertInstanceFactoryMock, + savedObjectsClient, + callClusterMock + ); + await alert.executor(payload); + expect(logger.error).toHaveBeenCalled(); + expect(logger.error.mock.calls[0][0]).toContain( + 'Machine learning rule is missing job id and/or anomaly threshold' + ); + }); + + it('should throw an error if Machine learning job summary was null', async () => { + const ruleAlert = getMlResult(); + payload = getPayload( + ruleAlert, + alertInstanceFactoryMock, + savedObjectsClient, + callClusterMock + ); + jobsSummaryMock.mockResolvedValue([]); + await alert.executor(payload); + expect(logger.warn).toHaveBeenCalled(); + expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job is not started'); + expect(ruleStatusService.error).toHaveBeenCalled(); + expect(ruleStatusService.error.mock.calls[0][0]).toContain( + 'Machine learning job is not started' + ); + }); + + it('should log an error if Machine learning job was not started', async () => { + const ruleAlert = getMlResult(); + payload = getPayload( + ruleAlert, + alertInstanceFactoryMock, + savedObjectsClient, + callClusterMock + ); + jobsSummaryMock.mockResolvedValue([ + { + id: 'some_job_id', + jobState: 'starting', + datafeedState: 'started', + }, + ]); + (findMlSignals as jest.Mock).mockResolvedValue({ + hits: { + hits: [], + }, + }); + await alert.executor(payload); + expect(logger.warn).toHaveBeenCalled(); + expect(logger.warn.mock.calls[0][0]).toContain('Machine learning job is not started'); + expect(ruleStatusService.error).toHaveBeenCalled(); + expect(ruleStatusService.error.mock.calls[0][0]).toContain( + 'Machine learning job is not started' + ); + }); + + it('should not call ruleStatusService.success if no anomalies were found', async () => { + const ruleAlert = getMlResult(); + payload = getPayload( + ruleAlert, + alertInstanceFactoryMock, + savedObjectsClient, + callClusterMock + ); + jobsSummaryMock.mockResolvedValue([]); + (findMlSignals as jest.Mock).mockResolvedValue({ + hits: { + hits: [], + }, + }); + (bulkCreateMlSignals as jest.Mock).mockResolvedValue({ + success: true, + bulkCreateDuration: 0, + createdItemsCount: 0, + }); + await alert.executor(payload); + expect(ruleStatusService.success).not.toHaveBeenCalled(); + }); + + it('should call ruleStatusService.success if signals were created', async () => { + const ruleAlert = getMlResult(); + payload = getPayload( + ruleAlert, + alertInstanceFactoryMock, + savedObjectsClient, + callClusterMock + ); + jobsSummaryMock.mockResolvedValue([ + { + id: 'some_job_id', + jobState: 'started', + datafeedState: 'started', + }, + ]); + (findMlSignals as jest.Mock).mockResolvedValue({ + hits: { + hits: [{}], + }, + }); + (bulkCreateMlSignals as jest.Mock).mockResolvedValue({ + success: true, + bulkCreateDuration: 1, + createdItemsCount: 1, + }); + await alert.executor(payload); + expect(ruleStatusService.success).toHaveBeenCalled(); + }); + + it('should call scheduleActions if signalsCount was greater than 0 and rule has actions defined', async () => { + const ruleAlert = getMlResult(); + ruleAlert.actions = [ + { + actionTypeId: '.slack', + params: { + message: + 'Rule generated {{state.signals_count}} signals\n\n{{context.rule.name}}\n{{{context.results_link}}}', + }, + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + }, + ]; + payload = getPayload( + ruleAlert, + alertInstanceFactoryMock, + savedObjectsClient, + callClusterMock + ); + savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + jobsSummaryMock.mockResolvedValue([]); + (findMlSignals as jest.Mock).mockResolvedValue({ + hits: { + hits: [{}], + }, + }); + (bulkCreateMlSignals as jest.Mock).mockResolvedValue({ + success: true, + bulkCreateDuration: 1, + createdItemsCount: 1, + }); + + await alert.executor(payload); + + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + signalsCount: 1, + }) + ); + }); + }); + }); + + describe('should catch error', () => { + it('when bulk indexing failed', async () => { + (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({ + success: false, + searchAfterTimes: [], + bulkCreateTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + }); + await alert.executor(payload); + expect(logger.error).toHaveBeenCalled(); + expect(logger.error.mock.calls[0][0]).toContain( + 'Bulk Indexing of signals failed. Check logs for further details.' + ); + expect(ruleStatusService.error).toHaveBeenCalled(); + }); + + it('when error was thrown', async () => { + (searchAfterAndBulkCreate as jest.Mock).mockResolvedValue({}); + await alert.executor(payload); + expect(logger.error).toHaveBeenCalled(); + expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution'); + expect(ruleStatusService.error).toHaveBeenCalled(); + }); + + it('and call ruleStatusService with the default message', async () => { + (searchAfterAndBulkCreate as jest.Mock).mockRejectedValue({}); + await alert.executor(payload); + expect(logger.error).toHaveBeenCalled(); + expect(logger.error.mock.calls[0][0]).toContain('An error occurred during rule execution'); + expect(ruleStatusService.error).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 246701e94c99a28..417fcbbe42a565f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -19,16 +19,16 @@ import { } from './search_after_bulk_create'; import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; -import { getGapBetweenRuns, makeFloatString } from './utils'; +import { getGapBetweenRuns, makeFloatString, parseScheduleDates } from './utils'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; import { findMlSignals } from './find_ml_signals'; import { bulkCreateMlSignals } from './bulk_create_ml_signals'; -import { getSignalsCount } from '../notifications/get_signals_count'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; import { ruleStatusServiceFactory } from './rule_status_service'; import { buildRuleMessageFactory } from './rule_messages'; import { ruleStatusSavedObjectsClientFactory } from './rule_status_saved_objects_client'; +import { getNotificationResultsLink } from '../notifications/utils'; export const signalRulesAlertType = ({ logger, @@ -71,6 +71,7 @@ export const signalRulesAlertType = ({ bulkCreateTimes: [], searchAfterTimes: [], lastLookBackDate: null, + createdSignalsCount: 0, }; const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient); const ruleStatusService = await ruleStatusServiceFactory({ @@ -161,7 +162,7 @@ export const signalRulesAlertType = ({ logger.info(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`)); } - const { success, bulkCreateDuration } = await bulkCreateMlSignals({ + const { success, bulkCreateDuration, createdItemsCount } = await bulkCreateMlSignals({ actions, throttle, someResult: anomalyResults, @@ -180,6 +181,7 @@ export const signalRulesAlertType = ({ tags, }); result.success = success; + result.createdSignalsCount = createdItemsCount; if (bulkCreateDuration) { result.bulkCreateTimes.push(bulkCreateDuration); } @@ -249,23 +251,26 @@ export const signalRulesAlertType = ({ name, id: savedObject.id, }; - const { signalsCount, resultsLink } = await getSignalsCount({ - from: `now-${interval}`, - to: 'now', - index: ruleParams.outputIndex, - ruleId: ruleParams.ruleId!, + + const fromInMs = parseScheduleDates(`now-${interval}`)?.format('x'); + const toInMs = parseScheduleDates('now')?.format('x'); + + const resultsLink = getNotificationResultsLink({ + from: fromInMs, + to: toInMs, + id: savedObject.id, kibanaSiemAppUrl: meta?.kibanaSiemAppUrl as string, - ruleAlertId: savedObject.id, - callCluster: services.callCluster, }); - logger.info(buildRuleMessage(`Found ${signalsCount} signals for notification.`)); + logger.info( + buildRuleMessage(`Found ${result.createdSignalsCount} signals for notification.`) + ); - if (signalsCount) { + if (result.createdSignalsCount) { const alertInstance = services.alertInstanceFactory(alertId); scheduleNotificationActions({ alertInstance, - signalsCount, + signalsCount: result.createdSignalsCount, resultsLink, ruleParams: notificationRuleParams, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts index 45b5610e2d3c3f9..56f061cdfa3ca97 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -144,7 +144,7 @@ describe('singleBulkCreate', () => { }, ], }); - const { success } = await singleBulkCreate({ + const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleDocSearchResultsNoSortId(), ruleParams: sampleParams, services: mockService, @@ -163,6 +163,7 @@ describe('singleBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(true); + expect(createdItemsCount).toEqual(0); }); test('create successful bulk create with docs with no versioning', async () => { @@ -176,7 +177,7 @@ describe('singleBulkCreate', () => { }, ], }); - const { success } = await singleBulkCreate({ + const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleDocSearchResultsNoSortIdNoVersion(), ruleParams: sampleParams, services: mockService, @@ -195,12 +196,13 @@ describe('singleBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(true); + expect(createdItemsCount).toEqual(0); }); test('create unsuccessful bulk create due to empty search results', async () => { const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(false); - const { success } = await singleBulkCreate({ + const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleEmptyDocSearchResults(), ruleParams: sampleParams, services: mockService, @@ -219,13 +221,14 @@ describe('singleBulkCreate', () => { throttle: 'no_actions', }); expect(success).toEqual(true); + expect(createdItemsCount).toEqual(0); }); test('create successful bulk create when bulk create has duplicate errors', async () => { const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); - const { success } = await singleBulkCreate({ + const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleSearchResult(), ruleParams: sampleParams, services: mockService, @@ -246,13 +249,14 @@ describe('singleBulkCreate', () => { expect(mockLogger.error).not.toHaveBeenCalled(); expect(success).toEqual(true); + expect(createdItemsCount).toEqual(1); }); test('create successful bulk create when bulk create has multiple error statuses', async () => { const sampleParams = sampleRuleAlertParams(); const sampleSearchResult = sampleDocSearchResultsNoSortId; mockService.callCluster.mockReturnValue(sampleBulkCreateErrorResult); - const { success } = await singleBulkCreate({ + const { success, createdItemsCount } = await singleBulkCreate({ someResult: sampleSearchResult(), ruleParams: sampleParams, services: mockService, @@ -273,6 +277,7 @@ describe('singleBulkCreate', () => { expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(true); + expect(createdItemsCount).toEqual(1); }); test('filter duplicate rules will return an empty array given an empty array', () => { @@ -341,4 +346,29 @@ describe('singleBulkCreate', () => { }, ]); }); + + test('create successful and returns proper createdItemsCount', async () => { + const sampleParams = sampleRuleAlertParams(); + mockService.callCluster.mockReturnValue(sampleBulkCreateDuplicateResult); + const { success, createdItemsCount } = await singleBulkCreate({ + someResult: sampleDocSearchResultsNoSortId(), + ruleParams: sampleParams, + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + signalsIndex: DEFAULT_SIGNALS_INDEX, + actions: [], + name: 'rule-name', + createdAt: '2020-01-28T15:58:34.810Z', + updatedAt: '2020-01-28T15:59:14.004Z', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + tags: ['some fake tag 1', 'some fake tag 2'], + throttle: 'no_actions', + }); + expect(success).toEqual(true); + expect(createdItemsCount).toEqual(1); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts index ffec40b839bf649..6dd8823b57e4de0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_bulk_create.ts @@ -58,6 +58,7 @@ export const filterDuplicateRules = ( export interface SingleBulkCreateResponse { success: boolean; bulkCreateDuration?: string; + createdItemsCount: number; } // Bulk Index documents. @@ -81,7 +82,7 @@ export const singleBulkCreate = async ({ }: SingleBulkCreateParams): Promise => { someResult.hits.hits = filterDuplicateRules(id, someResult); if (someResult.hits.hits.length === 0) { - return { success: true }; + return { success: true, createdItemsCount: 0 }; } // index documents after creating an ID based on the // source documents' originating index, and the original @@ -145,5 +146,8 @@ export const singleBulkCreate = async ({ ); } } - return { success: true, bulkCreateDuration: makeFloatString(end - start) }; + + const createdItemsCount = countBy(response.items, 'create.status')['201'] ?? 0; + + return { success: true, bulkCreateDuration: makeFloatString(end - start), createdItemsCount }; }; From dfa083dc6041edbe584ca58618ecb9fe2f81d81e Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 6 Apr 2020 13:45:46 -0400 Subject: [PATCH 14/15] Prep for embed saved object refactor + helper (#62486) --- .../list_container/embeddable_list_item.tsx | 64 --------------- .../public/list_container/list_container.tsx | 19 ++--- .../list_container_component.tsx | 26 ++++-- .../list_container/list_container_factory.ts | 6 +- .../multi_task_todo_component.tsx | 17 ++-- .../multi_task_todo_embeddable.tsx | 27 +++---- examples/embeddable_examples/public/plugin.ts | 7 +- .../searchable_list_container.tsx | 16 ++-- .../searchable_list_container_component.tsx | 79 +++++++++++++------ .../searchable_list_container_factory.ts | 6 +- .../public/todo/todo_component.tsx | 10 ++- examples/embeddable_explorer/public/app.tsx | 13 +-- .../public/embeddable_panel_example.tsx | 49 ++---------- .../public/list_container_example.tsx | 10 ++- .../embeddable_child_panel.test.tsx | 2 +- .../lib/embeddables/with_subscription.tsx | 12 +-- .../lib/panel/embeddable_panel.test.tsx | 4 +- .../inspect_panel_action.test.tsx | 2 +- src/plugins/embeddable/public/mocks.ts | 10 ++- .../public/{plugin.ts => plugin.tsx} | 54 ++++++++++--- .../public/tests/apply_filter_action.test.ts | 2 +- .../embeddable/public/tests/test_plugin.ts | 11 ++- test/examples/embeddables/list_container.ts | 9 +-- .../public/np_ready/public/app/app.tsx | 34 ++------ .../app/dashboard_container_example.tsx | 33 ++------ .../public/np_ready/public/plugin.tsx | 15 +--- 26 files changed, 234 insertions(+), 303 deletions(-) delete mode 100644 examples/embeddable_examples/public/list_container/embeddable_list_item.tsx rename src/plugins/embeddable/public/{plugin.ts => plugin.tsx} (76%) diff --git a/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx b/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx deleted file mode 100644 index 2c80cef8a636486..000000000000000 --- a/examples/embeddable_examples/public/list_container/embeddable_list_item.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { EuiPanel, EuiLoadingSpinner, EuiFlexItem } from '@elastic/eui'; -import { IEmbeddable } from '../../../../src/plugins/embeddable/public'; - -interface Props { - embeddable: IEmbeddable; -} - -export class EmbeddableListItem extends React.Component { - private embeddableRoot: React.RefObject; - private rendered = false; - - constructor(props: Props) { - super(props); - this.embeddableRoot = React.createRef(); - } - - public componentDidMount() { - if (this.embeddableRoot.current && this.props.embeddable) { - this.props.embeddable.render(this.embeddableRoot.current); - this.rendered = true; - } - } - - public componentDidUpdate() { - if (this.embeddableRoot.current && this.props.embeddable && !this.rendered) { - this.props.embeddable.render(this.embeddableRoot.current); - this.rendered = true; - } - } - - public render() { - return ( - - - {this.props.embeddable ? ( -
- ) : ( - - )} - - - ); - } -} diff --git a/examples/embeddable_examples/public/list_container/list_container.tsx b/examples/embeddable_examples/public/list_container/list_container.tsx index bbbd0d6e3230460..9e7bec7a1c951ec 100644 --- a/examples/embeddable_examples/public/list_container/list_container.tsx +++ b/examples/embeddable_examples/public/list_container/list_container.tsx @@ -31,16 +31,14 @@ export class ListContainer extends Container<{}, ContainerInput> { public readonly type = LIST_CONTAINER; private node?: HTMLElement; - constructor( - input: ContainerInput, - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory'] - ) { - super(input, { embeddableLoaded: {} }, getEmbeddableFactory); + constructor(input: ContainerInput, private embeddableServices: EmbeddableStart) { + super(input, { embeddableLoaded: {} }, embeddableServices.getEmbeddableFactory); } - // This container has no input itself. - getInheritedInput(id: string) { - return {}; + getInheritedInput() { + return { + viewMode: this.input.viewMode, + }; } public render(node: HTMLElement) { @@ -48,7 +46,10 @@ export class ListContainer extends Container<{}, ContainerInput> { if (this.node) { ReactDOM.unmountComponentAtNode(this.node); } - ReactDOM.render(, node); + ReactDOM.render( + , + node + ); } public destroy() { diff --git a/examples/embeddable_examples/public/list_container/list_container_component.tsx b/examples/embeddable_examples/public/list_container/list_container_component.tsx index f6e04933ee8971a..da27889a276036d 100644 --- a/examples/embeddable_examples/public/list_container/list_container_component.tsx +++ b/examples/embeddable_examples/public/list_container/list_container_component.tsx @@ -24,30 +24,35 @@ import { withEmbeddableSubscription, ContainerInput, ContainerOutput, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; -import { EmbeddableListItem } from './embeddable_list_item'; interface Props { embeddable: IContainer; input: ContainerInput; output: ContainerOutput; + embeddableServices: EmbeddableStart; } -function renderList(embeddable: IContainer, panels: ContainerInput['panels']) { +function renderList( + embeddable: IContainer, + panels: ContainerInput['panels'], + embeddableServices: EmbeddableStart +) { let number = 0; const list = Object.values(panels).map(panel => { const child = embeddable.getChild(panel.explicitInput.id); number++; return ( - +

{number}

- +
@@ -56,12 +61,12 @@ function renderList(embeddable: IContainer, panels: ContainerInput['panels']) { return list; } -export function ListContainerComponentInner(props: Props) { +export function ListContainerComponentInner({ embeddable, input, embeddableServices }: Props) { return (
-

{props.embeddable.getTitle()}

+

{embeddable.getTitle()}

- {renderList(props.embeddable, props.input.panels)} + {renderList(embeddable, input.panels, embeddableServices)}
); } @@ -71,4 +76,9 @@ export function ListContainerComponentInner(props: Props) { // anything on input or output state changes. If you don't want that to happen (for example // if you expect something on input or output state to change frequently that your react // component does not care about, then you should probably hook this up manually). -export const ListContainerComponent = withEmbeddableSubscription(ListContainerComponentInner); +export const ListContainerComponent = withEmbeddableSubscription< + ContainerInput, + ContainerOutput, + IContainer, + { embeddableServices: EmbeddableStart } +>(ListContainerComponentInner); diff --git a/examples/embeddable_examples/public/list_container/list_container_factory.ts b/examples/embeddable_examples/public/list_container/list_container_factory.ts index 1fde254110c624f..02a024b95349fa8 100644 --- a/examples/embeddable_examples/public/list_container/list_container_factory.ts +++ b/examples/embeddable_examples/public/list_container/list_container_factory.ts @@ -26,7 +26,7 @@ import { import { LIST_CONTAINER, ListContainer } from './list_container'; interface StartServices { - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + embeddableServices: EmbeddableStart; } export class ListContainerFactory implements EmbeddableFactoryDefinition { @@ -40,8 +40,8 @@ export class ListContainerFactory implements EmbeddableFactoryDefinition { } public create = async (initialInput: ContainerInput) => { - const { getEmbeddableFactory } = await this.getStartServices(); - return new ListContainer(initialInput, getEmbeddableFactory); + const { embeddableServices } = await this.getStartServices(); + return new ListContainer(initialInput, embeddableServices); }; public getDisplayName() { diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx index e33dfab0eaf4a2e..b2882c97ef501ec 100644 --- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx +++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_component.tsx @@ -54,7 +54,7 @@ function wrapSearchTerms(task: string, search?: string) { ); } -function renderTasks(tasks: MultiTaskTodoOutput['tasks'], search?: string) { +function renderTasks(tasks: MultiTaskTodoInput['tasks'], search?: string) { return tasks.map(task => ( + {icon ? : } - +

{wrapSearchTerms(title, search)}

@@ -89,6 +88,8 @@ export function MultiTaskTodoEmbeddableComponentInner({ ); } -export const MultiTaskTodoEmbeddableComponent = withEmbeddableSubscription( - MultiTaskTodoEmbeddableComponentInner -); +export const MultiTaskTodoEmbeddableComponent = withEmbeddableSubscription< + MultiTaskTodoInput, + MultiTaskTodoOutput, + MultiTaskTodoEmbeddable +>(MultiTaskTodoEmbeddableComponentInner); diff --git a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx index a2197c9c06fe946..a9e58c5538107b1 100644 --- a/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx +++ b/examples/embeddable_examples/public/multi_task_todo/multi_task_todo_embeddable.tsx @@ -36,30 +36,27 @@ export interface MultiTaskTodoInput extends EmbeddableInput { title: string; } -// This embeddable has output! It's the tasks list that is filtered. -// Output state is something only the embeddable itself can update. It -// can be something completely internal, or it can be state that is +// This embeddable has output! Output state is something only the embeddable itself +// can update. It can be something completely internal, or it can be state that is // derived from input state and updates when input does. export interface MultiTaskTodoOutput extends EmbeddableOutput { - tasks: string[]; + hasMatch: boolean; } -function getFilteredTasks(tasks: string[], search?: string) { - const filteredTasks: string[] = []; - if (search === undefined) return tasks; +function getHasMatch(tasks: string[], title?: string, search?: string) { + if (search === undefined || search === '') return false; - tasks.forEach(task => { - if (task.match(search)) { - filteredTasks.push(task); - } - }); + if (title && title.match(search)) return true; + + const match = tasks.find(task => task.match(search)); + if (match) return true; - return filteredTasks; + return false; } function getOutput(input: MultiTaskTodoInput) { - const tasks = getFilteredTasks(input.tasks, input.search); - return { tasks, hasMatch: tasks.length > 0 || (input.search && input.title.match(input.search)) }; + const hasMatch = getHasMatch(input.tasks, input.title, input.search); + return { hasMatch }; } export class MultiTaskTodoEmbeddable extends Embeddable { diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts index 5c202d96ceb1a25..31a3037332dda9c 100644 --- a/examples/embeddable_examples/public/plugin.ts +++ b/examples/embeddable_examples/public/plugin.ts @@ -53,20 +53,17 @@ export class EmbeddableExamplesPlugin new MultiTaskTodoEmbeddableFactory() ); - // These are registered in the start method because `getEmbeddableFactory ` - // is only available in start. We could reconsider this I think and make it - // available in both. deps.embeddable.registerEmbeddableFactory( SEARCHABLE_LIST_CONTAINER, new SearchableListContainerFactory(async () => ({ - getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + embeddableServices: (await core.getStartServices())[1].embeddable, })) ); deps.embeddable.registerEmbeddableFactory( LIST_CONTAINER, new ListContainerFactory(async () => ({ - getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory, + embeddableServices: (await core.getStartServices())[1].embeddable, })) ); diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx index 06462937c768d79..f6efb0b722c4c4c 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container.tsx @@ -40,11 +40,8 @@ export class SearchableListContainer extends Container, node); + ReactDOM.render( + , + node + ); } public destroy() { diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx index b79f86e2a0192d9..49dbce74788bfa8 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_component.tsx @@ -34,14 +34,15 @@ import { withEmbeddableSubscription, ContainerOutput, EmbeddableOutput, + EmbeddableStart, } from '../../../../src/plugins/embeddable/public'; -import { EmbeddableListItem } from '../list_container/embeddable_list_item'; import { SearchableListContainer, SearchableContainerInput } from './searchable_list_container'; interface Props { embeddable: SearchableListContainer; input: SearchableContainerInput; output: ContainerOutput; + embeddableServices: EmbeddableStart; } interface State { @@ -111,13 +112,27 @@ export class SearchableListContainerComponentInner extends Component { + const { input, embeddable } = this.props; + const checked: { [key: string]: boolean } = {}; + Object.values(input.panels).map(panel => { + const child = embeddable.getChild(panel.explicitInput.id); + const output = child.getOutput(); + if (hasHasMatchOutput(output) && output.hasMatch) { + checked[panel.explicitInput.id] = true; + } + }); + this.setState({ checked }); + }; + private toggleCheck = (isChecked: boolean, id: string) => { this.setState(prevState => ({ checked: { ...prevState.checked, [id]: isChecked } })); }; public renderControls() { + const { input } = this.props; return ( - + this.deleteChecked()}> @@ -125,6 +140,17 @@ export class SearchableListContainerComponentInner extends Component + + + this.checkMatching()} + > + Check matching + + + -

{embeddable.getTitle()}

- - {this.renderControls()} - - {this.renderList()} -
+ + +

{embeddable.getTitle()}

+ + {this.renderControls()} + + {this.renderList()} +
+
); } private renderList() { + const { embeddableServices, input, embeddable } = this.props; let id = 0; - const list = Object.values(this.props.input.panels).map(panel => { - const embeddable = this.props.embeddable.getChild(panel.explicitInput.id); - if (this.props.input.search && !this.state.hasMatch[panel.explicitInput.id]) return; + const list = Object.values(input.panels).map(panel => { + const childEmbeddable = embeddable.getChild(panel.explicitInput.id); id++; - return embeddable ? ( - - + return childEmbeddable ? ( + + this.toggleCheck(e.target.checked, embeddable.id)} + data-test-subj={`todoCheckBox-${childEmbeddable.id}`} + disabled={!childEmbeddable} + id={childEmbeddable ? childEmbeddable.id : ''} + checked={this.state.checked[childEmbeddable.id]} + onChange={e => this.toggleCheck(e.target.checked, childEmbeddable.id)} /> - + @@ -183,6 +211,9 @@ export class SearchableListContainerComponentInner extends Component(SearchableListContainerComponentInner); diff --git a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts index 382bb65e769ef24..34ea43c29462a8e 100644 --- a/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts +++ b/examples/embeddable_examples/public/searchable_list_container/searchable_list_container_factory.ts @@ -29,7 +29,7 @@ import { } from './searchable_list_container'; interface StartServices { - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; + embeddableServices: EmbeddableStart; } export class SearchableListContainerFactory implements EmbeddableFactoryDefinition { @@ -43,8 +43,8 @@ export class SearchableListContainerFactory implements EmbeddableFactoryDefiniti } public create = async (initialInput: SearchableContainerInput) => { - const { getEmbeddableFactory } = await this.getStartServices(); - return new SearchableListContainer(initialInput, getEmbeddableFactory); + const { embeddableServices } = await this.getStartServices(); + return new SearchableListContainer(initialInput, embeddableServices); }; public getDisplayName() { diff --git a/examples/embeddable_examples/public/todo/todo_component.tsx b/examples/embeddable_examples/public/todo/todo_component.tsx index fbebfc98627b583..a4593bea3cc5e5e 100644 --- a/examples/embeddable_examples/public/todo/todo_component.tsx +++ b/examples/embeddable_examples/public/todo/todo_component.tsx @@ -51,12 +51,12 @@ function wrapSearchTerms(task: string, search?: string) { export function TodoEmbeddableComponentInner({ input: { icon, title, task, search } }: Props) { return ( - + {icon ? : } - +

{wrapSearchTerms(title || '', search)}

@@ -71,4 +71,8 @@ export function TodoEmbeddableComponentInner({ input: { icon, title, task, searc ); } -export const TodoEmbeddableComponent = withEmbeddableSubscription(TodoEmbeddableComponentInner); +export const TodoEmbeddableComponent = withEmbeddableSubscription< + TodoInput, + EmbeddableOutput, + TodoEmbeddable +>(TodoEmbeddableComponentInner); diff --git a/examples/embeddable_explorer/public/app.tsx b/examples/embeddable_explorer/public/app.tsx index 9c8568454855db0..e18012b4b3d806b 100644 --- a/examples/embeddable_explorer/public/app.tsx +++ b/examples/embeddable_explorer/public/app.tsx @@ -117,18 +117,7 @@ const EmbeddableExplorerApp = ({ { title: 'Dynamically adding children to a container', id: 'embeddablePanelExamplae', - component: ( - - ), + component: , }, ]; diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx index b26111bed7ff23b..54cd7c5b5b2c01e 100644 --- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx +++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx @@ -29,43 +29,19 @@ import { EuiText, } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { OverlayStart, CoreStart, SavedObjectsStart, IUiSettingsClient } from 'kibana/public'; -import { - EmbeddablePanel, - EmbeddableStart, - IEmbeddable, -} from '../../../src/plugins/embeddable/public'; +import { EmbeddableStart, IEmbeddable } from '../../../src/plugins/embeddable/public'; import { HELLO_WORLD_EMBEDDABLE, TODO_EMBEDDABLE, MULTI_TASK_TODO_EMBEDDABLE, SEARCHABLE_LIST_CONTAINER, } from '../../embeddable_examples/public'; -import { UiActionsStart } from '../../../src/plugins/ui_actions/public'; -import { Start as InspectorStartContract } from '../../../src/plugins/inspector/public'; -import { getSavedObjectFinder } from '../../../src/plugins/saved_objects/public'; interface Props { - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - uiActionsApi: UiActionsStart; - overlays: OverlayStart; - notifications: CoreStart['notifications']; - inspector: InspectorStartContract; - savedObject: SavedObjectsStart; - uiSettingsClient: IUiSettingsClient; + embeddableServices: EmbeddableStart; } -export function EmbeddablePanelExample({ - inspector, - notifications, - overlays, - getAllEmbeddableFactories, - getEmbeddableFactory, - uiActionsApi, - savedObject, - uiSettingsClient, -}: Props) { +export function EmbeddablePanelExample({ embeddableServices }: Props) { const searchableInput = { id: '1', title: 'My searchable todo list', @@ -105,7 +81,7 @@ export function EmbeddablePanelExample({ useEffect(() => { ref.current = true; if (!embeddable) { - const factory = getEmbeddableFactory(SEARCHABLE_LIST_CONTAINER); + const factory = embeddableServices.getEmbeddableFactory(SEARCHABLE_LIST_CONTAINER); const promise = factory?.create(searchableInput); if (promise) { promise.then(e => { @@ -134,22 +110,13 @@ export function EmbeddablePanelExample({ You can render your embeddable inside the EmbeddablePanel component. This adds some extra rendering and offers a context menu with pluggable actions. Using EmbeddablePanel - to render your embeddable means you get access to the "e;Add panel flyout"e;. - Now you can see how to add embeddables to your container, and how - "e;getExplicitInput"e; is used to grab input not provided by the container. + to render your embeddable means you get access to the "Add panel flyout". Now + you can see how to add embeddables to your container, and how + "getExplicitInput" is used to grab input not provided by the container. {embeddable ? ( - + ) : ( Loading... )} diff --git a/examples/embeddable_explorer/public/list_container_example.tsx b/examples/embeddable_explorer/public/list_container_example.tsx index 969fdb0ca46db84..98ad50418d3fea3 100644 --- a/examples/embeddable_explorer/public/list_container_example.tsx +++ b/examples/embeddable_explorer/public/list_container_example.tsx @@ -29,7 +29,11 @@ import { EuiText, } from '@elastic/eui'; import { EuiSpacer } from '@elastic/eui'; -import { EmbeddableFactoryRenderer, EmbeddableStart } from '../../../src/plugins/embeddable/public'; +import { + EmbeddableFactoryRenderer, + EmbeddableStart, + ViewMode, +} from '../../../src/plugins/embeddable/public'; import { HELLO_WORLD_EMBEDDABLE, TODO_EMBEDDABLE, @@ -46,6 +50,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) { const listInput = { id: 'hello', title: 'My todo list', + viewMode: ViewMode.VIEW, panels: { '1': { type: HELLO_WORLD_EMBEDDABLE, @@ -76,6 +81,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) { const searchableInput = { id: '1', title: 'My searchable todo list', + viewMode: ViewMode.VIEW, panels: { '1': { type: HELLO_WORLD_EMBEDDABLE, @@ -150,7 +156,7 @@ export function ListContainerExample({ getEmbeddableFactory }: Props) {

- Check out the "e;Dynamically adding children"e; section, to see how to add + Check out the "Dynamically adding children" section, to see how to add children to this container, and see it rendered inside an `EmbeddablePanel` component.

diff --git a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx index 9e47da5cea0329e..2a0ffd723850b3a 100644 --- a/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/containers/embeddable_child_panel.test.tsx @@ -29,7 +29,7 @@ import { ContactCardEmbeddable, } from '../test_samples/embeddables/contact_card/contact_card_embeddable'; // eslint-disable-next-line -import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; +import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { mount } from 'enzyme'; import { embeddablePluginMock } from '../../mocks'; diff --git a/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx b/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx index 47b8001961cf514..9bc5889715c7605 100644 --- a/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx +++ b/src/plugins/embeddable/public/lib/embeddables/with_subscription.tsx @@ -23,18 +23,19 @@ import { IEmbeddable, EmbeddableInput, EmbeddableOutput } from './i_embeddable'; export const withEmbeddableSubscription = < I extends EmbeddableInput, O extends EmbeddableOutput, - E extends IEmbeddable = IEmbeddable + E extends IEmbeddable = IEmbeddable, + ExtraProps = {} >( - WrappedComponent: React.ComponentType<{ input: I; output: O; embeddable: E }> -): React.ComponentType<{ embeddable: E }> => + WrappedComponent: React.ComponentType<{ input: I; output: O; embeddable: E } & ExtraProps> +): React.ComponentType<{ embeddable: E } & ExtraProps> => class WithEmbeddableSubscription extends React.Component< - { embeddable: E }, + { embeddable: E } & ExtraProps, { input: I; output: O } > { private subscription?: Rx.Subscription; private mounted: boolean = false; - constructor(props: { embeddable: E }) { + constructor(props: { embeddable: E } & ExtraProps) { super(props); this.state = { input: this.props.embeddable.getInput(), @@ -71,6 +72,7 @@ export const withEmbeddableSubscription = < input={this.state.input} output={this.state.output} embeddable={this.props.embeddable} + {...this.props} /> ); } diff --git a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx index 649677dc67c7d14..1e7cbb2f3dafcff 100644 --- a/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/embeddable_panel.test.tsx @@ -25,7 +25,7 @@ import { nextTick } from 'test_utils/enzyme_helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; import { I18nProvider } from '@kbn/i18n/react'; import { CONTEXT_MENU_TRIGGER } from '../triggers'; -import { Action, UiActionsStart, ActionType } from 'src/plugins/ui_actions/public'; +import { Action, UiActionsStart, ActionType } from '../../../../ui_actions/public'; import { Trigger, ViewMode } from '../types'; import { isErrorEmbeddable } from '../embeddables'; import { EmbeddablePanel } from './embeddable_panel'; @@ -41,7 +41,7 @@ import { ContactCardEmbeddableOutput, } from '../test_samples/embeddables/contact_card/contact_card_embeddable'; // eslint-disable-next-line -import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; +import { inspectorPluginMock } from '../../../../inspector/public/mocks'; import { EuiBadge } from '@elastic/eui'; import { embeddablePluginMock } from '../../mocks'; diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx index ee31127cb5a405b..491eaad9faefa4c 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/inspect_panel_action.test.tsx @@ -27,7 +27,7 @@ import { ContactCardEmbeddable, } from '../../../test_samples'; // eslint-disable-next-line -import { inspectorPluginMock } from 'src/plugins/inspector/public/mocks'; +import { inspectorPluginMock } from '../../../../../../../plugins/inspector/public/mocks'; import { EmbeddableOutput, isErrorEmbeddable, ErrorEmbeddable } from '../../../embeddables'; import { of } from '../../../../tests/helpers'; import { esFilters } from '../../../../../../../plugins/data/public'; diff --git a/src/plugins/embeddable/public/mocks.ts b/src/plugins/embeddable/public/mocks.ts index 2ee05d8316aced6..65b15f3a7614fb4 100644 --- a/src/plugins/embeddable/public/mocks.ts +++ b/src/plugins/embeddable/public/mocks.ts @@ -16,11 +16,12 @@ * specific language governing permissions and limitations * under the License. */ - import { EmbeddableStart, EmbeddableSetup } from '.'; import { EmbeddablePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; +// eslint-disable-next-line +import { inspectorPluginMock } from '../../inspector/public/mocks'; // eslint-disable-next-line import { uiActionsPluginMock } from '../../ui_actions/public/mocks'; @@ -39,6 +40,7 @@ const createStartContract = (): Start => { const startContract: Start = { getEmbeddableFactories: jest.fn(), getEmbeddableFactory: jest.fn(), + EmbeddablePanel: jest.fn(), }; return startContract; }; @@ -48,7 +50,11 @@ const createInstance = () => { const setup = plugin.setup(coreMock.createSetup(), { uiActions: uiActionsPluginMock.createSetupContract(), }); - const doStart = () => plugin.start(coreMock.createStart()); + const doStart = () => + plugin.start(coreMock.createStart(), { + uiActions: uiActionsPluginMock.createStartContract(), + inspector: inspectorPluginMock.createStartContract(), + }); return { plugin, setup, diff --git a/src/plugins/embeddable/public/plugin.ts b/src/plugins/embeddable/public/plugin.tsx similarity index 76% rename from src/plugins/embeddable/public/plugin.ts rename to src/plugins/embeddable/public/plugin.tsx index a483f90f76dde79..01fbf52c8018236 100644 --- a/src/plugins/embeddable/public/plugin.ts +++ b/src/plugins/embeddable/public/plugin.tsx @@ -16,7 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { UiActionsSetup } from 'src/plugins/ui_actions/public'; +import React from 'react'; +import { getSavedObjectFinder } from '../../saved_objects/public'; +import { UiActionsSetup, UiActionsStart } from '../../ui_actions/public'; +import { Start as InspectorStart } from '../../inspector/public'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { EmbeddableFactoryRegistry, EmbeddableFactoryProvider } from './types'; import { bootstrap } from './bootstrap'; @@ -26,6 +29,7 @@ import { EmbeddableOutput, defaultEmbeddableFactoryProvider, IEmbeddable, + EmbeddablePanel, } from './lib'; import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition'; @@ -33,6 +37,11 @@ export interface EmbeddableSetupDependencies { uiActions: UiActionsSetup; } +export interface EmbeddableStartDependencies { + uiActions: UiActionsStart; + inspector: InspectorStart; +} + export interface EmbeddableSetup { registerEmbeddableFactory: ( id: string, @@ -50,6 +59,7 @@ export interface EmbeddableStart { embeddableFactoryId: string ) => EmbeddableFactory | undefined; getEmbeddableFactories: () => IterableIterator; + EmbeddablePanel: React.FC<{ embeddable: IEmbeddable; hideHeader?: boolean }>; } export class EmbeddablePublicPlugin implements Plugin { @@ -78,7 +88,10 @@ export class EmbeddablePublicPlugin implements Plugin { this.embeddableFactories.set( def.type, @@ -89,15 +102,36 @@ export class EmbeddablePublicPlugin implements Plugin { - this.ensureFactoriesExist(); - return this.embeddableFactories.values(); - }, + getEmbeddableFactories: this.getEmbeddableFactories, + EmbeddablePanel: ({ + embeddable, + hideHeader, + }: { + embeddable: IEmbeddable; + hideHeader?: boolean; + }) => ( + + ), }; } public stop() {} + private getEmbeddableFactories = () => { + this.ensureFactoriesExist(); + return this.embeddableFactories.values(); + }; + private registerEmbeddableFactory = ( embeddableFactoryId: string, factory: EmbeddableFactoryDefinition @@ -130,11 +164,11 @@ export class EmbeddablePublicPlugin implements Plugin { this.embeddableFactoryDefinitions.forEach(def => this.ensureFactoryExists(def.type)); - } + }; - private ensureFactoryExists(type: string) { + private ensureFactoryExists = (type: string) => { if (!this.embeddableFactories.get(type)) { const def = this.embeddableFactoryDefinitions.get(type); if (!def) return; @@ -145,5 +179,5 @@ export class EmbeddablePublicPlugin implements Plugin { diff --git a/src/plugins/embeddable/public/tests/test_plugin.ts b/src/plugins/embeddable/public/tests/test_plugin.ts index e199ef193aa1cc8..e13a906e30338f4 100644 --- a/src/plugins/embeddable/public/tests/test_plugin.ts +++ b/src/plugins/embeddable/public/tests/test_plugin.ts @@ -18,9 +18,11 @@ */ import { CoreSetup, CoreStart } from 'src/core/public'; +import { UiActionsStart } from '../../../ui_actions/public'; // eslint-disable-next-line -import { uiActionsPluginMock } from 'src/plugins/ui_actions/public/mocks'; -import { UiActionsStart } from 'src/plugins/ui_actions/public'; +import { uiActionsPluginMock } from '../../../ui_actions/public/mocks'; +// eslint-disable-next-line +import { inspectorPluginMock } from '../../../inspector/public/mocks'; import { coreMock } from '../../../../core/public/mocks'; import { EmbeddablePublicPlugin, EmbeddableSetup, EmbeddableStart } from '../plugin'; @@ -48,7 +50,10 @@ export const testPlugin = ( coreStart, setup, doStart: (anotherCoreStart: CoreStart = coreStart) => { - const start = plugin.start(anotherCoreStart); + const start = plugin.start(anotherCoreStart, { + uiActions: uiActionsPluginMock.createStartContract(), + inspector: inspectorPluginMock.createStartContract(), + }); return start; }, uiActions: uiActions.doStart(coreStart), diff --git a/test/examples/embeddables/list_container.ts b/test/examples/embeddables/list_container.ts index b1b91ad2c37f1b1..9e93d479471e8a3 100644 --- a/test/examples/embeddables/list_container.ts +++ b/test/examples/embeddables/list_container.ts @@ -57,13 +57,12 @@ export default function({ getService }: PluginFunctionalProviderContext) { expect(text).to.eql(['HELLO WORLD!']); }); - it('searchable container filters multi-task children', async () => { + it('searchable container finds matches in multi-task children', async () => { await testSubjects.setValue('filterTodos', 'earth'); + await testSubjects.click('checkMatchingTodos'); + await testSubjects.click('deleteCheckedTodos'); - await retry.try(async () => { - const tasks = await testSubjects.getVisibleTextAll('multiTaskTodoTask'); - expect(tasks).to.eql(['Watch planet earth']); - }); + await testSubjects.missingOrFail('multiTaskTodoTask'); }); }); } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx index 54d13efe4d79097..2ecde823dc4dfc9 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/app.tsx @@ -18,21 +18,11 @@ */ import { EuiTab } from '@elastic/eui'; import React, { Component } from 'react'; -import { CoreStart } from 'src/core/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; -import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions/public'; import { DashboardContainerExample } from './dashboard_container_example'; -import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public'; export interface AppProps { - getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - notifications: CoreStart['notifications']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; - I18nContext: CoreStart['i18n']['Context']; + embeddableServices: EmbeddableStart; } export class App extends Component { @@ -72,29 +62,17 @@ export class App extends Component { public render() { return ( - -
-
{this.renderTabs()}
- {this.getContentsForTab()} -
-
+
+
{this.renderTabs()}
+ {this.getContentsForTab()} +
); } private getContentsForTab() { switch (this.state.selectedTabId) { case 'dashboardContainer': { - return ( - - ); + return ; } } } diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx index fd07416cadbc5d8..16c2840d6a32e8d 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/app/dashboard_container_example.tsx @@ -19,32 +19,17 @@ import React from 'react'; import { EuiButton, EuiLoadingChart } from '@elastic/eui'; import { ContainerOutput } from 'src/plugins/embeddable/public'; -import { - ErrorEmbeddable, - ViewMode, - isErrorEmbeddable, - EmbeddablePanel, - EmbeddableStart, -} from '../embeddable_api'; +import { ErrorEmbeddable, ViewMode, isErrorEmbeddable, EmbeddableStart } from '../embeddable_api'; import { DASHBOARD_CONTAINER_TYPE, DashboardContainer, DashboardContainerInput, } from '../../../../../../../../src/plugins/dashboard/public'; -import { CoreStart } from '../../../../../../../../src/core/public'; import { dashboardInput } from './dashboard_input'; -import { Start as InspectorStartContract } from '../../../../../../../../src/plugins/inspector/public'; -import { UiActionsService } from '../../../../../../../../src/plugins/ui_actions/public'; interface Props { - getActions: UiActionsService['getTriggerCompatibleActions']; - getEmbeddableFactory: EmbeddableStart['getEmbeddableFactory']; - getAllEmbeddableFactories: EmbeddableStart['getEmbeddableFactories']; - overlays: CoreStart['overlays']; - notifications: CoreStart['notifications']; - inspector: InspectorStartContract; - SavedObjectFinder: React.ComponentType; + embeddableServices: EmbeddableStart; } interface State { @@ -67,7 +52,7 @@ export class DashboardContainerExample extends React.Component { public async componentDidMount() { this.mounted = true; - const dashboardFactory = this.props.getEmbeddableFactory< + const dashboardFactory = this.props.embeddableServices.getEmbeddableFactory< DashboardContainerInput, ContainerOutput, DashboardContainer @@ -99,6 +84,7 @@ export class DashboardContainerExample extends React.Component { }; public render() { + const { embeddableServices } = this.props; return (

Dashboard Container

@@ -108,16 +94,7 @@ export class DashboardContainerExample extends React.Component { {!this.state.loaded || !this.container ? ( ) : ( - + )}
); diff --git a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx index 18ceec652392d1c..e5f5faa6ac361df 100644 --- a/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx +++ b/test/plugin_functional/plugins/kbn_tp_embeddable_explorer/public/np_ready/public/plugin.tsx @@ -33,7 +33,6 @@ const REACT_ROOT_ID = 'embeddableExplorerRoot'; import { SayHelloAction, createSendMessageAction } from './embeddable_api'; import { App } from './app'; -import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public'; import { EmbeddableStart, EmbeddableSetup, @@ -78,19 +77,7 @@ export class EmbeddableExplorerPublicPlugin plugins.__LEGACY.onRenderComplete(() => { const root = document.getElementById(REACT_ROOT_ID); - ReactDOM.render( - , - root - ); + ReactDOM.render(, root); }); } From 29abe5b81bddd17dcdd671cf3456f99bfe7b08a0 Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Mon, 6 Apr 2020 13:54:21 -0400 Subject: [PATCH 15/15] [Ingest] EMT-146: agent status impl preparation (#62557) [Ingest] EMT-146: very light refactor a precursor for endpoint status change --- x-pack/plugins/endpoint/server/plugin.ts | 2 +- .../endpoint/server/routes/alerts/details/handlers.ts | 2 +- .../server/routes/{metadata.ts => metadata/index.ts} | 9 +++------ .../server/routes/{ => metadata}/metadata.test.ts | 10 +++++----- .../metadata/query_builders.test.ts} | 5 +---- .../metadata/query_builders.ts} | 4 ++-- 6 files changed, 13 insertions(+), 19 deletions(-) rename x-pack/plugins/endpoint/server/routes/{metadata.ts => metadata/index.ts} (93%) rename x-pack/plugins/endpoint/server/routes/{ => metadata}/metadata.test.ts (96%) rename x-pack/plugins/endpoint/server/{services/endpoint/metadata_query_builders.test.ts => routes/metadata/query_builders.test.ts} (97%) rename x-pack/plugins/endpoint/server/{services/endpoint/metadata_query_builders.ts => routes/metadata/query_builders.ts} (100%) diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index 6d2e9e510551ae9..d3a399124124f20 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -9,9 +9,9 @@ import { PluginSetupContract as FeaturesPluginSetupContract } from '../../featur import { createConfig$, EndpointConfigType } from './config'; import { EndpointAppContext } from './types'; -import { registerEndpointRoutes } from './routes/metadata'; import { registerAlertRoutes } from './routes/alerts'; import { registerResolverRoutes } from './routes/resolver'; +import { registerEndpointRoutes } from './routes/metadata'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index b95c1aaf87c1488..725e362f91ec76c 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -9,7 +9,7 @@ import { AlertEvent, EndpointAppConstants } from '../../../../common/types'; import { EndpointAppContext } from '../../../types'; import { AlertDetailsRequestParams } from '../types'; import { AlertDetailsPagination } from './lib'; -import { getHostData } from '../../../routes/metadata'; +import { getHostData } from '../../metadata'; export const alertDetailsHandlerWrapper = function( endpointAppContext: EndpointAppContext diff --git a/x-pack/plugins/endpoint/server/routes/metadata.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts similarity index 93% rename from x-pack/plugins/endpoint/server/routes/metadata.ts rename to x-pack/plugins/endpoint/server/routes/metadata/index.ts index 787ffe58a537298..ef01db9af98c4b3 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -8,12 +8,9 @@ import { IRouter, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; -import { - kibanaRequestToMetadataListESQuery, - getESQueryHostMetadataByID, -} from '../services/endpoint/metadata_query_builders'; -import { HostMetadata, HostResultList } from '../../common/types'; -import { EndpointAppContext } from '../types'; +import { HostMetadata, HostResultList } from '../../../common/types'; +import { EndpointAppContext } from '../../types'; +import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; interface HitSource { _source: HostMetadata; diff --git a/x-pack/plugins/endpoint/server/routes/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts similarity index 96% rename from x-pack/plugins/endpoint/server/routes/metadata.test.ts rename to x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index 65e07edbcde249b..e0fd11e737e7dcb 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -17,12 +17,12 @@ import { httpServerMock, httpServiceMock, loggingServiceMock, -} from '../../../../../src/core/server/mocks'; -import { HostMetadata, HostResultList } from '../../common/types'; +} from '../../../../../../src/core/server/mocks'; +import { HostMetadata, HostResultList } from '../../../common/types'; import { SearchResponse } from 'elasticsearch'; -import { registerEndpointRoutes } from './metadata'; -import { EndpointConfigSchema } from '../config'; -import * as data from '../test_data/all_metadata_data.json'; +import { EndpointConfigSchema } from '../../config'; +import * as data from '../../test_data/all_metadata_data.json'; +import { registerEndpointRoutes } from './index'; describe('test endpoint route', () => { let routerMock: jest.Mocked; diff --git a/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts similarity index 97% rename from x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts rename to x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts index 0966b52c79f7d05..2514d5aa858111f 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts @@ -5,10 +5,7 @@ */ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; -import { - kibanaRequestToMetadataListESQuery, - getESQueryHostMetadataByID, -} from './metadata_query_builders'; +import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; import { EndpointAppConstants } from '../../../common/types'; describe('query builder', () => { diff --git a/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts similarity index 100% rename from x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts rename to x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts index 57b0a4ef1051947..bd07604fe9ad248 100644 --- a/x-pack/plugins/endpoint/server/services/endpoint/metadata_query_builders.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { KibanaRequest } from 'kibana/server'; -import { EndpointAppConstants } from '../../../common/types'; -import { EndpointAppContext } from '../../types'; import { esKuery } from '../../../../../../src/plugins/data/server'; +import { EndpointAppContext } from '../../types'; +import { EndpointAppConstants } from '../../../common/types'; export const kibanaRequestToMetadataListESQuery = async ( request: KibanaRequest,