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/test/plugin_functional/test_suites/app_plugins/index.js b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts similarity index 85% rename from test/plugin_functional/test_suites/app_plugins/index.js rename to packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts index 83faa7377c7ac..ca5ba1b90fe95 100644 --- a/test/plugin_functional/test_suites/app_plugins/index.js +++ b/packages/kbn-optimizer/src/common/disallowed_syntax_plugin/index.ts @@ -17,8 +17,4 @@ * under the License. */ -export default function({ loadTestFile }) { - describe('app plugins', () => { - loadTestFile(require.resolve('./app_navigation')); - }); -} +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/kibana/public/dashboard/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts index 55e1475fcb03a..86bce5997cdd2 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy_imports.ts @@ -25,7 +25,6 @@ */ export { npSetup, npStart } from 'ui/new_platform'; -export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; export { configureAppAngularModule, migrateLegacyQuery, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts deleted file mode 100644 index 60ca1b39d29d6..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.test.ts +++ /dev/null @@ -1,118 +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. - */ - -jest.mock('../', () => ({ - DashboardConstants: { - ADD_EMBEDDABLE_ID: 'addEmbeddableId', - ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', - }, -})); - -jest.mock('../legacy_imports', () => { - return { - absoluteToParsedUrl: jest.fn(() => { - return { - basePath: '/pep', - appId: 'kibana', - appPath: '/dashboard?addEmbeddableType=lens&addEmbeddableId=123eb456cd&x=1&y=2&z=3', - hostname: 'localhost', - port: 5601, - protocol: 'http:', - addQueryParameter: () => {}, - getAbsoluteUrl: () => { - return 'http://localhost:5601/pep/app/kibana#/dashboard?addEmbeddableType=lens&addEmbeddableId=123eb456cd&x=1&y=2&z=3'; - }, - }; - }), - }; -}); - -import { - addEmbeddableToDashboardUrl, - getLensUrlFromDashboardAbsoluteUrl, - getUrlVars, -} from './url_helper'; - -describe('Dashboard URL Helper', () => { - beforeEach(() => { - jest.resetModules(); - }); - - it('addEmbeddableToDashboardUrl', () => { - const id = '123eb456cd'; - const type = 'lens'; - const urlVars = { - x: '1', - y: '2', - z: '3', - }; - const basePath = '/pep'; - const url = - "http://localhost:5601/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(addEmbeddableToDashboardUrl(url, basePath, id, urlVars, type)).toEqual( - `http://localhost:5601/pep/app/kibana#/dashboard?addEmbeddableType=${type}&addEmbeddableId=${id}&x=1&y=2&z=3` - ); - }); - - it('getUrlVars', () => { - let url = - "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getUrlVars(url)).toEqual({ - _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))', - _a: "(description:'',filters:!()", - }); - url = 'http://mybusiness.mydomain.com/app/kibana#/dashboard?x=y&y=z'; - expect(getUrlVars(url)).toEqual({ - x: 'y', - y: 'z', - }); - url = 'http://localhost:5601/app/kibana#/dashboard/777182'; - expect(getUrlVars(url)).toEqual({}); - url = - 'http://localhost:5601/app/kibana#/dashboard/777182?title=Some%20Dashboard%20With%20Spaces'; - expect(getUrlVars(url)).toEqual({ title: 'Some Dashboard With Spaces' }); - }); - - it('getLensUrlFromDashboardAbsoluteUrl', () => { - const id = '1244'; - const basePath = '/wev'; - let url = - "http://localhost:5601/wev/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( - 'http://localhost:5601/wev/app/kibana#/lens/edit/1244' - ); - - url = - "http://localhost:5601/wev/app/kibana#/dashboard/625357282?_a=(description:'',filters:!()&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"; - expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( - 'http://localhost:5601/wev/app/kibana#/lens/edit/1244' - ); - - url = 'http://myserver.mydomain.com:5601/wev/app/kibana#/dashboard/777182'; - expect(getLensUrlFromDashboardAbsoluteUrl(url, basePath, id)).toEqual( - 'http://myserver.mydomain.com:5601/wev/app/kibana#/lens/edit/1244' - ); - - url = - "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; - expect(getLensUrlFromDashboardAbsoluteUrl(url, '', id)).toEqual( - 'http://localhost:5601/app/kibana#/lens/edit/1244' - ); - }); -}); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts b/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts deleted file mode 100644 index 73383f2ff3f68..0000000000000 --- a/src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper.ts +++ /dev/null @@ -1,100 +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 { parse } from 'url'; -import { absoluteToParsedUrl } from '../legacy_imports'; -import { DashboardConstants } from './dashboard_constants'; -/** - * Return query params from URL - * @param url given url - */ -export function getUrlVars(url: string): Record { - const vars: Record = {}; - for (const [, key, value] of url.matchAll(/[?&]+([^=&]+)=([^&]*)/gi)) { - vars[key] = decodeURIComponent(value); - } - return vars; -} - -/** * - * Returns dashboard URL with added embeddableType and embeddableId query params - * eg. - * input: url: http://localhost:5601/lib/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345, embeddableType: 'lens' - * output: http://localhost:5601/lib/app/kibana#dashboard?addEmbeddableType=lens&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) - * @param url dasbhoard absolute url - * @param embeddableId id of the saved visualization - * @param basePath current base path - * @param urlVars url query params (optional) - * @param embeddableType 'lens' or 'visualization' (optional, default is 'lens') - */ -export function addEmbeddableToDashboardUrl( - url: string | undefined, - basePath: string, - embeddableId: string, - urlVars?: Record, - embeddableType?: string -): string | null { - if (!url) { - return null; - } - const dashboardUrl = getUrlWithoutQueryParams(url); - const dashboardParsedUrl = absoluteToParsedUrl(dashboardUrl, basePath); - if (urlVars) { - const keys = Object.keys(urlVars).sort(); - keys.forEach(key => { - dashboardParsedUrl.addQueryParameter(key, urlVars[key]); - }); - } - dashboardParsedUrl.addQueryParameter( - DashboardConstants.ADD_EMBEDDABLE_TYPE, - embeddableType || 'lens' - ); - dashboardParsedUrl.addQueryParameter(DashboardConstants.ADD_EMBEDDABLE_ID, embeddableId); - return dashboardParsedUrl.getAbsoluteUrl(); -} - -/** - * Return Lens URL from dashboard absolute URL - * @param dashboardAbsoluteUrl - * @param basePath current base path - * @param id Lens id - */ -export function getLensUrlFromDashboardAbsoluteUrl( - dashboardAbsoluteUrl: string | undefined | null, - basePath: string | null | undefined, - id: string -): string | null { - if (!dashboardAbsoluteUrl || basePath === null || basePath === undefined) { - return null; - } - const { host, protocol } = parse(dashboardAbsoluteUrl); - return `${protocol}//${host}${basePath}/app/kibana#/lens/edit/${id}`; -} - -/** - * Returns the portion of the URL without query params - * eg. - * input: http://localhost:5601/lib/app/kibana#/dashboard?param1=x¶m2=y¶m3=z - * output:http://localhost:5601/lib/app/kibana#/dashboard - * input: http://localhost:5601/lib/app/kibana#/dashboard/39292992?param1=x¶m2=y¶m3=z - * output: http://localhost:5601/lib/app/kibana#/dashboard/39292992 - * @param url url to parse - */ -function getUrlWithoutQueryParams(url: string): string { - return url.split('?')[0]; -} diff --git a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts index f29f07ba4b20b..2ed7e3d43168c 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/kibana_services.ts @@ -45,7 +45,6 @@ export interface VisualizeKibanaServices { core: CoreStart; data: DataPublicPluginStart; embeddable: EmbeddableStart; - getBasePath: () => string; indexPatterns: IndexPatternsContract; localStorage: Storage; navigation: NavigationStart; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index a6774e2dd47e8..6a2034d9a62e4 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts @@ -24,9 +24,6 @@ * directly where they are needed. */ -export { absoluteToParsedUrl } from 'ui/url/absolute_to_parsed_url'; -export { KibanaParsedUrl } from 'ui/url/kibana_parsed_url'; -export { wrapInI18nContext } from 'ui/i18n'; export { DashboardConstants } from '../dashboard/np_ready/dashboard_constants'; export { VisSavedObject, diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js index c5325ca3108b4..9ccd45dfc1b45 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/editor.js @@ -26,14 +26,14 @@ import { EventEmitter } from 'events'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { makeStateful, useVisualizeAppState } from './lib'; +import { makeStateful, useVisualizeAppState, addEmbeddableToDashboardUrl } from './lib'; import { VisualizeConstants } from '../visualize_constants'; import { getEditBreadcrumbs } from '../breadcrumbs'; import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; import { unhashUrl, removeQueryParam } from '../../../../../../../plugins/kibana_utils/public'; import { MarkdownSimple, toMountPoint } from '../../../../../../../plugins/kibana_react/public'; -import { addFatalError, kbnBaseUrl } from '../../../../../../../plugins/kibana_legacy/public'; +import { addFatalError } from '../../../../../../../plugins/kibana_legacy/public'; import { SavedObjectSaveModal, showSaveModal, @@ -46,14 +46,7 @@ import { import { initVisEditorDirective } from './visualization_editor'; import { initVisualizationDirective } from './visualization'; -import { - VISUALIZE_EMBEDDABLE_TYPE, - subscribeWithScope, - absoluteToParsedUrl, - KibanaParsedUrl, - migrateLegacyQuery, - DashboardConstants, -} from '../../legacy_imports'; +import { subscribeWithScope, migrateLegacyQuery, DashboardConstants } from '../../legacy_imports'; import { getServices } from '../../kibana_services'; @@ -79,7 +72,6 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState data: { query: queryService }, toastNotifications, chrome, - getBasePath, core: { docLinks, fatalErrors }, savedQueryService, uiSettings, @@ -653,29 +645,14 @@ function VisualizeAppController($scope, $route, $injector, $timeout, kbnUrlState }); if ($scope.isAddToDashMode()) { - const savedVisualizationParsedUrl = new KibanaParsedUrl({ - basePath: getBasePath(), - appId: kbnBaseUrl.slice('/app/'.length), - appPath: `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(savedVis.id)}`, - }); + const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(savedVis.id)}`; // Manually insert a new url so the back button will open the saved visualization. - history.replace(savedVisualizationParsedUrl.appPath); - setActiveUrl(savedVisualizationParsedUrl.appPath); + history.replace(appPath); + setActiveUrl(appPath); - const lastDashboardAbsoluteUrl = chrome.navLinks.get('kibana:dashboard').url; - const dashboardParsedUrl = absoluteToParsedUrl( - lastDashboardAbsoluteUrl, - getBasePath() - ); - dashboardParsedUrl.addQueryParameter( - DashboardConstants.ADD_EMBEDDABLE_TYPE, - VISUALIZE_EMBEDDABLE_TYPE - ); - dashboardParsedUrl.addQueryParameter( - DashboardConstants.ADD_EMBEDDABLE_ID, - savedVis.id - ); - history.push(dashboardParsedUrl.appPath); + const lastDashboardUrl = chrome.navLinks.get('kibana:dashboard').url; + const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardUrl, savedVis.id); + history.push(dashboardUrl); } else if (savedVis.id === $route.current.params.id) { chrome.docTitle.change(savedVis.lastSavedTitle); chrome.setBreadcrumbs($injector.invoke(getEditBreadcrumbs)); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts index fa5b91b00edaf..6e2f759c73f2f 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/index.ts @@ -19,3 +19,4 @@ export { useVisualizeAppState } from './visualize_app_state'; export { makeStateful } from './make_stateful'; +export { addEmbeddableToDashboardUrl } from './url_helper'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts new file mode 100644 index 0000000000000..e6974af9af832 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.test.ts @@ -0,0 +1,48 @@ +/* + * 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 { addEmbeddableToDashboardUrl } from './url_helper'; + +jest.mock('../../../legacy_imports', () => ({ + DashboardConstants: { + ADD_EMBEDDABLE_ID: 'addEmbeddableId', + ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + CREATE_NEW_DASHBOARD_URL: '/dashboard', + }, + VISUALIZE_EMBEDDABLE_TYPE: 'visualization', +})); + +describe('', () => { + it('addEmbeddableToDashboardUrl when dashboard is not saved', () => { + const id = '123eb456cd'; + const url = + "/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id)).toEqual( + `/dashboard?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=visualization` + ); + }); + it('addEmbeddableToDashboardUrl when dashboard is saved', () => { + const id = '123eb456cd'; + const url = + "/pep/app/kibana#/dashboard/9b780cd0-3dd3-11e8-b2b9-5d5dc1715159?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id)).toEqual( + `/dashboard/9b780cd0-3dd3-11e8-b2b9-5d5dc1715159?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=visualization` + ); + }); +}); diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts new file mode 100644 index 0000000000000..c7937c856184a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/editor/lib/url_helper.ts @@ -0,0 +1,39 @@ +/* + * 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 { parseUrl, stringify } from 'query-string'; +import { DashboardConstants, VISUALIZE_EMBEDDABLE_TYPE } from '../../../legacy_imports'; + +/** * + * Returns relative dashboard URL with added embeddableType and embeddableId query params + * eg. + * input: url: lol/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345 + * output: /dashboard?addEmbeddableType=visualization&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) + * @param url dasbhoard absolute url + * @param embeddableId id of the saved visualization + */ +export function addEmbeddableToDashboardUrl(dashboardUrl: string, embeddableId: string) { + const { url, query } = parseUrl(dashboardUrl); + const [, dashboardId] = url.split(DashboardConstants.CREATE_NEW_DASHBOARD_URL); + + query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = VISUALIZE_EMBEDDABLE_TYPE; + query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId; + + return `${DashboardConstants.CREATE_NEW_DASHBOARD_URL}${dashboardId}?${stringify(query)}`; +} diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js index 6c02afb672e4c..098633d046062 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing.js @@ -18,19 +18,17 @@ */ import { addHelpMenuToAppChrome } from '../help_menu/help_menu_util'; -import { VisualizeListingTable } from './visualize_listing_table'; +import { withI18nContext } from './visualize_listing_table'; import { VisualizeConstants } from '../visualize_constants'; import { i18n } from '@kbn/i18n'; import { getServices } from '../../kibana_services'; -import { wrapInI18nContext } from '../../legacy_imports'; - import { syncQueryStateWithUrl } from '../../../../../../../plugins/data/public'; -export function initListingDirective(app) { +export function initListingDirective(app, I18nContext) { app.directive('visualizeListingTable', reactDirective => - reactDirective(wrapInI18nContext(VisualizeListingTable)) + reactDirective(withI18nContext(I18nContext)) ); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js index b770625cd3d70..932ac8996e97e 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/listing/visualize_listing_table.js @@ -230,4 +230,10 @@ VisualizeListingTable.propTypes = { listingLimit: PropTypes.number.isRequired, }; -export { VisualizeListingTable }; +const withI18nContext = I18nContext => props => ( + + + +); + +export { withI18nContext }; diff --git a/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts b/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts index 1e7ac668697de..a4afac23f4842 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/np_ready/visualize_app.ts @@ -27,5 +27,5 @@ import { initListingDirective } from './listing/visualize_listing'; export function initVisualizeAppDirective(app: IModule, deps: VisualizeKibanaServices) { initEditorDirective(app, deps); - initListingDirective(app); + initListingDirective(app, deps.core.i18n.Context); } diff --git a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts index 59b814c98dd08..6d32579f5c541 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/plugin.ts @@ -140,7 +140,6 @@ export class VisualizePlugin implements Plugin { chrome: coreStart.chrome, data: dataStart, embeddable, - getBasePath: core.http.basePath.get, indexPatterns: dataStart.indexPatterns, localStorage: new Storage(localStorage), navigation, diff --git a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx b/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx index 0b6d4e5982a00..58e67b5064da5 100644 --- a/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx +++ b/src/legacy/core_plugins/vis_default_editor/public/default_editor_controller.tsx @@ -20,7 +20,6 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n/react'; import { EventEmitter } from 'events'; import { EditorRenderProps } from 'src/legacy/core_plugins/kibana/public/visualize/np_ready/types'; @@ -83,7 +82,7 @@ class DefaultEditorController { render({ data, core, ...props }: EditorRenderProps) { render( - + - , + , this.el ); } 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/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/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/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/config.js b/test/plugin_functional/config.js index 7017c01cc5634..c7fa0f40e1d0c 100644 --- a/test/plugin_functional/config.js +++ b/test/plugin_functional/config.js @@ -32,7 +32,6 @@ export default async function({ readConfigFile }) { return { testFiles: [ - require.resolve('./test_suites/app_plugins'), require.resolve('./test_suites/custom_visualizations'), require.resolve('./test_suites/panel_actions'), require.resolve('./test_suites/embeddable_explorer'), 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/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js b/test/plugin_functional/plugins/kbn_top_nav/public/index.ts similarity index 75% rename from test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js rename to test/plugin_functional/plugins/kbn_top_nav/public/index.ts index b2497a824ba2b..bd478f1dd3bdb 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/index.js +++ b/test/plugin_functional/plugins/kbn_top_nav/public/index.ts @@ -17,10 +17,8 @@ * under the License. */ -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - hacks: ['plugins/kbn_tp_custom_visualizations/self_changing_vis/self_changing_vis'], - }, - }); -} +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/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/public/app.js b/test/plugin_functional/plugins/kbn_top_nav/public/types.ts similarity index 81% rename from test/plugin_functional/plugins/kbn_tp_sample_app_plugin/public/app.js rename to test/plugin_functional/plugins/kbn_top_nav/public/types.ts index a7a516bb0cdbd..c70a78bedb54f 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/public/app.js +++ b/test/plugin_functional/plugins/kbn_top_nav/public/types.ts @@ -16,8 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -import 'ui/autoload/all'; -import chrome from 'ui/chrome'; +import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; -chrome.setRootTemplate('
Super simple app plugin
'); +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/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/index.js b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts similarity index 68% rename from test/plugin_functional/plugins/kbn_tp_sample_app_plugin/index.js rename to test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts index ff4be4113eeb3..cb821a2698479 100644 --- a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/index.js +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/public/index.ts @@ -17,14 +17,14 @@ * under the License. */ -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - title: 'Test Plugin App', - description: 'This is a sample plugin for the functional tests.', - main: 'plugins/kbn_tp_sample_app_plugin/app', - }, - }, - }); -} +import { PluginInitializer } from 'kibana/public'; +import { + CustomVisualizationsPublicPlugin, + CustomVisualizationsSetup, + CustomVisualizationsStart, +} from './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_sample_app_plugin/package.json b/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/package.json deleted file mode 100644 index 2537bb9a7ed5c..0000000000000 --- a/test/plugin_functional/plugins/kbn_tp_sample_app_plugin/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "kbn_tp_sample_app_plugin", - "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/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/app_plugins/app_navigation.js b/test/plugin_functional/test_suites/app_plugins/app_navigation.js deleted file mode 100644 index bb39e52287556..0000000000000 --- a/test/plugin_functional/test_suites/app_plugins/app_navigation.js +++ /dev/null @@ -1,38 +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 expect from '@kbn/expect'; - -export default function({ getService, getPageObjects }) { - const appsMenu = getService('appsMenu'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'header', 'home']); - - describe('app navigation', function describeIndexTests() { - before(async () => { - await PageObjects.common.navigateToApp('settings'); - }); - - it('should show nav link that navigates to the app', async () => { - await appsMenu.clickLink('Test Plugin App'); - const pluginContent = await testSubjects.find('pluginContent'); - expect(await pluginContent.getVisibleText()).to.be('Super simple app plugin'); - }); - }); -} 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/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/graph/index.ts b/x-pack/legacy/plugins/graph/index.ts index 53d32a836cfa1..5c7f8fa46c18b 100644 --- a/x-pack/legacy/plugins/graph/index.ts +++ b/x-pack/legacy/plugins/graph/index.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; - // @ts-ignore import migrations from './migrations'; import mappings from './mappings.json'; @@ -30,40 +28,5 @@ export const graph: LegacyPluginInitializer = kibana => { .default('configAndData'), }).default(); }, - - init(server) { - server.plugins.xpack_main.registerFeature({ - id: 'graph', - name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { - defaultMessage: 'Graph', - }), - order: 1200, - icon: 'graphApp', - navLinkId: 'graph', - app: ['graph', 'kibana'], - catalogue: ['graph'], - validLicenses: ['platinum', 'enterprise', 'trial'], - privileges: { - all: { - app: ['graph', 'kibana'], - catalogue: ['graph'], - savedObject: { - all: ['graph-workspace'], - read: ['index-pattern'], - }, - ui: ['save', 'delete'], - }, - read: { - app: ['graph', 'kibana'], - catalogue: ['graph'], - savedObject: { - all: [], - read: ['index-pattern', 'graph-workspace'], - }, - ui: [], - }, - }, - }); - }, }); }; diff --git a/x-pack/legacy/plugins/lens/public/helpers/index.ts b/x-pack/legacy/plugins/lens/public/helpers/index.ts new file mode 100644 index 0000000000000..f464b5dcc97a3 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/helpers/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { addEmbeddableToDashboardUrl, getUrlVars } from './url_helper'; diff --git a/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts b/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts new file mode 100644 index 0000000000000..9c59c9a96d00f --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/helpers/url_helper.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../legacy_imports', () => ({ + DashboardConstants: { + ADD_EMBEDDABLE_ID: 'addEmbeddableId', + ADD_EMBEDDABLE_TYPE: 'addEmbeddableType', + }, +})); + +import { addEmbeddableToDashboardUrl, getUrlVars } from './url_helper'; + +describe('Dashboard URL Helper', () => { + it('addEmbeddableToDashboardUrl', () => { + const id = '123eb456cd'; + const urlVars = { + x: '1', + y: '2', + z: '3', + }; + const url = + "/pep/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!())"; + expect(addEmbeddableToDashboardUrl(url, id, urlVars)).toEqual( + `/pep/app/kibana#/dashboard?_a=%28description%3A%27%27%2Cfilters%3A%21%28%29%29&_g=%28refreshInterval%3A%28pause%3A%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3Anow-15m%2Cto%3Anow%29%29&addEmbeddableId=${id}&addEmbeddableType=lens&x=1&y=2&z=3` + ); + }); + + it('getUrlVars', () => { + let url = + "http://localhost:5601/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))&_a=(description:'',filters:!()"; + expect(getUrlVars(url)).toEqual({ + _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))', + _a: "(description:'',filters:!()", + }); + url = 'http://mybusiness.mydomain.com/app/kibana#/dashboard?x=y&y=z'; + expect(getUrlVars(url)).toEqual({ + x: 'y', + y: 'z', + }); + url = 'http://localhost:5601/app/kibana#/dashboard/777182'; + expect(getUrlVars(url)).toEqual({}); + url = + 'http://localhost:5601/app/kibana#/dashboard/777182?title=Some%20Dashboard%20With%20Spaces'; + expect(getUrlVars(url)).toEqual({ title: 'Some Dashboard With Spaces' }); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts b/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts new file mode 100644 index 0000000000000..fca44195b98c4 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/helpers/url_helper.ts @@ -0,0 +1,45 @@ +/* + * 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 { parseUrl, stringify } from 'query-string'; +import { DashboardConstants } from '../legacy_imports'; + +type UrlVars = Record; + +/** + * Return query params from URL + * @param url given url + */ +export function getUrlVars(url: string): Record { + const vars: UrlVars = {}; + for (const [, key, value] of url.matchAll(/[?&]+([^=&]+)=([^&]*)/gi)) { + vars[key] = decodeURIComponent(value); + } + return vars; +} + +/** * + * Returns dashboard URL with added embeddableType and embeddableId query params + * eg. + * input: url: /lol/app/kibana#/dashboard?_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)), embeddableId: 12345 + * output: /lol/app/kibana#/dashboard?addEmbeddableType=lens&addEmbeddableId=12345&_g=(refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now)) + * @param url dasbhoard absolute url + * @param embeddableId id of the saved visualization + * @param urlVars url query params + */ +export function addEmbeddableToDashboardUrl(url: string, embeddableId: string, urlVars: UrlVars) { + const dashboardParsedUrl = parseUrl(url); + const keys = Object.keys(urlVars).sort(); + + keys.forEach(key => { + dashboardParsedUrl.query[key] = urlVars[key]; + }); + dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_TYPE] = 'lens'; + dashboardParsedUrl.query[DashboardConstants.ADD_EMBEDDABLE_ID] = embeddableId; + const query = stringify(dashboardParsedUrl.query); + + return `${dashboardParsedUrl.url}?${query}`; +} diff --git a/x-pack/legacy/plugins/lens/public/legacy_imports.ts b/x-pack/legacy/plugins/lens/public/legacy_imports.ts index 5c5afc1a87df0..857443ae0fa1c 100644 --- a/x-pack/legacy/plugins/lens/public/legacy_imports.ts +++ b/x-pack/legacy/plugins/lens/public/legacy_imports.ts @@ -7,3 +7,4 @@ import { npSetup } from 'ui/new_platform'; export const { visualizations } = npSetup.plugins; export { VisualizationsSetup } from '../../../../../src/plugins/visualizations/public'; +export { DashboardConstants } from '../../../../../src/legacy/core_plugins/kibana/public/dashboard'; diff --git a/x-pack/legacy/plugins/lens/public/plugin.tsx b/x-pack/legacy/plugins/lens/public/plugin.tsx index fad1371199e6a..45817fdc3c05f 100644 --- a/x-pack/legacy/plugins/lens/public/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/plugin.tsx @@ -8,10 +8,14 @@ import React from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { HashRouter, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { render, unmountComponentAtNode } from 'react-dom'; -import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; -import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; import rison, { RisonObject, RisonValue } from 'rison-node'; import { isObject } from 'lodash'; + +import { AppMountParameters, CoreSetup, CoreStart } from 'src/core/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from 'src/plugins/data/public'; +import { EmbeddableSetup, EmbeddableStart } from 'src/plugins/embeddable/public'; +import { ExpressionsSetup, ExpressionsStart } from 'src/plugins/expressions/public'; +import { KibanaLegacySetup } from 'src/plugins/kibana_legacy/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { EditorFrameService } from './editor_frame_service'; import { IndexPatternDatasource } from './indexpattern_datasource'; @@ -19,7 +23,6 @@ import { addHelpMenuToAppChrome } from './help_menu_util'; import { SavedObjectIndexStore } from './persistence'; import { XyVisualization } from './xy_visualization'; import { MetricVisualization } from './metric_visualization'; -import { ExpressionsSetup, ExpressionsStart } from '../../../../../src/plugins/expressions/public'; import { DatatableVisualization } from './datatable_visualization'; import { App } from './app_plugin'; import { @@ -30,17 +33,12 @@ import { } from './lens_ui_telemetry'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; -import { KibanaLegacySetup } from '../../../../../src/plugins/kibana_legacy/public'; import { NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../../../../plugins/lens/common'; -import { - addEmbeddableToDashboardUrl, - getUrlVars, - getLensUrlFromDashboardAbsoluteUrl, -} from '../../../../../src/legacy/core_plugins/kibana/public/dashboard/np_ready/url_helper'; -import { EmbeddableSetup, EmbeddableStart } from '../../../../../src/plugins/embeddable/public'; +import { addEmbeddableToDashboardUrl, getUrlVars } from './helpers'; import { EditorFrameStart } from './types'; import { getLensAliasConfig } from './vis_type_alias'; -import { VisualizationsSetup } from './legacy_imports'; +import { VisualizationsSetup, DashboardConstants } from './legacy_imports'; + export interface LensPluginSetupDependencies { kibanaLegacy: KibanaLegacySetup; expressions: ExpressionsSetup; @@ -144,40 +142,24 @@ export class LensPlugin { routeProps.history.push(`/lens/edit/${id}`); } else if (addToDashboardMode && id) { routeProps.history.push(`/lens/edit/${id}`); - const url = coreStart.chrome.navLinks.get('kibana:dashboard'); - if (!url) { + const lastDashboardLink = coreStart.chrome.navLinks.get('kibana:dashboard'); + if (!lastDashboardLink || !lastDashboardLink.url) { throw new Error('Cannot get last dashboard url'); } - const lastDashboardAbsoluteUrl = url.url; - const basePath = coreStart.http.basePath.get(); - const lensUrl = getLensUrlFromDashboardAbsoluteUrl( - lastDashboardAbsoluteUrl, - basePath, - id - ); - if (!lastDashboardAbsoluteUrl || !lensUrl) { - throw new Error('Cannot get last dashboard url'); - } - window.history.pushState({}, '', lensUrl); - const urlVars = getUrlVars(lastDashboardAbsoluteUrl); + const urlVars = getUrlVars(lastDashboardLink.url); updateUrlTime(urlVars); // we need to pass in timerange in query params directly - const dashboardParsedUrl = addEmbeddableToDashboardUrl( - lastDashboardAbsoluteUrl, - basePath, - id, - urlVars - ); - if (!dashboardParsedUrl) { - throw new Error('Problem parsing dashboard url'); - } - window.history.pushState({}, '', dashboardParsedUrl); + const dashboardUrl = addEmbeddableToDashboardUrl(lastDashboardLink.url, id, urlVars); + window.history.pushState({}, '', dashboardUrl); } }; const renderEditor = (routeProps: RouteComponentProps<{ id?: string }>) => { trackUiEvent('loaded'); const addToDashboardMode = - !!routeProps.location.search && routeProps.location.search.includes('addToDashboard'); + !!routeProps.location.search && + routeProps.location.search.includes( + DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM + ); return ( { 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/cypress/integration/signal_detection_rules_custom.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts index 86b8ca1ff3894..b7e42f7e46a70 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/signal_detection_rules_custom.spec.ts @@ -8,6 +8,7 @@ import { newRule, totalNumberOfPrebuiltRules } from '../objects/rule'; import { ABOUT_FALSE_POSITIVES, + ABOUT_INVESTIGATION_NOTES, ABOUT_MITRE, ABOUT_RISK, ABOUT_RULE_DESCRIPTION, @@ -19,6 +20,9 @@ import { DEFINITION_INDEX_PATTERNS, DEFINITION_TIMELINE, DEFINITION_STEP, + INVESTIGATION_NOTES_MARKDOWN, + INVESTIGATION_NOTES_TOGGLE, + RULE_ABOUT_DETAILS_HEADER_TOGGLE, RULE_NAME_HEADER, SCHEDULE_LOOPBACK, SCHEDULE_RUNS, @@ -170,6 +174,13 @@ describe('Signal detection rules, custom', () => { .invoke('text') .should('eql', expectedTags); + cy.get(RULE_ABOUT_DETAILS_HEADER_TOGGLE) + .eq(INVESTIGATION_NOTES_TOGGLE) + .click({ force: true }); + cy.get(ABOUT_INVESTIGATION_NOTES) + .invoke('text') + .should('eql', INVESTIGATION_NOTES_MARKDOWN); + cy.get(DEFINITION_INDEX_PATTERNS).then(patterns => { cy.wrap(patterns).each((pattern, index) => { cy.wrap(pattern) diff --git a/x-pack/legacy/plugins/siem/cypress/objects/rule.ts b/x-pack/legacy/plugins/siem/cypress/objects/rule.ts index a3c648c9cc934..37c325c3b8030 100644 --- a/x-pack/legacy/plugins/siem/cypress/objects/rule.ts +++ b/x-pack/legacy/plugins/siem/cypress/objects/rule.ts @@ -22,6 +22,7 @@ export interface CustomRule { referenceUrls: string[]; falsePositivesExamples: string[]; mitre: Mitre[]; + note: string; } export interface MachineLearningRule { @@ -36,6 +37,7 @@ export interface MachineLearningRule { referenceUrls: string[]; falsePositivesExamples: string[]; mitre: Mitre[]; + note: string; } const mitre1: Mitre = { @@ -58,6 +60,7 @@ export const newRule: CustomRule = { referenceUrls: ['https://www.google.com/', 'https://elastic.co/'], falsePositivesExamples: ['False1', 'False2'], mitre: [mitre1, mitre2], + note: '# test markdown', }; export const machineLearningRule: MachineLearningRule = { @@ -71,4 +74,5 @@ export const machineLearningRule: MachineLearningRule = { referenceUrls: ['https://elastic.co/'], falsePositivesExamples: ['False1'], mitre: [mitre1], + note: '# test markdown', }; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts b/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts index e603e2ee5158e..db9866cdf7f63 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/create_new_rule.ts @@ -24,7 +24,8 @@ export const CUSTOM_QUERY_INPUT = '[data-test-subj="queryInput"]'; export const DEFINE_CONTINUE_BUTTON = '[data-test-subj="define-continue"]'; -export const SCHEDULE_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]'; +export const INVESTIGATION_NOTES_TEXTAREA = + '[data-test-subj="detectionEngineStepAboutRuleNote"] textarea'; export const FALSE_POSITIVES_INPUT = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] input'; @@ -53,6 +54,8 @@ export const RULE_DESCRIPTION_INPUT = export const RULE_NAME_INPUT = '[data-test-subj="detectionEngineStepAboutRuleName"] [data-test-subj="input"]'; +export const SCHEDULE_CONTINUE_BUTTON = '[data-test-subj="schedule-continue"]'; + export const SEVERITY_DROPDOWN = '[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]'; diff --git a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts index fc9e4c56dd824..ec57e142125da 100644 --- a/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts +++ b/x-pack/legacy/plugins/siem/cypress/screens/rule_details.ts @@ -6,6 +6,8 @@ export const ABOUT_FALSE_POSITIVES = 3; +export const ABOUT_INVESTIGATION_NOTES = '[data-test-subj="stepAboutDetailsNoteContent"]'; + export const ABOUT_MITRE = 4; export const ABOUT_RULE_DESCRIPTION = '[data-test-subj=stepAboutRuleDetailsToggleDescriptionText]'; @@ -32,10 +34,16 @@ export const DEFINITION_INDEX_PATTERNS = export const DEFINITION_STEP = '[data-test-subj=definitionRule] [data-test-subj="listItemColumnStepRuleDescription"] .euiDescriptionList__description'; +export const INVESTIGATION_NOTES_MARKDOWN = 'test markdown'; + +export const INVESTIGATION_NOTES_TOGGLE = 1; + export const MACHINE_LEARNING_JOB_ID = '[data-test-subj="machineLearningJobId"]'; export const MACHINE_LEARNING_JOB_STATUS = '[data-test-subj="machineLearningJobStatus" ]'; +export const RULE_ABOUT_DETAILS_HEADER_TOGGLE = '[data-test-subj="stepAboutDetailsToggle"]'; + export const RULE_NAME_HEADER = '[data-test-subj="header-page-title"]'; export const RULE_TYPE = 0; diff --git a/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts b/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts index 59ed156bf56b1..a20ad372a689c 100644 --- a/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts +++ b/x-pack/legacy/plugins/siem/cypress/tasks/create_new_rule.ts @@ -14,6 +14,7 @@ import { CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, FALSE_POSITIVES_INPUT, + INVESTIGATION_NOTES_TEXTAREA, MACHINE_LEARNING_DROPDOWN, MACHINE_LEARNING_LIST, MACHINE_LEARNING_TYPE, @@ -82,6 +83,8 @@ export const fillAboutRuleAndContinue = (rule: CustomRule | MachineLearningRule) cy.get(MITRE_BTN).click({ force: true }); }); + cy.get(INVESTIGATION_NOTES_TEXTAREA).type(rule.note, { force: true }); + cy.get(ABOUT_CONTINUE_BTN) .should('exist') .click({ force: true }); diff --git a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx index e5066eef18c8b..a0e0c70d2f204 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx +++ b/x-pack/legacy/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx @@ -8,7 +8,11 @@ import styled from 'styled-components'; import React, { useState, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui'; import { SiemJob } from '../types'; -import { isJobLoading, isJobStarted, isJobFailed } from '../../ml/helpers'; +import { + isJobLoading, + isJobFailed, + isJobStarted, +} from '../../../../common/detection_engine/ml_helpers'; const StaticSwitch = styled(EuiSwitch)` .euiSwitch__thumb, diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts index 60ebd2578b7c0..a779d579bf4d1 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.test.ts @@ -4,16 +4,48 @@ * you may not use this file except in compliance with the Elastic License. */ import { cloneDeep, omit } from 'lodash/fp'; +import { Dispatch } from 'redux'; -import { mockTimelineResults } from '../../mock/timeline_results'; +import { + mockTimelineResults, + mockTimelineResult, + mockTimelineModel, +} from '../../mock/timeline_results'; import { timelineDefaults } from '../../store/timeline/defaults'; +import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../store/inputs/actions'; +import { + setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, + applyKqlFilterQuery as dispatchApplyKqlFilterQuery, + addTimeline as dispatchAddTimeline, + addNote as dispatchAddGlobalTimelineNote, +} from '../../store/timeline/actions'; +import { + addNotes as dispatchAddNotes, + updateNote as dispatchUpdateNote, +} from '../../store/app/actions'; import { defaultTimelineToTimelineModel, getNotesCount, getPinnedEventCount, isUntitled, + omitTypenameInTimeline, + dispatchUpdateTimeline, } from './helpers'; -import { OpenTimelineResult } from './types'; +import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; +import { KueryFilterQueryKind } from '../../store/model'; +import { Note } from '../../lib/note'; +import moment from 'moment'; +import sinon from 'sinon'; + +jest.mock('../../store/inputs/actions'); +jest.mock('../../store/timeline/actions'); +jest.mock('../../store/app/actions'); +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); describe('helpers', () => { let mockResults: OpenTimelineResult[]; @@ -620,4 +652,229 @@ describe('helpers', () => { }); }); }); + + describe('omitTypenameInTimeline', () => { + test('it does not modify the passed in timeline if no __typename exists', () => { + const result = omitTypenameInTimeline(mockTimelineResult); + + expect(result).toEqual(mockTimelineResult); + }); + + test('it returns timeline with __typename removed when it exists', () => { + const mockTimeline = { + ...mockTimelineResult, + __typename: 'something, something', + }; + const result = omitTypenameInTimeline(mockTimeline); + const expectedTimeline = { + ...mockTimeline, + __typename: undefined, + }; + + expect(result).toEqual(expectedTimeline); + }); + }); + + describe('dispatchUpdateTimeline', () => { + const dispatch = jest.fn() as Dispatch; + const anchor = '2020-03-27T20:34:51.337Z'; + const unix = moment(anchor).valueOf(); + let clock: sinon.SinonFakeTimers; + let timelineDispatch: DispatchUpdateTimeline; + + beforeEach(() => { + jest.clearAllMocks(); + + clock = sinon.useFakeTimers(unix); + timelineDispatch = dispatchUpdateTimeline(dispatch); + }); + + afterEach(function() { + clock.restore(); + }); + + test('it invokes date range picker dispatch', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ + from: 1585233356356, + to: 1585233716356, + }); + }); + + test('it invokes add timeline dispatch', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddTimeline).toHaveBeenCalledWith({ + id: 'timeline-1', + timeline: mockTimelineModel, + }); + }); + + test('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + }); + + test('it does not invoke notes dispatch if duplicate is true', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddNotes).not.toHaveBeenCalled(); + }); + + test('it does not invoke kql filter query dispatches if timeline.kqlQuery.kuery is null', () => { + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: null, + serializedQuery: 'some-serialized-query', + }, + filterQueryDraft: null, + }, + }; + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimeline, + })(); + + expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + }); + + test('it invokes kql filter query dispatches if timeline.kqlQuery.filterQuery.kuery is not null', () => { + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, + serializedQuery: 'some-serialized-query', + }, + filterQueryDraft: null, + }, + }; + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimeline, + })(); + + expect(dispatchSetKqlFilterQueryDraft).toHaveBeenCalledWith({ + id: 'timeline-1', + filterQueryDraft: { + kind: 'kuery', + expression: 'expression', + }, + }); + expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ + id: 'timeline-1', + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'expression', + }, + serializedQuery: 'some-serialized-query', + }, + }); + }); + + test('it invokes dispatchAddNotes if duplicate is false', () => { + timelineDispatch({ + duplicate: false, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [ + { + created: 1585233356356, + updated: 1585233356356, + noteId: 'note-id', + note: 'I am a note', + }, + ], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddGlobalTimelineNote).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).not.toHaveBeenCalled(); + expect(dispatchAddNotes).toHaveBeenCalledWith({ + notes: [ + { + created: new Date('2020-03-26T14:35:56.356Z'), + id: 'note-id', + lastEdit: new Date('2020-03-26T14:35:56.356Z'), + note: 'I am a note', + user: 'unknown', + saveObjectId: 'note-id', + version: undefined, + }, + ], + }); + }); + + test('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + ruleNote: '# this would be some markdown', + })(); + const expectedNote: Note = { + created: new Date(anchor), + id: 'uuid.v4()', + lastEdit: null, + note: '# this would be some markdown', + saveObjectId: null, + user: 'elastic', + version: null, + }; + + expect(dispatchAddNotes).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); + expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ + id: 'timeline-1', + noteId: 'uuid.v4()', + }); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts index 4f7d6cd64f1d9..16ba2de872bd1 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/helpers.ts @@ -5,18 +5,23 @@ */ import ApolloClient from 'apollo-client'; -import { getOr, set } from 'lodash/fp'; +import { getOr, set, isEmpty } from 'lodash/fp'; import { Action } from 'typescript-fsa'; +import uuid from 'uuid'; import { Dispatch } from 'redux'; import { oneTimelineQuery } from '../../containers/timeline/one/index.gql_query'; import { TimelineResult, GetOneTimeline, NoteResult } from '../../graphql/types'; -import { addNotes as dispatchAddNotes } from '../../store/app/actions'; +import { + addNotes as dispatchAddNotes, + updateNote as dispatchUpdateNote, +} from '../../store/app/actions'; import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../store/inputs/actions'; import { setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, applyKqlFilterQuery as dispatchApplyKqlFilterQuery, addTimeline as dispatchAddTimeline, + addNote as dispatchAddGlobalTimelineNote, } from '../../store/timeline/actions'; import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; @@ -32,6 +37,7 @@ import { import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types'; import { getTimeRangeSettings } from '../../utils/default_date_settings'; +import { createNote } from '../notes/helpers'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -250,6 +256,7 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli notes, timeline, to, + ruleNote, }: UpdateTimeline): (() => void) => () => { dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); dispatch(dispatchAddTimeline({ id, timeline })); @@ -281,6 +288,14 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli }) ); } + + if (duplicate && ruleNote != null && !isEmpty(ruleNote)) { + const getNewNoteId = (): string => uuid.v4(); + const newNote = createNote({ newNote: ruleNote, getNewNoteId }); + dispatch(dispatchUpdateNote({ note: newNote })); + dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); + } + if (!duplicate) { dispatch( dispatchAddNotes({ diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx index 8805037ecc4ca..b0f8963dd501e 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -70,6 +70,25 @@ describe('#getActionsColumns', () => { expect(wrapper.find('[data-test-subj="open-duplicate"]').exists()).toBe(true); }); + test('it renders only duplicate icon (without heading)', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('[data-test-subj="open-duplicate"]') + .first() + .text() + ).toEqual(''); + }); + test('it does NOT render the duplicate timeline when actionTimelineToShow is NOT including the action duplicate)', () => { const testProps: TimelinesTableProps = { ...getMockTimelinesTableProps(mockResults), diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx index 8588beed64b79..746503308c833 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx @@ -42,6 +42,7 @@ export const getActionsColumns = ({ timelineId: savedObjectId ?? '', }); }, + type: 'icon', enabled: ({ savedObjectId }: OpenTimelineResult) => savedObjectId != null, description: i18n.OPEN_AS_DUPLICATE, 'data-test-subj': 'open-duplicate', diff --git a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts index 51c72681c0863..b7cc92ebd183f 100644 --- a/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/open_timeline/types.ts @@ -173,6 +173,7 @@ export interface UpdateTimeline { notes: NoteResult[] | null | undefined; timeline: TimelineModel; to: number; + ruleNote?: string; } export type DispatchUpdateTimeline = ({ @@ -182,4 +183,5 @@ export type DispatchUpdateTimeline = ({ notes, timeline, to, + ruleNote, }: UpdateTimeline) => () => void; diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx index fa474c4d601ad..cf1a4ebec9bb6 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiPopover, EuiSelectableOption } from '@elastic/eui'; +import { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useDispatch } from 'react-redux'; @@ -62,13 +62,15 @@ export const InsertTimelinePopoverComponent: React.FC = ({ const insertTimelineButton = useMemo( () => ( - + {i18n.INSERT_TIMELINE}

}> + +
), [handleOpenPopover, isDisabled] ); 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/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..cb3df78257dc1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -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_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/containers/timeline/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts index c54238c5d8687..53d0b98570bcb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts +++ b/x-pack/legacy/plugins/siem/public/containers/timeline/index.gql_query.ts @@ -206,6 +206,7 @@ export const timelineQuery = gql` query to filters + note } } suricata { diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json index 5d43024625d0d..2a9dd8f2aacfe 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json +++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json @@ -4696,6 +4696,14 @@ "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "note", + "description": "", + "args": [], + "type": { "kind": "SCALAR", "name": "ToStringArray", "ofType": null }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts index a5d1e3fbcba27..e15c099a007ad 100644 --- a/x-pack/legacy/plugins/siem/public/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts @@ -1012,6 +1012,8 @@ export interface RuleField { updated_by?: Maybe; version?: Maybe; + + note?: Maybe; } export interface SuricataEcsFields { @@ -4660,6 +4662,8 @@ export namespace GetTimelineQuery { to: Maybe; filters: Maybe; + + note: Maybe; }; export type Suricata = { 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 + + + - column.id !== 'event.action'), + dateRange: { start: 1584539198929, end: 1584539558929 }, + description: 'This is a sample rule description', + eventType: 'all', + filters: [ + { + meta: { + key: 'host.name', + negate: false, + params: '"{"query":"placeholder"}"', + type: 'phrase', + }, + query: '"{"match_phrase":{"host.name":"placeholder"}}"', + }, + ], + kqlMode: 'filter', + title: 'Test rule', + savedQueryId: null, + sort: { columnId: '@timestamp', sortDirection: 'desc' }, + version: '1', +}; + +export const mockTimelineApolloResult = { + data: { + getOneTimeline: mockTimelineResult, + }, + loading: false, + networkStatus: 7, + stale: false, +}; + +export const defaultTimelineProps: CreateTimelineProps = { + from: 1541444305937, + timeline: { + columns: [ + { columnHeaderType: 'not-filtered', id: '@timestamp', width: 190 }, + { columnHeaderType: 'not-filtered', id: 'message', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'event.category', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'event.action', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'host.name', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'source.ip', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'destination.ip', width: 180 }, + { columnHeaderType: 'not-filtered', id: 'user.name', width: 180 }, + ], + dataProviders: [ + { + and: [], + enabled: true, + excluded: false, + id: + 'send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-1', + kqlQuery: '', + name: '1', + queryMatch: { field: '_id', operator: ':', value: '1' }, + }, + ], + dateRange: { end: 1541444605937, start: 1541444305937 }, + deletedEventIds: [], + description: '', + eventIdToNoteIds: {}, + eventType: 'all', + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'timeline-1', + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { kuery: { expression: '', kind: 'kuery' }, serializedQuery: '' }, + filterQueryDraft: { expression: '', kind: 'kuery' }, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: null, + selectedEventIds: {}, + show: false, + showCheckboxes: false, + showRowRenderers: true, + sort: { columnId: '@timestamp', sortDirection: Direction.desc }, + title: '', + version: null, + width: 1100, + }, + to: 1541444605937, + ruleNote: '# this is some markdown documentation', +}; 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..5f61ccf68fc86 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 @@ -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', () => { 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..dcfa1712c6ef9 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 @@ -21,7 +21,7 @@ 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'; @@ -107,11 +107,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 +137,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 +158,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) => { @@ -289,7 +307,7 @@ export const AllCases = React.memo(() => { - {(isCasesLoading || isDeleting) && !isDataEmpty && ( + {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( )} 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..04b79967aa36e 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 @@ -34,7 +34,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] 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..18d5191fe6d33 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}; `} `; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts index d1f04a34b7bad..49caeae1c3a34 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/configure_cases/translations.ts @@ -155,7 +155,7 @@ export const WARNING_NO_CONNECTOR_MESSAGE = i18n.translate( 'xpack.siem.case.configureCases.warningMessage', { defaultMessage: - 'Configuration seems to be invalid. The selected connector is missing. Did you delete the connector?', + 'The selected connector has been deleted. Either select a different connector or create a new one.', } ); 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..340e24e8fa55b 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 @@ -154,6 +154,7 @@ export const UserActionItem = ({ labelQuoteAction={labelQuoteAction} labelTitle={labelTitle ?? <>} 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..af1a1fdff26ce 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'; @@ -33,6 +40,7 @@ interface UserActionTitleProps { labelQuoteAction?: string; labelTitle: JSX.Element; linkId?: string | null; + fullName?: string | null; updatedAt?: string | null; username: string; onEdit?: (id: string) => void; @@ -48,6 +56,7 @@ export const UserActionTitle = ({ labelQuoteAction, labelTitle, linkId, + fullName, username, updatedAt, onEdit, @@ -105,7 +114,9 @@ export const UserActionTitle = ({ - {username} + {fullName ?? username}

}> + {username} +
{labelTitle} @@ -137,20 +148,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..87a446c45d891 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,8 +41,8 @@ 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) => ( @@ -49,11 +50,13 @@ const renderUsers = ( -

- - {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( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx new file mode 100644 index 0000000000000..8aaed08a0a0a1 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx @@ -0,0 +1,380 @@ +/* + * 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 sinon from 'sinon'; +import moment from 'moment'; + +import { sendSignalToTimelineAction, determineToAndFrom } from './actions'; +import { + mockEcsDataWithSignal, + defaultTimelineProps, + apolloClient, + mockTimelineApolloResult, +} from '../../../../mock/'; +import { CreateTimeline, UpdateTimelineLoading } from './types'; +import { Ecs } from '../../../../graphql/types'; + +jest.mock('apollo-client'); + +describe('signals actions', () => { + const anchor = '2020-03-01T17:59:46.349Z'; + const unix = moment(anchor).valueOf(); + let createTimeline: CreateTimeline; + let updateTimelineIsLoading: UpdateTimelineLoading; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + createTimeline = jest.fn() as jest.Mocked; + updateTimelineIsLoading = jest.fn() as jest.Mocked; + + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResult); + + clock = sinon.useFakeTimers(unix); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('sendSignalToTimelineAction', () => { + describe('timeline id is NOT empty string and apollo client exists', () => { + test('it invokes updateTimelineIsLoading to set to true', async () => { + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + }); + + test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + const expected = { + from: 1541444305937, + timeline: { + columns: [ + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: '@timestamp', + placeholder: undefined, + type: undefined, + width: 190, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'message', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'event.category', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'host.name', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'source.ip', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'destination.ip', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'user.name', + placeholder: undefined, + type: undefined, + width: 180, + }, + ], + dataProviders: [], + dateRange: { + end: 1541444605937, + start: 1541444305937, + }, + deletedEventIds: [], + description: 'This is a sample rule description', + eventIdToNoteIds: {}, + eventType: 'all', + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + key: 'host.name', + negate: false, + params: { + query: 'apache', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'apache', + }, + }, + }, + ], + highlightedDropAndProviderId: '', + historyIds: [], + id: '', + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + expression: '', + kind: 'kuery', + }, + serializedQuery: '', + }, + filterQueryDraft: { + expression: '', + kind: 'kuery', + }, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: null, + selectedEventIds: {}, + show: true, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: '', + version: null, + width: 1100, + }, + to: 1541444605937, + ruleNote: '# this is some markdown documentation', + }; + + expect(createTimeline).toHaveBeenCalledWith(expected); + }); + + test('it invokes createTimeline with kqlQuery.filterQuery.kuery.kind as "kuery" if not specified in returned timeline template', async () => { + const mockTimelineApolloResultModified = { + ...mockTimelineApolloResult, + kqlQuery: { + filterQuery: { + kuery: { + expression: [''], + }, + }, + filterQueryDraft: { + expression: [''], + }, + }, + }; + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + // @ts-ignore + const createTimelineArg = createTimeline.mock.calls[0][0]; + + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); + }); + + test('it invokes createTimeline with kqlQuery.filterQueryDraft.kuery.kind as "kuery" if not specified in returned timeline template', async () => { + const mockTimelineApolloResultModified = { + ...mockTimelineApolloResult, + kqlQuery: { + filterQuery: { + kuery: { + expression: [''], + }, + }, + filterQueryDraft: { + expression: [''], + }, + }, + }; + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + // @ts-ignore + const createTimelineArg = createTimeline.mock.calls[0][0]; + + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimelineArg.timeline.kqlQuery.filterQueryDraft.kind).toEqual('kuery'); + }); + + test('it invokes createTimeline with default timeline if apolloClient throws', async () => { + jest.spyOn(apolloClient, 'query').mockImplementation(() => { + throw new Error('Test error'); + }); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: 'timeline-1', + isLoading: false, + }); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + + describe('timelineId is empty string', () => { + test('it invokes createTimeline with timelineDefaults', async () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + signal: { + rule: { + ...mockEcsDataWithSignal.signal?.rule!, + timeline_id: null, + }, + }, + }; + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: ecsDataMock, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + + describe('apolloClient is not defined', () => { + test('it invokes createTimeline with timelineDefaults', async () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + signal: { + rule: { + ...mockEcsDataWithSignal.signal?.rule!, + timeline_id: [''], + }, + }, + }; + + await sendSignalToTimelineAction({ + createTimeline, + ecsData: ecsDataMock, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + }); + + describe('determineToAndFrom', () => { + test('it uses ecs.Data.timestamp if one is provided', () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + timestamp: '2020-03-20T17:59:46.349Z', + }; + const result = determineToAndFrom({ ecsData: ecsDataMock }); + + expect(result.from).toEqual(1584726886349); + expect(result.to).toEqual(1584727186349); + }); + + test('it uses current time timestamp if ecsData.timestamp is not provided', () => { + const { timestamp, ...ecsDataMock } = { + ...mockEcsDataWithSignal, + }; + const result = determineToAndFrom({ ecsData: ecsDataMock }); + + expect(result.from).toEqual(1583085286349); + expect(result.to).toEqual(1583085586349); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx index b23b051e8b2e8..c71ede32d8403 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx @@ -10,7 +10,7 @@ import moment from 'moment'; import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api'; import { SendSignalToTimelineActionProps, UpdateSignalStatusActionProps } from './types'; -import { TimelineNonEcsData, GetOneTimeline, TimelineResult } from '../../../../graphql/types'; +import { TimelineNonEcsData, GetOneTimeline, TimelineResult, Ecs } from '../../../../graphql/types'; import { oneTimelineQuery } from '../../../../containers/timeline/one/index.gql_query'; import { omitTypenameInTimeline, @@ -72,16 +72,7 @@ export const updateSignalStatusAction = async ({ } }; -export const sendSignalToTimelineAction = async ({ - apolloClient, - createTimeline, - ecsData, - updateTimelineIsLoading, -}: SendSignalToTimelineActionProps) => { - let openSignalInBasicTimeline = true; - const timelineId = - ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; - +export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { const ellapsedTimeRule = moment.duration( moment().diff( dateMath.parse(ecsData.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s') @@ -93,6 +84,21 @@ export const sendSignalToTimelineAction = async ({ .valueOf(); const to = moment(ecsData.timestamp ?? new Date()).valueOf(); + return { to, from }; +}; + +export const sendSignalToTimelineAction = async ({ + apolloClient, + createTimeline, + ecsData, + updateTimelineIsLoading, +}: SendSignalToTimelineActionProps) => { + let openSignalInBasicTimeline = true; + const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : ''; + const timelineId = + ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; + const { to, from } = determineToAndFrom({ ecsData }); + if (timelineId !== '' && apolloClient != null) { try { updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); @@ -106,10 +112,10 @@ export const sendSignalToTimelineAction = async ({ id: timelineId, }, }); - const timelineTemplate: TimelineResult = omitTypenameInTimeline( - getOr({}, 'data.getOneTimeline', responseTimeline) - ); - if (!isEmpty(timelineTemplate)) { + const resultingTimeline: TimelineResult = getOr({}, 'data.getOneTimeline', responseTimeline); + + if (!isEmpty(resultingTimeline)) { + const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline); openSignalInBasicTimeline = false; const { timeline } = formatTimelineResultToModel(timelineTemplate, true); const query = replaceTemplateFieldFromQuery( @@ -148,6 +154,7 @@ export const sendSignalToTimelineAction = async ({ show: true, }, to, + ruleNote: noteContent, }); } } catch { @@ -197,6 +204,7 @@ export const sendSignalToTimelineAction = async ({ }, }, to, + ruleNote: noteContent, }); } }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx new file mode 100644 index 0000000000000..6212cad7e1845 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mount, ReactWrapper } from 'enzyme'; +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; + +import { Filter } from '../../../../../../../../../src/plugins/data/common/es_query'; +import { TimelineAction } from '../../../../components/timeline/body/actions'; +import { buildSignalsRuleIdFilter, getSignalsActions } from './default_config'; +import { + CreateTimeline, + SetEventsDeletedProps, + SetEventsLoadingProps, + UpdateTimelineLoading, +} from './types'; +import { mockEcsDataWithSignal } from '../../../../mock/mock_ecs'; +import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; +import * as i18n from './translations'; + +jest.mock('./actions'); + +describe('signals default_config', () => { + describe('buildSignalsRuleIdFilter', () => { + test('given a rule id this will return an array with a single filter', () => { + const filters: Filter[] = buildSignalsRuleIdFilter('rule-id-1'); + const expectedFilter: Filter = { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'signal.rule.id', + params: { + query: 'rule-id-1', + }, + }, + query: { + match_phrase: { + 'signal.rule.id': 'rule-id-1', + }, + }, + }; + expect(filters).toHaveLength(1); + expect(filters[0]).toEqual(expectedFilter); + }); + }); + + describe('getSignalsActions', () => { + let setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; + let setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; + let createTimeline: CreateTimeline; + let updateTimelineIsLoading: UpdateTimelineLoading; + + beforeEach(() => { + setEventsLoading = jest.fn(); + setEventsDeleted = jest.fn(); + createTimeline = jest.fn(); + updateTimelineIsLoading = jest.fn(); + }); + + describe('timeline tooltip', () => { + test('it invokes sendSignalToTimelineAction when button clicked', () => { + const signalsActions = getSignalsActions({ + canUserCRUD: true, + hasIndexWrite: true, + setEventsLoading, + setEventsDeleted, + createTimeline, + status: 'open', + updateTimelineIsLoading, + }); + const timelineAction = signalsActions[0].getAction({ + eventId: 'even-id', + ecsData: mockEcsDataWithSignal, + }); + const wrapper = mount(timelineAction as React.ReactElement); + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(sendSignalToTimelineAction).toHaveBeenCalled(); + }); + }); + + describe('signal open action', () => { + let signalsActions: TimelineAction[]; + let signalOpenAction: JSX.Element; + let wrapper: ReactWrapper; + + beforeEach(() => { + signalsActions = getSignalsActions({ + canUserCRUD: true, + hasIndexWrite: true, + setEventsLoading, + setEventsDeleted, + createTimeline, + status: 'open', + updateTimelineIsLoading, + }); + + signalOpenAction = signalsActions[1].getAction({ + eventId: 'event-id', + ecsData: mockEcsDataWithSignal, + }); + + wrapper = mount(signalOpenAction as React.ReactElement); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + test('it invokes updateSignalStatusAction when button clicked', () => { + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(updateSignalStatusAction).toHaveBeenCalledWith({ + signalIds: ['event-id'], + status: 'open', + setEventsLoading, + setEventsDeleted, + }); + }); + + test('it displays expected text on hover', () => { + const openSignal = wrapper.find(EuiToolTip); + openSignal.simulate('mouseOver'); + const tooltip = wrapper.find('.euiToolTipPopover').text(); + + expect(tooltip).toEqual(i18n.ACTION_OPEN_SIGNAL); + }); + + test('it displays expected icon', () => { + const icon = wrapper.find(EuiButtonIcon).props().iconType; + + expect(icon).toEqual('securitySignalDetected'); + }); + }); + + describe('signal close action', () => { + let signalsActions: TimelineAction[]; + let signalCloseAction: JSX.Element; + let wrapper: ReactWrapper; + + beforeEach(() => { + signalsActions = getSignalsActions({ + canUserCRUD: true, + hasIndexWrite: true, + setEventsLoading, + setEventsDeleted, + createTimeline, + status: 'closed', + updateTimelineIsLoading, + }); + + signalCloseAction = signalsActions[1].getAction({ + eventId: 'event-id', + ecsData: mockEcsDataWithSignal, + }); + + wrapper = mount(signalCloseAction as React.ReactElement); + }); + + afterEach(() => { + wrapper.unmount(); + }); + + test('it invokes updateSignalStatusAction when status button clicked', () => { + wrapper.find(EuiButtonIcon).simulate('click'); + + expect(updateSignalStatusAction).toHaveBeenCalledWith({ + signalIds: ['event-id'], + status: 'closed', + setEventsLoading, + setEventsDeleted, + }); + }); + + test('it displays expected text on hover', () => { + const closeSignal = wrapper.find(EuiToolTip); + closeSignal.simulate('mouseOver'); + const tooltip = wrapper.find('.euiToolTipPopover').text(); + expect(tooltip).toEqual(i18n.ACTION_CLOSE_SIGNAL); + }); + + test('it displays expected icon', () => { + const icon = wrapper.find(EuiButtonIcon).props().iconType; + + expect(icon).toEqual('securitySignalResolved'); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx index 44c48b1879e89..fd3b9a6f68e82 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx @@ -23,7 +23,12 @@ import { timelineDefaults } from '../../../../store/timeline/defaults'; import { FILTER_OPEN } from './signals_filter_group'; import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; import * as i18n from './translations'; -import { CreateTimeline, SetEventsDeletedProps, SetEventsLoadingProps } from './types'; +import { + CreateTimeline, + SetEventsDeletedProps, + SetEventsLoadingProps, + UpdateTimelineLoading, +} from './types'; export const signalsOpenFilters: Filter[] = [ { @@ -198,13 +203,13 @@ export const getSignalsActions = ({ setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; createTimeline: CreateTimeline; status: 'open' | 'closed'; - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void; + updateTimelineIsLoading: UpdateTimelineLoading; }): TimelineAction[] => [ { getAction: ({ ecsData }: TimelineActionProps): JSX.Element => ( { let localValueToChange = valueToChange; - if (keuryNode.function === 'is' && templateFields.includes(keuryNode.arguments[0].value)) { + if (kueryNode.function === 'is' && templateFields.includes(kueryNode.arguments[0].value)) { localValueToChange = [ ...localValueToChange, { - field: keuryNode.arguments[0].value, - valueToChange: keuryNode.arguments[1].value, + field: kueryNode.arguments[0].value, + valueToChange: kueryNode.arguments[1].value, }, ]; } - return keuryNode.arguments.reduce( + return kueryNode.arguments.reduce( (addValueToChange: FindValueToChangeInQuery[], ast: KueryNode) => { if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { return [ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx index afd325f539966..6cdb2f326901e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/index.tsx @@ -114,7 +114,7 @@ const SignalsTableComponent: React.FC = ({ // Callback for creating a new timeline -- utilized by row/batch actions const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline }: CreateTimelineProps) => { + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); updateTimeline({ duplicate: true, @@ -126,6 +126,7 @@ const SignalsTableComponent: React.FC = ({ show: true, }, to: toTimeline, + ruleNote, })(); }, [updateTimeline, updateTimelineIsLoading] diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts index c2807db179780..f68dcd932bc32 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts @@ -95,9 +95,9 @@ export const ACTION_CLOSE_SIGNAL = i18n.translate( } ); -export const ACTION_VIEW_IN_TIMELINE = i18n.translate( - 'xpack.siem.detectionEngine.signals.actions.viewInTimelineTitle', +export const ACTION_INVESTIGATE_IN_TIMELINE = i18n.translate( + 'xpack.siem.detectionEngine.signals.actions.investigateInTimelineTitle', { - defaultMessage: 'View in timeline', + defaultMessage: 'Investigate in timeline', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts index b3e7ed75cfb99..909b217646746 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/types.ts @@ -45,13 +45,16 @@ export interface SendSignalToTimelineActionProps { apolloClient?: ApolloClient<{}>; createTimeline: CreateTimeline; ecsData: Ecs; - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => void; + updateTimelineIsLoading: UpdateTimelineLoading; } +export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void; + export interface CreateTimelineProps { from: number; timeline: TimelineModel; to: number; + ruleNote?: string; } export type CreateTimeline = ({ from, timeline, to }: CreateTimelineProps) => void; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap index 9a534297e5e29..31abea53462fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap @@ -145,7 +145,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against # this is some markdown documentation , - "title": "Investigation notes", + "title": "Investigation guide", }, ] } @@ -287,7 +287,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against # this is some markdown documentation , - "title": "Investigation notes", + "title": "Investigation guide", }, ] } @@ -430,7 +430,7 @@ exports[`description_step StepRuleDescriptionComponent renders correctly against # this is some markdown documentation , - "title": "Investigation notes", + "title": "Investigation guide", }, ] } 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.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx index a01aec0ccf2cf..8e8927cb7bbd1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx @@ -461,12 +461,12 @@ describe('description_step', () => { test('returns default "note" description', () => { const result: ListItems[] = getDescriptionItem( 'note', - 'Investigation notes', + 'Investigation guide', mockAboutStep, mockFilterManager ); - expect(result[0].title).toEqual('Investigation notes'); + expect(result[0].title).toEqual('Investigation guide'); expect(React.isValidElement(result[0].description)).toBeTruthy(); }); }); 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_about_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx index 8cb38b9dc7393..7c088c068c9b2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx @@ -178,12 +178,12 @@ export const schema: FormSchema = { }, note: { type: FIELD_TYPES.TEXTAREA, - label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.noteLabel', { - defaultMessage: 'Investigation notes', + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideLabel', { + defaultMessage: 'Investigation guide', }), - helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.noteHelpText', { + helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideHelpText', { defaultMessage: - 'Provide helpful information for analysts that are performing a signal investigation. These notes will appear on both the rule details page and in timelines created from signals generated by this rule.', + 'Provide helpful information for analysts that are performing a signal investigation. This guide will appear on both the rule details page and in timelines created from signals generated by this rule.', }), labelAppend: OptionalFieldLabel, }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts index dfa60268e903a..0b1e712c663f3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts @@ -72,6 +72,6 @@ export const URL_FORMAT_INVALID = i18n.translate( export const ADD_RULE_NOTE_HELP_TEXT = i18n.translate( 'xpack.siem.detectionEngine.createRule.stepAboutrule.noteHelpText', { - defaultMessage: 'Add rule investigation notes...', + defaultMessage: 'Add rule investigation guide...', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx index bbd037af10c3f..76a3c590a62a6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx @@ -136,7 +136,7 @@ describe('StepAboutRuleToggleDetails', () => { expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy(); wrapper - .find('input[title="Investigation notes"]') + .find('input[title="Investigation guide"]') .at(0) .simulate('change', { target: { value: 'notes' } }); @@ -159,7 +159,7 @@ describe('StepAboutRuleToggleDetails', () => { ); wrapper - .find('input[title="Investigation notes"]') + .find('input[title="Investigation guide"]') .at(0) .simulate('change', { target: { value: 'notes' } }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts index fa725366210de..79c5eb12d4663 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts @@ -20,8 +20,8 @@ export const ABOUT_TEXT = i18n.translate( ); export const ABOUT_PANEL_NOTES_TAB = i18n.translate( - 'xpack.siem.detectionEngine.details.stepAboutRule.investigationNotesLabel', + 'xpack.siem.detectionEngine.details.stepAboutRule.investigationGuideLabel', { - defaultMessage: 'Investigation notes', + defaultMessage: 'Investigation guide', } ); 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/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index 7abe5a576c0e5..1bc5d85258ffd 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 @@ -12,8 +12,10 @@ import { NOTIFICATION_THROTTLE_RULE, NOTIFICATION_THROTTLE_NO_ACTIONS, } from '../../../../../common/constants'; -import { NewRule, RuleType } from '../../../../containers/detection_engine/rules'; 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 +27,6 @@ import { AboutStepRuleJson, ActionsStepRuleJson, } from '../types'; -import { isMlRule } from '../helpers'; export const getTimeTypeValue = (time: string): { unit: string; value: number } => { const timeObj = { 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..710dd2cabeb65 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, @@ -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/graphql/ecs/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts index f897236b3470e..9bf55cfe1ed2a 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/ecs/schema.gql.ts @@ -410,6 +410,7 @@ export const ecsSchema = gql` created_by: ToStringArray updated_by: ToStringArray version: ToStringArray + note: ToStringArray } type SignalField { diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts index e2b365f8bfa5b..d272b7ff59b79 100644 --- a/x-pack/legacy/plugins/siem/server/graphql/types.ts +++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts @@ -1014,6 +1014,8 @@ export interface RuleField { updated_by?: Maybe; version?: Maybe; + + note?: Maybe; } export interface SuricataEcsFields { @@ -4822,6 +4824,8 @@ export namespace RuleFieldResolvers { updated_by?: UpdatedByResolver, TypeParent, TContext>; version?: VersionResolver, TypeParent, TContext>; + + note?: NoteResolver, TypeParent, TContext>; } export type IdResolver< @@ -4974,6 +4978,11 @@ export namespace RuleFieldResolvers { Parent = RuleField, TContext = SiemContext > = Resolver; + export type NoteResolver< + R = Maybe, + Parent = RuleField, + TContext = SiemContext + > = Resolver; } export namespace SuricataEcsFieldsResolvers { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index 36764439462c3..3195483013c19 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -30,9 +30,13 @@ export const createIndexRoute = (router: IRouter) => { 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..daeb11e88508b 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 @@ -37,15 +37,12 @@ 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 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_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 4da879d12f809..a77911bbb35e8 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 @@ -60,6 +60,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); 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..f68f204c12730 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 @@ -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 }); } 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..33ffc245e7668 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 @@ -35,11 +35,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) { 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..a4e659da76bb2 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 @@ -34,12 +34,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) { 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..77351d2e0751b 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 @@ -32,10 +32,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) { 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..d9fc89740c9ef 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, 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..b19039321a6d8 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 @@ -37,11 +37,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) { 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..fab53079361ad 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 @@ -74,12 +74,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) { 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..bc52445feee76 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 @@ -32,10 +32,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 { 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..789f7d1ca0744 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 @@ -37,15 +37,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 }); } 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..454fe1f0706cb 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 @@ -67,6 +67,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..5856575eb9799 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 @@ -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 }); } 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..a0458dc3a133d 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, @@ -300,5 +295,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/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/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index ada11174c5340..d8dacc7c64397 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: { 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/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/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index ab9def14bef65..de4ec68e8fc8a 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,14 @@ 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 { 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 +24,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 +67,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 +88,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 +120,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 +157,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 +180,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 +205,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,10 +240,10 @@ export const signalRulesAlertType = ({ tags, throttle, }); - creationSucceeded.searchAfterTimes.push(makeFloatString(end - start)); + result.searchAfterTimes.push(makeFloatString(end - start)); } - if (creationSucceeded.success) { + if (result.success) { if (meta?.throttle === NOTIFICATION_THROTTLE_RULE && actions.length) { const notificationRuleParams = { ...ruleParams, @@ -242,9 +260,7 @@ export const signalRulesAlertType = ({ 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 +273,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/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..08b3f864314f9 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[]; diff --git a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts b/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts index eb483de000915..f2662c79d3393 100644 --- a/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts +++ b/x-pack/legacy/plugins/siem/server/lib/ecs_fields/index.ts @@ -316,6 +316,7 @@ export const signalFieldsMap: Readonly> = { 'signal.rule.created_by': 'signal.rule.created_by', 'signal.rule.updated_by': 'signal.rule.updated_by', 'signal.rule.version': 'signal.rule.version', + 'signal.rule.note': 'signal.rule.note', }; export const ruleFieldsMap: Readonly> = { 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..dcaf805c54f6e 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'; @@ -48,6 +49,7 @@ export interface SetupPlugins { licensing: LicensingPluginSetup; security?: SecuritySetup; spaces?: SpacesSetup; + ml?: MlSetup; } export interface StartPlugins { @@ -164,6 +166,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/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/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/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; diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index a88b1d9049c76..528b9a69327fa 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -49,6 +49,8 @@ export interface LogEntriesAroundParams { export const LOG_ENTRIES_PAGE_SIZE = 200; +const FIELDS_FROM_CONTEXT = ['log.file.path', 'host.name', 'container.id'] as const; + export class InfraLogEntriesDomain { constructor( private readonly adapter: LogEntriesAdapter, @@ -154,6 +156,14 @@ export class InfraLogEntriesDomain { } } ), + context: FIELDS_FROM_CONTEXT.reduce((ctx, field) => { + // Users might have different types here in their mappings. + const value = doc.fields[field]; + if (typeof value === 'string') { + ctx[field] = value; + } + return ctx; + }, {}), }; }); @@ -329,7 +339,9 @@ const getRequiredFields = ( ); const fieldsFromFormattingRules = messageFormattingRules.requiredFields; - return Array.from(new Set([...fieldsFromCustomColumns, ...fieldsFromFormattingRules])); + return Array.from( + new Set([...fieldsFromCustomColumns, ...fieldsFromFormattingRules, ...FIELDS_FROM_CONTEXT]) + ); }; const createHighlightQueryDsl = (phrase: string, fields: string[]) => ({ 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..727d26b5868de 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -76,7 +76,8 @@ export const getFileHandler: RequestHandler { try { const { pkgkey, filePath } = request.params; - const registryResponse = await getFile(`/package/${pkgkey}/${filePath}`); + const [pkgName, pkgVersion] = pkgkey.split('-'); + 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/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 95a8dfbb308f8..9791cd9210fe2 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 @@ -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/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/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/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 a84400f236134..0454d40e78923 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -586,6 +586,7 @@ class JobService { const data = { index: job.datafeed_config.indices, body, + ...(job.datafeed_config.indices_options || {}), }; ml.esSearch(data) 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/snapshot_restore/public/application/constants/index.ts b/x-pack/plugins/snapshot_restore/public/application/constants/index.ts index 9c8fb3d288d24..ea6f5c80b9343 100644 --- a/x-pack/plugins/snapshot_restore/public/application/constants/index.ts +++ b/x-pack/plugins/snapshot_restore/public/application/constants/index.ts @@ -14,9 +14,9 @@ export type Section = 'repositories' | 'snapshots' | 'restore_status' | 'policie export const MINIMUM_TIMEOUT_MS = 300; export enum REPOSITORY_DOC_PATHS { - default = 'modules-snapshots.html', - fs = 'modules-snapshots.html#_shared_file_system_repository', - url = 'modules-snapshots.html#_read_only_url_repository', + default = 'snapshot-restore.html', + fs = 'snapshots-register-repository.html#snapshots-filesystem-repository', + url = 'snapshots-register-repository.html#snapshots-read-only-repository', source = 'snapshots-register-repository.html#snapshots-source-only-repository', s3 = 'repository-s3.html', hdfs = 'repository-hdfs.html', diff --git a/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts b/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts index 5e59685d6be47..daeb14c39f68b 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/documentation/documentation_links.ts @@ -46,15 +46,15 @@ class DocumentationLinksService { } public getSnapshotDocUrl() { - return `${this.esDocBasePath}modules-snapshots.html#snapshots-take-snapshot`; + return `${this.esDocBasePath}snapshots-take-snapshot.html`; } public getRestoreDocUrl() { - return `${this.esDocBasePath}modules-snapshots.html#restore-snapshot`; + return `${this.esDocBasePath}snapshots-restore-snapshot.html`; } public getRestoreIndexSettingsUrl() { - return `${this.esDocBasePath}modules-snapshots.html#_changing_index_settings_during_restore`; + return `${this.esDocBasePath}snapshots-restore-snapshot.html#_changing_index_settings_during_restore`; } public getIndexSettingsUrl() { 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/infra/log_entries.ts b/x-pack/test/api_integration/apis/infra/log_entries.ts index 4f447d518a751..3c12f5e4dc789 100644 --- a/x-pack/test/api_integration/apis/infra/log_entries.ts +++ b/x-pack/test/api_integration/apis/infra/log_entries.ts @@ -126,6 +126,32 @@ export default function({ getService }: FtrProviderContext) { expect(messageColumn.message.length).to.be.greaterThan(0); }); + it('Returns the context fields', async () => { + const { body } = await supertest + .post(LOG_ENTRIES_PATH) + .set(COMMON_HEADERS) + .send( + logEntriesRequestRT.encode({ + sourceId: 'default', + startTimestamp: EARLIEST_KEY_WITH_DATA.time, + endTimestamp: LATEST_KEY_WITH_DATA.time, + center: KEY_WITHIN_DATA_RANGE, + }) + ) + .expect(200); + + const logEntriesResponse = pipe( + logEntriesResponseRT.decode(body), + fold(throwErrors(createPlainError), identity) + ); + + const entries = logEntriesResponse.data.entries; + const entry = entries[0]; + + expect(entry.context).to.have.property('host.name'); + expect(entry.context['host.name']).to.be('demo-stack-nginx-01'); + }); + it('Paginates correctly with `after`', async () => { const { body: firstPageBody } = await supertest .post(LOG_ENTRIES_PATH) diff --git a/x-pack/test/encrypted_saved_objects_api_integration/config.ts b/x-pack/test/encrypted_saved_objects_api_integration/config.ts new file mode 100644 index 0000000000000..c1be2e98b3b99 --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/config.ts @@ -0,0 +1,30 @@ +/* + * 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 { resolve } from 'path'; +import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; +import { services } from './services'; + +export default async function({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js')); + + return { + testFiles: [require.resolve('./tests')], + servers: xPackAPITestsConfig.get('servers'), + services, + junit: { + reportName: 'X-Pack Encrypted Saved Objects API Integration Tests', + }, + esTestCluster: xPackAPITestsConfig.get('esTestCluster'), + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + serverArgs: [ + ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), + `--plugin-path=${resolve(__dirname, './fixtures/api_consumer_plugin')}`, + ], + }, + }; +} diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/kibana.json b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/kibana.json new file mode 100644 index 0000000000000..92449d0136ce5 --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "eso", + "version": "8.0.0", + "kibanaVersion": "kibana", + "requiredPlugins": ["encryptedSavedObjects", "spaces"], + "server": true, + "ui": false +} diff --git a/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts new file mode 100644 index 0000000000000..170b7e0c6d09d --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/fixtures/api_consumer_plugin/server/index.ts @@ -0,0 +1,78 @@ +/* + * 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 { CoreSetup, PluginInitializer } from '../../../../../../src/core/server'; +import { deepFreeze } from '../../../../../../src/core/utils'; +import { + EncryptedSavedObjectsPluginSetup, + EncryptedSavedObjectsPluginStart, +} from '../../../../../plugins/encrypted_saved_objects/server'; +import { SpacesPluginSetup } from '../../../../../plugins/spaces/server'; + +const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; + +interface PluginsSetup { + encryptedSavedObjects: EncryptedSavedObjectsPluginSetup; + spaces: SpacesPluginSetup; +} + +interface PluginsStart { + encryptedSavedObjects: EncryptedSavedObjectsPluginStart; + spaces: never; +} + +export const plugin: PluginInitializer = () => ({ + 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/test/encrypted_saved_objects_api_integration/services.ts b/x-pack/test/encrypted_saved_objects_api_integration/services.ts new file mode 100644 index 0000000000000..b7398349cce5d --- /dev/null +++ b/x-pack/test/encrypted_saved_objects_api_integration/services.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { 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..c67f472e8fb78 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' }, }, @@ -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' }, }, @@ -54,7 +54,7 @@ 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'); @@ -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' }, }, @@ -88,7 +88,7 @@ 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'); @@ -105,7 +105,7 @@ 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'); @@ -121,7 +121,7 @@ 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'); @@ -135,7 +135,7 @@ 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'); 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/login_selector_api_integration/config.ts b/x-pack/test/login_selector_api_integration/config.ts index 6ca9d19b74c17..d8e42b4583bed 100644 --- a/x-pack/test/login_selector_api_integration/config.ts +++ b/x-pack/test/login_selector_api_integration/config.ts @@ -130,11 +130,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { saml2: { order: 5, realm: 'saml2', maxRedirectURLSize: '100b' }, }, })}`, - '--server.xsrf.whitelist', - JSON.stringify([ - '/api/oidc_provider/token_endpoint', - '/api/oidc_provider/userinfo_endpoint', - ]), ], }, }; diff --git a/x-pack/test/oidc_api_integration/config.ts b/x-pack/test/oidc_api_integration/config.ts index 557dea4d51b0e..9ef00320f0e60 100644 --- a/x-pack/test/oidc_api_integration/config.ts +++ b/x-pack/test/oidc_api_integration/config.ts @@ -51,12 +51,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { `--plugin-path=${plugin}`, '--xpack.security.authc.providers=["oidc"]', '--xpack.security.authc.oidc.realm="oidc1"', - '--server.xsrf.whitelist', - JSON.stringify([ - '/api/security/oidc/initiate_login', - '/api/oidc_provider/token_endpoint', - '/api/oidc_provider/userinfo_endpoint', - ]), ], }, }; diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/init_routes.js b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/init_routes.js deleted file mode 100644 index 3023479f7be9d..0000000000000 --- a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/init_routes.js +++ /dev/null @@ -1,104 +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 Joi from 'joi'; -import { createTokens } from '../oidc_tools'; - -export function initRoutes(server) { - let nonce = ''; - - server.route({ - path: '/api/oidc_provider/setup', - method: 'POST', - config: { - auth: false, - validate: { - payload: Joi.object({ - nonce: Joi.string().required(), - }), - }, - }, - handler: request => { - 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 b5e72e07f1efe..b3c2aa94d07d1 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,10 +5317,15 @@ 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.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" - integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + version "5.7.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" + integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== acorn@^3.0.4, acorn@^3.1.0: version "3.3.0" @@ -5327,17 +5337,12 @@ acorn@^4.0.4, acorn@~4.0.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c= -acorn@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" - integrity sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA== - -acorn@^6.2.1: - version "6.3.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" - integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== +acorn@^6.0.1, acorn@^6.2.1: + version "6.4.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==