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/visualize/legacy_imports.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy_imports.ts index f6557ed4af155..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,7 +24,6 @@ * directly where they are needed. */ -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/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/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/index.ts b/x-pack/legacy/plugins/apm/index.ts index d1f7ce325d23e..d2383acd45eba 100644 --- a/x-pack/legacy/plugins/apm/index.ts +++ b/x-pack/legacy/plugins/apm/index.ts @@ -105,10 +105,17 @@ export const apm: LegacyPluginInitializer = kibana => { privileges: { all: { app: ['apm', 'kibana'], - api: ['apm', 'apm_write', 'actions-read', 'alerting-read'], + api: [ + 'apm', + 'apm_write', + 'actions-read', + 'actions-all', + 'alerting-read', + 'alerting-all' + ], catalogue: ['apm'], savedObject: { - all: ['action', 'action_task_params'], + all: ['alert', 'action', 'action_task_params'], read: [] }, ui: [ @@ -124,13 +131,27 @@ export const apm: LegacyPluginInitializer = kibana => { }, read: { app: ['apm', 'kibana'], - api: ['apm', 'actions-read', 'alerting-read'], + api: [ + 'apm', + 'actions-read', + 'actions-all', + 'alerting-read', + 'alerting-all' + ], catalogue: ['apm'], savedObject: { - all: ['action', 'action_task_params'], + all: ['alert', 'action', 'action_task_params'], read: [] }, - ui: ['show', 'alerting:show', 'actions:show'] + ui: [ + 'show', + 'alerting:show', + 'actions:show', + 'alerting:save', + 'actions:save', + 'alerting:delete', + 'actions:delete' + ] } } }); diff --git a/x-pack/legacy/plugins/apm/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/maps/public/actions/map_actions.d.ts b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts index c1f5c31eb4210..b4a8ff90c3512 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.d.ts @@ -5,10 +5,14 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { Filter, Query } from 'src/plugins/data/public'; +import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { AnyAction } from 'redux'; import { LAYER_TYPE } from '../../common/constants'; import { DataMeta, MapFilters } from '../../common/descriptor_types'; +import { + MapCenterAndZoom, + MapRefreshConfig, +} from '../../../../../plugins/maps/common/descriptor_types'; export type SyncContext = { startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; @@ -27,31 +31,20 @@ export function updateSourceProp( newLayerType?: LAYER_TYPE ): void; -export interface MapCenter { - lat: number; - lon: number; - zoom: number; -} - -export function setGotoWithCenter(config: MapCenter): AnyAction; +export function setGotoWithCenter(config: MapCenterAndZoom): AnyAction; export function replaceLayerList(layerList: unknown[]): AnyAction; -export interface QueryGroup { +export type QueryGroup = { filters: Filter[]; query?: Query; - timeFilters: unknown; - refresh: unknown; -} + timeFilters?: TimeRange; + refresh?: boolean; +}; export function setQuery(query: QueryGroup): AnyAction; -export interface RefreshConfig { - isPaused: boolean; - interval: number; -} - -export function setRefreshConfig(config: RefreshConfig): AnyAction; +export function setRefreshConfig(config: MapRefreshConfig): AnyAction; export function disableScrollZoom(): AnyAction; diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 519ba0b1e3d96..bc97643689e12 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -310,9 +310,15 @@ app.controller( const layerListConfigOnly = copyPersistentState(layerList); const savedLayerList = savedMap.getLayerList(); - const oldConfig = savedLayerList ? savedLayerList : initialLayerListConfig; - return !_.isEqual(layerListConfigOnly, oldConfig); + return !savedLayerList + ? !_.isEqual(layerListConfigOnly, initialLayerListConfig) + : // savedMap stores layerList as a JSON string using JSON.stringify. + // JSON.stringify removes undefined properties from objects. + // savedMap.getLayerList converts the JSON string back into Javascript array of objects. + // Need to perform the same process for layerListConfigOnly to compare apples to apples + // and avoid undefined properties in layerListConfigOnly triggering unsaved changes. + !_.isEqual(JSON.parse(JSON.stringify(layerListConfigOnly)), savedLayerList); } function isOnMapNow() { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js index e51e59ec41e18..04de5f71f5bfc 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js @@ -7,7 +7,7 @@ import { connect } from 'react-redux'; import { LayerControl } from './view'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui.js'; +import { FLYOUT_STATE } from '../../../../../../../plugins/maps/public/reducers/ui'; import { updateFlyout, setIsLayerTOCOpen } from '../../../actions/ui_actions'; import { setSelectedLayer } from '../../../actions/map_actions'; import { diff --git a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js index ececc5a90ab89..588445d0b4992 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/index.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import { connect } from 'react-redux'; import { TOCEntry } from './view'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { FLYOUT_STATE } from '../../../../../../../../../plugins/maps/public/reducers/ui.js'; +import { FLYOUT_STATE } from '../../../../../../../../../plugins/maps/public/reducers/ui'; import { updateFlyout, hideTOCDetails, showTOCDetails } from '../../../../../actions/ui_actions'; import { getIsReadOnly, getOpenTOCDetails } from '../../../../../selectors/ui_selectors'; import { diff --git a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx index 69f55815d16a0..3c9069c7a836f 100644 --- a/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/legacy/plugins/maps/public/embeddable/map_embeddable.tsx @@ -45,8 +45,8 @@ import { hideLayerControl, hideViewControl, setHiddenLayers, - MapCenter, } from '../actions/map_actions'; +import { MapCenterAndZoom } from '../../../../../plugins/maps/common/descriptor_types'; import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; import { @@ -71,7 +71,6 @@ export interface MapEmbeddableInput extends EmbeddableInput { timeRange?: TimeRange; filters: Filter[]; query?: Query; - refresh?: unknown; refreshConfig: RefreshInterval; isLayerTOCOpen: boolean; openTOCDetails?: string[]; @@ -80,7 +79,7 @@ export interface MapEmbeddableInput extends EmbeddableInput { hideToolbarOverlay?: boolean; hideLayerControl?: boolean; hideViewControl?: boolean; - mapCenter?: MapCenter; + mapCenter?: MapCenterAndZoom; hiddenLayers?: string[]; hideFilterActions?: boolean; } @@ -153,7 +152,12 @@ export class MapEmbeddable extends Embeddable) { + }: { + query?: Query; + timeRange?: TimeRange; + filters: Filter[]; + refresh?: boolean; + }) { this._prevTimeRange = timeRange; this._prevQuery = query; this._prevFilters = filters; diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js index a1c15e27c9eb3..5e8f720fcc5e3 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js @@ -28,12 +28,20 @@ export function DynamicColorForm({ }; if (type === COLOR_MAP_TYPE.ORDINAL) { newColorOptions.useCustomColorRamp = useCustomColorMap; - newColorOptions.customColorRamp = customColorMap; - newColorOptions.color = color; + if (customColorMap) { + newColorOptions.customColorRamp = customColorMap; + } + if (color) { + newColorOptions.color = color; + } } else { newColorOptions.useCustomColorPalette = useCustomColorMap; - newColorOptions.customColorPalette = customColorMap; - newColorOptions.colorCategory = color; + if (customColorMap) { + newColorOptions.customColorPalette = customColorMap; + } + if (color) { + newColorOptions.colorCategory = color; + } } onDynamicStyleChange(styleProperty.getStyleName(), newColorOptions); diff --git a/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.ts b/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.ts index c77af11d0ae24..46e27bbd770a1 100644 --- a/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.ts +++ b/x-pack/legacy/plugins/maps/public/layers/tooltips/tooltip_property.ts @@ -6,6 +6,7 @@ import _ from 'lodash'; import { PhraseFilter } from '../../../../../../../src/plugins/data/public'; +import { TooltipFeature } from '../../../../../../plugins/maps/common/descriptor_types'; export interface ITooltipProperty { getPropertyKey(): string; @@ -16,11 +17,6 @@ export interface ITooltipProperty { getESFilters(): Promise; } -export interface MapFeature { - id: number; - layerId: string; -} - export interface LoadFeatureProps { layerId: string; featureId: number; @@ -34,7 +30,7 @@ export interface FeatureGeometry { export interface RenderTooltipContentParams { addFilters(filter: object): void; closeTooltip(): void; - features: MapFeature[]; + features: TooltipFeature[]; isLocked: boolean; getLayerName(layerId: string): Promise; loadFeatureProperties({ layerId, featureId }: LoadFeatureProps): Promise; diff --git a/x-pack/legacy/plugins/maps/public/layers/util/is_refresh_only_query.ts b/x-pack/legacy/plugins/maps/public/layers/util/is_refresh_only_query.ts index a56da4b23aa1e..3599f18671ced 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/is_refresh_only_query.ts +++ b/x-pack/legacy/plugins/maps/public/layers/util/is_refresh_only_query.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Query } from '../../../common/descriptor_types'; +import { MapQuery } from '../../../common/descriptor_types'; // Refresh only query is query where timestamps are different but query is the same. // Triggered by clicking "Refresh" button in QueryBar export function isRefreshOnlyQuery( - prevQuery: Query | undefined, - newQuery: Query | undefined + prevQuery: MapQuery | undefined, + newQuery: MapQuery | undefined ): boolean { if (!prevQuery || !newQuery) { return false; diff --git a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts index 237a04027e21b..8c99e0adcc14f 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/legacy/plugins/maps/public/selectors/map_selectors.d.ts @@ -5,12 +5,14 @@ */ import { AnyAction } from 'redux'; -import { MapCenter } from '../actions/map_actions'; +import { MapCenter } from '../../common/descriptor_types'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MapStoreState } from '../../../../../plugins/maps/public/reducers/store'; -export function getHiddenLayerIds(state: unknown): string[]; +export function getHiddenLayerIds(state: MapStoreState): string[]; -export function getMapZoom(state: unknown): number; +export function getMapZoom(state: MapStoreState): number; -export function getMapCenter(state: unknown): MapCenter; +export function getMapCenter(state: MapStoreState): MapCenter; -export function getQueryableUniqueIndexPatternIds(state: unknown): string[]; +export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[]; diff --git a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js deleted file mode 100644 index 912ee08396212..0000000000000 --- a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const getFlyoutDisplay = ({ ui }) => ui.flyoutDisplay; -export const getIsSetViewOpen = ({ ui }) => ui.isSetViewOpen; -export const getIsLayerTOCOpen = ({ ui }) => ui.isLayerTOCOpen; -export const getOpenTOCDetails = ({ ui }) => ui.openTOCDetails; -export const getIsFullScreen = ({ ui }) => ui.isFullScreen; -export const getIsReadOnly = ({ ui }) => ui.isReadOnly; -export const getIndexingStage = ({ ui }) => ui.importIndexingStage; diff --git a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts new file mode 100644 index 0000000000000..fdf2a8ea0e4f3 --- /dev/null +++ b/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { MapStoreState } from '../../../../../plugins/maps/public/reducers/store'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FLYOUT_STATE, INDEXING_STAGE } from '../../../../../plugins/maps/public/reducers/ui'; + +export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; +export const getIsSetViewOpen = ({ ui }: MapStoreState): boolean => ui.isSetViewOpen; +export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; +export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; +export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; +export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; +export const getIndexingStage = ({ ui }: MapStoreState): INDEXING_STAGE | null => + ui.importIndexingStage; diff --git a/x-pack/legacy/plugins/siem/public/components/ml/helpers/index.test.ts b/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.test.ts similarity index 96% rename from x-pack/legacy/plugins/siem/public/components/ml/helpers/index.test.ts rename to x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.test.ts index 693f0bd0dd0fd..ba93b2e4b8a0d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/helpers/index.test.ts +++ b/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isJobStarted, isJobLoading, isJobFailed } from './'; +import { isJobStarted, isJobLoading, isJobFailed } from './ml_helpers'; describe('isJobStarted', () => { test('returns false if only jobState is enabled', () => { diff --git a/x-pack/legacy/plugins/siem/public/components/ml/helpers/index.ts b/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.ts similarity index 89% rename from x-pack/legacy/plugins/siem/public/components/ml/helpers/index.ts rename to x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.ts index c06596b49317d..e4158d08d448d 100644 --- a/x-pack/legacy/plugins/siem/public/components/ml/helpers/index.ts +++ b/x-pack/legacy/plugins/siem/common/detection_engine/ml_helpers.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RuleType } from './types'; + // Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js const enabledStates = ['started', 'opened']; const loadingStates = ['starting', 'stopping', 'opening', 'closing']; @@ -20,3 +22,5 @@ export const isJobLoading = (jobState: string, datafeedState: string): boolean = export const isJobFailed = (jobState: string, datafeedState: string): boolean => { return failureStates.includes(jobState) || failureStates.includes(datafeedState); }; + +export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; diff --git a/x-pack/legacy/plugins/siem/common/detection_engine/types.ts b/x-pack/legacy/plugins/siem/common/detection_engine/types.ts index 0de370b11cdaf..39012d0b4b683 100644 --- a/x-pack/legacy/plugins/siem/common/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/common/detection_engine/types.ts @@ -3,9 +3,17 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import * as t from 'io-ts'; import { AlertAction } from '../../../../../plugins/alerting/common'; export type RuleAlertAction = Omit & { action_type_id: string; }; + +export const RuleTypeSchema = t.keyof({ + query: null, + saved_query: null, + machine_learning: null, +}); +export type RuleType = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/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/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap index 24b1756aade2e..c8d4b6ec3b4c8 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap +++ b/x-pack/legacy/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap @@ -19,6 +19,7 @@ exports[`EditableTitle it renders 1`] = ` aria-label="You can edit Test title by clicking" data-test-subj="editable-title-edit-icon" iconType="pencil" + isDisabled={false} onClick={[Function]} /> diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx index 29cc1579f9bcc..165be00384779 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/editable_title.tsx @@ -34,12 +34,18 @@ const MySpinner = styled(EuiLoadingSpinner)` `; interface Props { + disabled?: boolean; isLoading: boolean; title: string | React.ReactNode; onSubmit: (title: string) => void; } -const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) => { +const EditableTitleComponent: React.FC = ({ + disabled = false, + onSubmit, + isLoading, + title, +}) => { const [editMode, setEditMode] = useState(false); const [changedTitle, onTitleChange] = useState(typeof title === 'string' ? title : ''); @@ -104,6 +110,7 @@ const EditableTitleComponent: React.FC = ({ onSubmit, isLoading, title }) {isLoading && } {!isLoading && ( { + 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/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx index 0cfb07cccfd6c..e4d828b68f3dc 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx @@ -17,7 +17,7 @@ export const useInsertTimeline = (form: FormHook, fieldNa }); const handleOnTimelineChange = useCallback( (title: string, id: string | null) => { - const builtLink = `${basePath}/app/siem#/timelines?timeline=(id:${id},isOpen:!t)`; + const builtLink = `${basePath}/app/siem#/timelines?timeline=(id:'${id}',isOpen:!t)`; const currentValue = form.getFormData()[fieldName]; const newValue: string = [ currentValue.slice(0, cursorPosition.start), diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts index de3e3c8e792fe..101837168350f 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/timeline/translations.ts @@ -25,5 +25,5 @@ export const SEARCH_BOX_TIMELINE_PLACEHOLDER = i18n.translate( ); export const INSERT_TIMELINE = i18n.translate('xpack.siem.insert.timeline.insertTimelineButton', { - defaultMessage: 'Insert Timeline…', + defaultMessage: 'Insert timeline link', }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 7d5ae53b78ff8..bd243d0ba5f64 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -119,7 +119,7 @@ export const getCases = async ({ signal, }: FetchCasesProps): Promise => { const query = { - reporters: filterOptions.reporters.map(r => r.username), + reporters: filterOptions.reporters.map(r => r.username ?? '').filter(r => r !== ''), tags: filterOptions.tags, ...(filterOptions.status !== '' ? { status: filterOptions.status } : {}), ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts new file mode 100644 index 0000000000000..dbd618f40155d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/translations.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../translations'; + +export const SUCCESS_CONFIGURE = i18n.translate('xpack.siem.case.configure.successSaveToast', { + defaultMessage: 'Saved external connection settings', +}); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx index b25667f070fdf..6524c40a8e6e4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/configure/use_configure.tsx @@ -7,8 +7,8 @@ import { useState, useEffect, useCallback } from 'react'; import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; -import { useStateToaster, errorToToaster } from '../../../components/toasters'; -import * as i18n from '../translations'; +import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters'; +import * as i18n from './translations'; import { ClosureType } from './types'; import { CurrentConfiguration } from '../../../pages/case/components/configure_cases/reducer'; @@ -124,6 +124,8 @@ export const useCaseConfigure = ({ closureType: res.closureType, }); } + + displaySuccessToast(i18n.SUCCESS_CONFIGURE, dispatchToaster); } } catch (error) { if (!didCancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts index 601db373f041e..a453be32480e2 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/translations.ts @@ -10,6 +10,46 @@ export const ERROR_TITLE = i18n.translate('xpack.siem.containers.case.errorTitle defaultMessage: 'Error fetching data', }); +export const ERROR_DELETING = i18n.translate('xpack.siem.containers.case.errorDeletingTitle', { + defaultMessage: 'Error deleting data', +}); + +export const UPDATED_CASE = (caseTitle: string) => + i18n.translate('xpack.siem.containers.case.updatedCase', { + values: { caseTitle }, + defaultMessage: 'Updated "{caseTitle}"', + }); + +export const DELETED_CASES = (totalCases: number, caseTitle?: string) => + i18n.translate('xpack.siem.containers.case.deletedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Deleted {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const CLOSED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.siem.containers.case.closedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Closed {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + +export const REOPENED_CASES = ({ + totalCases, + caseTitle, +}: { + totalCases: number; + caseTitle?: string; +}) => + i18n.translate('xpack.siem.containers.case.reopenedCases', { + values: { caseTitle, totalCases }, + defaultMessage: 'Reopened {totalCases, plural, =1 {"{caseTitle}"} other {{totalCases} cases}}', + }); + export const TAG_FETCH_FAILURE = i18n.translate( 'xpack.siem.containers.case.tagFetchFailDescription', { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index bb215d6ac271c..d2a58e9eeeff4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -90,7 +90,7 @@ export enum SortFieldCase { export interface ElasticUser { readonly email?: string | null; readonly fullName?: string | null; - readonly username: string; + readonly username?: string | null; } export interface FetchCasesProps extends ApiProps { @@ -114,3 +114,8 @@ export interface ActionLicense { enabledInConfig: boolean; enabledInLicense: boolean; } + +export interface DeleteCase { + id: string; + title?: string; +} diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx index f1129bae9f537..7d040c49f1971 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_bulk_update_case.tsx @@ -5,7 +5,7 @@ */ import { useCallback, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { patchCasesStatus } from './api'; import { BulkUpdateStatus, Case } from './types'; @@ -71,9 +71,22 @@ export const useUpdateCases = (): UseUpdateCase => { const patchData = async () => { try { dispatch({ type: 'FETCH_INIT' }); - await patchCasesStatus(cases, abortCtrl.signal); + const patchResponse = await patchCasesStatus(cases, abortCtrl.signal); if (!cancel) { + const resultCount = Object.keys(patchResponse).length; + const firstTitle = patchResponse[0].title; + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + const messageArgs = { + totalCases: resultCount, + caseTitle: resultCount === 1 ? firstTitle : '', + }; + const message = + resultCount && patchResponse[0].status === 'open' + ? i18n.REOPENED_CASES(messageArgs) + : i18n.CLOSED_CASES(messageArgs); + + displaySuccessToast(message, dispatchToaster); } } catch (error) { if (!cancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx index b44e01d06acaf..07e3786758aeb 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -5,9 +5,10 @@ */ import { useCallback, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import * as i18n from './translations'; import { deleteCases } from './api'; +import { DeleteCase } from './types'; interface DeleteState { isDisplayConfirmDeleteModal: boolean; @@ -57,9 +58,10 @@ const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { return state; } }; + interface UseDeleteCase extends DeleteState { dispatchResetIsDeleted: () => void; - handleOnDeleteConfirm: (caseIds: string[]) => void; + handleOnDeleteConfirm: (caseIds: DeleteCase[]) => void; handleToggleModal: () => void; } @@ -72,21 +74,26 @@ export const useDeleteCases = (): UseDeleteCase => { }); const [, dispatchToaster] = useStateToaster(); - const dispatchDeleteCases = useCallback((caseIds: string[]) => { + const dispatchDeleteCases = useCallback((cases: DeleteCase[]) => { let cancel = false; const abortCtrl = new AbortController(); const deleteData = async () => { try { dispatch({ type: 'FETCH_INIT' }); + const caseIds = cases.map(theCase => theCase.id); await deleteCases(caseIds, abortCtrl.signal); if (!cancel) { dispatch({ type: 'FETCH_SUCCESS', payload: true }); + displaySuccessToast( + i18n.DELETED_CASES(cases.length, cases.length === 1 ? cases[0].title : ''), + dispatchToaster + ); } } catch (error) { if (!cancel) { errorToToaster({ - title: i18n.ERROR_TITLE, + title: i18n.ERROR_DELETING, error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx index 6974000414a06..2478172a3394b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_reporters.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash/fp'; import { User } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getReporters } from './api'; @@ -44,9 +45,12 @@ export const useGetReporters = (): UseGetReporters => { }); try { const response = await getReporters(abortCtrl.signal); + const myReporters = response + .map(r => (r.full_name == null || isEmpty(r.full_name) ? r.username ?? '' : r.full_name)) + .filter(u => !isEmpty(u)); if (!didCancel) { setReporterState({ - reporters: response.map(r => r.full_name ?? r.username ?? 'N/A'), + reporters: myReporters, respReporters: response, isLoading: false, isError: false, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx index 03e10249317ee..d9a32f26f7fe7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_push_to_service.tsx @@ -148,7 +148,7 @@ const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { createdAt, createdBy: { fullName: createdBy.fullName ?? null, - username: createdBy?.username, + username: createdBy?.username ?? '', }, comments: comments .filter(c => { @@ -168,14 +168,14 @@ const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { createdAt: c.createdAt, createdBy: { fullName: c.createdBy.fullName ?? null, - username: c.createdBy.username, + username: c.createdBy.username ?? '', }, updatedAt: c.updatedAt, updatedBy: c.updatedBy != null ? { fullName: c.updatedBy.fullName ?? null, - username: c.updatedBy.username, + username: c.updatedBy.username ?? '', } : null, })), @@ -187,7 +187,7 @@ const formatServiceRequestData = (myCase: Case): ServiceConnectorCaseParams => { updatedBy != null ? { fullName: updatedBy.fullName ?? null, - username: updatedBy.username, + username: updatedBy.username ?? '', } : null, }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 85ad4fd3fc47a..4973deef4d91a 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -5,8 +5,8 @@ */ import { useReducer, useCallback } from 'react'; +import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; import { CasePatchRequest } from '../../../../../../plugins/case/common/api'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchCase } from './api'; import * as i18n from './translations'; @@ -94,6 +94,7 @@ export const useUpdateCase = ({ caseId }: { caseId: string }): UseUpdateCase => updateCase(response[0]); } dispatch({ type: 'FETCH_SUCCESS' }); + displaySuccessToast(i18n.UPDATED_CASE(response[0].title), dispatchToaster); } } catch (error) { if (!cancel) { diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index f676ab944fce4..bc559c5ac4972 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -6,12 +6,7 @@ import * as t from 'io-ts'; -export const RuleTypeSchema = t.keyof({ - query: null, - saved_query: null, - machine_learning: null, -}); -export type RuleType = t.TypeOf; +import { RuleTypeSchema } from '../../../../common/detection_engine/types'; /** * Params is an "record", since it is a type of AlertActionParams which is action templates. diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx index 7269bf1baa5e5..0a30329baf68d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx @@ -12,7 +12,7 @@ import { ReturnRulesStatuses, } from './use_rule_status'; import * as api from './api'; -import { RuleType, Rule } from '../rules/types'; +import { Rule } from '../rules/types'; jest.mock('./api'); @@ -57,7 +57,7 @@ const testRule: Rule = { threat: [], throttle: null, to: 'now', - type: 'query' as RuleType, + type: 'query', updated_at: 'mm/dd/yyyyTHH:MM:sssz', updated_by: 'mockUser', }; diff --git a/x-pack/legacy/plugins/siem/public/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 + + + - { let didCancel = false; const fetchData = async () => { try { - const response = await security.authc.getCurrentUser(); - if (!didCancel) { - setUser(convertToCamelCase(response)); + if (security != null) { + const response = await security.authc.getCurrentUser(); + if (!didCancel) { + setUser(convertToCamelCase(response)); + } + } else { + setUser({ + username: i18n.translate('xpack.siem.getCurrentUser.unknownUser', { + defaultMessage: 'Unknown', + }), + email: '', + fullName: '', + roles: [], + enabled: false, + authenticationRealm: { name: '', type: '' }, + lookupRealm: { name: '', type: '' }, + authenticationProvider: '', + }); } } catch (error) { if (!didCancel) { @@ -81,3 +96,29 @@ export const useCurrentUser = (): AuthenticatedElasticUser | null => { }, []); return user; }; + +export interface UseGetUserSavedObjectPermissions { + crud: boolean; + read: boolean; +} + +export const useGetUserSavedObjectPermissions = () => { + const [ + savedObjectsPermissions, + setSavedObjectsPermissions, + ] = useState(null); + const uiCapabilities = useKibana().services.application.capabilities; + + useEffect(() => { + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; + const capabilitiesCanUserRead: boolean = + typeof uiCapabilities.siem.show === 'boolean' ? uiCapabilities.siem.show : false; + setSavedObjectsPermissions({ + crud: capabilitiesCanUserCRUD, + read: capabilitiesCanUserRead, + }); + }, [uiCapabilities]); + + return savedObjectsPermissions; +}; diff --git a/x-pack/legacy/plugins/siem/public/mock/index.ts b/x-pack/legacy/plugins/siem/public/mock/index.ts index dbf5f2e55e713..bdad0ab1712ab 100644 --- a/x-pack/legacy/plugins/siem/public/mock/index.ts +++ b/x-pack/legacy/plugins/siem/public/mock/index.ts @@ -13,3 +13,5 @@ export * from './mock_detail_item'; export * from './netflow'; export * from './test_providers'; export * from './utils'; +export * from './mock_ecs'; +export * from './timeline_results'; diff --git a/x-pack/legacy/plugins/siem/public/mock/mock_ecs.ts b/x-pack/legacy/plugins/siem/public/mock/mock_ecs.ts index 5d32d95804e69..59e26039e6bff 100644 --- a/x-pack/legacy/plugins/siem/public/mock/mock_ecs.ts +++ b/x-pack/legacy/plugins/siem/public/mock/mock_ecs.ts @@ -1280,3 +1280,69 @@ export const mockEcsData: Ecs[] = [ zeek: null, }, ]; + +export const mockEcsDataWithSignal: Ecs = { + _id: '1', + timestamp: '2018-11-05T19:03:25.937Z', + host: { + name: ['apache'], + ip: ['192.168.0.1'], + }, + event: { + id: ['1'], + action: ['Action'], + category: ['Access'], + module: ['nginx'], + severity: [3], + }, + source: { + ip: ['192.168.0.1'], + port: [80], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + user: { + id: ['1'], + name: ['john.dee'], + }, + geo: { + region_name: ['xx'], + country_iso_code: ['xx'], + }, + signal: { + rule: { + created_at: ['2020-01-10T21:11:45.839Z'], + updated_at: ['2020-01-10T21:11:45.839Z'], + created_by: ['elastic'], + description: ['24/7'], + enabled: [true], + false_positives: ['test-1'], + filters: [], + from: ['now-300s'], + id: ['b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea'], + immutable: [false], + index: ['auditbeat-*'], + interval: ['5m'], + rule_id: ['rule-id-1'], + language: ['kuery'], + output_index: ['.siem-signals-default'], + max_signals: [100], + risk_score: ['21'], + query: ['user.name: root or user.name: admin'], + references: ['www.test.co'], + saved_id: ["Garrett's IP"], + timeline_id: ['1234-2136-11ea-9864-ebc8cc1cb8c2'], + timeline_title: ['Untitled timeline'], + severity: ['low'], + updated_by: ['elastic'], + tags: [], + to: ['now'], + type: ['saved_query'], + threat: [], + note: ['# this is some markdown documentation'], + version: ['1'], + }, + }, +}; diff --git a/x-pack/legacy/plugins/siem/public/mock/timeline_results.ts b/x-pack/legacy/plugins/siem/public/mock/timeline_results.ts index d6dc0ae131391..363281e563317 100644 --- a/x-pack/legacy/plugins/siem/public/mock/timeline_results.ts +++ b/x-pack/legacy/plugins/siem/public/mock/timeline_results.ts @@ -7,7 +7,10 @@ import { OpenTimelineResult } from '../components/open_timeline/types'; import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../graphql/types'; import { allTimelinesQuery } from '../containers/timeline/all/index.gql_query'; - +import { CreateTimelineProps } from '../pages/detection_engine/components/signals/types'; +import { TimelineModel } from '../store/timeline/model'; +import { timelineDefaults } from '../store/timeline/defaults'; +import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; export interface MockedProvidedQuery { request: { query: GetAllTimeline.Query; @@ -2006,3 +2009,196 @@ export const mockTimelineResults: OpenTimelineResult[] = [ updatedBy: 'karen', }, ]; + +export const mockTimelineModel: TimelineModel = { + 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: '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: [], + dateRange: { + end: 1584539558929, + start: 1584539198929, + }, + deletedEventIds: [], + description: 'This is a sample rule description', + eventIdToNoteIds: {}, + eventType: 'all', + filters: [ + { + $state: { + store: FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: true, + key: 'host.name', + negate: false, + params: '"{"query":"placeholder"}"', + type: 'phrase', + }, + query: '"{"match_phrase":{"host.name":"placeholder"}}"', + }, + ], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'ef579e40-jibber-jabber', + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + isSelectAllChecked: false, + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'ef579e40-jibber-jabber', + selectedEventIds: {}, + show: false, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + title: 'Test rule', + version: '1', + width: 1100, +}; + +export const mockTimelineResult: TimelineResult = { + savedObjectId: 'ef579e40-jibber-jabber', + columns: timelineDefaults.columns.filter(column => 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/case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx index 9255dee461940..2ae35796387b8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case.tsx @@ -7,16 +7,34 @@ import React from 'react'; import { WrapperPage } from '../../components/wrapper_page'; -import { AllCases } from './components/all_cases'; +import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { AllCases } from './components/all_cases'; + +import { getSavedObjectReadOnly, CaseCallOut } from './components/callout'; +import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; + +const infoReadSavedObject = getSavedObjectReadOnly(); + +export const CasesPage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); -export const CasesPage = React.memo(() => ( - <> - - - - - -)); + return userPermissions == null || userPermissions?.read ? ( + <> + + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + + + + + ) : ( + + ); +}); CasesPage.displayName = 'CasesPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx index 890df91c8560e..cbc7bbc62fbf9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/case_details.tsx @@ -5,22 +5,36 @@ */ import React from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, Redirect } from 'react-router-dom'; -import { CaseView } from './components/case_view'; +import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; +import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { getCaseUrl } from '../../components/link_to'; +import { navTabs } from '../home/home_navigations'; +import { CaseView } from './components/case_view'; +import { getSavedObjectReadOnly, CaseCallOut } from './components/callout'; + +const infoReadSavedObject = getSavedObjectReadOnly(); export const CaseDetailsPage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); const { detailName: caseId } = useParams(); - if (!caseId) { - return null; + const search = useGetUrlSearch(navTabs.case); + + if (userPermissions != null && !userPermissions.read) { + return ; } - return ( + + return caseId != null ? ( <> - + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + - ); + ) : null; }); CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx index 46a777984c6e0..ecc57c50e28eb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -31,6 +31,7 @@ const initialCommentValue: CommentRequest = { interface AddCommentProps { caseId: string; + disabled?: boolean; insertQuote: string | null; onCommentSaving?: () => void; onCommentPosted: (newCase: Case) => void; @@ -38,7 +39,7 @@ interface AddCommentProps { } export const AddComment = React.memo( - ({ caseId, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { + ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { const { isLoading, postComment } = usePostComment(caseId); const { form } = useForm({ defaultValue: initialCommentValue, @@ -87,7 +88,7 @@ export const AddComment = React.memo( bottomRightContent: ( - {createdBy.fullName ?? createdBy.username ?? 'N/A'} + {createdBy.fullName ?? createdBy.username ?? ''} ); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index bdcb87b483851..a6da45a8c5bb1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -87,7 +87,7 @@ describe('AllCases', () => { it('should render AllCases', () => { const wrapper = mount( - + ); expect( @@ -132,7 +132,7 @@ describe('AllCases', () => { it('should tableHeaderSortButton AllCases', () => { const wrapper = mount( - + ); wrapper @@ -149,7 +149,7 @@ describe('AllCases', () => { it('closes case when row action icon clicked', () => { const wrapper = mount( - + ); wrapper @@ -182,7 +182,7 @@ describe('AllCases', () => { const wrapper = mount( - + ); wrapper @@ -202,7 +202,7 @@ describe('AllCases', () => { .last() .simulate('click'); expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( - useGetCasesMockState.data.cases.map(theCase => theCase.id) + useGetCasesMockState.data.cases.map(({ id }) => ({ id })) ); }); it('Bulk close status update', () => { @@ -213,7 +213,7 @@ describe('AllCases', () => { const wrapper = mount( - + ); wrapper @@ -238,7 +238,7 @@ describe('AllCases', () => { const wrapper = mount( - + ); wrapper @@ -259,7 +259,7 @@ describe('AllCases', () => { mount( - + ); expect(refetchCases).toBeCalled(); @@ -274,7 +274,7 @@ describe('AllCases', () => { mount( - + ); expect(refetchCases).toBeCalled(); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 27316ab8427cb..161910bb5498a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -17,11 +17,12 @@ import { EuiTableSortingType, } from '@elastic/eui'; import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; +import { isEmpty } from 'lodash/fp'; import styled, { css } from 'styled-components'; import * as i18n from './translations'; import { getCasesColumns } from './columns'; -import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; +import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cases'; import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; @@ -35,7 +36,7 @@ import { UtilityBarSection, UtilityBarText, } from '../../../../components/utility_bar'; -import { getConfigureCasesUrl, getCreateCaseUrl } from '../../../../components/link_to'; +import { getCreateCaseUrl } from '../../../../components/link_to'; import { getBulkItems } from '../bulk_actions'; import { CaseHeaderPage } from '../case_header_page'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; @@ -45,6 +46,11 @@ import { navTabs } from '../../../home/home_navigations'; import { getActions } from './actions'; import { CasesTableFilters } from './table_filters'; import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; +import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; +import { getActionLicenseError } from '../use_push_to_service/helpers'; +import { CaseCallOut } from '../callout'; +import { ConfigureCaseButton } from '../configure_cases/button'; +import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -75,9 +81,13 @@ const getSortField = (field: string): SortFieldCase => { } return SortFieldCase.createdAt; }; -export const AllCases = React.memo(() => { - const urlSearch = useGetUrlSearch(navTabs.case); +interface AllCasesProps { + userCanCrud: boolean; +} +export const AllCases = React.memo(({ userCanCrud }) => { + const urlSearch = useGetUrlSearch(navTabs.case); + const { actionLicense } = useGetActionLicense(); const { countClosedCases, countOpenCases, @@ -107,11 +117,24 @@ export const AllCases = React.memo(() => { isDisplayConfirmDeleteModal, } = useDeleteCases(); - const { dispatchResetIsUpdated, isUpdated, updateBulkStatus } = useUpdateCases(); + // Update case + const { + dispatchResetIsUpdated, + isLoading: isUpdating, + isUpdated, + updateBulkStatus, + } = useUpdateCases(); + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); const refreshCases = useCallback(() => { refetchCases(filterOptions, queryParams); fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); }, [filterOptions, queryParams]); useEffect(() => { @@ -124,11 +147,6 @@ export const AllCases = React.memo(() => { dispatchResetIsUpdated(); } }, [isDeleted, isUpdated]); - const [deleteThisCase, setDeleteThisCase] = useState({ - title: '', - id: '', - }); - const [deleteBulk, setDeleteBulk] = useState([]); const confirmDeleteModal = useMemo( () => ( { onCancel={handleToggleModal} onConfirm={handleOnDeleteConfirm.bind( null, - deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id] + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] )} /> ), @@ -150,10 +168,20 @@ export const AllCases = React.memo(() => { setDeleteThisCase(deleteCase); }, []); - const toggleBulkDeleteModal = useCallback((deleteCases: string[]) => { - handleToggleModal(); - setDeleteBulk(deleteCases); - }, []); + const toggleBulkDeleteModal = useCallback( + (caseIds: string[]) => { + handleToggleModal(); + if (caseIds.length === 1) { + const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]); + if (singleCase) { + return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + } + } + const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id })); + setDeleteBulk(convertToDeleteCases); + }, + [selectedCases] + ); const handleUpdateCaseStatus = useCallback( (status: string) => { @@ -199,6 +227,8 @@ export const AllCases = React.memo(() => { [filterOptions.status, toggleDeleteModal, handleDispatchUpdate] ); + const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); + const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { let newQueryParams = queryParams; @@ -233,10 +263,10 @@ export const AllCases = React.memo(() => { [filterOptions, queryParams] ); - const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions, filterOptions.status), [ - actions, - filterOptions.status, - ]); + const memoizedGetCasesColumns = useMemo( + () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status), + [actions, filterOptions.status, userCanCrud] + ); const memoizedPagination = useMemo( () => ({ pageIndex: queryParams.page - 1, @@ -259,8 +289,12 @@ export const AllCases = React.memo(() => { [loading] ); const isDataEmpty = useMemo(() => data.total === 0, [data]); + return ( <> + {!isEmpty(actionsErrors) && ( + + )} @@ -278,18 +312,28 @@ export const AllCases = React.memo(() => { /> - - {i18n.CONFIGURE_CASES_BUTTON} - + } + titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} + urlSearch={urlSearch} + /> - + {i18n.CREATE_TITLE} - {(isCasesLoading || isDeleting) && !isDataEmpty && ( + {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( )} @@ -321,15 +365,16 @@ export const AllCases = React.memo(() => { {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} - - {i18n.BULK_ACTIONS} - - + {userCanCrud && ( + + {i18n.BULK_ACTIONS} + + )} {i18n.REFRESH} @@ -339,7 +384,7 @@ export const AllCases = React.memo(() => { { body={i18n.NO_CASES_BODY} actions={ { } onChange={tableOnChangeCallback} pagination={memoizedPagination} - selection={euiBasicTableSelectionProps} + selection={userCanCrud ? euiBasicTableSelectionProps : {}} sorting={sorting} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index a71ad1c45a980..a344dd7891010 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -43,7 +43,7 @@ const CasesTableFiltersComponent = ({ initial = defaultInitial, }: CasesTableFiltersProps) => { const [selectedReporters, setselectedReporters] = useState( - initial.reporters.map(r => r.full_name ?? r.username) + initial.reporters.map(r => r.full_name ?? r.username ?? '') ); const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx new file mode 100644 index 0000000000000..929e8640dceb6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/helpers.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as i18n from './translations'; + +export const getSavedObjectReadOnly = () => ({ + title: i18n.READ_ONLY_SAVED_OBJECT_TITLE, + description: i18n.READ_ONLY_SAVED_OBJECT_MSG, +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx similarity index 59% rename from x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx rename to x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx index 15b50e4b4cd8d..30a95db2d82a5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/errors_push_service_callout/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/callout/index.tsx @@ -5,22 +5,28 @@ */ import { EuiCallOut, EuiButton, EuiDescriptionList, EuiSpacer } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; import React, { memo, useCallback, useState } from 'react'; import * as i18n from './translations'; -interface ErrorsPushServiceCallOut { - errors: Array<{ title: string; description: JSX.Element }>; +export * from './helpers'; + +interface CaseCallOutProps { + title: string; + message?: string; + messages?: Array<{ title: string; description: JSX.Element }>; } -const ErrorsPushServiceCallOutComponent = ({ errors }: ErrorsPushServiceCallOut) => { +const CaseCallOutComponent = ({ title, message, messages }: CaseCallOutProps) => { const [showCallOut, setShowCallOut] = useState(true); const handleCallOut = useCallback(() => setShowCallOut(false), [setShowCallOut]); return showCallOut ? ( <> - - + + {!isEmpty(messages) && } + {!isEmpty(message) &&

{message}

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

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

{i18n.TAGS}

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

{i18n.TAGS}

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

{i18n.NO_TAGS}

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

{i18n.NO_TAGS}

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

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

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

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

- - {username} - -

+ {fullName ?? username}

}> +

+ + {username} + +

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

{headline}

@@ -89,9 +92,12 @@ export const UserList = React.memo(({ email, headline, loading, users }: UserLis
)} - {renderUsers(users, handleSendEmail)} + {renderUsers( + users.filter(({ username }) => username != null && username !== ''), + handleSendEmail + )} - ); + ) : null; }); UserList.displayName = 'UserList'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx index b7e7ced308331..7515efa0e1b7a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/configure_cases.tsx @@ -5,16 +5,18 @@ */ import React, { useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; +import { getCaseUrl } from '../../components/link_to'; +import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; import { WrapperPage } from '../../components/wrapper_page'; -import { CaseHeaderPage } from './components/case_header_page'; +import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; -import { getCaseUrl } from '../../components/link_to'; +import { navTabs } from '../home/home_navigations'; +import { CaseHeaderPage } from './components/case_header_page'; +import { ConfigureCases } from './components/configure_cases'; import { WhitePageWrapper, SectionWrapper } from './components/wrappers'; import * as i18n from './translations'; -import { ConfigureCases } from './components/configure_cases'; -import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; -import { navTabs } from '../home/home_navigations'; const wrapperPageStyle: Record = { paddingLeft: '0', @@ -23,6 +25,7 @@ const wrapperPageStyle: Record = { }; const ConfigureCasesPageComponent: React.FC = () => { + const userPermissions = useGetUserSavedObjectPermissions(); const search = useGetUrlSearch(navTabs.case); const backOptions = useMemo( @@ -33,6 +36,10 @@ const ConfigureCasesPageComponent: React.FC = () => { [search] ); + if (userPermissions != null && !userPermissions.read) { + return ; + } + return ( <> @@ -40,7 +47,7 @@ const ConfigureCasesPageComponent: React.FC = () => {
- + diff --git a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx index bd1f6da0ca28b..06cb7fadfb8d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/create_case.tsx @@ -5,17 +5,20 @@ */ import React, { useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; +import { getCaseUrl } from '../../components/link_to'; +import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; import { WrapperPage } from '../../components/wrapper_page'; -import { Create } from './components/create'; +import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; import { SpyRoute } from '../../utils/route/spy_routes'; +import { navTabs } from '../home/home_navigations'; import { CaseHeaderPage } from './components/case_header_page'; +import { Create } from './components/create'; import * as i18n from './translations'; -import { getCaseUrl } from '../../components/link_to'; -import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; -import { navTabs } from '../home/home_navigations'; export const CreateCasePage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); const search = useGetUrlSearch(navTabs.case); const backOptions = useMemo( @@ -26,6 +29,10 @@ export const CreateCasePage = React.memo(() => { [search] ); + if (userPermissions != null && !userPermissions.crud) { + return ; + } + return ( <> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/saved_object_no_permissions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/saved_object_no_permissions.tsx new file mode 100644 index 0000000000000..689c290c91019 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/saved_object_no_permissions.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { EmptyPage } from '../../components/empty_page'; +import * as i18n from './translations'; +import { useKibana } from '../../lib/kibana'; + +export const CaseSavedObjectNoPermissions = React.memo(() => { + const docLinks = useKibana().services.docLinks; + + return ( + + ); +}); + +CaseSavedObjectNoPermissions.displayName = 'CaseSavedObjectNoPermissions'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 8f9d2087699f8..0d1e6d1435ca3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -6,6 +6,21 @@ import { i18n } from '@kbn/i18n'; +export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.siem.case.caseSavedObjectNoPermissionsTitle', + { + defaultMessage: 'Kibana feature privileges required', + } +); + +export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.siem.case.caseSavedObjectNoPermissionsMessage', + { + defaultMessage: + 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + } +); + export const BACK_TO_ALL = i18n.translate('xpack.siem.case.caseView.backLabel', { defaultMessage: 'Back to cases', }); @@ -169,3 +184,10 @@ export const ADD_COMMENT_HELP_TEXT = i18n.translate( export const SAVE = i18n.translate('xpack.siem.case.caseView.description.save', { defaultMessage: 'Save', }); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.siem.case.caseView.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/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..2235207070fe3 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 { @@ -119,6 +121,10 @@ export class Plugin { pinnedEventSavedObjectType, timelineSavedObjectType, ruleStatusSavedObjectType, + 'cases', + 'cases-comments', + 'cases-configure', + 'cases-user-actions', ], read: ['config'], }, @@ -145,6 +151,10 @@ export class Plugin { pinnedEventSavedObjectType, timelineSavedObjectType, ruleStatusSavedObjectType, + 'cases', + 'cases-comments', + 'cases-configure', + 'cases-user-actions', ], }, ui: [ @@ -164,6 +174,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/case/common/api/user.ts b/x-pack/plugins/case/common/api/user.ts index 3adb78ccdac07..af198470737cf 100644 --- a/x-pack/plugins/case/common/api/user.ts +++ b/x-pack/plugins/case/common/api/user.ts @@ -9,7 +9,7 @@ import * as rt from 'io-ts'; export const UserRT = rt.type({ email: rt.union([rt.undefined, rt.null, rt.string]), full_name: rt.union([rt.undefined, rt.null, rt.string]), - username: rt.string, + username: rt.union([rt.undefined, rt.null, rt.string]), }); export const UsersRt = rt.array(UserRT); diff --git a/x-pack/plugins/case/kibana.json b/x-pack/plugins/case/kibana.json index f565dc1b6924e..55416ee28c7df 100644 --- a/x-pack/plugins/case/kibana.json +++ b/x-pack/plugins/case/kibana.json @@ -2,7 +2,7 @@ "configPath": ["xpack", "case"], "id": "case", "kibanaVersion": "kibana", - "requiredPlugins": ["security", "actions"], + "requiredPlugins": ["actions"], "optionalPlugins": [ "spaces", "security" diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index a6a459373b0ed..670e6ec797a9f 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -60,7 +60,7 @@ export class CasePlugin { ); const caseService = await caseServicePlugin.setup({ - authentication: plugins.security.authc, + authentication: plugins.security != null ? plugins.security.authc : null, }); const caseConfigureService = await caseConfigureServicePlugin.setup(); const userActionService = await userActionServicePlugin.setup(); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index af6f8bf223ee5..23039da681ec6 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -20,6 +20,10 @@ describe('POST comment', () => { let routeHandler: RequestHandler; beforeAll(async () => { routeHandler = await createRoute(initPostCommentApi, 'post'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); }); it(`Posts a new comment`, async () => { const request = httpServerMock.createKibanaRequest({ @@ -92,7 +96,7 @@ describe('POST comment', () => { expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); - it(`Returns an error if user authentication throws`, async () => { + it(`Allow user to create comments without authentications`, async () => { routeHandler = await createRoute(initPostCommentApi, 'post', true); const request = httpServerMock.createKibanaRequest({ @@ -114,7 +118,21 @@ describe('POST comment', () => { ); const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(500); - expect(response.payload.isBoom).toEqual(true); + expect(response.status).toEqual(200); + expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({ + comment: 'Wow, good luck catching that bad meanie!', + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + email: null, + full_name: null, + username: null, + }, + id: 'mock-comment', + pushed_at: null, + pushed_by: null, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index 96ce3c1a7eead..5899102224774 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -19,6 +19,10 @@ describe('POST cases', () => { let routeHandler: RequestHandler; beforeAll(async () => { routeHandler = await createRoute(initPostCaseApi, 'post'); + const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; + spyOnDate.mockImplementation(() => ({ + toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), + })); }); it(`Posts a new case`, async () => { const request = httpServerMock.createKibanaRequest({ @@ -85,7 +89,7 @@ describe('POST cases', () => { expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); - it(`Returns an error if user authentication throws`, async () => { + it(`Allow user to create case without authentication`, async () => { routeHandler = await createRoute(initPostCaseApi, 'post', true); const request = httpServerMock.createKibanaRequest({ @@ -105,7 +109,27 @@ describe('POST cases', () => { ); const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(500); - expect(response.payload.isBoom).toEqual(true); + expect(response.status).toEqual(200); + expect(response.payload).toEqual({ + closed_at: null, + closed_by: null, + comments: [], + created_at: '2019-11-25T21:54:48.952Z', + created_by: { + email: null, + full_name: null, + username: null, + }, + description: 'This is a brand new case of a bad meanie defacing data', + external_service: null, + id: 'mock-it', + status: 'open', + tags: ['defacement'], + title: 'Super Bad Security Issue', + totalComment: 0, + updated_at: null, + updated_by: null, + version: 'WzksMV0=', + }); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index 1b24904ce03b7..aff057adea37f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -15,7 +15,6 @@ import { flattenCaseSavedObject, wrapError, escapeHatch } from '../utils'; import { CaseExternalServiceRequestRt, CaseResponseRt, throwErrors } from '../../../../common/api'; import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; import { RouteDeps } from '../types'; -import { CASE_COMMENT_SAVED_OBJECT } from '../../../saved_object_types'; export function initPushCaseUserActionApi({ caseConfigureService, @@ -54,7 +53,6 @@ export function initPushCaseUserActionApi({ client, caseId, options: { - filter: `not ${CASE_COMMENT_SAVED_OBJECT}.attributes.pushed_at: *`, fields: [], page: 1, perPage: 1, @@ -72,7 +70,6 @@ export function initPushCaseUserActionApi({ client, caseId, options: { - filter: `not ${CASE_COMMENT_SAVED_OBJECT}.attributes.pushed_at: *`, fields: [], page: 1, perPage: totalCommentsFindByCases.total, @@ -105,16 +102,16 @@ export function initPushCaseUserActionApi({ }), caseService.patchComments({ client, - comments: comments.saved_objects.map(comment => ({ - commentId: comment.id, - updatedAttributes: { - pushed_at: pushedDate, - pushed_by: { username, full_name, email }, - updated_at: pushedDate, - updated_by: { username, full_name, email }, - }, - version: comment.version, - })), + comments: comments.saved_objects + .filter(comment => comment.attributes.pushed_at == null) + .map(comment => ({ + commentId: comment.id, + updatedAttributes: { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + }, + version: comment.version, + })), }), userActionService.postUserActions({ client, diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 822d6d70c7d61..a3df0fc93d2ac 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -33,10 +33,10 @@ export const transformNewCase = ({ username, }: { createdDate: string; - email?: string; - full_name?: string; + email?: string | null; + full_name?: string | null; newCase: CasePostRequest; - username: string; + username?: string | null; }): CaseAttributes => ({ ...newCase, closed_at: null, @@ -52,9 +52,9 @@ export const transformNewCase = ({ interface NewCommentArgs { comment: string; createdDate: string; - email?: string; - full_name?: string; - username: string; + email?: string | null; + full_name?: string | null; + username?: string | null; } export const transformNewComment = ({ comment, diff --git a/x-pack/plugins/case/server/scripts/mock/case/post_case.json b/x-pack/plugins/case/server/scripts/mock/case/post_case.json index 25a9780596828..743fa396295ca 100644 --- a/x-pack/plugins/case/server/scripts/mock/case/post_case.json +++ b/x-pack/plugins/case/server/scripts/mock/case/post_case.json @@ -1,7 +1,6 @@ { "description": "This looks not so good", "title": "Bad meanie defacing data", - "status": "open", "tags": [ "defacement" ] diff --git a/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json b/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json index cf066d2c8a1e8..13efe436a640d 100644 --- a/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json +++ b/x-pack/plugins/case/server/scripts/mock/case/post_case_v2.json @@ -1,7 +1,6 @@ { "description": "I hope there are some good security engineers at this company...", "title": "Another bad dude", - "status": "open", "tags": [ "phishing" ] diff --git a/x-pack/plugins/case/server/services/index.ts b/x-pack/plugins/case/server/services/index.ts index 52f41aae293ab..cdc5fd21a8138 100644 --- a/x-pack/plugins/case/server/services/index.ts +++ b/x-pack/plugins/case/server/services/index.ts @@ -95,7 +95,7 @@ interface GetUserArgs { } interface CaseServiceDeps { - authentication: SecurityPluginSetup['authc']; + authentication: SecurityPluginSetup['authc'] | null; } export interface CaseServiceSetup { deleteCase(args: GetCaseArgs): Promise<{}>; @@ -107,7 +107,7 @@ export interface CaseServiceSetup { getComment(args: GetCommentArgs): Promise>; getTags(args: ClientArgs): Promise; getReporters(args: ClientArgs): Promise; - getUser(args: GetUserArgs): Promise; + getUser(args: GetUserArgs): Promise; postNewCase(args: PostCaseArgs): Promise>; postNewComment(args: PostCommentArgs): Promise>; patchCase(args: PatchCaseArgs): Promise>; @@ -207,13 +207,28 @@ export class CaseService { } }, getUser: async ({ request, response }: GetUserArgs) => { - this.log.debug(`Attempting to authenticate a user`); - const user = authentication!.getCurrentUser(request); - if (!user) { - this.log.debug(`Error on GET user: Bad User`); - throw new Error('Bad User - the user is not authenticated'); + try { + this.log.debug(`Attempting to authenticate a user`); + if (authentication != null) { + const user = authentication.getCurrentUser(request); + if (!user) { + return { + username: null, + full_name: null, + email: null, + }; + } + return user; + } + return { + username: null, + full_name: null, + email: null, + }; + } catch (error) { + this.log.debug(`Error on GET cases: ${error}`); + throw error; } - return user; }, postNewCase: async ({ client, attributes }: PostCaseArgs) => { try { diff --git a/x-pack/plugins/case/server/services/user_actions/helpers.ts b/x-pack/plugins/case/server/services/user_actions/helpers.ts index 95d35d5a57a57..e89700419b19d 100644 --- a/x-pack/plugins/case/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/case/server/services/user_actions/helpers.ts @@ -35,7 +35,7 @@ export const transformNewUserAction = ({ full_name?: string | null; newValue?: string | null; oldValue?: string | null; - username: string; + username?: string | null; }): CaseUserActionAttributes => ({ action_field: actionField, action, diff --git a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts index c493e8ce86781..70bdcdfd3cf1f 100644 --- a/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/es_search_strategy.ts @@ -33,7 +33,7 @@ export const enhancedEsSearchStrategyProvider: TSearchStrategyProvider { 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/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 814825483d0dd..30a3350ad754e 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -90,16 +90,16 @@ export const FEATURE_VISIBLE_PROPERTY_NAME = '__kbn_isvisibleduetojoin__'; export const MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER = '_'; -export const ES_GEO_FIELD_TYPE = { - GEO_POINT: 'geo_point', - GEO_SHAPE: 'geo_shape', -}; +export enum ES_GEO_FIELD_TYPE { + GEO_POINT = 'geo_point', + GEO_SHAPE = 'geo_shape', +} -export const ES_SPATIAL_RELATIONS = { - INTERSECTS: 'INTERSECTS', - DISJOINT: 'DISJOINT', - WITHIN: 'WITHIN', -}; +export enum ES_SPATIAL_RELATIONS { + INTERSECTS = 'INTERSECTS', + DISJOINT = 'DISJOINT', + WITHIN = 'WITHIN', +} export const GEO_JSON_TYPE = { POINT: 'Point', @@ -120,11 +120,11 @@ export const EMPTY_FEATURE_COLLECTION = { features: [], }; -export const DRAW_TYPE = { - BOUNDS: 'BOUNDS', - DISTANCE: 'DISTANCE', - POLYGON: 'POLYGON', -}; +export enum DRAW_TYPE { + BOUNDS = 'BOUNDS', + DISTANCE = 'DISTANCE', + POLYGON = 'POLYGON', +} export enum AGG_TYPE { AVG = 'avg', diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts index a0102a4249a59..ca0e474491780 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.d.ts @@ -5,21 +5,14 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { Query } from './map_descriptor'; - -type Extent = { - maxLat: number; - maxLon: number; - minLat: number; - minLon: number; -}; +import { MapExtent, MapQuery } from './map_descriptor'; // Global map state passed to every layer. export type MapFilters = { - buffer: Extent; // extent with additional buffer - extent: Extent; // map viewport + buffer: MapExtent; // extent with additional buffer + extent: MapExtent; // map viewport filters: unknown[]; - query: Query; + query: MapQuery; refreshTimerLastTriggeredAt: string; timeFilters: unknown; zoom: number; @@ -29,14 +22,14 @@ export type VectorSourceRequestMeta = MapFilters & { applyGlobalQuery: boolean; fieldNames: string[]; geogridPrecision: number; - sourceQuery: Query; + sourceQuery: MapQuery; sourceMeta: unknown; }; export type VectorStyleRequestMeta = MapFilters & { dynamicStyleFields: string[]; isTimeAware: boolean; - sourceQuery: Query; + sourceQuery: MapQuery; timeFilters: unknown; }; diff --git a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts index 570398e37c5d4..b2a4c6b85a856 100644 --- a/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts +++ b/x-pack/plugins/maps/common/descriptor_types/map_descriptor.ts @@ -3,11 +3,67 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - /* eslint-disable @typescript-eslint/consistent-type-definitions */ -export type Query = { - language: string; - query: string; +import { Query } from '../../../../../src/plugins/data/public'; +import { DRAW_TYPE, ES_GEO_FIELD_TYPE, ES_SPATIAL_RELATIONS } from '../constants'; + +export type MapExtent = { + maxLat: number; + maxLon: number; + minLat: number; + minLon: number; +}; + +export type MapQuery = Query & { queryLastTriggeredAt: string; }; + +export type MapRefreshConfig = { + isPaused: boolean; + interval: number; +}; + +export type MapCenter = { + lat: number; + lon: number; +}; + +export type MapCenterAndZoom = MapCenter & { + zoom: number; +}; + +// TODO replace with map_descriptors.MapExtent. Both define the same thing but with different casing +type MapBounds = { + min_lon: number; + max_lon: number; + min_lat: number; + max_lat: number; +}; + +export type Goto = { + bounds?: MapBounds; + center?: MapCenterAndZoom; +}; + +export type TooltipFeature = { + id: number; + layerId: string; +}; + +export type TooltipState = { + features: TooltipFeature[]; + id: string; + isLocked: boolean; + location: number[]; // 0 index is lon, 1 index is lat +}; + +export type DrawState = { + drawType: DRAW_TYPE; + filterLabel?: string; // point radius filter alias + geoFieldName?: string; + geoFieldType?: ES_GEO_FIELD_TYPE; + geometryLabel?: string; + indexPatternId?: string; + relation?: ES_SPATIAL_RELATIONS; +}; diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts new file mode 100644 index 0000000000000..30271d4d5fa8b --- /dev/null +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { + DrawState, + Goto, + LayerDescriptor, + MapCenter, + MapExtent, + MapQuery, + MapRefreshConfig, + TooltipState, +} from '../../common/descriptor_types'; +import { Filter, TimeRange } from '../../../../../src/plugins/data/public'; + +export type MapContext = { + zoom?: number; + center?: MapCenter; + scrollZoom: boolean; + extent?: MapExtent; + mouseCoordinates?: { + lat: number; + lon: number; + }; + timeFilters?: TimeRange; + query?: MapQuery; + filters: Filter[]; + refreshConfig?: MapRefreshConfig; + refreshTimerLastTriggeredAt?: string; + drawState?: DrawState; + disableInteractive: boolean; + disableTooltipControl: boolean; + hideToolbarOverlay: boolean; + hideLayerControl: boolean; + hideViewControl: boolean; +}; + +export type MapState = { + ready: boolean; + mapInitError?: string | null; + goto?: Goto | null; + openTooltips: TooltipState[]; + mapState: MapContext; + selectedLayerId: string | null; + __transientLayerId: string | null; + layerList: LayerDescriptor[]; + waitingForMapReadyLayerList: LayerDescriptor[]; +}; diff --git a/x-pack/plugins/maps/public/reducers/store.d.ts b/x-pack/plugins/maps/public/reducers/store.d.ts index ebed396e20399..72713f943d6a6 100644 --- a/x-pack/plugins/maps/public/reducers/store.d.ts +++ b/x-pack/plugins/maps/public/reducers/store.d.ts @@ -5,7 +5,14 @@ */ import { Store } from 'redux'; +import { MapState } from './map'; +import { MapUiState } from './ui'; -export type MapStore = Store; +export interface MapStoreState { + ui: MapUiState; + map: MapState; +} + +export type MapStore = Store; export function createMapStore(): MapStore; diff --git a/x-pack/plugins/maps/public/reducers/ui.js b/x-pack/plugins/maps/public/reducers/ui.ts similarity index 76% rename from x-pack/plugins/maps/public/reducers/ui.js rename to x-pack/plugins/maps/public/reducers/ui.ts index 287e1f8dd3dda..7429545ec0e46 100644 --- a/x-pack/plugins/maps/public/reducers/ui.js +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ import { UPDATE_FLYOUT, @@ -15,19 +16,30 @@ import { SHOW_TOC_DETAILS, HIDE_TOC_DETAILS, UPDATE_INDEXING_STAGE, + // @ts-ignore } from '../actions/ui_actions'; -export const FLYOUT_STATE = { - NONE: 'NONE', - LAYER_PANEL: 'LAYER_PANEL', - ADD_LAYER_WIZARD: 'ADD_LAYER_WIZARD', -}; +export enum FLYOUT_STATE { + NONE = 'NONE', + LAYER_PANEL = 'LAYER_PANEL', + ADD_LAYER_WIZARD = 'ADD_LAYER_WIZARD', +} + +export enum INDEXING_STAGE { + READY = 'READY', + TRIGGERED = 'TRIGGERED', + SUCCESS = 'SUCCESS', + ERROR = 'ERROR', +} -export const INDEXING_STAGE = { - READY: 'READY', - TRIGGERED: 'TRIGGERED', - SUCCESS: 'SUCCESS', - ERROR: 'ERROR', +export type MapUiState = { + flyoutDisplay: FLYOUT_STATE; + isFullScreen: boolean; + isReadOnly: boolean; + isLayerTOCOpen: boolean; + isSetViewOpen: boolean; + openTOCDetails: string[]; + importIndexingStage: INDEXING_STAGE | null; }; export const DEFAULT_IS_LAYER_TOC_OPEN = true; @@ -45,7 +57,7 @@ const INITIAL_STATE = { }; // Reducer -export function ui(state = INITIAL_STATE, action) { +export function ui(state: MapUiState = INITIAL_STATE, action: any) { switch (action.type) { case UPDATE_FLYOUT: return { ...state, flyoutDisplay: action.display }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index e8f59ea7a65b2..d77f19c0df79d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -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/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/ml/bucket_span_estimator.ts b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts index a50d65a48c2bb..3f56fb927d131 100644 --- a/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts +++ b/x-pack/test/api_integration/apis/ml/bucket_span_estimator.ts @@ -18,7 +18,7 @@ export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const esSupertest = getService('esSupertest'); const supertest = getService('supertestWithoutAuth'); - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); const testDataList = [ { @@ -103,7 +103,7 @@ export default ({ getService }: FtrProviderContext) => { it(`estimates the bucket span ${testData.testTitleSuffix}`, async () => { const { body } = await supertest .post('/api/ml/validate/estimate_bucket_span') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); @@ -133,7 +133,7 @@ export default ({ getService }: FtrProviderContext) => { it(`estimates the bucket span`, async () => { const { body } = await supertest .post('/api/ml/validate/estimate_bucket_span') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); @@ -162,7 +162,7 @@ export default ({ getService }: FtrProviderContext) => { it(`estimates the bucket span`, async () => { const { body } = await supertest .post('/api/ml/validate/estimate_bucket_span') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); diff --git a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts b/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts index 7fb0a10d94a4b..c36621a9a6403 100644 --- a/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts +++ b/x-pack/test/api_integration/apis/ml/calculate_model_memory_limit.ts @@ -15,7 +15,7 @@ const COMMON_HEADERS = { export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); const testDataList = [ { @@ -158,7 +158,7 @@ export default ({ getService }: FtrProviderContext) => { it(`calculates the model memory limit ${testData.testTitleSuffix}`, async () => { await supertest .post('/api/ml/validate/calculate_model_memory_limit') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); diff --git a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts index aab7a65a7c122..b8ee2e7f6562c 100644 --- a/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts +++ b/x-pack/test/api_integration/apis/ml/categorization_field_examples.ts @@ -79,7 +79,7 @@ const defaultRequestBody = { export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); const testDataList = [ { @@ -300,7 +300,7 @@ export default ({ getService }: FtrProviderContext) => { it(testData.title, async () => { const { body } = await supertest .post('/api/ml/jobs/categorization_field_examples') - .auth(testData.user, mlSecurity.getPasswordForUser(testData.user)) + .auth(testData.user, ml.securityCommon.getPasswordForUser(testData.user)) .set(COMMON_HEADERS) .send(testData.requestBody) .expect(testData.expected.responseCode); diff --git a/x-pack/test/api_integration/apis/ml/get_module.ts b/x-pack/test/api_integration/apis/ml/get_module.ts index 4478236c494a8..6dcd9594fc9aa 100644 --- a/x-pack/test/api_integration/apis/ml/get_module.ts +++ b/x-pack/test/api_integration/apis/ml/get_module.ts @@ -37,12 +37,12 @@ const moduleIds = [ // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { const supertest = getService('supertestWithoutAuth'); - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); async function executeGetModuleRequest(module: string, user: USER, rspCode: number) { const { body } = await supertest .get(`/api/ml/modules/get_module/${module}`) - .auth(user, mlSecurity.getPasswordForUser(user)) + .auth(user, ml.securityCommon.getPasswordForUser(user)) .set(COMMON_HEADERS) .expect(rspCode); diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 78f99d8d9776a..4e21faa610bfe 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -7,24 +7,26 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ getService, loadTestFile }: FtrProviderContext) { - const mlSecurity = getService('mlSecurity'); + const ml = getService('ml'); describe('Machine Learning', function() { this.tags(['mlqa']); before(async () => { - await mlSecurity.createMlRoles(); - await mlSecurity.createMlUsers(); + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); }); after(async () => { - await mlSecurity.cleanMlUsers(); - await mlSecurity.cleanMlRoles(); + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); }); loadTestFile(require.resolve('./bucket_span_estimator')); loadTestFile(require.resolve('./calculate_model_memory_limit')); loadTestFile(require.resolve('./categorization_field_examples')); loadTestFile(require.resolve('./get_module')); + loadTestFile(require.resolve('./recognize_module')); + loadTestFile(require.resolve('./setup_module')); }); } diff --git a/x-pack/test/api_integration/apis/ml/recognize_module.ts b/x-pack/test/api_integration/apis/ml/recognize_module.ts new file mode 100644 index 0000000000000..2110bded7394c --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/recognize_module.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { USER } from '../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataList = [ + { + testTitleSuffix: 'for sample logs dataset', + sourceDataArchive: 'ml/sample_logs', + indexPattern: 'kibana_sample_data_logs', + user: USER.ML_POWERUSER, + expected: { + responseCode: 200, + moduleIds: ['sample_data_weblogs'], + }, + }, + { + testTitleSuffix: 'for non existent index pattern', + sourceDataArchive: 'empty_kibana', + indexPattern: 'non-existent-index-pattern', + user: USER.ML_POWERUSER, + expected: { + responseCode: 200, + moduleIds: [], + }, + }, + ]; + + async function executeRecognizeModuleRequest(indexPattern: string, user: USER, rspCode: number) { + const { body } = await supertest + .get(`/api/ml/modules/recognize/${indexPattern}`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .expect(rspCode); + + return body; + } + + describe('module recognizer', function() { + for (const testData of testDataList) { + describe('lists matching modules', function() { + before(async () => { + await esArchiver.load(testData.sourceDataArchive); + }); + + after(async () => { + await esArchiver.unload(testData.sourceDataArchive); + }); + + it(testData.testTitleSuffix, async () => { + const rspBody = await executeRecognizeModuleRequest( + testData.indexPattern, + testData.user, + testData.expected.responseCode + ); + expect(rspBody).to.be.an(Array); + + const responseModuleIds = rspBody.map((module: { id: string }) => module.id); + expect(responseModuleIds).to.eql(testData.expected.moduleIds); + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/setup_module.ts b/x-pack/test/api_integration/apis/ml/setup_module.ts new file mode 100644 index 0000000000000..71f3910cd4e93 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/setup_module.ts @@ -0,0 +1,229 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { JOB_STATE, DATAFEED_STATE } from '../../../../plugins/ml/common/constants/states'; +import { USER } from '../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testDataListPositive = [ + { + testTitleSuffix: 'for sample logs dataset with prefix and startDatafeed false', + sourceDataArchive: 'ml/sample_logs', + module: 'sample_data_weblogs', + user: USER.ML_POWERUSER, + requestBody: { + prefix: 'pf1_', + indexPatternName: 'kibana_sample_data_logs', + startDatafeed: false, + }, + expected: { + responseCode: 200, + jobs: [ + { + jobId: 'pf1_low_request_rate', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + }, + { + jobId: 'pf1_response_code_rates', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + }, + { + jobId: 'pf1_url_scanning', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + }, + ], + }, + }, + ]; + + const testDataListNegative = [ + { + testTitleSuffix: 'for non existent index pattern', + sourceDataArchive: 'empty_kibana', + module: 'sample_data_weblogs', + user: USER.ML_POWERUSER, + requestBody: { + indexPatternName: 'non-existent-index-pattern', + startDatafeed: false, + }, + expected: { + responseCode: 400, + error: 'Bad Request', + message: + "Module's jobs contain custom URLs which require a kibana index pattern (non-existent-index-pattern) which cannot be found.", + }, + }, + { + testTitleSuffix: 'for unauthorized user', + sourceDataArchive: 'ml/sample_logs', + module: 'sample_data_weblogs', + user: USER.ML_UNAUTHORIZED, + requestBody: { + prefix: 'pf1_', + indexPatternName: 'kibana_sample_data_logs', + startDatafeed: false, + }, + expected: { + responseCode: 403, + error: 'Forbidden', + message: + '[security_exception] action [cluster:monitor/xpack/ml/info/get] is unauthorized for user [ml_unauthorized]', + }, + }, + ]; + + async function executeSetupModuleRequest( + module: string, + user: USER, + rqBody: object, + rspCode: number + ) { + const { body } = await supertest + .post(`/api/ml/modules/setup/${module}`) + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_HEADERS) + .send(rqBody) + .expect(rspCode); + + return body; + } + + function compareById(a: { id: string }, b: { id: string }) { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + } + + describe('module setup', function() { + for (const testData of testDataListPositive) { + describe('sets up module data', function() { + before(async () => { + await esArchiver.load(testData.sourceDataArchive); + }); + + after(async () => { + await esArchiver.unload(testData.sourceDataArchive); + await ml.api.cleanMlIndices(); + }); + + it(testData.testTitleSuffix, async () => { + const rspBody = await executeSetupModuleRequest( + testData.module, + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + // verify response + if (testData.expected.jobs.length > 0) { + // jobs + expect(rspBody).to.have.property('jobs'); + + const expectedRspJobs = testData.expected.jobs + .map(job => { + return { id: job.jobId, success: true }; + }) + .sort(compareById); + + const actualRspJobs = rspBody.jobs.sort(compareById); + + expect(actualRspJobs).to.eql( + expectedRspJobs, + `Expected setup module response jobs to be '${JSON.stringify( + expectedRspJobs + )}' (got '${JSON.stringify(actualRspJobs)}')` + ); + + // datafeeds + expect(rspBody).to.have.property('datafeeds'); + + const expectedRspDatafeeds = testData.expected.jobs + .map(job => { + return { + id: `datafeed-${job.jobId}`, + success: true, + started: testData.requestBody.startDatafeed, + }; + }) + .sort(compareById); + + const actualRspDatafeeds = rspBody.datafeeds.sort(compareById); + + expect(actualRspDatafeeds).to.eql( + expectedRspDatafeeds, + `Expected setup module response datafeeds to be '${JSON.stringify( + expectedRspDatafeeds + )}' (got '${JSON.stringify(actualRspDatafeeds)}')` + ); + + // TODO in future updates: add response validations for created saved objects + } + + // verify job and datafeed creation + states + for (const job of testData.expected.jobs) { + const datafeedId = `datafeed-${job.jobId}`; + await ml.api.waitForAnomalyDetectionJobToExist(job.jobId); + await ml.api.waitForDatafeedToExist(datafeedId); + await ml.api.waitForJobState(job.jobId, job.jobState); + await ml.api.waitForDatafeedState(datafeedId, job.datafeedState); + } + }); + + // TODO in future updates: add creation validations for created saved objects + }); + } + + for (const testData of testDataListNegative) { + describe('rejects request', function() { + before(async () => { + await esArchiver.load(testData.sourceDataArchive); + }); + + after(async () => { + await esArchiver.unload(testData.sourceDataArchive); + await ml.api.cleanMlIndices(); + }); + + it(testData.testTitleSuffix, async () => { + const rspBody = await executeSetupModuleRequest( + testData.module, + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(rspBody) + .to.have.property('error') + .eql(testData.expected.error); + + expect(rspBody) + .to.have.property('message') + .eql(testData.expected.message); + }); + }); + } + }); +}; diff --git a/x-pack/test/api_integration/services/index.ts b/x-pack/test/api_integration/services/index.ts index c29116e1270c5..9c945f557a2d8 100644 --- a/x-pack/test/api_integration/services/index.ts +++ b/x-pack/test/api_integration/services/index.ts @@ -21,7 +21,7 @@ import { } from './infraops_graphql_client'; import { SiemGraphQLClientProvider, SiemGraphQLClientFactoryProvider } from './siem_graphql_client'; import { InfraOpsSourceConfigurationProvider } from './infraops_source_configuration'; -import { MachineLearningSecurityCommonProvider } from '../../functional/services/machine_learning'; +import { MachineLearningProvider } from './ml'; export const services = { ...commonServices, @@ -38,5 +38,5 @@ export const services = { siemGraphQLClientFactory: SiemGraphQLClientFactoryProvider, supertestWithoutAuth: SupertestWithoutAuthProvider, usageAPI: UsageAPIProvider, - mlSecurity: MachineLearningSecurityCommonProvider, + ml: MachineLearningProvider, }; diff --git a/x-pack/test/api_integration/services/ml.ts b/x-pack/test/api_integration/services/ml.ts new file mode 100644 index 0000000000000..841b200b87080 --- /dev/null +++ b/x-pack/test/api_integration/services/ml.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../functional/ftr_provider_context'; + +import { + MachineLearningAPIProvider, + MachineLearningSecurityCommonProvider, +} from '../../functional/services/machine_learning'; + +export function MachineLearningProvider(context: FtrProviderContext) { + const api = MachineLearningAPIProvider(context); + const securityCommon = MachineLearningSecurityCommonProvider(context); + + return { + api, + securityCommon, + }; +} diff --git a/x-pack/test/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/legacy/plugins/maps/public/selectors/ui_selectors.d.ts b/x-pack/test/encrypted_saved_objects_api_integration/services.ts similarity index 66% rename from x-pack/legacy/plugins/maps/public/selectors/ui_selectors.d.ts rename to x-pack/test/encrypted_saved_objects_api_integration/services.ts index 812e2082241bd..b7398349cce5d 100644 --- a/x-pack/legacy/plugins/maps/public/selectors/ui_selectors.d.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/services.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export function getOpenTOCDetails(state: unknown): string[]; - -export function getIsLayerTOCOpen(state: unknown): boolean; +export { services } from '../api_integration/services'; diff --git a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts similarity index 99% rename from x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts rename to x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts index ab9f7d2cdd339..7fe3d28911211 100644 --- a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/encrypted_saved_objects_api.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/encrypted_saved_objects_api.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { SavedObject } from 'src/core/server'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; export default function({ getService }: FtrProviderContext) { const es = getService('legacyEs'); diff --git a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts similarity index 88% rename from x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts rename to x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts index 424160e84495e..8c816a3404ddb 100644 --- a/x-pack/test/plugin_api_integration/test_suites/encrypted_saved_objects/index.ts +++ b/x-pack/test/encrypted_saved_objects_api_integration/tests/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { describe('encryptedSavedObjects', function encryptedSavedObjectsSuite() { diff --git a/x-pack/test/epm_api_integration/apis/file.ts b/x-pack/test/epm_api_integration/apis/file.ts index 2989263af40a7..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/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts index 252d0a0a78782..4b105263f3ba5 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_spaces.ts @@ -14,7 +14,8 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const config = getService('config'); - describe('spaces feature controls', () => { + // FLAKY: https://github.com/elastic/kibana/issues/57413 + describe.skip('spaces feature controls', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); }); diff --git a/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz b/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz new file mode 100644 index 0000000000000..03ceb319a6afe Binary files /dev/null and b/x-pack/test/functional/es_archives/ml/sample_logs/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json b/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json new file mode 100644 index 0000000000000..1c7490e139be5 --- /dev/null +++ b/x-pack/test/functional/es_archives/ml/sample_logs/mappings.json @@ -0,0 +1,3162 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "kibana_sample_data_logs", + "mappings": { + "properties": { + "@timestamp": { + "path": "timestamp", + "type": "alias" + }, + "agent": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "bytes": { + "type": "long" + }, + "clientip": { + "type": "ip" + }, + "event": { + "properties": { + "dataset": { + "type": "keyword" + } + } + }, + "extension": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "geo": { + "properties": { + "coordinates": { + "type": "geo_point" + }, + "dest": { + "type": "keyword" + }, + "src": { + "type": "keyword" + }, + "srcdest": { + "type": "keyword" + } + } + }, + "host": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ip": { + "type": "ip" + }, + "machine": { + "properties": { + "os": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "ram": { + "type": "long" + } + } + }, + "memory": { + "type": "double" + }, + "message": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "phpmemory": { + "type": "long" + }, + "referer": { + "type": "keyword" + }, + "request": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "response": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "timestamp": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "utc_time": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "agent_configs": "38abaf89513877745c359e7700c0c66a", + "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "agents": "75c0f4a11560dbc38b65e5e1d98fc9da", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "e8619030e08b671291af04c4603b4944", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "08b8b110dbca273d37e8aef131ecab61", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "datasources": "d4bc0c252b2b5683ff21ea32d00acffc", + "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "epm-package": "75d12cd13c867fd713d7dfb27366bc20", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "9ecce5b58867403613d82fe496470b34", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "268da3a48066123fc5baf35abaa55014", + "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "outputs": "aee9782e0d500b867859650a36280165", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "server": "ec97f1c5da1a19609a60874e5af1100c", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "actions": { + "properties": { + "created_at": { + "type": "date" + }, + "data": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "name": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 256, + "type": "keyword" + }, + "name": { + "ignore_above": 256, + "type": "keyword" + }, + "version": { + "ignore_above": 256, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "defaultIndex": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "time": { + "type": "integer" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "canvas-workpad": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "graph-workspace": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "map": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "visualization": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "dynamic": "true", + "properties": { + "indexName": { + "type": "keyword" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/services/machine_learning/api.ts b/x-pack/test/functional/services/machine_learning/api.ts index 74dc5912df36f..afc2567f3cce9 100644 --- a/x-pack/test/functional/services/machine_learning/api.ts +++ b/x-pack/test/functional/services/machine_learning/api.ts @@ -277,6 +277,16 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { return await esSupertest.get(`/_ml/anomaly_detectors/${jobId}`).expect(200); }, + async waitForAnomalyDetectionJobToExist(jobId: string) { + await retry.waitForWithTimeout(`'${jobId}' to exist`, 5 * 1000, async () => { + if (await this.getAnomalyDetectionJob(jobId)) { + return true; + } else { + throw new Error(`expected anomaly detection job '${jobId}' to exist`); + } + }); + }, + async createAnomalyDetectionJob(jobConfig: Job) { const jobId = jobConfig.job_id; log.debug(`Creating anomaly detection job with id '${jobId}'...`); @@ -285,19 +295,23 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { .send(jobConfig) .expect(200); - await retry.waitForWithTimeout(`'${jobId}' to be created`, 5 * 1000, async () => { - if (await this.getAnomalyDetectionJob(jobId)) { - return true; - } else { - throw new Error(`expected anomaly detection job '${jobId}' to be created`); - } - }); + await this.waitForAnomalyDetectionJobToExist(jobId); }, async getDatafeed(datafeedId: string) { return await esSupertest.get(`/_ml/datafeeds/${datafeedId}`).expect(200); }, + async waitForDatafeedToExist(datafeedId: string) { + await retry.waitForWithTimeout(`'${datafeedId}' to exist`, 5 * 1000, async () => { + if (await this.getDatafeed(datafeedId)) { + return true; + } else { + throw new Error(`expected datafeed '${datafeedId}' to exist`); + } + }); + }, + async createDatafeed(datafeedConfig: Datafeed) { const datafeedId = datafeedConfig.datafeed_id; log.debug(`Creating datafeed with id '${datafeedId}'...`); @@ -306,13 +320,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { .send(datafeedConfig) .expect(200); - await retry.waitForWithTimeout(`'${datafeedId}' to be created`, 5 * 1000, async () => { - if (await this.getDatafeed(datafeedId)) { - return true; - } else { - throw new Error(`expected datafeed '${datafeedId}' to be created`); - } - }); + await this.waitForDatafeedToExist(datafeedId); }, async openAnomalyDetectionJob(jobId: string) { diff --git a/x-pack/test/functional/services/machine_learning/security_common.ts b/x-pack/test/functional/services/machine_learning/security_common.ts index d59c1edcb00ab..1145b6f93a4f8 100644 --- a/x-pack/test/functional/services/machine_learning/security_common.ts +++ b/x-pack/test/functional/services/machine_learning/security_common.ts @@ -12,6 +12,7 @@ export type MlSecurityCommon = ProvidedType { - nonce = request.payload.nonce; - return {}; - }, - }); - - server.route({ - path: '/api/oidc_provider/token_endpoint', - method: 'POST', - // Token endpoint needs authentication (with the client credentials) but we don't attempt to - // validate this OIDC behavior here - config: { - auth: false, - validate: { - payload: Joi.object({ - grant_type: Joi.string().optional(), - code: Joi.string().optional(), - redirect_uri: Joi.string().optional(), - }), - }, - }, - async handler(request) { - const userId = request.payload.code.substring(4); - const { accessToken, idToken } = createTokens(userId, nonce); - try { - const userId = request.payload.code.substring(4); - return { - access_token: accessToken, - token_type: 'Bearer', - refresh_token: `valid-refresh-token${userId}`, - expires_in: 3600, - id_token: idToken, - }; - } catch (err) { - return err; - } - }, - }); - - server.route({ - path: '/api/oidc_provider/userinfo_endpoint', - method: 'GET', - config: { - auth: false, - }, - handler: request => { - const accessToken = request.headers.authorization.substring(7); - if (accessToken === 'valid-access-token1') { - return { - sub: 'user1', - name: 'Tony Stark', - given_name: 'Tony', - family_name: 'Stark', - preferred_username: 'ironman', - email: 'ironman@avengers.com', - }; - } - if (accessToken === 'valid-access-token2') { - return { - sub: 'user2', - name: 'Peter Parker', - given_name: 'Peter', - family_name: 'Parker', - preferred_username: 'spiderman', - email: 'spiderman@avengers.com', - }; - } - if (accessToken === 'valid-access-token3') { - return { - sub: 'user3', - name: 'Bruce Banner', - given_name: 'Bruce', - family_name: 'Banner', - preferred_username: 'hulk', - email: 'hulk@avengers.com', - }; - } - return {}; - }, - }); -} diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json new file mode 100644 index 0000000000000..faaa0b9165828 --- /dev/null +++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "oidc_provider_plugin", + "version": "8.0.0", + "kibanaVersion": "kibana", + "server": true, + "ui": false +} diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json deleted file mode 100644 index 358c6e2020afe..0000000000000 --- a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "oidc_provider_plugin", - "version": "1.0.0", - "kibana": { - "version": "kibana", - "templateVersion": "1.0.0" - }, - "license": "Apache-2.0", - "dependencies": { - "joi": "^13.5.2", - "jsonwebtoken": "^8.3.0" - } -} diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts similarity index 55% rename from x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js rename to x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts index 17d45527397b8..456abecd201be 100644 --- a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/index.js +++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/index.ts @@ -4,16 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PluginInitializer } from '../../../../../../src/core/server'; import { initRoutes } from './init_routes'; -export default function(kibana) { - return new kibana.Plugin({ - name: 'oidcProvider', - id: 'oidcProvider', - require: ['elasticsearch'], - - init(server) { - initRoutes(server); - }, - }); -} +export const plugin: PluginInitializer = () => ({ + setup: core => initRoutes(core.http.createRouter()), + start: () => {}, + stop: () => {}, +}); diff --git a/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.ts b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.ts new file mode 100644 index 0000000000000..6d3248f4377b1 --- /dev/null +++ b/x-pack/test/oidc_api_integration/fixtures/oidc_provider/server/init_routes.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IRouter } from '../../../../../../src/core/server'; +import { createTokens } from '../../oidc_tools'; + +export function initRoutes(router: IRouter) { + let nonce = ''; + + router.post( + { + path: '/api/oidc_provider/setup', + validate: { body: value => ({ value }) }, + options: { authRequired: false }, + }, + (context, request, response) => { + nonce = request.body.nonce; + return response.ok({ body: {} }); + } + ); + + router.post( + { + path: '/api/oidc_provider/token_endpoint', + validate: { body: value => ({ value }) }, + // Token endpoint needs authentication (with the client credentials) but we don't attempt to + // validate this OIDC behavior here + options: { authRequired: false, xsrfRequired: false }, + }, + (context, request, response) => { + const userId = request.body.code.substring(4); + const { accessToken, idToken } = createTokens(userId, nonce); + return response.ok({ + body: { + access_token: accessToken, + token_type: 'Bearer', + refresh_token: `valid-refresh-token${userId}`, + expires_in: 3600, + id_token: idToken, + }, + }); + } + ); + + router.get( + { + path: '/api/oidc_provider/userinfo_endpoint', + validate: false, + options: { authRequired: false }, + }, + (context, request, response) => { + const accessToken = (request.headers.authorization as string).substring(7); + if (accessToken === 'valid-access-token1') { + return response.ok({ + body: { + sub: 'user1', + name: 'Tony Stark', + given_name: 'Tony', + family_name: 'Stark', + preferred_username: 'ironman', + email: 'ironman@avengers.com', + }, + }); + } + + if (accessToken === 'valid-access-token2') { + return response.ok({ + body: { + sub: 'user2', + name: 'Peter Parker', + given_name: 'Peter', + family_name: 'Parker', + preferred_username: 'spiderman', + email: 'spiderman@avengers.com', + }, + }); + } + + if (accessToken === 'valid-access-token3') { + return response.ok({ + body: { + sub: 'user3', + name: 'Bruce Banner', + given_name: 'Bruce', + family_name: 'Banner', + preferred_username: 'hulk', + email: 'hulk@avengers.com', + }, + }); + } + + return response.ok({ body: {} }); + } + ); +} diff --git a/x-pack/test/plugin_api_integration/config.js b/x-pack/test/plugin_api_integration/config.js index 830933278f2bc..83e8b1f84a9e0 100644 --- a/x-pack/test/plugin_api_integration/config.js +++ b/x-pack/test/plugin_api_integration/config.js @@ -18,10 +18,7 @@ export default async function({ readConfigFile }) { ); return { - testFiles: [ - require.resolve('./test_suites/task_manager'), - require.resolve('./test_suites/encrypted_saved_objects'), - ], + testFiles: [require.resolve('./test_suites/task_manager')], services, servers: integrationConfig.get('servers'), esTestCluster: integrationConfig.get('esTestCluster'), diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts deleted file mode 100644 index e61b8f24a1f69..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Request } from 'hapi'; -import { boomify, badRequest } from 'boom'; -import { Legacy } from 'kibana'; -import { - EncryptedSavedObjectsPluginSetup, - EncryptedSavedObjectsPluginStart, -} from '../../../../plugins/encrypted_saved_objects/server'; - -const SAVED_OBJECT_WITH_SECRET_TYPE = 'saved-object-with-secret'; - -// eslint-disable-next-line import/no-default-export -export default function esoPlugin(kibana: any) { - return new kibana.Plugin({ - id: 'eso', - require: ['encryptedSavedObjects'], - uiExports: { mappings: require('./mappings.json') }, - init(server: Legacy.Server) { - server.route({ - method: 'GET', - path: '/api/saved_objects/get-decrypted-as-internal-user/{id}', - async handler(request: Request) { - const encryptedSavedObjectsStart = server.newPlatform.start.plugins - .encryptedSavedObjects as EncryptedSavedObjectsPluginStart; - const namespace = server.plugins.spaces && server.plugins.spaces.getSpaceId(request); - try { - return await encryptedSavedObjectsStart.getDecryptedAsInternalUser( - SAVED_OBJECT_WITH_SECRET_TYPE, - request.params.id, - { namespace: namespace === 'default' ? undefined : namespace } - ); - } catch (err) { - if (encryptedSavedObjectsStart.isEncryptionError(err)) { - return badRequest('Failed to encrypt attributes'); - } - - return boomify(err); - } - }, - }); - - (server.newPlatform.setup.plugins - .encryptedSavedObjects as EncryptedSavedObjectsPluginSetup).registerType({ - type: SAVED_OBJECT_WITH_SECRET_TYPE, - attributesToEncrypt: new Set(['privateProperty']), - attributesToExcludeFromAAD: new Set(['publicPropertyExcludedFromAAD']), - }); - }, - }); -} diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json deleted file mode 100644 index b727850793bbe..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/mappings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "saved-object-with-secret": { - "properties": { - "publicProperty": { - "type": "keyword" - }, - "publicPropertyExcludedFromAAD": { - "type": "keyword" - }, - "privateProperty": { - "type": "binary" - } - } - } -} diff --git a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json b/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json deleted file mode 100644 index 723904757ae8a..0000000000000 --- a/x-pack/test/plugin_api_integration/plugins/encrypted_saved_objects/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "eso", - "version": "kibana" -} \ No newline at end of file diff --git a/x-pack/test/saml_api_integration/config.ts b/x-pack/test/saml_api_integration/config.ts index 0580c28555d16..a92f11363b0fc 100644 --- a/x-pack/test/saml_api_integration/config.ts +++ b/x-pack/test/saml_api_integration/config.ts @@ -50,7 +50,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), '--optimize.enabled=false', - '--server.xsrf.whitelist=["/api/security/saml/callback"]', `--xpack.security.authc.providers=${JSON.stringify(['saml', 'basic'])}`, '--xpack.security.authc.saml.realm=saml1', '--xpack.security.authc.saml.maxRedirectURLSize=100b', diff --git a/yarn.lock b/yarn.lock index 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==