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(
-
+
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"
>
+
+
+
+
+ Cancel
+
+
+
+
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==