diff --git a/packages/kbn-optimizer/package.json b/packages/kbn-optimizer/package.json index d32c7489641a0..b648004760d7c 100644 --- a/packages/kbn-optimizer/package.json +++ b/packages/kbn-optimizer/package.json @@ -14,9 +14,12 @@ "@kbn/babel-preset": "1.0.0", "@kbn/dev-utils": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", + "@types/estree": "^0.0.44", "@types/loader-utils": "^1.1.3", "@types/watchpack": "^1.1.5", "@types/webpack": "^4.41.3", + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1", "autoprefixer": "^9.7.4", "babel-loader": "^8.0.6", "clean-webpack-plugin": "^3.0.0", diff --git a/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap b/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap similarity index 64% rename from packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap rename to packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap index 2973ac116d6bd..f537674c3fff7 100644 --- a/packages/kbn-optimizer/src/worker/__snapshots__/parse_path.test.ts.snap +++ b/packages/kbn-optimizer/src/common/__snapshots__/parse_path.test.ts.snap @@ -4,6 +4,7 @@ exports[`parseDirPath() parses / 1`] = ` Object { "dirs": Array [], "filename": undefined, + "query": undefined, "root": "/", } `; @@ -14,6 +15,7 @@ Object { "foo", ], "filename": undefined, + "query": undefined, "root": "/", } `; @@ -26,6 +28,7 @@ Object { "baz", ], "filename": undefined, + "query": undefined, "root": "/", } `; @@ -38,6 +41,7 @@ Object { "baz", ], "filename": undefined, + "query": undefined, "root": "/", } `; @@ -46,6 +50,7 @@ exports[`parseDirPath() parses c:\\ 1`] = ` Object { "dirs": Array [], "filename": undefined, + "query": undefined, "root": "c:", } `; @@ -56,6 +61,7 @@ Object { "foo", ], "filename": undefined, + "query": undefined, "root": "c:", } `; @@ -68,6 +74,7 @@ Object { "baz", ], "filename": undefined, + "query": undefined, "root": "c:", } `; @@ -80,6 +87,7 @@ Object { "baz", ], "filename": undefined, + "query": undefined, "root": "c:", } `; @@ -88,6 +96,7 @@ exports[`parseFilePath() parses /foo 1`] = ` Object { "dirs": Array [], "filename": "foo", + "query": undefined, "root": "/", } `; @@ -99,6 +108,7 @@ Object { "bar", ], "filename": "baz", + "query": undefined, "root": "/", } `; @@ -110,6 +120,36 @@ Object { "bar", ], "filename": "baz.json", + "query": undefined, + "root": "/", +} +`; + +exports[`parseFilePath() parses /foo/bar/baz.json?light 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "query": Object { + "light": "", + }, + "root": "/", +} +`; + +exports[`parseFilePath() parses /foo/bar/baz.json?light=true&dark=false 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "query": Object { + "dark": "false", + "light": "true", + }, "root": "/", } `; @@ -121,6 +161,7 @@ Object { "bar", ], "filename": "baz.json", + "query": undefined, "root": "c:", } `; @@ -129,6 +170,7 @@ exports[`parseFilePath() parses c:\\foo 1`] = ` Object { "dirs": Array [], "filename": "foo", + "query": undefined, "root": "c:", } `; @@ -140,6 +182,7 @@ Object { "bar", ], "filename": "baz", + "query": undefined, "root": "c:", } `; @@ -151,6 +194,36 @@ Object { "bar", ], "filename": "baz.json", + "query": undefined, + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo\\bar\\baz.json?dark 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "query": Object { + "dark": "", + }, + "root": "c:", +} +`; + +exports[`parseFilePath() parses c:\\foo\\bar\\baz.json?dark=true&light=false 1`] = ` +Object { + "dirs": Array [ + "foo", + "bar", + ], + "filename": "baz.json", + "query": Object { + "dark": "true", + "light": "false", + }, "root": "c:", } `; diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts new file mode 100644 index 0000000000000..ba19bdc9c3be7 --- /dev/null +++ b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax.ts @@ -0,0 +1,194 @@ +/* + * 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 estree from 'estree'; + +export interface DisallowedSyntaxCheck { + name: string; + nodeType: estree.Node['type'] | Array; + test?: (n: any) => boolean | void; +} + +export const checks: DisallowedSyntaxCheck[] = [ + /** + * es2015 + */ + // https://github.com/estree/estree/blob/master/es2015.md#functions + { + name: '[es2015] generator function', + nodeType: ['FunctionDeclaration', 'FunctionExpression'], + test: (n: estree.FunctionDeclaration | estree.FunctionExpression) => !!n.generator, + }, + // https://github.com/estree/estree/blob/master/es2015.md#forofstatement + { + name: '[es2015] for-of statement', + nodeType: 'ForOfStatement', + }, + // https://github.com/estree/estree/blob/master/es2015.md#variabledeclaration + { + name: '[es2015] let/const variable declaration', + nodeType: 'VariableDeclaration', + test: (n: estree.VariableDeclaration) => n.kind === 'let' || n.kind === 'const', + }, + // https://github.com/estree/estree/blob/master/es2015.md#expressions + { + name: '[es2015] `super`', + nodeType: 'Super', + }, + // https://github.com/estree/estree/blob/master/es2015.md#expressions + { + name: '[es2015] ...spread', + nodeType: 'SpreadElement', + }, + // https://github.com/estree/estree/blob/master/es2015.md#arrowfunctionexpression + { + name: '[es2015] arrow function expression', + nodeType: 'ArrowFunctionExpression', + }, + // https://github.com/estree/estree/blob/master/es2015.md#yieldexpression + { + name: '[es2015] `yield` expression', + nodeType: 'YieldExpression', + }, + // https://github.com/estree/estree/blob/master/es2015.md#templateliteral + { + name: '[es2015] template literal', + nodeType: 'TemplateLiteral', + }, + // https://github.com/estree/estree/blob/master/es2015.md#patterns + { + name: '[es2015] destructuring', + nodeType: ['ObjectPattern', 'ArrayPattern', 'AssignmentPattern'], + }, + // https://github.com/estree/estree/blob/master/es2015.md#classes + { + name: '[es2015] class', + nodeType: [ + 'ClassDeclaration', + 'ClassExpression', + 'ClassBody', + 'MethodDefinition', + 'MetaProperty', + ], + }, + + /** + * es2016 + */ + { + name: '[es2016] exponent operator', + nodeType: 'BinaryExpression', + test: (n: estree.BinaryExpression) => n.operator === '**', + }, + { + name: '[es2016] exponent assignment', + nodeType: 'AssignmentExpression', + test: (n: estree.AssignmentExpression) => n.operator === '**=', + }, + + /** + * es2017 + */ + // https://github.com/estree/estree/blob/master/es2017.md#function + { + name: '[es2017] async function', + nodeType: ['FunctionDeclaration', 'FunctionExpression'], + test: (n: estree.FunctionDeclaration | estree.FunctionExpression) => n.async, + }, + // https://github.com/estree/estree/blob/master/es2017.md#awaitexpression + { + name: '[es2017] await expression', + nodeType: 'AwaitExpression', + }, + + /** + * es2018 + */ + // https://github.com/estree/estree/blob/master/es2018.md#statements + { + name: '[es2018] for-await-of statements', + nodeType: 'ForOfStatement', + test: (n: estree.ForOfStatement) => n.await, + }, + // https://github.com/estree/estree/blob/master/es2018.md#expressions + { + name: '[es2018] object spread properties', + nodeType: 'ObjectExpression', + test: (n: estree.ObjectExpression) => n.properties.some(p => p.type === 'SpreadElement'), + }, + // https://github.com/estree/estree/blob/master/es2018.md#template-literals + { + name: '[es2018] tagged template literal with invalid escape', + nodeType: 'TemplateElement', + test: (n: estree.TemplateElement) => n.value.cooked === null, + }, + // https://github.com/estree/estree/blob/master/es2018.md#patterns + { + name: '[es2018] rest properties', + nodeType: 'ObjectPattern', + test: (n: estree.ObjectPattern) => n.properties.some(p => p.type === 'RestElement'), + }, + + /** + * es2019 + */ + // https://github.com/estree/estree/blob/master/es2019.md#catchclause + { + name: '[es2019] catch clause without a binding', + nodeType: 'CatchClause', + test: (n: estree.CatchClause) => !n.param, + }, + + /** + * es2020 + */ + // https://github.com/estree/estree/blob/master/es2020.md#bigintliteral + { + name: '[es2020] bigint literal', + nodeType: 'Literal', + test: (n: estree.Literal) => typeof n.value === 'bigint', + }, + + /** + * webpack transforms import/export in order to support tree shaking and async imports + * + * // https://github.com/estree/estree/blob/master/es2020.md#importexpression + * { + * name: '[es2020] import expression', + * nodeType: 'ImportExpression', + * }, + * // https://github.com/estree/estree/blob/master/es2020.md#exportalldeclaration + * { + * name: '[es2020] export all declaration', + * nodeType: 'ExportAllDeclaration', + * }, + * + */ +]; + +export const checksByNodeType = new Map(); +for (const check of checks) { + const nodeTypes = Array.isArray(check.nodeType) ? check.nodeType : [check.nodeType]; + for (const nodeType of nodeTypes) { + if (!checksByNodeType.has(nodeType)) { + checksByNodeType.set(nodeType, []); + } + checksByNodeType.get(nodeType)!.push(check); + } +} diff --git a/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts new file mode 100644 index 0000000000000..7377462eb267b --- /dev/null +++ b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/disallowed_syntax_plugin.ts @@ -0,0 +1,73 @@ +/* + * 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 webpack from 'webpack'; +import acorn from 'acorn'; +import * as AcornWalk from 'acorn-walk'; + +import { checksByNodeType, DisallowedSyntaxCheck } from './disallowed_syntax'; +import { parseFilePath } from '../parse_path'; + +export class DisallowedSyntaxPlugin { + apply(compiler: webpack.Compiler) { + compiler.hooks.normalModuleFactory.tap(DisallowedSyntaxPlugin.name, factory => { + factory.hooks.parser.for('javascript/auto').tap(DisallowedSyntaxPlugin.name, parser => { + parser.hooks.program.tap(DisallowedSyntaxPlugin.name, (program: acorn.Node) => { + const module = parser.state?.current; + if (!module || !module.resource) { + return; + } + + const resource: string = module.resource; + const { dirs } = parseFilePath(resource); + + if (!dirs.includes('node_modules')) { + return; + } + + const failedChecks = new Set(); + + AcornWalk.full(program, node => { + const checks = checksByNodeType.get(node.type as any); + if (!checks) { + return; + } + + for (const check of checks) { + if (!check.test || check.test(node)) { + failedChecks.add(check); + } + } + }); + + if (!failedChecks.size) { + return; + } + + // throw an error to trigger a parse failure, causing this module to be reported as invalid + throw new Error( + `disallowed syntax found in file ${resource}:\n - ${Array.from(failedChecks) + .map(c => c.name) + .join('\n - ')}` + ); + }); + }); + }); + } +} diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts similarity index 92% rename from src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts rename to packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts index 4b69616a777e9..ca5ba1b90fe95 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/index.ts +++ b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from '../../../../../../plugins/embeddable/public'; +export * from './disallowed_syntax_plugin'; diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index ea0560f132153..c51905be04565 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -26,3 +26,5 @@ export * from './ts_helpers'; export * from './rxjs_helpers'; export * from './array_helpers'; export * from './event_stream_helpers'; +export * from './disallowed_syntax_plugin'; +export * from './parse_path'; diff --git a/packages/kbn-optimizer/src/worker/parse_path.test.ts b/packages/kbn-optimizer/src/common/parse_path.test.ts similarity index 83% rename from packages/kbn-optimizer/src/worker/parse_path.test.ts rename to packages/kbn-optimizer/src/common/parse_path.test.ts index 72197e8c8fb07..61be44348cfae 100644 --- a/packages/kbn-optimizer/src/worker/parse_path.test.ts +++ b/packages/kbn-optimizer/src/common/parse_path.test.ts @@ -21,7 +21,15 @@ import { parseFilePath, parseDirPath } from './parse_path'; const DIRS = ['/', '/foo/bar/baz/', 'c:\\', 'c:\\foo\\bar\\baz\\']; const AMBIGUOUS = ['/foo', '/foo/bar/baz', 'c:\\foo', 'c:\\foo\\bar\\baz']; -const FILES = ['/foo/bar/baz.json', 'c:/foo/bar/baz.json', 'c:\\foo\\bar\\baz.json']; +const FILES = [ + '/foo/bar/baz.json', + 'c:/foo/bar/baz.json', + 'c:\\foo\\bar\\baz.json', + '/foo/bar/baz.json?light', + '/foo/bar/baz.json?light=true&dark=false', + 'c:\\foo\\bar\\baz.json?dark', + 'c:\\foo\\bar\\baz.json?dark=true&light=false', +]; describe('parseFilePath()', () => { it.each([...FILES, ...AMBIGUOUS])('parses %s', path => { diff --git a/packages/kbn-optimizer/src/worker/parse_path.ts b/packages/kbn-optimizer/src/common/parse_path.ts similarity index 83% rename from packages/kbn-optimizer/src/worker/parse_path.ts rename to packages/kbn-optimizer/src/common/parse_path.ts index 88152df55b84f..4c96417800252 100644 --- a/packages/kbn-optimizer/src/worker/parse_path.ts +++ b/packages/kbn-optimizer/src/common/parse_path.ts @@ -18,6 +18,7 @@ */ import normalizePath from 'normalize-path'; +import Qs from 'querystring'; /** * Parse an absolute path, supporting normalized paths from webpack, @@ -33,11 +34,19 @@ export function parseDirPath(path: string) { } export function parseFilePath(path: string) { - const normalized = normalizePath(path); + let normalized = normalizePath(path); + let query; + const queryIndex = normalized.indexOf('?'); + if (queryIndex !== -1) { + query = Qs.parse(normalized.slice(queryIndex + 1)); + normalized = normalized.slice(0, queryIndex); + } + const [root, ...others] = normalized.split('/'); return { root: root === '' ? '/' : root, dirs: others.slice(0, -1), + query, filename: others[others.length - 1] || undefined, }; } diff --git a/packages/kbn-optimizer/src/index.ts b/packages/kbn-optimizer/src/index.ts index 48777f1d54aaf..8026cf39db73d 100644 --- a/packages/kbn-optimizer/src/index.ts +++ b/packages/kbn-optimizer/src/index.ts @@ -20,3 +20,4 @@ export { OptimizerConfig } from './optimizer'; export * from './run_optimizer'; export * from './log_optimizer_state'; +export * from './common/disallowed_syntax_plugin'; diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts index e87ddc7d0185c..0dfce4b5addba 100644 --- a/packages/kbn-optimizer/src/worker/run_compilers.ts +++ b/packages/kbn-optimizer/src/worker/run_compilers.ts @@ -27,10 +27,17 @@ import webpack, { Stats } from 'webpack'; import * as Rx from 'rxjs'; import { mergeMap, map, mapTo, takeUntil } from 'rxjs/operators'; -import { CompilerMsgs, CompilerMsg, maybeMap, Bundle, WorkerConfig, ascending } from '../common'; +import { + CompilerMsgs, + CompilerMsg, + maybeMap, + Bundle, + WorkerConfig, + ascending, + parseFilePath, +} from '../common'; import { getWebpackConfig } from './webpack.config'; import { isFailureStats, failedStatsToErrorMessage } from './webpack_helpers'; -import { parseFilePath } from './parse_path'; import { isExternalModule, isNormalModule, diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index dabfed7f9725c..9337daf419bfa 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -29,8 +29,7 @@ import webpackMerge from 'webpack-merge'; import { CleanWebpackPlugin } from 'clean-webpack-plugin'; import * as SharedDeps from '@kbn/ui-shared-deps'; -import { Bundle, WorkerConfig } from '../common'; -import { parseDirPath } from './parse_path'; +import { Bundle, WorkerConfig, parseDirPath, DisallowedSyntaxPlugin } from '../common'; const IS_CODE_COVERAGE = !!process.env.CODE_COVERAGE; const ISTANBUL_PRESET_PATH = require.resolve('@kbn/babel-preset/istanbul_preset'); @@ -77,7 +76,7 @@ export function getWebpackConfig(bundle: Bundle, worker: WorkerConfig) { ...SharedDeps.externals, }, - plugins: [new CleanWebpackPlugin()], + plugins: [new CleanWebpackPlugin(), new DisallowedSyntaxPlugin()], module: { // no parse rules for a few known large packages which have no require() statements diff --git a/packages/kbn-optimizer/src/worker/webpack_helpers.ts b/packages/kbn-optimizer/src/worker/webpack_helpers.ts index a11c85c64198e..e30920b960144 100644 --- a/packages/kbn-optimizer/src/worker/webpack_helpers.ts +++ b/packages/kbn-optimizer/src/worker/webpack_helpers.ts @@ -18,7 +18,6 @@ */ import webpack from 'webpack'; -import { defaults } from 'lodash'; // @ts-ignore import Stats from 'webpack/lib/Stats'; @@ -55,12 +54,14 @@ const STATS_WARNINGS_FILTER = new RegExp( ); export function failedStatsToErrorMessage(stats: webpack.Stats) { - const details = stats.toString( - defaults( - { colors: true, warningsFilter: STATS_WARNINGS_FILTER }, - Stats.presetToOptions('minimal') - ) - ); + const details = stats.toString({ + ...Stats.presetToOptions('minimal'), + colors: true, + warningsFilter: STATS_WARNINGS_FILTER, + errors: true, + errorDetails: true, + moduleTrace: true, + }); return `Optimizations failure.\n${details.split('\n').join('\n ')}`; } diff --git a/renovate.json5 b/renovate.json5 index 57f175d1afc8e..ffa006264873d 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -265,6 +265,14 @@ '(\\b|_)eslint(\\b|_)', ], }, + { + groupSlug: 'estree', + groupName: 'estree related packages', + packageNames: [ + 'estree', + '@types/estree', + ], + }, { groupSlug: 'fancy-log', groupName: 'fancy-log related packages', diff --git a/src/legacy/core_plugins/embeddable_api/README.md b/src/legacy/core_plugins/embeddable_api/README.md deleted file mode 100644 index c2f67572df873..0000000000000 --- a/src/legacy/core_plugins/embeddable_api/README.md +++ /dev/null @@ -1,2 +0,0 @@ -- Embeddables have been moved to `/src/plugins/embeddable` NP plugin. -- This legacy plugin is still there to make necessary CSS working, but soon will be completely deleted. diff --git a/src/legacy/core_plugins/embeddable_api/package.json b/src/legacy/core_plugins/embeddable_api/package.json deleted file mode 100644 index f625408fe4c6c..0000000000000 --- a/src/legacy/core_plugins/embeddable_api/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "embeddable_api", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/mocks.ts b/src/legacy/core_plugins/embeddable_api/public/np_ready/public/mocks.ts deleted file mode 100644 index 10510bff0c97e..0000000000000 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/mocks.ts +++ /dev/null @@ -1,21 +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. - */ - -// eslint-disable-next-line -export * from '../../../../../../plugins/embeddable/public/mocks'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx index e21033ffe10ec..cc7299b884890 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app.tsx @@ -21,7 +21,7 @@ import moment from 'moment'; import { Subscription } from 'rxjs'; import { History } from 'history'; -import { ViewMode } from '../../../../embeddable_api/public/np_ready/public'; +import { ViewMode } from '../../../../../../plugins/embeddable/public'; import { SavedObjectDashboard } from '../../../../../../plugins/dashboard/public'; import { DashboardAppState, SavedDashboardPanel } from './types'; import { diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx index 0c6686c993371..a39266ecd8db3 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/dashboard_app_controller.tsx @@ -58,7 +58,7 @@ import { isErrorEmbeddable, openAddPanelFlyout, ViewMode, -} from '../../../../embeddable_api/public/np_ready/public'; +} from '../../../../../../plugins/embeddable/public'; import { NavAction, SavedDashboardPanel } from './types'; import { showOptionsPopover } from './top_nav/show_options_popover'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.test.ts index b2a2f43b9152d..d3c3dc46c7057 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.test.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/lib/embeddable_saved_object_converters.test.ts @@ -24,7 +24,7 @@ import { } from './embeddable_saved_object_converters'; import { SavedDashboardPanel } from '../types'; import { DashboardPanelState } from 'src/plugins/dashboard/public'; -import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { EmbeddableInput } from 'src/plugins/embeddable/public'; interface CustomInput extends EmbeddableInput { something: string; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts index d09b7612af49c..3cb8bce80fa41 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/embeddable/search_embeddable.ts @@ -34,7 +34,7 @@ import { Query, IFieldType, } from '../../../../../../../plugins/data/public'; -import { Container, Embeddable } from '../../../../../embeddable_api/public/np_ready/public'; +import { Container, Embeddable } from '../../../../../../../plugins/embeddable/public'; import * as columnActions from '../angular/doc_table/actions/columns'; import searchTemplate from './search_template.html'; import { ISearchEmbeddable, SearchInput, SearchOutput } from './types'; diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index 4b7618712cdd8..a66d3b24732f0 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -195,6 +195,7 @@ export default () => }), workers: Joi.number().min(1), profile: Joi.boolean().default(false), + validateSyntaxOfNodeModules: Joi.boolean().default(true), }).default(), status: Joi.object({ allowAnonymous: Joi.boolean().default(false), diff --git a/src/legacy/ui/public/chrome/api/sub_url_hooks.js b/src/legacy/ui/public/chrome/api/sub_url_hooks.js index 27d147b1ffc72..f147aef7b4b7d 100644 --- a/src/legacy/ui/public/chrome/api/sub_url_hooks.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks.js @@ -17,11 +17,10 @@ * under the License. */ -import url from 'url'; - import { unhashUrl } from '../../../../../plugins/kibana_utils/public'; import { toastNotifications } from '../../notify/toasts'; import { npSetup } from '../../new_platform'; +import { areHashesDifferentButDecodedHashesEquals } from './sub_url_hooks_utils'; export function registerSubUrlHooks(angularModule, internals) { angularModule.run(($rootScope, Private, $location) => { @@ -49,17 +48,10 @@ export function registerSubUrlHooks(angularModule, internals) { $rootScope.$on('$locationChangeStart', (e, newUrl) => { // This handler fixes issue #31238 where browser back navigation // fails due to angular 1.6 parsing url encoded params wrong. - const parsedAbsUrl = url.parse($location.absUrl()); - const absUrlHash = parsedAbsUrl.hash ? parsedAbsUrl.hash.slice(1) : ''; - const decodedAbsUrlHash = decodeURIComponent(absUrlHash); - - const parsedNewUrl = url.parse(newUrl); - const newHash = parsedNewUrl.hash ? parsedNewUrl.hash.slice(1) : ''; - const decodedHash = decodeURIComponent(newHash); - - if (absUrlHash !== newHash && decodedHash === decodedAbsUrlHash) { + if (areHashesDifferentButDecodedHashesEquals($location.absUrl(), newUrl)) { // replace the urlencoded hash with the version that angular sees. - $location.url(absUrlHash).replace(); + const newHash = newUrl.split('#')[1] || ''; + $location.url(newHash).replace(); } }); diff --git a/src/legacy/ui/public/chrome/api/sub_url_hooks_utils.test.ts b/src/legacy/ui/public/chrome/api/sub_url_hooks_utils.test.ts new file mode 100644 index 0000000000000..4dec526302344 --- /dev/null +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks_utils.test.ts @@ -0,0 +1,58 @@ +/* + * 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 { areHashesDifferentButDecodedHashesEquals } from './sub_url_hooks_utils'; + +test('false for different hashes', () => { + const url1 = `https://localhost/kibana/#/dashboard/id`; + const url2 = `https://localhost/kibana/#/dashboard/DIFFERENT`; + expect(areHashesDifferentButDecodedHashesEquals(url1, url2)).toBeFalsy(); +}); + +test('false for same hashes', () => { + const hash = `/dashboard/id?_a=(filters:!(),query:(language:kuery,query:''))&_g=(filters:!(),time:(from:now-120m,to:now))`; + const url1 = `https://localhost/kibana/#/${hash}`; + expect(areHashesDifferentButDecodedHashesEquals(url1, url1)).toBeFalsy(); +}); + +test('true for same hashes, but one is encoded', () => { + const hash = `/dashboard/id?_a=(filters:!(),query:(language:kuery,query:''))&_g=(filters:!(),time:(from:now-120m,to:now))`; + const url1 = `https://localhost/kibana/#/${hash}`; + const url2 = `https://localhost/kibana/#/${encodeURIComponent(hash)}`; + expect(areHashesDifferentButDecodedHashesEquals(url1, url2)).toBeTruthy(); +}); + +/** + * This edge case occurs when trying to navigate within kibana app using core's `navigateToApp` api + * and there is reserved characters in hash (see: query:'' part) + * For example: + * ```ts + * navigateToApp('kibana', { + * path: '#/dashboard/f8bc19f0-6918-11ea-9258-a74c2ded064d?_a=(filters:!(),query:(language:kuery,query:''))&_g=(filters:!(),time:(from:now-120m,to:now))' + * }) + * ``` + * Core internally is using url.parse which parses ' -> %27 and performs the navigation + * Then angular decodes it back and causes redundant history record if not the fix which is covered by the test below + */ +test("true for same hashes, but one has reserved character (') encoded", () => { + const hash = `/dashboard/id?_a=(filters:!(),query:(language:kuery,query:''))&_g=(filters:!(),time:(from:now-120m,to:now))`; + const url1 = `https://localhost/kibana/#/${hash}`; + const url2 = `https://localhost/kibana/#/${hash.replace(/\'/g, '%27')}`; + expect(areHashesDifferentButDecodedHashesEquals(url1, url2)).toBeTruthy(); +}); diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js b/src/legacy/ui/public/chrome/api/sub_url_hooks_utils.ts similarity index 67% rename from test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js rename to src/legacy/ui/public/chrome/api/sub_url_hooks_utils.ts index b2497a824ba2b..8517877acd387 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js +++ b/src/legacy/ui/public/chrome/api/sub_url_hooks_utils.ts @@ -17,10 +17,13 @@ * under the License. */ -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - hacks: ['plugins/kbn_tp_custom_visualizations/self_changing_vis/self_changing_vis'], - }, - }); +export function areHashesDifferentButDecodedHashesEquals(urlA: string, urlB: string): boolean { + const getHash = (url: string) => url.split('#')[1] ?? ''; + const hashA = getHash(urlA); + const decodedHashA = decodeURIComponent(hashA); + + const hashB = getHash(urlB); + const decodedHashB = decodeURIComponent(hashB); + + return hashA !== hashB && decodedHashA === decodedHashB; } diff --git a/src/legacy/ui/ui_bundles/ui_bundles_controller.js b/src/legacy/ui/ui_bundles/ui_bundles_controller.js index 1a78569e874f2..7afa283af83e0 100644 --- a/src/legacy/ui/ui_bundles/ui_bundles_controller.js +++ b/src/legacy/ui/ui_bundles/ui_bundles_controller.js @@ -73,6 +73,7 @@ export class UiBundlesController { this._workingDir = config.get('optimize.bundleDir'); this._env = config.get('env.name'); + this._validateSyntaxOfNodeModules = config.get('optimize.validateSyntaxOfNodeModules'); this._context = { env: config.get('env.name'), sourceMaps: config.get('optimize.sourceMaps'), @@ -135,6 +136,10 @@ export class UiBundlesController { return this._env === 'development'; } + shouldValidateSyntaxOfNodeModules() { + return !!this._validateSyntaxOfNodeModules; + } + getWebpackPluginProviders() { return this._webpackPluginProviders || []; } diff --git a/src/optimize/dynamic_dll_plugin/dll_config_model.js b/src/optimize/dynamic_dll_plugin/dll_config_model.js index 9ca6071b8f515..eec369b194fef 100644 --- a/src/optimize/dynamic_dll_plugin/dll_config_model.js +++ b/src/optimize/dynamic_dll_plugin/dll_config_model.js @@ -28,6 +28,7 @@ import * as UiSharedDeps from '@kbn/ui-shared-deps'; function generateDLL(config) { const { dllAlias, + dllValidateSyntax, dllNoParseRules, dllContext, dllEntry, @@ -44,6 +45,22 @@ function generateDLL(config) { const BABEL_PRESET_PATH = require.resolve('@kbn/babel-preset/webpack_preset'); const BABEL_EXCLUDE_RE = [/[\/\\](webpackShims|node_modules|bower_components)[\/\\]/]; + /** + * Wrap plugin loading in a function so that we can require + * `@kbn/optimizer` only when absolutely necessary since we + * don't ship this package in the distributable but this code + * is still shipped, though it's not used. + */ + const getValidateSyntaxPlugins = () => { + if (!dllValidateSyntax) { + return []; + } + + // only require @kbn/optimizer + const { DisallowedSyntaxPlugin } = require('@kbn/optimizer'); + return [new DisallowedSyntaxPlugin()]; + }; + return { entry: dllEntry, context: dllContext, @@ -140,6 +157,7 @@ function generateDLL(config) { new MiniCssExtractPlugin({ filename: dllStyleFilename, }), + ...getValidateSyntaxPlugins(), ], // Single runtime for the dll bundles which assures that common transient dependencies won't be evaluated twice. // The module cache will be shared, even when module code may be duplicated across chunks. @@ -163,6 +181,7 @@ function generateDLL(config) { function extendRawConfig(rawConfig) { // Build all extended configs from raw config const dllAlias = rawConfig.uiBundles.getAliases(); + const dllValidateSyntax = rawConfig.uiBundles.shouldValidateSyntaxOfNodeModules(); const dllNoParseRules = rawConfig.uiBundles.getWebpackNoParseRules(); const dllDevMode = rawConfig.uiBundles.isDevMode(); const dllContext = rawConfig.context; @@ -195,6 +214,7 @@ function extendRawConfig(rawConfig) { // Export dll config map return { dllAlias, + dllValidateSyntax, dllNoParseRules, dllDevMode, dllContext, diff --git a/src/plugins/dashboard/public/url_generator.test.ts b/src/plugins/dashboard/public/url_generator.test.ts index 5dfc47b694f60..d48aacc1d8c1e 100644 --- a/src/plugins/dashboard/public/url_generator.test.ts +++ b/src/plugins/dashboard/public/url_generator.test.ts @@ -21,6 +21,7 @@ import { createDirectAccessDashboardLinkGenerator } from './url_generator'; import { hashedItemStore } from '../../kibana_utils/public'; // eslint-disable-next-line import { mockStorage } from '../../kibana_utils/public/storage/hashed_item_store/mock'; +import { esFilters } from '../../data/public'; const APP_BASE_PATH: string = 'xyz/app/kibana'; @@ -50,12 +51,13 @@ describe('dashboard url generator', () => { ); }); - test('creates a link with filters, time range and query to a saved object', async () => { + test('creates a link with filters, time range, refresh interval and query to a saved object', async () => { const generator = createDirectAccessDashboardLinkGenerator(() => Promise.resolve({ appBasePath: APP_BASE_PATH, useHashedUrl: false }) ); const url = await generator.createUrl!({ timeRange: { to: 'now', from: 'now-15m', mode: 'relative' }, + refreshInterval: { pause: false, value: 300 }, dashboardId: '123', filters: [ { @@ -66,11 +68,22 @@ describe('dashboard url generator', () => { }, query: { query: 'hi' }, }, + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { query: 'hi' }, + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + }, ], query: { query: 'bye', language: 'kuery' }, }); expect(url).toMatchInlineSnapshot( - `"xyz/app/kibana#/dashboard/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(time:(from:now-15m,mode:relative,to:now))"` + `"xyz/app/kibana#/dashboard/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))"` ); }); diff --git a/src/plugins/dashboard/public/url_generator.ts b/src/plugins/dashboard/public/url_generator.ts index 5f1255bc9d45f..0fdf395e75bca 100644 --- a/src/plugins/dashboard/public/url_generator.ts +++ b/src/plugins/dashboard/public/url_generator.ts @@ -17,7 +17,14 @@ * under the License. */ -import { TimeRange, Filter, Query } from '../../data/public'; +import { + TimeRange, + Filter, + Query, + esFilters, + QueryState, + RefreshInterval, +} from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { UrlGeneratorsDefinition, UrlGeneratorState } from '../../share/public'; @@ -36,10 +43,15 @@ export type DashboardAppLinkGeneratorState = UrlGeneratorState<{ * Optionally set the time range in the time picker. */ timeRange?: TimeRange; + + /** + * Optionally set the refresh interval. + */ + refreshInterval?: RefreshInterval; + /** * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the - * saved dashboard has filters saved with it, this will _replace_ those filters. This will set - * app filters, not global filters. + * saved dashboard has filters saved with it, this will _replace_ those filters. */ filters?: Filter[]; /** @@ -64,21 +76,32 @@ export const createDirectAccessDashboardLinkGenerator = ( const appBasePath = startServices.appBasePath; const hash = state.dashboardId ? `dashboard/${state.dashboardId}` : `dashboard`; + const cleanEmptyKeys = (stateObj: Record) => { + Object.keys(stateObj).forEach(key => { + if (stateObj[key] === undefined) { + delete stateObj[key]; + } + }); + return stateObj; + }; + const appStateUrl = setStateToKbnUrl( STATE_STORAGE_KEY, - { + cleanEmptyKeys({ query: state.query, - filters: state.filters, - }, + filters: state.filters?.filter(f => !esFilters.isFilterPinned(f)), + }), { useHash }, `${appBasePath}#/${hash}` ); - return setStateToKbnUrl( + return setStateToKbnUrl( GLOBAL_STATE_STORAGE_KEY, - { + cleanEmptyKeys({ time: state.timeRange, - }, + filters: state.filters?.filter(f => esFilters.isFilterPinned(f)), + refreshInterval: state.refreshInterval, + }), { useHash }, appStateUrl ); diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts index b0bb2f754d6cf..0c3947ade8221 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.test.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.test.ts @@ -197,6 +197,22 @@ describe('filter manager utilities', () => { expect(compareFilters([f1], [f2], COMPARE_ALL_OPTIONS)).toBeTruthy(); }); + test('should compare alias with alias true', () => { + const f1 = { + $state: { store: FilterStateStore.GLOBAL_STATE }, + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + const f2 = { + $state: { store: FilterStateStore.GLOBAL_STATE }, + ...buildQueryFilter({ _type: { match: { query: 'apache', type: 'phrase' } } }, 'index', ''), + }; + + f2.meta.alias = 'wassup'; + f2.meta.alias = 'dog'; + + expect(compareFilters([f1], [f2], { alias: true })).toBeFalsy(); + }); + test('should compare alias with COMPARE_ALL_OPTIONS', () => { const f1 = { $state: { store: FilterStateStore.GLOBAL_STATE }, diff --git a/src/plugins/data/common/query/filter_manager/compare_filters.ts b/src/plugins/data/common/query/filter_manager/compare_filters.ts index e047d5e0665d5..3be52a9a60977 100644 --- a/src/plugins/data/common/query/filter_manager/compare_filters.ts +++ b/src/plugins/data/common/query/filter_manager/compare_filters.ts @@ -46,7 +46,7 @@ const mapFilter = ( if (comparators.negate) cleaned.negate = filter.meta && Boolean(filter.meta.negate); if (comparators.disabled) cleaned.disabled = filter.meta && Boolean(filter.meta.disabled); - if (comparators.disabled) cleaned.alias = filter.meta?.alias; + if (comparators.alias) cleaned.alias = filter.meta?.alias; return cleaned; }; diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts index 5da929c441cde..06e4c1c8be6d5 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.test.ts @@ -463,3 +463,97 @@ describe('connect_to_app_state', () => { }); }); }); + +describe('filters with different state', () => { + let queryServiceStart: QueryStart; + let filterManager: FilterManager; + let state: BaseStateContainer; + let stateSub: Subscription; + let stateChangeTriggered = jest.fn(); + let filterManagerChangeSub: Subscription; + let filterManagerChangeTriggered = jest.fn(); + + let filter: Filter; + + beforeEach(() => { + const queryService = new QueryService(); + queryService.setup({ + uiSettings: setupMock.uiSettings, + storage: new Storage(new StubBrowserStorage()), + }); + queryServiceStart = queryService.start(startMock.savedObjects); + filterManager = queryServiceStart.filterManager; + + state = createStateContainer({}); + stateChangeTriggered = jest.fn(); + stateSub = state.state$.subscribe(stateChangeTriggered); + + filterManagerChangeTriggered = jest.fn(); + filterManagerChangeSub = filterManager.getUpdates$().subscribe(filterManagerChangeTriggered); + + filter = getFilter(FilterStateStore.GLOBAL_STATE, true, true, 'key1', 'value1'); + }); + + // applies filter state changes, changes only internal $state.store value + function runChanges() { + filter = { ...filter, $state: { store: FilterStateStore.GLOBAL_STATE } }; + + state.set({ + filters: [filter], + }); + + filter = { ...filter, $state: { store: FilterStateStore.APP_STATE } }; + + state.set({ + filters: [filter], + }); + + filter = { ...filter }; + delete filter.$state; + + state.set({ + filters: [filter], + }); + } + + test('when syncing all filters, changes to filter.state$ should be taken into account', () => { + const stop = connectToQueryState(queryServiceStart, state, { + filters: true, + }); + + runChanges(); + + expect(filterManagerChangeTriggered).toBeCalledTimes(3); + + stop(); + }); + + test('when syncing app state filters, changes to filter.state$ should be ignored', () => { + const stop = connectToQueryState(queryServiceStart, state, { + filters: FilterStateStore.APP_STATE, + }); + + runChanges(); + + expect(filterManagerChangeTriggered).toBeCalledTimes(1); + + stop(); + }); + + test('when syncing global state filters, changes to filter.state$ should be ignored', () => { + const stop = connectToQueryState(queryServiceStart, state, { + filters: FilterStateStore.GLOBAL_STATE, + }); + + runChanges(); + + expect(filterManagerChangeTriggered).toBeCalledTimes(1); + + stop(); + }); + + afterEach(() => { + stateSub.unsubscribe(); + filterManagerChangeSub.unsubscribe(); + }); +}); diff --git a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts index 331d8969f2483..3256c1cbd65a1 100644 --- a/src/plugins/data/public/query/state_sync/connect_to_query_state.ts +++ b/src/plugins/data/public/query/state_sync/connect_to_query_state.ts @@ -91,7 +91,10 @@ export const connectToQueryState = ( } else if (syncConfig.filters === FilterStateStore.GLOBAL_STATE) { if ( !initialState.filters || - !compareFilters(initialState.filters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS) + !compareFilters(initialState.filters, filterManager.getGlobalFilters(), { + ...COMPARE_ALL_OPTIONS, + state: false, + }) ) { initialState.filters = filterManager.getGlobalFilters(); initialDirty = true; @@ -99,7 +102,10 @@ export const connectToQueryState = ( } else if (syncConfig.filters === FilterStateStore.APP_STATE) { if ( !initialState.filters || - !compareFilters(initialState.filters, filterManager.getAppFilters(), COMPARE_ALL_OPTIONS) + !compareFilters(initialState.filters, filterManager.getAppFilters(), { + ...COMPARE_ALL_OPTIONS, + state: false, + }) ) { initialState.filters = filterManager.getAppFilters(); initialDirty = true; @@ -173,11 +179,21 @@ export const connectToQueryState = ( filterManager.setFilters(_.cloneDeep(filters)); } } else if (syncConfig.filters === FilterStateStore.APP_STATE) { - if (!compareFilters(filters, filterManager.getAppFilters(), COMPARE_ALL_OPTIONS)) { + if ( + !compareFilters(filters, filterManager.getAppFilters(), { + ...COMPARE_ALL_OPTIONS, + state: false, + }) + ) { filterManager.setAppFilters(_.cloneDeep(filters)); } } else if (syncConfig.filters === FilterStateStore.GLOBAL_STATE) { - if (!compareFilters(filters, filterManager.getGlobalFilters(), COMPARE_ALL_OPTIONS)) { + if ( + !compareFilters(filters, filterManager.getGlobalFilters(), { + ...COMPARE_ALL_OPTIONS, + state: false, + }) + ) { filterManager.setGlobalFilters(_.cloneDeep(filters)); } } diff --git a/tasks/config/run.js b/tasks/config/run.js index 50417ebd8333d..dca0f69c35668 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -58,6 +58,7 @@ module.exports = function(grunt) { '--env.name=development', '--plugins.initialize=false', '--optimize.bundleFilter=tests', + '--optimize.validateSyntaxOfNodeModules=false', '--server.port=5610', '--migrations.skip=true', ]; diff --git a/test/plugin_functional/plugins/kbn_top_nav/kibana.json b/test/plugin_functional/plugins/kbn_top_nav/kibana.json new file mode 100644 index 0000000000000..b274e80b9ef65 --- /dev/null +++ b/test/plugin_functional/plugins/kbn_top_nav/kibana.json @@ -0,0 +1,9 @@ +{ + "id": "kbn_top_nav", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["kbn_top_nav"], + "server": false, + "ui": true, + "requiredPlugins": ["navigation"] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/kbn_top_nav/package.json b/test/plugin_functional/plugins/kbn_top_nav/package.json new file mode 100644 index 0000000000000..510d681a4a75c --- /dev/null +++ b/test/plugin_functional/plugins/kbn_top_nav/package.json @@ -0,0 +1,18 @@ +{ + "name": "kbn_top_nav", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/kbn_top_nav", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "typescript": "3.7.2" + } +} + diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx b/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx similarity index 71% rename from test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx rename to test/plugin_functional/plugins/kbn_top_nav/public/application.tsx index f77db4fe1654e..0f65e6159796b 100644 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/top_nav.tsx +++ b/test/plugin_functional/plugins/kbn_top_nav/public/application.tsx @@ -18,11 +18,15 @@ */ import React from 'react'; -import './initialize'; -import { npStart } from 'ui/new_platform'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppMountParameters } from 'kibana/public'; +import { AppPluginDependencies } from './types'; -export const AppWithTopNav = () => { - const { TopNavMenu } = npStart.plugins.navigation.ui; +export const renderApp = ( + depsStart: AppPluginDependencies, + { appBasePath, element }: AppMountParameters +) => { + const { TopNavMenu } = depsStart.navigation.ui; const config = [ { id: 'new', @@ -32,10 +36,12 @@ export const AppWithTopNav = () => { testId: 'demoNewButton', }, ]; - - return ( + render( Hey - + , + element ); + + return () => unmountComponentAtNode(element); }; diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/index.ts b/test/plugin_functional/plugins/kbn_top_nav/public/index.ts similarity index 75% rename from src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/index.ts rename to test/plugin_functional/plugins/kbn_top_nav/public/index.ts index 4f0537aff5dc2..bd478f1dd3bdb 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/lib/test_samples/index.ts +++ b/test/plugin_functional/plugins/kbn_top_nav/public/index.ts @@ -17,5 +17,8 @@ * under the License. */ -// eslint-disable-next-line -export * from '../../../../../../../../plugins/embeddable/public/lib/test_samples'; +import { PluginInitializer } from 'kibana/public'; +import { TopNavTestPlugin, TopNavTestPluginSetup, TopNavTestPluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new TopNavTestPlugin(); diff --git a/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx b/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx new file mode 100644 index 0000000000000..a433de98357fb --- /dev/null +++ b/test/plugin_functional/plugins/kbn_top_nav/public/plugin.tsx @@ -0,0 +1,65 @@ +/* + * 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 { CoreSetup, Plugin, AppMountParameters } from 'kibana/public'; +import { NavigationPublicPluginSetup } from '../../../../../src/plugins/navigation/public'; +import { AppPluginDependencies } from './types'; + +export class TopNavTestPlugin implements Plugin { + public setup(core: CoreSetup, { navigation }: { navigation: NavigationPublicPluginSetup }) { + const customExtension = { + id: 'registered-prop', + label: 'Registered Button', + description: 'Registered Demo', + run() {}, + testId: 'demoRegisteredNewButton', + }; + + navigation.registerMenuItem(customExtension); + + const customDiscoverExtension = { + id: 'registered-discover-prop', + label: 'Registered Discover Button', + description: 'Registered Discover Demo', + run() {}, + testId: 'demoDiscoverRegisteredNewButton', + appName: 'discover', + }; + + navigation.registerMenuItem(customDiscoverExtension); + + core.application.register({ + id: 'topNavMenu', + title: 'Top nav menu example', + async mount(params: AppMountParameters) { + const { renderApp } = await import('./application'); + const services = await core.getStartServices(); + return renderApp(services[1] as AppPluginDependencies, params); + }, + }); + + return {}; + } + + public start() {} + public stop() {} +} + +export type TopNavTestPluginSetup = ReturnType; +export type TopNavTestPluginStart = ReturnType; diff --git a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy.ts b/test/plugin_functional/plugins/kbn_top_nav/public/types.ts similarity index 81% rename from src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy.ts rename to test/plugin_functional/plugins/kbn_top_nav/public/types.ts index 5357c2458e3b0..c70a78bedb54f 100644 --- a/src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy.ts +++ b/test/plugin_functional/plugins/kbn_top_nav/public/types.ts @@ -17,8 +17,8 @@ * under the License. */ -// eslint-disable-next-line -import { npSetup, npStart } from 'ui/new_platform'; +import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; -export const setup = npSetup.plugins.embeddable; -export const start = npStart.plugins.embeddable; +export interface AppPluginDependencies { + navigation: NavigationPublicPluginStart; +} diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/tsconfig.json b/test/plugin_functional/plugins/kbn_top_nav/tsconfig.json similarity index 100% rename from test/plugin_functional/plugins/kbn_tp_top_nav/tsconfig.json rename to test/plugin_functional/plugins/kbn_top_nav/tsconfig.json diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json new file mode 100644 index 0000000000000..622cbd80090ba --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "kbn_tp_custom_visualizations", + "version": "0.0.1", + "kibanaVersion": "kibana", + "requiredPlugins": [ + "visualizations" + ], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json index 344aae30b5bbc..9ee7845816faa 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json @@ -1,6 +1,7 @@ { "name": "kbn_tp_custom_visualizations", "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/kbn_tp_custom_visualizations", "kibana": { "version": "kibana", "templateVersion": "1.0.0" @@ -9,5 +10,13 @@ "dependencies": { "@elastic/eui": "21.0.1", "react": "^16.12.0" + }, + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + }, + "devDependencies": { + "@kbn/plugin-helpers": "9.0.2", + "typescript": "3.7.2" } } diff --git a/src/legacy/core_plugins/embeddable_api/index.ts b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts similarity index 68% rename from src/legacy/core_plugins/embeddable_api/index.ts rename to test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts index 52206e3d0f105..cb821a2698479 100644 --- a/src/legacy/core_plugins/embeddable_api/index.ts +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts @@ -17,9 +17,14 @@ * under the License. */ -import { LegacyPluginApi, LegacyPluginSpec, ArrayOrItem } from 'src/legacy/plugin_discovery/types'; +import { PluginInitializer } from 'kibana/public'; +import { + CustomVisualizationsPublicPlugin, + CustomVisualizationsSetup, + CustomVisualizationsStart, +} from './plugin'; -// eslint-disable-next-line import/no-default-export -export default function(kibana: LegacyPluginApi): ArrayOrItem { - return new kibana.Plugin({}); -} +export { CustomVisualizationsPublicPlugin as Plugin }; + +export const plugin: PluginInitializer = () => + new CustomVisualizationsPublicPlugin(); diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.ts b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.ts new file mode 100644 index 0000000000000..1be4aa9ee42ae --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/plugin.ts @@ -0,0 +1,61 @@ +/* + * 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 { CoreSetup, Plugin } from 'kibana/public'; +import { VisualizationsSetup } from '../../../../../src/plugins/visualizations/public'; +import { SelfChangingEditor } from './self_changing_vis/self_changing_editor'; +import { SelfChangingComponent } from './self_changing_vis/self_changing_components'; + +export interface SetupDependencies { + visualizations: VisualizationsSetup; +} + +export class CustomVisualizationsPublicPlugin + implements Plugin { + public setup(core: CoreSetup, setupDeps: SetupDependencies) { + setupDeps.visualizations.createReactVisualization({ + name: 'self_changing_vis', + title: 'Self Changing Vis', + icon: 'controlsHorizontal', + description: + 'This visualization is able to change its own settings, that you could also set in the editor.', + visConfig: { + component: SelfChangingComponent, + defaults: { + counter: 0, + }, + }, + editorConfig: { + optionTabs: [ + { + name: 'options', + title: 'Options', + editor: SelfChangingEditor, + }, + ], + }, + requestHandler: 'none', + }); + } + + public start() {} + public stop() {} +} + +export type CustomVisualizationsSetup = ReturnType; +export type CustomVisualizationsStart = ReturnType; diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js deleted file mode 100644 index c5b074db43a1b..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.js +++ /dev/null @@ -1,52 +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 { EuiBadge } from '@elastic/eui'; - -export class SelfChangingComponent extends React.Component { - onClick = () => { - this.props.vis.params.counter++; - this.props.vis.updateState(); - }; - - render() { - return ( -
- - {this.props.vis.params.counter} - -
- ); - } - - componentDidMount() { - this.props.renderComplete(); - } - - componentDidUpdate() { - this.props.renderComplete(); - } -} diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/index.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx similarity index 59% rename from test/plugin_functional/plugins/kbn_tp_top_nav/index.js rename to test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx index b4c3e05c28b66..2f01908122457 100644 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/index.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_components.tsx @@ -17,15 +17,32 @@ * under the License. */ -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - title: 'Top Nav Menu test', - description: 'This is a sample plugin for the functional tests.', - main: 'plugins/kbn_tp_top_nav/app', - }, - hacks: ['plugins/kbn_tp_top_nav/initialize'], - }, +import React, { useEffect } from 'react'; + +import { EuiBadge } from '@elastic/eui'; + +interface SelfChangingComponentProps { + renderComplete: () => {}; + visParams: { + counter: number; + }; +} + +export function SelfChangingComponent(props: SelfChangingComponentProps) { + useEffect(() => { + props.renderComplete(); }); + + return ( +
+ {}} + data-test-subj="counter" + onClickAriaLabel="Increase counter" + color="primary" + > + {props.visParams.counter} + +
+ ); } diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx similarity index 76% rename from test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js rename to test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx index fa3a0c8b9f6fe..d3f66d708603c 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/self_changing_vis/self_changing_editor.tsx @@ -20,10 +20,15 @@ import React from 'react'; import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import { VisOptionsProps } from '../../../../../../src/legacy/core_plugins/vis_default_editor/public/vis_options_props'; -export class SelfChangingEditor extends React.Component { - onCounterChange = ev => { - this.props.setValue('counter', parseInt(ev.target.value)); +interface CounterParams { + counter: number; +} + +export class SelfChangingEditor extends React.Component> { + onCounterChange = (ev: any) => { + this.props.setValue('counter', parseInt(ev.target.value, 10)); }; render() { diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json new file mode 100644 index 0000000000000..d8096d9aab27a --- /dev/null +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true, + "types": [ + "node", + "jest", + "react" + ] + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*", + ], + "exclude": [] +} \ No newline at end of file diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/package.json b/test/plugin_functional/plugins/kbn_tp_top_nav/package.json deleted file mode 100644 index 7102d24d3292d..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "kbn_tp_top_nav", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0" -} diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js b/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js deleted file mode 100644 index e7f97e68c086d..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/app.js +++ /dev/null @@ -1,54 +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 { render, unmountComponentAtNode } from 'react-dom'; - -import { uiModules } from 'ui/modules'; -import chrome from 'ui/chrome'; - -// This is required so some default styles and required scripts/Angular modules are loaded, -// or the timezone setting is correctly applied. -import 'ui/autoload/all'; - -import { AppWithTopNav } from './top_nav'; - -const app = uiModules.get('apps/topnavDemoPlugin', ['kibana']); - -app.config($locationProvider => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); -}); - -function RootController($scope, $element) { - const domNode = $element[0]; - - // render react to DOM - render(, domNode); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); -} - -chrome.setRootController('topnavDemoPlugin', RootController); diff --git a/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js b/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js deleted file mode 100644 index d46e47f6d248a..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_top_nav/public/initialize.js +++ /dev/null @@ -1,41 +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 { npSetup } from 'ui/new_platform'; - -const customExtension = { - id: 'registered-prop', - label: 'Registered Button', - description: 'Registered Demo', - run() {}, - testId: 'demoRegisteredNewButton', -}; - -npSetup.plugins.navigation.registerMenuItem(customExtension); - -const customDiscoverExtension = { - id: 'registered-discover-prop', - label: 'Registered Discover Button', - description: 'Registered Discover Demo', - run() {}, - testId: 'demoDiscoverRegisteredNewButton', - appName: 'discover', -}; - -npSetup.plugins.navigation.registerMenuItem(customDiscoverExtension); diff --git a/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js b/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js index ef6f0a626bd15..83258a1ca3bdc 100644 --- a/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js +++ b/test/plugin_functional/test_suites/custom_visualizations/self_changing_vis.js @@ -28,11 +28,7 @@ export default function({ getService, getPageObjects }) { return await testSubjects.getVisibleText('counter'); } - async function getEditorValue() { - return await testSubjects.getAttribute('counterEditor', 'value'); - } - - describe.skip('self changing vis', function describeIndexTests() { + describe('self changing vis', function describeIndexTests() { before(async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('self_changing_vis'); @@ -45,16 +41,17 @@ export default function({ getService, getPageObjects }) { const isApplyEnabled = await PageObjects.visEditor.isApplyEnabled(); expect(isApplyEnabled).to.be(true); await PageObjects.visEditor.clickGo(); + await renderable.waitForRender(); const counter = await getCounterValue(); expect(counter).to.be('10'); }); - it('should allow changing params from within the vis', async () => { + it.skip('should allow changing params from within the vis', async () => { await testSubjects.click('counter'); await renderable.waitForRender(); const visValue = await getCounterValue(); expect(visValue).to.be('11'); - const editorValue = await getEditorValue(); + const editorValue = await testSubjects.getAttribute('counterEditor', 'value'); expect(editorValue).to.be('11'); // If changing a param from within the vis it should immediately apply and not bring editor in an unchanged state const isApplyEnabled = await PageObjects.visEditor.isApplyEnabled(); diff --git a/x-pack/legacy/plugins/apm/index.ts b/x-pack/legacy/plugins/apm/index.ts index d1f7ce325d23e..d2383acd45eba 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -105,10 +105,17 @@ export const apm: LegacyPluginInitializer = kibana => { privileges: { all: { app: ['apm', 'kibana'], - api: ['apm', 'apm_write', 'actions-read', 'alerting-read'], + api: [ + 'apm', + 'apm_write', + 'actions-read', + 'actions-all', + 'alerting-read', + 'alerting-all' + ], catalogue: ['apm'], savedObject: { - all: ['action', 'action_task_params'], + all: ['alert', 'action', 'action_task_params'], read: [] }, ui: [ @@ -124,13 +131,27 @@ export const apm: LegacyPluginInitializer = kibana => { }, read: { app: ['apm', 'kibana'], - api: ['apm', 'actions-read', 'alerting-read'], + api: [ + 'apm', + 'actions-read', + 'actions-all', + 'alerting-read', + 'alerting-all' + ], catalogue: ['apm'], savedObject: { - all: ['action', 'action_task_params'], + all: ['alert', 'action', 'action_task_params'], read: [] }, - ui: ['show', 'alerting:show', 'actions:show'] + ui: [ + 'show', + 'alerting:show', + 'actions:show', + 'alerting:save', + 'actions:save', + 'alerting:delete', + 'actions:delete' + ] } } }); diff --git a/x-pack/legacy/plugins/apm/mappings.json b/x-pack/legacy/plugins/apm/mappings.json index 5d14ae03f9a33..1e906dd2a5967 100644 --- a/x-pack/legacy/plugins/apm/mappings.json +++ b/x-pack/legacy/plugins/apm/mappings.json @@ -9,7 +9,7 @@ "properties": { "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -19,15 +19,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -35,7 +35,7 @@ "properties": { "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -43,15 +43,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } } @@ -65,7 +65,7 @@ "properties": { "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -75,15 +75,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -91,15 +91,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -107,15 +107,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } } @@ -129,7 +129,7 @@ "properties": { "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -139,15 +139,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -155,15 +155,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -171,15 +171,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } } @@ -193,7 +193,7 @@ "properties": { "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -203,15 +203,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -219,15 +219,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -235,15 +235,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } } @@ -257,7 +257,7 @@ "properties": { "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -267,15 +267,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -283,15 +283,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -299,15 +299,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } } @@ -321,7 +321,7 @@ "properties": { "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -331,15 +331,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -347,15 +347,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -363,15 +363,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } } @@ -385,7 +385,7 @@ "properties": { "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -395,15 +395,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -411,15 +411,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -427,15 +427,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } } @@ -449,7 +449,7 @@ "properties": { "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -459,15 +459,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -475,15 +475,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } }, @@ -491,15 +491,15 @@ "properties": { "composite": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "name": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 }, "version": { "type": "keyword", - "ignore_above": 256 + "ignore_above": 1024 } } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap b/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap index 88d9d7864576f..2b1f835a14f4a 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap +++ b/x-pack/legacy/plugins/apm/public/components/app/Home/__snapshots__/Home.test.tsx.snap @@ -15,6 +15,10 @@ exports[`Home component should render services 1`] = ` "chrome": Object { "setBreadcrumbs": [Function], }, + "docLinks": Object { + "DOC_LINK_VERSION": "0", + "ELASTIC_WEBSITE_URL": "https://www.elastic.co/", + }, "http": Object { "basePath": Object { "prepend": [Function], @@ -27,9 +31,6 @@ exports[`Home component should render services 1`] = ` }, }, }, - "packageInfo": Object { - "version": "0", - }, "plugins": Object {}, } } @@ -55,6 +56,10 @@ exports[`Home component should render traces 1`] = ` "chrome": Object { "setBreadcrumbs": [Function], }, + "docLinks": Object { + "DOC_LINK_VERSION": "0", + "ELASTIC_WEBSITE_URL": "https://www.elastic.co/", + }, "http": Object { "basePath": Object { "prepend": [Function], @@ -67,9 +72,6 @@ exports[`Home component should render traces 1`] = ` }, }, }, - "packageInfo": Object { - "version": "0", - }, "plugins": Object {}, } } diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx new file mode 100644 index 0000000000000..938962cc9dd18 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/WaterfallContainer.stories.tsx @@ -0,0 +1,76 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { TraceAPIResponse } from '../../../../../../../../../plugins/apm/server/lib/traces/get_trace'; +import { WaterfallContainer } from './index'; +import { + location, + urlParams, + simpleTrace, + traceWithErrors, + traceChildStartBeforeParent +} from './waterfallContainer.stories.data'; +import { getWaterfall } from './Waterfall/waterfall_helpers/waterfall_helpers'; + +storiesOf('app/TransactionDetails/Waterfall', module).add( + 'simple', + () => { + const waterfall = getWaterfall( + simpleTrace as TraceAPIResponse, + '975c8d5bfd1dd20b' + ); + return ( + + ); + }, + { info: { source: false } } +); + +storiesOf('app/TransactionDetails/Waterfall', module).add( + 'with errors', + () => { + const waterfall = getWaterfall( + (traceWithErrors as unknown) as TraceAPIResponse, + '975c8d5bfd1dd20b' + ); + return ( + + ); + }, + { info: { source: false } } +); + +storiesOf('app/TransactionDetails/Waterfall', module).add( + 'child starts before parent', + () => { + const waterfall = getWaterfall( + traceChildStartBeforeParent as TraceAPIResponse, + '975c8d5bfd1dd20b' + ); + return ( + + ); + }, + { info: { source: false } } +); diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts new file mode 100644 index 0000000000000..835183e73b298 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -0,0 +1,1647 @@ +/* + * 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 { Location } from 'history'; +import { IUrlParams } from '../../../../../context/UrlParamsContext/types'; + +export const location = { + pathname: '/services/opbeans-go/transactions/view', + search: + '?rangeFrom=now-24h&rangeTo=now&refreshPaused=true&refreshInterval=0&kuery=service.name%253A%2520%2522opbeans-java%2522%2520or%2520service.name%2520%253A%2520%2522opbeans-go%2522&traceId=513d33fafe99bbe6134749310c9b5322&transactionId=975c8d5bfd1dd20b&transactionName=GET%20%2Fapi%2Forders&transactionType=request', + hash: '' +} as Location; + +export const urlParams = { + start: '2020-03-22T15:16:38.742Z', + end: '2020-03-23T15:16:38.742Z', + rangeFrom: 'now-24h', + rangeTo: 'now', + refreshPaused: true, + refreshInterval: 0, + page: 0, + transactionId: '975c8d5bfd1dd20b', + traceId: '513d33fafe99bbe6134749310c9b5322', + kuery: 'service.name: "opbeans-java" or service.name : "opbeans-go"', + transactionName: 'GET /api/orders', + transactionType: 'request', + processorEvent: 'transaction', + serviceName: 'opbeans-go' +} as IUrlParams; + +export const simpleTrace = { + trace: { + items: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 46 + } + }, + source: { + ip: '172.19.0.13' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'] + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13' + }, + body: { + original: '[REDACTED]' + } + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'] + }, + status_code: 200, + finished: true, + headers_sent: true + }, + version: '1.1' + }, + client: { + ip: '172.19.0.13' + }, + transaction: { + duration: { + us: 18842 + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other' + } + }, + timestamp: { + us: 1584975868785000 + } + }, + { + parent: { + id: 'fc107f7b556eb49b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0' + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + duration: { + us: 16597 + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + timestamp: { + us: 1584975868787052 + } + }, + { + parent: { + id: 'daae24d83c269918' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + timestamp: { + us: 1584975868788603 + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 14648 + }, + name: 'GET opbeans.views.orders', + span_count: { + dropped: 0, + started: 1 + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true + } + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + parent: { + id: '49809ad3c26adf74' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 44 + } + }, + destination: { + address: 'opbeans-go', + port: 3000 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}" + }, + transaction: { + id: '49809ad3c26adf74' + }, + timestamp: { + us: 1584975868785273 + }, + span: { + duration: { + us: 17530 + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external' + } + }, + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-go:3000/api/orders' + } + }, + id: 'fc107f7b556eb49b', + type: 'external' + } + }, + { + parent: { + id: '975c8d5bfd1dd20b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '975c8d5bfd1dd20b' + }, + timestamp: { + us: 1584975868787174 + }, + span: { + duration: { + us: 16250 + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external' + } + }, + name: 'GET opbeans-python:3000', + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-python:3000/api/orders' + } + }, + id: 'daae24d83c269918', + type: 'external' + } + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + parent: { + id: '6fb0ff7365b87298' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + id: '6fb0ff7365b87298' + }, + timestamp: { + us: 1584975868790080 + }, + span: { + duration: { + us: 2519 + }, + subtype: 'postgresql', + name: 'SELECT FROM opbeans_order', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db' + } + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql' + } + } + } + ], + exceedsMax: false, + errorDocs: [] + }, + errorsPerTransaction: {} +}; + +export const traceWithErrors = { + trace: { + items: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 46 + } + }, + source: { + ip: '172.19.0.13' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'] + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13' + }, + body: { + original: '[REDACTED]' + } + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'] + }, + status_code: 200, + finished: true, + headers_sent: true + }, + version: '1.1' + }, + client: { + ip: '172.19.0.13' + }, + transaction: { + duration: { + us: 18842 + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other' + } + }, + timestamp: { + us: 1584975868785000 + } + }, + { + parent: { + id: 'fc107f7b556eb49b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0' + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + duration: { + us: 16597 + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + timestamp: { + us: 1584975868787052 + } + }, + { + parent: { + id: 'daae24d83c269918' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + timestamp: { + us: 1584975868788603 + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 14648 + }, + name: 'GET opbeans.views.orders', + span_count: { + dropped: 0, + started: 1 + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true + } + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + parent: { + id: '49809ad3c26adf74' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 44 + } + }, + destination: { + address: 'opbeans-go', + port: 3000 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}" + }, + transaction: { + id: '49809ad3c26adf74' + }, + timestamp: { + us: 1584975868785273 + }, + span: { + duration: { + us: 17530 + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external' + } + }, + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-go:3000/api/orders' + } + }, + id: 'fc107f7b556eb49b', + type: 'external' + } + }, + { + parent: { + id: '975c8d5bfd1dd20b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '975c8d5bfd1dd20b' + }, + timestamp: { + us: 1584975868787174 + }, + span: { + duration: { + us: 16250 + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external' + } + }, + name: 'GET opbeans-python:3000', + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-python:3000/api/orders' + } + }, + id: 'daae24d83c269918', + type: 'external' + } + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + parent: { + id: '6fb0ff7365b87298' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + id: '6fb0ff7365b87298' + }, + timestamp: { + us: 1584975868790080 + }, + span: { + duration: { + us: 2519 + }, + subtype: 'postgresql', + name: 'SELECT FROM opbeans_order', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db' + } + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql' + } + } + } + ], + exceedsMax: false, + errorDocs: [ + { + parent: { + id: '975c8d5bfd1dd20b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + error: { + culprit: 'logrusMiddleware', + log: { + level: 'error', + message: 'GET //api/products (502)' + }, + id: '1f3cb98206b5c54225cb7c8908a658da', + grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a' + }, + processor: { + name: 'error', + event: 'error' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T16:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '975c8d5bfd1dd20b', + sampled: false + }, + timestamp: { + us: 1584975868787052 + } + }, + { + parent: { + id: '6fb0ff7365b87298' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + error: { + culprit: 'logrusMiddleware', + log: { + level: 'error', + message: 'GET //api/products (502)' + }, + id: '1f3cb98206b5c54225cb7c8908a658d2', + grouping_key: '4dba2ff58fe6c036a5dee2ce411e512a' + }, + processor: { + name: 'error', + event: 'error' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T16:04:28.790Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-python', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '6fb0ff7365b87298', + sampled: false + }, + timestamp: { + us: 1584975868790000 + } + } + ] + }, + errorsPerTransaction: { + '975c8d5bfd1dd20b': 1, + '6fb0ff7365b87298': 1 + } +}; + +export const traceChildStartBeforeParent = { + trace: { + items: [ + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 46 + } + }, + source: { + ip: '172.19.0.13' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: '172.19.0.9', + full: 'http://172.19.0.9:3000/api/orders' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + type: 'apm-server', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + http: { + request: { + headers: { + Accept: ['*/*'], + 'User-Agent': ['Python/3.7 aiohttp/3.3.2'], + Host: ['172.19.0.9:3000'], + 'Accept-Encoding': ['gzip, deflate'] + }, + method: 'get', + socket: { + encrypted: false, + remote_address: '172.19.0.13' + }, + body: { + original: '[REDACTED]' + } + }, + response: { + headers: { + 'Transfer-Encoding': ['chunked'], + Date: ['Mon, 23 Mar 2020 15:04:28 GMT'], + 'Content-Type': ['application/json;charset=ISO-8859-1'] + }, + status_code: 200, + finished: true, + headers_sent: true + }, + version: '1.1' + }, + client: { + ip: '172.19.0.13' + }, + transaction: { + duration: { + us: 18842 + }, + result: 'HTTP 2xx', + name: 'DispatcherServlet#doGet', + id: '49809ad3c26adf74', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + user_agent: { + original: 'Python/3.7 aiohttp/3.3.2', + name: 'Other', + device: { + name: 'Other' + } + }, + timestamp: { + us: 1584975868785000 + } + }, + { + parent: { + id: 'fc107f7b556eb49b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + framework: { + name: 'gin', + version: 'v1.4.0' + }, + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + duration: { + us: 16597 + }, + result: 'HTTP 2xx', + name: 'GET /api/orders', + id: '975c8d5bfd1dd20b', + span_count: { + dropped: 0, + started: 1 + }, + type: 'request', + sampled: true + }, + timestamp: { + us: 1584975868787052 + } + }, + { + parent: { + id: 'daae24d83c269918' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + timestamp: { + us: 1584975868780000 + }, + processor: { + name: 'transaction', + event: 'transaction' + }, + url: { + path: '/api/orders', + scheme: 'http', + port: 3000, + domain: 'opbeans-go', + full: 'http://opbeans-go:3000/api/orders' + }, + '@timestamp': '2020-03-23T15:04:28.788Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + result: 'HTTP 2xx', + duration: { + us: 1464 + }, + name: 'I started before my parent 😰', + span_count: { + dropped: 0, + started: 1 + }, + id: '6fb0ff7365b87298', + type: 'request', + sampled: true + } + }, + { + container: { + id: '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + parent: { + id: '49809ad3c26adf74' + }, + process: { + pid: 6, + title: '/usr/lib/jvm/java-10-openjdk-amd64/bin/java', + ppid: 1 + }, + agent: { + name: 'java', + ephemeral_id: '99ce8403-5875-4945-b074-d37dc10563eb', + version: '1.14.1-SNAPSHOT' + }, + internal: { + sampler: { + value: 44 + } + }, + destination: { + address: 'opbeans-go', + port: 3000 + }, + processor: { + name: 'transaction', + event: 'span' + }, + observer: { + hostname: 'f37f48d8b60b', + id: 'd8522e1f-be8e-43c2-b290-ac6b6c0f171e', + type: 'apm-server', + ephemeral_id: '6ed88f14-170e-478d-a4f5-ea5e7f4b16b9', + version: '8.0.0', + version_major: 8 + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.785Z', + ecs: { + version: '1.4.0' + }, + service: { + node: { + name: + '4cf84d094553201997ddb7fea344b7c6ef18dcb8233eba39278946ee8449794e' + }, + environment: 'production', + name: 'opbeans-java', + runtime: { + name: 'Java', + version: '10.0.2' + }, + language: { + name: 'Java', + version: '10.0.2' + }, + version: 'None' + }, + host: { + hostname: '4cf84d094553', + os: { + platform: 'Linux' + }, + ip: '172.19.0.9', + name: '4cf84d094553', + architecture: 'amd64' + }, + connection: { + hash: + "{service.environment:'production'}/{service.name:'opbeans-java'}/{span.subtype:'http'}/{destination.address:'opbeans-go'}/{span.type:'external'}" + }, + transaction: { + id: '49809ad3c26adf74' + }, + timestamp: { + us: 1584975868785273 + }, + span: { + duration: { + us: 17530 + }, + subtype: 'http', + name: 'GET opbeans-go', + destination: { + service: { + resource: 'opbeans-go:3000', + name: 'http://opbeans-go:3000', + type: 'external' + } + }, + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-go:3000/api/orders' + } + }, + id: 'fc107f7b556eb49b', + type: 'external' + } + }, + { + parent: { + id: '975c8d5bfd1dd20b' + }, + agent: { + name: 'go', + version: '1.7.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.787Z', + service: { + node: { + name: + 'e948a08b8f5efe99b5da01f50da48c7d8aee3bbf4701f3da85ebe760c2ffef29' + }, + environment: 'production', + name: 'opbeans-go', + runtime: { + name: 'gc', + version: 'go1.14.1' + }, + language: { + name: 'go', + version: 'go1.14.1' + }, + version: 'None' + }, + transaction: { + id: '975c8d5bfd1dd20b' + }, + timestamp: { + us: 1584975868787174 + }, + span: { + duration: { + us: 16250 + }, + subtype: 'http', + destination: { + service: { + resource: 'opbeans-python:3000', + name: 'http://opbeans-python:3000', + type: 'external' + } + }, + name: 'I am his 👇🏻 parent 😡', + http: { + response: { + status_code: 200 + }, + url: { + original: 'http://opbeans-python:3000/api/orders' + } + }, + id: 'daae24d83c269918', + type: 'external' + } + }, + { + container: { + id: 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + parent: { + id: '6fb0ff7365b87298' + }, + agent: { + name: 'python', + version: '5.5.2' + }, + processor: { + name: 'transaction', + event: 'span' + }, + trace: { + id: '513d33fafe99bbe6134749310c9b5322' + }, + '@timestamp': '2020-03-23T15:04:28.790Z', + service: { + node: { + name: + 'a636915f1f6eec81ab44342b13a3ea9597ef03a24391e4e55f34ae2e20b30f51' + }, + environment: 'production', + framework: { + name: 'django', + version: '2.1.13' + }, + name: 'opbeans-python', + runtime: { + name: 'CPython', + version: '3.6.10' + }, + language: { + name: 'python', + version: '3.6.10' + }, + version: 'None' + }, + transaction: { + id: '6fb0ff7365b87298' + }, + timestamp: { + us: 1584975868781000 + }, + span: { + duration: { + us: 2519 + }, + subtype: 'postgresql', + name: 'I am using my parents skew 😇', + destination: { + service: { + resource: 'postgresql', + name: 'postgresql', + type: 'db' + } + }, + action: 'query', + id: 'c9407abb4d08ead1', + type: 'db', + sync: true, + db: { + statement: + 'SELECT "opbeans_order"."id", "opbeans_order"."customer_id", "opbeans_customer"."full_name", "opbeans_order"."created_at" FROM "opbeans_order" INNER JOIN "opbeans_customer" ON ("opbeans_order"."customer_id" = "opbeans_customer"."id") LIMIT 1000', + type: 'sql' + } + } + } + ], + exceedsMax: false, + errorDocs: [] + }, + errorsPerTransaction: {} +}; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx index 9fcab049e224f..8c2829a515f83 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Links/ElasticDocsLink.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui'; +import React from 'react'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; // union type constisting of valid guide sections that we link to @@ -17,8 +17,11 @@ interface Props extends EuiLinkAnchorProps { } export function ElasticDocsLink({ section, path, children, ...rest }: Props) { - const { version } = useApmPluginContext().packageInfo; - const href = `https://www.elastic.co/guide/en${section}/${version}${path}`; + const { docLinks } = useApmPluginContext().core; + const baseUrl = docLinks.ELASTIC_WEBSITE_URL; + const version = docLinks.DOC_LINK_VERSION; + const href = `${baseUrl}guide/en${section}/${version}${path}`; + return typeof children === 'function' ? ( children(href) ) : ( diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx index 8775dc98c3e1a..cc2e382611628 100644 --- a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx +++ b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/MockApmPluginContext.tsx @@ -12,6 +12,10 @@ const mockCore = { chrome: { setBreadcrumbs: () => {} }, + docLinks: { + DOC_LINK_VERSION: '0', + ELASTIC_WEBSITE_URL: 'https://www.elastic.co/' + }, http: { basePath: { prepend: (path: string) => `/basepath${path}` @@ -36,7 +40,6 @@ const mockConfig: ConfigSchema = { export const mockApmPluginContextValue = { config: mockConfig, core: mockCore, - packageInfo: { version: '0' }, plugins: {} }; diff --git a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx index d8934ba4b0151..acc3886586889 100644 --- a/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx +++ b/x-pack/legacy/plugins/apm/public/context/ApmPluginContext/index.tsx @@ -5,7 +5,7 @@ */ import { createContext } from 'react'; -import { AppMountContext, PackageInfo } from 'kibana/public'; +import { AppMountContext } from 'kibana/public'; import { ApmPluginSetupDeps, ConfigSchema } from '../../new-platform/plugin'; export type AppMountContextBasePath = AppMountContext['core']['http']['basePath']; @@ -13,7 +13,6 @@ export type AppMountContextBasePath = AppMountContext['core']['http']['basePath' export interface ApmPluginContextValue { config: ConfigSchema; core: AppMountContext['core']; - packageInfo: PackageInfo; plugins: ApmPluginSetupDeps; } diff --git a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx index e30bed1810c1d..a291678e9a20c 100644 --- a/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx +++ b/x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx @@ -9,13 +9,11 @@ import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import { ApmRoute } from '@elastic/apm-rum-react'; import styled from 'styled-components'; -import { metadata } from 'ui/metadata'; import { i18n } from '@kbn/i18n'; import { AlertType } from '../../../../../plugins/apm/common/alert_types'; import { CoreSetup, CoreStart, - PackageInfo, Plugin, PluginInitializerContext } from '../../../../../../src/core/public'; @@ -124,14 +122,6 @@ export class ApmPlugin // Until then we use a shim to get it from legacy injectedMetadata: const config = getConfigFromInjectedMetadata(); - // Once we're actually an NP plugin we'll get the package info from the - // initializerContext like: - // - // const packageInfo = this.initializerContext.env.packageInfo - // - // Until then we use a shim to get it from legacy metadata: - const packageInfo = metadata as PackageInfo; - // render APM feedback link in global help menu setHelpExtension(core); setReadonlyBadge(core); @@ -140,7 +130,6 @@ export class ApmPlugin const apmPluginContextValue = { config, core, - packageInfo, plugins }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts index 60026adc0998a..2985a68cf855c 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_lens.ts @@ -5,8 +5,8 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { TimeRange } from 'src/plugins/data/public'; -import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { TimeRange, Filter as DataFilter } from 'src/plugins/data/public'; +import { EmbeddableInput } from 'src/plugins/embeddable/public'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { Filter, TimeRange as TimeRangeArg } from '../../../types'; import { @@ -15,7 +15,6 @@ import { EmbeddableExpression, } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; -import { Filter as DataFilter } from '../../../../../../../src/plugins/data/public'; interface Arguments { id: string; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts index 78240eee7ce13..4b045b0c5edcf 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/saved_map.ts @@ -5,8 +5,8 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { TimeRange } from 'src/plugins/data/public'; -import { EmbeddableInput } from 'src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +import { TimeRange, Filter as DataFilter } from 'src/plugins/data/public'; +import { EmbeddableInput } from 'src/plugins/embeddable/public'; import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; import { Filter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; import { @@ -15,7 +15,6 @@ import { EmbeddableExpression, } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; -import { Filter as DataFilter } from '../../../../../../../src/plugins/data/public'; interface Arguments { id: string; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 3cdb6eb460224..817be6e144fc8 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -14,7 +14,6 @@ import { EmbeddablePanel, EmbeddableFactoryNotFoundError, } from '../../../../../../../src/plugins/embeddable/public'; -import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; import { EmbeddableExpression } from '../../expression_types/embeddable'; import { RendererStrings } from '../../../i18n'; import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public'; @@ -39,8 +38,8 @@ const renderEmbeddable = (embeddableObject: IEmbeddable, domNode: HTMLElement) = ({ const uniqueId = handlers.getElementId(); if (!embeddablesRegistry[uniqueId]) { - const factory = Array.from(start.getEmbeddableFactories()).find( + const factory = Array.from(npStart.plugins.embeddable.getEmbeddableFactories()).find( embeddableFactory => embeddableFactory.type === embeddableType ) as EmbeddableFactory; diff --git a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 576c7c4794b08..08cd3084c35cf 100644 --- a/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/legacy/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -5,13 +5,12 @@ */ import React from 'react'; - +import { npStart } from 'ui/new_platform'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; import { SavedObjectFinderUi, SavedObjectMetaData, } from '../../../../../../../src/plugins/saved_objects/public/'; -import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; import { ComponentStrings } from '../../../i18n'; import { CoreStart } from '../../../../../../../src/core/public'; @@ -27,7 +26,7 @@ export interface Props { export class AddEmbeddableFlyout extends React.Component { onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = start.getEmbeddableFactories(); + const embeddableFactories = npStart.plugins.embeddable.getEmbeddableFactories(); // Find the embeddable type from the saved object type const found = Array.from(embeddableFactories).find(embeddableFactory => { @@ -43,7 +42,7 @@ export class AddEmbeddableFlyout extends React.Component { }; render() { - const embeddableFactories = start.getEmbeddableFactories(); + const embeddableFactories = npStart.plugins.embeddable.getEmbeddableFactories(); const availableSavedObjects = Array.from(embeddableFactories) .filter(factory => { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 2bde698e23562..1caea1b4b728f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -22,7 +22,7 @@ import { ErrorEmbeddable, EmbeddableInput, IContainer, -} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; +} from '../../../../../../../src/plugins/embeddable/public'; import { Embeddable } from './embeddable'; import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; import { getEditPath } from '../../../../../../plugins/lens/common'; diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts index c1f5c31eb4210..b4a8ff90c3512 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts @@ -5,10 +5,14 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { Filter, Query } from 'src/plugins/data/public'; +import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { AnyAction } from 'redux'; import { LAYER_TYPE } from '../../common/constants'; import { DataMeta, MapFilters } from '../../common/descriptor_types'; +import { + MapCenterAndZoom, + MapRefreshConfig, +} from '../../../../../plugins/maps/common/descriptor_types'; export type SyncContext = { startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; @@ -27,31 +31,20 @@ export function updateSourceProp( newLayerType?: LAYER_TYPE ): void; -export interface MapCenter { - lat: number; - lon: number; - zoom: number; -} - -export function setGotoWithCenter(config: MapCenter): AnyAction; +export function setGotoWithCenter(config: MapCenterAndZoom): AnyAction; export function replaceLayerList(layerList: unknown[]): AnyAction; -export interface QueryGroup { +export type QueryGroup = { filters: Filter[]; query?: Query; - timeFilters: unknown; - refresh: unknown; -} + timeFilters?: TimeRange; + refresh?: boolean; +}; export function setQuery(query: QueryGroup): AnyAction; -export interface RefreshConfig { - isPaused: boolean; - interval: number; -} - -export function setRefreshConfig(config: RefreshConfig): AnyAction; +export function setRefreshConfig(config: MapRefreshConfig): AnyAction; export function disableScrollZoom(): AnyAction; diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 519ba0b1e3d96..bc97643689e12 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -310,9 +310,15 @@ app.controller( const layerListConfigOnly = copyPersistentState(layerList); const savedLayerList = savedMap.getLayerList(); - const oldConfig = savedLayerList ? savedLayerList : initialLayerListConfig; - return !_.isEqual(layerListConfigOnly, oldConfig); + return !savedLayerList + ? !_.isEqual(layerListConfigOnly, initialLayerListConfig) + : // savedMap stores layerList as a JSON string using JSON.stringify. + // JSON.stringify removes undefined properties from objects. + // savedMap.getLayerList converts the JSON string back into Javascript array of objects. + // Need to perform the same process for layerListConfigOnly to compare apples to apples + // and avoid undefined properties in layerListConfigOnly triggering unsaved changes. + !_.isEqual(JSON.parse(JSON.stringify(layerListConfigOnly)), savedLayerList); } function isOnMapNow() { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js index e51e59ec41e18..04de5f71f5bfc 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import { LayerControl } from './view'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui.js'; +import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui'; import { updateFlyout, setIsLayerTOCOpen } from '../../../actions/ui_actions'; import { setSelectedLayer } from '../../../actions/map_actions'; import { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js index ececc5a90ab89..588445d0b4992 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import { connect } from 'react-redux'; import { TOCEntry } from './view'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../../../plugins/maps/public/reducers/ui.js'; +import { FLYOUT_STATE } from '../../../../../../../../../plugins/maps/public/reducers/ui'; import { updateFlyout, hideTOCDetails, showTOCDetails } from '../../../../../actions/ui_actions'; import { getIsReadOnly, getOpenTOCDetails } from '../../../../../selectors/ui_selectors'; import { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx index 69f55815d16a0..3c9069c7a836f 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx @@ -45,8 +45,8 @@ import { hideLayerControl, hideViewControl, setHiddenLayers, - MapCenter, } from '../actions/map_actions'; +import { MapCenterAndZoom } from '../../../../../plugins/maps/common/descriptor_types'; import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; import { @@ -71,7 +71,6 @@ export interface MapEmbeddableInput extends EmbeddableInput { timeRange?: TimeRange; filters: Filter[]; query?: Query; - refresh?: unknown; refreshConfig: RefreshInterval; isLayerTOCOpen: boolean; openTOCDetails?: string[]; @@ -80,7 +79,7 @@ export interface MapEmbeddableInput extends EmbeddableInput { hideToolbarOverlay?: boolean; hideLayerControl?: boolean; hideViewControl?: boolean; - mapCenter?: MapCenter; + mapCenter?: MapCenterAndZoom; hiddenLayers?: string[]; hideFilterActions?: boolean; } @@ -153,7 +152,12 @@ export class MapEmbeddable extends Embeddable) { + }: { + query?: Query; + timeRange?: TimeRange; + filters: Filter[]; + refresh?: boolean; + }) { this._prevTimeRange = timeRange; this._prevQuery = query; this._prevFilters = filters; diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts index ddb937dd98926..b9cb66f831281 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -11,14 +11,13 @@ import { i18n } from '@kbn/i18n'; import { npSetup, npStart } from 'ui/new_platform'; import { SavedObjectLoader } from 'src/plugins/saved_objects/public'; import { IIndexPattern } from 'src/plugins/data/public'; +import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable'; +import { getIndexPatternService } from '../kibana_services'; import { EmbeddableFactory, ErrorEmbeddable, IContainer, -} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { setup } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; -import { MapEmbeddable, MapEmbeddableInput } from './map_embeddable'; -import { getIndexPatternService } from '../kibana_services'; +} from '../../../../../../src/plugins/embeddable/public'; import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -171,4 +170,7 @@ export class MapEmbeddableFactory extends EmbeddableFactory { } } -setup.registerEmbeddableFactory(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); +npSetup.plugins.embeddable.registerEmbeddableFactory( + MAP_SAVED_OBJECT_TYPE, + new MapEmbeddableFactory() +); diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js index a1c15e27c9eb3..5e8f720fcc5e3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -28,12 +28,20 @@ export function DynamicColorForm({ }; if (type === COLOR_MAP_TYPE.ORDINAL) { newColorOptions.useCustomColorRamp = useCustomColorMap; - newColorOptions.customColorRamp = customColorMap; - newColorOptions.color = color; + if (customColorMap) { + newColorOptions.customColorRamp = customColorMap; + } + if (color) { + newColorOptions.color = color; + } } else { newColorOptions.useCustomColorPalette = useCustomColorMap; - newColorOptions.customColorPalette = customColorMap; - newColorOptions.colorCategory = color; + if (customColorMap) { + newColorOptions.customColorPalette = customColorMap; + } + if (color) { + newColorOptions.colorCategory = color; + } } onDynamicStyleChange(styleProperty.getStyleName(), newColorOptions); diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.ts b/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.ts index c77af11d0ae24..46e27bbd770a1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.ts +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.ts @@ -6,6 +6,7 @@ import _ from 'lodash'; import { PhraseFilter } from '../../../../../../../src/plugins/data/public'; +import { TooltipFeature } from '../../../../../../plugins/maps/common/descriptor_types'; export interface ITooltipProperty { getPropertyKey(): string; @@ -16,11 +17,6 @@ export interface ITooltipProperty { getESFilters(): Promise; } -export interface MapFeature { - id: number; - layerId: string; -} - export interface LoadFeatureProps { layerId: string; featureId: number; @@ -34,7 +30,7 @@ export interface FeatureGeometry { export interface RenderTooltipContentParams { addFilters(filter: object): void; closeTooltip(): void; - features: MapFeature[]; + features: TooltipFeature[]; isLocked: boolean; getLayerName(layerId: string): Promise; loadFeatureProperties({ layerId, featureId }: LoadFeatureProps): Promise; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/is_refresh_only_query.ts b/x-pack/legacy/plugins/maps/public/layers/util/is_refresh_only_query.ts index a56da4b23aa1e..3599f18671ced 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/is_refresh_only_query.ts +++ b/x-pack/legacy/plugins/maps/public/layers/util/is_refresh_only_query.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Query } from '../../../common/descriptor_types'; +import { MapQuery } from '../../../common/descriptor_types'; // Refresh only query is query where timestamps are different but query is the same. // Triggered by clicking "Refresh" button in QueryBar export function isRefreshOnlyQuery( - prevQuery: Query | undefined, - newQuery: Query | undefined + prevQuery: MapQuery | undefined, + newQuery: MapQuery | undefined ): boolean { if (!prevQuery || !newQuery) { return false; diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts index 237a04027e21b..8c99e0adcc14f 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts @@ -5,12 +5,14 @@ */ import { AnyAction } from 'redux'; -import { MapCenter } from '../actions/map_actions'; +import { MapCenter } from '../../common/descriptor_types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MapStoreState } from '../../../../../plugins/maps/public/reducers/store'; -export function getHiddenLayerIds(state: unknown): string[]; +export function getHiddenLayerIds(state: MapStoreState): string[]; -export function getMapZoom(state: unknown): number; +export function getMapZoom(state: MapStoreState): number; -export function getMapCenter(state: unknown): MapCenter; +export function getMapCenter(state: MapStoreState): MapCenter; -export function getQueryableUniqueIndexPatternIds(state: unknown): string[]; +export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[]; diff --git a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js deleted file mode 100644 index 912ee08396212..0000000000000 --- a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const getFlyoutDisplay = ({ ui }) => ui.flyoutDisplay; -export const getIsSetViewOpen = ({ ui }) => ui.isSetViewOpen; -export const getIsLayerTOCOpen = ({ ui }) => ui.isLayerTOCOpen; -export const getOpenTOCDetails = ({ ui }) => ui.openTOCDetails; -export const getIsFullScreen = ({ ui }) => ui.isFullScreen; -export const getIsReadOnly = ({ ui }) => ui.isReadOnly; -export const getIndexingStage = ({ ui }) => ui.importIndexingStage; diff --git a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts new file mode 100644 index 0000000000000..fdf2a8ea0e4f3 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MapStoreState } from '../../../../../plugins/maps/public/reducers/store'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FLYOUT_STATE, INDEXING_STAGE } from '../../../../../plugins/maps/public/reducers/ui'; + +export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; +export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen; +export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; +export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; +export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; +export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; +export const getIndexingStage = ({ ui }: MapStoreState): INDEXING_STAGE | null => + ui.importIndexingStage; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/helpers/index.test.ts b/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.test.ts similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/ml/helpers/index.test.ts rename to x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.test.ts index 693f0bd0dd0fd..ba93b2e4b8a0d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/helpers/index.test.ts +++ b/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isJobStarted, isJobLoading, isJobFailed } from './'; +import { isJobStarted, isJobLoading, isJobFailed } from './ml_helpers'; describe('isJobStarted', () => { test('returns false if only jobState is enabled', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/helpers/index.ts b/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.ts similarity index 89% rename from x-pack/legacy/plugins/siem/public/components/ml/helpers/index.ts rename to x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.ts index c06596b49317d..e4158d08d448d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/helpers/index.ts +++ b/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RuleType } from './types'; + // Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js const enabledStates = ['started', 'opened']; const loadingStates = ['starting', 'stopping', 'opening', 'closing']; @@ -20,3 +22,5 @@ export const isJobLoading = (jobState: string, datafeedState: string): boolean = export const isJobFailed = (jobState: string, datafeedState: string): boolean => { return failureStates.includes(jobState) || failureStates.includes(datafeedState); }; + +export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; diff --git a/x-pack/legacy/plugins/siem/common/detection_engine/types.ts b/x-pack/legacy/plugins/siem/common/detection_engine/types.ts index 0de370b11cdaf..39012d0b4b683 100644 --- a/x-pack/legacy/plugins/siem/common/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/common/detection_engine/types.ts @@ -3,9 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import * as t from 'io-ts'; import { AlertAction } from '../../../../../plugins/alerting/common'; export type RuleAlertAction = Omit & { action_type_id: string; }; + +export const RuleTypeSchema = t.keyof({ + query: null, + saved_query: null, + machine_learning: null, +}); +export type RuleType = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx index a3c4a655a4937..c7e368da1338f 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map.tsx @@ -8,9 +8,9 @@ import { EuiLink, EuiText } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; import { createPortalNode, InPortal } from 'react-reverse-portal'; import styled, { css } from 'styled-components'; +import { npStart } from 'ui/new_platform'; -import { EmbeddablePanel } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; -import { start } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; +import { EmbeddablePanel } from '../../../../../../../src/plugins/embeddable/public'; import { DEFAULT_INDEX_KEY } from '../../../common/constants'; import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers'; import { useIndexPatterns } from '../../hooks/use_index_patterns'; @@ -198,8 +198,8 @@ export const EmbeddedMapComponent = ({ data-test-subj="embeddable-panel" embeddable={embeddable} getActions={services.uiActions.getTriggerCompatibleActions} - getEmbeddableFactory={start.getEmbeddableFactory} - getAllEmbeddableFactories={start.getEmbeddableFactories} + getEmbeddableFactory={npStart.plugins.embeddable.getEmbeddableFactory} + getAllEmbeddableFactories={npStart.plugins.embeddable.getEmbeddableFactories} notifications={services.notifications} overlays={services.overlays} inspector={services.inspector} diff --git a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx index 4b32fd8299ef7..56211c9ff8935 100644 --- a/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx @@ -8,14 +8,13 @@ import uuid from 'uuid'; import React from 'react'; import { OutPortal, PortalNode } from 'react-reverse-portal'; import minimatch from 'minimatch'; -import { ViewMode } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public'; import { IndexPatternMapping, SetQuery } from './types'; import { getLayerList } from './map_config'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../plugins/maps/public'; import { MapEmbeddable, RenderTooltipContentParams } from '../../../../maps/public'; import * as i18n from './translations'; import { Query, Filter } from '../../../../../../../src/plugins/data/public'; -import { EmbeddableStart } from '../../../../../../../src/plugins/embeddable/public'; +import { EmbeddableStart, ViewMode } from '../../../../../../../src/plugins/embeddable/public'; import { IndexPatternSavedObject } from '../../hooks/types'; /** diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap index 24b1756aade2e..c8d4b6ec3b4c8 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap @@ -19,6 +19,7 @@ exports[`EditableTitle it renders 1`] = ` aria-label="You can edit Test title by clicking" data-test-subj="editable-title-edit-icon" iconType="pencil" + isDisabled={false} onClick={[Function]} /> diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx index 29cc1579f9bcc..165be00384779 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx @@ -34,12 +34,18 @@ const MySpinner = styled(EuiLoadingSpinner)` `; interface Props { + disabled?: boolean; isLoading: boolean; title: string | React.ReactNode; onSubmit: (title: string) => void; } -const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) => { +const EditableTitleComponent: React.FC = ({ + disabled = false, + onSubmit, + isLoading, + title, +}) => { const [editMode, setEditMode] = useState(false); const [changedTitle, onTitleChange] = useState(typeof title === 'string' ? title : ''); @@ -104,6 +110,7 @@ const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) {isLoading && } {!isLoading && ( = ({ const insertTimelineButton = useMemo( () => ( - + {i18n.INSERT_TIMELINE}

}> + +
), [handleOpenPopover, isDisabled] ); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx index 0cfb07cccfd6c..e4d828b68f3dc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx @@ -17,7 +17,7 @@ export const useInsertTimeline = (form: FormHook, fieldNa }); const handleOnTimelineChange = useCallback( (title: string, id: string | null) => { - const builtLink = `${basePath}/app/siem#/timelines?timeline=(id:${id},isOpen:!t)`; + const builtLink = `${basePath}/app/siem#/timelines?timeline=(id:'${id}',isOpen:!t)`; const currentValue = form.getFormData()[fieldName]; const newValue: string = [ currentValue.slice(0, cursorPosition.start), diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts index de3e3c8e792fe..101837168350f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts @@ -25,5 +25,5 @@ export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate( ); export const INSERT_TIMELINE = i18n.translate('xpack.siem.insert.timeline.insertTimelineButton', { - defaultMessage: 'Insert Timeline…', + defaultMessage: 'Insert timeline link', }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 7d5ae53b78ff8..bd243d0ba5f64 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -119,7 +119,7 @@ export const getCases = async ({ signal, }: FetchCasesProps): Promise => { const query = { - reporters: filterOptions.reporters.map(r => r.username), + reporters: filterOptions.reporters.map(r => r.username ?? '').filter(r => r !== ''), tags: filterOptions.tags, ...(filterOptions.status !== '' ? { status: filterOptions.status } : {}), ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts new file mode 100644 index 0000000000000..dbd618f40155d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../translations'; + +export const SUCCESS_CONFIGURE = i18n.translate('xpack.siem.case.configure.successSaveToast', { + defaultMessage: 'Saved external connection settings', +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx index b25667f070fdf..6524c40a8e6e4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx @@ -7,8 +7,8 @@ import { useState, useEffect, useCallback } from 'react'; import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; -import { useStateToaster, errorToToaster } from '../../../components/toasters'; -import * as i18n from '../translations'; +import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters'; +import * as i18n from './translations'; import { ClosureType } from './types'; import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer'; @@ -124,6 +124,8 @@ export const useCaseConfigure = ({ closureType: res.closureType, }); } + + displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); } } catch (error) { if (!didCancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts index 601db373f041e..a453be32480e2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts @@ -10,6 +10,46 @@ export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle defaultMessage: 'Error fetching data', }); +export const ERROR_DELETING = i18n.translate('xpack.siem.containers.case.errorDeletingTitle', { + defaultMessage: 'Error deleting data', +}); + +export const UPDATED_CASE = (caseTitle: string) => + i18n.translate('xpack.siem.containers.case.updatedCase', { + values: { caseTitle }, + defaultMessage: 'Updated "{caseTitle}"', + }); + +export const DELETED_CASES = (totalCases: number, caseTitle?: string) => + i18n.translate('xpack.siem.containers.case.deletedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Deleted {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const CLOSED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.siem.containers.case.closedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const REOPENED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.siem.containers.case.reopenedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + export const TAG_FETCH_FAILURE = i18n.translate( 'xpack.siem.containers.case.tagFetchFailDescription', { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index bb215d6ac271c..d2a58e9eeeff4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -90,7 +90,7 @@ export enum SortFieldCase { export interface ElasticUser { readonly email?: string | null; readonly fullName?: string | null; - readonly username: string; + readonly username?: string | null; } export interface FetchCasesProps extends ApiProps { @@ -114,3 +114,8 @@ export interface ActionLicense { enabledInConfig: boolean; enabledInLicense: boolean; } + +export interface DeleteCase { + id: string; + title?: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx index f1129bae9f537..7d040c49f1971 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx @@ -5,7 +5,7 @@ */ import { useCallback, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { patchCasesStatus } from './api'; import { BulkUpdateStatus, Case } from './types'; @@ -71,9 +71,22 @@ export const useUpdateCases = (): UseUpdateCase => { const patchData = async () => { try { dispatch({ type: 'FETCH_INIT' }); - await patchCasesStatus(cases, abortCtrl.signal); + const patchResponse = await patchCasesStatus(cases, abortCtrl.signal); if (!cancel) { + const resultCount = Object.keys(patchResponse).length; + const firstTitle = patchResponse[0].title; + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + const messageArgs = { + totalCases: resultCount, + caseTitle: resultCount === 1 ? firstTitle : '', + }; + const message = + resultCount && patchResponse[0].status === 'open' + ? i18n.REOPENED_CASES(messageArgs) + : i18n.CLOSED_CASES(messageArgs); + + displaySuccessToast(message, dispatchToaster); } } catch (error) { if (!cancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx index b44e01d06acaf..07e3786758aeb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -5,9 +5,10 @@ */ import { useCallback, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { deleteCases } from './api'; +import { DeleteCase } from './types'; interface DeleteState { isDisplayConfirmDeleteModal: boolean; @@ -57,9 +58,10 @@ const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { return state; } }; + interface UseDeleteCase extends DeleteState { dispatchResetIsDeleted: () => void; - handleOnDeleteConfirm: (caseIds: string[]) => void; + handleOnDeleteConfirm: (caseIds: DeleteCase[]) => void; handleToggleModal: () => void; } @@ -72,21 +74,26 @@ export const useDeleteCases = (): UseDeleteCase => { }); const [, dispatchToaster] = useStateToaster(); - const dispatchDeleteCases = useCallback((caseIds: string[]) => { + const dispatchDeleteCases = useCallback((cases: DeleteCase[]) => { let cancel = false; const abortCtrl = new AbortController(); const deleteData = async () => { try { dispatch({ type: 'FETCH_INIT' }); + const caseIds = cases.map(theCase => theCase.id); await deleteCases(caseIds, abortCtrl.signal); if (!cancel) { dispatch({ type: 'FETCH_SUCCESS', payload: true }); + displaySuccessToast( + i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''), + dispatchToaster + ); } } catch (error) { if (!cancel) { errorToToaster({ - title: i18n.ERROR_TITLE, + title: i18n.ERROR_DELETING, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx index 6974000414a06..2478172a3394b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash/fp'; import { User } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getReporters } from './api'; @@ -44,9 +45,12 @@ export const useGetReporters = (): UseGetReporters => { }); try { const response = await getReporters(abortCtrl.signal); + const myReporters = response + .map(r => (r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name)) + .filter(u => !isEmpty(u)); if (!didCancel) { setReporterState({ - reporters: response.map(r => r.full_name ?? r.username ?? 'N/A'), + reporters: myReporters, respReporters: response, isLoading: false, isError: false, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx index 03e10249317ee..d9a32f26f7fe7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx @@ -148,7 +148,7 @@ const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { createdAt, createdBy: { fullName: createdBy.fullName ?? null, - username: createdBy?.username, + username: createdBy?.username ?? '', }, comments: comments .filter(c => { @@ -168,14 +168,14 @@ const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { createdAt: c.createdAt, createdBy: { fullName: c.createdBy.fullName ?? null, - username: c.createdBy.username, + username: c.createdBy.username ?? '', }, updatedAt: c.updatedAt, updatedBy: c.updatedBy != null ? { fullName: c.updatedBy.fullName ?? null, - username: c.updatedBy.username, + username: c.updatedBy.username ?? '', } : null, })), @@ -187,7 +187,7 @@ const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { updatedBy != null ? { fullName: updatedBy.fullName ?? null, - username: updatedBy.username, + username: updatedBy.username ?? '', } : null, }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 85ad4fd3fc47a..4973deef4d91a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -5,8 +5,8 @@ */ import { useReducer, useCallback } from 'react'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import { CasePatchRequest } from '../../../../../../plugins/case/common/api'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchCase } from './api'; import * as i18n from './translations'; @@ -94,6 +94,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => updateCase(response[0]); } dispatch({ type: 'FETCH_SUCCESS' }); + displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster); } } catch (error) { if (!cancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index f676ab944fce4..bc559c5ac4972 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -6,12 +6,7 @@ import * as t from 'io-ts'; -export const RuleTypeSchema = t.keyof({ - query: null, - saved_query: null, - machine_learning: null, -}); -export type RuleType = t.TypeOf; +import { RuleTypeSchema } from '../../../../common/detection_engine/types'; /** * Params is an "record", since it is a type of AlertActionParams which is action templates. diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx index 7269bf1baa5e5..0a30329baf68d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx @@ -12,7 +12,7 @@ import { ReturnRulesStatuses, } from './use_rule_status'; import * as api from './api'; -import { RuleType, Rule } from '../rules/types'; +import { Rule } from '../rules/types'; jest.mock('./api'); @@ -57,7 +57,7 @@ const testRule: Rule = { threat: [], throttle: null, to: 'now', - type: 'query' as RuleType, + type: 'query', updated_at: 'mm/dd/yyyyTHH:MM:sssz', updated_by: 'mockUser', }; diff --git a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx index d67007399abea..536798ffad41b 100644 --- a/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx +++ b/x-pack/legacy/plugins/siem/public/lib/connectors/servicenow.tsx @@ -181,6 +181,7 @@ const ServiceNowConnectorFields: React.FunctionComponent + + + - { let didCancel = false; const fetchData = async () => { try { - const response = await security.authc.getCurrentUser(); - if (!didCancel) { - setUser(convertToCamelCase(response)); + if (security != null) { + const response = await security.authc.getCurrentUser(); + if (!didCancel) { + setUser(convertToCamelCase(response)); + } + } else { + setUser({ + username: i18n.translate('xpack.siem.getCurrentUser.unknownUser', { + defaultMessage: 'Unknown', + }), + email: '', + fullName: '', + roles: [], + enabled: false, + authenticationRealm: { name: '', type: '' }, + lookupRealm: { name: '', type: '' }, + authenticationProvider: '', + }); } } catch (error) { if (!didCancel) { @@ -81,3 +96,29 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => { }, []); return user; }; + +export interface UseGetUserSavedObjectPermissions { + crud: boolean; + read: boolean; +} + +export const useGetUserSavedObjectPermissions = () => { + const [ + savedObjectsPermissions, + setSavedObjectsPermissions, + ] = useState(null); + const uiCapabilities = useKibana().services.application.capabilities; + + useEffect(() => { + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; + const capabilitiesCanUserRead: boolean = + typeof uiCapabilities.siem.show === 'boolean' ? uiCapabilities.siem.show : false; + setSavedObjectsPermissions({ + crud: capabilitiesCanUserCRUD, + read: capabilitiesCanUserRead, + }); + }, [uiCapabilities]); + + return savedObjectsPermissions; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 9255dee461940..2ae35796387b8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -7,16 +7,34 @@ import React from 'react'; import { WrapperPage } from '../../components/wrapper_page'; -import { AllCases } from './components/all_cases'; +import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { AllCases } from './components/all_cases'; + +import { getSavedObjectReadOnly, CaseCallOut } from './components/callout'; +import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; + +const infoReadSavedObject = getSavedObjectReadOnly(); + +export const CasesPage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); -export const CasesPage = React.memo(() => ( - <> - - - - - -)); + return userPermissions == null || userPermissions?.read ? ( + <> + + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + + + + + ) : ( + + ); +}); CasesPage.displayName = 'CasesPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index 890df91c8560e..cbc7bbc62fbf9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -5,22 +5,36 @@ */ import React from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, Redirect } from 'react-router-dom'; -import { CaseView } from './components/case_view'; +import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; +import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { getCaseUrl } from '../../components/link_to'; +import { navTabs } from '../home/home_navigations'; +import { CaseView } from './components/case_view'; +import { getSavedObjectReadOnly, CaseCallOut } from './components/callout'; + +const infoReadSavedObject = getSavedObjectReadOnly(); export const CaseDetailsPage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); const { detailName: caseId } = useParams(); - if (!caseId) { - return null; + const search = useGetUrlSearch(navTabs.case); + + if (userPermissions != null && !userPermissions.read) { + return ; } - return ( + + return caseId != null ? ( <> - + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + - ); + ) : null; }); CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx index 46a777984c6e0..ecc57c50e28eb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -31,6 +31,7 @@ const initialCommentValue: CommentRequest = { interface AddCommentProps { caseId: string; + disabled?: boolean; insertQuote: string | null; onCommentSaving?: () => void; onCommentPosted: (newCase: Case) => void; @@ -38,7 +39,7 @@ interface AddCommentProps { } export const AddComment = React.memo( - ({ caseId, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { + ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { const { isLoading, postComment } = usePostComment(caseId); const { form } = useForm({ defaultValue: initialCommentValue, @@ -87,7 +88,7 @@ export const AddComment = React.memo( bottomRightContent: ( - {createdBy.fullName ?? createdBy.username ?? 'N/A'} + {createdBy.fullName ?? createdBy.username ?? ''} ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index bdcb87b483851..a6da45a8c5bb1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -87,7 +87,7 @@ describe('AllCases', () => { it('should render AllCases', () => { const wrapper = mount( - + ); expect( @@ -132,7 +132,7 @@ describe('AllCases', () => { it('should tableHeaderSortButton AllCases', () => { const wrapper = mount( - + ); wrapper @@ -149,7 +149,7 @@ describe('AllCases', () => { it('closes case when row action icon clicked', () => { const wrapper = mount( - + ); wrapper @@ -182,7 +182,7 @@ describe('AllCases', () => { const wrapper = mount( - + ); wrapper @@ -202,7 +202,7 @@ describe('AllCases', () => { .last() .simulate('click'); expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( - useGetCasesMockState.data.cases.map(theCase => theCase.id) + useGetCasesMockState.data.cases.map(({ id }) => ({ id })) ); }); it('Bulk close status update', () => { @@ -213,7 +213,7 @@ describe('AllCases', () => { const wrapper = mount( - + ); wrapper @@ -238,7 +238,7 @@ describe('AllCases', () => { const wrapper = mount( - + ); wrapper @@ -259,7 +259,7 @@ describe('AllCases', () => { mount( - + ); expect(refetchCases).toBeCalled(); @@ -274,7 +274,7 @@ describe('AllCases', () => { mount( - + ); expect(refetchCases).toBeCalled(); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 27316ab8427cb..161910bb5498a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -17,11 +17,12 @@ import { EuiTableSortingType, } from '@elastic/eui'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; +import { isEmpty } from 'lodash/fp'; import styled, { css } from 'styled-components'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; -import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; +import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cases'; import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; @@ -35,7 +36,7 @@ import { UtilityBarSection, UtilityBarText, } from '../../../../components/utility_bar'; -import { getConfigureCasesUrl, getCreateCaseUrl } from '../../../../components/link_to'; +import { getCreateCaseUrl } from '../../../../components/link_to'; import { getBulkItems } from '../bulk_actions'; import { CaseHeaderPage } from '../case_header_page'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; @@ -45,6 +46,11 @@ import { navTabs } from '../../../home/home_navigations'; import { getActions } from './actions'; import { CasesTableFilters } from './table_filters'; import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; +import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; +import { getActionLicenseError } from '../use_push_to_service/helpers'; +import { CaseCallOut } from '../callout'; +import { ConfigureCaseButton } from '../configure_cases/button'; +import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -75,9 +81,13 @@ const getSortField = (field: string): SortFieldCase => { } return SortFieldCase.createdAt; }; -export const AllCases = React.memo(() => { - const urlSearch = useGetUrlSearch(navTabs.case); +interface AllCasesProps { + userCanCrud: boolean; +} +export const AllCases = React.memo(({ userCanCrud }) => { + const urlSearch = useGetUrlSearch(navTabs.case); + const { actionLicense } = useGetActionLicense(); const { countClosedCases, countOpenCases, @@ -107,11 +117,24 @@ export const AllCases = React.memo(() => { isDisplayConfirmDeleteModal, } = useDeleteCases(); - const { dispatchResetIsUpdated, isUpdated, updateBulkStatus } = useUpdateCases(); + // Update case + const { + dispatchResetIsUpdated, + isLoading: isUpdating, + isUpdated, + updateBulkStatus, + } = useUpdateCases(); + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); const refreshCases = useCallback(() => { refetchCases(filterOptions, queryParams); fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); }, [filterOptions, queryParams]); useEffect(() => { @@ -124,11 +147,6 @@ export const AllCases = React.memo(() => { dispatchResetIsUpdated(); } }, [isDeleted, isUpdated]); - const [deleteThisCase, setDeleteThisCase] = useState({ - title: '', - id: '', - }); - const [deleteBulk, setDeleteBulk] = useState([]); const confirmDeleteModal = useMemo( () => ( { onCancel={handleToggleModal} onConfirm={handleOnDeleteConfirm.bind( null, - deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id] + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] )} /> ), @@ -150,10 +168,20 @@ export const AllCases = React.memo(() => { setDeleteThisCase(deleteCase); }, []); - const toggleBulkDeleteModal = useCallback((deleteCases: string[]) => { - handleToggleModal(); - setDeleteBulk(deleteCases); - }, []); + const toggleBulkDeleteModal = useCallback( + (caseIds: string[]) => { + handleToggleModal(); + if (caseIds.length === 1) { + const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]); + if (singleCase) { + return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + } + } + const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id })); + setDeleteBulk(convertToDeleteCases); + }, + [selectedCases] + ); const handleUpdateCaseStatus = useCallback( (status: string) => { @@ -199,6 +227,8 @@ export const AllCases = React.memo(() => { [filterOptions.status, toggleDeleteModal, handleDispatchUpdate] ); + const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); + const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { let newQueryParams = queryParams; @@ -233,10 +263,10 @@ export const AllCases = React.memo(() => { [filterOptions, queryParams] ); - const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions, filterOptions.status), [ - actions, - filterOptions.status, - ]); + const memoizedGetCasesColumns = useMemo( + () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status), + [actions, filterOptions.status, userCanCrud] + ); const memoizedPagination = useMemo( () => ({ pageIndex: queryParams.page - 1, @@ -259,8 +289,12 @@ export const AllCases = React.memo(() => { [loading] ); const isDataEmpty = useMemo(() => data.total === 0, [data]); + return ( <> + {!isEmpty(actionsErrors) && ( + + )} @@ -278,18 +312,28 @@ export const AllCases = React.memo(() => { /> - - {i18n.CONFIGURE_CASES_BUTTON} - + } + titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} + urlSearch={urlSearch} + /> - + {i18n.CREATE_TITLE} - {(isCasesLoading || isDeleting) && !isDataEmpty && ( + {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( )} @@ -321,15 +365,16 @@ export const AllCases = React.memo(() => { {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} - - {i18n.BULK_ACTIONS} - - + {userCanCrud && ( + + {i18n.BULK_ACTIONS} + + )} {i18n.REFRESH} @@ -339,7 +384,7 @@ export const AllCases = React.memo(() => { { body={i18n.NO_CASES_BODY} actions={ { } onChange={tableOnChangeCallback} pagination={memoizedPagination} - selection={euiBasicTableSelectionProps} + selection={userCanCrud ? euiBasicTableSelectionProps : {}} sorting={sorting} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index a71ad1c45a980..a344dd7891010 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -43,7 +43,7 @@ const CasesTableFiltersComponent = ({ initial = defaultInitial, }: CasesTableFiltersProps) => { const [selectedReporters, setselectedReporters] = useState( - initial.reporters.map(r => r.full_name ?? r.username) + initial.reporters.map(r => r.full_name ?? r.username ?? '') ); const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx new file mode 100644 index 0000000000000..929e8640dceb6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as i18n from './translations'; + +export const getSavedObjectReadOnly = () => ({ + title: i18n.READ_ONLY_SAVED_OBJECT_TITLE, + description: i18n.READ_ONLY_SAVED_OBJECT_MSG, +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx similarity index 59% rename from x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx index 15b50e4b4cd8d..30a95db2d82a5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx @@ -5,22 +5,28 @@ */ import { EuiCallOut, EuiButton, EuiDescriptionList, EuiSpacer } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; import React, { memo, useCallback, useState } from 'react'; import * as i18n from './translations'; -interface ErrorsPushServiceCallOut { - errors: Array<{ title: string; description: JSX.Element }>; +export * from './helpers'; + +interface CaseCallOutProps { + title: string; + message?: string; + messages?: Array<{ title: string; description: JSX.Element }>; } -const ErrorsPushServiceCallOutComponent = ({ errors }: ErrorsPushServiceCallOut) => { +const CaseCallOutComponent = ({ title, message, messages }: CaseCallOutProps) => { const [showCallOut, setShowCallOut] = useState(true); const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); return showCallOut ? ( <> - - + + {!isEmpty(messages) && } + {!isEmpty(message) &&

{message}

} {i18n.DISMISS_CALLOUT} @@ -30,4 +36,4 @@ const ErrorsPushServiceCallOutComponent = ({ errors }: ErrorsPushServiceCallOut) ) : null; }; -export const ErrorsPushServiceCallOut = memo(ErrorsPushServiceCallOutComponent); +export const CaseCallOut = memo(CaseCallOutComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/translations.ts similarity index 50% rename from x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts rename to x-pack/legacy/plugins/siem/public/pages/case/components/callout/translations.ts index 57712e720f6d0..f70225b841162 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/translations.ts @@ -6,10 +6,18 @@ import { i18n } from '@kbn/i18n'; -export const ERROR_PUSH_SERVICE_CALLOUT_TITLE = i18n.translate( - 'xpack.siem.case.errorsPushServiceCallOutTitle', +export const READ_ONLY_SAVED_OBJECT_TITLE = i18n.translate( + 'xpack.siem.case.readOnlySavedObjectTitle', { - defaultMessage: 'To send cases to external systems, you need to:', + defaultMessage: 'You have read-only feature privileges', + } +); + +export const READ_ONLY_SAVED_OBJECT_MSG = i18n.translate( + 'xpack.siem.case.readOnlySavedObjectDescription', + { + defaultMessage: + 'You are only allowed to view cases. If you need to open and update cases, contact your Kibana administrator', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx index 5037987845326..2b16dfa150d61 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx @@ -35,6 +35,7 @@ interface CaseStatusProps { badgeColor: string; buttonLabel: string; caseData: Case; + disabled?: boolean; icon: string; isLoading: boolean; isSelected: boolean; @@ -49,6 +50,7 @@ const CaseStatusComp: React.FC = ({ badgeColor, buttonLabel, caseData, + disabled = false, icon, isLoading, isSelected, @@ -89,6 +91,7 @@ const CaseStatusComp: React.FC = ({ = ({ /> - +
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx index c4f1888df39e9..0e57326707e97 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -12,6 +12,7 @@ const fetchCase = jest.fn(); export const caseProps: CaseProps = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + userCanCrud: true, caseData: { closedAt: null, closedBy: null, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx index 1be0d6a3b5fcc..49f5f44cba271 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx @@ -60,6 +60,6 @@ describe('CaseView actions', () => { expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([data.id]); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([{ id: data.id, title: data.title }]); }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx index 1d90470eab0e1..0b08b866df964 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; import React, { useMemo } from 'react'; - import { Redirect } from 'react-router-dom'; import * as i18n from './translations'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; @@ -16,9 +16,10 @@ import { Case } from '../../../../containers/case/types'; interface CaseViewActions { caseData: Case; + disabled?: boolean; } -const CaseViewActionsComponent: React.FC = ({ caseData }) => { +const CaseViewActionsComponent: React.FC = ({ caseData, disabled = false }) => { // Delete case const { handleToggleModal, @@ -34,7 +35,7 @@ const CaseViewActionsComponent: React.FC = ({ caseData }) => { isModalVisible={isDisplayConfirmDeleteModal} isPlural={false} onCancel={handleToggleModal} - onConfirm={handleOnDeleteConfirm.bind(null, [caseData.id])} + onConfirm={handleOnDeleteConfirm.bind(null, [{ id: caseData.id, title: caseData.title }])} /> ), [isDisplayConfirmDeleteModal, caseData] @@ -43,11 +44,12 @@ const CaseViewActionsComponent: React.FC = ({ caseData }) => { const propertyActions = useMemo( () => [ { + disabled, iconType: 'trash', label: i18n.DELETE_CASE, onClick: handleToggleModal, }, - ...(caseData.externalService?.externalUrl !== null + ...(caseData.externalService != null && !isEmpty(caseData.externalService?.externalUrl) ? [ { iconType: 'popout', @@ -57,7 +59,7 @@ const CaseViewActionsComponent: React.FC = ({ caseData }) => { ] : []), ], - [handleToggleModal, caseData] + [disabled, handleToggleModal, caseData] ); if (isDeleted) { diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx index 92fc43eff53e9..3f5b3a3127177 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -16,10 +16,10 @@ import { TestProviders } from '../../../../mock'; import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions'; import { wait } from '../../../../lib/helpers'; -import { usePushToService } from './push_to_service'; +import { usePushToService } from '../use_push_to_service'; jest.mock('../../../../containers/case/use_update_case'); jest.mock('../../../../containers/case/use_get_case_user_actions'); -jest.mock('./push_to_service'); +jest.mock('../use_push_to_service'); const useUpdateCaseMock = useUpdateCase as jest.Mock; const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; const usePushToServiceMock = usePushToService as jest.Mock; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 07834c3fb0678..947da51365d66 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -34,10 +34,11 @@ import { CaseStatus } from '../case_status'; import { navTabs } from '../../../home/home_navigations'; import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions'; -import { usePushToService } from './push_to_service'; +import { usePushToService } from '../use_push_to_service'; interface Props { caseId: string; + userCanCrud: boolean; } const MyWrapper = styled(WrapperPage)` @@ -55,15 +56,14 @@ const MyEuiHorizontalRule = styled(EuiHorizontalRule)` } `; -export interface CaseProps { - caseId: string; +export interface CaseProps extends Props { fetchCase: () => void; caseData: Case; updateCase: (newCase: Case) => void; } export const CaseComponent = React.memo( - ({ caseId, caseData, fetchCase, updateCase }) => { + ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { const basePath = window.location.origin + useBasePath(); const caseLink = `${basePath}/app/siem#/case/${caseId}`; const search = useGetUrlSearch(navTabs.case); @@ -152,6 +152,7 @@ export const CaseComponent = React.memo( caseStatus: caseData.status, isNew: caseUserActions.filter(cua => cua.action === 'push-to-service').length === 0, updateCase: handleUpdateCase, + userCanCrud, }); const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); @@ -219,6 +220,7 @@ export const CaseComponent = React.memo( data-test-subj="case-view-title" titleNode={ ( > ( lastIndexPushToService={lastIndexPushToService} onUpdateField={onUpdateField} updateCase={updateCase} + userCanCrud={userCanCrud} /> @@ -260,6 +264,7 @@ export const CaseComponent = React.memo( ( /> ( } ); -export const CaseView = React.memo(({ caseId }: Props) => { +export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId); if (isError) { return null; @@ -317,7 +323,13 @@ export const CaseView = React.memo(({ caseId }: Props) => { } return ( - + ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index 3fc963fc23102..17132b9610754 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -118,56 +118,3 @@ export const EMAIL_BODY = (caseUrl: string) => values: { caseUrl }, defaultMessage: 'Case reference: {caseUrl}', }); - -export const PUSH_SERVICENOW = i18n.translate('xpack.siem.case.caseView.pushAsServicenowIncident', { - defaultMessage: 'Push as ServiceNow incident', -}); - -export const UPDATE_PUSH_SERVICENOW = i18n.translate( - 'xpack.siem.case.caseView.updatePushAsServicenowIncident', - { - defaultMessage: 'Update ServiceNow incident', - } -); - -export const PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE = i18n.translate( - 'xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigTitle', - { - defaultMessage: 'Configure external connector', - } -); - -export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE = i18n.translate( - 'xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedTitle', - { - defaultMessage: 'Reopen the case', - } -); - -export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate( - 'xpack.siem.case.caseView.pushToServiceDisableByConfigTitle', - { - defaultMessage: 'Enable ServiceNow in Kibana configuration file', - } -); - -export const PUSH_DISABLE_BY_LICENSE_TITLE = i18n.translate( - 'xpack.siem.case.caseView.pushToServiceDisableByLicenseTitle', - { - defaultMessage: 'Upgrade to Elastic Platinum', - } -); - -export const LINK_CLOUD_DEPLOYMENT = i18n.translate( - 'xpack.siem.case.caseView.cloudDeploymentLink', - { - defaultMessage: 'cloud deployment', - } -); - -export const LINK_CONNECTOR_CONFIGURE = i18n.translate( - 'xpack.siem.case.caseView.connectorConfigureLink', - { - defaultMessage: 'connector', - } -); 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 new file mode 100644 index 0000000000000..9cfc51da22e87 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/button.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiToolTip } from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import { getConfigureCasesUrl } from '../../../../components/link_to'; + +interface ConfigureCaseButtonProps { + label: string; + isDisabled: boolean; + msgTooltip: JSX.Element; + showToolTip: boolean; + titleTooltip: string; + urlSearch: string; +} + +const ConfigureCaseButtonComponent: React.FC = ({ + isDisabled, + label, + msgTooltip, + showToolTip, + titleTooltip, + urlSearch, +}: ConfigureCaseButtonProps) => { + const configureCaseButton = useMemo( + () => ( + + {label} + + ), + [label, isDisabled, urlSearch] + ); + return showToolTip ? ( + {msgTooltip}

}> + {configureCaseButton} +
+ ) : ( + <>{configureCaseButton} + ); +}; + +export const ConfigureCaseButton = memo(ConfigureCaseButtonComponent); 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 bb0c50b3b193a..8fb1cfb1aa6cc 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 @@ -48,7 +48,9 @@ const ConnectorsComponent: React.FC = ({ {i18n.INCIDENT_MANAGEMENT_SYSTEM_LABEL} - {i18n.ADD_NEW_CONNECTOR} + + {i18n.ADD_NEW_CONNECTOR} + ); 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 a1f24275df6cd..241b0b1230274 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 @@ -57,8 +57,12 @@ const FormWrapper = styled.div` margin-top 40px; } - padding-top: ${theme.eui.paddingSizes.l}; - padding-bottom: ${theme.eui.paddingSizes.l}; + & > :first-child { + margin-top: 0; + } + + padding-top: ${theme.eui.paddingSizes.xl}; + padding-bottom: ${theme.eui.paddingSizes.xl}; `} `; @@ -80,7 +84,11 @@ const actionTypes: ActionType[] = [ }, ]; -const ConfigureCasesComponent: React.FC = () => { +interface ConfigureCasesComponentProps { + userCanCrud: boolean; +} + +const ConfigureCasesComponent: React.FC = ({ userCanCrud }) => { const search = useGetUrlSearch(navTabs.case); const { http, triggers_actions_ui, notifications, application } = useKibana().services; @@ -251,7 +259,7 @@ const ConfigureCasesComponent: React.FC = () => { { void; iconType: string; label: string; @@ -16,13 +17,14 @@ export interface PropertyActionButtonProps { const ComponentId = 'property-actions'; const PropertyActionButton = React.memo( - ({ onClick, iconType, label }) => ( + ({ disabled = false, onClick, iconType, label }) => ( {label} @@ -76,6 +78,7 @@ export const PropertyActions = React.memo(({ propertyActio {propertyActions.map((action, key) => ( onClosePopover(action.onClick)} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx index 3513d4de12aa1..7c456d27aceda 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -23,6 +23,7 @@ import { schema } from './schema'; import { CommonUseField } from '../create'; interface TagListProps { + disabled?: boolean; isLoading: boolean; onSubmit: (a: string[]) => void; tags: string[]; @@ -37,89 +38,98 @@ const MyFlexGroup = styled(EuiFlexGroup)` `} `; -export const TagList = React.memo(({ isLoading, onSubmit, tags }: TagListProps) => { - const { form } = useForm({ - defaultValue: { tags }, - options: { stripEmptyFields: false }, - schema, - }); - const [isEditTags, setIsEditTags] = useState(false); +export const TagList = React.memo( + ({ disabled = false, isLoading, onSubmit, tags }: TagListProps) => { + const { form } = useForm({ + defaultValue: { tags }, + options: { stripEmptyFields: false }, + schema, + }); + const [isEditTags, setIsEditTags] = useState(false); - const onSubmitTags = useCallback(async () => { - const { isValid, data: newData } = await form.submit(); - if (isValid && newData.tags) { - onSubmit(newData.tags); - setIsEditTags(false); - } - }, [form, onSubmit]); + const onSubmitTags = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid && newData.tags) { + onSubmit(newData.tags); + setIsEditTags(false); + } + }, [form, onSubmit]); - return ( - - - -

{i18n.TAGS}

-
- {isLoading && } - {!isLoading && ( + return ( + + - +

{i18n.TAGS}

- )} -
- - - {tags.length === 0 && !isEditTags &&

{i18n.NO_TAGS}

} - {tags.length > 0 && - !isEditTags && - tags.map((tag, key) => ( - - {tag} - - ))} - {isEditTags && ( - - -
- - -
- - - - - {i18n.SAVE} - - - - - {i18n.CANCEL} - - - + {isLoading && } + {!isLoading && ( + + -
- )} -
-
- ); -}); + )} +
+ + + {tags.length === 0 && !isEditTags &&

{i18n.NO_TAGS}

} + {tags.length > 0 && + !isEditTags && + tags.map((tag, key) => ( + + {tag} + + ))} + {isEditTags && ( + + +
+ + +
+ + + + + {i18n.SAVE} + + + + + {i18n.CANCEL} + + + + +
+ )} +
+
+ ); + } +); TagList.displayName = 'TagList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx new file mode 100644 index 0000000000000..1e4fd92058e8d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx @@ -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 { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + +import * as i18n from './translations'; +import { ActionLicense } from '../../../../containers/case/types'; + +export const getLicenseError = () => ({ + title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE, + description: ( + + {i18n.LINK_CLOUD_DEPLOYMENT} + + ), + }} + /> + ), +}); + +export const getKibanaConfigError = () => ({ + title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE, + description: ( + + {'coming soon...'} + + ), + }} + /> + ), +}); + +export const getActionLicenseError = ( + actionLicense: ActionLicense | null +): Array<{ title: string; description: JSX.Element }> => { + let errors: Array<{ title: string; description: JSX.Element }> = []; + if (actionLicense != null && !actionLicense.enabledInLicense) { + errors = [...errors, getLicenseError()]; + } + if (actionLicense != null && !actionLicense.enabledInConfig) { + errors = [...errors, getKibanaConfigError()]; + } + return errors; +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx similarity index 71% rename from x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx rename to x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx index 944302c1940ee..aeb694e52b7fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/push_to_service.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx @@ -5,8 +5,8 @@ */ import { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useState, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useState, useMemo } from 'react'; import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; import { Case } from '../../../../containers/case/types'; @@ -15,7 +15,8 @@ import { usePostPushToService } from '../../../../containers/case/use_post_push_ import { getConfigureCasesUrl } from '../../../../components/link_to'; import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; import { navTabs } from '../../../home/home_navigations'; -import { ErrorsPushServiceCallOut } from '../errors_push_service_callout'; +import { CaseCallOut } from '../callout'; +import { getLicenseError, getKibanaConfigError } from './helpers'; import * as i18n from './translations'; interface UsePushToService { @@ -23,6 +24,7 @@ interface UsePushToService { caseStatus: string; isNew: boolean; updateCase: (newCase: Case) => void; + userCanCrud: boolean; } interface Connector { @@ -38,8 +40,9 @@ interface ReturnUsePushToService { export const usePushToService = ({ caseId, caseStatus, - updateCase, isNew, + updateCase, + userCanCrud, }: UsePushToService): ReturnUsePushToService => { const urlSearch = useGetUrlSearch(navTabs.case); const [connector, setConnector] = useState(null); @@ -69,25 +72,7 @@ export const usePushToService = ({ const errorsMsg = useMemo(() => { let errors: Array<{ title: string; description: JSX.Element }> = []; if (actionLicense != null && !actionLicense.enabledInLicense) { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE, - description: ( - - {i18n.LINK_CLOUD_DEPLOYMENT} - - ), - }} - /> - ), - }, - ]; + errors = [...errors, getLicenseError()]; } if (connector == null && !loadingCaseConfigure && !loadingLicense) { errors = [ @@ -125,25 +110,7 @@ export const usePushToService = ({ ]; } if (actionLicense != null && !actionLicense.enabledInConfig) { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE, - description: ( - - {'coming soon...'} - - ), - }} - /> - ), - }, - ]; + errors = [...errors, getKibanaConfigError()]; } return errors; }, [actionLicense, caseStatus, connector, loadingCaseConfigure, loadingLicense, urlSearch]); @@ -154,13 +121,27 @@ export const usePushToService = ({ fill iconType="importAction" onClick={handlePushToService} - disabled={isLoading || loadingLicense || loadingCaseConfigure || errorsMsg.length > 0} + disabled={ + isLoading || + loadingLicense || + loadingCaseConfigure || + errorsMsg.length > 0 || + !userCanCrud + } isLoading={isLoading} > {isNew ? i18n.PUSH_SERVICENOW : i18n.UPDATE_PUSH_SERVICENOW} ), - [isNew, handlePushToService, isLoading, loadingLicense, loadingCaseConfigure, errorsMsg] + [ + isNew, + handlePushToService, + isLoading, + loadingLicense, + loadingCaseConfigure, + errorsMsg, + userCanCrud, + ] ); const objToReturn = useMemo( @@ -177,7 +158,10 @@ export const usePushToService = ({ ) : ( <>{pushToServiceButton} ), - pushCallouts: errorsMsg.length > 0 ? : null, + pushCallouts: + errorsMsg.length > 0 ? ( + + ) : null, }), [errorsMsg, pushToServiceButton] ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts new file mode 100644 index 0000000000000..14bdb0c69712c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts @@ -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 { i18n } from '@kbn/i18n'; + +export const ERROR_PUSH_SERVICE_CALLOUT_TITLE = i18n.translate( + 'xpack.siem.case.caseView.errorsPushServiceCallOutTitle', + { + defaultMessage: 'To send cases to external systems, you need to:', + } +); + +export const PUSH_SERVICENOW = i18n.translate('xpack.siem.case.caseView.pushAsServicenowIncident', { + defaultMessage: 'Push as ServiceNow incident', +}); + +export const UPDATE_PUSH_SERVICENOW = i18n.translate( + 'xpack.siem.case.caseView.updatePushAsServicenowIncident', + { + defaultMessage: 'Update ServiceNow incident', + } +); + +export const PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE = i18n.translate( + 'xpack.siem.case.caseView.pushToServiceDisableByNoCaseConfigTitle', + { + defaultMessage: 'Configure external connector', + } +); + +export const PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE = i18n.translate( + 'xpack.siem.case.caseView.pushToServiceDisableBecauseCaseClosedTitle', + { + defaultMessage: 'Reopen the case', + } +); + +export const PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE = i18n.translate( + 'xpack.siem.case.caseView.pushToServiceDisableByConfigTitle', + { + defaultMessage: 'Enable ServiceNow in Kibana configuration file', + } +); + +export const PUSH_DISABLE_BY_LICENSE_TITLE = i18n.translate( + 'xpack.siem.case.caseView.pushToServiceDisableByLicenseTitle', + { + defaultMessage: 'Upgrade to Elastic Platinum', + } +); + +export const LINK_CLOUD_DEPLOYMENT = i18n.translate( + 'xpack.siem.case.caseView.cloudDeploymentLink', + { + defaultMessage: 'cloud deployment', + } +); + +export const LINK_CONNECTOR_CONFIGURE = i18n.translate( + 'xpack.siem.case.caseView.connectorConfigureLink', + { + defaultMessage: 'connector', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index 75013c0afde5d..0892d5dcb3ee7 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -29,6 +29,7 @@ export interface UserActionTreeProps { lastIndexPushToService: number; onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void; updateCase: (newCase: Case) => void; + userCanCrud: boolean; } const MyEuiFlexGroup = styled(EuiFlexGroup)` @@ -49,6 +50,7 @@ export const UserActionTree = React.memo( lastIndexPushToService, onUpdateField, updateCase, + userCanCrud, }: UserActionTreeProps) => { const { commentId } = useParams(); const handlerTimeoutId = useRef(0); @@ -146,13 +148,14 @@ export const UserActionTree = React.memo( () => ( ), - [caseData.id, handleUpdate, insertQuote] + [caseData.id, handleUpdate, insertQuote, userCanCrud] ); useEffect(() => { @@ -168,17 +171,18 @@ export const UserActionTree = React.memo( <> {i18n.ADDED_DESCRIPTION}} - fullName={caseData.createdBy.fullName ?? caseData.createdBy.username} + fullName={caseData.createdBy.fullName ?? caseData.createdBy.username ?? ''} markdown={MarkdownDescription} onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)} onQuote={handleManageQuote.bind(null, caseData.description)} - username={caseData.createdBy.username} + username={caseData.createdBy.username ?? 'Unknown'} /> {caseUserActions.map((action, index) => { @@ -189,6 +193,7 @@ export const UserActionTree = React.memo( {i18n.ADDED_COMMENT}} - fullName={comment.createdBy.fullName ?? comment.createdBy.username} + fullName={comment.createdBy.fullName ?? comment.createdBy.username ?? ''} markdown={ ); @@ -231,6 +236,7 @@ export const UserActionTree = React.memo( ); } @@ -263,12 +269,13 @@ export const UserActionTree = React.memo( )} ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts index 0ca6bcff513fc..066145f7762c9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/translations.ts @@ -22,13 +22,13 @@ export const REQUIRED_UPDATE_TO_SERVICE = i18n.translate( } ); -export const COPY_LINK_COMMENT = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', { - defaultMessage: 'click to copy comment link', +export const COPY_REFERENCE_LINK = i18n.translate('xpack.siem.case.caseView.copyCommentLinkAria', { + defaultMessage: 'Copy reference link', }); export const MOVE_TO_ORIGINAL_COMMENT = i18n.translate( 'xpack.siem.case.caseView.moveToCommentAria', { - defaultMessage: 'click to highlight the reference comment', + defaultMessage: 'Highlight the referenced comment', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx index cc36e791e35b4..89b94d98f91db 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx @@ -21,6 +21,7 @@ import * as i18n from './translations'; interface UserActionItemProps { createdAt: string; + disabled: boolean; id: string; isEditable: boolean; isLoading: boolean; @@ -110,6 +111,7 @@ const PushedInfoContainer = styled.div` export const UserActionItem = ({ createdAt, + disabled, id, idToOutline, isEditable, @@ -148,12 +150,14 @@ export const UserActionItem = ({ > } linkId={linkId} + fullName={fullName} username={username} updatedAt={updatedAt} onEdit={onEdit} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx index 94185cb4d130c..9ccf921c87602 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiLoadingSpinner, EuiFlexGroup, EuiFlexItem, EuiText, EuiButtonIcon } from '@elastic/eui'; +import { + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButtonIcon, + EuiToolTip, +} from '@elastic/eui'; import { FormattedRelative } from '@kbn/i18n/react'; import copy from 'copy-to-clipboard'; import { isEmpty } from 'lodash/fp'; @@ -27,12 +34,14 @@ const MySpinner = styled(EuiLoadingSpinner)` interface UserActionTitleProps { createdAt: string; + disabled: boolean; id: string; isLoading: boolean; labelEditAction?: string; labelQuoteAction?: string; labelTitle: JSX.Element; linkId?: string | null; + fullName?: string | null; updatedAt?: string | null; username: string; onEdit?: (id: string) => void; @@ -42,12 +51,14 @@ interface UserActionTitleProps { export const UserActionTitle = ({ createdAt, + disabled, id, isLoading, labelEditAction, labelQuoteAction, labelTitle, linkId, + fullName, username, updatedAt, onEdit, @@ -61,6 +72,7 @@ export const UserActionTitle = ({ ...(labelEditAction != null && onEdit != null ? [ { + disabled, iconType: 'pencil', label: labelEditAction, onClick: () => onEdit(id), @@ -70,6 +82,7 @@ export const UserActionTitle = ({ ...(labelQuoteAction != null && onQuote != null ? [ { + disabled, iconType: 'quote', label: labelQuoteAction, onClick: () => onQuote(id), @@ -77,7 +90,7 @@ export const UserActionTitle = ({ ] : []), ]; - }, [id, labelEditAction, onEdit, labelQuoteAction, onQuote]); + }, [disabled, id, labelEditAction, onEdit, labelQuoteAction, onQuote]); const handleAnchorLink = useCallback(() => { copy( @@ -105,7 +118,9 @@ export const UserActionTitle = ({ - {username} + {fullName ?? username}

}> + {username} +
{labelTitle} @@ -137,20 +152,24 @@ export const UserActionTitle = ({ {!isEmpty(linkId) && ( - + {i18n.MOVE_TO_ORIGINAL_COMMENT}

}> + +
)} - + {i18n.COPY_REFERENCE_LINK}

}> + +
{propertyActions.length > 0 && ( diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index 3109f2382c362..914bbe1d3f38f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -13,6 +13,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, + EuiToolTip, } from '@elastic/eui'; import styled, { css } from 'styled-components'; import { ElasticUser } from '../../../../containers/case/types'; @@ -40,20 +41,22 @@ const MyFlexGroup = styled(EuiFlexGroup)` const renderUsers = ( users: ElasticUser[], handleSendEmail: (emailAddress: string | undefined | null) => void -) => { - return users.map(({ fullName, username, email }, key) => ( +) => + users.map(({ fullName, username, email }, key) => ( - + -

- - {username} - -

+ {fullName ?? username}

}> +

+ + {username} + +

+
@@ -63,11 +66,11 @@ const renderUsers = ( onClick={handleSendEmail.bind(null, email)} iconType="email" aria-label="email" + isDisabled={email == null} />
)); -}; export const UserList = React.memo(({ email, headline, loading, users }: UserListProps) => { const handleSendEmail = useCallback( @@ -78,7 +81,7 @@ export const UserList = React.memo(({ email, headline, loading, users }: UserLis }, [email.subject] ); - return ( + return users.filter(({ username }) => username != null && username !== '').length > 0 ? (

{headline}

@@ -89,9 +92,12 @@ export const UserList = React.memo(({ email, headline, loading, users }: UserLis
)} - {renderUsers(users, handleSendEmail)} + {renderUsers( + users.filter(({ username }) => username != null && username !== ''), + handleSendEmail + )} - ); + ) : null; }); UserList.displayName = 'UserList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx index b7e7ced308331..7515efa0e1b7a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx @@ -5,16 +5,18 @@ */ import React, { useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; +import { getCaseUrl } from '../../components/link_to'; +import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; import { WrapperPage } from '../../components/wrapper_page'; -import { CaseHeaderPage } from './components/case_header_page'; +import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { getCaseUrl } from '../../components/link_to'; +import { navTabs } from '../home/home_navigations'; +import { CaseHeaderPage } from './components/case_header_page'; +import { ConfigureCases } from './components/configure_cases'; import { WhitePageWrapper, SectionWrapper } from './components/wrappers'; import * as i18n from './translations'; -import { ConfigureCases } from './components/configure_cases'; -import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; -import { navTabs } from '../home/home_navigations'; const wrapperPageStyle: Record = { paddingLeft: '0', @@ -23,6 +25,7 @@ const wrapperPageStyle: Record = { }; const ConfigureCasesPageComponent: React.FC = () => { + const userPermissions = useGetUserSavedObjectPermissions(); const search = useGetUrlSearch(navTabs.case); const backOptions = useMemo( @@ -33,6 +36,10 @@ const ConfigureCasesPageComponent: React.FC = () => { [search] ); + if (userPermissions != null && !userPermissions.read) { + return ; + } + return ( <> @@ -40,7 +47,7 @@ const ConfigureCasesPageComponent: React.FC = () => {
- + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx index bd1f6da0ca28b..06cb7fadfb8d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -5,17 +5,20 @@ */ import React, { useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; +import { getCaseUrl } from '../../components/link_to'; +import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; import { WrapperPage } from '../../components/wrapper_page'; -import { Create } from './components/create'; +import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { navTabs } from '../home/home_navigations'; import { CaseHeaderPage } from './components/case_header_page'; +import { Create } from './components/create'; import * as i18n from './translations'; -import { getCaseUrl } from '../../components/link_to'; -import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; -import { navTabs } from '../home/home_navigations'; export const CreateCasePage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); const search = useGetUrlSearch(navTabs.case); const backOptions = useMemo( @@ -26,6 +29,10 @@ export const CreateCasePage = React.memo(() => { [search] ); + if (userPermissions != null && !userPermissions.crud) { + return ; + } + return ( <> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/saved_object_no_permissions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/saved_object_no_permissions.tsx new file mode 100644 index 0000000000000..689c290c91019 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/saved_object_no_permissions.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EmptyPage } from '../../components/empty_page'; +import * as i18n from './translations'; +import { useKibana } from '../../lib/kibana'; + +export const CaseSavedObjectNoPermissions = React.memo(() => { + const docLinks = useKibana().services.docLinks; + + return ( + + ); +}); + +CaseSavedObjectNoPermissions.displayName = 'CaseSavedObjectNoPermissions'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 8f9d2087699f8..0d1e6d1435ca3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -6,6 +6,21 @@ import { i18n } from '@kbn/i18n'; +export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.siem.case.caseSavedObjectNoPermissionsTitle', + { + defaultMessage: 'Kibana feature privileges required', + } +); + +export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.siem.case.caseSavedObjectNoPermissionsMessage', + { + defaultMessage: + 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + } +); + export const BACK_TO_ALL = i18n.translate('xpack.siem.case.caseView.backLabel', { defaultMessage: 'Back to cases', }); @@ -169,3 +184,10 @@ export const ADD_COMMENT_HELP_TEXT = i18n.translate( export const SAVE = i18n.translate('xpack.siem.case.caseView.description.save', { defaultMessage: 'Save', }); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.siem.case.caseView.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index 6d76fde49634d..5e0293325289b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -71,7 +71,7 @@ export const mockRule = (id: string): Rule => ({ to: 'now', type: 'saved_query', threat: [], - throttle: null, + throttle: 'no_actions', note: '# this is some markdown documentation', version: 1, }); @@ -145,7 +145,7 @@ export const mockRuleWithEverything = (id: string): Rule => ({ ], }, ], - throttle: null, + throttle: 'no_actions', note: '# this is some markdown documentation', version: 1, }); @@ -184,7 +184,7 @@ export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStep actions: [], kibanaSiemAppUrl: 'http://localhost:5601/app/siem', enabled, - throttle: null, + throttle: 'no_actions', }); export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index ebdf0fc27b6c8..a155f3eb2803c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -48,7 +48,6 @@ export const getActions = ( icon: 'controlsHorizontal', name: i18n.EDIT_RULE_SETTINGS, onClick: (rowItem: Rule) => editRuleAction(rowItem, history), - enabled: (rowItem: Rule) => !rowItem.immutable, }, { description: i18n.DUPLICATE_RULE, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index f9b255a95d869..79da7999b081a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -21,13 +21,13 @@ import styled from 'styled-components'; import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; +import { RuleType } from '../../../../../../common/detection_engine/types'; import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; import * as i18n from './translations'; import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; import { SeverityBadge } from '../severity_badge'; import ListTreeIcon from './assets/list_tree_icon.svg'; -import { RuleType } from '../../../../../containers/detection_engine/rules'; import { assertUnreachable } from '../../../../../lib/helpers'; const NoteDescriptionContainer = styled(EuiFlexItem)` diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 69c4ee1017155..05e47225c8f4b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -15,7 +15,7 @@ import { esFilters, FilterManager, } from '../../../../../../../../../../src/plugins/data/public'; -import { RuleType } from '../../../../../containers/detection_engine/rules'; +import { RuleType } from '../../../../../../common/detection_engine/types'; import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; import { useKibana } from '../../../../../lib/kibana'; import { IMitreEnterpriseAttack } from '../../types'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx index 947bf29c07148..8276aa3578563 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx @@ -11,8 +11,8 @@ import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; import { useKibana } from '../../../../../lib/kibana'; import { SiemJob } from '../../../../../components/ml_popover/types'; import { ListItems } from './types'; -import { isJobStarted } from '../../../../../components/ml/helpers'; import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations'; +import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; enum MessageLevels { info = 'info', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx index 4ccde78f3cda7..9d3b37f1788fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx @@ -16,10 +16,10 @@ import { EuiText, } from '@elastic/eui'; +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; +import { RuleType } from '../../../../../../common/detection_engine/types'; import { FieldHook } from '../../../../../shared_imports'; -import { RuleType } from '../../../../../containers/detection_engine/rules/types'; import * as i18n from './translations'; -import { isMlRule } from '../../helpers'; const MlCardDescription = ({ hasValidLicense = false }: { hasValidLicense?: boolean }) => ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 6c46ab0b171a2..05043e5b96a30 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -12,10 +12,11 @@ import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; import { useMlCapabilities } from '../../../../../components/ml_popover/hooks/use_ml_capabilities'; import { useUiSetting$ } from '../../../../../lib/kibana'; -import { setFieldValue, isMlRule } from '../../helpers'; +import { setFieldValue } from '../../helpers'; import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index 271c8fabed3a5..4a132f94a9871 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -10,6 +10,7 @@ import { isEmpty } from 'lodash/fp'; import React from 'react'; import { esKuery } from '../../../../../../../../../../src/plugins/data/public'; +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; import { FieldValueQueryBar } from '../query_bar'; import { ERROR_CODE, @@ -19,7 +20,6 @@ import { ValidationFunc, } from '../../../../../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; -import { isMlRule } from '../../helpers'; export const schema: FormSchema = { index: { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx index 9c16a61822662..aec315938b6ae 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx @@ -141,6 +141,7 @@ const StepRuleActionsComponent: FC = ({ /> )} + diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx index 511427978db3a..bc3b0dfe720bc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx @@ -10,6 +10,7 @@ import { FormSchema } from '../../../../../shared_imports'; export const schema: FormSchema = { actions: {}, + enabled: {}, kibanaSiemAppUrl: {}, throttle: { label: i18n.translate( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts index 212147ec6d4d8..efb601b6bd207 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts @@ -515,10 +515,9 @@ describe('helpers', () => { actions: [], enabled: false, meta: { - throttle: 'no_actions', kibanaSiemAppUrl: 'http://localhost:5601/app/siem', }, - throttle: null, + throttle: 'no_actions', }; expect(result).toEqual(expected); @@ -534,10 +533,9 @@ describe('helpers', () => { actions: [], enabled: false, meta: { - throttle: mockStepData.throttle, kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, }, - throttle: null, + throttle: 'no_actions', }; expect(result).toEqual(expected); @@ -568,10 +566,9 @@ describe('helpers', () => { ], enabled: false, meta: { - throttle: mockStepData.throttle, kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, }, - throttle: null, + throttle: 'rule', }; expect(result).toEqual(expected); @@ -602,7 +599,6 @@ describe('helpers', () => { ], enabled: false, meta: { - throttle: mockStepData.throttle, kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, }, throttle: mockStepData.throttle, @@ -635,10 +631,9 @@ describe('helpers', () => { ], enabled: false, meta: { - throttle: null, kibanaSiemAppUrl: mockStepData.kibanaSiemAppUrl, }, - throttle: null, + throttle: 'no_actions', }; expect(result).toEqual(expected); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index 7abe5a576c0e5..151e3a9bdf4d6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -8,12 +8,11 @@ import { has, isEmpty } from 'lodash/fp'; import moment from 'moment'; import deepmerge from 'deepmerge'; -import { - NOTIFICATION_THROTTLE_RULE, - NOTIFICATION_THROTTLE_NO_ACTIONS, -} from '../../../../../common/constants'; -import { NewRule, RuleType } from '../../../../containers/detection_engine/rules'; +import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../common/constants'; import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; +import { RuleType } from '../../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../../common/detection_engine/ml_helpers'; +import { NewRule } from '../../../../containers/detection_engine/rules'; import { AboutStepRule, @@ -25,7 +24,6 @@ import { AboutStepRuleJson, ActionsStepRuleJson, } from '../types'; -import { isMlRule } from '../helpers'; export const getTimeTypeValue = (time: string): { unit: string; value: number } => { const timeObj = { @@ -144,11 +142,6 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule }; }; -export const getAlertThrottle = (throttle: string | null) => - throttle && ![NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].includes(throttle) - ? throttle - : null; - export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { const { actions = [], @@ -160,9 +153,8 @@ export const formatActionsStepData = (actionsStepData: ActionsStepRule): Actions return { actions: actions.map(transformAlertToRuleAction), enabled, - throttle: actions.length ? getAlertThrottle(throttle) : null, + throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, meta: { - throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, kibanaSiemAppUrl, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index a35caf4acf67b..b8e2310ef0614 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -277,7 +277,7 @@ const RuleDetailsPageComponent: FC = ({ {ruleI18n.EDIT_RULE_SETTINGS} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index f89e3206cc67d..60d6158987a1d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -119,6 +119,7 @@ const EditRulePageComponent: FC = () => { { id: RuleStep.defineRule, name: ruleI18n.DEFINITION, + disabled: rule?.immutable, content: ( <> @@ -140,6 +141,7 @@ const EditRulePageComponent: FC = () => { { id: RuleStep.aboutRule, name: ruleI18n.ABOUT, + disabled: rule?.immutable, content: ( <> @@ -161,6 +163,7 @@ const EditRulePageComponent: FC = () => { { id: RuleStep.scheduleRule, name: ruleI18n.SCHEDULE, + disabled: rule?.immutable, content: ( <> @@ -203,6 +206,7 @@ const EditRulePageComponent: FC = () => { }, ], [ + rule, loading, initLoading, isLoading, @@ -331,10 +335,11 @@ const EditRulePageComponent: FC = () => { }, [rule]); useEffect(() => { - setSelectedTab(tabs[0]); - }, []); + const tabIndex = rule?.immutable ? 3 : 0; + setSelectedTab(tabs[tabIndex]); + }, [rule]); - if (isSaved || (rule != null && rule.immutable)) { + if (isSaved) { displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); return ; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx index fbdfcf4fc75d8..522464d585cca 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -107,7 +107,12 @@ describe('rule helpers', () => { ], }; const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; - const ruleActionsStepData = { enabled: true, throttle: undefined, isNew: false, actions: [] }; + const ruleActionsStepData = { + enabled: true, + throttle: 'no_actions', + isNew: false, + actions: [], + }; const aboutRuleDataDetailsData = { note: '# this is some markdown documentation', description: '24/7', @@ -303,7 +308,7 @@ describe('rule helpers', () => { actions: [], enabled: mockedRule.enabled, isNew: false, - throttle: undefined, + throttle: 'no_actions', }; expect(result).toEqual(expected); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 50b76552ddc8f..b6afba527ccdc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -10,10 +10,11 @@ import moment from 'moment'; import memoizeOne from 'memoize-one'; import { useLocation } from 'react-router-dom'; -import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { Rule, RuleType } from '../../../containers/detection_engine/rules'; +import { Rule } from '../../../containers/detection_engine/rules'; import { FormData, FormHook, FormSchema } from '../../../shared_imports'; import { AboutStepRule, @@ -57,12 +58,12 @@ export const getStepsData = ({ export const getActionsStepsData = ( rule: Omit & { actions: RuleAlertAction[] } ): ActionsStepRule => { - const { enabled, actions = [], meta } = rule; + const { enabled, throttle, meta, actions = [] } = rule; return { actions: actions?.map(transformRuleToAlertAction), isNew: false, - throttle: meta?.throttle, + throttle, kibanaSiemAppUrl: meta?.kibanaSiemAppUrl, enabled, }; @@ -214,8 +215,6 @@ export const setFieldValue = ( } }); -export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; - export const redirectToDetections = ( isSignalIndexExists: boolean | null, isAuthenticated: boolean | null, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index c1db24991c17c..1c366e6640b29 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -5,9 +5,8 @@ */ import { AlertAction } from '../../../../../../../plugins/alerting/common'; -import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; import { Filter } from '../../../../../../../../src/plugins/data/common'; -import { RuleType } from '../../../containers/detection_engine/rules/types'; import { FieldValueQueryBar } from './components/query_bar'; import { FormData, FormHook } from '../../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx new file mode 100644 index 0000000000000..62399891c9606 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { TimelinesPageComponent } from './timelines_page'; +import { useKibana } from '../../lib/kibana'; +import { shallow, ShallowWrapper } from 'enzyme'; +import React from 'react'; +import ApolloClient from 'apollo-client'; + +jest.mock('../../lib/kibana', () => { + return { + useKibana: jest.fn(), + }; +}); +describe('TimelinesPageComponent', () => { + const mockAppollloClient = {} as ApolloClient; + let wrapper: ShallowWrapper; + + describe('If the user is authorised', () => { + beforeAll(() => { + ((useKibana as unknown) as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: true, + }, + }, + }, + }, + }); + wrapper = shallow(); + }); + + afterAll(() => { + ((useKibana as unknown) as jest.Mock).mockReset(); + }); + + test('should not show the import timeline modal by default', () => { + expect( + wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle') + ).toEqual(false); + }); + + test('should show the import timeline button', () => { + expect(wrapper.find('[data-test-subj="open-import-data-modal-btn"]').exists()).toEqual(true); + }); + + test('should show the import timeline modal after user clicking on the button', () => { + wrapper.find('[data-test-subj="open-import-data-modal-btn"]').simulate('click'); + expect( + wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle') + ).toEqual(true); + }); + }); + + describe('If the user is not authorised', () => { + beforeAll(() => { + ((useKibana as unknown) as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: false, + }, + }, + }, + }, + }); + wrapper = shallow(); + }); + + afterAll(() => { + ((useKibana as unknown) as jest.Mock).mockReset(); + }); + test('should not show the import timeline modal by default', () => { + expect( + wrapper.find('[data-test-subj="stateful-open-timeline"]').prop('importDataModalToggle') + ).toEqual(false); + }); + + test('should not show the import timeline button', () => { + expect(wrapper.find('[data-test-subj="open-import-data-modal-btn"]').exists()).toEqual(false); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx index 75bef7a04a4c9..73070d2b94aac 100644 --- a/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/timelines/timelines_page.tsx @@ -28,7 +28,7 @@ type OwnProps = TimelinesProps; export const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; -const TimelinesPageComponent: React.FC = ({ apolloClient }) => { +export const TimelinesPageComponent: React.FC = ({ apolloClient }) => { const [importDataModalToggle, setImportDataModalToggle] = useState(false); const onImportTimelineBtnClick = useCallback(() => { setImportDataModalToggle(true); @@ -43,7 +43,11 @@ const TimelinesPageComponent: React.FC = ({ apolloClient }) => { {capabilitiesCanUserCRUD && ( - + {i18n.ALL_TIMELINES_IMPORT_TIMELINE_TITLE} )} @@ -57,6 +61,7 @@ const TimelinesPageComponent: React.FC = ({ apolloClient }) => { importDataModalToggle={importDataModalToggle && capabilitiesCanUserCRUD} setImportDataModalToggle={setImportDataModalToggle} title={i18n.ALL_TIMELINES_PANEL_TITLE} + data-test-subj="stateful-open-timeline" /> diff --git a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json b/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json index bec6988bdebd9..c4705c8b8c16a 100644 --- a/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json +++ b/x-pack/legacy/plugins/siem/scripts/optimize_tsconfig/tsconfig.json @@ -4,7 +4,8 @@ "plugins/siem/**/*", "legacy/plugins/siem/**/*", "plugins/apm/typings/numeral.d.ts", - "legacy/plugins/canvas/types/webpack.d.ts" + "legacy/plugins/canvas/types/webpack.d.ts", + "plugins/triggers_actions_ui/**/*" ], "exclude": [ "test/**/*", diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts index edcd821353bc8..128a7965cd7dc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/notifications/types.ts @@ -49,7 +49,7 @@ export type UpdateNotificationParams = Omit { try { const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); const callCluster = clusterClient.callAsCurrentUser; + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } + const index = siemClient.signalsIndex; const indexExists = await getIndexExists(callCluster, index); if (indexExists) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index aa418c11d9d16..c667e7ae9c463 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -38,7 +38,11 @@ export const deleteIndexRoute = (router: IRouter) => { try { const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); + + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } const callCluster = clusterClient.callAsCurrentUser; const index = siemClient.signalsIndex; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 4fc5a4e1f347f..047176f155611 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -23,7 +23,11 @@ export const readIndexRoute = (router: IRouter) => { try { const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); + + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } const index = siemClient.signalsIndex; const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts index aa4f6150889f9..3209f5ce9f519 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.test.ts @@ -62,6 +62,13 @@ describe('read_privileges route', () => { expect(response.status).toEqual(500); expect(response.body).toEqual({ message: 'Test error', status_code: 500 }); }); + + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getPrivilegeRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); }); describe('when security plugin is disabled', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 2f5ea4d1ec767..d86880de65386 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -27,9 +27,14 @@ export const readPrivilegesRoute = ( }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); + try { const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); + + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } const index = siemClient.signalsIndex; const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index f53efc8a3234d..f0b975379388f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -63,7 +63,7 @@ describe('add_prepackaged_rules_route', () => { addPrepackedRulesRoute(server.router); }); - describe('status codes with actionClient and alertClient', () => { + describe('status codes', () => { test('returns 200 when creating with a valid actionClient and alertClient', async () => { const request = addPrepackagedRulesRequest(); const response = await server.inject(request, context); @@ -96,6 +96,13 @@ describe('add_prepackaged_rules_route', () => { ), }); }); + + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(addPrepackagedRulesRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); }); describe('responses', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 4e08188af0d12..3eba04debb21f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -33,16 +33,13 @@ export const addPrepackedRulesRoute = (router: IRouter) => { const siemResponse = buildSiemResponse(response); try { - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const clusterClient = context.core.elasticsearch.dataClient; const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 32b8eca298229..e6facf6f3b7a8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -42,7 +42,7 @@ describe('create_rules_bulk', () => { createRulesBulkRoute(server.router); }); - describe('status codes with actionClient and alertClient', () => { + describe('status codes', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { const response = await server.inject(getReadBulkRequest(), context); expect(response.status).toEqual(200); @@ -54,6 +54,13 @@ describe('create_rules_bulk', () => { expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getReadBulkRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); }); describe('unhappy paths', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 1ca9f7ef9075e..d0e36515946a8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -23,6 +23,7 @@ import { } from '../utils'; import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; export const createRulesBulkRoute = (router: IRouter) => { router.post( @@ -37,15 +38,13 @@ export const createRulesBulkRoute = (router: IRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const savedObjectsClient = context.core.savedObjects.client; + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } @@ -115,7 +114,6 @@ export const createRulesBulkRoute = (router: IRouter) => { const createdRule = await createRules({ alertsClient, actionsClient, - actions, anomalyThreshold, description, enabled, @@ -139,7 +137,6 @@ export const createRulesBulkRoute = (router: IRouter) => { name, severity, tags, - throttle, to, type, threat, @@ -148,7 +145,18 @@ export const createRulesBulkRoute = (router: IRouter) => { version, lists, }); - return transformValidateBulkError(ruleIdOrUuid, createdRule); + + const ruleActions = await updateRulesNotifications({ + ruleAlertId: createdRule.id, + alertsClient, + savedObjectsClient, + enabled, + actions, + throttle, + name, + }); + + return transformValidateBulkError(ruleIdOrUuid, createdRule, ruleActions); } catch (err) { return transformBulkError(ruleIdOrUuid, err); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 4da879d12f809..f15f47432f838 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -15,13 +15,12 @@ import { getEmptyIndex, getFindResultWithSingleHit, createMlRuleRequest, - createRuleWithActionsRequest, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; -import { createNotifications } from '../../notifications/create_notifications'; -jest.mock('../../notifications/create_notifications'); +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +jest.mock('../../rules/update_rules_notifications'); describe('create_rules', () => { let server: ReturnType; @@ -49,6 +48,12 @@ describe('create_rules', () => { describe('status codes with actionClient and alertClient', () => { test('returns 200 when creating a single rule with a valid actionClient and alertClient', async () => { + (updateRulesNotifications as jest.Mock).mockResolvedValue({ + id: 'id', + actions: [], + alertThrottle: null, + ruleThrottle: 'no_actions', + }); const response = await server.inject(getCreateRequest(), context); expect(response.status).toEqual(200); }); @@ -60,6 +65,13 @@ describe('create_rules', () => { expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getCreateRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + it('returns 200 if license is not platinum', async () => { (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); @@ -86,18 +98,6 @@ describe('create_rules', () => { }); }); - describe('creating a Notification if throttle and actions were provided ', () => { - it('is successful', async () => { - const response = await server.inject(createRuleWithActionsRequest(), context); - expect(response.status).toEqual(200); - expect(createNotifications).toHaveBeenCalledWith( - expect.objectContaining({ - ruleAlertId: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - }) - ); - }); - }); - describe('unhappy paths', () => { test('it returns a 400 if the index does not exist', async () => { clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index edf37bcb8dbe7..6038ad2095323 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -22,7 +22,7 @@ import { buildSiemResponse, validateLicenseForRuleType, } from '../utils'; -import { createNotifications } from '../../notifications/create_notifications'; +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; export const createRulesRoute = (router: IRouter): void => { router.post( @@ -72,16 +72,13 @@ export const createRulesRoute = (router: IRouter): void => { try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const clusterClient = context.core.elasticsearch.dataClient; const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } @@ -105,7 +102,6 @@ export const createRulesRoute = (router: IRouter): void => { const createdRule = await createRules({ alertsClient, actionsClient, - actions, anomalyThreshold, description, enabled, @@ -129,7 +125,6 @@ export const createRulesRoute = (router: IRouter): void => { name, severity, tags, - throttle, to, type, threat, @@ -139,16 +134,15 @@ export const createRulesRoute = (router: IRouter): void => { lists, }); - if (throttle && actions.length) { - await createNotifications({ - alertsClient, - enabled, - name, - interval, - actions, - ruleAlertId: createdRule.id, - }); - } + const ruleActions = await updateRulesNotifications({ + ruleAlertId: createdRule.id, + alertsClient, + savedObjectsClient, + enabled, + actions, + throttle, + name, + }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes @@ -160,7 +154,11 @@ export const createRulesRoute = (router: IRouter): void => { search: `${createdRule.id}`, searchFields: ['alertId'], }); - const [validated, errors] = transformValidate(createdRule, ruleStatuses.saved_objects[0]); + const [validated, errors] = transformValidate( + createdRule, + ruleActions, + ruleStatuses.saved_objects[0] + ); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 85cfeefdceead..0c5ad2e060924 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -18,6 +18,7 @@ import { import { deleteRules } from '../../rules/delete_rules'; import { deleteNotifications } from '../../notifications/delete_notifications'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; type Config = RouteConfig; type Handler = RequestHandler; @@ -35,11 +36,8 @@ export const deleteRulesBulkRoute = (router: IRouter) => { const handler: Handler = async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!actionsClient || !alertsClient) { @@ -59,6 +57,10 @@ export const deleteRulesBulkRoute = (router: IRouter) => { }); if (rule != null) { await deleteNotifications({ alertsClient, ruleAlertId: rule.id }); + await deleteRuleActionsSavedObject({ + ruleAlertId: rule.id, + savedObjectsClient, + }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -70,7 +72,7 @@ export const deleteRulesBulkRoute = (router: IRouter) => { ruleStatuses.saved_objects.forEach(async obj => savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) ); - return transformValidateBulkError(idOrRuleIdOrUnknown, rule, ruleStatuses); + return transformValidateBulkError(idOrRuleIdOrUnknown, rule, undefined, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 6fd50abd9364a..71724e3ba9b58 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -17,6 +17,7 @@ import { } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { deleteNotifications } from '../../notifications/delete_notifications'; +import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; export const deleteRulesRoute = (router: IRouter) => { router.delete( @@ -34,12 +35,9 @@ export const deleteRulesRoute = (router: IRouter) => { try { const { id, rule_id: ruleId } = request.query; - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!actionsClient || !alertsClient) { @@ -54,6 +52,10 @@ export const deleteRulesRoute = (router: IRouter) => { }); if (rule != null) { await deleteNotifications({ alertsClient, ruleAlertId: rule.id }); + await deleteRuleActionsSavedObject({ + ruleAlertId: rule.id, + savedObjectsClient, + }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -65,7 +67,11 @@ export const deleteRulesRoute = (router: IRouter) => { ruleStatuses.saved_objects.forEach(async obj => savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) ); - const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]); + const [validated, errors] = transformValidate( + rule, + undefined, + ruleStatuses.saved_objects[0] + ); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index c434f42780e47..50eafe163c265 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -28,10 +28,7 @@ export const exportRulesRoute = (router: IRouter, config: LegacyServices['config }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); if (!alertsClient) { return siemResponse.error({ statusCode: 404 }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 961859417ef1b..85555c1a57084 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -15,6 +15,7 @@ import { findRulesSchema } from '../schemas/find_rules_schema'; import { transformValidateFindAlerts } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; export const findRulesRoute = (router: IRouter) => { router.get( @@ -32,10 +33,7 @@ export const findRulesRoute = (router: IRouter) => { try { const { query } = request; - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!alertsClient) { @@ -65,7 +63,18 @@ export const findRulesRoute = (router: IRouter) => { return results; }) ); - const [validated, errors] = transformValidateFindAlerts(rules, ruleStatuses); + const ruleActions = await Promise.all( + rules.data.map(async rule => { + const results = await getRuleActionsSavedObject({ + savedObjectsClient, + ruleAlertId: rule.id, + }); + + return results; + }) + ); + + const [validated, errors] = transformValidateFindAlerts(rules, ruleActions, ruleStatuses); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 4f4ae7c2c1fa6..6fee4d71a904e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -35,10 +35,7 @@ export const findRulesStatusesRoute = (router: IRouter) => { async (context, request, response) => { const { query } = request; const siemResponse = buildSiemResponse(response); - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!alertsClient) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts index 7e16b4495593e..7f0bf4bf81179 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.ts @@ -29,10 +29,7 @@ export const getPrepackagedRulesStatusRoute = (router: IRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); if (!alertsClient) { return siemResponse.error({ statusCode: 404 }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index aacf83b9ec58a..61f5e6faf1bdb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -101,6 +101,13 @@ describe('import_rules_route', () => { expect(response.status).toEqual(404); expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(request, contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); }); describe('unhappy paths', () => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 2e6c72a87ec7f..43e970702ba72 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -57,30 +57,27 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); - const clusterClient = context.core.elasticsearch.dataClient; - const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + try { + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); + const clusterClient = context.core.elasticsearch.dataClient; + const savedObjectsClient = context.core.savedObjects.client; + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { - return siemResponse.error({ statusCode: 404 }); - } + if (!siemClient || !actionsClient || !alertsClient) { + return siemResponse.error({ statusCode: 404 }); + } - const { filename } = request.body.file.hapi; - const fileExtension = extname(filename).toLowerCase(); - if (fileExtension !== '.ndjson') { - return siemResponse.error({ - statusCode: 400, - body: `Invalid file extension ${fileExtension}`, - }); - } + const { filename } = request.body.file.hapi; + const fileExtension = extname(filename).toLowerCase(); + if (fileExtension !== '.ndjson') { + return siemResponse.error({ + statusCode: 400, + body: `Invalid file extension ${fileExtension}`, + }); + } - const objectLimit = config().get('savedObjects.maxImportExportSize'); - try { + const objectLimit = config().get('savedObjects.maxImportExportSize'); const readStream = createRulesStreamFromNdJson(objectLimit); const parsedObjects = await createPromiseFromStreams([ request.body.file, @@ -112,7 +109,6 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config return null; } const { - actions, anomaly_threshold: anomalyThreshold, description, enabled, @@ -135,7 +131,6 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config severity, tags, threat, - throttle, to, type, references, @@ -171,7 +166,6 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config await createRules({ alertsClient, actionsClient, - actions, anomalyThreshold, description, enabled, @@ -198,7 +192,6 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config to, type, threat, - throttle, references, note, version, @@ -209,7 +202,6 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config await patchRules({ alertsClient, actionsClient, - actions, savedObjectsClient, description, enabled, @@ -236,7 +228,6 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config to, type, threat, - throttle, references, note, version, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 645dbdadf8cab..85255594ee480 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -22,6 +22,7 @@ import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; export const patchRulesBulkRoute = (router: IRouter) => { router.patch( @@ -37,11 +38,8 @@ export const patchRulesBulkRoute = (router: IRouter) => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!actionsClient || !alertsClient) { @@ -92,7 +90,6 @@ export const patchRulesBulkRoute = (router: IRouter) => { const rule = await patchRules({ alertsClient, actionsClient, - actions, description, enabled, falsePositives, @@ -118,7 +115,6 @@ export const patchRulesBulkRoute = (router: IRouter) => { to, type, threat, - throttle, references, note, version, @@ -126,6 +122,15 @@ export const patchRulesBulkRoute = (router: IRouter) => { machineLearningJobId, }); if (rule != null) { + const ruleActions = await updateRulesNotifications({ + ruleAlertId: rule.id, + alertsClient, + savedObjectsClient, + enabled: rule.enabled!, + actions, + throttle, + name: rule.name!, + }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -136,7 +141,12 @@ export const patchRulesBulkRoute = (router: IRouter) => { search: rule.id, searchFields: ['alertId'], }); - return transformValidateBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); + return transformValidateBulkError( + rule.id, + rule, + ruleActions, + ruleStatuses.saved_objects[0] + ); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 1e344d8ea7e31..dbb0a3bb3e1da 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -35,6 +35,7 @@ describe('patch_rules', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + clients.alertsClient.get.mockResolvedValue(getResult()); // existing rule clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule clients.alertsClient.update.mockResolvedValue(getResult()); // successful update clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // successful transform diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 620bcd8fc17b0..f553ccd2c6f81 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -21,6 +21,7 @@ import { import { getIdError } from './utils'; import { transformValidate } from './validate'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; export const patchRulesRoute = (router: IRouter) => { router.patch( @@ -74,12 +75,8 @@ export const patchRulesRoute = (router: IRouter) => { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); } - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; if (!actionsClient || !alertsClient) { @@ -89,7 +86,6 @@ export const patchRulesRoute = (router: IRouter) => { const rule = await patchRules({ actionsClient, alertsClient, - actions, description, enabled, falsePositives, @@ -115,7 +111,6 @@ export const patchRulesRoute = (router: IRouter) => { to, type, threat, - throttle, references, note, version, @@ -123,6 +118,15 @@ export const patchRulesRoute = (router: IRouter) => { machineLearningJobId, }); if (rule != null) { + const ruleActions = await updateRulesNotifications({ + ruleAlertId: rule.id, + alertsClient, + savedObjectsClient, + enabled: rule.enabled!, + actions, + throttle, + name: rule.name!, + }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -134,7 +138,11 @@ export const patchRulesRoute = (router: IRouter) => { searchFields: ['alertId'], }); - const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]); + const [validated, errors] = transformValidate( + rule, + ruleActions, + ruleStatuses.saved_objects[0] + ); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index e4117166ed4fa..77747448e94fd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -16,6 +16,7 @@ import { IRuleSavedAttributesSavedObjectAttributes, } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; export const readRulesRoute = (router: IRouter) => { router.get( @@ -32,10 +33,7 @@ export const readRulesRoute = (router: IRouter) => { const { id, rule_id: ruleId } = request.query; const siemResponse = buildSiemResponse(response); - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; try { @@ -49,6 +47,10 @@ export const readRulesRoute = (router: IRouter) => { ruleId, }); if (rule != null) { + const ruleActions = await getRuleActionsSavedObject({ + savedObjectsClient, + ruleAlertId: rule.id, + }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -59,7 +61,11 @@ export const readRulesRoute = (router: IRouter) => { search: rule.id, searchFields: ['alertId'], }); - const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]); + const [validated, errors] = transformValidate( + rule, + ruleActions, + ruleStatuses.saved_objects[0] + ); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 611b38ccbae8b..332a47d0c0fc2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -69,6 +69,13 @@ describe('update_rules_bulk', () => { expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getUpdateBulkRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + test('returns an error if update throws', async () => { clients.alertsClient.update.mockImplementation(() => { throw new Error('Test error'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 4abeb840c8c0a..9916972f41843 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -22,6 +22,7 @@ import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; export const updateRulesBulkRoute = (router: IRouter) => { router.put( @@ -37,15 +38,12 @@ export const updateRulesBulkRoute = (router: IRouter) => { async (context, request, response) => { const siemResponse = buildSiemResponse(response); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } @@ -93,11 +91,9 @@ export const updateRulesBulkRoute = (router: IRouter) => { const rule = await updateRules({ alertsClient, actionsClient, - actions, anomalyThreshold, description, enabled, - immutable: false, falsePositives, from, query, @@ -122,13 +118,21 @@ export const updateRulesBulkRoute = (router: IRouter) => { to, type, threat, - throttle, references, note, version, lists, }); if (rule != null) { + const ruleActions = await updateRulesNotifications({ + ruleAlertId: rule.id, + alertsClient, + savedObjectsClient, + enabled, + actions, + throttle, + name, + }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -139,7 +143,12 @@ export const updateRulesBulkRoute = (router: IRouter) => { search: rule.id, searchFields: ['alertId'], }); - return transformValidateBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); + return transformValidateBulkError( + rule.id, + rule, + ruleActions, + ruleStatuses.saved_objects[0] + ); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 717f2cc4a52fe..53c52153e84e6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -18,6 +18,8 @@ import { import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +jest.mock('../../rules/update_rules_notifications'); describe('update_rules', () => { let server: ReturnType; @@ -35,6 +37,7 @@ describe('update_rules', () => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + clients.alertsClient.get.mockResolvedValue(getResult()); // existing rule clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.alertsClient.update.mockResolvedValue(getResult()); // successful update clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // successful transform @@ -44,6 +47,12 @@ describe('update_rules', () => { describe('status codes with actionClient and alertClient', () => { test('returns 200 when updating a single rule with a valid actionClient and alertClient', async () => { + (updateRulesNotifications as jest.Mock).mockResolvedValue({ + id: 'id', + actions: [], + alertThrottle: null, + ruleThrottle: 'no_actions', + }); const response = await server.inject(getUpdateRequest(), context); expect(response.status).toEqual(200); }); @@ -67,6 +76,13 @@ describe('update_rules', () => { expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getUpdateRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + test('returns error when updating non-rule', async () => { clients.alertsClient.find.mockResolvedValue(nonRuleFindResult()); const response = await server.inject(getUpdateRequest(), context); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index f0d5f08c5f636..21dd2a4429cca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -21,7 +21,7 @@ import { getIdError } from './utils'; import { transformValidate } from './validate'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; -import { updateNotifications } from '../../notifications/update_notifications'; +import { updateRulesNotifications } from '../../rules/update_rules_notifications'; export const updateRulesRoute = (router: IRouter) => { router.put( @@ -74,15 +74,12 @@ export const updateRulesRoute = (router: IRouter) => { try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - if (!context.alerting || !context.actions) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); - const actionsClient = context.actions.getActionsClient(); + const alertsClient = context.alerting?.getAlertsClient(); + const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); - if (!actionsClient || !alertsClient) { + if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } @@ -90,13 +87,11 @@ export const updateRulesRoute = (router: IRouter) => { const rule = await updateRules({ alertsClient, actionsClient, - actions, anomalyThreshold, description, enabled, falsePositives, from, - immutable: false, query, language, machineLearningJobId, @@ -119,7 +114,6 @@ export const updateRulesRoute = (router: IRouter) => { to, type, threat, - throttle, references, note, version, @@ -127,15 +121,15 @@ export const updateRulesRoute = (router: IRouter) => { }); if (rule != null) { - await updateNotifications({ + const ruleActions = await updateRulesNotifications({ + ruleAlertId: rule.id, alertsClient, - actions, + savedObjectsClient, enabled, - ruleAlertId: rule.id, - interval: throttle, + actions, + throttle, name, }); - const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -146,7 +140,11 @@ export const updateRulesRoute = (router: IRouter) => { search: rule.id, searchFields: ['alertId'], }); - const [validated, errors] = transformValidate(rule, ruleStatuses.saved_objects[0]); + const [validated, errors] = transformValidate( + rule, + ruleActions, + ruleStatuses.saved_objects[0] + ); if (errors != null) { return siemResponse.error({ statusCode: 500, body: errors }); } else { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 3a047f91a0bcb..31a0f37fe81c9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -215,17 +215,20 @@ describe('utils', () => { describe('transformFindAlerts', () => { test('outputs empty data set when data set is empty correct', () => { - const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }); + const output = transformFindAlerts({ data: [], page: 1, perPage: 0, total: 0 }, []); expect(output).toEqual({ data: [], page: 1, perPage: 0, total: 0 }); }); test('outputs 200 if the data is of type siem alert', () => { - const output = transformFindAlerts({ - page: 1, - perPage: 0, - total: 0, - data: [getResult()], - }); + const output = transformFindAlerts( + { + page: 1, + perPage: 0, + total: 0, + data: [getResult()], + }, + [] + ); const expected = getOutputRuleAlertForRest(); expect(output).toEqual({ page: 1, @@ -237,12 +240,15 @@ describe('utils', () => { test('returns 500 if the data is not of type siem alert', () => { const unsafeCast = ([{ name: 'something else' }] as unknown) as SanitizedAlert[]; - const output = transformFindAlerts({ - data: unsafeCast, - page: 1, - perPage: 1, - total: 1, - }); + const output = transformFindAlerts( + { + data: unsafeCast, + page: 1, + perPage: 1, + total: 1, + }, + [] + ); expect(output).toBeNull(); }); }); @@ -364,14 +370,24 @@ describe('utils', () => { describe('transformOrBulkError', () => { test('outputs 200 if the data is of type siem alert', () => { - const output = transformOrBulkError('rule-1', getResult()); + const output = transformOrBulkError('rule-1', getResult(), { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + actions: [], + ruleThrottle: 'no_actions', + alertThrottle: null, + }); const expected = getOutputRuleAlertForRest(); expect(output).toEqual(expected); }); test('returns 500 if the data is not of type siem alert', () => { const unsafeCast = ({ name: 'something else' } as unknown) as PartialAlert; - const output = transformOrBulkError('rule-1', unsafeCast); + const output = transformOrBulkError('rule-1', unsafeCast, { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + actions: [], + ruleThrottle: 'no_actions', + alertThrottle: null, + }); const expected: BulkError = { rule_id: 'rule-1', error: { message: 'Internal error transforming', status_code: 500 }, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index ca0d133627210..4d13fa1b6ae50 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -19,12 +19,7 @@ import { isRuleStatusFindTypes, isRuleStatusSavedObjectType, } from '../../rules/types'; -import { - OutputRuleAlertRest, - ImportRuleAlertRest, - RuleAlertParamsRest, - RuleType, -} from '../../types'; +import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; import { createBulkErrorObject, BulkError, @@ -34,7 +29,8 @@ import { OutputError, } from '../utils'; import { hasListsFeature } from '../../feature_flags'; -import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; +// import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; +import { RuleActions } from '../../rule_actions/types'; type PromiseFromStreams = ImportRuleAlertRest | Error; @@ -105,10 +101,11 @@ export const transformTags = (tags: string[]): string[] => { // those on the export export const transformAlertToRule = ( alert: RuleAlertType, + ruleActions?: RuleActions | null, ruleStatus?: SavedObject ): Partial => { return pickBy((value: unknown) => value != null, { - actions: alert.actions.map(transformAlertToRuleAction), + actions: ruleActions?.actions ?? [], created_at: alert.createdAt.toISOString(), updated_at: alert.updatedAt.toISOString(), created_by: alert.createdBy, @@ -141,7 +138,7 @@ export const transformAlertToRule = ( to: alert.params.to, type: alert.params.type, threat: alert.params.threat, - throttle: alert.throttle, + throttle: ruleActions?.ruleThrottle || 'no_actions', note: alert.params.note, version: alert.params.version, status: ruleStatus?.attributes.status, @@ -172,6 +169,7 @@ export const transformAlertsToRules = ( export const transformFindAlerts = ( findResults: FindResult, + ruleActions: Array, ruleStatuses?: Array> ): { page: number; @@ -184,7 +182,7 @@ export const transformFindAlerts = ( page: findResults.page, perPage: findResults.perPage, total: findResults.total, - data: findResults.data.map(alert => transformAlertToRule(alert)), + data: findResults.data.map((alert, idx) => transformAlertToRule(alert, ruleActions[idx])), }; } else if (isAlertTypes(findResults.data) && isRuleStatusFindTypes(ruleStatuses)) { return { @@ -192,7 +190,7 @@ export const transformFindAlerts = ( perPage: findResults.perPage, total: findResults.total, data: findResults.data.map((alert, idx) => - transformAlertToRule(alert, ruleStatuses[idx].saved_objects[0]) + transformAlertToRule(alert, ruleActions[idx], ruleStatuses[idx].saved_objects[0]) ), }; } else { @@ -202,28 +200,31 @@ export const transformFindAlerts = ( export const transform = ( alert: PartialAlert, + ruleActions?: RuleActions | null, ruleStatus?: SavedObject ): Partial | null => { - if (!ruleStatus && isAlertType(alert)) { - return transformAlertToRule(alert); - } - if (isAlertType(alert) && isRuleStatusSavedObjectType(ruleStatus)) { - return transformAlertToRule(alert, ruleStatus); - } else { - return null; + if (isAlertType(alert)) { + return transformAlertToRule( + alert, + ruleActions, + isRuleStatusSavedObjectType(ruleStatus) ? ruleStatus : undefined + ); } + + return null; }; export const transformOrBulkError = ( ruleId: string, alert: PartialAlert, + ruleActions: RuleActions, ruleStatus?: unknown ): Partial | BulkError => { if (isAlertType(alert)) { if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { - return transformAlertToRule(alert, ruleStatus?.saved_objects[0] ?? ruleStatus); + return transformAlertToRule(alert, ruleActions, ruleStatus?.saved_objects[0] ?? ruleStatus); } else { - return transformAlertToRule(alert); + return transformAlertToRule(alert, ruleActions); } } else { return createBulkErrorObject({ @@ -300,5 +301,3 @@ export const getTupleDuplicateErrorsAndUniqueRules = ( return [Array.from(errors.values()), Array.from(rulesAcc.values())]; }; - -export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index 3727908ac62de..77e05796fbcbe 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -43,6 +43,7 @@ export const ruleOutput: RulesSchema = { tags: [], to: 'now', type: 'query', + throttle: 'no_actions', threat: [ { framework: 'MITRE ATT&CK', @@ -154,7 +155,7 @@ describe('validate', () => { describe('transformValidateFindAlerts', () => { test('it should do a validation correctly of a find alert', () => { const findResult: FindResult = { data: [getResult()], page: 1, perPage: 0, total: 0 }; - const [validated, errors] = transformValidateFindAlerts(findResult); + const [validated, errors] = transformValidateFindAlerts(findResult, []); expect(validated).toEqual({ data: [ruleOutput], page: 1, perPage: 0, total: 0 }); expect(errors).toEqual(null); }); @@ -162,7 +163,7 @@ describe('validate', () => { test('it should do an in-validation correctly of a partial alert', () => { const findResult: FindResult = { data: [getResult()], page: 1, perPage: 0, total: 0 }; delete findResult.page; - const [validated, errors] = transformValidateFindAlerts(findResult); + const [validated, errors] = transformValidateFindAlerts(findResult, []); expect(validated).toEqual(null); expect(errors).toEqual('Invalid value "undefined" supplied to "page"'); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts index e654da99fe67b..1f3d1ec856684 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.ts @@ -22,9 +22,11 @@ import { rulesSchema, RulesSchema } from '../schemas/response/rules_schema'; import { exactCheck } from '../schemas/response/exact_check'; import { transformFindAlerts, transform, transformAlertToRule } from './utils'; import { findRulesSchema } from '../schemas/response/find_rules_schema'; +import { RuleActions } from '../../rule_actions/types'; export const transformValidateFindAlerts = ( findResults: FindResult, + ruleActions: Array, ruleStatuses?: Array> ): [ { @@ -35,7 +37,7 @@ export const transformValidateFindAlerts = ( } | null, string | null ] => { - const transformed = transformFindAlerts(findResults, ruleStatuses); + const transformed = transformFindAlerts(findResults, ruleActions, ruleStatuses); if (transformed == null) { return [null, 'Internal error transforming']; } else { @@ -54,9 +56,10 @@ export const transformValidateFindAlerts = ( export const transformValidate = ( alert: PartialAlert, + ruleActions?: RuleActions | null, ruleStatus?: SavedObject ): [RulesSchema | null, string | null] => { - const transformed = transform(alert, ruleStatus); + const transformed = transform(alert, ruleActions, ruleStatus); if (transformed == null) { return [null, 'Internal error transforming']; } else { @@ -67,11 +70,16 @@ export const transformValidate = ( export const transformValidateBulkError = ( ruleId: string, alert: PartialAlert, + ruleActions?: RuleActions | null, ruleStatus?: unknown ): RulesSchema | BulkError => { if (isAlertType(alert)) { if (isRuleStatusFindType(ruleStatus) && ruleStatus?.saved_objects.length > 0) { - const transformed = transformAlertToRule(alert, ruleStatus?.saved_objects[0] ?? ruleStatus); + const transformed = transformAlertToRule( + alert, + ruleActions, + ruleStatus?.saved_objects[0] ?? ruleStatus + ); const [validated, errors] = validate(transformed, rulesSchema); if (errors != null || validated == null) { return createBulkErrorObject({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts index b5a01e3e5c6df..25e76f367037a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts @@ -8,6 +8,7 @@ import * as t from 'io-ts'; import { Either, left, fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; +import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; import { dependentRulesSchema, RequiredRulesSchema, @@ -47,7 +48,7 @@ export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixe }; export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { - if (typeAndTimelineOnly.type === 'machine_learning') { + if (isMlRule(typeAndTimelineOnly.type)) { return [ t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), t.exact( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index 612d08c09785a..72f3c89f660c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -49,6 +49,13 @@ describe('set signal status', () => { expect(response.status).toEqual(200); }); + it('returns 404 if siem client is unavailable', async () => { + const { siem, ...contextWithoutSiem } = context; + const response = await server.inject(getSetSignalStatusByQueryRequest(), contextWithoutSiem); + expect(response.status).toEqual(404); + expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); + }); + test('catches error if callAsCurrentUser throws error', async () => { clients.clusterClient.callAsCurrentUser.mockImplementation(async () => { throw new Error('Test error'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index c1cba641de3ef..2daf63c468593 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -24,9 +24,13 @@ export const setSignalsStatusRoute = (router: IRouter) => { async (context, request, response) => { const { signal_ids: signalIds, query, status } = request.body; const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem?.getSiemClient(); const siemResponse = buildSiemResponse(response); + if (!siemClient) { + return siemResponse.error({ statusCode: 404 }); + } + let queryObject; if (signalIds) { queryObject = { ids: { values: signalIds } }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index 77b62b058fa54..f05f494619b9c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -24,7 +24,7 @@ export const querySignalsRoute = (router: IRouter) => { async (context, request, response) => { const { query, aggs, _source, track_total_hits, size } = request.body; const clusterClient = context.core.elasticsearch.dataClient; - const siemClient = context.siem.getSiemClient(); + const siemClient = context.siem!.getSiemClient(); const siemResponse = buildSiemResponse(response); try { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts index e12bf50169c17..adabc62a9456f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/tags/read_tags_route.ts @@ -20,11 +20,7 @@ export const readTagsRoute = (router: IRouter) => { }, async (context, request, response) => { const siemResponse = buildSiemResponse(response); - - if (!context.alerting) { - return siemResponse.error({ statusCode: 404 }); - } - const alertsClient = context.alerting.getAlertsClient(); + const alertsClient = context.alerting?.getAlertsClient(); if (!alertsClient) { return siemResponse.error({ statusCode: 404 }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts index 90c7d4a07ddf8..8d7360bae8eb9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -16,9 +16,9 @@ import { } from '../../../../../../../../src/core/server'; import { ILicense } from '../../../../../../../plugins/licensing/server'; import { MINIMUM_ML_LICENSE } from '../../../../common/constants'; +import { RuleType } from '../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; import { BadRequestError } from '../errors/bad_request_error'; -import { RuleType } from '../types'; -import { isMlRule } from './rules/utils'; export interface OutputError { message: string; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..23c99b36cb4a7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/create_rule_actions_saved_object.ts @@ -0,0 +1,35 @@ +/* + * 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 { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { ruleActionsSavedObjectType } from './saved_object_mappings'; +import { IRuleActionsAttributesSavedObjectAttributes } from './types'; +import { getThrottleOptions, getRuleActionsFromSavedObject } from './utils'; + +interface CreateRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; + actions: RuleAlertAction[] | undefined; + throttle: string | undefined; +} + +export const createRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, + actions = [], + throttle, +}: CreateRuleActionsSavedObject) => { + const ruleActionsSavedObject = await savedObjectsClient.create< + IRuleActionsAttributesSavedObjectAttributes + >(ruleActionsSavedObjectType, { + ruleAlertId, + actions, + ...getThrottleOptions(throttle), + }); + + return getRuleActionsFromSavedObject(ruleActionsSavedObject); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..4e8781dd45692 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/delete_rule_actions_saved_object.ts @@ -0,0 +1,25 @@ +/* + * 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 { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { ruleActionsSavedObjectType } from './saved_object_mappings'; +import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; + +interface DeleteRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; +} + +export const deleteRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, +}: DeleteRuleActionsSavedObject) => { + const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); + + if (!ruleActions) return null; + + return savedObjectsClient.delete(ruleActionsSavedObjectType, ruleActions.id); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..3ae9090526d69 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/get_rule_actions_saved_object.ts @@ -0,0 +1,35 @@ +/* + * 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 { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { ruleActionsSavedObjectType } from './saved_object_mappings'; +import { IRuleActionsAttributesSavedObjectAttributes } from './types'; +import { getRuleActionsFromSavedObject } from './utils'; + +interface GetRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; +} + +export const getRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, +}: GetRuleActionsSavedObject) => { + const { saved_objects } = await savedObjectsClient.find< + IRuleActionsAttributesSavedObjectAttributes + >({ + type: ruleActionsSavedObjectType, + perPage: 1, + search: `${ruleAlertId}`, + searchFields: ['ruleAlertId'], + }); + + if (!saved_objects[0]) { + return null; + } + + return getRuleActionsFromSavedObject(saved_objects[0]); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts new file mode 100644 index 0000000000000..f54f43c41ef6e --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts @@ -0,0 +1,40 @@ +/* + * 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 ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions'; + +export const ruleActionsSavedObjectMappings = { + [ruleActionsSavedObjectType]: { + properties: { + alertThrottle: { + type: 'keyword', + }, + ruleAlertId: { + type: 'keyword', + }, + ruleThrottle: { + type: 'keyword', + }, + actions: { + properties: { + group: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + action_type_id: { + type: 'keyword', + }, + params: { + dynamic: true, + properties: {}, + }, + }, + }, + }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/types.ts new file mode 100644 index 0000000000000..525eb74d18fb2 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/types.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 { get } from 'lodash/fp'; +import { SavedObject, SavedObjectAttributes, SavedObjectsFindResponse } from 'kibana/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IRuleActionsAttributes extends Record { + ruleAlertId: string; + actions: RuleAlertAction[]; + ruleThrottle: string; + alertThrottle: string | null; +} + +export interface RuleActions { + id: string; + actions: RuleAlertAction[]; + ruleThrottle: string; + alertThrottle: string | null; +} + +export interface IRuleActionsAttributesSavedObjectAttributes + extends IRuleActionsAttributes, + SavedObjectAttributes {} + +export interface RuleActionsResponse { + [key: string]: { + actions: IRuleActionsAttributes | null | undefined; + }; +} + +export interface IRuleActionsSavedObject { + type: string; + id: string; + attributes: Array>; + references: unknown[]; + updated_at: string; + version: string; +} + +export interface IRuleActionsFindType { + page: number; + per_page: number; + total: number; + saved_objects: IRuleActionsSavedObject[]; +} + +export const isRuleActionsSavedObjectType = ( + obj: unknown +): obj is SavedObject => { + return get('attributes', obj) != null; +}; + +export const isRuleActionsFindType = ( + obj: unknown +): obj is SavedObjectsFindResponse => { + return get('saved_objects', obj) != null; +}; + +export const isRuleActionsFindTypes = ( + obj: unknown[] | undefined +): obj is Array> => { + return obj ? obj.every(ruleStatus => isRuleActionsFindType(ruleStatus)) : false; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..3856f75255262 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_or_create_rule_actions_saved_object.ts @@ -0,0 +1,39 @@ +/* + * 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 { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; +import { createRuleActionsSavedObject } from './create_rule_actions_saved_object'; +import { updateRuleActionsSavedObject } from './update_rule_actions_saved_object'; +import { RuleActions } from './types'; + +interface UpdateOrCreateRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; + actions: RuleAlertAction[] | undefined; + throttle: string | undefined; +} + +export const updateOrCreateRuleActionsSavedObject = async ({ + savedObjectsClient, + ruleAlertId, + actions, + throttle, +}: UpdateOrCreateRuleActionsSavedObject): Promise => { + const currentRuleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); + + if (currentRuleActions) { + return updateRuleActionsSavedObject({ + ruleAlertId, + savedObjectsClient, + actions, + throttle, + }) as Promise; + } + + return createRuleActionsSavedObject({ ruleAlertId, savedObjectsClient, actions, throttle }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts new file mode 100644 index 0000000000000..56bce3c8b67a3 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/update_rule_actions_saved_object.ts @@ -0,0 +1,56 @@ +/* + * 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 { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { ruleActionsSavedObjectType } from './saved_object_mappings'; +import { getRuleActionsSavedObject } from './get_rule_actions_saved_object'; +import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { getThrottleOptions } from './utils'; +import { IRuleActionsAttributesSavedObjectAttributes } from './types'; + +interface DeleteRuleActionsSavedObject { + ruleAlertId: string; + savedObjectsClient: AlertServices['savedObjectsClient']; + actions: RuleAlertAction[] | undefined; + throttle: string | undefined; +} + +export const updateRuleActionsSavedObject = async ({ + ruleAlertId, + savedObjectsClient, + actions, + throttle, +}: DeleteRuleActionsSavedObject) => { + const ruleActions = await getRuleActionsSavedObject({ ruleAlertId, savedObjectsClient }); + + if (!ruleActions) return null; + + const throttleOptions = throttle + ? getThrottleOptions(throttle) + : { + ruleThrottle: ruleActions.ruleThrottle, + alertThrottle: ruleActions.alertThrottle, + }; + + const options = { + actions: actions ?? ruleActions.actions, + ...throttleOptions, + }; + + await savedObjectsClient.update( + ruleActionsSavedObjectType, + ruleActions.id, + { + ruleAlertId, + ...options, + } + ); + + return { + id: ruleActions.id, + ...options, + }; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts new file mode 100644 index 0000000000000..3c297ed848555 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rule_actions/utils.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsUpdateResponse } from 'kibana/server'; +import { IRuleActionsAttributesSavedObjectAttributes } from './types'; + +export const getThrottleOptions = (throttle = 'no_actions') => ({ + ruleThrottle: throttle, + alertThrottle: ['no_actions', 'rule'].includes(throttle) ? null : throttle, +}); + +export const getRuleActionsFromSavedObject = ( + savedObject: SavedObjectsUpdateResponse +) => ({ + id: savedObject.id, + actions: savedObject.attributes.actions || [], + alertThrottle: savedObject.attributes.alertThrottle || null, + ruleThrottle: savedObject.attributes.ruleThrottle || 'no_actions', +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts index 14b8ffdfdacec..4c8d0f51f251b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.test.ts @@ -28,12 +28,10 @@ describe('createRules', () => { await createRules({ alertsClient, actionsClient, - actions: [], ...params, ruleId: 'new-rule-id', enabled: true, interval: '', - throttle: null, name: '', tags: [], }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index a45b28ba3e105..bebf4f350483b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -6,15 +6,12 @@ import { Alert } from '../../../../../../../plugins/alerting/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; -import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; import { hasListsFeature } from '../feature_flags'; export const createRules = async ({ alertsClient, - actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... - actions, anomalyThreshold, description, enabled, @@ -39,7 +36,6 @@ export const createRules = async ({ severity, tags, threat, - throttle, to, type, references, @@ -85,8 +81,8 @@ export const createRules = async ({ }, schedule: { interval }, enabled, - actions: actions?.map(transformRuleToAlertAction), - throttle, + actions: [], + throttle: null, }, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index 20ddcdc3f5362..ca6fb15e1fad9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -76,6 +76,7 @@ describe('getExportAll', () => { ], }, ], + throttle: 'no_actions', note: '# Investigative notes', version: 1, lists: [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index e6d4c68d7108d..175c906f7996c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -84,6 +84,7 @@ describe('get_export_by_object_ids', () => { ], }, ], + throttle: 'no_actions', note: '# Investigative notes', version: 1, lists: [ @@ -205,6 +206,7 @@ describe('get_export_by_object_ids', () => { ], }, ], + throttle: 'no_actions', note: '# Investigative notes', version: 1, lists: [ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 801f3d949ed78..bcbe460fb6a66 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -18,7 +18,6 @@ export const installPrepackagedRules = ( ): Array> => rules.reduce>>((acc, rule) => { const { - actions, anomaly_threshold: anomalyThreshold, description, enabled, @@ -44,7 +43,6 @@ export const installPrepackagedRules = ( to, type, threat, - throttle, references, note, version, @@ -55,7 +53,6 @@ export const installPrepackagedRules = ( createRules({ alertsClient, actionsClient, - actions, anomalyThreshold, description, enabled, @@ -82,7 +79,6 @@ export const installPrepackagedRules = ( to, type, threat, - throttle, references, note, version, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts index cd18bee6f606f..3108fc5f3b718 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts @@ -28,12 +28,10 @@ describe('patchRules', () => { await patchRules({ alertsClient, actionsClient, - actions: [], savedObjectsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ...rule.params, enabled: false, - throttle: null, interval: '', name: '', tags: [], @@ -56,12 +54,10 @@ describe('patchRules', () => { await patchRules({ alertsClient, actionsClient, - actions: [], savedObjectsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ...rule.params, enabled: true, - throttle: null, interval: '', name: '', tags: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 5394af526c917..d7655a15499eb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -6,7 +6,6 @@ import { defaults } from 'lodash/fp'; import { PartialAlert } from '../../../../../../../plugins/alerting/server'; -import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { readRules } from './read_rules'; import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; import { addTags } from './add_tags'; @@ -16,7 +15,6 @@ import { calculateVersion, calculateName, calculateInterval } from './utils'; export const patchRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types - actions, savedObjectsClient, description, falsePositives, @@ -41,7 +39,6 @@ export const patchRules = async ({ severity, tags, threat, - throttle, to, type, references, @@ -57,7 +54,6 @@ export const patchRules = async ({ } const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { - actions, description, falsePositives, query, @@ -77,7 +73,6 @@ export const patchRules = async ({ severity, tags, threat, - throttle, to, type, references, @@ -125,12 +120,12 @@ export const patchRules = async ({ id: rule.id, data: { tags: addTags(tags ?? rule.tags, rule.params.ruleId, immutable ?? rule.params.immutable), - throttle: throttle !== undefined ? throttle : rule.throttle, + throttle: rule.throttle, name: calculateName({ updatedName: name, originalName: rule.name }), schedule: { interval: calculateInterval(interval, rule.schedule.interval), }, - actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, + actions: rule.actions, params: nextParams, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index ada11174c5340..38b1097a845f8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -16,7 +16,6 @@ import { import { AlertsClient, PartialAlert } from '../../../../../../../plugins/alerting/server'; import { Alert } from '../../../../../../../plugins/alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; -import { LegacyRequest } from '../../../types'; import { ActionsClient } from '../../../../../../../plugins/actions/server'; import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; @@ -39,14 +38,6 @@ export interface FindParamsRest { filter: string; } -export interface PatchRulesRequest extends LegacyRequest { - payload: PatchRuleAlertParamsRest; -} - -export interface UpdateRulesRequest extends LegacyRequest { - payload: UpdateRuleAlertParamsRest; -} - export interface RuleAlertType extends Alert { params: RuleTypeParams; } @@ -93,7 +84,7 @@ export interface IRuleStatusFindType { saved_objects: IRuleStatusSavedObject[]; } -export type RuleStatusString = 'succeeded' | 'failed' | 'going to run' | 'executing'; +export type RuleStatusString = 'succeeded' | 'failed' | 'going to run'; export interface HapiReadableStream extends Readable { hapi: { @@ -151,12 +142,12 @@ export interface Clients { actionsClient: ActionsClient; } -export type PatchRuleParams = Partial & { +export type PatchRuleParams = Partial> & { id: string | undefined | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; -export type UpdateRuleParams = RuleAlertParams & { +export type UpdateRuleParams = Omit & { id: string | undefined | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; @@ -166,7 +157,9 @@ export type DeleteRuleParams = Clients & { ruleId: string | undefined | null; }; -export type CreateRuleParams = Omit & { ruleId: string } & Clients; +export type CreateRuleParams = Omit & { + ruleId: string; +} & Clients; export interface ReadRuleParams { alertsClient: AlertsClient; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts new file mode 100644 index 0000000000000..7a3f233475117 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -0,0 +1,62 @@ +/* + * 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 { savedObjectsClientMock } from '../../../../../../../../src/core/server/mocks'; +import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { actionsClientMock } from '../../../../../../../plugins/actions/server/mocks'; +import { mockPrepackagedRule } from '../routes/__mocks__/request_responses'; +import { updatePrepackagedRules } from './update_prepacked_rules'; +import { patchRules } from './patch_rules'; +jest.mock('./patch_rules'); + +describe('updatePrepackagedRules', () => { + let actionsClient: ReturnType; + let alertsClient: ReturnType; + let savedObjectsClient: ReturnType; + + beforeEach(() => { + actionsClient = actionsClientMock.create(); + alertsClient = alertsClientMock.create(); + savedObjectsClient = savedObjectsClientMock.create(); + }); + + it('should omit actions and enabled when calling patchRules', async () => { + const actions = [ + { + group: 'group', + id: 'id', + action_type_id: 'action_type_id', + params: {}, + }, + ]; + const outputIndex = 'outputIndex'; + const prepackagedRule = mockPrepackagedRule(); + + await updatePrepackagedRules( + alertsClient, + actionsClient, + savedObjectsClient, + [{ ...prepackagedRule, actions }], + outputIndex + ); + + expect(patchRules).toHaveBeenCalledWith( + expect.objectContaining({ + ruleId: 'rule-1', + }) + ); + expect(patchRules).not.toHaveBeenCalledWith( + expect.objectContaining({ + enabled: true, + }) + ); + expect(patchRules).not.toHaveBeenCalledWith( + expect.objectContaining({ + actions, + }) + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index cc67622176a04..7eb0d8d1399be 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -19,7 +19,6 @@ export const updatePrepackagedRules = async ( ): Promise => { await rules.forEach(async rule => { const { - actions, description, false_positives: falsePositives, from, @@ -40,7 +39,6 @@ export const updatePrepackagedRules = async ( to, type, threat, - throttle, references, version, note, @@ -51,7 +49,6 @@ export const updatePrepackagedRules = async ( return patchRules({ alertsClient, actionsClient, - actions, description, falsePositives, from, @@ -75,7 +72,6 @@ export const updatePrepackagedRules = async ( to, type, threat, - throttle, references, version, note, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.ts new file mode 100644 index 0000000000000..ac10143c1d8d0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rule_actions.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 { AlertsClient, AlertServices } from '../../../../../../../plugins/alerting/server'; +import { getRuleActionsSavedObject } from '../rule_actions/get_rule_actions_saved_object'; +import { readRules } from './read_rules'; +import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; + +interface UpdateRuleActions { + alertsClient: AlertsClient; + savedObjectsClient: AlertServices['savedObjectsClient']; + ruleAlertId: string; +} + +export const updateRuleActions = async ({ + alertsClient, + savedObjectsClient, + ruleAlertId, +}: UpdateRuleActions) => { + const rule = await readRules({ alertsClient, id: ruleAlertId }); + if (rule == null) { + return null; + } + + const ruleActions = await getRuleActionsSavedObject({ + savedObjectsClient, + ruleAlertId, + }); + + if (!ruleActions) { + return null; + } + + return alertsClient.update({ + id: ruleAlertId, + data: { + actions: !ruleActions.alertThrottle + ? ruleActions.actions.map(transformRuleToAlertAction) + : [], + throttle: null, + name: rule.name, + tags: rule.tags, + schedule: rule.schedule, + params: rule.params, + }, + }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts index af00816abfc3d..ca299db6ace50 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.test.ts @@ -28,12 +28,10 @@ describe('updateRules', () => { await updateRules({ alertsClient, actionsClient, - actions: [], savedObjectsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ...rule.params, enabled: false, - throttle: null, interval: '', name: '', tags: [], @@ -56,12 +54,10 @@ describe('updateRules', () => { await updateRules({ alertsClient, actionsClient, - actions: [], savedObjectsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ...rule.params, enabled: true, - throttle: null, interval: '', name: '', tags: [], @@ -86,12 +82,10 @@ describe('updateRules', () => { await updateRules({ alertsClient, actionsClient, - actions: [], savedObjectsClient, id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', ...params, enabled: true, - throttle: null, interval: '', name: '', tags: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 72cbc959c0105..0e70e05f4de78 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -5,7 +5,6 @@ */ import { PartialAlert } from '../../../../../../../plugins/alerting/server'; -import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { readRules } from './read_rules'; import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; import { addTags } from './add_tags'; @@ -16,7 +15,6 @@ import { hasListsFeature } from '../feature_flags'; export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types - actions, savedObjectsClient, description, falsePositives, @@ -30,7 +28,6 @@ export const updateRules = async ({ meta, filters, from, - immutable, id, ruleId, index, @@ -41,7 +38,6 @@ export const updateRules = async ({ severity, tags, threat, - throttle, to, type, references, @@ -57,7 +53,6 @@ export const updateRules = async ({ } const calculatedVersion = calculateVersion(rule.params.immutable, rule.params.version, { - actions, description, falsePositives, query, @@ -77,7 +72,6 @@ export const updateRules = async ({ severity, tags, threat, - throttle, to, type, references, @@ -93,17 +87,17 @@ export const updateRules = async ({ const update = await alertsClient.update({ id: rule.id, data: { - tags: addTags(tags, rule.params.ruleId, immutable), + tags: addTags(tags, rule.params.ruleId, rule.params.immutable), name, schedule: { interval }, - actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, - throttle: throttle !== undefined ? throttle : rule.throttle, + actions: rule.actions, + throttle: rule.throttle, params: { description, ruleId: rule.params.ruleId, falsePositives, from, - immutable, + immutable: rule.params.immutable, query, language, outputIndex, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts new file mode 100644 index 0000000000000..f70c591243a76 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules_notifications.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleAlertAction } from '../../../../common/detection_engine/types'; +import { AlertsClient, AlertServices } from '../../../../../../../plugins/alerting/server'; +import { updateOrCreateRuleActionsSavedObject } from '../rule_actions/update_or_create_rule_actions_saved_object'; +import { updateNotifications } from '../notifications/update_notifications'; +import { updateRuleActions } from './update_rule_actions'; + +interface UpdateRulesNotifications { + alertsClient: AlertsClient; + savedObjectsClient: AlertServices['savedObjectsClient']; + ruleAlertId: string; + actions: RuleAlertAction[] | undefined; + throttle: string | undefined; + enabled: boolean; + name: string; +} + +export const updateRulesNotifications = async ({ + alertsClient, + savedObjectsClient, + ruleAlertId, + actions, + enabled, + name, + throttle, +}: UpdateRulesNotifications) => { + const ruleActions = await updateOrCreateRuleActionsSavedObject({ + savedObjectsClient, + ruleAlertId, + actions, + throttle, + }); + + await updateRuleActions({ + alertsClient, + savedObjectsClient, + ruleAlertId, + }); + + await updateNotifications({ + alertsClient, + ruleAlertId, + enabled, + name, + actions: ruleActions.actions, + interval: ruleActions?.alertThrottle, + }); + + return ruleActions; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 31b922e0067cd..6d7d7e93d7e6e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -5,9 +5,15 @@ */ import { SignalSourceHit, SignalSearchResponse } from '../types'; -import { Logger } from 'kibana/server'; +import { + Logger, + SavedObject, + SavedObjectsFindResponse, +} from '../../../../../../../../../src/core/server'; import { loggingServiceMock } from '../../../../../../../../../src/core/server/mocks'; import { RuleTypeParams, OutputRuleAlertRest } from '../../types'; +import { IRuleStatusAttributes } from '../../rules/types'; +import { ruleStatusSavedObjectType } from '../../../../saved_objects'; export const sampleRuleAlertParams = ( maxSignals?: number | undefined, @@ -373,4 +379,34 @@ export const sampleRule = (): Partial => { }; }; +export const exampleRuleStatus: () => SavedObject = () => ({ + type: ruleStatusSavedObjectType, + id: '042e6d90-7069-11ea-af8b-0f8ae4fa817e', + attributes: { + alertId: 'f4b8e31d-cf93-4bde-a265-298bde885cd7', + statusDate: '2020-03-27T22:55:59.517Z', + status: 'succeeded', + lastFailureAt: null, + lastSuccessAt: '2020-03-27T22:55:59.517Z', + lastFailureMessage: null, + lastSuccessMessage: 'succeeded', + gap: null, + bulkCreateTimeDurations: [], + searchAfterTimeDurations: [], + lastLookBackDate: null, + }, + references: [], + updated_at: '2020-03-27T22:55:59.577Z', + version: 'WzgyMiwxXQ==', +}); + +export const exampleFindRuleStatusResponse: ( + mockStatuses: Array> +) => SavedObjectsFindResponse = (mockStatuses = [exampleRuleStatus()]) => ({ + total: 1, + per_page: 6, + page: 1, + saved_objects: mockStatuses, +}); + export const mockLogger: Logger = loggingServiceMock.createLogger(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_client.mock.ts new file mode 100644 index 0000000000000..7528dc8b656ec --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/rule_status_saved_objects_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 { RuleStatusSavedObjectsClient } from '../rule_status_saved_objects_client'; + +const createMockRuleStatusSavedObjectsClient = (): jest.Mocked => ({ + find: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), +}); + +export const ruleStatusSavedObjectsClientMock = { + create: createMockRuleStatusSavedObjectsClient, +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index c86696d6ec5eb..f2c2b99bdac8c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -33,7 +33,7 @@ describe('buildBulkBody', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -80,6 +80,7 @@ describe('buildBulkBody', () => { references: ['http://google.com'], severity: 'high', tags: ['some fake tag 1', 'some fake tag 2'], + throttle: 'no_actions', type: 'query', to: 'now', note: '', @@ -143,7 +144,7 @@ describe('buildBulkBody', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -208,6 +209,7 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, + throttle: 'no_actions', lists: [ { field: 'source.ip', @@ -261,7 +263,7 @@ describe('buildBulkBody', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -325,6 +327,7 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, + throttle: 'no_actions', lists: [ { field: 'source.ip', @@ -376,7 +379,7 @@ describe('buildBulkBody', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); // Timestamp will potentially always be different so remove it for the test delete fakeSignalSourceHit['@timestamp']; @@ -435,6 +438,7 @@ describe('buildBulkBody', () => { version: 1, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, created_at: fakeSignalSourceHit.signal.rule?.created_at, + throttle: 'no_actions', lists: [ { field: 'source.ip', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts index f485769dffabc..75c4d75cedf1d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.ts @@ -24,7 +24,7 @@ interface BuildBulkBodyParams { interval: string; enabled: boolean; tags: string[]; - throttle: string | null; + throttle: string; } // format search_after result for signals index. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts index 37d7ed8a51082..e360ceaf02f4d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -38,7 +38,7 @@ describe('buildRule', () => { updatedBy: 'elastic', interval: 'some interval', tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); const expected: Partial = { actions: [], @@ -67,6 +67,7 @@ describe('buildRule', () => { updated_by: 'elastic', updated_at: rule.updated_at, created_at: rule.created_at, + throttle: 'no_actions', filters: [ { query: 'host.name: Rebecca', @@ -124,7 +125,7 @@ describe('buildRule', () => { updatedBy: 'elastic', interval: 'some interval', tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); const expected: Partial = { actions: [], @@ -154,6 +155,7 @@ describe('buildRule', () => { version: 1, updated_at: rule.updated_at, created_at: rule.created_at, + throttle: 'no_actions', lists: [ { field: 'source.ip', @@ -199,7 +201,7 @@ describe('buildRule', () => { updatedBy: 'elastic', interval: 'some interval', tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); const expected: Partial = { actions: [], @@ -229,80 +231,7 @@ describe('buildRule', () => { version: 1, updated_at: rule.updated_at, created_at: rule.created_at, - lists: [ - { - field: 'source.ip', - boolean_operator: 'and', - values: [ - { - name: '127.0.0.1', - type: 'value', - }, - ], - }, - { - field: 'host.name', - boolean_operator: 'and not', - values: [ - { - name: 'rock01', - type: 'value', - }, - { - name: 'mothra', - type: 'value', - }, - ], - }, - ], - }; - expect(rule).toEqual(expected); - }); - - test('it omits a null value such as if "throttle" is undefined if is present', () => { - const ruleParams = sampleRuleAlertParams(); - const rule = buildRule({ - actions: [], - ruleParams, - name: 'some-name', - id: sampleRuleGuid, - enabled: true, - createdAt: '2020-01-28T15:58:34.810Z', - updatedAt: '2020-01-28T15:59:14.004Z', - createdBy: 'elastic', - updatedBy: 'elastic', - interval: 'some interval', - tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, - }); - const expected: Partial = { - actions: [], - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: 'some interval', - language: 'kuery', - max_signals: 10000, - name: 'some-name', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://google.com'], - risk_score: 50, - rule_id: 'rule-1', - severity: 'high', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - note: '', - updated_by: 'elastic', - version: 1, - updated_at: rule.updated_at, - created_at: rule.created_at, + throttle: 'no_actions', lists: [ { field: 'source.ip', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index 1de80ca0b7eaf..9c375d7d45d5e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -20,7 +20,7 @@ interface BuildRuleParams { updatedBy: string; interval: string; tags: string[]; - throttle: string | null; + throttle: string; } export const buildRule = ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts index c1b61ef24462d..355041d9efbdb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -30,7 +30,7 @@ interface BulkCreateMlSignalsParams { interval: string; enabled: boolean; tags: string[]; - throttle: string | null; + throttle: string; } interface EcsAnomaly extends Anomaly { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts deleted file mode 100644 index 1fee8bcd6c2f0..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_current_status_saved_object.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObjectsFindResponse, SavedObject } from 'src/core/server'; - -import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; -import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; - -interface CurrentStatusSavedObjectParams { - alertId: string; - services: AlertServices; - ruleStatusSavedObjects: SavedObjectsFindResponse; -} - -export const getCurrentStatusSavedObject = async ({ - alertId, - services, - ruleStatusSavedObjects, -}: CurrentStatusSavedObjectParams): Promise> => { - if (ruleStatusSavedObjects.saved_objects.length === 0) { - // create - const date = new Date().toISOString(); - const currentStatusSavedObject = await services.savedObjectsClient.create< - IRuleSavedAttributesSavedObjectAttributes - >(ruleStatusSavedObjectType, { - alertId, // do a search for this id. - statusDate: date, - status: 'going to run', - lastFailureAt: null, - lastSuccessAt: null, - lastFailureMessage: null, - lastSuccessMessage: null, - gap: null, - bulkCreateTimeDurations: [], - searchAfterTimeDurations: [], - lastLookBackDate: null, - }); - return currentStatusSavedObject; - } else { - // update 0th to executing. - const currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0]; - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'going to run'; - currentStatusSavedObject.attributes.statusDate = sDate; - await services.savedObjectsClient.update( - ruleStatusSavedObjectType, - currentStatusSavedObject.id, - { - ...currentStatusSavedObject.attributes, - } - ); - return currentStatusSavedObject; - } -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts new file mode 100644 index 0000000000000..913efbe04aa16 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_or_create_rule_statuses.ts @@ -0,0 +1,52 @@ +/* + * 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 { SavedObject } from 'src/core/server'; + +import { IRuleStatusAttributes } from '../rules/types'; +import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; +import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; + +interface RuleStatusParams { + alertId: string; + ruleStatusClient: RuleStatusSavedObjectsClient; +} + +export const createNewRuleStatus = async ({ + alertId, + ruleStatusClient, +}: RuleStatusParams): Promise> => { + const now = new Date().toISOString(); + return ruleStatusClient.create({ + alertId, + statusDate: now, + status: 'going to run', + lastFailureAt: null, + lastSuccessAt: null, + lastFailureMessage: null, + lastSuccessMessage: null, + gap: null, + bulkCreateTimeDurations: [], + searchAfterTimeDurations: [], + lastLookBackDate: null, + }); +}; + +export const getOrCreateRuleStatuses = async ({ + alertId, + ruleStatusClient, +}: RuleStatusParams): Promise>> => { + const ruleStatuses = await getRuleStatusSavedObjects({ + alertId, + ruleStatusClient, + }); + if (ruleStatuses.saved_objects.length > 0) { + return ruleStatuses.saved_objects; + } + const newStatus = await createNewRuleStatus({ alertId, ruleStatusClient }); + + return [newStatus]; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts index 5a59d0413cfb9..828b4ea41096e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_rule_status_saved_objects.ts @@ -5,24 +5,21 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; -import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; +import { IRuleStatusAttributes } from '../rules/types'; +import { MAX_RULE_STATUSES } from './rule_status_service'; +import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; interface GetRuleStatusSavedObject { alertId: string; - services: AlertServices; + ruleStatusClient: RuleStatusSavedObjectsClient; } export const getRuleStatusSavedObjects = async ({ alertId, - services, -}: GetRuleStatusSavedObject): Promise> => { - return services.savedObjectsClient.find({ - type: ruleStatusSavedObjectType, - perPage: 6, // 0th element is current status, 1-5 is last 5 failures. + ruleStatusClient, +}: GetRuleStatusSavedObject): Promise> => { + return ruleStatusClient.find({ + perPage: MAX_RULE_STATUSES, sortField: 'statusDate', sortOrder: 'desc', search: `${alertId}`, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts new file mode 100644 index 0000000000000..8e4b5ce3c9924 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.test.ts @@ -0,0 +1,61 @@ +/* + * 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 { BuildRuleMessageFactoryParams, buildRuleMessageFactory } from './rule_messages'; + +describe('buildRuleMessageFactory', () => { + let factoryParams: BuildRuleMessageFactoryParams; + beforeEach(() => { + factoryParams = { + name: 'name', + id: 'id', + ruleId: 'ruleId', + index: 'index', + }; + }); + + it('appends rule attributes to the provided message', () => { + const buildMessage = buildRuleMessageFactory(factoryParams); + + const message = buildMessage('my message'); + expect(message).toEqual(expect.stringContaining('my message')); + expect(message).toEqual(expect.stringContaining('name: "name"')); + expect(message).toEqual(expect.stringContaining('id: "id"')); + expect(message).toEqual(expect.stringContaining('rule id: "ruleId"')); + expect(message).toEqual(expect.stringContaining('signals index: "index"')); + }); + + it('joins message parts with newlines', () => { + const buildMessage = buildRuleMessageFactory(factoryParams); + + const message = buildMessage('my message'); + const messageParts = message.split('\n'); + expect(messageParts).toContain('my message'); + expect(messageParts).toContain('name: "name"'); + expect(messageParts).toContain('id: "id"'); + expect(messageParts).toContain('rule id: "ruleId"'); + expect(messageParts).toContain('signals index: "index"'); + }); + + it('joins multiple arguments with newlines', () => { + const buildMessage = buildRuleMessageFactory(factoryParams); + + const message = buildMessage('my message', 'here is more'); + const messageParts = message.split('\n'); + expect(messageParts).toContain('my message'); + expect(messageParts).toContain('here is more'); + }); + + it('defaults the rule ID if not provided ', () => { + const buildMessage = buildRuleMessageFactory({ + ...factoryParams, + ruleId: undefined, + }); + + const message = buildMessage('my message', 'here is more'); + expect(message).toEqual(expect.stringContaining('rule id: "(unknown rule id)"')); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts new file mode 100644 index 0000000000000..d5f9d332bbcdd --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_messages.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type BuildRuleMessage = (...messages: string[]) => string; +export interface BuildRuleMessageFactoryParams { + name: string; + id: string; + ruleId: string | null | undefined; + index: string; +} + +export const buildRuleMessageFactory = ({ + id, + ruleId, + index, + name, +}: BuildRuleMessageFactoryParams): BuildRuleMessage => (...messages) => + [ + ...messages, + `name: "${name}"`, + `id: "${id}"`, + `rule id: "${ruleId ?? '(unknown rule id)'}"`, + `signals index: "${index}"`, + ].join('\n'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts new file mode 100644 index 0000000000000..11cbf67304409 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_saved_objects_client.ts @@ -0,0 +1,37 @@ +/* + * 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 { + SavedObjectsClientContract, + SavedObject, + SavedObjectsUpdateResponse, + SavedObjectsFindOptions, + SavedObjectsFindResponse, +} from '../../../../../../../../src/core/server'; +import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; +import { IRuleStatusAttributes } from '../rules/types'; + +export interface RuleStatusSavedObjectsClient { + find: ( + options?: Omit + ) => Promise>; + create: (attributes: IRuleStatusAttributes) => Promise>; + update: ( + id: string, + attributes: Partial + ) => Promise>; + delete: (id: string) => Promise<{}>; +} + +export const ruleStatusSavedObjectsClientFactory = ( + savedObjectsClient: SavedObjectsClientContract +): RuleStatusSavedObjectsClient => ({ + find: options => + savedObjectsClient.find({ ...options, type: ruleStatusSavedObjectType }), + create: attributes => savedObjectsClient.create(ruleStatusSavedObjectType, attributes), + update: (id, attributes) => savedObjectsClient.update(ruleStatusSavedObjectType, id, attributes), + delete: id => savedObjectsClient.delete(ruleStatusSavedObjectType, id), +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.test.ts new file mode 100644 index 0000000000000..ea9534710d418 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.test.ts @@ -0,0 +1,195 @@ +/* + * 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 { ruleStatusSavedObjectsClientMock } from './__mocks__/rule_status_saved_objects_client.mock'; +import { + buildRuleStatusAttributes, + RuleStatusService, + ruleStatusServiceFactory, + MAX_RULE_STATUSES, +} from './rule_status_service'; +import { exampleRuleStatus, exampleFindRuleStatusResponse } from './__mocks__/es_results'; + +const expectIsoDateString = expect.stringMatching(/Z$/); +const buildStatuses = (n: number) => + Array(n) + .fill(exampleRuleStatus()) + .map((status, index) => ({ + ...status, + id: `status-index-${index}`, + })); + +describe('buildRuleStatusAttributes', () => { + it('generates a new date on each call', async () => { + const { statusDate } = buildRuleStatusAttributes('going to run'); + await new Promise(resolve => setTimeout(resolve, 10)); // ensure time has passed + const { statusDate: statusDate2 } = buildRuleStatusAttributes('going to run'); + + expect(statusDate).toEqual(expectIsoDateString); + expect(statusDate2).toEqual(expectIsoDateString); + expect(statusDate).not.toEqual(statusDate2); + }); + + it('returns a status and statusDate if "going to run"', () => { + const result = buildRuleStatusAttributes('going to run'); + expect(result).toEqual({ + status: 'going to run', + statusDate: expectIsoDateString, + }); + }); + + it('returns success fields if "success"', () => { + const result = buildRuleStatusAttributes('succeeded', 'success message'); + expect(result).toEqual({ + status: 'succeeded', + statusDate: expectIsoDateString, + lastSuccessAt: expectIsoDateString, + lastSuccessMessage: 'success message', + }); + + expect(result.statusDate).toEqual(result.lastSuccessAt); + }); + + it('returns failure fields if "failed"', () => { + const result = buildRuleStatusAttributes('failed', 'failure message'); + expect(result).toEqual({ + status: 'failed', + statusDate: expectIsoDateString, + lastFailureAt: expectIsoDateString, + lastFailureMessage: 'failure message', + }); + + expect(result.statusDate).toEqual(result.lastFailureAt); + }); +}); + +describe('ruleStatusService', () => { + let currentStatus: ReturnType; + let ruleStatusClient: ReturnType; + let service: RuleStatusService; + + beforeEach(async () => { + currentStatus = exampleRuleStatus(); + ruleStatusClient = ruleStatusSavedObjectsClientMock.create(); + ruleStatusClient.find.mockResolvedValue(exampleFindRuleStatusResponse([currentStatus])); + service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient }); + }); + + describe('goingToRun', () => { + it('updates the current status to "going to run"', async () => { + await service.goingToRun(); + + expect(ruleStatusClient.update).toHaveBeenCalledWith( + currentStatus.id, + expect.objectContaining({ + status: 'going to run', + statusDate: expectIsoDateString, + }) + ); + }); + }); + + describe('success', () => { + it('updates the current status to "succeeded"', async () => { + await service.success('hey, it worked'); + + expect(ruleStatusClient.update).toHaveBeenCalledWith( + currentStatus.id, + expect.objectContaining({ + status: 'succeeded', + statusDate: expectIsoDateString, + lastSuccessAt: expectIsoDateString, + lastSuccessMessage: 'hey, it worked', + }) + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + // mock the creation of our new status + ruleStatusClient.create.mockResolvedValue(exampleRuleStatus()); + }); + + it('updates the current status to "failed"', async () => { + await service.error('oh no, it broke'); + + expect(ruleStatusClient.update).toHaveBeenCalledWith( + currentStatus.id, + expect.objectContaining({ + status: 'failed', + statusDate: expectIsoDateString, + lastFailureAt: expectIsoDateString, + lastFailureMessage: 'oh no, it broke', + }) + ); + }); + + it('does not delete statuses if we have less than the max number of statuses', async () => { + await service.error('oh no, it broke'); + + expect(ruleStatusClient.delete).not.toHaveBeenCalled(); + }); + + it('does not delete rule statuses when we just hit the limit', async () => { + // max - 1 in store, meaning our new error will put us at max + ruleStatusClient.find.mockResolvedValue( + exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES - 1)) + ); + service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient }); + + await service.error('oh no, it broke'); + + expect(ruleStatusClient.delete).not.toHaveBeenCalled(); + }); + + it('deletes stale rule status when we already have max statuses', async () => { + // max in store, meaning our new error will push one off the end + ruleStatusClient.find.mockResolvedValue( + exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES)) + ); + service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient }); + + await service.error('oh no, it broke'); + + expect(ruleStatusClient.delete).toHaveBeenCalledTimes(1); + // we should delete the 6th (index 5) + expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5'); + }); + + it('deletes any number of rule statuses in excess of the max', async () => { + // max + 1 in store, meaning our new error will put us two over + ruleStatusClient.find.mockResolvedValue( + exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES + 1)) + ); + service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient }); + + await service.error('oh no, it broke'); + + expect(ruleStatusClient.delete).toHaveBeenCalledTimes(2); + // we should delete the 6th (index 5) + expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5'); + // we should delete the 7th (index 6) + expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-6'); + }); + + it('handles multiple error calls', async () => { + // max in store, meaning our new error will push one off the end + ruleStatusClient.find.mockResolvedValue( + exampleFindRuleStatusResponse(buildStatuses(MAX_RULE_STATUSES)) + ); + service = await ruleStatusServiceFactory({ alertId: 'mock-alert-id', ruleStatusClient }); + + await service.error('oh no, it broke'); + await service.error('oh no, it broke'); + + expect(ruleStatusClient.delete).toHaveBeenCalledTimes(2); + // we should delete the 6th (index 5) + expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5'); + expect(ruleStatusClient.delete).toHaveBeenCalledWith('status-index-5'); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.ts new file mode 100644 index 0000000000000..5bfef134b0bae --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/rule_status_service.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { assertUnreachable } from '../../../utils/build_query'; +import { IRuleStatusAttributes, RuleStatusString } from '../rules/types'; +import { getOrCreateRuleStatuses } from './get_or_create_rule_statuses'; +import { RuleStatusSavedObjectsClient } from './rule_status_saved_objects_client'; + +// 1st is mutable status, followed by 5 most recent failures +export const MAX_RULE_STATUSES = 6; + +interface Attributes { + searchAfterTimeDurations?: string[]; + bulkCreateTimeDurations?: string[]; + lastLookBackDate?: string; + gap?: string; +} + +export interface RuleStatusService { + goingToRun: () => Promise; + success: (message: string, attributes?: Attributes) => Promise; + error: (message: string, attributes?: Attributes) => Promise; +} + +export const buildRuleStatusAttributes: ( + status: RuleStatusString, + message?: string, + attributes?: Attributes +) => Partial = (status, message, attributes = {}) => { + const now = new Date().toISOString(); + const baseAttributes: Partial = { + ...attributes, + status, + statusDate: now, + }; + + switch (status) { + case 'succeeded': { + return { + ...baseAttributes, + lastSuccessAt: now, + lastSuccessMessage: message, + }; + } + case 'failed': { + return { + ...baseAttributes, + lastFailureAt: now, + lastFailureMessage: message, + }; + } + case 'going to run': { + return baseAttributes; + } + } + + assertUnreachable(status); +}; + +export const ruleStatusServiceFactory = async ({ + alertId, + ruleStatusClient, +}: { + alertId: string; + ruleStatusClient: RuleStatusSavedObjectsClient; +}): Promise => { + return { + goingToRun: async () => { + const [currentStatus] = await getOrCreateRuleStatuses({ + alertId, + ruleStatusClient, + }); + + await ruleStatusClient.update(currentStatus.id, { + ...currentStatus.attributes, + ...buildRuleStatusAttributes('going to run'), + }); + }, + + success: async (message, attributes) => { + const [currentStatus] = await getOrCreateRuleStatuses({ + alertId, + ruleStatusClient, + }); + + await ruleStatusClient.update(currentStatus.id, { + ...currentStatus.attributes, + ...buildRuleStatusAttributes('succeeded', message, attributes), + }); + }, + + error: async (message, attributes) => { + const ruleStatuses = await getOrCreateRuleStatuses({ + alertId, + ruleStatusClient, + }); + const [currentStatus] = ruleStatuses; + + const failureAttributes = { + ...currentStatus.attributes, + ...buildRuleStatusAttributes('failed', message, attributes), + }; + + // We always update the newest status, so to 'persist' a failure we push a copy to the head of the list + await ruleStatusClient.update(currentStatus.id, failureAttributes); + const newStatus = await ruleStatusClient.create(failureAttributes); + + // drop oldest failures + const oldStatuses = [newStatus, ...ruleStatuses].slice(MAX_RULE_STATUSES); + await Promise.all(oldStatuses.map(status => ruleStatusClient.delete(status.id))); + }, + }; +}; 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 b12c21b7a5b56..06652028b3741 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 @@ -53,7 +53,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(mockService.callCluster).toHaveBeenCalledTimes(0); expect(success).toEqual(true); @@ -111,7 +111,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(mockService.callCluster).toHaveBeenCalledTimes(5); expect(success).toEqual(true); @@ -140,7 +140,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); @@ -176,7 +176,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(mockLogger.error).toHaveBeenCalled(); expect(success).toEqual(false); @@ -212,7 +212,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(success).toEqual(true); }); @@ -250,7 +250,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(success).toEqual(true); }); @@ -288,7 +288,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(success).toEqual(true); }); @@ -328,7 +328,7 @@ describe('searchAfterAndBulkCreate', () => { pageSize: 1, filter: undefined, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(success).toEqual(false); }); 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 ff263333fb798..a5d5dd0a7b710 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 @@ -31,7 +31,7 @@ interface SearchAfterAndBulkCreateParams { pageSize: number; filter: unknown; tags: string[]; - throttle: string | null; + throttle: string; } export interface SearchAfterAndBulkCreateReturnType { 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 ab9def14bef65..78b0cd84eeda3 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 @@ -6,11 +6,10 @@ import { performance } from 'perf_hooks'; import { Logger } from 'src/core/server'; -import { - SIGNALS_ID, - DEFAULT_SEARCH_AFTER_PAGE_SIZE, - NOTIFICATION_THROTTLE_RULE, -} from '../../../../common/constants'; + +import { SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; +import { isJobStarted, isMlRule } from '../../../../common/detection_engine/ml_helpers'; +import { SetupPlugins } from '../../../plugin'; import { buildEventsSearchQuery } from './build_events_query'; import { getInputIndex } from './get_input_output_index'; @@ -21,24 +20,24 @@ import { import { getFilter } from './get_filter'; import { SignalRuleAlertTypeDefinition, RuleAlertAttributes } from './types'; import { getGapBetweenRuns, makeFloatString } from './utils'; -import { writeSignalRuleExceptionToSavedObject } from './write_signal_rule_exception_to_saved_object'; import { signalParamsSchema } from './signal_params_schema'; import { siemRuleActionGroups } from './siem_rule_action_groups'; -import { writeGapErrorToSavedObject } from './write_gap_error_to_saved_object'; -import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; -import { getCurrentStatusSavedObject } from './get_current_status_saved_object'; -import { writeCurrentStatusSucceeded } from './write_current_status_succeeded'; 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'; export const signalRulesAlertType = ({ logger, version, + ml, }: { logger: Logger; version: string; + ml: SetupPlugins['ml']; }): SignalRuleAlertTypeDefinition => { return { id: SIGNALS_ID, @@ -64,22 +63,15 @@ export const signalRulesAlertType = ({ to, type, } = params; + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(services.savedObjectsClient); + const ruleStatusService = await ruleStatusServiceFactory({ + alertId, + ruleStatusClient, + }); const savedObject = await services.savedObjectsClient.get( 'alert', alertId ); - - const ruleStatusSavedObjects = await getRuleStatusSavedObjects({ - alertId, - services, - }); - - const currentStatusSavedObject = await getCurrentStatusSavedObject({ - alertId, - services, - ruleStatusSavedObjects, - }); - const { actions, name, @@ -92,23 +84,31 @@ export const signalRulesAlertType = ({ throttle, params: ruleParams, } = savedObject.attributes; - const updatedAt = savedObject.updated_at ?? ''; - - const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); - await writeGapErrorToSavedObject({ - alertId, - logger, - ruleId: ruleId ?? '(unknown rule id)', - currentStatusSavedObject, - services, - gap, - ruleStatusSavedObjects, + const buildRuleMessage = buildRuleMessageFactory({ + id: alertId, + ruleId, name, + index: outputIndex, }); + logger.debug(buildRuleMessage('[+] Starting Signal Rule execution')); + await ruleStatusService.goingToRun(); + + const gap = getGapBetweenRuns({ previousStartedAt, interval, from, to }); + if (gap != null && gap.asMilliseconds() > 0) { + const gapString = gap.humanize(); + const gapMessage = buildRuleMessage( + `${gapString} (${gap.asMilliseconds()}ms) has passed since last rule execution, and signals may have been missed.`, + 'Consider increasing your look behind time or adding more Kibana instances.' + ); + logger.warn(gapMessage); + + await ruleStatusService.error(gapMessage, { gap: gapString }); + } + const searchAfterSize = Math.min(params.maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); - let creationSucceeded: SearchAfterAndBulkCreateReturnType = { + let result: SearchAfterAndBulkCreateReturnType = { success: false, bulkCreateTimes: [], searchAfterTimes: [], @@ -116,11 +116,34 @@ export const signalRulesAlertType = ({ }; try { - if (type === 'machine_learning') { + if (isMlRule(type)) { + if (ml == null) { + throw new Error('ML plugin unavailable during rule execution'); + } if (machineLearningJobId == null || anomalyThreshold == null) { throw new Error( - `Attempted to execute machine learning rule, but it is missing job id and/or anomaly threshold for rule id: "${ruleId}", name: "${name}", signals index: "${outputIndex}", job id: "${machineLearningJobId}", anomaly threshold: "${anomalyThreshold}"` + [ + 'Machine learning rule is missing job id and/or anomaly threshold:', + `job id: "${machineLearningJobId}"`, + `anomaly threshold: "${anomalyThreshold}"`, + ].join('\n') + ); + } + + const summaryJobs = await ml + .jobServiceProvider(ml.mlClient.callAsInternalUser) + .jobsSummary([machineLearningJobId]); + const jobSummary = summaryJobs.find(job => job.id === machineLearningJobId); + + if (jobSummary == null || !isJobStarted(jobSummary.jobState, jobSummary.datafeedState)) { + const errorMessage = buildRuleMessage( + 'Machine learning job is not started:', + `job id: "${machineLearningJobId}"`, + `job status: "${jobSummary?.jobState}"`, + `datafeed status: "${jobSummary?.datafeedState}"` ); + logger.warn(errorMessage); + await ruleStatusService.error(errorMessage); } const anomalyResults = await findMlSignals( @@ -130,12 +153,9 @@ export const signalRulesAlertType = ({ to, services.callCluster ); - const anomalyCount = anomalyResults.hits.hits.length; if (anomalyCount) { - logger.info( - `Found ${anomalyCount} signals from ML anomalies for signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", pushing signals to index "${outputIndex}"` - ); + logger.info(buildRuleMessage(`Found ${anomalyCount} signals from ML anomalies.`)); } const { success, bulkCreateDuration } = await bulkCreateMlSignals({ @@ -156,9 +176,9 @@ export const signalRulesAlertType = ({ enabled, tags, }); - creationSucceeded.success = success; + result.success = success; if (bulkCreateDuration) { - creationSucceeded.bulkCreateTimes.push(bulkCreateDuration); + result.bulkCreateTimes.push(bulkCreateDuration); } } else { const inputIndex = await getInputIndex(services, version, index); @@ -181,27 +201,21 @@ export const signalRulesAlertType = ({ searchAfterSortId: undefined, }); - logger.debug( - `Starting signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` - ); - logger.debug( - `[+] Initial search call of signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` - ); + logger.debug(buildRuleMessage('[+] Initial search call')); const start = performance.now(); const noReIndexResult = await services.callCluster('search', noReIndex); const end = performance.now(); - if (noReIndexResult.hits.total.value !== 0) { + const signalCount = noReIndexResult.hits.total.value; + if (signalCount !== 0) { logger.info( - `Found ${ - noReIndexResult.hits.total.value - } signals from the indexes of "[${inputIndex.join( - ', ' - )}]" using signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", pushing signals to index "${outputIndex}"` + buildRuleMessage( + `Found ${signalCount} signals from the indexes of "[${inputIndex.join(', ')}]"` + ) ); } - creationSucceeded = await searchAfterAndBulkCreate({ + result = await searchAfterAndBulkCreate({ someResult: noReIndexResult, ruleParams: params, services, @@ -222,11 +236,11 @@ export const signalRulesAlertType = ({ tags, throttle, }); - creationSucceeded.searchAfterTimes.push(makeFloatString(end - start)); + result.searchAfterTimes.push(makeFloatString(end - start)); } - if (creationSucceeded.success) { - if (meta?.throttle === NOTIFICATION_THROTTLE_RULE && actions.length) { + if (result.success) { + if (actions.length) { const notificationRuleParams = { ...ruleParams, name, @@ -237,14 +251,12 @@ export const signalRulesAlertType = ({ to: 'now', index: ruleParams.outputIndex, ruleId: ruleParams.ruleId!, - kibanaSiemAppUrl: meta.kibanaSiemAppUrl as string, + kibanaSiemAppUrl: meta?.kibanaSiemAppUrl as string, ruleAlertId: savedObject.id, callCluster: services.callCluster, }); - logger.info( - `Found ${signalsCount} signals using signal rule name: "${notificationRuleParams.name}", id: "${notificationRuleParams.ruleId}", rule_id: "${notificationRuleParams.ruleId}" in "${notificationRuleParams.outputIndex}" index` - ); + logger.info(buildRuleMessage(`Found ${signalsCount} signals for notification.`)); if (signalsCount) { const alertInstance = services.alertInstanceFactory(alertId); @@ -257,44 +269,35 @@ export const signalRulesAlertType = ({ } } - logger.debug( - `Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` - ); - await writeCurrentStatusSucceeded({ - services, - currentStatusSavedObject, - bulkCreateTimes: creationSucceeded.bulkCreateTimes, - searchAfterTimes: creationSucceeded.searchAfterTimes, - lastLookBackDate: creationSucceeded.lastLookBackDate?.toISOString() ?? null, + logger.debug(buildRuleMessage('[+] Signal Rule execution completed.')); + await ruleStatusService.success('succeeded', { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookBackDate?.toISOString(), }); } else { - await writeSignalRuleExceptionToSavedObject({ - name, - alertId, - currentStatusSavedObject, - logger, - message: `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`, - services, - ruleStatusSavedObjects, - ruleId: ruleId ?? '(unknown rule id)', - bulkCreateTimes: creationSucceeded.bulkCreateTimes, - searchAfterTimes: creationSucceeded.searchAfterTimes, - lastLookBackDate: creationSucceeded.lastLookBackDate?.toISOString() ?? null, + const errorMessage = buildRuleMessage( + 'Bulk Indexing of signals failed. Check logs for further details.' + ); + logger.error(errorMessage); + await ruleStatusService.error(errorMessage, { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookBackDate?.toISOString(), }); } - } catch (err) { - await writeSignalRuleExceptionToSavedObject({ - name, - alertId, - currentStatusSavedObject, - logger, - message: `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`, - services, - ruleStatusSavedObjects, - ruleId: ruleId ?? '(unknown rule id)', - bulkCreateTimes: creationSucceeded.bulkCreateTimes, - searchAfterTimes: creationSucceeded.searchAfterTimes, - lastLookBackDate: creationSucceeded.lastLookBackDate?.toISOString() ?? null, + } catch (error) { + const errorMessage = error.message ?? '(no error message given)'; + const message = buildRuleMessage( + 'An error occurred during rule execution:', + `message: "${errorMessage}"` + ); + + logger.error(message); + await ruleStatusService.error(message, { + bulkCreateTimeDurations: result.bulkCreateTimes, + searchAfterTimeDurations: result.searchAfterTimes, + lastLookBackDate: result.lastLookBackDate?.toISOString(), }); } }, 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 93f9c24a057f2..45b5610e2d3c3 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 @@ -160,7 +160,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(success).toEqual(true); }); @@ -192,7 +192,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(success).toEqual(true); }); @@ -216,7 +216,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(success).toEqual(true); }); @@ -241,7 +241,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(mockLogger.error).not.toHaveBeenCalled(); @@ -268,7 +268,7 @@ describe('singleBulkCreate', () => { interval: '5m', enabled: true, tags: ['some fake tag 1', 'some fake tag 2'], - throttle: null, + throttle: 'no_actions', }); expect(mockLogger.error).toHaveBeenCalled(); 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 0192ff76efa54..ffec40b839bf6 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 @@ -30,7 +30,7 @@ interface SingleBulkCreateParams { interval: string; enabled: boolean; tags: string[]; - throttle: string | null; + throttle: string; } /** diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index 93c48ed38c7c4..543e8bf0619b0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -158,7 +158,7 @@ export interface AlertAttributes { schedule: { interval: string; }; - throttle: string | null; + throttle: string; } export interface RuleAlertAttributes extends AlertAttributes { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts deleted file mode 100644 index 50136790c3479..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_current_status_succeeded.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SavedObject } from 'src/core/server'; -import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; - -import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; - -interface GetRuleStatusSavedObject { - services: AlertServices; - currentStatusSavedObject: SavedObject; - lastLookBackDate: string | null | undefined; - bulkCreateTimes: string[] | null | undefined; - searchAfterTimes: string[] | null | undefined; -} - -export const writeCurrentStatusSucceeded = async ({ - services, - currentStatusSavedObject, - lastLookBackDate, - bulkCreateTimes, - searchAfterTimes, -}: GetRuleStatusSavedObject): Promise => { - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'succeeded'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastSuccessAt = sDate; - currentStatusSavedObject.attributes.lastSuccessMessage = 'succeeded'; - if (lastLookBackDate != null) { - currentStatusSavedObject.attributes.lastLookBackDate = lastLookBackDate; - } - if (bulkCreateTimes != null) { - currentStatusSavedObject.attributes.bulkCreateTimeDurations = bulkCreateTimes; - } - if (searchAfterTimes != null) { - currentStatusSavedObject.attributes.searchAfterTimeDurations = searchAfterTimes; - } - await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, { - ...currentStatusSavedObject.attributes, - }); -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts deleted file mode 100644 index e47e5388527da..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_gap_error_to_saved_object.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment'; -import { Logger, SavedObject, SavedObjectsFindResponse } from 'src/core/server'; - -import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; -import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; - -interface WriteGapErrorToSavedObjectParams { - logger: Logger; - alertId: string; - ruleId: string; - currentStatusSavedObject: SavedObject; - ruleStatusSavedObjects: SavedObjectsFindResponse; - services: AlertServices; - gap: moment.Duration | null | undefined; - name: string; -} - -export const writeGapErrorToSavedObject = async ({ - alertId, - currentStatusSavedObject, - logger, - services, - ruleStatusSavedObjects, - ruleId, - gap, - name, -}: WriteGapErrorToSavedObjectParams): Promise => { - if (gap != null && gap.asMilliseconds() > 0) { - logger.warn( - `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.` - ); - // write a failure status whenever we have a time gap - // this is a temporary solution until general activity - // monitoring is developed as a feature - const gapDate = new Date().toISOString(); - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - alertId, - statusDate: gapDate, - status: 'failed', - lastFailureAt: gapDate, - lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt, - lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`, - lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage, - gap: gap.humanize(), - }); - - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } - } -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts deleted file mode 100644 index 2a14184859591..0000000000000 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/write_signal_rule_exception_to_saved_object.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Logger, SavedObject, SavedObjectsFindResponse } from 'src/core/server'; - -import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../rules/types'; -import { ruleStatusSavedObjectType } from '../rules/saved_object_mappings'; - -interface SignalRuleExceptionParams { - logger: Logger; - alertId: string; - ruleId: string; - currentStatusSavedObject: SavedObject; - ruleStatusSavedObjects: SavedObjectsFindResponse; - message: string; - services: AlertServices; - name: string; - lastLookBackDate?: string | null | undefined; - bulkCreateTimes?: string[] | null | undefined; - searchAfterTimes?: string[] | null | undefined; -} - -export const writeSignalRuleExceptionToSavedObject = async ({ - alertId, - currentStatusSavedObject, - logger, - message, - services, - ruleStatusSavedObjects, - ruleId, - name, - lastLookBackDate, - bulkCreateTimes, - searchAfterTimes, -}: SignalRuleExceptionParams): Promise => { - logger.error( - `Error from signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" message: ${message}` - ); - const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'failed'; - currentStatusSavedObject.attributes.statusDate = sDate; - currentStatusSavedObject.attributes.lastFailureAt = sDate; - currentStatusSavedObject.attributes.lastFailureMessage = message; - if (lastLookBackDate) { - currentStatusSavedObject.attributes.lastLookBackDate = lastLookBackDate; - } - if (bulkCreateTimes) { - currentStatusSavedObject.attributes.bulkCreateTimeDurations = bulkCreateTimes; - } - if (searchAfterTimes) { - currentStatusSavedObject.attributes.searchAfterTimeDurations = searchAfterTimes; - } - // current status is failing - await services.savedObjectsClient.update(ruleStatusSavedObjectType, currentStatusSavedObject.id, { - ...currentStatusSavedObject.attributes, - }); - // create new status for historical purposes - await services.savedObjectsClient.create(ruleStatusSavedObjectType, { - ...currentStatusSavedObject.attributes, - }); - - if (ruleStatusSavedObjects.saved_objects.length >= 6) { - // delete fifth status and prepare to insert a newer one. - const toDelete = ruleStatusSavedObjects.saved_objects.slice(5); - await toDelete.forEach(async item => - services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id) - ); - } -}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index aae8763a7ea39..efa0a92cc573b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -8,7 +8,7 @@ import { CallAPIOptions } from '../../../../../../../src/core/server'; import { Filter } from '../../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; import { ListsDefaultArraySchema } from './routes/schemas/types/lists_default_array'; -import { RuleAlertAction } from '../../../common/detection_engine/types'; +import { RuleAlertAction, RuleType } from '../../../common/detection_engine/types'; export type PartialFilter = Partial; @@ -28,7 +28,6 @@ export interface ThreatParams { // TODO: Eventually this whole RuleAlertParams will be replaced with io-ts. For now we can slowly strangle it out and reduce duplicate types // We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove // types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. -export type RuleType = 'query' | 'saved_query' | 'machine_learning'; export interface RuleAlertParams { actions: RuleAlertAction[]; @@ -61,7 +60,7 @@ export interface RuleAlertParams { threat: ThreatParams[] | undefined | null; type: RuleType; version: number; - throttle: string | null; + throttle: string; lists: ListsDefaultArraySchema | null | undefined; } @@ -119,7 +118,6 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & { created_by: string | undefined | null; updated_by: string | undefined | null; immutable: boolean; - throttle: string | undefined | null; }; export type ImportRuleAlertRest = Omit & { diff --git a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts index 63aee97729141..6552f973a66fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/timeline/routes/schemas/schemas.ts @@ -6,14 +6,14 @@ import Joi from 'joi'; const allowEmptyString = Joi.string().allow([null, '']); -const columnHeaderType = Joi.string(); +const columnHeaderType = allowEmptyString; export const created = Joi.number().allow(null); -export const createdBy = Joi.string(); +export const createdBy = allowEmptyString; export const description = allowEmptyString; export const end = Joi.number(); export const eventId = allowEmptyString; -export const eventType = Joi.string(); +export const eventType = allowEmptyString; export const filters = Joi.array() .items( @@ -24,19 +24,11 @@ export const filters = Joi.array() disabled: Joi.boolean().allow(null), field: allowEmptyString, formattedValue: allowEmptyString, - index: { - type: 'keyword', - }, - key: { - type: 'keyword', - }, - negate: { - type: 'boolean', - }, + index: allowEmptyString, + key: allowEmptyString, + negate: Joi.boolean().allow(null), params: allowEmptyString, - type: { - type: 'keyword', - }, + type: allowEmptyString, value: allowEmptyString, }), exists: allowEmptyString, @@ -68,22 +60,22 @@ export const version = allowEmptyString; export const columns = Joi.array().items( Joi.object({ aggregatable: Joi.boolean().allow(null), - category: Joi.string(), + category: allowEmptyString, columnHeaderType, description, example: allowEmptyString, indexes: allowEmptyString, - id: Joi.string(), + id: allowEmptyString, name, placeholder: allowEmptyString, searchable: Joi.boolean().allow(null), - type: Joi.string(), + type: allowEmptyString, }).required() ); export const dataProviders = Joi.array() .items( Joi.object({ - id: Joi.string(), + id: allowEmptyString, name: allowEmptyString, enabled: Joi.boolean().allow(null), excluded: Joi.boolean().allow(null), @@ -98,7 +90,7 @@ export const dataProviders = Joi.array() and: Joi.array() .items( Joi.object({ - id: Joi.string(), + id: allowEmptyString, name, enabled: Joi.boolean().allow(null), excluded: Joi.boolean().allow(null), @@ -122,9 +114,9 @@ export const dateRange = Joi.object({ }); export const favorite = Joi.array().items( Joi.object({ - keySearch: Joi.string(), - fullName: Joi.string(), - userName: Joi.string(), + keySearch: allowEmptyString, + fullName: allowEmptyString, + userName: allowEmptyString, favoriteDate: Joi.number(), }).allow(null) ); @@ -141,26 +133,26 @@ const noteItem = Joi.object({ }); export const eventNotes = Joi.array().items(noteItem); export const globalNotes = Joi.array().items(noteItem); -export const kqlMode = Joi.string(); +export const kqlMode = allowEmptyString; export const kqlQuery = Joi.object({ filterQuery: Joi.object({ kuery: Joi.object({ - kind: Joi.string(), + kind: allowEmptyString, expression: allowEmptyString, }), serializedQuery: allowEmptyString, }), }); export const pinnedEventIds = Joi.array() - .items(Joi.string()) + .items(allowEmptyString) .allow(null); export const sort = Joi.object({ - columnId: Joi.string(), - sortDirection: Joi.string(), + columnId: allowEmptyString, + sortDirection: allowEmptyString, }); /* eslint-disable @typescript-eslint/camelcase */ -export const ids = Joi.array().items(Joi.string()); +export const ids = Joi.array().items(allowEmptyString); export const exclude_export_details = Joi.boolean(); export const file_name = allowEmptyString; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index 98631ea220a54..13b58fa1d57eb 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -18,6 +18,7 @@ import { } from '../../../../../src/core/server'; import { SecurityPluginSetup as SecuritySetup } from '../../../../plugins/security/server'; import { PluginSetupContract as FeaturesSetup } from '../../../../plugins/features/server'; +import { MlPluginSetup as MlSetup } from '../../../../plugins/ml/server'; import { EncryptedSavedObjectsPluginSetup as EncryptedSavedObjectsSetup } from '../../../../plugins/encrypted_saved_objects/server'; import { SpacesPluginSetup as SpacesSetup } from '../../../../plugins/spaces/server'; import { PluginStartContract as ActionsStart } from '../../../../plugins/actions/server'; @@ -35,6 +36,7 @@ import { pinnedEventSavedObjectType, timelineSavedObjectType, ruleStatusSavedObjectType, + ruleActionsSavedObjectType, } from './saved_objects'; import { SiemClientFactory } from './client'; import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; @@ -48,6 +50,7 @@ export interface SetupPlugins { licensing: LicensingPluginSetup; security?: SecuritySetup; spaces?: SpacesSetup; + ml?: MlSetup; } export interface StartPlugins { @@ -119,6 +122,11 @@ export class Plugin { pinnedEventSavedObjectType, timelineSavedObjectType, ruleStatusSavedObjectType, + ruleActionsSavedObjectType, + 'cases', + 'cases-comments', + 'cases-configure', + 'cases-user-actions', ], read: ['config'], }, @@ -145,6 +153,11 @@ export class Plugin { pinnedEventSavedObjectType, timelineSavedObjectType, ruleStatusSavedObjectType, + ruleActionsSavedObjectType, + 'cases', + 'cases-comments', + 'cases-configure', + 'cases-user-actions', ], }, ui: [ @@ -164,6 +177,7 @@ export class Plugin { const signalRuleType = signalRulesAlertType({ logger: this.logger, version: this.context.env.packageInfo.version, + ml: plugins.ml, }); const ruleNotificationType = rulesNotificationAlertType({ logger: this.logger, diff --git a/x-pack/legacy/plugins/siem/server/saved_objects.ts b/x-pack/legacy/plugins/siem/server/saved_objects.ts index 76d8837883b8b..7b097eefedb46 100644 --- a/x-pack/legacy/plugins/siem/server/saved_objects.ts +++ b/x-pack/legacy/plugins/siem/server/saved_objects.ts @@ -17,11 +17,16 @@ import { ruleStatusSavedObjectMappings, ruleStatusSavedObjectType, } from './lib/detection_engine/rules/saved_object_mappings'; +import { + ruleActionsSavedObjectMappings, + ruleActionsSavedObjectType, +} from './lib/detection_engine/rule_actions/saved_object_mappings'; export { noteSavedObjectType, pinnedEventSavedObjectType, ruleStatusSavedObjectType, + ruleActionsSavedObjectType, timelineSavedObjectType, }; export const savedObjectMappings = { @@ -29,4 +34,5 @@ export const savedObjectMappings = { ...noteSavedObjectMappings, ...pinnedEventSavedObjectMappings, ...ruleStatusSavedObjectMappings, + ...ruleActionsSavedObjectMappings, }; diff --git a/x-pack/legacy/plugins/siem/server/types.ts b/x-pack/legacy/plugins/siem/server/types.ts index 4119645a5af47..a52322f5f830c 100644 --- a/x-pack/legacy/plugins/siem/server/types.ts +++ b/x-pack/legacy/plugins/siem/server/types.ts @@ -7,12 +7,8 @@ import { Legacy } from 'kibana'; import { SiemClient } from './client'; -export { LegacyRequest } from '../../../../../src/core/server'; - export interface LegacyServices { - alerting?: Legacy.Server['plugins']['alerting']; config: Legacy.Server['config']; - route: Legacy.Server['route']; } export { SiemClient }; @@ -23,6 +19,6 @@ export interface SiemRequestContext { declare module 'src/core/server' { interface RequestHandlerContext { - siem: SiemRequestContext; + siem?: SiemRequestContext; } } diff --git a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx index cfed52f4e5d27..168d71a31dd45 100644 --- a/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/functional/location_map/embeddables/embedded_map.tsx @@ -7,9 +7,9 @@ import React, { useEffect, useState, useContext, useRef } from 'react'; import uuid from 'uuid'; import styled from 'styled-components'; +import { npStart } from 'ui/new_platform'; import { ViewMode } from '../../../../../../../../../src/plugins/embeddable/public'; -import { start } from '../../../../../../../../../src/legacy/core_plugins/embeddable_api/public/np_ready/public/legacy'; import * as i18n from './translations'; import { MapEmbeddable, MapEmbeddableInput } from '../../../../../../maps/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../../../../../plugins/maps/public'; @@ -47,7 +47,7 @@ export const EmbeddedMap = React.memo(({ upPoints, downPoints }: EmbeddedMapProp const { colors } = useContext(UptimeThemeContext); const [embeddable, setEmbeddable] = useState(); const embeddableRoot: React.RefObject = useRef(null); - const factory = start.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); + const factory = npStart.plugins.embeddable.getEmbeddableFactory(MAP_SAVED_OBJECT_TYPE); const input: MapEmbeddableInput = { id: uuid.v4(), diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap index 354521e7c55b9..ead27425c26f3 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/__tests__/__snapshots__/ml_flyout.test.tsx.snap @@ -53,9 +53,18 @@ exports[`ML Flyout component renders without errors 1`] = ` + + + Cancel + + @@ -206,8 +215,26 @@ exports[`ML Flyout component shows license info if no ml available 1`] = ` class="euiFlyoutFooter" >
+
+ +
diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx index 917367f3e8dad..fdecfbf20810c 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/ml_flyout.tsx @@ -7,6 +7,7 @@ import React, { useContext } from 'react'; import { EuiButton, + EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiFlyout, @@ -64,11 +65,15 @@ export function MLFlyoutView({ isCreatingJob, onClickCreate, onClose, canCreateM {labels.TAKE_SOME_TIME_TEXT}

- - + + + onClose()} disabled={isCreatingJob || isLoadingMLJob}> + {labels.CANCEL_LABEL} + + onClickCreate()} diff --git a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx index 570dd9d1bfa26..32374674771e8 100644 --- a/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx +++ b/x-pack/legacy/plugins/uptime/public/components/monitor_details/ml/translations.tsx @@ -124,6 +124,13 @@ export const CREATE_NEW_JOB = i18n.translate( } ); +export const CANCEL_LABEL = i18n.translate( + 'xpack.uptime.ml.enableAnomalyDetectionPanel.cancelLabel', + { + defaultMessage: 'Cancel', + } +); + export const CREAT_ML_JOB_DESC = i18n.translate( 'xpack.uptime.ml.enableAnomalyDetectionPanel.createMLJobDescription', { diff --git a/x-pack/plugins/case/common/api/user.ts b/x-pack/plugins/case/common/api/user.ts index 3adb78ccdac07..af198470737cf 100644 --- a/x-pack/plugins/case/common/api/user.ts +++ b/x-pack/plugins/case/common/api/user.ts @@ -9,7 +9,7 @@ import * as rt from 'io-ts'; export const UserRT = rt.type({ email: rt.union([rt.undefined, rt.null, rt.string]), full_name: rt.union([rt.undefined, rt.null, rt.string]), - username: rt.string, + username: rt.union([rt.undefined, rt.null, rt.string]), }); export const UsersRt = rt.array(UserRT); diff --git a/x-pack/plugins/case/kibana.json b/x-pack/plugins/case/kibana.json index f565dc1b6924e..55416ee28c7df 100644 --- a/x-pack/plugins/case/kibana.json +++ b/x-pack/plugins/case/kibana.json @@ -2,7 +2,7 @@ "configPath": ["xpack", "case"], "id": "case", "kibanaVersion": "kibana", - "requiredPlugins": ["security", "actions"], + "requiredPlugins": ["actions"], "optionalPlugins": [ "spaces", "security" diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index a6a459373b0ed..670e6ec797a9f 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -60,7 +60,7 @@ export class CasePlugin { ); const caseService = await caseServicePlugin.setup({ - authentication: plugins.security.authc, + authentication: plugins.security != null ? plugins.security.authc : null, }); const caseConfigureService = await caseConfigureServicePlugin.setup(); const userActionService = await userActionServicePlugin.setup(); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index af6f8bf223ee5..23039da681ec6 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -20,6 +20,10 @@ describe('POST comment', () => { let routeHandler: RequestHandler; beforeAll(async () => { routeHandler = await createRoute(initPostCommentApi, 'post'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); }); it(`Posts a new comment`, async () => { const request = httpServerMock.createKibanaRequest({ @@ -92,7 +96,7 @@ describe('POST comment', () => { expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); - it(`Returns an error if user authentication throws`, async () => { + it(`Allow user to create comments without authentications`, async () => { routeHandler = await createRoute(initPostCommentApi, 'post', true); const request = httpServerMock.createKibanaRequest({ @@ -114,7 +118,21 @@ describe('POST comment', () => { ); const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(500); - expect(response.payload.isBoom).toEqual(true); + expect(response.status).toEqual(200); + expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({ + comment: 'Wow, good luck catching that bad meanie!', + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + email: null, + full_name: null, + username: null, + }, + id: 'mock-comment', + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index 96ce3c1a7eead..5899102224774 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -19,6 +19,10 @@ describe('POST cases', () => { let routeHandler: RequestHandler; beforeAll(async () => { routeHandler = await createRoute(initPostCaseApi, 'post'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); }); it(`Posts a new case`, async () => { const request = httpServerMock.createKibanaRequest({ @@ -85,7 +89,7 @@ describe('POST cases', () => { expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); - it(`Returns an error if user authentication throws`, async () => { + it(`Allow user to create case without authentication`, async () => { routeHandler = await createRoute(initPostCaseApi, 'post', true); const request = httpServerMock.createKibanaRequest({ @@ -105,7 +109,27 @@ describe('POST cases', () => { ); const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(500); - expect(response.payload.isBoom).toEqual(true); + expect(response.status).toEqual(200); + expect(response.payload).toEqual({ + closed_at: null, + closed_by: null, + comments: [], + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + email: null, + full_name: null, + username: null, + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + id: 'mock-it', + status: 'open', + tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index 1b24904ce03b7..aff057adea37f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -15,7 +15,6 @@ import { flattenCaseSavedObject, wrapError, escapeHatch } from '../utils'; import { CaseExternalServiceRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; -import { CASE_COMMENT_SAVED_OBJECT } from '../../../saved_object_types'; export function initPushCaseUserActionApi({ caseConfigureService, @@ -54,7 +53,6 @@ export function initPushCaseUserActionApi({ client, caseId, options: { - filter: `not ${CASE_COMMENT_SAVED_OBJECT}.attributes.pushed_at: *`, fields: [], page: 1, perPage: 1, @@ -72,7 +70,6 @@ export function initPushCaseUserActionApi({ client, caseId, options: { - filter: `not ${CASE_COMMENT_SAVED_OBJECT}.attributes.pushed_at: *`, fields: [], page: 1, perPage: totalCommentsFindByCases.total, @@ -105,16 +102,16 @@ export function initPushCaseUserActionApi({ }), caseService.patchComments({ client, - comments: comments.saved_objects.map(comment => ({ - commentId: comment.id, - updatedAttributes: { - pushed_at: pushedDate, - pushed_by: { username, full_name, email }, - updated_at: pushedDate, - updated_by: { username, full_name, email }, - }, - version: comment.version, - })), + comments: comments.saved_objects + .filter(comment => comment.attributes.pushed_at == null) + .map(comment => ({ + commentId: comment.id, + updatedAttributes: { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + }, + version: comment.version, + })), }), userActionService.postUserActions({ client, diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 822d6d70c7d61..a3df0fc93d2ac 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -33,10 +33,10 @@ export const transformNewCase = ({ username, }: { createdDate: string; - email?: string; - full_name?: string; + email?: string | null; + full_name?: string | null; newCase: CasePostRequest; - username: string; + username?: string | null; }): CaseAttributes => ({ ...newCase, closed_at: null, @@ -52,9 +52,9 @@ export const transformNewCase = ({ interface NewCommentArgs { comment: string; createdDate: string; - email?: string; - full_name?: string; - username: string; + email?: string | null; + full_name?: string | null; + username?: string | null; } export const transformNewComment = ({ comment, diff --git a/x-pack/plugins/case/server/scripts/mock/case/post_case.json b/x-pack/plugins/case/server/scripts/mock/case/post_case.json index 25a9780596828..743fa396295ca 100644 --- a/x-pack/plugins/case/server/scripts/mock/case/post_case.json +++ b/x-pack/plugins/case/server/scripts/mock/case/post_case.json @@ -1,7 +1,6 @@ { "description": "This looks not so good", "title": "Bad meanie defacing data", - "status": "open", "tags": [ "defacement" ] diff --git a/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json b/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json index cf066d2c8a1e8..13efe436a640d 100644 --- a/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json +++ b/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json @@ -1,7 +1,6 @@ { "description": "I hope there are some good security engineers at this company...", "title": "Another bad dude", - "status": "open", "tags": [ "phishing" ] diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 52f41aae293ab..cdc5fd21a8138 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -95,7 +95,7 @@ interface GetUserArgs { } interface CaseServiceDeps { - authentication: SecurityPluginSetup['authc']; + authentication: SecurityPluginSetup['authc'] | null; } export interface CaseServiceSetup { deleteCase(args: GetCaseArgs): Promise<{}>; @@ -107,7 +107,7 @@ export interface CaseServiceSetup { getComment(args: GetCommentArgs): Promise>; getTags(args: ClientArgs): Promise; getReporters(args: ClientArgs): Promise; - getUser(args: GetUserArgs): Promise; + getUser(args: GetUserArgs): Promise; postNewCase(args: PostCaseArgs): Promise>; postNewComment(args: PostCommentArgs): Promise>; patchCase(args: PatchCaseArgs): Promise>; @@ -207,13 +207,28 @@ export class CaseService { } }, getUser: async ({ request, response }: GetUserArgs) => { - this.log.debug(`Attempting to authenticate a user`); - const user = authentication!.getCurrentUser(request); - if (!user) { - this.log.debug(`Error on GET user: Bad User`); - throw new Error('Bad User - the user is not authenticated'); + try { + this.log.debug(`Attempting to authenticate a user`); + if (authentication != null) { + const user = authentication.getCurrentUser(request); + if (!user) { + return { + username: null, + full_name: null, + email: null, + }; + } + return user; + } + return { + username: null, + full_name: null, + email: null, + }; + } catch (error) { + this.log.debug(`Error on GET cases: ${error}`); + throw error; } - return user; }, postNewCase: async ({ client, attributes }: PostCaseArgs) => { try { diff --git a/x-pack/plugins/case/server/services/user_actions/helpers.ts b/x-pack/plugins/case/server/services/user_actions/helpers.ts index 95d35d5a57a57..e89700419b19d 100644 --- a/x-pack/plugins/case/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/case/server/services/user_actions/helpers.ts @@ -35,7 +35,7 @@ export const transformNewUserAction = ({ full_name?: string | null; newValue?: string | null; oldValue?: string | null; - username: string; + username?: string | null; }): CaseUserActionAttributes => ({ action_field: actionField, action, diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts index c493e8ce86781..70bdcdfd3cf1f 100644 --- a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts @@ -33,7 +33,7 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider void; + onCloseMenu: () => void; + onViewDetails: () => void; +} + +const MENU_LABEL = i18n.translate('xpack.infra.logEntryItemView.logEntryActionsMenuToolTip', { + defaultMessage: 'View Details', +}); + +const LOG_DETAILS_LABEL = i18n.translate('xpack.infra.logs.logEntryActionsDetailsButton', { + defaultMessage: 'View actions for line', +}); + +export const LogEntryActionsColumn: React.FC = ({ + isHovered, + isMenuOpen, + onOpenMenu, + onCloseMenu, + onViewDetails, +}) => { + const handleClickViewDetails = useCallback(() => { + onCloseMenu(); + onViewDetails(); + }, [onCloseMenu, onViewDetails]); + + const button = ( + + + + ); + + return ( + + {isHovered || isMenuOpen ? ( + + +
+ + + + + + +
+
+
+ ) : null} +
+ ); +}; + +const ActionsColumnContent = euiStyled(LogEntryColumnContent)` + overflow: hidden; + user-select: none; +`; + +const ButtonWrapper = euiStyled.div` + background: ${props => props.theme.eui.euiColorPrimary}; + border-radius: 50%; +`; + +// this prevents the button from influencing the line height +const AbsoluteWrapper = euiStyled.div` + overflow: hidden; + position: absolute; +`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx index 5fc4606a774d5..d6068b6e60992 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.test.tsx @@ -25,8 +25,6 @@ describe('LogEntryFieldColumn', () => { columnValue={column} highlights={[]} isActiveHighlight={false} - isHighlighted={false} - isHovered={false} wrapMode="pre-wrapped" />, { wrappingComponent: EuiThemeProvider } as any // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36075 @@ -58,8 +56,6 @@ describe('LogEntryFieldColumn', () => { columnValue={column} highlights={[]} isActiveHighlight={false} - isHighlighted={false} - isHovered={false} wrapMode="pre-wrapped" />, { wrappingComponent: EuiThemeProvider } as any // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36075 @@ -81,8 +77,6 @@ describe('LogEntryFieldColumn', () => { columnValue={column} highlights={[]} isActiveHighlight={false} - isHighlighted={false} - isHovered={false} wrapMode="pre-wrapped" />, { wrappingComponent: EuiThemeProvider } as any // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36075 diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx index 202108cda5ac0..c73c9674f9683 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_field_column.tsx @@ -13,7 +13,6 @@ import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './h import { LogEntryColumnContent } from './log_entry_column'; import { LogColumn } from '../../../../common/http_api'; import { - hoveredContentStyle, longWrappedContentStyle, preWrappedContentStyle, unwrappedContentStyle, @@ -24,8 +23,6 @@ interface LogEntryFieldColumnProps { columnValue: LogColumn; highlights: LogColumn[]; isActiveHighlight: boolean; - isHighlighted: boolean; - isHovered: boolean; wrapMode: WrapMode; } @@ -33,8 +30,6 @@ export const LogEntryFieldColumn: React.FunctionComponent { const value = useMemo(() => { @@ -63,11 +58,7 @@ export const LogEntryFieldColumn: React.FunctionComponent - {formattedValue} - - ); + return {formattedValue}; }; const CommaSeparatedLi = euiStyled.li` @@ -81,15 +72,12 @@ const CommaSeparatedLi = euiStyled.li` `; interface LogEntryColumnContentProps { - isHighlighted: boolean; - isHovered: boolean; wrapMode: WrapMode; } const FieldColumnContent = euiStyled(LogEntryColumnContent)` text-overflow: ellipsis; - ${props => (props.isHovered || props.isHighlighted ? hoveredContentStyle : '')}; ${props => props.wrapMode === 'long' ? longWrappedContentStyle diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_icon_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_icon_column.tsx deleted file mode 100644 index a4099cdf5a1fb..0000000000000 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_icon_column.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonIcon } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -import { LogEntryColumnContent } from './log_entry_column'; -import { hoveredContentStyle } from './text_styles'; -import { euiStyled } from '../../../../../observability/public'; - -interface LogEntryIconColumnProps { - isHighlighted: boolean; - isHovered: boolean; -} - -export const LogEntryIconColumn: React.FunctionComponent = ({ - children, - isHighlighted, - isHovered, -}) => { - return ( - - {children} - - ); -}; - -export const LogEntryDetailsIconColumn: React.FunctionComponent void; -}> = ({ isHighlighted, isHovered, openFlyout }) => { - const label = i18n.translate('xpack.infra.logEntryItemView.viewDetailsToolTip', { - defaultMessage: 'View Details', - }); - - return ( - - {isHovered ? ( - - - - ) : null} - - ); -}; - -interface IconColumnContentProps { - isHighlighted: boolean; - isHovered: boolean; -} - -const IconColumnContent = euiStyled(LogEntryColumnContent)` - background-color: ${props => props.theme.eui.euiColorEmptyShade}; - overflow: hidden; - user-select: none; - - ${props => (props.isHovered || props.isHighlighted ? hoveredContentStyle : '')}; -`; - -// this prevents the button from influencing the line height -const AbsoluteIconButtonWrapper = euiStyled.div` - overflow: hidden; - position: absolute; -`; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx index 5ad7cba6427d1..0fe0cbdfac593 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_message_column.tsx @@ -18,7 +18,6 @@ import { import { ActiveHighlightMarker, highlightFieldValue, HighlightMarker } from './highlighting'; import { LogEntryColumnContent } from './log_entry_column'; import { - hoveredContentStyle, longWrappedContentStyle, preWrappedContentStyle, unwrappedContentStyle, @@ -30,13 +29,11 @@ interface LogEntryMessageColumnProps { columnValue: LogColumn; highlights: LogColumn[]; isActiveHighlight: boolean; - isHighlighted: boolean; - isHovered: boolean; wrapMode: WrapMode; } export const LogEntryMessageColumn = memo( - ({ columnValue, highlights, isActiveHighlight, isHighlighted, isHovered, wrapMode }) => { + ({ columnValue, highlights, isActiveHighlight, wrapMode }) => { const message = useMemo( () => isMessageColumn(columnValue) @@ -45,24 +42,16 @@ export const LogEntryMessageColumn = memo( [columnValue, highlights, isActiveHighlight] ); - return ( - - {message} - - ); + return {message}; } ); interface MessageColumnContentProps { - isHovered: boolean; - isHighlighted: boolean; wrapMode: WrapMode; } const MessageColumnContent = euiStyled(LogEntryColumnContent)` text-overflow: ellipsis; - - ${props => (props.isHovered || props.isHighlighted ? hoveredContentStyle : '')}; ${props => props.wrapMode === 'long' ? longWrappedContentStyle diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx index ce264245d385b..7d7df796d13ad 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_row.tsx @@ -17,10 +17,10 @@ import { import { TextScale } from '../../../../common/log_text_scale'; import { LogEntryColumn, LogEntryColumnWidths, iconColumnId } from './log_entry_column'; import { LogEntryFieldColumn } from './log_entry_field_column'; -import { LogEntryDetailsIconColumn } from './log_entry_icon_column'; +import { LogEntryActionsColumn } from './log_entry_actions_column'; import { LogEntryMessageColumn } from './log_entry_message_column'; import { LogEntryTimestampColumn } from './log_entry_timestamp_column'; -import { monospaceTextStyle } from './text_styles'; +import { monospaceTextStyle, hoveredContentStyle, highlightedContentStyle } from './text_styles'; import { LogEntry, LogColumn } from '../../../../common/http_api'; interface LogEntryRowProps { @@ -50,14 +50,13 @@ export const LogEntryRow = memo( wrap, }: LogEntryRowProps) => { const [isHovered, setIsHovered] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); - const setItemIsHovered = useCallback(() => { - setIsHovered(true); - }, []); + const openMenu = useCallback(() => setIsMenuOpen(true), []); + const closeMenu = useCallback(() => setIsMenuOpen(false), []); - const setItemIsNotHovered = useCallback(() => { - setIsHovered(false); - }, []); + const setItemIsHovered = useCallback(() => setIsHovered(true), []); + const setItemIsNotHovered = useCallback(() => setIsHovered(false), []); const openFlyout = useCallback(() => openFlyoutWithItem?.(logEntry.id), [ openFlyoutWithItem, @@ -105,6 +104,7 @@ export const LogEntryRow = memo( } onMouseEnter={setItemIsHovered} onMouseLeave={setItemIsNotHovered} + isHighlighted={isHighlighted} scale={scale} > {columnConfigurations.map(columnConfiguration => { @@ -119,11 +119,7 @@ export const LogEntryRow = memo( {...columnWidth} > {isTimestampColumn(column) ? ( - + ) : null} ); @@ -141,9 +137,7 @@ export const LogEntryRow = memo( ) : null} @@ -164,8 +158,6 @@ export const LogEntryRow = memo( columnValue={column} highlights={highlightsByColumnId[column.columnId] || []} isActiveHighlight={isActiveHighlight} - isHighlighted={isHighlighted} - isHovered={isHovered} wrapMode={wrap ? 'long' : 'pre-wrapped'} /> ) : null} @@ -177,10 +169,12 @@ export const LogEntryRow = memo( key="logColumn iconLogColumn iconLogColumn:details" {...columnWidths[iconColumnId]} > - @@ -190,6 +184,7 @@ export const LogEntryRow = memo( interface LogEntryRowWrapperProps { scale: TextScale; + isHighlighted?: boolean; } export const LogEntryRowWrapper = euiStyled.div.attrs(() => ({ @@ -204,4 +199,9 @@ export const LogEntryRowWrapper = euiStyled.div.attrs(() => ({ overflow: hidden; ${props => monospaceTextStyle(props.scale)}; + ${props => (props.isHighlighted ? highlightedContentStyle : '')} + + &:hover { + ${hoveredContentStyle} + } `; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx index f3ea9c81108c6..cf9c75a361b55 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/log_entry_timestamp_column.tsx @@ -4,54 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -import { darken, transparentize } from 'polished'; import React, { memo } from 'react'; -import { euiStyled, css } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../observability/public'; import { TimeFormat, useFormattedTime } from '../../formatted_time'; import { LogEntryColumnContent } from './log_entry_column'; interface LogEntryTimestampColumnProps { format?: TimeFormat; - isHighlighted: boolean; - isHovered: boolean; time: number; } export const LogEntryTimestampColumn = memo( - ({ format = 'time', isHighlighted, isHovered, time }) => { + ({ format = 'time', time }) => { const formattedTime = useFormattedTime(time, { format }); - return ( - - {formattedTime} - - ); + return {formattedTime}; } ); -const hoveredContentStyle = css` - background-color: ${props => - props.theme.darkMode - ? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight)) - : darken(0.05, props.theme.eui.euiColorHighlight)}; - border-color: ${props => - props.theme.darkMode - ? transparentize(0.7, darken(0.2, props.theme.eui.euiColorHighlight)) - : darken(0.2, props.theme.eui.euiColorHighlight)}; - color: ${props => props.theme.eui.euiColorFullShade}; -`; - -interface TimestampColumnContentProps { - isHovered: boolean; - isHighlighted: boolean; -} - -const TimestampColumnContent = euiStyled(LogEntryColumnContent)` +const TimestampColumnContent = euiStyled(LogEntryColumnContent)` color: ${props => props.theme.eui.euiColorDarkShade}; overflow: hidden; text-overflow: clip; white-space: pre; - - ${props => (props.isHovered || props.isHighlighted ? hoveredContentStyle : '')}; `; diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx index 434258343eefb..69a6abbca4b34 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/text_styles.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { darken, transparentize } from 'polished'; import React, { useMemo, useState, useCallback } from 'react'; import { euiStyled, css } from '../../../../../observability/public'; @@ -30,10 +29,11 @@ export const monospaceTextStyle = (scale: TextScale) => css` `; export const hoveredContentStyle = css` - background-color: ${props => - props.theme.darkMode - ? transparentize(0.9, darken(0.05, props.theme.eui.euiColorHighlight)) - : darken(0.05, props.theme.eui.euiColorHighlight)}; + background-color: ${props => props.theme.eui.euiFocusBackgroundColor}; +`; + +export const highlightedContentStyle = css` + background-color: ${props => props.theme.eui.euiFocusBackgroundColor}; `; export const longWrappedContentStyle = css` diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx index 023082154565c..3855706bb6d47 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_categories/sections/top_categories/category_example_message.tsx @@ -34,12 +34,7 @@ export const CategoryExampleMessage: React.FunctionComponent<{ return ( - + @@ -63,8 +56,6 @@ export const CategoryExampleMessage: React.FunctionComponent<{ highlights: [], }} highlights={noHighlights} - isHovered={false} - isHighlighted={false} isActiveHighlight={false} wrapMode="none" /> diff --git a/x-pack/plugins/ingest_manager/CHANGELOG.md b/x-pack/plugins/ingest_manager/CHANGELOG.md new file mode 100644 index 0000000000000..b336c65b7c4b9 --- /dev/null +++ b/x-pack/plugins/ingest_manager/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog +Significant or breaking changes to the Ingest Manager API will be documented in this file + +## 2020-03-30 + +### Breaking Changes +* Change EPM file path route from epm/packages/{pkgkey}/{filePath*} to epm/packages/{packageName}/{packageVersion}/{filePath*} [#61910](https://github.com/elastic/kibana/pull/61910) \ No newline at end of file diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 5bf7c910168c0..a31d38a723c2c 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -13,12 +13,13 @@ export const FLEET_API_ROOT = `${API_ROOT}/fleet`; // EPM API routes const EPM_PACKAGES_MANY = `${EPM_API_ROOT}/packages`; const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgkey}`; +const EPM_PACKAGES_FILE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`; export const EPM_API_ROUTES = { LIST_PATTERN: EPM_PACKAGES_MANY, INFO_PATTERN: EPM_PACKAGES_ONE, INSTALL_PATTERN: EPM_PACKAGES_ONE, DELETE_PATTERN: EPM_PACKAGES_ONE, - FILEPATH_PATTERN: `${EPM_PACKAGES_ONE}/{filePath*}`, + FILEPATH_PATTERN: `${EPM_PACKAGES_FILE}/{filePath*}`, CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, }; diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 8623d02e72862..48f37a4d65ac6 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -75,8 +75,8 @@ export const getFileHandler: RequestHandler { try { - const { pkgkey, filePath } = request.params; - const registryResponse = await getFile(`/package/${pkgkey}/${filePath}`); + const { pkgName, pkgVersion, filePath } = request.params; + const registryResponse = await getFile(`/package/${pkgName}/${pkgVersion}/${filePath}`); const contentType = registryResponse.headers.get('Content-Type'); const customResponseObj: CustomHttpResponseOptions = { body: registryResponse.body, diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts index 5153f9205dde7..6d5ca036aeb13 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.test.ts @@ -11,19 +11,18 @@ const tests = [ { package: { assets: [ - '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', - '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', + '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', + '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', ], - name: 'coredns', - version: '1.0.1', + path: '/package/coredns/1.0.1', }, dataset: 'log', filter: (path: string) => { return true; }, expected: [ - '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', - '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', + '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', + '/package/coredns/1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', ], }, { @@ -32,8 +31,7 @@ const tests = [ '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-plaintext.json', '/package/coredns-1.0.1/dataset/log/elasticsearch/ingest-pipeline/pipeline-json.json', ], - name: 'coredns', - version: '1.0.1', + path: '/package/coredns/1.0.1', }, // Non existant dataset dataset: 'foo', diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts index e36c2de1b4e80..d7a5c5569986e 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/assets.ts @@ -9,14 +9,16 @@ import * as Registry from '../registry'; import { cacheHas } from '../registry/cache'; // paths from RegistryPackage are routes to the assets on EPR -// e.g. `/package/nginx-1.2.0/dataset/access/fields/fields.yml` +// e.g. `/package/nginx/1.2.0/dataset/access/fields/fields.yml` // paths for ArchiveEntry are routes to the assets in the archive // e.g. `nginx-1.2.0/dataset/access/fields/fields.yml` // RegistryPackage paths have a `/package/` prefix compared to ArchiveEntry paths +// and different package and version structure const EPR_PATH_PREFIX = '/package'; function registryPathToArchivePath(registryPath: RegistryPackage['path']): string { - const archivePath = registryPath.replace(`${EPR_PATH_PREFIX}/`, ''); - return archivePath; + const path = registryPath.replace(`${EPR_PATH_PREFIX}/`, ''); + const [pkgName, pkgVersion] = path.split('/'); + return path.replace(`${pkgName}/${pkgVersion}`, `${pkgName}-${pkgVersion}`); } export function getAssets( @@ -35,7 +37,7 @@ export function getAssets( // if dataset, filter for them if (datasetName) { - const comparePath = `${EPR_PATH_PREFIX}/${packageInfo.name}-${packageInfo.version}/dataset/${datasetName}/`; + const comparePath = `${packageInfo.path}/dataset/${datasetName}/`; if (!path.includes(comparePath)) { continue; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts index 7c315f7616e1f..36a04b88bba29 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts @@ -6,7 +6,6 @@ import { Response } from 'node-fetch'; import { URL } from 'url'; -import { sortBy } from 'lodash'; import { AssetParts, AssetsGroupedByServiceByType, @@ -51,11 +50,7 @@ export async function fetchFindLatestPackage( const res = await fetchUrl(url.toString()); const searchResults = JSON.parse(res); if (searchResults.length) { - // sort by version, then get the last (most recent) - const latestPackage = sortBy(searchResults, ['version'])[ - searchResults.length - 1 - ]; - return latestPackage; + return searchResults[0]; } else { throw new Error('package not found'); } @@ -63,7 +58,8 @@ export async function fetchFindLatestPackage( export async function fetchInfo(pkgkey: string): Promise { const registryUrl = appContextService.getConfig()?.epm.registryUrl; - return fetchUrl(`${registryUrl}/package/${pkgkey}`).then(JSON.parse); + // change pkg-version to pkg/version + return fetchUrl(`${registryUrl}/package/${pkgkey.replace('-', '/')}`).then(JSON.parse); } export async function fetchFile(filePath: string): Promise { diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts index 2ca83276b0228..3ed6ee553a507 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts @@ -13,7 +13,8 @@ export const GetPackagesRequestSchema = { export const GetFileRequestSchema = { params: schema.object({ - pkgkey: schema.string(), + pkgName: schema.string(), + pkgVersion: schema.string(), filePath: schema.string(), }), }; diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 814825483d0dd..30a3350ad754e 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -90,16 +90,16 @@ export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; -export const ES_GEO_FIELD_TYPE = { - GEO_POINT: 'geo_point', - GEO_SHAPE: 'geo_shape', -}; +export enum ES_GEO_FIELD_TYPE { + GEO_POINT = 'geo_point', + GEO_SHAPE = 'geo_shape', +} -export const ES_SPATIAL_RELATIONS = { - INTERSECTS: 'INTERSECTS', - DISJOINT: 'DISJOINT', - WITHIN: 'WITHIN', -}; +export enum ES_SPATIAL_RELATIONS { + INTERSECTS = 'INTERSECTS', + DISJOINT = 'DISJOINT', + WITHIN = 'WITHIN', +} export const GEO_JSON_TYPE = { POINT: 'Point', @@ -120,11 +120,11 @@ export const EMPTY_FEATURE_COLLECTION = { features: [], }; -export const DRAW_TYPE = { - BOUNDS: 'BOUNDS', - DISTANCE: 'DISTANCE', - POLYGON: 'POLYGON', -}; +export enum DRAW_TYPE { + BOUNDS = 'BOUNDS', + DISTANCE = 'DISTANCE', + POLYGON = 'POLYGON', +} export enum AGG_TYPE { AVG = 'avg', diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts index a0102a4249a59..ca0e474491780 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts @@ -5,21 +5,14 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { Query } from './map_descriptor'; - -type Extent = { - maxLat: number; - maxLon: number; - minLat: number; - minLon: number; -}; +import { MapExtent, MapQuery } from './map_descriptor'; // Global map state passed to every layer. export type MapFilters = { - buffer: Extent; // extent with additional buffer - extent: Extent; // map viewport + buffer: MapExtent; // extent with additional buffer + extent: MapExtent; // map viewport filters: unknown[]; - query: Query; + query: MapQuery; refreshTimerLastTriggeredAt: string; timeFilters: unknown; zoom: number; @@ -29,14 +22,14 @@ export type VectorSourceRequestMeta = MapFilters & { applyGlobalQuery: boolean; fieldNames: string[]; geogridPrecision: number; - sourceQuery: Query; + sourceQuery: MapQuery; sourceMeta: unknown; }; export type VectorStyleRequestMeta = MapFilters & { dynamicStyleFields: string[]; isTimeAware: boolean; - sourceQuery: Query; + sourceQuery: MapQuery; timeFilters: unknown; }; diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index 570398e37c5d4..b2a4c6b85a856 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -3,11 +3,67 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - /* eslint-disable @typescript-eslint/consistent-type-definitions */ -export type Query = { - language: string; - query: string; +import { Query } from '../../../../../src/plugins/data/public'; +import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; + +export type MapExtent = { + maxLat: number; + maxLon: number; + minLat: number; + minLon: number; +}; + +export type MapQuery = Query & { queryLastTriggeredAt: string; }; + +export type MapRefreshConfig = { + isPaused: boolean; + interval: number; +}; + +export type MapCenter = { + lat: number; + lon: number; +}; + +export type MapCenterAndZoom = MapCenter & { + zoom: number; +}; + +// TODO replace with map_descriptors.MapExtent. Both define the same thing but with different casing +type MapBounds = { + min_lon: number; + max_lon: number; + min_lat: number; + max_lat: number; +}; + +export type Goto = { + bounds?: MapBounds; + center?: MapCenterAndZoom; +}; + +export type TooltipFeature = { + id: number; + layerId: string; +}; + +export type TooltipState = { + features: TooltipFeature[]; + id: string; + isLocked: boolean; + location: number[]; // 0 index is lon, 1 index is lat +}; + +export type DrawState = { + drawType: DRAW_TYPE; + filterLabel?: string; // point radius filter alias + geoFieldName?: string; + geoFieldType?: ES_GEO_FIELD_TYPE; + geometryLabel?: string; + indexPatternId?: string; + relation?: ES_SPATIAL_RELATIONS; +}; diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts new file mode 100644 index 0000000000000..30271d4d5fa8b --- /dev/null +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { + DrawState, + Goto, + LayerDescriptor, + MapCenter, + MapExtent, + MapQuery, + MapRefreshConfig, + TooltipState, +} from '../../common/descriptor_types'; +import { Filter, TimeRange } from '../../../../../src/plugins/data/public'; + +export type MapContext = { + zoom?: number; + center?: MapCenter; + scrollZoom: boolean; + extent?: MapExtent; + mouseCoordinates?: { + lat: number; + lon: number; + }; + timeFilters?: TimeRange; + query?: MapQuery; + filters: Filter[]; + refreshConfig?: MapRefreshConfig; + refreshTimerLastTriggeredAt?: string; + drawState?: DrawState; + disableInteractive: boolean; + disableTooltipControl: boolean; + hideToolbarOverlay: boolean; + hideLayerControl: boolean; + hideViewControl: boolean; +}; + +export type MapState = { + ready: boolean; + mapInitError?: string | null; + goto?: Goto | null; + openTooltips: TooltipState[]; + mapState: MapContext; + selectedLayerId: string | null; + __transientLayerId: string | null; + layerList: LayerDescriptor[]; + waitingForMapReadyLayerList: LayerDescriptor[]; +}; diff --git a/x-pack/plugins/maps/public/reducers/store.d.ts b/x-pack/plugins/maps/public/reducers/store.d.ts index ebed396e20399..72713f943d6a6 100644 --- a/x-pack/plugins/maps/public/reducers/store.d.ts +++ b/x-pack/plugins/maps/public/reducers/store.d.ts @@ -5,7 +5,14 @@ */ import { Store } from 'redux'; +import { MapState } from './map'; +import { MapUiState } from './ui'; -export type MapStore = Store; +export interface MapStoreState { + ui: MapUiState; + map: MapState; +} + +export type MapStore = Store; export function createMapStore(): MapStore; diff --git a/x-pack/plugins/maps/public/reducers/ui.js b/x-pack/plugins/maps/public/reducers/ui.ts similarity index 76% rename from x-pack/plugins/maps/public/reducers/ui.js rename to x-pack/plugins/maps/public/reducers/ui.ts index 287e1f8dd3dda..7429545ec0e46 100644 --- a/x-pack/plugins/maps/public/reducers/ui.js +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ import { UPDATE_FLYOUT, @@ -15,19 +16,30 @@ import { SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, UPDATE_INDEXING_STAGE, + // @ts-ignore } from '../actions/ui_actions'; -export const FLYOUT_STATE = { - NONE: 'NONE', - LAYER_PANEL: 'LAYER_PANEL', - ADD_LAYER_WIZARD: 'ADD_LAYER_WIZARD', -}; +export enum FLYOUT_STATE { + NONE = 'NONE', + LAYER_PANEL = 'LAYER_PANEL', + ADD_LAYER_WIZARD = 'ADD_LAYER_WIZARD', +} + +export enum INDEXING_STAGE { + READY = 'READY', + TRIGGERED = 'TRIGGERED', + SUCCESS = 'SUCCESS', + ERROR = 'ERROR', +} -export const INDEXING_STAGE = { - READY: 'READY', - TRIGGERED: 'TRIGGERED', - SUCCESS: 'SUCCESS', - ERROR: 'ERROR', +export type MapUiState = { + flyoutDisplay: FLYOUT_STATE; + isFullScreen: boolean; + isReadOnly: boolean; + isLayerTOCOpen: boolean; + isSetViewOpen: boolean; + openTOCDetails: string[]; + importIndexingStage: INDEXING_STAGE | null; }; export const DEFAULT_IS_LAYER_TOC_OPEN = true; @@ -45,7 +57,7 @@ const INITIAL_STATE = { }; // Reducer -export function ui(state = INITIAL_STATE, action) { +export function ui(state: MapUiState = INITIAL_STATE, action: any) { switch (action.type) { case UPDATE_FLYOUT: return { ...state, flyoutDisplay: action.display }; diff --git a/x-pack/plugins/ml/common/util/errors.ts b/x-pack/plugins/ml/common/util/errors.ts new file mode 100644 index 0000000000000..4446624bf2e7f --- /dev/null +++ b/x-pack/plugins/ml/common/util/errors.ts @@ -0,0 +1,19 @@ +/* + * 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 { isErrorResponse } from '../types/errors'; + +export function getErrorMessage(error: any) { + if (isErrorResponse(error)) { + return `${error.body.error}: ${error.body.message}`; + } + + if (typeof error === 'object' && typeof error.message === 'string') { + return error.message; + } + + return JSON.stringify(error); +} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 95a8dfbb308f8..d77f19c0df79d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -11,7 +11,7 @@ import { Subscription } from 'rxjs'; import { cloneDeep } from 'lodash'; import { ml } from '../../services/ml_api_service'; import { Dictionary } from '../../../../common/types/common'; -import { getErrorMessage } from '../pages/analytics_management/hooks/use_create_analytics_form'; +import { getErrorMessage } from '../../../../common/util/errors'; import { SavedSearchQuery } from '../../contexts/ml'; import { SortDirection } from '../../components/ml_in_memory_table'; @@ -62,6 +62,9 @@ export interface LoadExploreDataArg { export const SEARCH_SIZE = 1000; +export const TRAINING_PERCENT_MIN = 1; +export const TRAINING_PERCENT_MAX = 100; + export const defaultSearchQuery = { match_all: {}, }; @@ -172,6 +175,19 @@ export const getDependentVar = (analysis: AnalysisConfig) => { return depVar; }; +export const getTrainingPercent = (analysis: AnalysisConfig) => { + let trainingPercent; + + if (isRegressionAnalysis(analysis)) { + trainingPercent = analysis.regression.training_percent; + } + + if (isClassificationAnalysis(analysis)) { + trainingPercent = analysis.classification.training_percent; + } + return trainingPercent; +}; + export const getPredictionFieldName = (analysis: AnalysisConfig) => { // If undefined will be defaulted to dependent_variable when config is created let predictionFieldName; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index 263d43ceb2630..41430b163c029 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -18,6 +18,7 @@ import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -47,11 +48,11 @@ const jobCapsErrorTitle = i18n.translate( interface Props { jobId: string; - jobStatus: DATA_FRAME_TASK_STATE; } -export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { +export const ClassificationExploration: FC = ({ jobId }) => { const [jobConfig, setJobConfig] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); @@ -65,6 +66,15 @@ export const ClassificationExploration: FC = ({ jobId, jobStatus }) => { setIsLoadingJobConfig(true); try { const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx index 1c5563bdb4f83..91dae49ba5c49 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/evaluate_panel.tsx @@ -50,10 +50,47 @@ const defaultPanelWidth = 500; interface Props { jobConfig: DataFrameAnalyticsConfig; - jobStatus: DATA_FRAME_TASK_STATE; + jobStatus?: DATA_FRAME_TASK_STATE; searchQuery: ResultsSearchQuery; } +enum SUBSET_TITLE { + TRAINING = 'training', + TESTING = 'testing', + ENTIRE = 'entire', +} + +const entireDatasetHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixEntireHelpText', + { + defaultMessage: 'Normalized confusion matrix for entire dataset', + } +); + +const testingDatasetHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTestingHelpText', + { + defaultMessage: 'Normalized confusion matrix for testing dataset', + } +); + +const trainingDatasetHelpText = i18n.translate( + 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixTrainingHelpText', + { + defaultMessage: 'Normalized confusion matrix for training dataset', + } +); + +function getHelpText(dataSubsetTitle: string) { + let helpText = entireDatasetHelpText; + if (dataSubsetTitle === SUBSET_TITLE.TESTING) { + helpText = testingDatasetHelpText; + } else if (dataSubsetTitle === SUBSET_TITLE.TRAINING) { + helpText = trainingDatasetHelpText; + } + return helpText; +} + export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) => { const { services: { docLinks }, @@ -66,6 +103,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) const [popoverContents, setPopoverContents] = useState([]); const [docsCount, setDocsCount] = useState(null); const [error, setError] = useState(null); + const [dataSubsetTitle, setDataSubsetTitle] = useState(SUBSET_TITLE.ENTIRE); const [panelWidth, setPanelWidth] = useState(defaultPanelWidth); // Column visibility const [visibleColumns, setVisibleColumns] = useState(() => @@ -197,6 +235,18 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) hasIsTrainingClause[0] && hasIsTrainingClause[0].match[`${resultsField}.is_training`]; + const noTrainingQuery = isTrainingClause === false || isTrainingClause === undefined; + + if (noTrainingQuery) { + setDataSubsetTitle(SUBSET_TITLE.ENTIRE); + } else { + setDataSubsetTitle( + isTrainingClause && isTrainingClause.query === 'true' + ? SUBSET_TITLE.TRAINING + : SUBSET_TITLE.TESTING + ); + } + loadData({ isTrainingClause }); }, [JSON.stringify(searchQuery)]); @@ -268,9 +318,11 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
- - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} @@ -302,14 +354,7 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - - {i18n.translate( - 'xpack.ml.dataframe.analytics.classificationExploration.confusionMatrixHelpText', - { - defaultMessage: 'Normalized confusion matrix', - } - )} - + {getHelpText(dataSubsetTitle)} >; } @@ -381,9 +381,11 @@ export const ResultsTable: FC = React.memo( - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} = React.memo( - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx index 030447873f6a5..7cdd15e49bd14 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.test.tsx @@ -6,7 +6,6 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { MlContext } from '../../../../../contexts/ml'; import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; @@ -22,7 +21,7 @@ describe('Data Frame Analytics: ', () => { test('Minimal initialization', () => { const wrapper = shallow( - + ); // Without the jobConfig being loaded, the component will just return empty. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx index 214bc01c6a2ef..d686c605f1912 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/outlier_exploration.tsx @@ -27,7 +27,6 @@ import { import { sortColumns, INDEX_STATUS, defaultSearchQuery } from '../../../../common'; -import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/columns'; import { useExploreData, TableItem } from '../../hooks/use_explore_data'; @@ -50,7 +49,6 @@ const ExplorationTitle: FC<{ jobId: string }> = ({ jobId }) => ( interface ExplorationProps { jobId: string; - jobStatus: DATA_FRAME_TASK_STATE; } const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => { @@ -63,11 +61,12 @@ const getFeatureCount = (resultsField: string, tableItems: TableItem[] = []) => ).length; }; -export const OutlierExploration: FC = React.memo(({ jobId, jobStatus }) => { +export const OutlierExploration: FC = React.memo(({ jobId }) => { const { errorMessage, indexPattern, jobConfig, + jobStatus, pagination, searchQuery, selectedFields, @@ -173,9 +172,11 @@ export const OutlierExploration: FC = React.memo(({ jobId, job - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )}
{(columns.length > 0 || searchQuery !== defaultSearchQuery) && ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx index 74937bf761285..9f235ae6c45c0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_panel.tsx @@ -39,7 +39,7 @@ import { interface Props { jobConfig: DataFrameAnalyticsConfig; - jobStatus: DATA_FRAME_TASK_STATE; + jobStatus?: DATA_FRAME_TASK_STATE; searchQuery: ResultsSearchQuery; } @@ -248,9 +248,11 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery }) - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx index 3dfd95a27f8a7..4f3c4048d40d5 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/regression_exploration.tsx @@ -18,6 +18,7 @@ import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/common/index_patterns'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; import { useMlContext } from '../../../../../contexts/ml'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; export const ExplorationTitle: React.FC<{ jobId: string }> = ({ jobId }) => ( @@ -47,11 +48,11 @@ const jobCapsErrorTitle = i18n.translate( interface Props { jobId: string; - jobStatus: DATA_FRAME_TASK_STATE; } -export const RegressionExploration: FC = ({ jobId, jobStatus }) => { +export const RegressionExploration: FC = ({ jobId }) => { const [jobConfig, setJobConfig] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); const [isLoadingJobConfig, setIsLoadingJobConfig] = useState(false); const [isInitialized, setIsInitialized] = useState(false); const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState(undefined); @@ -65,6 +66,15 @@ export const RegressionExploration: FC = ({ jobId, jobStatus }) => { setIsLoadingJobConfig(true); try { const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx index 7a6b2b23ba7a3..b896c34a582f7 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/results_table.tsx @@ -86,7 +86,7 @@ const showingFirstDocs = i18n.translate( interface Props { jobConfig: DataFrameAnalyticsConfig; - jobStatus: DATA_FRAME_TASK_STATE; + jobStatus?: DATA_FRAME_TASK_STATE; setEvaluateSearchQuery: React.Dispatch>; } @@ -381,9 +381,11 @@ export const ResultsTable: FC = React.memo( - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} = React.memo( - - {getTaskStateBadge(jobStatus)} - + {jobStatus !== undefined && ( + + {getTaskStateBadge(jobStatus)} + + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts index 6ad0a1822e490..d637057a4430d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/hooks/use_explore_data/use_explore_data.ts @@ -19,6 +19,7 @@ import { newJobCapsService } from '../../../../../services/new_job_capabilities_ import { getIndexPatternIdFromName } from '../../../../../util/index_utils'; import { getNestedProperty } from '../../../../../util/object_utils'; import { useMlContext } from '../../../../../contexts/ml'; +import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics'; import { getDefaultSelectableFields, @@ -31,6 +32,7 @@ import { import { isKeywordAndTextType } from '../../../../common/fields'; import { getOutlierScoreFieldName } from './common'; +import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; export type TableItem = Record; @@ -40,6 +42,7 @@ interface UseExploreDataReturnType { errorMessage: string; indexPattern: IndexPattern | undefined; jobConfig: DataFrameAnalyticsConfig | undefined; + jobStatus: DATA_FRAME_TASK_STATE | undefined; pagination: Pagination; searchQuery: SavedSearchQuery; selectedFields: EsFieldName[]; @@ -74,6 +77,7 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => { const [indexPattern, setIndexPattern] = useState(undefined); const [jobConfig, setJobConfig] = useState(undefined); + const [jobStatus, setJobStatus] = useState(undefined); const [errorMessage, setErrorMessage] = useState(''); const [status, setStatus] = useState(INDEX_STATUS.UNUSED); @@ -90,6 +94,15 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => { useEffect(() => { (async function() { const analyticsConfigs = await ml.dataFrameAnalytics.getDataFrameAnalytics(jobId); + const analyticsStats = await ml.dataFrameAnalytics.getDataFrameAnalyticsStats(jobId); + const stats = isGetDataFrameAnalyticsStatsResponseOk(analyticsStats) + ? analyticsStats.data_frame_analytics[0] + : undefined; + + if (stats !== undefined && stats.state) { + setJobStatus(stats.state); + } + if ( Array.isArray(analyticsConfigs.data_frame_analytics) && analyticsConfigs.data_frame_analytics.length > 0 @@ -215,6 +228,7 @@ export const useExploreData = (jobId: string): UseExploreDataReturnType => { errorMessage, indexPattern, jobConfig, + jobStatus, pagination, rowCount, searchQuery, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index efbebc1564bf9..c8349084dbda8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -27,13 +27,11 @@ import { RegressionExploration } from './components/regression_exploration'; import { ClassificationExploration } from './components/classification_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics'; -import { DATA_FRAME_TASK_STATE } from '../analytics_management/components/analytics_list/common'; export const Page: FC<{ jobId: string; analysisType: ANALYSIS_CONFIG_TYPE; - jobStatus: DATA_FRAME_TASK_STATE; -}> = ({ jobId, analysisType, jobStatus }) => ( +}> = ({ jobId, analysisType }) => ( @@ -68,13 +66,13 @@ export const Page: FC<{ {analysisType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && ( - + )} {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && ( - + )} {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( - + )} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx index 425e3bc903d04..4e19df9ae22a8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/actions.tsx @@ -33,13 +33,12 @@ export const AnalyticsViewAction = { isPrimary: true, render: (item: DataFrameAnalyticsListRow) => { const analysisType = getAnalysisType(item.config.analysis); - const jobStatus = item.stats.state; const isDisabled = !isRegressionAnalysis(item.config.analysis) && !isOutlierAnalysis(item.config.analysis) && !isClassificationAnalysis(item.config.analysis); - const url = getResultsUrl(item.id, analysisType, jobStatus); + const url = getResultsUrl(item.id, analysisType); return ( = ({ actions, state }) => { - const { - resetAdvancedEditorMessages, - setAdvancedEditorRawString, - setFormState, - setJobConfig, - } = actions; + const { setAdvancedEditorRawString, setFormState } = actions; const { advancedEditorMessages, advancedEditorRawString, isJobCreated, requestMessages } = state; @@ -45,12 +39,6 @@ export const CreateAnalyticsAdvancedEditor: FC = ({ ac const onChange = (str: string) => { setAdvancedEditorRawString(str); - try { - const resultJobConfig = JSON.parse(collapseLiteralStrings(str)); - setJobConfig(resultJobConfig); - } catch (e) { - resetAdvancedEditorMessages(); - } }; // Temp effect to close the context menu popover on Clone button click diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx index 32384e1949d0a..b0f13e398cc50 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_flyout/create_analytics_flyout.tsx @@ -26,7 +26,14 @@ export const CreateAnalyticsFlyout: FC = ({ state, }) => { const { closeModal, createAnalyticsJob, startAnalyticsJob } = actions; - const { isJobCreated, isJobStarted, isModalButtonDisabled, isValid, cloneJob } = state; + const { + isJobCreated, + isJobStarted, + isModalButtonDisabled, + isValid, + isAdvancedEditorValidJson, + cloneJob, + } = state; const headerText = !!cloneJob ? i18n.translate('xpack.ml.dataframe.analytics.clone.flyoutHeaderTitle', { @@ -61,7 +68,7 @@ export const CreateAnalyticsFlyout: FC = ({ {!isJobCreated && !isJobStarted && ( = ({ actions, sta })} > setFormState({ trainingPercent: e.target.value })} + onChange={e => setFormState({ trainingPercent: +e.target.value })} data-test-subj="mlAnalyticsCreateJobFlyoutTrainingPercentSlider" /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts index 0eb3d7180c200..9df0b542f50a1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/index.ts @@ -4,8 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { - useCreateAnalyticsForm, - CreateAnalyticsFormProps, - getErrorMessage, -} from './use_create_analytics_form'; +export { useCreateAnalyticsForm, CreateAnalyticsFormProps } from './use_create_analytics_form'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts index 8112a0fdb9e29..c40ab31f6615f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.test.ts @@ -16,9 +16,11 @@ type SourceIndex = DataFrameAnalyticsConfig['source']['index']; const getMockState = ({ index, + trainingPercent = 75, modelMemoryLimit = '100mb', }: { index: SourceIndex; + trainingPercent?: number; modelMemoryLimit?: string; }) => merge(getInitialState(), { @@ -31,7 +33,9 @@ const getMockState = ({ jobConfig: { source: { index }, dest: { index: 'the-destination-index' }, - analysis: {}, + analysis: { + classification: { dependent_variable: 'the-variable', training_percent: trainingPercent }, + }, model_memory_limit: modelMemoryLimit, }, }); @@ -151,6 +155,24 @@ describe('useCreateAnalyticsForm', () => { .isValid ).toBe(false); }); + + test('validateAdvancedEditor(): check training percent validation', () => { + // valid training_percent value + expect( + validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 75 })) + .isValid + ).toBe(true); + // invalid training_percent numeric value + expect( + validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 102 })) + .isValid + ).toBe(false); + // invalid training_percent numeric value if 0 + expect( + validateAdvancedEditor(getMockState({ index: 'the-source-index', trainingPercent: 0 })) + .isValid + ).toBe(false); + }); }); describe('validateMinMML', () => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index d045749a1a0dd..28d8afbcd88cc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -11,6 +11,8 @@ import numeral from '@elastic/numeral'; import { isEmpty } from 'lodash'; import { isValidIndexName } from '../../../../../../../common/util/es_utils'; +import { collapseLiteralStrings } from '../../../../../../../../../../src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools'; + import { Action, ACTION } from './actions'; import { getInitialState, getJobConfigFromFormState, State } from './state'; import { @@ -29,9 +31,12 @@ import { } from '../../../../../../../common/constants/validation'; import { getDependentVar, + getTrainingPercent, isRegressionAnalysis, isClassificationAnalysis, ANALYSIS_CONFIG_TYPE, + TRAINING_PERCENT_MIN, + TRAINING_PERCENT_MAX, } from '../../../../common/analytics'; import { indexPatterns } from '../../../../../../../../../../src/plugins/data/public'; @@ -141,6 +146,7 @@ export const validateAdvancedEditor = (state: State): State => { let dependentVariableEmpty = false; let excludesValid = true; + let trainingPercentValid = true; if ( jobConfig.analysis === undefined && @@ -169,6 +175,30 @@ export const validateAdvancedEditor = (state: State): State => { message: '', }); } + + const trainingPercent = getTrainingPercent(jobConfig.analysis); + if ( + trainingPercent !== undefined && + (isNaN(trainingPercent) || + trainingPercent < TRAINING_PERCENT_MIN || + trainingPercent > TRAINING_PERCENT_MAX) + ) { + trainingPercentValid = false; + + state.advancedEditorMessages.push({ + error: i18n.translate( + 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.trainingPercentInvalid', + { + defaultMessage: 'The training percent must be a value between {min} and {max}.', + values: { + min: TRAINING_PERCENT_MIN, + max: TRAINING_PERCENT_MAX, + }, + } + ), + message: '', + }); + } } if (sourceIndexNameEmpty) { @@ -249,6 +279,7 @@ export const validateAdvancedEditor = (state: State): State => { state.isValid = maxDistinctValuesError === undefined && excludesValid && + trainingPercentValid && state.form.modelMemoryLimitUnitValid && !jobIdEmpty && jobIdValid && @@ -365,7 +396,23 @@ export function reducer(state: State, action: Action): State { return getInitialState(); case ACTION.SET_ADVANCED_EDITOR_RAW_STRING: - return { ...state, advancedEditorRawString: action.advancedEditorRawString }; + let resultJobConfig; + try { + resultJobConfig = JSON.parse(collapseLiteralStrings(action.advancedEditorRawString)); + } catch (e) { + return { + ...state, + advancedEditorRawString: action.advancedEditorRawString, + isAdvancedEditorValidJson: false, + advancedEditorMessages: [], + }; + } + + return { + ...validateAdvancedEditor({ ...state, jobConfig: resultJobConfig }), + advancedEditorRawString: action.advancedEditorRawString, + isAdvancedEditorValidJson: true, + }; case ACTION.SET_FORM_STATE: const newFormState = { ...state.form, ...action.payload }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 719bb6c5b07c7..fe741fe9a92d4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -82,6 +82,7 @@ export interface State { indexNames: EsIndexName[]; indexPatternsMap: SourceIndexMap; isAdvancedEditorEnabled: boolean; + isAdvancedEditorValidJson: boolean; isJobCreated: boolean; isJobStarted: boolean; isModalButtonDisabled: boolean; @@ -140,6 +141,7 @@ export const getInitialState = (): State => ({ indexNames: [], indexPatternsMap: {}, isAdvancedEditorEnabled: false, + isAdvancedEditorValidJson: true, isJobCreated: false, isJobStarted: false, isModalVisible: false, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx index 1a248f8559ffa..182e50a5d74d1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx @@ -10,7 +10,8 @@ import { mountHook } from 'test_utils/enzyme_helpers'; import { MlContext } from '../../../../../contexts/ml'; import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; -import { getErrorMessage, useCreateAnalyticsForm } from './use_create_analytics_form'; +import { useCreateAnalyticsForm } from './use_create_analytics_form'; +import { getErrorMessage } from '../../../../../../../common/util/errors'; const getMountedHook = () => mountHook( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 86c43b232738c..34f1d04264900 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -9,7 +9,7 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; import { SimpleSavedObject } from 'kibana/public'; -import { isErrorResponse } from '../../../../../../../common/types/errors'; +import { getErrorMessage } from '../../../../../../../common/util/errors'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; @@ -41,18 +41,6 @@ export interface CreateAnalyticsFormProps { state: State; } -export function getErrorMessage(error: any) { - if (isErrorResponse(error)) { - return `${error.body.error}: ${error.body.message}`; - } - - if (typeof error === 'object' && typeof error.message === 'string') { - return error.message; - } - - return JSON.stringify(error); -} - export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { const mlContext = useMlContext(); const [state, dispatch] = useReducer(reducer, getInitialState()); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 29c79458fe431..9066e41fb8f23 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -366,7 +366,7 @@ export class EditJobFlyoutUI extends Component { diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 37b9fe5e1f2d0..1f2a57f999775 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -388,17 +388,23 @@ function getUrlVars(url) { } export function getSelectedJobIdFromUrl(url) { - if (typeof url === 'string' && url.includes('mlManagement') && url.includes('jobId')) { - const urlParams = getUrlVars(url); - const decodedJson = rison.decode(urlParams.mlManagement); - return decodedJson.jobId; + if (typeof url === 'string') { + url = decodeURIComponent(url); + if (url.includes('mlManagement') && url.includes('jobId')) { + const urlParams = getUrlVars(url); + const decodedJson = rison.decode(urlParams.mlManagement); + return decodedJson.jobId; + } } } export function clearSelectedJobIdFromUrl(url) { - if (typeof url === 'string' && url.includes('mlManagement') && url.includes('jobId')) { - const urlParams = getUrlVars(url); - const clearedParams = `ml#/jobs?_g=${urlParams._g}`; - window.history.replaceState({}, document.title, clearedParams); + if (typeof url === 'string') { + url = decodeURIComponent(url); + if (url.includes('mlManagement') && url.includes('jobId')) { + const urlParams = getUrlVars(url); + const clearedParams = `ml#/jobs?_g=${urlParams._g}`; + window.history.replaceState({}, document.title, clearedParams); + } } } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx index c24c018f50d75..2e7cc9c413a25 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { JobRunner } from '../../../../../common/job_runner'; import { useMlKibana } from '../../../../../../../contexts/kibana'; +import { getErrorMessage } from '../../../../../../../../../common/util/errors'; // @ts-ignore import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout/index'; @@ -69,7 +70,7 @@ export const PostSaveOptions: FC = ({ jobRunner }) => { defaultMessage: `Error starting job`, } ), - text: error.message, + text: getErrorMessage(error), }); } } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 75994b5358899..d8cd0f5e4f1f0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -24,6 +24,7 @@ import { mlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; import { DatafeedPreviewFlyout } from '../common/datafeed_preview_flyout'; import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; +import { getErrorMessage } from '../../../../../../../common/util/errors'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; import { DatafeedDetails } from './components/datafeed_details'; @@ -75,7 +76,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => title: i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { defaultMessage: `Job creation error`, }), - text: error.message, + text: getErrorMessage(error), }); setCreatingJob(false); } @@ -94,7 +95,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => title: i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { defaultMessage: `Job creation error`, }), - text: error.message, + text: getErrorMessage(error), }); setCreatingJob(false); } diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index e00ff0333bb73..2dde5426ec9a0 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -13,7 +13,6 @@ import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; -import { DATA_FRAME_TASK_STATE } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { ML_BREADCRUMB } from '../../breadcrumbs'; const breadcrumbs = [ @@ -46,11 +45,10 @@ const PageWrapper: FC = ({ location, deps }) => { } const jobId: string = globalState.ml.jobId; const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType; - const jobStatus: DATA_FRAME_TASK_STATE = globalState.ml.jobStatus; return ( - + ); }; diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 0454d40e78923..bbfec49ac1388 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -169,12 +169,12 @@ class JobService { function error(err) { console.log('jobService error getting list of jobs:', err); - msgs.error( + msgs.notify.error( i18n.translate('xpack.ml.jobService.jobsListCouldNotBeRetrievedErrorMessage', { defaultMessage: 'Jobs list could not be retrieved', }) ); - msgs.error('', err); + msgs.notify.error('', err); reject({ jobs, err }); } }); @@ -256,12 +256,12 @@ class JobService { function error(err) { console.log('JobService error getting list of jobs:', err); - msgs.error( + msgs.notify.error( i18n.translate('xpack.ml.jobService.jobsListCouldNotBeRetrievedErrorMessage', { defaultMessage: 'Jobs list could not be retrieved', }) ); - msgs.error('', err); + msgs.notify.error('', err); reject({ jobs, err }); } }); @@ -302,12 +302,12 @@ class JobService { function error(err) { console.log('loadDatafeeds error getting list of datafeeds:', err); - msgs.error( + msgs.notify.error( i18n.translate('xpack.ml.jobService.datafeedsListCouldNotBeRetrievedErrorMessage', { defaultMessage: 'datafeeds list could not be retrieved', }) ); - msgs.error('', err); + msgs.notify.error('', err); reject({ jobs, err }); } }); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js index f06812b2a9128..50777485903d2 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js @@ -7,6 +7,7 @@ import { getToastNotifications } from '../../../util/dependency_cache'; import { ml } from '../../../services/ml_api_service'; import { i18n } from '@kbn/i18n'; +import { getErrorMessage } from '../../../../../common/util/errors'; export async function deleteCalendars(calendarsToDelete, callback) { if (calendarsToDelete === undefined || calendarsToDelete.length === 0) { @@ -36,17 +37,18 @@ export async function deleteCalendars(calendarsToDelete, callback) { await ml.deleteCalendar({ calendarId }); } catch (error) { console.log('Error deleting calendar:', error); - const errorMessage = i18n.translate( - 'xpack.ml.calendarsList.deleteCalendars.deletingCalendarErrorMessage', - { - defaultMessage: 'An error occurred deleting calendar {calendarId}{errorMessage}', - values: { - calendarId: calendar.calendar_id, - errorMessage: error.message ? ` : ${error.message}` : '', - }, - } - ); - toastNotifications.addDanger(errorMessage); + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.ml.calendarsList.deleteCalendars.deletingCalendarErrorMessage', + { + defaultMessage: 'An error occurred deleting calendar {calendarId}', + values: { + calendarId: calendar.calendar_id, + }, + } + ), + text: getErrorMessage(error), + }); } } diff --git a/x-pack/plugins/ml/public/application/util/ml_error.js b/x-pack/plugins/ml/public/application/util/ml_error.ts similarity index 86% rename from x-pack/plugins/ml/public/application/util/ml_error.js rename to x-pack/plugins/ml/public/application/util/ml_error.ts index c970b4296844f..2a0280404c189 100644 --- a/x-pack/plugins/ml/public/application/util/ml_error.js +++ b/x-pack/plugins/ml/public/application/util/ml_error.ts @@ -7,12 +7,14 @@ import { KbnError } from '../../../../../../src/plugins/kibana_utils/public'; export class MLRequestFailure extends KbnError { + origError: any; + resp: any; // takes an Error object and and optional response object // if error is falsy (null) the response object will be used // notify will show the full expandable stack trace of the response if a response object is used and no error is passed in. - constructor(error, resp) { + constructor(error: any, resp: any) { error = error || {}; - super(error.message || JSON.stringify(resp), MLRequestFailure); + super(error.message || JSON.stringify(resp)); this.origError = error; this.resp = typeof resp === 'string' ? JSON.parse(resp) : resp; diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 674c3886c12f8..7d3ef116e67ab 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -11,6 +11,7 @@ import { IScopedClusterClient, Logger, PluginInitializerContext, + ICustomClusterClient, } from 'kibana/server'; import { PluginsSetup, RouteInitialization } from './types'; import { PLUGIN_ID, PLUGIN_ICON } from '../common/constants/app'; @@ -49,7 +50,9 @@ declare module 'kibana/server' { } } -export type MlPluginSetup = SharedServices; +export interface MlPluginSetup extends SharedServices { + mlClient: ICustomClusterClient; +} export type MlPluginStart = void; export class MlServerPlugin implements Plugin { @@ -135,7 +138,10 @@ export class MlServerPlugin implements Plugin - @@ -170,6 +174,7 @@ exports[`LoginForm renders as expected 1`] = ` fullWidth={false} hasChildLabel={true} hasEmptyLabelSpace={false} + isInvalid={false} label={ - diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx index a028eb1ba4b70..01f5c40a69aeb 100644 --- a/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/login_form.tsx @@ -9,6 +9,7 @@ import ReactMarkdown from 'react-markdown'; import { EuiButton, EuiCallOut, + EuiFieldPassword, EuiFieldText, EuiFormRow, EuiPanel, @@ -18,6 +19,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { HttpStart, IHttpFetchError, NotificationsStart } from 'src/core/public'; +import { LoginValidator, LoginValidationResult } from './validate_login'; import { parseNext } from '../../../../../common/parse_next'; import { LoginSelector } from '../../../../../common/login_state'; @@ -40,6 +42,7 @@ interface State { message: | { type: MessageType.None } | { type: MessageType.Danger | MessageType.Info; content: string }; + formError: LoginValidationResult | null; } enum LoadingStateType { @@ -55,14 +58,21 @@ enum MessageType { } export class LoginForm extends Component { - public state: State = { - loadingState: { type: LoadingStateType.None }, - username: '', - password: '', - message: this.props.infoMessage - ? { type: MessageType.Info, content: this.props.infoMessage } - : { type: MessageType.None }, - }; + private readonly validator: LoginValidator; + + constructor(props: Props) { + super(props); + this.validator = new LoginValidator({ shouldValidate: false }); + this.state = { + loadingState: { type: LoadingStateType.None }, + username: '', + password: '', + message: this.props.infoMessage + ? { type: MessageType.Info, content: this.props.infoMessage } + : { type: MessageType.None }, + formError: null, + }; + } public render() { return ( @@ -90,6 +100,7 @@ export class LoginForm extends Component { defaultMessage="Username" /> } + {...this.validator.validateUsername(this.state.username)} > { defaultMessage="Password" /> } + {...this.validator.validatePassword(this.state.password)} > - { } } - private isFormValid = () => { - const { username, password } = this.state; - - return username && password; - }; - private onUsernameChange = (e: ChangeEvent) => { this.setState({ username: e.target.value, @@ -271,8 +276,15 @@ export class LoginForm extends Component { ) => { e.preventDefault(); - if (!this.isFormValid()) { + this.validator.enableValidation(); + + const { username, password } = this.state; + const result = this.validator.validateForLogin(username, password); + if (result.isInvalid) { + this.setState({ formError: result }); return; + } else { + this.setState({ formError: null }); } this.setState({ @@ -281,7 +293,6 @@ export class LoginForm extends Component { }); const { http } = this.props; - const { username, password } = this.state; try { await http.post('/internal/security/login', { body: JSON.stringify({ username, password }) }); diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.test.ts b/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.test.ts new file mode 100644 index 0000000000000..6cd582bbcb4c0 --- /dev/null +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.test.ts @@ -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 { LoginValidator, LoginValidationResult } from './validate_login'; + +function expectValid(result: LoginValidationResult) { + expect(result.isInvalid).toBe(false); +} + +function expectInvalid(result: LoginValidationResult) { + expect(result.isInvalid).toBe(true); +} + +describe('LoginValidator', () => { + describe('#validateUsername', () => { + it(`returns 'valid' if validation is disabled`, () => { + expectValid(new LoginValidator().validateUsername('')); + }); + + it(`returns 'invalid' if username is missing`, () => { + expectInvalid(new LoginValidator({ shouldValidate: true }).validateUsername('')); + }); + + it(`returns 'valid' for correct usernames`, () => { + expectValid(new LoginValidator({ shouldValidate: true }).validateUsername('u')); + }); + }); + + describe('#validatePassword', () => { + it(`returns 'valid' if validation is disabled`, () => { + expectValid(new LoginValidator().validatePassword('')); + }); + + it(`returns 'invalid' if password is missing`, () => { + expectInvalid(new LoginValidator({ shouldValidate: true }).validatePassword('')); + }); + + it(`returns 'valid' for correct passwords`, () => { + expectValid(new LoginValidator({ shouldValidate: true }).validatePassword('p')); + }); + }); + + describe('#validateForLogin', () => { + it(`returns 'valid' if validation is disabled`, () => { + expectValid(new LoginValidator().validateForLogin('', '')); + }); + + it(`returns 'invalid' if username is invalid`, () => { + expectInvalid(new LoginValidator({ shouldValidate: true }).validateForLogin('', 'p')); + }); + + it(`returns 'invalid' if password is invalid`, () => { + expectInvalid(new LoginValidator({ shouldValidate: true }).validateForLogin('u', '')); + }); + + it(`returns 'valid' if username and password are valid`, () => { + expectValid(new LoginValidator({ shouldValidate: true }).validateForLogin('u', 'p')); + }); + }); +}); diff --git a/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.ts b/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.ts new file mode 100644 index 0000000000000..0873098a0ff1d --- /dev/null +++ b/x-pack/plugins/security/public/authentication/login/components/login_form/validate_login.ts @@ -0,0 +1,97 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +interface LoginValidatorOptions { + shouldValidate?: boolean; +} + +export interface LoginValidationResult { + isInvalid: boolean; + error?: string; +} + +export class LoginValidator { + private shouldValidate?: boolean; + + constructor(options: LoginValidatorOptions = {}) { + this.shouldValidate = options.shouldValidate; + } + + public enableValidation() { + this.shouldValidate = true; + } + + public disableValidation() { + this.shouldValidate = false; + } + + public validateUsername(username: string): LoginValidationResult { + if (!this.shouldValidate) { + return valid(); + } + + if (!username) { + // Elasticsearch has more stringent requirements for usernames in the Native realm. However, the login page is used for other realms, + // such as LDAP and Active Directory. Because of that, the best validation we can do here is to ensure the username is not empty. + return invalid( + i18n.translate( + 'xpack.security.authentication.login.validateLogin.requiredUsernameErrorMessage', + { + defaultMessage: 'Username is required', + } + ) + ); + } + + return valid(); + } + + public validatePassword(password: string): LoginValidationResult { + if (!this.shouldValidate) { + return valid(); + } + + if (!password) { + // Elasticsearch has more stringent requirements for passwords in the Native realm. However, the login page is used for other realms, + // such as LDAP and Active Directory. Because of that, the best validation we can do here is to ensure the password is not empty. + return invalid( + i18n.translate( + 'xpack.security.authentication.login.validateLogin.requiredPasswordErrorMessage', + { + defaultMessage: 'Password is required', + } + ) + ); + } + return valid(); + } + + public validateForLogin(username: string, password: string): LoginValidationResult { + const { isInvalid: isUsernameInvalid } = this.validateUsername(username); + const { isInvalid: isPasswordInvalid } = this.validatePassword(password); + + if (isUsernameInvalid || isPasswordInvalid) { + return invalid(); + } + + return valid(); + } +} + +function invalid(error?: string): LoginValidationResult { + return { + isInvalid: true, + error, + }; +} + +function valid(): LoginValidationResult { + return { + isInvalid: false, + }; +} diff --git a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx index 51ca9f38a3d10..7965eeb779a3f 100644 --- a/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx +++ b/x-pack/plugins/transform/public/app/components/pivot_preview/pivot_preview.tsx @@ -163,6 +163,7 @@ export const PivotPreview: FC = React.memo( schema = 'boolean'; break; case ES_FIELD_TYPES.DATE: + case ES_FIELD_TYPES.DATE_NANOS: schema = 'datetime'; break; case ES_FIELD_TYPES.BYTE: @@ -235,7 +236,11 @@ export const PivotPreview: FC = React.memo( return null; } - if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.DATE) { + if ( + [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( + previewMappings.properties[columnId].type + ) + ) { return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx index 2a467ba4a5772..06ae4c81efa18 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_index_preview/source_index_preview.tsx @@ -22,6 +22,8 @@ import { EuiTitle, } from '@elastic/eui'; +import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; + import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/utils/date_utils'; import { getNestedProperty } from '../../../../../../common/utils/object_utils'; @@ -97,13 +99,14 @@ export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, q let schema; switch (field?.type) { - case 'date': + case KBN_FIELD_TYPES.DATE: schema = 'datetime'; break; - case 'geo_point': + case KBN_FIELD_TYPES.GEO_POINT: + case KBN_FIELD_TYPES.GEO_SHAPE: schema = 'json'; break; - case 'number': + case KBN_FIELD_TYPES.NUMBER: schema = 'numeric'; break; } @@ -177,7 +180,7 @@ export const SourceIndexPreview: React.FC = React.memo(({ indexPattern, q } const field = indexPattern.fields.getByName(columnId); - if (field?.type === 'date') { + if (field?.type === KBN_FIELD_TYPES.DATE) { return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index e56a519f80803..d47af47214851 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -10,6 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; +import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; + import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { TransformId } from '../../../../../../common'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; @@ -148,7 +150,7 @@ export const StepDetailsForm: FC = React.memo( }, []); const dateFieldNames = searchItems.indexPattern.fields - .filter(f => f.type === 'date') + .filter(f => f.type === KBN_FIELD_TYPES.DATE) .map(f => f.name) .sort(); const isContinuousModeAvailable = dateFieldNames.length > 0; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f2ea8f8c6dd0c..ccfb2707a51b9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6303,7 +6303,6 @@ "xpack.infra.logEntryActionsMenu.apmActionLabel": "APMで表示", "xpack.infra.logEntryActionsMenu.buttonLabel": "アクション", "xpack.infra.logEntryActionsMenu.uptimeActionLabel": "監視ステータスを表示", - "xpack.infra.logEntryItemView.viewDetailsToolTip": "詳細を表示", "xpack.infra.logFlyout.fieldColumnLabel": "フィールド", "xpack.infra.logFlyout.filterAriaLabel": "フィルター", "xpack.infra.logFlyout.flyoutTitle": "ログイベントドキュメントの詳細", @@ -7420,7 +7419,6 @@ "xpack.ml.calendarsEdit.newEventModal.startDateAriaLabel": "開始日", "xpack.ml.calendarsEdit.newEventModal.toLabel": "終了:", "xpack.ml.calendarsList.deleteCalendars.calendarsLabel": "{calendarsToDeleteCount} 件のカレンダー", - "xpack.ml.calendarsList.deleteCalendars.deletingCalendarErrorMessage": "カレンダー {calendarId} の削除中にエラーが発生しました: {errorMessage}", "xpack.ml.calendarsList.deleteCalendars.deletingCalendarsNotificationMessage": "{messageId} を削除中", "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "{messageId} が選択されました", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0dd584e32a248..06eb805d5af0e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6303,7 +6303,6 @@ "xpack.infra.logEntryActionsMenu.apmActionLabel": "在 APM 中查看", "xpack.infra.logEntryActionsMenu.buttonLabel": "操作", "xpack.infra.logEntryActionsMenu.uptimeActionLabel": "查看监测状态", - "xpack.infra.logEntryItemView.viewDetailsToolTip": "查看详情", "xpack.infra.logFlyout.fieldColumnLabel": "字段", "xpack.infra.logFlyout.filterAriaLabel": "筛选", "xpack.infra.logFlyout.flyoutTitle": "日志事件文档详情", @@ -7420,7 +7419,6 @@ "xpack.ml.calendarsEdit.newEventModal.startDateAriaLabel": "开始日期", "xpack.ml.calendarsEdit.newEventModal.toLabel": "到:", "xpack.ml.calendarsList.deleteCalendars.calendarsLabel": "{calendarsToDeleteCount} 个日历", - "xpack.ml.calendarsList.deleteCalendars.deletingCalendarErrorMessage": "删除日历 {calendarId} 时出错。{errorMessage}", "xpack.ml.calendarsList.deleteCalendars.deletingCalendarsNotificationMessage": "正在删除 {messageId}", "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "已删除 {messageId}", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "取消", diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 242ee890d4847..b0ca33b00fde8 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -27,6 +27,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'), require.resolve('../test/pki_api_integration/config.ts'), require.resolve('../test/login_selector_api_integration/config.ts'), + require.resolve('../test/encrypted_saved_objects_api_integration/config.ts'), require.resolve('../test/spaces_api_integration/spaces_only/config.ts'), require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial.ts'), require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic.ts'), diff --git a/x-pack/test/api_integration/apis/apm/custom_link.ts b/x-pack/test/api_integration/apis/apm/custom_link.ts new file mode 100644 index 0000000000000..8aefadd811775 --- /dev/null +++ b/x-pack/test/api_integration/apis/apm/custom_link.ts @@ -0,0 +1,144 @@ +/* + * 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 querystring from 'querystring'; +// import {isEmpty} from 'lodash' +import URL from 'url'; +import expect from '@kbn/expect'; +import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function customLinksTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + + function searchCustomLinks(filters?: any) { + const path = URL.format({ + pathname: `/api/apm/settings/custom_links`, + query: filters, + }); + return supertest.get(path).set('kbn-xsrf', 'foo'); + } + + async function createCustomLink(customLink: CustomLink) { + log.debug('creating configuration', customLink); + const res = await supertest + .post(`/api/apm/settings/custom_links`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function updateCustomLink(id: string, customLink: CustomLink) { + log.debug('updating configuration', id, customLink); + const res = await supertest + .put(`/api/apm/settings/custom_links/${id}`) + .send(customLink) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + async function deleteCustomLink(id: string) { + log.debug('deleting configuration', id); + const res = await supertest + .delete(`/api/apm/settings/custom_links/${id}`) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + function throwOnError(res: any) { + const { statusCode, req, body } = res; + if (statusCode !== 200) { + throw new Error(` + Endpoint: ${req.method} ${req.path} + Service: ${JSON.stringify(res.request._data.service)} + Status code: ${statusCode} + Response: ${body.message}`); + } + } + + describe('custom links', () => { + before(async () => { + const customLink = { + url: 'https://elastic.co', + label: 'with filters', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + } as CustomLink; + await createCustomLink(customLink); + }); + it('fetches a custom link', async () => { + const { status, body } = await searchCustomLinks({ + 'service.name': 'baz', + 'transaction.type': 'qux', + }); + const { label, url, filters } = body[0]; + + expect(status).to.equal(200); + expect({ label, url, filters }).to.eql({ + label: 'with filters', + url: 'https://elastic.co', + filters: [ + { key: 'service.name', value: 'baz' }, + { key: 'transaction.type', value: 'qux' }, + ], + }); + }); + it('updates a custom link', async () => { + let { status, body } = await searchCustomLinks({ + 'service.name': 'baz', + 'transaction.type': 'qux', + }); + expect(status).to.equal(200); + await updateCustomLink(body[0].id, { + label: 'foo', + url: 'https://elastic.co?service.name={{service.name}}', + filters: [ + { key: 'service.name', value: 'quz' }, + { key: 'transaction.name', value: 'bar' }, + ], + }); + ({ status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + })); + const { label, url, filters } = body[0]; + expect(status).to.equal(200); + expect({ label, url, filters }).to.eql({ + label: 'foo', + url: 'https://elastic.co?service.name={{service.name}}', + filters: [ + { key: 'service.name', value: 'quz' }, + { key: 'transaction.name', value: 'bar' }, + ], + }); + }); + it('deletes a custom link', async () => { + let { status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + }); + expect(status).to.equal(200); + await deleteCustomLink(body[0].id); + ({ status, body } = await searchCustomLinks({ + 'service.name': 'quz', + 'transaction.name': 'bar', + })); + expect(status).to.equal(200); + expect(body).to.eql([]); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/api_integration/apis/apm/feature_controls.ts index 8ce55b8fb1d5f..9f76941935bb7 100644 --- a/x-pack/test/api_integration/apis/apm/feature_controls.ts +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -149,12 +149,27 @@ export default function featureControlsTests({ getService }: FtrProviderContext) log.error(JSON.stringify(res, null, 2)); }, }, + { + req: { + url: `/api/apm/settings/custom_links`, + }, + expectForbidden: expect404, + expectResponse: expect200, + }, + { + req: { + url: `/api/apm/settings/custom_links/transaction`, + }, + expectForbidden: expect404, + expectResponse: expect200, + }, ]; const elasticsearchPrivileges = { indices: [ { names: ['apm-*'], privileges: ['read', 'view_index_metadata'] }, { names: ['.apm-agent-configuration'], privileges: ['read', 'write', 'view_index_metadata'] }, + { names: ['.apm-custom-link'], privileges: ['read', 'write', 'view_index_metadata'] }, ], }; diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/api_integration/apis/apm/index.ts index 6f41f4abfecc3..4a4265cfd0739 100644 --- a/x-pack/test/api_integration/apis/apm/index.ts +++ b/x-pack/test/api_integration/apis/apm/index.ts @@ -10,5 +10,6 @@ export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderCont describe('APM specs', () => { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./agent_configuration')); + loadTestFile(require.resolve('./custom_link')); }); } diff --git a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts index a50d65a48c2bb..3f56fb927d131 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts @@ -18,7 +18,7 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const esSupertest = getService('esSupertest'); const supertest = getService('supertestWithoutAuth'); - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); const testDataList = [ { @@ -103,7 +103,7 @@ export default ({ getService }: FtrProviderContext) => { it(`estimates the bucket span ${testData.testTitleSuffix}`, async () => { const { body } = await supertest .post('/api/ml/validate/estimate_bucket_span') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); @@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => { it(`estimates the bucket span`, async () => { const { body } = await supertest .post('/api/ml/validate/estimate_bucket_span') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); @@ -162,7 +162,7 @@ export default ({ getService }: FtrProviderContext) => { it(`estimates the bucket span`, async () => { const { body } = await supertest .post('/api/ml/validate/estimate_bucket_span') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts index 7fb0a10d94a4b..c36621a9a6403 100644 --- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts +++ b/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts @@ -15,7 +15,7 @@ const COMMON_HEADERS = { export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); const testDataList = [ { @@ -158,7 +158,7 @@ export default ({ getService }: FtrProviderContext) => { it(`calculates the model memory limit ${testData.testTitleSuffix}`, async () => { await supertest .post('/api/ml/validate/calculate_model_memory_limit') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts index aab7a65a7c122..b8ee2e7f6562c 100644 --- a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts @@ -79,7 +79,7 @@ const defaultRequestBody = { export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); const testDataList = [ { @@ -300,7 +300,7 @@ export default ({ getService }: FtrProviderContext) => { it(testData.title, async () => { const { body } = await supertest .post('/api/ml/jobs/categorization_field_examples') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); diff --git a/x-pack/test/api_integration/apis/ml/get_module.ts b/x-pack/test/api_integration/apis/ml/get_module.ts index 4478236c494a8..6dcd9594fc9aa 100644 --- a/x-pack/test/api_integration/apis/ml/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/get_module.ts @@ -37,12 +37,12 @@ const moduleIds = [ // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); async function executeGetModuleRequest(module: string, user: USER, rspCode: number) { const { body } = await supertest .get(`/api/ml/modules/get_module/${module}`) - .auth(user, mlSecurity.getPasswordForUser(user)) + .auth(user, ml.securityCommon.getPasswordForUser(user)) .set(COMMON_HEADERS) .expect(rspCode); diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 78f99d8d9776a..4e21faa610bfe 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -7,24 +7,26 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); describe('Machine Learning', function() { this.tags(['mlqa']); before(async () => { - await mlSecurity.createMlRoles(); - await mlSecurity.createMlUsers(); + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); }); after(async () => { - await mlSecurity.cleanMlUsers(); - await mlSecurity.cleanMlRoles(); + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); }); loadTestFile(require.resolve('./bucket_span_estimator')); loadTestFile(require.resolve('./calculate_model_memory_limit')); loadTestFile(require.resolve('./categorization_field_examples')); loadTestFile(require.resolve('./get_module')); + loadTestFile(require.resolve('./recognize_module')); + loadTestFile(require.resolve('./setup_module')); }); } diff --git a/x-pack/test/api_integration/apis/ml/recognize_module.ts b/x-pack/test/api_integration/apis/ml/recognize_module.ts new file mode 100644 index 0000000000000..2110bded7394c --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/recognize_module.ts @@ -0,0 +1,80 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { USER } from '../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitleSuffix: 'for sample logs dataset', + sourceDataArchive: 'ml/sample_logs', + indexPattern: 'kibana_sample_data_logs', + user: USER.ML_POWERUSER, + expected: { + responseCode: 200, + moduleIds: ['sample_data_weblogs'], + }, + }, + { + testTitleSuffix: 'for non existent index pattern', + sourceDataArchive: 'empty_kibana', + indexPattern: 'non-existent-index-pattern', + user: USER.ML_POWERUSER, + expected: { + responseCode: 200, + moduleIds: [], + }, + }, + ]; + + async function executeRecognizeModuleRequest(indexPattern: string, user: USER, rspCode: number) { + const { body } = await supertest + .get(`/api/ml/modules/recognize/${indexPattern}`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .expect(rspCode); + + return body; + } + + describe('module recognizer', function() { + for (const testData of testDataList) { + describe('lists matching modules', function() { + before(async () => { + await esArchiver.load(testData.sourceDataArchive); + }); + + after(async () => { + await esArchiver.unload(testData.sourceDataArchive); + }); + + it(testData.testTitleSuffix, async () => { + const rspBody = await executeRecognizeModuleRequest( + testData.indexPattern, + testData.user, + testData.expected.responseCode + ); + expect(rspBody).to.be.an(Array); + + const responseModuleIds = rspBody.map((module: { id: string }) => module.id); + expect(responseModuleIds).to.eql(testData.expected.moduleIds); + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/setup_module.ts b/x-pack/test/api_integration/apis/ml/setup_module.ts new file mode 100644 index 0000000000000..71f3910cd4e93 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/setup_module.ts @@ -0,0 +1,229 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states'; +import { USER } from '../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataListPositive = [ + { + testTitleSuffix: 'for sample logs dataset with prefix and startDatafeed false', + sourceDataArchive: 'ml/sample_logs', + module: 'sample_data_weblogs', + user: USER.ML_POWERUSER, + requestBody: { + prefix: 'pf1_', + indexPatternName: 'kibana_sample_data_logs', + startDatafeed: false, + }, + expected: { + responseCode: 200, + jobs: [ + { + jobId: 'pf1_low_request_rate', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + }, + { + jobId: 'pf1_response_code_rates', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + }, + { + jobId: 'pf1_url_scanning', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + }, + ], + }, + }, + ]; + + const testDataListNegative = [ + { + testTitleSuffix: 'for non existent index pattern', + sourceDataArchive: 'empty_kibana', + module: 'sample_data_weblogs', + user: USER.ML_POWERUSER, + requestBody: { + indexPatternName: 'non-existent-index-pattern', + startDatafeed: false, + }, + expected: { + responseCode: 400, + error: 'Bad Request', + message: + "Module's jobs contain custom URLs which require a kibana index pattern (non-existent-index-pattern) which cannot be found.", + }, + }, + { + testTitleSuffix: 'for unauthorized user', + sourceDataArchive: 'ml/sample_logs', + module: 'sample_data_weblogs', + user: USER.ML_UNAUTHORIZED, + requestBody: { + prefix: 'pf1_', + indexPatternName: 'kibana_sample_data_logs', + startDatafeed: false, + }, + expected: { + responseCode: 403, + error: 'Forbidden', + message: + '[security_exception] action [cluster:monitor/xpack/ml/info/get] is unauthorized for user [ml_unauthorized]', + }, + }, + ]; + + async function executeSetupModuleRequest( + module: string, + user: USER, + rqBody: object, + rspCode: number + ) { + const { body } = await supertest + .post(`/api/ml/modules/setup/${module}`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(rqBody) + .expect(rspCode); + + return body; + } + + function compareById(a: { id: string }, b: { id: string }) { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + } + + describe('module setup', function() { + for (const testData of testDataListPositive) { + describe('sets up module data', function() { + before(async () => { + await esArchiver.load(testData.sourceDataArchive); + }); + + after(async () => { + await esArchiver.unload(testData.sourceDataArchive); + await ml.api.cleanMlIndices(); + }); + + it(testData.testTitleSuffix, async () => { + const rspBody = await executeSetupModuleRequest( + testData.module, + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // verify response + if (testData.expected.jobs.length > 0) { + // jobs + expect(rspBody).to.have.property('jobs'); + + const expectedRspJobs = testData.expected.jobs + .map(job => { + return { id: job.jobId, success: true }; + }) + .sort(compareById); + + const actualRspJobs = rspBody.jobs.sort(compareById); + + expect(actualRspJobs).to.eql( + expectedRspJobs, + `Expected setup module response jobs to be '${JSON.stringify( + expectedRspJobs + )}' (got '${JSON.stringify(actualRspJobs)}')` + ); + + // datafeeds + expect(rspBody).to.have.property('datafeeds'); + + const expectedRspDatafeeds = testData.expected.jobs + .map(job => { + return { + id: `datafeed-${job.jobId}`, + success: true, + started: testData.requestBody.startDatafeed, + }; + }) + .sort(compareById); + + const actualRspDatafeeds = rspBody.datafeeds.sort(compareById); + + expect(actualRspDatafeeds).to.eql( + expectedRspDatafeeds, + `Expected setup module response datafeeds to be '${JSON.stringify( + expectedRspDatafeeds + )}' (got '${JSON.stringify(actualRspDatafeeds)}')` + ); + + // TODO in future updates: add response validations for created saved objects + } + + // verify job and datafeed creation + states + for (const job of testData.expected.jobs) { + const datafeedId = `datafeed-${job.jobId}`; + await ml.api.waitForAnomalyDetectionJobToExist(job.jobId); + await ml.api.waitForDatafeedToExist(datafeedId); + await ml.api.waitForJobState(job.jobId, job.jobState); + await ml.api.waitForDatafeedState(datafeedId, job.datafeedState); + } + }); + + // TODO in future updates: add creation validations for created saved objects + }); + } + + for (const testData of testDataListNegative) { + describe('rejects request', function() { + before(async () => { + await esArchiver.load(testData.sourceDataArchive); + }); + + after(async () => { + await esArchiver.unload(testData.sourceDataArchive); + await ml.api.cleanMlIndices(); + }); + + it(testData.testTitleSuffix, async () => { + const rspBody = await executeSetupModuleRequest( + testData.module, + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(rspBody) + .to.have.property('error') + .eql(testData.expected.error); + + expect(rspBody) + .to.have.property('message') + .eql(testData.expected.message); + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index c29116e1270c5..9c945f557a2d8 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -21,7 +21,7 @@ import { } from './infraops_graphql_client'; import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client'; import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration'; -import { MachineLearningSecurityCommonProvider } from '../../functional/services/machine_learning'; +import { MachineLearningProvider } from './ml'; export const services = { ...commonServices, @@ -38,5 +38,5 @@ export const services = { siemGraphQLClientFactory: SiemGraphQLClientFactoryProvider, supertestWithoutAuth: SupertestWithoutAuthProvider, usageAPI: UsageAPIProvider, - mlSecurity: MachineLearningSecurityCommonProvider, + ml: MachineLearningProvider, }; diff --git a/x-pack/test/api_integration/services/ml.ts b/x-pack/test/api_integration/services/ml.ts new file mode 100644 index 0000000000000..841b200b87080 --- /dev/null +++ b/x-pack/test/api_integration/services/ml.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../functional/ftr_provider_context'; + +import { + MachineLearningAPIProvider, + MachineLearningSecurityCommonProvider, +} from '../../functional/services/machine_learning'; + +export function MachineLearningProvider(context: FtrProviderContext) { + const api = MachineLearningAPIProvider(context); + const securityCommon = MachineLearningSecurityCommonProvider(context); + + return { + api, + securityCommon, + }; +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts index c92e351ed5918..f1404b79a07af 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -151,6 +151,7 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial = () => ({ + setup(core: CoreSetup, deps) { + core.savedObjects.registerType({ + name: SAVED_OBJECT_WITH_SECRET_TYPE, + hidden: false, + namespaceAgnostic: false, + mappings: deepFreeze({ + properties: { + publicProperty: { type: 'keyword' }, + publicPropertyExcludedFromAAD: { type: 'keyword' }, + privateProperty: { type: 'binary' }, + }, + }), + }); + + deps.encryptedSavedObjects.registerType({ + type: SAVED_OBJECT_WITH_SECRET_TYPE, + attributesToEncrypt: new Set(['privateProperty']), + attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']), + }); + + core.http.createRouter().get( + { + path: '/api/saved_objects/get-decrypted-as-internal-user/{id}', + validate: { params: value => ({ value }) }, + }, + async (context, request, response) => { + const [, { encryptedSavedObjects }] = await core.getStartServices(); + const spaceId = deps.spaces.spacesService.getSpaceId(request); + const namespace = deps.spaces.spacesService.spaceIdToNamespace(spaceId); + + try { + return response.ok({ + body: await encryptedSavedObjects.getDecryptedAsInternalUser( + SAVED_OBJECT_WITH_SECRET_TYPE, + request.params.id, + { namespace } + ), + }); + } catch (err) { + if (encryptedSavedObjects.isEncryptionError(err)) { + return response.badRequest({ body: 'Failed to encrypt attributes' }); + } + + return response.customError({ body: err, statusCode: 500 }); + } + } + ); + }, + start() {}, + stop() {}, +}); diff --git a/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.ts new file mode 100644 index 0000000000000..e3add3748f56d --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/ftr_provider_context.d.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 { GenericFtrProviderContext } from '@kbn/test/types/ftr'; + +import { services } from './services'; + +export type FtrProviderContext = GenericFtrProviderContext; diff --git a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.d.ts b/x-pack/test/encrypted_saved_objects_api_integration/services.ts similarity index 66% rename from x-pack/legacy/plugins/maps/public/selectors/ui_selectors.d.ts rename to x-pack/test/encrypted_saved_objects_api_integration/services.ts index 812e2082241bd..b7398349cce5d 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.d.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/services.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getOpenTOCDetails(state: unknown): string[]; - -export function getIsLayerTOCOpen(state: unknown): boolean; +export { services } from '../api_integration/services'; diff --git a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts similarity index 99% rename from x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts rename to x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index ab9f7d2cdd339..7fe3d28911211 100644 --- a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { SavedObject } from 'src/core/server'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const es = getService('legacyEs'); diff --git a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts similarity index 88% rename from x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts rename to x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts index 424160e84495e..8c816a3404ddb 100644 --- a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { describe('encryptedSavedObjects', function encryptedSavedObjectsSuite() { diff --git a/x-pack/test/epm_api_integration/apis/file.ts b/x-pack/test/epm_api_integration/apis/file.ts index 2989263af40a7..11ab906aa0ea4 100644 --- a/x-pack/test/epm_api_integration/apis/file.ts +++ b/x-pack/test/epm_api_integration/apis/file.ts @@ -19,7 +19,7 @@ export default function({ getService }: FtrProviderContext) { it('fetches a .png screenshot image', async () => { server.on({ method: 'GET', - path: '/package/auditd-2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png', + path: '/package/auditd/2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png', reply: { headers: { 'content-type': 'image/png' }, }, @@ -28,7 +28,7 @@ export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); await supertest .get( - '/api/ingest_manager/epm/packages/auditd-2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png' + '/api/ingest_manager/epm/packages/auditd/2.0.4/img/screenshots/auditbeat-file-integrity-dashboard.png' ) .set('kbn-xsrf', 'xxx') .expect('Content-Type', 'image/png') @@ -38,7 +38,7 @@ export default function({ getService }: FtrProviderContext) { it('fetches an .svg icon image', async () => { server.on({ method: 'GET', - path: '/package/auditd-2.0.4/img/icon.svg', + path: '/package/auditd/2.0.4/img/icon.svg', reply: { headers: { 'content-type': 'image/svg' }, }, @@ -46,7 +46,7 @@ export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); await supertest - .get('/api/ingest_manager/epm/packages/auditd-2.0.4/img/icon.svg') + .get('/api/ingest_manager/epm/packages/auditd/2.0.4/img/icon.svg') .set('kbn-xsrf', 'xxx') .expect('Content-Type', 'image/svg'); }); @@ -54,13 +54,13 @@ export default function({ getService }: FtrProviderContext) { it('fetches an auditbeat .conf rule file', async () => { server.on({ method: 'GET', - path: '/package/auditd-2.0.4/auditbeat/rules/sample-rules-linux-32bit.conf', + path: '/package/auditd/2.0.4/auditbeat/rules/sample-rules-linux-32bit.conf', }); const supertest = getService('supertest'); await supertest .get( - '/api/ingest_manager/epm/packages/auditd-2.0.4/auditbeat/rules/sample-rules-linux-32bit.conf' + '/api/ingest_manager/epm/packages/auditd/2.0.4/auditbeat/rules/sample-rules-linux-32bit.conf' ) .set('kbn-xsrf', 'xxx') .expect('Content-Type', 'application/json; charset=utf-8') @@ -70,7 +70,7 @@ export default function({ getService }: FtrProviderContext) { it('fetches an auditbeat .yml config file', async () => { server.on({ method: 'GET', - path: '/package/auditd-2.0.4/auditbeat/config/config.yml', + path: '/package/auditd/2.0.4/auditbeat/config/config.yml', reply: { headers: { 'content-type': 'text/yaml; charset=UTF-8' }, }, @@ -78,7 +78,7 @@ export default function({ getService }: FtrProviderContext) { const supertest = getService('supertest'); await supertest - .get('/api/ingest_manager/epm/packages/auditd-2.0.4/auditbeat/config/config.yml') + .get('/api/ingest_manager/epm/packages/auditd/2.0.4/auditbeat/config/config.yml') .set('kbn-xsrf', 'xxx') .expect('Content-Type', 'text/yaml; charset=UTF-8') .expect(200); @@ -88,13 +88,13 @@ export default function({ getService }: FtrProviderContext) { server.on({ method: 'GET', path: - '/package/auditd-2.0.4/kibana/visualization/b21e0c70-c252-11e7-8692-232bd1143e8a-ecs.json', + '/package/auditd/2.0.4/kibana/visualization/b21e0c70-c252-11e7-8692-232bd1143e8a-ecs.json', }); const supertest = getService('supertest'); await supertest .get( - '/api/ingest_manager/epm/packages/auditd-2.0.4/kibana/visualization/b21e0c70-c252-11e7-8692-232bd1143e8a-ecs.json' + '/api/ingest_manager/epm/packages/auditd/2.0.4/kibana/visualization/b21e0c70-c252-11e7-8692-232bd1143e8a-ecs.json' ) .set('kbn-xsrf', 'xxx') .expect('Content-Type', 'application/json; charset=utf-8') @@ -105,13 +105,13 @@ export default function({ getService }: FtrProviderContext) { server.on({ method: 'GET', path: - '/package/auditd-2.0.4/kibana/dashboard/7de391b0-c1ca-11e7-8995-936807a28b16-ecs.json', + '/package/auditd/2.0.4/kibana/dashboard/7de391b0-c1ca-11e7-8995-936807a28b16-ecs.json', }); const supertest = getService('supertest'); await supertest .get( - '/api/ingest_manager/epm/packages/auditd-2.0.4/kibana/dashboard/7de391b0-c1ca-11e7-8995-936807a28b16-ecs.json' + '/api/ingest_manager/epm/packages/auditd/2.0.4/kibana/dashboard/7de391b0-c1ca-11e7-8995-936807a28b16-ecs.json' ) .set('kbn-xsrf', 'xxx') .expect('Content-Type', 'application/json; charset=utf-8') @@ -121,12 +121,12 @@ export default function({ getService }: FtrProviderContext) { it('fetches an .json index pattern file', async () => { server.on({ method: 'GET', - path: '/package/auditd-2.0.4/kibana/index-pattern/auditbeat-*.json', + path: '/package/auditd/2.0.4/kibana/index-pattern/auditbeat-*.json', }); const supertest = getService('supertest'); await supertest - .get('/api/ingest_manager/epm/packages/auditd-2.0.4/kibana/index-pattern/auditbeat-*.json') + .get('/api/ingest_manager/epm/packages/auditd/2.0.4/kibana/index-pattern/auditbeat-*.json') .set('kbn-xsrf', 'xxx') .expect('Content-Type', 'application/json; charset=utf-8') .expect(200); @@ -135,13 +135,13 @@ export default function({ getService }: FtrProviderContext) { it('fetches a .json search file', async () => { server.on({ method: 'GET', - path: '/package/auditd-2.0.4/kibana/search/0f10c430-c1c3-11e7-8995-936807a28b16-ecs.json', + path: '/package/auditd/2.0.4/kibana/search/0f10c430-c1c3-11e7-8995-936807a28b16-ecs.json', }); const supertest = getService('supertest'); await supertest .get( - '/api/ingest_manager/epm/packages/auditd-2.0.4/kibana/search/0f10c430-c1c3-11e7-8995-936807a28b16-ecs.json' + '/api/ingest_manager/epm/packages/auditd/2.0.4/kibana/search/0f10c430-c1c3-11e7-8995-936807a28b16-ecs.json' ) .set('kbn-xsrf', 'xxx') .expect('Content-Type', 'application/json; charset=utf-8') diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 1f22ca59ab2d4..7e15ff436d12c 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -138,7 +138,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { }); }); - describe('no advanced_settings privileges', function() { + // FLAKY: https://github.com/elastic/kibana/issues/57377 + describe.skip('no advanced_settings privileges', function() { this.tags(['skipCoverage']); before(async () => { await security.role.create('no_advanced_settings_privileges_role', { diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index 252d0a0a78782..4b105263f3ba5 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -14,7 +14,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const config = getService('config'); - describe('spaces feature controls', () => { + // FLAKY: https://github.com/elastic/kibana/issues/57413 + describe.skip('spaces feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); diff --git a/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz b/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz new file mode 100644 index 0000000000000..03ceb319a6afe Binary files /dev/null and b/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json b/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json new file mode 100644 index 0000000000000..1c7490e139be5 --- /dev/null +++ b/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json @@ -0,0 +1,3162 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "kibana_sample_data_logs", + "mappings": { + "properties": { + "@timestamp": { + "path": "timestamp", + "type": "alias" + }, + "agent": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "bytes": { + "type": "long" + }, + "clientip": { + "type": "ip" + }, + "event": { + "properties": { + "dataset": { + "type": "keyword" + } + } + }, + "extension": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "geo": { + "properties": { + "coordinates": { + "type": "geo_point" + }, + "dest": { + "type": "keyword" + }, + "src": { + "type": "keyword" + }, + "srcdest": { + "type": "keyword" + } + } + }, + "host": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ip": { + "type": "ip" + }, + "machine": { + "properties": { + "os": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ram": { + "type": "long" + } + } + }, + "memory": { + "type": "double" + }, + "message": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "phpmemory": { + "type": "long" + }, + "referer": { + "type": "keyword" + }, + "request": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "response": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "utc_time": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "agent_configs": "38abaf89513877745c359e7700c0c66a", + "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "agents": "75c0f4a11560dbc38b65e5e1d98fc9da", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "e8619030e08b671291af04c4603b4944", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "08b8b110dbca273d37e8aef131ecab61", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "datasources": "d4bc0c252b2b5683ff21ea32d00acffc", + "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "epm-package": "75d12cd13c867fd713d7dfb27366bc20", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "9ecce5b58867403613d82fe496470b34", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "268da3a48066123fc5baf35abaa55014", + "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "outputs": "aee9782e0d500b867859650a36280165", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "server": "ec97f1c5da1a19609a60874e5af1100c", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "actions": { + "properties": { + "created_at": { + "type": "date" + }, + "data": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "name": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "time": { + "type": "integer" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "canvas-workpad": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "graph-workspace": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "map": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "dynamic": "true", + "properties": { + "indexName": { + "type": "keyword" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/services/machine_learning/api.ts b/x-pack/test/functional/services/machine_learning/api.ts index 74dc5912df36f..afc2567f3cce9 100644 --- a/x-pack/test/functional/services/machine_learning/api.ts +++ b/x-pack/test/functional/services/machine_learning/api.ts @@ -277,6 +277,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return await esSupertest.get(`/_ml/anomaly_detectors/${jobId}`).expect(200); }, + async waitForAnomalyDetectionJobToExist(jobId: string) { + await retry.waitForWithTimeout(`'${jobId}' to exist`, 5 * 1000, async () => { + if (await this.getAnomalyDetectionJob(jobId)) { + return true; + } else { + throw new Error(`expected anomaly detection job '${jobId}' to exist`); + } + }); + }, + async createAnomalyDetectionJob(jobConfig: Job) { const jobId = jobConfig.job_id; log.debug(`Creating anomaly detection job with id '${jobId}'...`); @@ -285,19 +295,23 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { .send(jobConfig) .expect(200); - await retry.waitForWithTimeout(`'${jobId}' to be created`, 5 * 1000, async () => { - if (await this.getAnomalyDetectionJob(jobId)) { - return true; - } else { - throw new Error(`expected anomaly detection job '${jobId}' to be created`); - } - }); + await this.waitForAnomalyDetectionJobToExist(jobId); }, async getDatafeed(datafeedId: string) { return await esSupertest.get(`/_ml/datafeeds/${datafeedId}`).expect(200); }, + async waitForDatafeedToExist(datafeedId: string) { + await retry.waitForWithTimeout(`'${datafeedId}' to exist`, 5 * 1000, async () => { + if (await this.getDatafeed(datafeedId)) { + return true; + } else { + throw new Error(`expected datafeed '${datafeedId}' to exist`); + } + }); + }, + async createDatafeed(datafeedConfig: Datafeed) { const datafeedId = datafeedConfig.datafeed_id; log.debug(`Creating datafeed with id '${datafeedId}'...`); @@ -306,13 +320,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { .send(datafeedConfig) .expect(200); - await retry.waitForWithTimeout(`'${datafeedId}' to be created`, 5 * 1000, async () => { - if (await this.getDatafeed(datafeedId)) { - return true; - } else { - throw new Error(`expected datafeed '${datafeedId}' to be created`); - } - }); + await this.waitForDatafeedToExist(datafeedId); }, async openAnomalyDetectionJob(jobId: string) { diff --git a/x-pack/test/functional/services/machine_learning/security_common.ts b/x-pack/test/functional/services/machine_learning/security_common.ts index d59c1edcb00ab..1145b6f93a4f8 100644 --- a/x-pack/test/functional/services/machine_learning/security_common.ts +++ b/x-pack/test/functional/services/machine_learning/security_common.ts @@ -12,6 +12,7 @@ export type MlSecurityCommon = ProvidedType { - nonce = request.payload.nonce; - return {}; - }, - }); - - server.route({ - path: '/api/oidc_provider/token_endpoint', - method: 'POST', - // Token endpoint needs authentication (with the client credentials) but we don't attempt to - // validate this OIDC behavior here - config: { - auth: false, - validate: { - payload: Joi.object({ - grant_type: Joi.string().optional(), - code: Joi.string().optional(), - redirect_uri: Joi.string().optional(), - }), - }, - }, - async handler(request) { - const userId = request.payload.code.substring(4); - const { accessToken, idToken } = createTokens(userId, nonce); - try { - const userId = request.payload.code.substring(4); - return { - access_token: accessToken, - token_type: 'Bearer', - refresh_token: `valid-refresh-token${userId}`, - expires_in: 3600, - id_token: idToken, - }; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/oidc_provider/userinfo_endpoint', - method: 'GET', - config: { - auth: false, - }, - handler: request => { - const accessToken = request.headers.authorization.substring(7); - if (accessToken === 'valid-access-token1') { - return { - sub: 'user1', - name: 'Tony Stark', - given_name: 'Tony', - family_name: 'Stark', - preferred_username: 'ironman', - email: 'ironman@avengers.com', - }; - } - if (accessToken === 'valid-access-token2') { - return { - sub: 'user2', - name: 'Peter Parker', - given_name: 'Peter', - family_name: 'Parker', - preferred_username: 'spiderman', - email: 'spiderman@avengers.com', - }; - } - if (accessToken === 'valid-access-token3') { - return { - sub: 'user3', - name: 'Bruce Banner', - given_name: 'Bruce', - family_name: 'Banner', - preferred_username: 'hulk', - email: 'hulk@avengers.com', - }; - } - return {}; - }, - }); -} diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json new file mode 100644 index 0000000000000..faaa0b9165828 --- /dev/null +++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "oidc_provider_plugin", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json deleted file mode 100644 index 358c6e2020afe..0000000000000 --- a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "oidc_provider_plugin", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0", - "dependencies": { - "joi": "^13.5.2", - "jsonwebtoken": "^8.3.0" - } -} diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts similarity index 55% rename from x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js rename to x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts index 17d45527397b8..456abecd201be 100644 --- a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js +++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts @@ -4,16 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializer } from '../../../../../../src/core/server'; import { initRoutes } from './init_routes'; -export default function(kibana) { - return new kibana.Plugin({ - name: 'oidcProvider', - id: 'oidcProvider', - require: ['elasticsearch'], - - init(server) { - initRoutes(server); - }, - }); -} +export const plugin: PluginInitializer = () => ({ + setup: core => initRoutes(core.http.createRouter()), + start: () => {}, + stop: () => {}, +}); diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.ts b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.ts new file mode 100644 index 0000000000000..6d3248f4377b1 --- /dev/null +++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.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 { IRouter } from '../../../../../../src/core/server'; +import { createTokens } from '../../oidc_tools'; + +export function initRoutes(router: IRouter) { + let nonce = ''; + + router.post( + { + path: '/api/oidc_provider/setup', + validate: { body: value => ({ value }) }, + options: { authRequired: false }, + }, + (context, request, response) => { + nonce = request.body.nonce; + return response.ok({ body: {} }); + } + ); + + router.post( + { + path: '/api/oidc_provider/token_endpoint', + validate: { body: value => ({ value }) }, + // Token endpoint needs authentication (with the client credentials) but we don't attempt to + // validate this OIDC behavior here + options: { authRequired: false, xsrfRequired: false }, + }, + (context, request, response) => { + const userId = request.body.code.substring(4); + const { accessToken, idToken } = createTokens(userId, nonce); + return response.ok({ + body: { + access_token: accessToken, + token_type: 'Bearer', + refresh_token: `valid-refresh-token${userId}`, + expires_in: 3600, + id_token: idToken, + }, + }); + } + ); + + router.get( + { + path: '/api/oidc_provider/userinfo_endpoint', + validate: false, + options: { authRequired: false }, + }, + (context, request, response) => { + const accessToken = (request.headers.authorization as string).substring(7); + if (accessToken === 'valid-access-token1') { + return response.ok({ + body: { + sub: 'user1', + name: 'Tony Stark', + given_name: 'Tony', + family_name: 'Stark', + preferred_username: 'ironman', + email: 'ironman@avengers.com', + }, + }); + } + + if (accessToken === 'valid-access-token2') { + return response.ok({ + body: { + sub: 'user2', + name: 'Peter Parker', + given_name: 'Peter', + family_name: 'Parker', + preferred_username: 'spiderman', + email: 'spiderman@avengers.com', + }, + }); + } + + if (accessToken === 'valid-access-token3') { + return response.ok({ + body: { + sub: 'user3', + name: 'Bruce Banner', + given_name: 'Bruce', + family_name: 'Banner', + preferred_username: 'hulk', + email: 'hulk@avengers.com', + }, + }); + } + + return response.ok({ body: {} }); + } + ); +} diff --git a/x-pack/test/plugin_api_integration/config.js b/x-pack/test/plugin_api_integration/config.js index 830933278f2bc..83e8b1f84a9e0 100644 --- a/x-pack/test/plugin_api_integration/config.js +++ b/x-pack/test/plugin_api_integration/config.js @@ -18,10 +18,7 @@ export default async function({ readConfigFile }) { ); return { - testFiles: [ - require.resolve('./test_suites/task_manager'), - require.resolve('./test_suites/encrypted_saved_objects'), - ], + testFiles: [require.resolve('./test_suites/task_manager')], services, servers: integrationConfig.get('servers'), esTestCluster: integrationConfig.get('esTestCluster'), diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts deleted file mode 100644 index e61b8f24a1f69..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Request } from 'hapi'; -import { boomify, badRequest } from 'boom'; -import { Legacy } from 'kibana'; -import { - EncryptedSavedObjectsPluginSetup, - EncryptedSavedObjectsPluginStart, -} from '../../../../plugins/encrypted_saved_objects/server'; - -const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; - -// eslint-disable-next-line import/no-default-export -export default function esoPlugin(kibana: any) { - return new kibana.Plugin({ - id: 'eso', - require: ['encryptedSavedObjects'], - uiExports: { mappings: require('./mappings.json') }, - init(server: Legacy.Server) { - server.route({ - method: 'GET', - path: '/api/saved_objects/get-decrypted-as-internal-user/{id}', - async handler(request: Request) { - const encryptedSavedObjectsStart = server.newPlatform.start.plugins - .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; - const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); - try { - return await encryptedSavedObjectsStart.getDecryptedAsInternalUser( - SAVED_OBJECT_WITH_SECRET_TYPE, - request.params.id, - { namespace: namespace === 'default' ? undefined : namespace } - ); - } catch (err) { - if (encryptedSavedObjectsStart.isEncryptionError(err)) { - return badRequest('Failed to encrypt attributes'); - } - - return boomify(err); - } - }, - }); - - (server.newPlatform.setup.plugins - .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup).registerType({ - type: SAVED_OBJECT_WITH_SECRET_TYPE, - attributesToEncrypt: new Set(['privateProperty']), - attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']), - }); - }, - }); -} diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json deleted file mode 100644 index b727850793bbe..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "saved-object-with-secret": { - "properties": { - "publicProperty": { - "type": "keyword" - }, - "publicPropertyExcludedFromAAD": { - "type": "keyword" - }, - "privateProperty": { - "type": "binary" - } - } - } -} diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json deleted file mode 100644 index 723904757ae8a..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "eso", - "version": "kibana" -} \ No newline at end of file diff --git a/x-pack/test/saml_api_integration/config.ts b/x-pack/test/saml_api_integration/config.ts index 0580c28555d16..a92f11363b0fc 100644 --- a/x-pack/test/saml_api_integration/config.ts +++ b/x-pack/test/saml_api_integration/config.ts @@ -50,7 +50,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), '--optimize.enabled=false', - '--server.xsrf.whitelist=["/api/security/saml/callback"]', `--xpack.security.authc.providers=${JSON.stringify(['saml', 'basic'])}`, '--xpack.security.authc.saml.realm=saml1', '--xpack.security.authc.saml.maxRedirectURLSize=100b', diff --git a/yarn.lock b/yarn.lock index a2ad812c860fd..aa96a740cb3a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3821,6 +3821,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== +"@types/estree@^0.0.44": + version "0.0.44" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.44.tgz#980cc5a29a3ef3bea6ff1f7d021047d7ea575e21" + integrity sha512-iaIVzr+w2ZJ5HkidlZ3EJM8VTZb2MJLCjw3V+505yVts0gRC4UMvjw0d1HPtGqI/HQC/KdsYtayfzl+AXY2R8g== + "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -5312,6 +5317,11 @@ acorn-walk@^7.0.0: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.0.0.tgz#c8ba6f0f1aac4b0a9e32d1f0af12be769528f36b" integrity sha512-7Bv1We7ZGuU79zZbb6rRqcpxo3OY+zrdtloZWoyD8fmGX+FeXRjE+iuGkZjSXLVovLzrsvMGMy0EkwA0E0umxg== +acorn-walk@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.1.1.tgz#345f0dffad5c735e7373d2fec9a1023e6a44b83e" + integrity sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ== + acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.1.2, acorn@^5.5.0: version "5.7.4" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" @@ -5332,7 +5342,7 @@ acorn@^6.0.1, acorn@^6.2.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== -acorn@^7.0.0, acorn@^7.1.0: +acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==