diff --git a/docs/developer/advanced/running-elasticsearch.asciidoc b/docs/developer/advanced/running-elasticsearch.asciidoc index 36f9ee420d41dbf..c04c53f66cf191b 100644 --- a/docs/developer/advanced/running-elasticsearch.asciidoc +++ b/docs/developer/advanced/running-elasticsearch.asciidoc @@ -71,13 +71,6 @@ elasticsearch.password: {{ password }} elasticsearch.ssl.verificationMode: none ---- -If many other users will be interacting with your remote cluster, you'll want to add the following to avoid causing conflicts: - -[source,bash] ----- -kibana.index: '.{YourGitHubHandle}-kibana' ----- - ==== Running remote clusters Setup remote clusters for cross cluster search (CCS) and cross cluster replication (CCR). diff --git a/package.json b/package.json index 6f38381dbb685c0..0bd1b5c9e5b5526 100644 --- a/package.json +++ b/package.json @@ -411,7 +411,6 @@ "venn.js": "0.2.20", "vinyl": "^2.2.0", "vt-pbf": "^3.1.1", - "wellknown": "^0.5.0", "whatwg-fetch": "^3.0.0", "xml2js": "^0.4.22", "yauzl": "^2.10.0" diff --git a/packages/kbn-dev-utils/src/serializers/recursive_serializer.ts b/packages/kbn-dev-utils/src/serializers/recursive_serializer.ts index 6e6572addbc833f..15d3f033a85a120 100644 --- a/packages/kbn-dev-utils/src/serializers/recursive_serializer.ts +++ b/packages/kbn-dev-utils/src/serializers/recursive_serializer.ts @@ -6,11 +6,26 @@ * Side Public License, v 1. */ -export function createRecursiveSerializer(test: (v: any) => boolean, print: (v: any) => string) { +class RawPrint { + static fromString(s: string) { + return new RawPrint(s); + } + constructor(public readonly v: string) {} +} + +export function createRecursiveSerializer( + test: (v: any) => boolean, + print: (v: any, printRaw: (v: string) => RawPrint) => string | RawPrint +) { return { test: (v: any) => test(v), serialize: (v: any, ...rest: any[]) => { - const replacement = print(v); + const replacement = print(v, RawPrint.fromString); + + if (replacement instanceof RawPrint) { + return replacement.v; + } + const printer = rest.pop()!; return printer(replacement, ...rest); }, diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index e54b4d5fbdb52c1..fbb5784afe5ac91 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -18,7 +18,7 @@ import readline from 'readline'; import Fs from 'fs'; import { RunWithCommands, createFlagError, CA_CERT_PATH } from '@kbn/dev-utils'; -import { readConfigFile, KbnClient } from '@kbn/test'; +import { readConfigFile, KbnClient, EsVersion } from '@kbn/test'; import { Client, HttpConnection } from '@elastic/elasticsearch'; import { EsArchiver } from './es_archiver'; @@ -45,7 +45,7 @@ export function runCli() { if (typeof configPath !== 'string') { throw createFlagError('--config must be a string'); } - const config = await readConfigFile(log, Path.resolve(configPath)); + const config = await readConfigFile(log, EsVersion.getDefault(), Path.resolve(configPath)); statsMeta.set('ftrConfigPath', configPath); let esUrl = flags['es-url']; diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index bf51f1a97e8dd2a..3896c67454b9028 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -59012,7 +59012,7 @@ async function setupRemoteCache(repoRootPath) { try { const { stdout - } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_3__["spawn"])('vault', ['read', '-field=readonly-key', 'secret/kibana-issues/dev/bazel-remote-cache'], { + } = await Object(_child_process__WEBPACK_IMPORTED_MODULE_3__["spawn"])('vault', ['read', '-field=readonly-key', 'secret/ui-team/kibana-bazel-remote-cache'], { stdio: 'pipe' }); apiKey = stdout.trim(); diff --git a/packages/kbn-pm/src/utils/bazel/setup_remote_cache.ts b/packages/kbn-pm/src/utils/bazel/setup_remote_cache.ts index fb510cfa81ffd03..0e740e674b7d81e 100644 --- a/packages/kbn-pm/src/utils/bazel/setup_remote_cache.ts +++ b/packages/kbn-pm/src/utils/bazel/setup_remote_cache.ts @@ -60,7 +60,7 @@ export async function setupRemoteCache(repoRootPath: string) { try { const { stdout } = await spawn( 'vault', - ['read', '-field=readonly-key', 'secret/kibana-issues/dev/bazel-remote-cache'], + ['read', '-field=readonly-key', 'secret/ui-team/kibana-bazel-remote-cache'], { stdio: 'pipe', } diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index 1ff9677615f5ab8..69addd9e3c4c7c8 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -69,6 +69,7 @@ RUNTIME_DEPS = [ "@npm//react-router-dom", "@npm//redux", "@npm//rxjs", + "@npm//semver", "@npm//strip-ansi", "@npm//xmlbuilder", "@npm//xml2js", @@ -108,6 +109,7 @@ TYPES_DEPS = [ "@npm//@types/react-dom", "@npm//@types/react-redux", "@npm//@types/react-router-dom", + "@npm//@types/semver", "@npm//@types/xml2js", ] diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index d9938bebea5bb0c..e013085e1b39a3c 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -35,6 +35,11 @@ export function runFtrCli() { const reportTime = getTimeReporter(toolingLog, 'scripts/functional_test_runner'); run( async ({ flags, log }) => { + const esVersion = flags['es-version'] || undefined; // convert "" to undefined + if (esVersion !== undefined && typeof esVersion !== 'string') { + throw createFlagError('expected --es-version to be a string'); + } + const functionalTestRunner = new FunctionalTestRunner( log, makeAbsolutePath(flags.config as string), @@ -57,7 +62,8 @@ export function runFtrCli() { }, updateBaselines: flags.updateBaselines || flags.u, updateSnapshots: flags.updateSnapshots || flags.u, - } + }, + esVersion ); if (flags.throttle) { @@ -131,6 +137,7 @@ export function runFtrCli() { 'include-tag', 'exclude-tag', 'kibana-install-dir', + 'es-version', ], boolean: [ 'bail', @@ -150,6 +157,7 @@ export function runFtrCli() { --bail stop tests after the first failure --grep pattern used to select which tests to run --invert invert grep to exclude tests + --es-version the elasticsearch version, formatted as "x.y.z" --include=file a test file to be included, pass multiple times for multiple files --exclude=file a test file to be excluded, pass multiple times for multiple files --include-tag=tag a tag to be included, pass multiple times for multiple tags. Only diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index 4130cd8d138b878..ea55a2672d670f6 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { Client as EsClient } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/dev-utils'; import { Suite, Test } from './fake_mocha_types'; @@ -21,6 +22,7 @@ import { DockerServersService, Config, SuiteTracker, + EsVersion, } from './lib'; export class FunctionalTestRunner { @@ -28,10 +30,12 @@ export class FunctionalTestRunner { public readonly failureMetadata = new FailureMetadata(this.lifecycle); private closed = false; + private readonly esVersion: EsVersion; constructor( private readonly log: ToolingLog, private readonly configFile: string, - private readonly configOverrides: any + private readonly configOverrides: any, + esVersion?: string | EsVersion ) { for (const [key, value] of Object.entries(this.lifecycle)) { if (value instanceof LifecyclePhase) { @@ -39,6 +43,12 @@ export class FunctionalTestRunner { value.after$.subscribe(() => log.verbose('starting %j lifecycle phase', key)); } } + this.esVersion = + esVersion === undefined + ? EsVersion.getDefault() + : esVersion instanceof EsVersion + ? esVersion + : new EsVersion(esVersion); } async run() { @@ -51,6 +61,27 @@ export class FunctionalTestRunner { ...readProviderSpec('PageObject', config.get('pageObjects')), ]); + // validate es version + if (providers.hasService('es')) { + const es = (await providers.getService('es')) as unknown as EsClient; + let esInfo; + try { + esInfo = await es.info(); + } catch (error) { + throw new Error( + `attempted to use the "es" service to fetch Elasticsearch version info but the request failed: ${error.stack}` + ); + } + + if (!this.esVersion.eql(esInfo.version.number)) { + throw new Error( + `ES reports a version number "${ + esInfo.version.number + }" which doesn't match supplied es version "${this.esVersion.toString()}"` + ); + } + } + await providers.loadAll(); const customTestRunner = config.get('testRunner'); @@ -61,7 +92,7 @@ export class FunctionalTestRunner { return (await providers.invokeProviderFn(customTestRunner)) || 0; } - const mocha = await setupMocha(this.lifecycle, this.log, config, providers); + const mocha = await setupMocha(this.lifecycle, this.log, config, providers, this.esVersion); await this.lifecycle.beforeTests.trigger(mocha.suite); this.log.info('Starting tests'); @@ -107,14 +138,14 @@ export class FunctionalTestRunner { ...readStubbedProviderSpec('PageObject', config.get('pageObjects'), []), ]); - const mocha = await setupMocha(this.lifecycle, this.log, config, providers); + const mocha = await setupMocha(this.lifecycle, this.log, config, providers, this.esVersion); const countTests = (suite: Suite): number => suite.suites.reduce((sum, s) => sum + countTests(s), suite.tests.length); return { testCount: countTests(mocha.suite), - excludedTests: mocha.excludedTests.map((t: Test) => t.fullTitle()), + testsExcludedByTag: mocha.testsExcludedByTag.map((t: Test) => t.fullTitle()), }; }); } @@ -125,7 +156,12 @@ export class FunctionalTestRunner { let runErrorOccurred = false; try { - const config = await readConfigFile(this.log, this.configFile, this.configOverrides); + const config = await readConfigFile( + this.log, + this.esVersion, + this.configFile, + this.configOverrides + ); this.log.info('Config loaded'); if ( @@ -148,6 +184,7 @@ export class FunctionalTestRunner { failureMetadata: () => this.failureMetadata, config: () => config, dockerServers: () => dockerServers, + esVersion: () => this.esVersion, }); return await handler(config, coreProviders); diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts index 268c6b2bd9a670f..1718b5f7a4bc57b 100644 --- a/packages/kbn-test/src/functional_test_runner/index.ts +++ b/packages/kbn-test/src/functional_test_runner/index.ts @@ -7,7 +7,7 @@ */ export { FunctionalTestRunner } from './functional_test_runner'; -export { readConfigFile, Config } from './lib'; +export { readConfigFile, Config, EsVersion } from './lib'; export { runFtrCli } from './cli'; export * from './lib/docker_servers'; export * from './public_types'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js index 60c307b58aee692..27434ce5a09ca0b 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.test.js @@ -9,34 +9,41 @@ import { ToolingLog } from '@kbn/dev-utils'; import { readConfigFile } from './read_config_file'; import { Config } from './config'; +import { EsVersion } from '../es_version'; const log = new ToolingLog(); +const esVersion = new EsVersion('8.0.0'); describe('readConfigFile()', () => { it('reads config from a file, returns an instance of Config class', async () => { - const config = await readConfigFile(log, require.resolve('./__fixtures__/config.1')); + const config = await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.1')); expect(config instanceof Config).toBeTruthy(); expect(config.get('testFiles')).toEqual(['config.1']); }); it('merges setting overrides into log', async () => { - const config = await readConfigFile(log, require.resolve('./__fixtures__/config.1'), { - screenshots: { - directory: 'foo.bar', - }, - }); + const config = await readConfigFile( + log, + esVersion, + require.resolve('./__fixtures__/config.1'), + { + screenshots: { + directory: 'foo.bar', + }, + } + ); expect(config.get('screenshots.directory')).toBe('foo.bar'); }); it('supports loading config files from within config files', async () => { - const config = await readConfigFile(log, require.resolve('./__fixtures__/config.2')); + const config = await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.2')); expect(config.get('testFiles')).toEqual(['config.1', 'config.2']); }); it('throws if settings are invalid', async () => { try { - await readConfigFile(log, require.resolve('./__fixtures__/config.invalid')); + await readConfigFile(log, esVersion, require.resolve('./__fixtures__/config.invalid')); throw new Error('expected readConfigFile() to fail'); } catch (err) { expect(err.message).toMatch(/"foo"/); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts index 374edea7a8db776..fd836f338edf02f 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/read_config_file.ts @@ -10,10 +10,16 @@ import { ToolingLog } from '@kbn/dev-utils'; import { defaultsDeep } from 'lodash'; import { Config } from './config'; +import { EsVersion } from '../es_version'; const cache = new WeakMap(); -async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrides: any) { +async function getSettingsFromFile( + log: ToolingLog, + esVersion: EsVersion, + path: string, + settingOverrides: any +) { const configModule = require(path); // eslint-disable-line @typescript-eslint/no-var-requires const configProvider = configModule.__esModule ? configModule.default : configModule; @@ -23,9 +29,10 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid configProvider, configProvider({ log, + esVersion, async readConfigFile(p: string, o: any) { return new Config({ - settings: await getSettingsFromFile(log, p, o), + settings: await getSettingsFromFile(log, esVersion, p, o), primary: false, path: p, }); @@ -43,9 +50,14 @@ async function getSettingsFromFile(log: ToolingLog, path: string, settingOverrid return settingsWithDefaults; } -export async function readConfigFile(log: ToolingLog, path: string, settingOverrides: any = {}) { +export async function readConfigFile( + log: ToolingLog, + esVersion: EsVersion, + path: string, + settingOverrides: any = {} +) { return new Config({ - settings: await getSettingsFromFile(log, path, settingOverrides), + settings: await getSettingsFromFile(log, esVersion, path, settingOverrides), primary: true, path, }); diff --git a/packages/kbn-test/src/functional_test_runner/lib/es_version.ts b/packages/kbn-test/src/functional_test_runner/lib/es_version.ts new file mode 100644 index 000000000000000..8b3acde47a4dc8d --- /dev/null +++ b/packages/kbn-test/src/functional_test_runner/lib/es_version.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import semver from 'semver'; +import { kibanaPackageJson } from '@kbn/utils'; + +export class EsVersion { + static getDefault() { + // example: https://storage.googleapis.com/kibana-ci-es-snapshots-daily/8.0.0/manifest-latest-verified.json + const manifestUrl = process.env.ES_SNAPSHOT_MANIFEST; + if (manifestUrl) { + const match = manifestUrl.match(/\d+\.\d+\.\d+/); + if (!match) { + throw new Error('unable to extract es version from ES_SNAPSHOT_MANIFEST_URL'); + } + return new EsVersion(match[0]); + } + + return new EsVersion(process.env.TEST_ES_BRANCH || kibanaPackageJson.version); + } + + public readonly parsed: semver.SemVer; + + constructor(version: string) { + const parsed = semver.coerce(version); + if (!parsed) { + throw new Error(`unable to parse es version [${version}]`); + } + this.parsed = parsed; + } + + toString() { + return this.parsed.version; + } + + /** + * Determine if the ES version matches a semver range, like >=7 or ^8.1.0 + */ + matchRange(range: string) { + return semver.satisfies(this.parsed, range); + } + + /** + * Determine if the ES version matches a specific version, ignores things like -SNAPSHOT + */ + eql(version: string) { + const other = semver.coerce(version); + return other && semver.compareLoose(this.parsed, other) === 0; + } +} diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts index 1cb1e58a265d579..98b5fec0597e4e7 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/index.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts @@ -17,3 +17,4 @@ export * from './docker_servers'; export { SuiteTracker } from './suite_tracker'; export type { Provider } from './providers'; +export * from './es_version'; diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js index 7610ca91286949f..e12ffdc8cd616a1 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/decorate_mocha_ui.js @@ -84,6 +84,9 @@ export function decorateMochaUi(log, lifecycle, context, { isDockerGroup, rootTa this._tags = [...this._tags, ...tagsToAdd]; }; + this.onlyEsVersion = (semver) => { + this._esVersionRequirement = semver; + }; provider.call(this); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js similarity index 86% rename from packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js index 10030a1c05632b2..191503af123d017 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.test.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.test.js @@ -12,9 +12,10 @@ import Mocha from 'mocha'; import { create as createSuite } from 'mocha/lib/suite'; import Test from 'mocha/lib/test'; -import { filterSuitesByTags } from './filter_suites_by_tags'; +import { filterSuites } from './filter_suites'; +import { EsVersion } from '../es_version'; -function setup({ include, exclude }) { +function setup({ include, exclude, esVersion }) { return new Promise((resolve) => { const history = []; @@ -55,6 +56,7 @@ function setup({ include, exclude }) { const level1b = createSuite(level1, 'level 1b'); level1b._tags = ['level1b']; + level1b._esVersionRequirement = '<=8'; level1b.addTest(new Test('test 1b', () => {})); const level2 = createSuite(mocha.suite, 'level 2'); @@ -62,7 +64,7 @@ function setup({ include, exclude }) { level2a._tags = ['level2a']; level2a.addTest(new Test('test 2a', () => {})); - filterSuitesByTags({ + filterSuites({ log: { info(...args) { history.push(`info: ${format(...args)}`); @@ -71,6 +73,7 @@ function setup({ include, exclude }) { mocha, include, exclude, + esVersion, }); mocha.run(); @@ -208,3 +211,27 @@ it('does nothing if everything excluded', async () => { ] `); }); + +it(`excludes tests which don't meet the esVersionRequirement`, async () => { + const { history } = await setup({ + include: [], + exclude: [], + esVersion: new EsVersion('9.0.0'), + }); + + expect(history).toMatchInlineSnapshot(` + Array [ + "info: Only running suites which are compatible with ES version 9.0.0", + "suite: ", + "suite: level 1", + "suite: level 1 level 1a", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 1a\\"", + "hook: level 1 \\"before each\\" hook: level1BeforeEach for \\"test 1a\\"", + "test: level 1 level 1a test 1a", + "suite: level 2", + "suite: level 2 level 2a", + "hook: \\"before each\\" hook: rootBeforeEach for \\"test 2a\\"", + "test: level 2 level 2a test 2a", + ] + `); +}); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts similarity index 55% rename from packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js rename to packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts index 9724956e121f34c..90bb3a894bc6c36 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites_by_tags.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/filter_suites.ts @@ -6,6 +6,24 @@ * Side Public License, v 1. */ +import { ToolingLog } from '@kbn/dev-utils'; +import { Suite, Test } from '../../fake_mocha_types'; +import { EsVersion } from '../es_version'; + +interface SuiteInternal extends Suite { + _tags?: string[]; + _esVersionRequirement?: string; + suites: SuiteInternal[]; +} + +interface Options { + log: ToolingLog; + mocha: any; + include: string[]; + exclude: string[]; + esVersion?: EsVersion; +} + /** * Given a mocha instance that has already loaded all of its suites, filter out * the suites based on the include/exclude tags. If there are include tags then @@ -16,23 +34,50 @@ * @param options.include an array of tags that suites must be tagged with to be run * @param options.exclude an array of tags that will be used to exclude suites from the run */ -export function filterSuitesByTags({ log, mocha, include, exclude }) { - mocha.excludedTests = []; +export function filterSuites({ log, mocha, include, exclude, esVersion }: Options) { + mocha.testsExcludedByTag = []; + mocha.testsExcludedByEsVersion = []; + // collect all the tests from some suite, including it's children - const collectTests = (suite) => + const collectTests = (suite: SuiteInternal): Test[] => suite.suites.reduce((acc, s) => acc.concat(collectTests(s)), suite.tests); + if (esVersion) { + // traverse the test graph and exclude any tests which don't meet their esVersionRequirement + log.info('Only running suites which are compatible with ES version', esVersion.toString()); + (function recurse(parentSuite: SuiteInternal) { + const children = parentSuite.suites; + parentSuite.suites = []; + + const meetsEsVersionRequirement = (suite: SuiteInternal) => + !suite._esVersionRequirement || esVersion.matchRange(suite._esVersionRequirement); + + for (const child of children) { + if (meetsEsVersionRequirement(child)) { + parentSuite.suites.push(child); + recurse(child); + } else { + mocha.testsExcludedByEsVersion = mocha.testsExcludedByEsVersion.concat( + collectTests(child) + ); + } + } + })(mocha.suite); + } + // if include tags were provided, filter the tree once to // only include branches that are included at some point if (include.length) { log.info('Only running suites (and their sub-suites) if they include the tag(s):', include); - const isIncluded = (suite) => + const isIncludedByTags = (suite: SuiteInternal) => !suite._tags ? false : suite._tags.some((t) => include.includes(t)); - const isChildIncluded = (suite) => + + const isIncluded = (suite: SuiteInternal) => isIncludedByTags(suite); + const isChildIncluded = (suite: SuiteInternal): boolean => suite.suites.some((s) => isIncluded(s) || isChildIncluded(s)); - (function recurse(parentSuite) { + (function recurse(parentSuite: SuiteInternal) { const children = parentSuite.suites; parentSuite.suites = []; @@ -47,13 +92,13 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { // itself, so strip out its tests and recurse to filter // out child suites which are not included if (isChildIncluded(child)) { - mocha.excludedTests = mocha.excludedTests.concat(child.tests); + mocha.testsExcludedByTag = mocha.testsExcludedByTag.concat(child.tests); child.tests = []; parentSuite.suites.push(child); recurse(child); continue; } else { - mocha.excludedTests = mocha.excludedTests.concat(collectTests(child)); + mocha.testsExcludedByTag = mocha.testsExcludedByTag.concat(collectTests(child)); } } })(mocha.suite); @@ -64,9 +109,10 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { if (exclude.length) { log.info('Filtering out any suites that include the tag(s):', exclude); - const isNotExcluded = (suite) => !suite._tags || !suite._tags.some((t) => exclude.includes(t)); + const isNotExcluded = (suite: SuiteInternal) => + !suite._tags || !suite._tags.some((t) => exclude.includes(t)); - (function recurse(parentSuite) { + (function recurse(parentSuite: SuiteInternal) { const children = parentSuite.suites; parentSuite.suites = []; @@ -77,7 +123,7 @@ export function filterSuitesByTags({ log, mocha, include, exclude }) { parentSuite.suites.push(child); recurse(child); } else { - mocha.excludedTests = mocha.excludedTests.concat(collectTests(child)); + mocha.testsExcludedByTag = mocha.testsExcludedByTag.concat(collectTests(child)); } } })(mocha.suite); diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js index 65b7c09242fdd4c..8d88410cb2c1daf 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js +++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/setup_mocha.js @@ -11,7 +11,7 @@ import { relative } from 'path'; import { REPO_ROOT } from '@kbn/utils'; import { loadTestFiles } from './load_test_files'; -import { filterSuitesByTags } from './filter_suites_by_tags'; +import { filterSuites } from './filter_suites'; import { MochaReporterProvider } from './reporter'; import { validateCiGroupTags } from './validate_ci_group_tags'; @@ -22,9 +22,10 @@ import { validateCiGroupTags } from './validate_ci_group_tags'; * @param {ToolingLog} log * @param {Config} config * @param {ProviderCollection} providers + * @param {EsVersion} esVersion * @return {Promise} */ -export async function setupMocha(lifecycle, log, config, providers) { +export async function setupMocha(lifecycle, log, config, providers, esVersion) { // configure mocha const mocha = new Mocha({ ...config.get('mochaOpts'), @@ -50,18 +51,26 @@ export async function setupMocha(lifecycle, log, config, providers) { // valiate that there aren't any tests in multiple ciGroups validateCiGroupTags(log, mocha); + filterSuites({ + log, + mocha, + include: [], + exclude: [], + esVersion, + }); + // Each suite has a tag that is the path relative to the root of the repo // So we just need to take input paths, make them relative to the root, and use them as tags // Also, this is a separate filterSuitesByTags() call so that the test suites will be filtered first by // files, then by tags. This way, you can target tags (like smoke) in a specific file. - filterSuitesByTags({ + filterSuites({ log, mocha, include: config.get('suiteFiles.include').map((file) => relative(REPO_ROOT, file)), exclude: config.get('suiteFiles.exclude').map((file) => relative(REPO_ROOT, file)), }); - filterSuitesByTags({ + filterSuites({ log, mocha, include: config.get('suiteTags.include').map((tag) => tag.replace(/-\d+$/, '')), diff --git a/packages/kbn-test/src/functional_test_runner/public_types.ts b/packages/kbn-test/src/functional_test_runner/public_types.ts index d1a0f7998b0a988..6cb6d5adf4b1915 100644 --- a/packages/kbn-test/src/functional_test_runner/public_types.ts +++ b/packages/kbn-test/src/functional_test_runner/public_types.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { ToolingLog } from '@kbn/dev-utils'; +import type { ToolingLog } from '@kbn/dev-utils'; -import { Config, Lifecycle, FailureMetadata, DockerServersService } from './lib'; -import { Test, Suite } from './fake_mocha_types'; +import type { Config, Lifecycle, FailureMetadata, DockerServersService, EsVersion } from './lib'; +import type { Test, Suite } from './fake_mocha_types'; export { Lifecycle, Config, FailureMetadata }; @@ -57,7 +57,7 @@ export interface GenericFtrProviderContext< * @param serviceName */ hasService( - serviceName: 'config' | 'log' | 'lifecycle' | 'failureMetadata' | 'dockerServers' + serviceName: 'config' | 'log' | 'lifecycle' | 'failureMetadata' | 'dockerServers' | 'esVersion' ): true; hasService(serviceName: K): serviceName is K; hasService(serviceName: string): serviceName is Extract; @@ -72,6 +72,7 @@ export interface GenericFtrProviderContext< getService(serviceName: 'lifecycle'): Lifecycle; getService(serviceName: 'dockerServers'): DockerServersService; getService(serviceName: 'failureMetadata'): FailureMetadata; + getService(serviceName: 'esVersion'): EsVersion; getService(serviceName: T): ServiceMap[T]; /** @@ -100,6 +101,7 @@ export class GenericFtrService; } diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap index ad2f82de87b82d8..fb00908e0c7548d 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/__snapshots__/args.test.js.snap @@ -37,6 +37,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "suiteFiles": Object { "exclude": Array [], @@ -58,6 +59,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "suiteFiles": Object { "exclude": Array [], @@ -80,6 +82,7 @@ Object { "createLogger": [Function], "debug": true, "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "suiteFiles": Object { "exclude": Array [], @@ -101,6 +104,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "suiteFiles": Object { "exclude": Array [], @@ -124,6 +128,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": Object { "server.foo": "bar", }, @@ -146,6 +151,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "quiet": true, "suiteFiles": Object { @@ -167,6 +173,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "silent": true, "suiteFiles": Object { @@ -188,6 +195,7 @@ Object { ], "createLogger": [Function], "esFrom": "source", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "suiteFiles": Object { "exclude": Array [], @@ -208,6 +216,7 @@ Object { ], "createLogger": [Function], "esFrom": "source", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "suiteFiles": Object { "exclude": Array [], @@ -228,6 +237,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "installDir": "foo", "suiteFiles": Object { @@ -249,6 +259,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "grep": "management", "suiteFiles": Object { @@ -270,6 +281,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "suiteFiles": Object { "exclude": Array [], @@ -291,6 +303,7 @@ Object { ], "createLogger": [Function], "esFrom": "snapshot", + "esVersion": "999.999.999", "extraKbnOpts": undefined, "suiteFiles": Object { "exclude": Array [], diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js index 901ff6394649def..497a9b9c6c53394 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.js @@ -10,6 +10,7 @@ import { resolve } from 'path'; import dedent from 'dedent'; import { ToolingLog, pickLevelFromFlags } from '@kbn/dev-utils'; +import { EsVersion } from '../../../functional_test_runner'; const options = { help: { desc: 'Display this menu and exit.' }, @@ -147,6 +148,7 @@ export function processOptions(userOptions, defaultConfigPaths) { configs: configs.map((c) => resolve(c)), createLogger, extraKbnOpts: userOptions._, + esVersion: EsVersion.getDefault(), }; } diff --git a/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js b/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js index 7786aee5af55264..72ba541466960d2 100644 --- a/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js +++ b/packages/kbn-test/src/functional_tests/cli/run_tests/args.test.js @@ -6,9 +6,20 @@ * Side Public License, v 1. */ -import { displayHelp, processOptions } from './args'; import { createAbsolutePathSerializer } from '@kbn/dev-utils'; +import { displayHelp, processOptions } from './args'; + +jest.mock('../../../functional_test_runner/lib/es_version', () => { + return { + EsVersion: class { + static getDefault() { + return '999.999.999'; + } + }, + }; +}); + expect.addSnapshotSerializer(createAbsolutePathSerializer(process.cwd())); const INITIAL_TEST_ES_FROM = process.env.TEST_ES_FROM; diff --git a/packages/kbn-test/src/functional_tests/lib/run_ftr.ts b/packages/kbn-test/src/functional_tests/lib/run_ftr.ts index f9e109928ddc0ba..77d6cd8e357a5a4 100644 --- a/packages/kbn-test/src/functional_tests/lib/run_ftr.ts +++ b/packages/kbn-test/src/functional_tests/lib/run_ftr.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ import type { ToolingLog } from '@kbn/dev-utils'; -import { FunctionalTestRunner, readConfigFile } from '../../functional_test_runner'; +import { FunctionalTestRunner, readConfigFile, EsVersion } from '../../functional_test_runner'; import { CliError } from './run_cli'; export interface CreateFtrOptions { @@ -26,6 +26,7 @@ export interface CreateFtrOptions { exclude?: string[]; }; updateSnapshots?: boolean; + esVersion: EsVersion; } export interface CreateFtrParams { @@ -34,31 +35,46 @@ export interface CreateFtrParams { } async function createFtr({ configPath, - options: { installDir, log, bail, grep, updateBaselines, suiteFiles, suiteTags, updateSnapshots }, + options: { + installDir, + log, + bail, + grep, + updateBaselines, + suiteFiles, + suiteTags, + updateSnapshots, + esVersion, + }, }: CreateFtrParams) { - const config = await readConfigFile(log, configPath); + const config = await readConfigFile(log, esVersion, configPath); return { config, - ftr: new FunctionalTestRunner(log, configPath, { - mochaOpts: { - bail: !!bail, - grep, + ftr: new FunctionalTestRunner( + log, + configPath, + { + mochaOpts: { + bail: !!bail, + grep, + }, + kbnTestServer: { + installDir, + }, + updateBaselines, + updateSnapshots, + suiteFiles: { + include: [...(suiteFiles?.include || []), ...config.get('suiteFiles.include')], + exclude: [...(suiteFiles?.exclude || []), ...config.get('suiteFiles.exclude')], + }, + suiteTags: { + include: [...(suiteTags?.include || []), ...config.get('suiteTags.include')], + exclude: [...(suiteTags?.exclude || []), ...config.get('suiteTags.exclude')], + }, }, - kbnTestServer: { - installDir, - }, - updateBaselines, - updateSnapshots, - suiteFiles: { - include: [...(suiteFiles?.include || []), ...config.get('suiteFiles.include')], - exclude: [...(suiteFiles?.exclude || []), ...config.get('suiteFiles.exclude')], - }, - suiteTags: { - include: [...(suiteTags?.include || []), ...config.get('suiteTags.include')], - exclude: [...(suiteTags?.exclude || []), ...config.get('suiteTags.exclude')], - }, - }), + esVersion + ), }; } @@ -71,15 +87,15 @@ export async function assertNoneExcluded({ configPath, options }: CreateFtrParam } const stats = await ftr.getTestStats(); - if (stats.excludedTests.length > 0) { + if (stats.testsExcludedByTag.length > 0) { throw new CliError(` - ${stats.excludedTests.length} tests in the ${configPath} config + ${stats.testsExcludedByTag.length} tests in the ${configPath} config are excluded when filtering by the tags run on CI. Make sure that all suites are tagged with one of the following tags: ${JSON.stringify(options.suiteTags)} - - ${stats.excludedTests.join('\n - ')} + - ${stats.testsExcludedByTag.join('\n - ')} `); } } diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index cace061be64a9cf..5906193ca145c77 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -23,7 +23,7 @@ import { CreateFtrOptions, } from './lib'; -import { readConfigFile } from '../functional_test_runner/lib'; +import { readConfigFile, EsVersion } from '../functional_test_runner/lib'; const makeSuccessMessage = (options: StartServerOptions) => { const installDirFlag = options.installDir ? ` --kibana-install-dir=${options.installDir}` : ''; @@ -55,6 +55,7 @@ interface RunTestsParams extends CreateFtrOptions { configs: string[]; /** run from source instead of snapshot */ esFrom?: string; + esVersion: EsVersion; createLogger: () => ToolingLog; extraKbnOpts: string[]; assertNoneExcluded: boolean; @@ -105,7 +106,7 @@ export async function runTests(options: RunTestsParams) { log.write(`--- [${progress}] Running ${relative(REPO_ROOT, configPath)}`); await withProcRunner(log, async (procs) => { - const config = await readConfigFile(log, configPath); + const config = await readConfigFile(log, options.esVersion, configPath); let es; try { @@ -145,6 +146,7 @@ interface StartServerOptions { createLogger: () => ToolingLog; extraKbnOpts: string[]; useDefaultConfig?: boolean; + esVersion: EsVersion; } export async function startServers({ ...options }: StartServerOptions) { @@ -162,7 +164,7 @@ export async function startServers({ ...options }: StartServerOptions) { }; await withProcRunner(log, async (procs) => { - const config = await readConfigFile(log, options.config); + const config = await readConfigFile(log, options.esVersion, options.config); const es = await runElasticsearch({ config, options: opts }); await runKibanaServer({ diff --git a/packages/kbn-test/src/kbn_archiver_cli.ts b/packages/kbn-test/src/kbn_archiver_cli.ts index 80e35efaec976fc..f7f17900efcfffc 100644 --- a/packages/kbn-test/src/kbn_archiver_cli.ts +++ b/packages/kbn-test/src/kbn_archiver_cli.ts @@ -12,7 +12,7 @@ import Url from 'url'; import { RunWithCommands, createFlagError, Flags } from '@kbn/dev-utils'; import { KbnClient } from './kbn_client'; -import { readConfigFile } from './functional_test_runner'; +import { readConfigFile, EsVersion } from './functional_test_runner'; function getSinglePositionalArg(flags: Flags) { const positional = flags._; @@ -57,7 +57,7 @@ export function runKbnArchiverCli() { throw createFlagError('expected --config to be a string'); } - config = await readConfigFile(log, Path.resolve(flags.config)); + config = await readConfigFile(log, EsVersion.getDefault(), Path.resolve(flags.config)); statsMeta.set('ftrConfigPath', flags.config); } diff --git a/packages/kbn-test/types/ftr_globals/mocha.d.ts b/packages/kbn-test/types/ftr_globals/mocha.d.ts index ac9e33d4b9dcc7e..d5895b40f124532 100644 --- a/packages/kbn-test/types/ftr_globals/mocha.d.ts +++ b/packages/kbn-test/types/ftr_globals/mocha.d.ts @@ -14,5 +14,11 @@ declare module 'mocha' { * Assign tags to the test suite to determine in which CI job it should be run. */ tags(tags: string[] | string): void; + /** + * Define the ES versions for which this test requires, any version which doesn't meet this range will + * cause these tests to be skipped + * @param semver any valid semver range, like ">=8" + */ + onlyEsVersion(semver: string): void; } } diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index c2a4f18218dd475..27e44cba1094f00 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -428,6 +428,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableInfrastructureView': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'banners:placement': { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 69ed647f0845a0e..8776bad89f8a609 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -39,6 +39,7 @@ export interface UsageStats { 'observability:enableInspectEsQueries': boolean; 'observability:maxSuggestions': number; 'observability:enableComparisonByDefault': boolean; + 'observability:enableInfrastructureView': boolean; 'visualize:enableLabs': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 4641d271b3e4f6e..9c2c71898dee727 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7790,6 +7790,12 @@ "description": "Non-default value of setting." } }, + "observability:enableInfrastructureView": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "banners:placement": { "type": "keyword", "_meta": { diff --git a/x-pack/plugins/actions/server/constants/event_log.ts b/x-pack/plugins/actions/server/constants/event_log.ts index 9163a0d105ce8a3..9dba72462f317f2 100644 --- a/x-pack/plugins/actions/server/constants/event_log.ts +++ b/x-pack/plugins/actions/server/constants/event_log.ts @@ -10,4 +10,5 @@ export const EVENT_LOG_ACTIONS = { execute: 'execute', executeStart: 'execute-start', executeViaHttp: 'execute-via-http', + executeTimeout: 'execute-timeout', }; diff --git a/x-pack/plugins/actions/server/lib/action_executor.mock.ts b/x-pack/plugins/actions/server/lib/action_executor.mock.ts index 6abfec1116c3ad7..54df74b2fbd3d07 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.mock.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.mock.ts @@ -11,6 +11,7 @@ const createActionExecutorMock = () => { const mocked: jest.Mocked = { initialize: jest.fn(), execute: jest.fn().mockResolvedValue({ status: 'ok', actionId: '' }), + logCancellation: jest.fn(), }; return mocked; }; diff --git a/x-pack/plugins/actions/server/lib/action_executor.test.ts b/x-pack/plugins/actions/server/lib/action_executor.test.ts index 30d4ed92e03f8e0..1d678c244c1b09d 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.test.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.test.ts @@ -115,6 +115,7 @@ test('successfully executes', async () => { Object { "event": Object { "action": "execute-start", + "kind": "action", }, "kibana": Object { "saved_objects": Array [ @@ -134,6 +135,7 @@ test('successfully executes', async () => { Object { "event": Object { "action": "execute", + "kind": "action", "outcome": "success", }, "kibana": Object { @@ -511,6 +513,34 @@ test('logs a warning when alert executor returns invalid status', async () => { ); }); +test('writes to event log for execute timeout', async () => { + setupActionExecutorMock(); + + await actionExecutor.logCancellation({ + actionId: 'action1', + relatedSavedObjects: [], + request: {} as KibanaRequest, + }); + expect(eventLogger.logEvent).toHaveBeenCalledTimes(1); + expect(eventLogger.logEvent.mock.calls[0][0]).toMatchObject({ + event: { + action: 'execute-timeout', + }, + kibana: { + saved_objects: [ + { + rel: 'primary', + type: 'action', + id: 'action1', + type_id: 'test', + namespace: 'some-namespace', + }, + ], + }, + message: `action: test:action1: 'action-1' execution cancelled due to timeout - exceeded default timeout of "5m"`, + }); +}); + test('writes to event log for execute and execute start', async () => { const executorMock = setupActionExecutorMock(); executorMock.mockResolvedValue({ diff --git a/x-pack/plugins/actions/server/lib/action_executor.ts b/x-pack/plugins/actions/server/lib/action_executor.ts index 9458180fdd2200c..9737630628823fa 100644 --- a/x-pack/plugins/actions/server/lib/action_executor.ts +++ b/x-pack/plugins/actions/server/lib/action_executor.ts @@ -19,16 +19,17 @@ import { ActionTypeExecutorResult, ActionTypeRegistryContract, GetServicesFunction, - RawAction, PreConfiguredAction, + RawAction, } from '../types'; import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server'; import { SpacesServiceStart } from '../../../spaces/server'; import { EVENT_LOG_ACTIONS } from '../constants/event_log'; -import { IEvent, IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; +import { IEventLogger, SAVED_OBJECT_REL_PRIMARY } from '../../../event_log/server'; import { ActionsClient } from '../actions_client'; import { ActionExecutionSource } from './action_execution_source'; import { RelatedSavedObjects } from './related_saved_objects'; +import { createActionEventLogRecordObject } from './create_action_event_log_record_object'; // 1,000,000 nanoseconds in 1 millisecond const Millis2Nanos = 1000 * 1000; @@ -68,6 +69,7 @@ export class ActionExecutor { private isInitialized = false; private actionExecutorContext?: ActionExecutorContext; private readonly isESOCanEncrypt: boolean; + private actionInfo: ActionInfo | undefined; constructor({ isESOCanEncrypt }: { isESOCanEncrypt: boolean }) { this.isESOCanEncrypt = isESOCanEncrypt; @@ -124,7 +126,7 @@ export class ActionExecutor { const spaceId = spaces && spaces.getSpaceId(request); const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {}; - const { actionTypeId, name, config, secrets } = await getActionInfo( + const actionInfo = await getActionInfoInternal( await getActionsClientWithRequest(request, source), encryptedSavedObjectsClient, preconfiguredActions, @@ -132,6 +134,12 @@ export class ActionExecutor { namespace.namespace ); + const { actionTypeId, name, config, secrets } = actionInfo; + + if (!this.actionInfo || this.actionInfo.actionId !== actionId) { + this.actionInfo = actionInfo; + } + if (span) { span.name = `execute_action ${actionTypeId}`; span.addLabels({ @@ -169,26 +177,25 @@ export class ActionExecutor { ? { task: { scheduled: taskInfo.scheduled.toISOString(), - schedule_delay: Millis2Nanos * (Date.now() - taskInfo.scheduled.getTime()), + scheduleDelay: Millis2Nanos * (Date.now() - taskInfo.scheduled.getTime()), }, } : {}; - const event: IEvent = { - event: { action: EVENT_LOG_ACTIONS.execute }, - kibana: { - ...task, - saved_objects: [ - { - rel: SAVED_OBJECT_REL_PRIMARY, - type: 'action', - id: actionId, - type_id: actionTypeId, - ...namespace, - }, - ], - }, - }; + const event = createActionEventLogRecordObject({ + actionId, + action: EVENT_LOG_ACTIONS.execute, + ...namespace, + ...task, + savedObjects: [ + { + type: 'action', + id: actionId, + typeId: actionTypeId, + relation: SAVED_OBJECT_REL_PRIMARY, + }, + ], + }); for (const relatedSavedObject of relatedSavedObjects || []) { event.kibana?.saved_objects?.push({ @@ -210,6 +217,7 @@ export class ActionExecutor { }, message: `action started: ${actionLabel}`, }); + eventLogger.logEvent(startEvent); let rawResult: ActionTypeExecutorResult; @@ -269,22 +277,77 @@ export class ActionExecutor { } ); } -} -function actionErrorToMessage(result: ActionTypeExecutorResult): string { - let message = result.message || 'unknown error running action'; - - if (result.serviceMessage) { - message = `${message}: ${result.serviceMessage}`; - } - - if (result.retry instanceof Date) { - message = `${message}; retry at ${result.retry.toISOString()}`; - } else if (result.retry) { - message = `${message}; retry: ${JSON.stringify(result.retry)}`; + public async logCancellation({ + actionId, + request, + relatedSavedObjects, + source, + taskInfo, + }: { + actionId: string; + request: KibanaRequest; + taskInfo?: TaskInfo; + relatedSavedObjects: RelatedSavedObjects; + source?: ActionExecutionSource; + }) { + const { + spaces, + encryptedSavedObjectsClient, + preconfiguredActions, + eventLogger, + getActionsClientWithRequest, + } = this.actionExecutorContext!; + + const spaceId = spaces && spaces.getSpaceId(request); + const namespace = spaceId && spaceId !== 'default' ? { namespace: spaceId } : {}; + if (!this.actionInfo || this.actionInfo.actionId !== actionId) { + this.actionInfo = await getActionInfoInternal( + await getActionsClientWithRequest(request, source), + encryptedSavedObjectsClient, + preconfiguredActions, + actionId, + namespace.namespace + ); + } + const task = taskInfo + ? { + task: { + scheduled: taskInfo.scheduled.toISOString(), + scheduleDelay: Millis2Nanos * (Date.now() - taskInfo.scheduled.getTime()), + }, + } + : {}; + // Write event log entry + const event = createActionEventLogRecordObject({ + actionId, + action: EVENT_LOG_ACTIONS.executeTimeout, + message: `action: ${this.actionInfo.actionTypeId}:${actionId}: '${ + this.actionInfo.name ?? '' + }' execution cancelled due to timeout - exceeded default timeout of "5m"`, + ...namespace, + ...task, + savedObjects: [ + { + type: 'action', + id: actionId, + typeId: this.actionInfo.actionTypeId, + relation: SAVED_OBJECT_REL_PRIMARY, + }, + ], + }); + + for (const relatedSavedObject of (relatedSavedObjects || []) as RelatedSavedObjects) { + event.kibana?.saved_objects?.push({ + rel: SAVED_OBJECT_REL_PRIMARY, + type: relatedSavedObject.type, + id: relatedSavedObject.id, + type_id: relatedSavedObject.typeId, + namespace: relatedSavedObject.namespace, + }); + } + eventLogger.logEvent(event); } - - return message; } interface ActionInfo { @@ -292,9 +355,10 @@ interface ActionInfo { name: string; config: unknown; secrets: unknown; + actionId: string; } -async function getActionInfo( +async function getActionInfoInternal( actionsClient: PublicMethodsOf, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, preconfiguredActions: PreConfiguredAction[], @@ -311,6 +375,7 @@ async function getActionInfo( name: pcAction.name, config: pcAction.config, secrets: pcAction.secrets, + actionId, }; } @@ -329,5 +394,22 @@ async function getActionInfo( name, config, secrets, + actionId, }; } + +function actionErrorToMessage(result: ActionTypeExecutorResult): string { + let message = result.message || 'unknown error running action'; + + if (result.serviceMessage) { + message = `${message}: ${result.serviceMessage}`; + } + + if (result.retry instanceof Date) { + message = `${message}; retry at ${result.retry.toISOString()}`; + } else if (result.retry) { + message = `${message}; retry: ${JSON.stringify(result.retry)}`; + } + + return message; +} diff --git a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts new file mode 100644 index 000000000000000..ee58f8a01488cee --- /dev/null +++ b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createActionEventLogRecordObject } from './create_action_event_log_record_object'; + +describe('createActionEventLogRecordObject', () => { + test('created action event "execute-start"', async () => { + expect( + createActionEventLogRecordObject({ + actionId: '1', + action: 'execute-start', + timestamp: '1970-01-01T00:00:00.000Z', + task: { + scheduled: '1970-01-01T00:00:00.000Z', + scheduleDelay: 0, + }, + savedObjects: [ + { + id: '1', + type: 'action', + typeId: 'test', + relation: 'primary', + }, + ], + }) + ).toStrictEqual({ + '@timestamp': '1970-01-01T00:00:00.000Z', + event: { + action: 'execute-start', + kind: 'action', + }, + kibana: { + saved_objects: [ + { + id: '1', + rel: 'primary', + type: 'action', + type_id: 'test', + }, + ], + task: { + schedule_delay: 0, + scheduled: '1970-01-01T00:00:00.000Z', + }, + }, + }); + }); + + test('created action event "execute"', async () => { + expect( + createActionEventLogRecordObject({ + actionId: '1', + name: 'test name', + action: 'execute', + message: 'action execution start', + namespace: 'default', + savedObjects: [ + { + id: '2', + type: 'action', + typeId: '.email', + relation: 'primary', + }, + ], + }) + ).toStrictEqual({ + event: { + action: 'execute', + kind: 'action', + }, + kibana: { + saved_objects: [ + { + id: '2', + namespace: 'default', + rel: 'primary', + type: 'action', + type_id: '.email', + }, + ], + }, + message: 'action execution start', + }); + }); + + test('created action event "execute-timeout"', async () => { + expect( + createActionEventLogRecordObject({ + actionId: '1', + action: 'execute-timeout', + task: { + scheduled: '1970-01-01T00:00:00.000Z', + }, + savedObjects: [ + { + id: '1', + type: 'action', + typeId: 'test', + relation: 'primary', + }, + ], + }) + ).toStrictEqual({ + event: { + action: 'execute-timeout', + kind: 'action', + }, + kibana: { + saved_objects: [ + { + id: '1', + rel: 'primary', + type: 'action', + type_id: 'test', + }, + ], + task: { + schedule_delay: undefined, + scheduled: '1970-01-01T00:00:00.000Z', + }, + }, + }); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts new file mode 100644 index 000000000000000..1a1c5e9e6b3aaa5 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/create_action_event_log_record_object.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEvent } from '../../../event_log/server'; + +export type Event = Exclude; + +interface CreateActionEventLogRecordParams { + actionId: string; + action: string; + name?: string; + message?: string; + namespace?: string; + timestamp?: string; + task?: { + scheduled?: string; + scheduleDelay?: number; + }; + savedObjects: Array<{ + type: string; + id: string; + typeId: string; + relation?: string; + }>; +} + +export function createActionEventLogRecordObject(params: CreateActionEventLogRecordParams): Event { + const { action, message, task, namespace } = params; + + const event: Event = { + ...(params.timestamp ? { '@timestamp': params.timestamp } : {}), + event: { + action, + kind: 'action', + }, + kibana: { + saved_objects: params.savedObjects.map((so) => ({ + ...(so.relation ? { rel: so.relation } : {}), + type: so.type, + id: so.id, + type_id: so.typeId, + ...(namespace ? { namespace } : {}), + })), + ...(task ? { task: { scheduled: task.scheduled, schedule_delay: task.scheduleDelay } } : {}), + }, + ...(message ? { message } : {}), + }; + return event; +} diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts index ec0aa48ef291e4b..0ea6b5316fb82a4 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.test.ts @@ -22,6 +22,7 @@ const spaceIdToNamespace = jest.fn(); const actionTypeRegistry = actionTypeRegistryMock.create(); const mockedEncryptedSavedObjectsClient = encryptedSavedObjectsMock.createClient(); const mockedActionExecutor = actionExecutorMock.create(); +const eventLogger = eventLoggerMock.create(); let fakeTimer: sinon.SinonFakeTimers; let taskRunnerFactory: TaskRunnerFactory; @@ -62,7 +63,7 @@ const actionExecutorInitializerParams = { actionTypeRegistry, getActionsClientWithRequest: jest.fn(async () => actionsClientMock.create()), encryptedSavedObjectsClient: mockedEncryptedSavedObjectsClient, - eventLogger: eventLoggerMock.create(), + eventLogger, preconfiguredActions: [], }; const taskRunnerFactoryInitializerParams = { @@ -236,6 +237,37 @@ test('cleans up action_task_params object', async () => { expect(services.savedObjectsClient.delete).toHaveBeenCalledWith('action_task_params', '3'); }); +test('task runner should implement CancellableTask cancel method with logging warning message', async () => { + mockedEncryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValueOnce({ + id: '3', + type: 'action_task_params', + attributes: { + actionId: '2', + params: { baz: true }, + apiKey: Buffer.from('123:abc').toString('base64'), + }, + references: [ + { + id: '2', + name: 'actionRef', + type: 'action', + }, + ], + }); + const taskRunner = taskRunnerFactory.create({ + taskInstance: mockedTaskInstance, + }); + + await taskRunner.cancel(); + expect(mockedActionExecutor.logCancellation.mock.calls[0][0].actionId).toBe('2'); + + expect(mockedActionExecutor.logCancellation.mock.calls.length).toBe(1); + + expect(taskRunnerFactoryInitializerParams.logger.debug).toHaveBeenCalledWith( + `Cancelling action task for action with id 2 - execution error due to timeout.` + ); +}); + test('runs successfully when cleanup fails and logs the error', async () => { const taskRunner = taskRunnerFactory.create({ taskInstance: mockedTaskInstance, diff --git a/x-pack/plugins/actions/server/lib/task_runner_factory.ts b/x-pack/plugins/actions/server/lib/task_runner_factory.ts index 4f6b9ac2e8b7d7b..f3fdf627e08fff0 100644 --- a/x-pack/plugins/actions/server/lib/task_runner_factory.ts +++ b/x-pack/plugins/actions/server/lib/task_runner_factory.ts @@ -93,31 +93,10 @@ export class TaskRunnerFactory { encryptedSavedObjectsClient, spaceIdToNamespace ); - - const requestHeaders: Record = {}; - if (apiKey) { - requestHeaders.authorization = `ApiKey ${apiKey}`; - } - const path = addSpaceIdToPath('/', spaceId); - // Since we're using API keys and accessing elasticsearch can only be done - // via a request, we're faking one with the proper authorization headers. - const fakeRequest = KibanaRequest.from({ - headers: requestHeaders, - path: '/', - route: { settings: {} }, - url: { - href: '/', - }, - raw: { - req: { - url: '/', - }, - }, - } as unknown as Request); - - basePathService.set(fakeRequest, path); + const request = getFakeRequest(apiKey); + basePathService.set(request, path); // Throwing an executor error means we will attempt to retry the task // TM will treat a task as a failure if `attempts >= maxAttempts` @@ -132,7 +111,7 @@ export class TaskRunnerFactory { params, actionId: actionId as string, isEphemeral: !isPersistedActionTask(actionTaskExecutorParams), - request: fakeRequest, + request, ...getSourceFromReferences(references), taskInfo, relatedSavedObjects: validatedRelatedSavedObjects(logger, relatedSavedObjects), @@ -181,7 +160,7 @@ export class TaskRunnerFactory { // We would idealy secure every operation but in order to support clean up of legacy alerts // we allow this operation in an unsecured manner // Once support for legacy alert RBAC is dropped, this can be secured - await getUnsecuredSavedObjectsClient(fakeRequest).delete( + await getUnsecuredSavedObjectsClient(request).delete( ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, actionTaskExecutorParams.actionTaskParamsId ); @@ -193,10 +172,65 @@ export class TaskRunnerFactory { } } }, + cancel: async () => { + // Write event log entry + const actionTaskExecutorParams = taskInstance.params as ActionTaskExecutorParams; + const { spaceId } = actionTaskExecutorParams; + + const { + attributes: { actionId, apiKey, relatedSavedObjects }, + references, + } = await getActionTaskParams( + actionTaskExecutorParams, + encryptedSavedObjectsClient, + spaceIdToNamespace + ); + + const request = getFakeRequest(apiKey); + const path = addSpaceIdToPath('/', spaceId); + basePathService.set(request, path); + + await actionExecutor.logCancellation({ + actionId, + request, + relatedSavedObjects: (relatedSavedObjects || []) as RelatedSavedObjects, + ...getSourceFromReferences(references), + }); + + logger.debug( + `Cancelling action task for action with id ${actionId} - execution error due to timeout.` + ); + return { state: {} }; + }, }; } } +function getFakeRequest(apiKey?: string) { + const requestHeaders: Record = {}; + if (apiKey) { + requestHeaders.authorization = `ApiKey ${apiKey}`; + } + + // Since we're using API keys and accessing elasticsearch can only be done + // via a request, we're faking one with the proper authorization headers. + const fakeRequest = KibanaRequest.from({ + headers: requestHeaders, + path: '/', + route: { settings: {} }, + url: { + href: '/', + }, + raw: { + req: { + url: '/', + }, + }, + } as unknown as Request); + + return fakeRequest; +} + async function getActionTaskParams( executorParams: ActionTaskExecutorParams, encryptedSavedObjectsClient: EncryptedSavedObjectsClient, diff --git a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx index 15efd28756b0b65..fc9250973fc3072 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/agent_configurations/List/index.tsx @@ -245,7 +245,6 @@ export function AgentConfigurationList({ items={configurations} initialSortField="service.name" initialSortDirection="asc" - initialPageSize={20} /> ); diff --git a/x-pack/plugins/apm/public/components/app/Settings/custom_link/custom_link_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/custom_link/custom_link_table.tsx index 5ce98f8b10884ca..d2603538d84bfac 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/custom_link/custom_link_table.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/custom_link/custom_link_table.tsx @@ -119,7 +119,6 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { } items={filteredItems} columns={columns} - initialPageSize={10} initialSortField="@timestamp" initialSortDirection="desc" /> diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx index 65681a398d8e657..adeb3b175044474 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/error_group_list/index.tsx @@ -233,7 +233,6 @@ function ErrorGroupList({ })} items={mainStatistics} columns={columns} - initialPageSize={25} initialSortField="occurrences" initialSortDirection="desc" sortItems={false} diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index 2d83f1f46bd3881..4617daac2ddcf93 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -315,7 +315,6 @@ export function ServiceList({ noItemsMessage={noItemsMessage} initialSortField={initialSortField} initialSortDirection="desc" - initialPageSize={50} sortFn={(itemsToSort, sortField, sortDirection) => { // For healthStatus, sort items by healthStatus first, then by TPM return sortField === 'healthStatus' diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index 0436c27cdd6b7b0..379632d33a80839 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -25,7 +25,6 @@ import { truncate, unit } from '../../../utils/style'; import { ServiceNodeMetricOverviewLink } from '../../shared/links/apm/service_node_metric_overview_link'; import { ITableColumn, ManagedTable } from '../../shared/managed_table'; -const INITIAL_PAGE_SIZE = 25; const INITIAL_SORT_FIELD = 'cpu'; const INITIAL_SORT_DIRECTION = 'desc'; @@ -170,7 +169,6 @@ function ServiceNodeOverview() { })} items={items} columns={columns} - initialPageSize={INITIAL_PAGE_SIZE} initialSortField={INITIAL_SORT_FIELD} initialSortDirection={INITIAL_SORT_DIRECTION} /> diff --git a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx index 9e5508a5810df7f..2c30027770f4351 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/index.tsx @@ -118,6 +118,7 @@ export function ServiceOverview() { isSingleColumn={isSingleColumn} start={start} end={end} + hidePerPageOptions={true} /> @@ -164,6 +165,7 @@ export function ServiceOverview() { {i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index cbf60b7b59e4d83..255dfbdeb427a93 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -24,12 +24,14 @@ interface ServiceOverviewDependenciesTableProps { fixedHeight?: boolean; isSingleColumn?: boolean; link?: ReactNode; + hidePerPageOptions?: boolean; } export function ServiceOverviewDependenciesTable({ fixedHeight, isSingleColumn = true, link, + hidePerPageOptions = false, }: ServiceOverviewDependenciesTableProps) { const { urlParams: { comparisonEnabled, comparisonType, latencyAggregationType }, @@ -139,6 +141,7 @@ export function ServiceOverviewDependenciesTable({ )} status={status} link={link} + hidePerPageOptions={hidePerPageOptions} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx index e5f3c7bcbee4e78..fe09a4784239e25 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/trace_list.tsx @@ -169,7 +169,6 @@ export function TraceList({ items = [], isLoading, isFailure }: Props) { initialSortField="impact" initialSortDirection="desc" noItemsMessage={noItemsMessage} - initialPageSize={25} /> ); } diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index 962fbb4eb6be6da..93c222164f02607 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -15,6 +15,7 @@ import { import { i18n } from '@kbn/i18n'; import { omit } from 'lodash'; import React from 'react'; +import { enableInfrastructureView } from '../../../../../../observability/public'; import { isIosAgentName, isJavaAgentName, @@ -159,7 +160,8 @@ export function isJVMsTabHidden({ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { const { agentName, runtimeName } = useApmServiceContext(); - const { config } = useApmPluginContext(); + const { config, core } = useApmPluginContext(); + const showInfraTab = core.uiSettings.get(enableInfrastructureView); const router = useApmRouter(); @@ -250,6 +252,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { label: i18n.translate('xpack.apm.home.infraTabLabel', { defaultMessage: 'Infrastructure', }), + hidden: !showInfraTab, }, { key: 'service-map', diff --git a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx index 7c2bc722ac1e6b0..844957defe67dbe 100644 --- a/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/dependencies_table/index.tsx @@ -45,6 +45,7 @@ interface Props { nameColumnTitle: React.ReactNode; status: FETCH_STATUS; compact?: boolean; + hidePerPageOptions?: boolean; } export function DependenciesTable(props: Props) { @@ -57,6 +58,7 @@ export function DependenciesTable(props: Props) { nameColumnTitle, status, compact = true, + hidePerPageOptions = false, } = props; // SparkPlots should be hidden if we're in two-column view and size XL (1200px) @@ -210,8 +212,8 @@ export function DependenciesTable(props: Props) { noItemsMessage={noItemsMessage} initialSortField="impactValue" initialSortDirection="desc" - initialPageSize={5} pagination={true} + hidePerPageOptions={hidePerPageOptions} /> diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap b/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap index 15613af4daf98a8..3559ee4afbb82c9 100644 --- a/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/shared/managed_table/__snapshots__/managed_table.test.tsx.snap @@ -44,9 +44,14 @@ exports[`ManagedTable should render a page-full of items, with defaults 1`] = ` onChange={[Function]} pagination={ Object { - "hidePerPageOptions": true, + "hidePerPageOptions": false, "pageIndex": 0, - "pageSize": 10, + "pageSize": 25, + "pageSizeOptions": Array [ + 10, + 25, + 50, + ], "totalItemCount": 3, } } @@ -102,6 +107,11 @@ exports[`ManagedTable should render when specifying initial values 1`] = ` "hidePerPageOptions": false, "pageIndex": 1, "pageSize": 2, + "pageSizeOptions": Array [ + 10, + 25, + 50, + ], "totalItemCount": 3, } } diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx index 16ab8cb1d920217..37d55887cd182b7 100644 --- a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx @@ -47,6 +47,9 @@ interface Props { tableLayout?: 'auto' | 'fixed'; } +const PAGE_SIZE_OPTIONS = [10, 25, 50]; +const INITIAL_PAGE_SIZE = 25; + function defaultSortFn( items: T[], sortField: string, @@ -61,10 +64,10 @@ function UnoptimizedManagedTable(props: Props) { items, columns, initialPageIndex = 0, - initialPageSize = 10, + initialPageSize = INITIAL_PAGE_SIZE, initialSortField = props.columns[0]?.field || '', initialSortDirection = 'asc', - hidePerPageOptions = true, + hidePerPageOptions = false, noItemsMessage, sortItems = true, sortFn = defaultSortFn, @@ -128,6 +131,7 @@ function UnoptimizedManagedTable(props: Props) { totalItemCount: items.length, pageIndex: page, pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; }, [hidePerPageOptions, items, page, pageSize, pagination]); diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx index b6e02b1b08c3cb8..49d5e95344ea43a 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx @@ -5,12 +5,7 @@ * 2.0. */ -import { - EuiBasicTableColumn, - EuiFlexGroup, - EuiFlexItem, - RIGHT_ALIGNMENT, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, RIGHT_ALIGNMENT } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ValuesType } from 'utility-types'; @@ -25,6 +20,7 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { ImpactBar } from '../impact_bar'; import { TransactionDetailLink } from '../links/apm/transaction_detail_link'; import { ListMetric } from '../list_metric'; +import { ITableColumn } from '../managed_table'; import { TruncateWithTooltip } from '../truncate_with_tooltip'; import { getLatencyColumnLabel } from './get_latency_column_label'; @@ -51,7 +47,7 @@ export function getColumns({ comparisonEnabled?: boolean; shouldShowSparkPlots?: boolean; comparisonType?: TimeRangeComparisonType; -}): Array> { +}): Array> { return [ { field: 'name', diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index f943cf4da4b059f..a98eda2d3b96169 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -5,12 +5,7 @@ * 2.0. */ -import { - EuiBasicTable, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { orderBy } from 'lodash'; import React, { useState } from 'react'; @@ -28,6 +23,7 @@ import { OverviewTableContainer } from '../overview_table_container'; import { getColumns } from './get_columns'; import { ElasticDocsLink } from '../links/elastic_docs_link'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; +import { ManagedTable } from '../managed_table'; type ApiResponse = APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics'>; @@ -60,6 +56,7 @@ interface Props { hideViewTransactionsLink?: boolean; isSingleColumn?: boolean; numberOfTransactionsPerPage?: number; + hidePerPageOptions?: boolean; showAggregationAccurateCallout?: boolean; environment: string; fixedHeight?: boolean; @@ -73,13 +70,14 @@ export function TransactionsTable({ hideViewTransactionsLink = false, isSingleColumn = true, numberOfTransactionsPerPage = 5, + hidePerPageOptions = false, showAggregationAccurateCallout = false, environment, kuery, start, end, }: Props) { - const [tableOptions, setTableOptions] = useState<{ + const [tableOptions] = useState<{ pageIndex: number; sort: { direction: SortDirection; @@ -228,13 +226,6 @@ export function TransactionsTable({ const isLoading = status === FETCH_STATUS.LOADING; const isNotInitiated = status === FETCH_STATUS.NOT_INITIATED; - const pagination = { - pageIndex, - pageSize: numberOfTransactionsPerPage, - totalItemCount: transactionGroupsTotalItems, - hidePerPageOptions: true, - }; - return ( @@ -309,7 +300,14 @@ export function TransactionsTable({ transactionGroupsTotalItems === 0 && isNotInitiated } > - { - setTableOptions({ - pageIndex: newTableOptions.page?.index ?? 0, - sort: newTableOptions.sort - ? { - field: newTableOptions.sort.field as SortField, - direction: newTableOptions.sort.direction, - } - : DEFAULT_SORT, - }); - }} + hidePerPageOptions={hidePerPageOptions} /> diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 5591c165bd7063b..1ad4ff1adbdd09c 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -157,8 +157,10 @@ class PackagePolicyService { ); } } + validatePackagePolicyOrThrow(packagePolicy, pkgInfo); const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version); + inputs = await this._compilePackagePolicyInputs( registryPkgInfo, pkgInfo, @@ -392,6 +394,8 @@ class PackagePolicyService { pkgVersion: packagePolicy.package.version, }); + validatePackagePolicyOrThrow(packagePolicy, pkgInfo); + const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version); inputs = await this._compilePackagePolicyInputs( registryPkgInfo, @@ -865,6 +869,31 @@ class PackagePolicyService { } } +function validatePackagePolicyOrThrow(packagePolicy: NewPackagePolicy, pkgInfo: PackageInfo) { + const validationResults = validatePackagePolicy(packagePolicy, pkgInfo, safeLoad); + if (validationHasErrors(validationResults)) { + const responseFormattedValidationErrors = Object.entries(getFlattenedObject(validationResults)) + .map(([key, value]) => ({ + key, + message: value, + })) + .filter(({ message }) => !!message); + + if (responseFormattedValidationErrors.length) { + throw new PackagePolicyValidationError( + i18n.translate('xpack.fleet.packagePolicyInvalidError', { + defaultMessage: 'Package policy is invalid: {errors}', + values: { + errors: responseFormattedValidationErrors + .map(({ key, message }) => `${key}: ${message}`) + .join('\n'), + }, + }) + ); + } + } +} + function assignStreamIdToInput(packagePolicyId: string, input: NewPackagePolicyInput) { return { ...input, @@ -1314,29 +1343,7 @@ export function preconfigurePackageInputs( inputs, }; - const validationResults = validatePackagePolicy(resultingPackagePolicy, packageInfo, safeLoad); - - if (validationHasErrors(validationResults)) { - const responseFormattedValidationErrors = Object.entries(getFlattenedObject(validationResults)) - .map(([key, value]) => ({ - key, - message: value, - })) - .filter(({ message }) => !!message); - - if (responseFormattedValidationErrors.length) { - throw new PackagePolicyValidationError( - i18n.translate('xpack.fleet.packagePolicyInvalidError', { - defaultMessage: 'Package policy is invalid: {errors}', - values: { - errors: responseFormattedValidationErrors - .map(({ key, message }) => `${key}: ${message}`) - .join('\n'), - }, - }) - ); - } - } + validatePackagePolicyOrThrow(resultingPackagePolicy, packageInfo); return resultingPackagePolicy; } diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index d4fcab060b01c00..096f5370ca3b97c 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -42,14 +42,24 @@ describe('hitsToGeoJson', () => { _id: 'doc1', _index: 'index1', fields: { - [geoFieldName]: '20,100', + [geoFieldName]: [ + { + type: 'Point', + coordinates: [100, 20], + }, + ], }, }, { _id: 'doc2', _index: 'index1', - _source: { - [geoFieldName]: '30,110', + fields: { + [geoFieldName]: [ + { + type: 'Point', + coordinates: [110, 30], + }, + ], }, }, ]; @@ -73,12 +83,17 @@ describe('hitsToGeoJson', () => { it('Should handle documents where geoField is not populated', () => { const hits = [ { - _source: { - [geoFieldName]: '20,100', + fields: { + [geoFieldName]: [ + { + type: 'Point', + coordinates: [100, 20], + }, + ], }, }, { - _source: {}, + fields: {}, }, ]; const geojson = hitsToGeoJson(hits, flattenHitMock, geoFieldName, 'geo_point', []); @@ -90,10 +105,15 @@ describe('hitsToGeoJson', () => { const hits = [ { _source: { - [geoFieldName]: '20,100', myField: 8, }, fields: { + [geoFieldName]: [ + { + type: 'Point', + coordinates: [100, 20], + }, + ], myScriptedField: 10, }, }, @@ -109,8 +129,17 @@ describe('hitsToGeoJson', () => { { _id: 'doc1', _index: 'index1', - _source: { - [geoFieldName]: ['20,100', '30,110'], + fields: { + [geoFieldName]: [ + { + type: 'Point', + coordinates: [100, 20], + }, + { + type: 'Point', + coordinates: [110, 30], + }, + ], myField: 8, }, }, @@ -151,15 +180,15 @@ describe('hitsToGeoJson', () => { { _id: 'doc1', _index: 'index1', - _source: { + fields: { [geoFieldName]: { type: 'GeometryCollection', geometries: [ { - type: 'geometrycollection', //explicitly test coercion to proper GeoJson type value + type: 'geometrycollection', geometries: [ { - type: 'point', //explicitly test coercion to proper GeoJson type value + type: 'Point', coordinates: [0, 0], }, ], @@ -216,8 +245,11 @@ describe('hitsToGeoJson', () => { { _id: 'doc1', _index: 'index1', - _source: { - [geoFieldName]: '20,100', + fields: { + [geoFieldName]: { + type: 'Point', + coordinates: [100, 20], + }, myDateField: '1587156257081', }, }, @@ -234,16 +266,21 @@ describe('hitsToGeoJson', () => { const geoFieldName = 'my.location'; const indexPatternFlattenHit = (hit) => { return { - [geoFieldName]: _.get(hit._source, geoFieldName), + [geoFieldName]: _.get(hit.fields, geoFieldName), }; }; it('Should handle geoField being an object', () => { const hits = [ { - _source: { + fields: { my: { - location: '20,100', + location: [ + { + type: 'Point', + coordinates: [100, 20], + }, + ], }, }, }, @@ -258,8 +295,13 @@ describe('hitsToGeoJson', () => { it('Should handle geoField containing dot in the name', () => { const hits = [ { - _source: { - ['my.location']: '20,100', + fields: { + ['my.location']: [ + { + type: 'Point', + coordinates: [100, 20], + }, + ], }, }, ]; @@ -273,15 +315,25 @@ describe('hitsToGeoJson', () => { it('Should not modify results of flattenHit', () => { const geoFieldName = 'location'; const cachedProperities = { - [geoFieldName]: '20,100', + [geoFieldName]: [ + { + type: 'Point', + coordinates: [100, 20], + }, + ], }; const cachedFlattenHit = () => { return cachedProperities; }; const hits = [ { - _source: { - [geoFieldName]: '20,100', + fields: { + [geoFieldName]: [ + { + type: 'Point', + coordinates: [100, 20], + }, + ], }, }, ]; @@ -296,8 +348,11 @@ describe('geoPointToGeometry', () => { const lat = 41.12; const lon = -71.34; - it('Should convert single docvalue_field', () => { - const value = `${lat},${lon}`; + it('Should convert value', () => { + const value = { + type: 'Point', + coordinates: [lon, lat], + }; const points = []; geoPointToGeometry(value, points); expect(points.length).toBe(1); @@ -305,10 +360,19 @@ describe('geoPointToGeometry', () => { expect(points[0].coordinates).toEqual([lon, lat]); }); - it('Should convert multiple docvalue_fields', () => { + it('Should convert array of values', () => { const lat2 = 30; const lon2 = -60; - const value = [`${lat},${lon}`, `${lat2},${lon2}`]; + const value = [ + { + type: 'Point', + coordinates: [lon, lat], + }, + { + type: 'Point', + coordinates: [lon2, lat2], + }, + ]; const points = []; geoPointToGeometry(value, points); expect(points.length).toBe(2); @@ -318,13 +382,13 @@ describe('geoPointToGeometry', () => { }); describe('geoShapeToGeometry', () => { - it('Should convert value stored as geojson', () => { + it('Should convert value', () => { const coordinates = [ [-77.03653, 38.897676], [-77.009051, 38.889939], ]; const value = { - type: 'linestring', + type: 'LineString', coordinates: coordinates, }; const shapes = []; @@ -340,7 +404,7 @@ describe('geoShapeToGeometry', () => { [101.0, 0.0], ]; const value = { - type: 'envelope', + type: 'Envelope', coordinates: coordinates, }; const shapes = []; @@ -366,11 +430,11 @@ describe('geoShapeToGeometry', () => { const pointCoordinates = [125.6, 10.1]; const value = [ { - type: 'linestring', + type: 'LineString', coordinates: linestringCoordinates, }, { - type: 'point', + type: 'Point', coordinates: pointCoordinates, }, ]; @@ -382,28 +446,6 @@ describe('geoShapeToGeometry', () => { expect(shapes[1].type).toBe('Point'); expect(shapes[1].coordinates).toEqual(pointCoordinates); }); - - it('Should convert wkt shapes to geojson', () => { - const pointWkt = 'POINT (32 40)'; - const linestringWkt = 'LINESTRING (50 60, 70 80)'; - - const shapes = []; - geoShapeToGeometry(pointWkt, shapes); - geoShapeToGeometry(linestringWkt, shapes); - - expect(shapes.length).toBe(2); - expect(shapes[0]).toEqual({ - coordinates: [32, 40], - type: 'Point', - }); - expect(shapes[1]).toEqual({ - coordinates: [ - [50, 60], - [70, 80], - ], - type: 'LineString', - }); - }); }); describe('roundCoordinates', () => { diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts index 98782e7447b348f..3e494976787447b 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts @@ -7,10 +7,6 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -// @ts-expect-error -import { parse } from 'wellknown'; -// @ts-expect-error -import turfCircle from '@turf/circle'; import { Feature, FeatureCollection, Geometry, Polygon, Point, Position } from 'geojson'; import { BBox } from '@turf/helpers'; import { @@ -89,12 +85,12 @@ export function hitsToGeoJson( ensureGeoField(geoFieldType); if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { geoPointToGeometry( - properties[geoFieldName] as string | string[] | undefined, + properties[geoFieldName] as Point | Point[] | undefined, tmpGeometriesAccumulator ); } else { geoShapeToGeometry( - properties[geoFieldName] as string | string[] | ESGeometry | ESGeometry[] | undefined, + properties[geoFieldName] as ESGeometry | ESGeometry[] | undefined, tmpGeometriesAccumulator ); } @@ -131,12 +127,9 @@ export function hitsToGeoJson( }; } -// Parse geo_point docvalue_field -// Either -// 1) Array of latLon strings -// 2) latLon string +// Parse geo_point fields API response export function geoPointToGeometry( - value: string[] | string | undefined, + value: Point[] | Point | undefined, accumulator: Geometry[] ): void { if (!value) { @@ -150,99 +143,12 @@ export function geoPointToGeometry( return; } - const commaSplit = value.split(','); - const lat = parseFloat(commaSplit[0]); - const lon = parseFloat(commaSplit[1]); - accumulator.push({ - type: GEO_JSON_TYPE.POINT, - coordinates: [lon, lat], - } as Point); -} - -export function convertESShapeToGeojsonGeometry(value: ESGeometry): Geometry { - const geoJson = { - type: value.type, - coordinates: value.coordinates, - }; - - // https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#input-structure - // For some unknown compatibility nightmarish reason, Elasticsearch types are not capitalized the same as geojson types - // For example: 'LineString' geojson type is 'linestring' in elasticsearch - // Convert feature types to geojson spec values - // Sometimes, the type in ES is capitalized correctly. Sometimes it is not. It depends on how the doc was ingested - // The below is the correction in-place. - switch (value.type) { - case 'point': - geoJson.type = GEO_JSON_TYPE.POINT; - break; - case 'linestring': - geoJson.type = GEO_JSON_TYPE.LINE_STRING; - break; - case 'polygon': - geoJson.type = GEO_JSON_TYPE.POLYGON; - break; - case 'multipoint': - geoJson.type = GEO_JSON_TYPE.MULTI_POINT; - break; - case 'multilinestring': - geoJson.type = GEO_JSON_TYPE.MULTI_LINE_STRING; - break; - case 'multipolygon': - geoJson.type = GEO_JSON_TYPE.MULTI_POLYGON; - break; - case 'geometrycollection': - case GEO_JSON_TYPE.GEOMETRY_COLLECTION: - // PEBKAC - geometry-collections need to be unrolled to their individual geometries first. - const invalidGeometrycollectionError = i18n.translate( - 'xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage', - { - defaultMessage: `Should not pass GeometryCollection to convertESShapeToGeojsonGeometry`, - } - ); - throw new Error(invalidGeometrycollectionError); - case 'envelope': - const envelopeCoords = geoJson.coordinates as Position[]; - // format defined here https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#_envelope - const polygon = formatEnvelopeAsPolygon({ - minLon: envelopeCoords[0][0], - maxLon: envelopeCoords[1][0], - minLat: envelopeCoords[1][1], - maxLat: envelopeCoords[0][1], - }); - geoJson.type = polygon.type; - geoJson.coordinates = polygon.coordinates; - break; - case 'circle': - const errorMessage = i18n.translate( - 'xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage', - { - defaultMessage: `Unable to convert {geometryType} geometry to geojson, not supported`, - values: { - geometryType: geoJson.type, - }, - } - ); - throw new Error(errorMessage); - } - return geoJson as unknown as Geometry; -} - -function convertWKTStringToGeojson(value: string): Geometry { - try { - return parse(value); - } catch (e) { - const errorMessage = i18n.translate('xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage', { - defaultMessage: `Unable to convert {wkt} to geojson. Valid WKT expected.`, - values: { - wkt: value, - }, - }); - throw new Error(errorMessage); - } + accumulator.push(value as Point); } +// Parse geo_shape fields API response export function geoShapeToGeometry( - value: string | ESGeometry | string[] | ESGeometry[] | undefined, + value: ESGeometry | ESGeometry[] | undefined, accumulator: Geometry[] ): void { if (!value) { @@ -257,21 +163,38 @@ export function geoShapeToGeometry( return; } - if (typeof value === 'string') { - const geoJson = convertWKTStringToGeojson(value); - accumulator.push(geoJson); - } else if ( - // Needs to deal with possible inconsistencies in capitalization - value.type === GEO_JSON_TYPE.GEOMETRY_COLLECTION || - value.type === 'geometrycollection' - ) { + if (value.type.toLowerCase() === GEO_JSON_TYPE.GEOMETRY_COLLECTION.toLowerCase()) { const geometryCollection = value as unknown as { geometries: ESGeometry[] }; for (let i = 0; i < geometryCollection.geometries.length; i++) { geoShapeToGeometry(geometryCollection.geometries[i], accumulator); } + return; + } + + // fields API does not return true geojson yet, circle and envelope still exist which are not part of geojson spec + if (value.type.toLowerCase() === 'envelope') { + const envelopeCoords = value.coordinates as Position[]; + // format defined here https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-shape.html#_envelope + const polygon = formatEnvelopeAsPolygon({ + minLon: envelopeCoords[0][0], + maxLon: envelopeCoords[1][0], + minLat: envelopeCoords[1][1], + maxLat: envelopeCoords[0][1], + }); + accumulator.push(polygon); + } else if (value.type.toLowerCase() === 'circle') { + const errorMessage = i18n.translate( + 'xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage', + { + defaultMessage: `Unable to convert {geometryType} geometry to geojson, not supported`, + values: { + geometryType: value.type, + }, + } + ); + throw new Error(errorMessage); } else { - const geoJson = convertESShapeToGeojsonGeometry(value); - accumulator.push(geoJson); + accumulator.push(value as Geometry); } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 687418edd25b5ba..1b7c9e1cd6aa0cb 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -281,21 +281,26 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource const indexPattern: IndexPattern = await this.getIndexPattern(); + const fieldNames = searchFilters.fieldNames.filter( + (fieldName) => fieldName !== this._descriptor.geoField + ); const { docValueFields, sourceOnlyFields, scriptFields } = getDocValueAndSourceFields( indexPattern, - searchFilters.fieldNames, + fieldNames, 'epoch_millis' ); const topHits: { size: number; script_fields: Record; docvalue_fields: Array; + fields: string[]; _source?: boolean | { includes: string[] }; sort?: Array>; } = { size: topHitsSize, script_fields: scriptFields, docvalue_fields: docValueFields, + fields: [this._descriptor.geoField], }; if (this._hasSort()) { @@ -389,9 +394,12 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource ) { const indexPattern = await this.getIndexPattern(); + const fieldNames = searchFilters.fieldNames.filter( + (fieldName) => fieldName !== this._descriptor.geoField + ); const { docValueFields, sourceOnlyFields } = getDocValueAndSourceFields( indexPattern, - searchFilters.fieldNames, + fieldNames, 'epoch_millis' ); @@ -418,6 +426,7 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource } else { searchSource.setField('source', sourceOnlyFields); } + searchSource.setField('fields', [this._descriptor.geoField]); if (this._hasSort()) { searchSource.setField('sort', this._buildEsSort()); } @@ -800,9 +809,12 @@ export class ESSearchSource extends AbstractESSource implements IMvtVectorSource const indexPattern = await this.getIndexPattern(); const indexSettings = await loadIndexSettings(indexPattern.title); + const fieldNames = searchFilters.fieldNames.filter( + (fieldName) => fieldName !== this._descriptor.geoField + ); const { docValueFields, sourceOnlyFields } = getDocValueAndSourceFields( indexPattern, - searchFilters.fieldNames, + fieldNames, 'epoch_millis' ); diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 03860fd3cd1227a..4f303390e1e1b28 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -10,6 +10,7 @@ export { enableInspectEsQueries, maxSuggestions, enableComparisonByDefault, + enableInfrastructureView, } from './ui_settings_keys'; export const casesFeatureId = 'observabilityCases'; diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 4d34e216a017c0e..ea8a2f20ea4e780 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -8,3 +8,4 @@ export const enableInspectEsQueries = 'observability:enableInspectEsQueries'; export const maxSuggestions = 'observability:maxSuggestions'; export const enableComparisonByDefault = 'observability:enableComparisonByDefault'; +export const enableInfrastructureView = 'observability:enableInfrastructureView'; diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index e502cf7fb37e0cd..d855d0178192ed4 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -23,7 +23,11 @@ export type { ObservabilityPublicPluginsSetup, ObservabilityPublicPluginsStart, }; -export { enableInspectEsQueries } from '../common/ui_settings_keys'; +export { + enableInspectEsQueries, + enableComparisonByDefault, + enableInfrastructureView, +} from '../common/ui_settings_keys'; export { uptimeOverviewLocatorID } from '../common'; export interface ConfigSchema { @@ -94,7 +98,6 @@ export type { AddInspectorRequest } from './context/inspector/inspector_context' export { InspectorContextProvider } from './context/inspector/inspector_context'; export { useInspectorContext } from './context/inspector/use_inspector_context'; -export { enableComparisonByDefault } from '../common/ui_settings_keys'; export type { SeriesConfig, ConfigProps } from './components/shared/exploratory_view/types'; export { ReportTypes, diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index ad0aa31542e8c71..8d37398b8a07b0c 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -13,6 +13,7 @@ import { enableComparisonByDefault, enableInspectEsQueries, maxSuggestions, + enableInfrastructureView, } from '../common/ui_settings_keys'; /** @@ -48,7 +49,18 @@ export const uiSettings: Record> = { }), value: true, description: i18n.translate('xpack.observability.enableComparisonByDefaultDescription', { - defaultMessage: 'Enable the comparison feature on APM UI', + defaultMessage: 'Enable the comparison feature in APM app', + }), + schema: schema.boolean(), + }, + [enableInfrastructureView]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.enableInfrastructureView', { + defaultMessage: 'Infrastructure feature', + }), + value: true, + description: i18n.translate('xpack.observability.enableInfrastructureViewDescription', { + defaultMessage: 'Enable the Infrastruture view feature in APM app', }), schema: schema.boolean(), }, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index d4fdb3687fd8692..6e408bfb0822a95 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -99,6 +99,7 @@ export enum SecurityPageName { hosts = 'hosts', hostsAnomalies = 'hosts-anomalies', hostsExternalAlerts = 'hosts-external_alerts', + hostsRisk = 'hosts-risk', investigate = 'investigate', network = 'network', networkAnomalies = 'network-anomalies', @@ -361,7 +362,7 @@ export const showAllOthersBucket: string[] = [ */ export const ELASTIC_NAME = 'estc' as const; -export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_' as const; +export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_' as const; export const TRANSFORM_STATES = { ABORTING: 'aborting', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts index 4273c08c638f3f7..1c0d20161823bae 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts @@ -11,13 +11,16 @@ import type { IEsSearchResponse, } from '../../../../../../../../src/plugins/data/common'; import { RISKY_HOSTS_INDEX_PREFIX } from '../../../../constants'; -import { Inspect, Maybe, TimerangeInput } from '../../../common'; +import { Direction, Inspect, Maybe, TimerangeInput } from '../../../common'; export interface HostsRiskScoreRequestOptions extends IEsSearchRequest { defaultIndex: string[]; factoryQueryType?: FactoryQueryTypes; hostNames?: string[]; timerange?: TimerangeInput; + onlyLatest?: boolean; + limit?: number; + sortOrder?: Direction.asc | Direction.desc; } export interface HostsRiskScoreStrategyResponse extends IEsSearchResponse { @@ -25,6 +28,7 @@ export interface HostsRiskScoreStrategyResponse extends IEsSearchResponse { } export interface HostsRiskScore { + '@timestamp': string; host: { name: string; }; @@ -37,9 +41,9 @@ export interface HostsRiskScore { export interface RuleRisk { rule_name: string; - rule_risk: string; + rule_risk: number; } -export const getHostRiskIndex = (spaceId: string): string => { - return `${RISKY_HOSTS_INDEX_PREFIX}${spaceId}`; +export const getHostRiskIndex = (spaceId: string, onlyLatest: boolean = true): string => { + return `${RISKY_HOSTS_INDEX_PREFIX}${onlyLatest ? 'latest_' : ''}${spaceId}`; }; diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/host_details_risk_tab.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/host_details_risk_tab.spec.ts new file mode 100644 index 000000000000000..8f17d69a0d35be0 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/host_details_risk_tab.spec.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loginAndWaitForHostDetailsPage } from '../../tasks/login'; + +import { cleanKibana } from '../../tasks/common'; +import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; +import { + navigateToHostRiskDetailTab, + openRiskFlyout, + waitForTableToLoad, +} from '../../tasks/host_risk'; +import { RULE_NAME, RISK_FLYOUT } from '../../screens/hosts/host_risk'; + +describe('risk tab', () => { + before(() => { + cleanKibana(); + esArchiverLoad('risky_hosts'); + loginAndWaitForHostDetailsPage('siem-kibana'); + navigateToHostRiskDetailTab(); + waitForTableToLoad(); + }); + + after(() => { + esArchiverUnload('risky_hosts'); + }); + + it('renders risk tab', () => { + cy.get(RULE_NAME).eq(3).should('have.text', 'Unusual Linux Username'); + }); + + it('shows risk information overlay when button is clicked', () => { + openRiskFlyout(); + cy.get(RISK_FLYOUT).should('have.text', 'How is host risk calculated?'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.spec.ts similarity index 100% rename from x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.ts rename to x-pack/plugins/security_solution/cypress/integration/hosts/hosts_risk_column.spec.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts index df194136c6bb2c3..be726f0323d48cd 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/fields_browser.spec.ts @@ -109,7 +109,7 @@ describe('Fields Browser', () => { filterFieldsBrowser(filterInput); - cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should('have.text', '4'); + cy.get(FIELDS_BROWSER_SELECTED_CATEGORY_COUNT).should('have.text', '5'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts new file mode 100644 index 000000000000000..00bd39b911fb811 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/screens/hosts/host_risk.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const RULE_NAME = '[data-test-subj="topHostScoreContributors"] .euiTableCellContent__text'; + +export const RISK_FLYOUT = '[data-test-subj="open-risk-information-flyout"] .euiFlyoutHeader'; + +export const RISK_DETAILS_NAV = '[data-test-subj="navigation-hostRisk"]'; + +export const RISK_FLYOUT_TRIGGER = '[data-test-subj="open-risk-information-flyout-trigger"]'; + +export const LOADING_TABLE = '.euiBasicTable-loading'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts new file mode 100644 index 000000000000000..7a357e8a5c7fb2c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/tasks/host_risk.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { LOADING_TABLE, RISK_DETAILS_NAV, RISK_FLYOUT_TRIGGER } from '../screens/hosts/host_risk'; + +export const navigateToHostRiskDetailTab = () => cy.get(RISK_DETAILS_NAV).click(); + +export const openRiskFlyout = () => cy.get(RISK_FLYOUT_TRIGGER).click(); + +export const waitForTableToLoad = () => { + cy.get(LOADING_TABLE).should('exist'); + cy.get(LOADING_TABLE).should('not.exist'); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/login.ts b/x-pack/plugins/security_solution/cypress/tasks/login.ts index 0610333352ce85e..ad6ad0486e5185f 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/login.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/login.ts @@ -334,8 +334,8 @@ export const loginAndWaitForTimeline = (timelineId: string, role?: ROLES) => { cy.get(TIMELINE_FLYOUT_BODY).should('be.visible'); }; -export const loginAndWaitForHostDetailsPage = () => { - loginAndWaitForPage(hostDetailsUrl('suricata-iowa')); +export const loginAndWaitForHostDetailsPage = (hostName = 'suricata-iowa') => { + loginAndWaitForPage(hostDetailsUrl(hostName)); cy.get('[data-test-subj="loading-spinner"]', { timeout: 12000 }).should('not.exist'); }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx index 9d60fbc496d8db0..945317036e7bcb7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx @@ -20,6 +20,7 @@ describe('HostRiskSummary', () => { isModuleEnabled: true, result: [ { + '@timestamp': '1641902481', host: { name: 'test-host-name', }, @@ -63,6 +64,7 @@ describe('HostRiskSummary', () => { isModuleEnabled: false, result: [ { + '@timestamp': '1641902530', host: { name: 'test-host-name', }, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index b123a26257683b6..d2a17e87cffcfa3 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -51,13 +51,14 @@ const hostName = 'siem-window'; describe('Table Navigation', () => { const mockHasMlUserPermissions = true; + const mockRiskyHostEnabled = true; const mockProps: TabNavigationProps & RouteSpyState = { pageName: 'hosts', pathName: '/hosts', detailName: undefined, search: '', tabName: HostsTableType.authentications, - navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions), + navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions, mockRiskyHostEnabled), [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: { diff --git a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/types.ts b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/types.ts new file mode 100644 index 000000000000000..4a231b38ce61268 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/types.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const enum HostRiskScoreQueryId { + DEFAULT = 'HostRiskScore', + HOST_RISK_SCORE_OVER_TIME = 'HostRiskScoreOverTimeQuery', + TOP_HOST_SCORE_CONTRIBUTORS = 'TopHostScoreContributorsQuery', + OVERVIEW_RISKY_HOSTS = 'OverviewRiskyHosts', +} diff --git a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts index debdacb570ad05e..3c47c4d02b358c1 100644 --- a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts +++ b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts @@ -8,17 +8,16 @@ import { i18n } from '@kbn/i18n'; import { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; - import { useAppToasts } from '../../hooks/use_app_toasts'; import { useKibana } from '../../lib/kibana'; import { inputsActions } from '../../store/actions'; import { isIndexNotFoundError } from '../../utils/exceptions'; -import { getHostRiskIndex, HostsRiskScore } from '../../../../common/search_strategy'; +import { Direction, getHostRiskIndex, HostsRiskScore } from '../../../../common/search_strategy'; import { useHostsRiskScoreComplete } from './use_hosts_risk_score_complete'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; +import { HostRiskScoreQueryId } from './types'; -export const QUERY_ID = 'host_risk_score'; const noop = () => {}; const isRecord = (item: unknown): item is Record => @@ -37,12 +36,23 @@ export interface HostRisk { result?: HostsRiskScore[]; } +/** + * @param queryId Provide this parameter when using query inspector to identify the query. + */ export const useHostsRiskScore = ({ timerange, hostName, + onlyLatest = true, + queryId = HostRiskScoreQueryId.DEFAULT, + sortOrder, + limit, }: { timerange?: { to: string; from: string }; hostName?: string; + onlyLatest?: boolean; + queryId?: HostRiskScoreQueryId; + limit?: number; + sortOrder?: Direction; }): HostRisk | null => { const riskyHostsFeatureEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); const [isModuleEnabled, setIsModuleEnabled] = useState(undefined); @@ -56,8 +66,8 @@ export const useHostsRiskScore = ({ const { error, result, start, loading: isHostsRiskScoreLoading } = useHostsRiskScoreComplete(); const deleteQuery = useCallback(() => { - dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: QUERY_ID })); - }, [dispatch]); + dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: queryId })); + }, [dispatch, queryId]); useEffect(() => { if (!isHostsRiskScoreLoading && result) { @@ -66,7 +76,7 @@ export const useHostsRiskScore = ({ dispatch( inputsActions.setQuery({ inputId: 'global', - id: QUERY_ID, + id: queryId, inspect: { dsl: result.inspect?.dsl ?? [], response: [JSON.stringify(result.rawResponse, null, 2)], @@ -77,7 +87,7 @@ export const useHostsRiskScore = ({ ); } return deleteQuery; - }, [deleteQuery, dispatch, isHostsRiskScoreLoading, result, setIsModuleEnabled]); + }, [deleteQuery, dispatch, isHostsRiskScoreLoading, result, setIsModuleEnabled, queryId]); useEffect(() => { if (error) { @@ -105,11 +115,24 @@ export const useHostsRiskScore = ({ ? { to: timerange.to, from: timerange.from, interval: '' } : undefined, hostNames: hostName ? [hostName] : undefined, - defaultIndex: [getHostRiskIndex(space.id)], + defaultIndex: [getHostRiskIndex(space.id, onlyLatest)], + onlyLatest, + sortOrder, + limit, }); }); } - }, [start, data, timerange, hostName, riskyHostsFeatureEnabled, spaces]); + }, [ + start, + data, + timerange, + hostName, + onlyLatest, + riskyHostsFeatureEnabled, + spaces, + sortOrder, + limit, + ]); if ((!hostName && !timerange) || !riskyHostsFeatureEnabled) { return null; diff --git a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts index 6faaa3c8f08dbf8..2531d533d830bf0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts +++ b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts @@ -30,6 +30,8 @@ export const getHostsRiskScore = ({ timerange, hostNames, signal, + limit, + sortOrder, }: GetHostsRiskScoreProps): Observable => data.search.search( { @@ -37,6 +39,8 @@ export const getHostsRiskScore = ({ factoryQueryType: HostsQueries.hostsRiskScore, timerange, hostNames, + limit, + sortOrder, }, { strategy: 'securitySolutionSearchStrategy', diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 2d804392580d0ae..cf7d9f1dae23e6f 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -83,6 +83,7 @@ export const mockGlobalState: State = { uncommonProcesses: { activePage: 0, limit: 10 }, anomalies: null, externalAlerts: { activePage: 0, limit: 10 }, + hostRisk: null, }, }, details: { @@ -98,6 +99,7 @@ export const mockGlobalState: State = { uncommonProcesses: { activePage: 0, limit: 10 }, anomalies: null, externalAlerts: { activePage: 0, limit: 10 }, + hostRisk: null, }, }, }, diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.test.tsx index 09d0375cf7dffb3..7f5e512978ee9c9 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.test.tsx @@ -7,25 +7,35 @@ import { render, fireEvent } from '@testing-library/react'; import React from 'react'; -import { HostRiskInformation } from '.'; +import { HostRiskInformationButtonIcon, HostRiskInformationButtonEmpty } from '.'; import { TestProviders } from '../../../common/mock'; describe('Host Risk Flyout', () => { - it('renders', () => { - const { queryByTestId } = render(); + describe('HostRiskInformationButtonIcon', () => { + it('renders', () => { + const { queryByTestId } = render(); - expect(queryByTestId('open-risk-information-flyout')).toBeInTheDocument(); + expect(queryByTestId('open-risk-information-flyout-trigger')).toBeInTheDocument(); + }); + }); + + describe('HostRiskInformationButtonEmpty', () => { + it('renders', () => { + const { queryByTestId } = render(); + + expect(queryByTestId('open-risk-information-flyout-trigger')).toBeInTheDocument(); + }); }); it('opens and displays table with 5 rows', () => { const NUMBER_OF_ROWS = 1 + 5; // 1 header row + 5 severity rows const { getByTestId, queryByTestId, queryAllByRole } = render( - + ); - fireEvent.click(getByTestId('open-risk-information-flyout')); + fireEvent.click(getByTestId('open-risk-information-flyout-trigger')); expect(queryByTestId('risk-information-table')).toBeInTheDocument(); expect(queryAllByRole('row')).toHaveLength(NUMBER_OF_ROWS); diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.tsx index b4632466672e28b..00230b0a4d27862 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/index.tsx @@ -21,10 +21,11 @@ import { EuiButton, EuiSpacer, EuiBasicTableColumn, + EuiButtonEmpty, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, memo } from 'react'; import { HostRiskSeverity } from '../../../../common/search_strategy'; import { RISKY_HOSTS_DOC_LINK } from '../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module'; import { HostRiskScore } from '../common/host_risk_score'; @@ -61,16 +62,8 @@ const tableItems: TableItem[] = [ export const HOST_RISK_INFO_BUTTON_CLASS = 'HostRiskInformation__button'; -export const HostRiskInformation = () => { - const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); - - const handleOnClose = useCallback(() => { - setIsFlyoutVisible(false); - }, []); - - const handleOnOpen = useCallback(() => { - setIsFlyoutVisible(true); - }, []); +export const HostRiskInformationButtonIcon = memo(() => { + const [isFlyoutVisible, handleOnOpen, handleOnClose] = useOnOpenCloseHandler(); return ( <> @@ -81,11 +74,39 @@ export const HostRiskInformation = () => { aria-label={i18n.INFORMATION_ARIA_LABEL} onClick={handleOnOpen} className={HOST_RISK_INFO_BUTTON_CLASS} - data-test-subj="open-risk-information-flyout" + data-test-subj="open-risk-information-flyout-trigger" /> {isFlyoutVisible && } ); +}); +HostRiskInformationButtonIcon.displayName = 'HostRiskInformationButtonIcon'; + +export const HostRiskInformationButtonEmpty = memo(() => { + const [isFlyoutVisible, handleOnOpen, handleOnClose] = useOnOpenCloseHandler(); + + return ( + <> + + {i18n.INFO_BUTTON_TEXT} + + {isFlyoutVisible && } + + ); +}); +HostRiskInformationButtonEmpty.displayName = 'HostRiskInformationButtonEmpty'; + +const useOnOpenCloseHandler = (): [boolean, () => void, () => void] => { + const [isOpen, setIsOpen] = useState(false); + + const handleOnClose = useCallback(() => { + setIsOpen(false); + }, []); + + const handleOnOpen = useCallback(() => { + setIsOpen(true); + }, []); + return [isOpen, handleOnOpen, handleOnClose]; }; const HostRiskInformationFlyout = ({ handleOnClose }: { handleOnClose: () => void }) => { @@ -94,7 +115,13 @@ const HostRiskInformationFlyout = ({ handleOnClose }: { handleOnClose: () => voi }); return ( - +

{i18n.TITLE}

diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/translations.ts index 244c7b458b20605..1e031a84ae8a50c 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/host_risk_information/translations.ts @@ -68,3 +68,10 @@ export const CLOSE_BUTTON_LTEXT = i18n.translate( defaultMessage: 'Close', } ); + +export const INFO_BUTTON_TEXT = i18n.translate( + 'xpack.securitySolution.hosts.hostRiskInformation.buttonLabel', + { + defaultMessage: 'How is risk score calculated?', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx new file mode 100644 index 000000000000000..9a7dfcc967fbb42 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { HostRiskScoreOverTime } from '.'; +import { TestProviders } from '../../../common/mock'; +import { useHostsRiskScore } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; + +jest.mock('../../../common/containers/hosts_risk/use_hosts_risk_score'); +const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; + +describe('Host Risk Flyout', () => { + it('renders', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('hostRiskScoreOverTime')).toBeInTheDocument(); + }); + + it('renders loader when HostsRiskScore is laoding', () => { + useHostsRiskScoreMock.mockReturnValueOnce({ + loading: true, + isModuleEnabled: true, + result: [], + }); + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('HostRiskScoreOverTime-loading')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx new file mode 100644 index 000000000000000..eb34f9100101b31 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/index.tsx @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useCallback } from 'react'; +import { + Chart, + LineSeries, + ScaleType, + Settings, + Axis, + Position, + AnnotationDomainType, + LineAnnotation, + TooltipValue, +} from '@elastic/charts'; +import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingChart, EuiText, EuiPanel } from '@elastic/eui'; +import styled from 'styled-components'; +import { chartDefaultSettings, useTheme } from '../../../common/components/charts/common'; +import { useTimeZone } from '../../../common/lib/kibana'; +import { histogramDateTimeFormatter } from '../../../common/components/utils'; +import { HeaderSection } from '../../../common/components/header_section'; +import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; +import * as i18n from './translations'; +import { useHostsRiskScore } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; +import { PreferenceFormattedDate } from '../../../common/components/formatted_date'; +import { HostRiskScoreQueryId } from '../../../common/containers/hosts_risk/types'; + +export interface HostRiskScoreOverTimeProps { + hostName: string; + from: string; + to: string; +} + +const RISKY_TRESHOULD = 70; +const DEFAULT_CHART_HEIGH = 250; + +const StyledEuiText = styled(EuiText)` + font-size: 9px; + font-weight: ${({ theme }) => theme.eui.euiFontWeightSemiBold}; + margin-right: ${({ theme }) => theme.eui.paddingSizes.xs}; +`; + +const LoadingChart = styled(EuiLoadingChart)` + display: block; + text-align: center; +`; + +const HostRiskScoreOverTimeComponent: React.FC = ({ + hostName, + from, + to, +}) => { + const timeZone = useTimeZone(); + + const memoizedDataTimeFormatter = useMemo( + () => histogramDateTimeFormatter([from, to]), + [from, to] + ); + const scoreFormatter = useCallback((d: number) => Math.round(d).toString(), []); + const headerFormatter = useCallback( + (tooltip: TooltipValue) => , + [] + ); + + const timerange = useMemo( + () => ({ + from, + to, + }), + [from, to] + ); + const theme = useTheme(); + + const hostRisk = useHostsRiskScore({ + hostName, + onlyLatest: false, + timerange, + queryId: HostRiskScoreQueryId.HOST_RISK_SCORE_OVER_TIME, + }); + + const data = useMemo( + () => + hostRisk?.result + ?.map((result) => ({ + x: result['@timestamp'], + y: result.risk_stats.risk_score, + })) + .reverse() ?? [], + [hostRisk] + ); + + return ( + + + + + + + + + + + + + + +
+ {hostRisk?.loading ? ( + + ) : ( + + + + + + + {i18n.RISKY} + + } + /> + + )} +
+
+
+
+
+ ); +}; + +HostRiskScoreOverTimeComponent.displayName = 'HostRiskScoreOverTimeComponent'; +export const HostRiskScoreOverTime = React.memo(HostRiskScoreOverTimeComponent); +HostRiskScoreOverTime.displayName = 'HostRiskScoreOverTime'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts new file mode 100644 index 000000000000000..5e1b4ca7410a835 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/host_score_over_time/translations.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const HOST_RISK_SCORE_OVER_TIME = i18n.translate( + 'xpack.securitySolution.hosts.hostScoreOverTime.title', + { + defaultMessage: 'Host risk score over time', + } +); + +export const HOST_RISK_THRESHOLD = i18n.translate( + 'xpack.securitySolution.hosts.hostScoreOverTime.riskyThresholdHeader', + { + defaultMessage: 'Risky threshold', + } +); + +export const RISKY = i18n.translate('xpack.securitySolution.hosts.hostScoreOverTime.riskyLabel', { + defaultMessage: 'Risky', +}); + +export const RISK_SCORE = i18n.translate( + 'xpack.securitySolution.hosts.hostScoreOverTime.riskScore', + { + defaultMessage: 'Risk score', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx index f882e12d211d39b..9e7e01c64a43253 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx @@ -33,7 +33,10 @@ import { import { useInspectQuery } from '../../../../common/hooks/use_inspect_query'; import { useErrorToast } from '../../../../common/hooks/use_error_toast'; import { HostRiskScore } from '../../common/host_risk_score'; -import { HostRiskInformation, HOST_RISK_INFO_BUTTON_CLASS } from '../../host_risk_information'; +import { + HostRiskInformationButtonIcon, + HOST_RISK_INFO_BUTTON_CLASS, +} from '../../host_risk_information'; import { HoverVisibilityContainer } from '../../../../common/components/hover_visibility_container'; const QUERY_ID = 'hostsKpiRiskyHostsQuery'; @@ -80,7 +83,7 @@ const RiskyHostsComponent: React.FC<{ - + {data?.inspect && ( diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx new file mode 100644 index 000000000000000..6315897fac1d2f4 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import { TopHostScoreContributors } from '.'; +import { TestProviders } from '../../../common/mock'; +import { useHostsRiskScore } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; + +jest.mock('../../../common/containers/hosts_risk/use_hosts_risk_score'); +const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; + +describe('Host Risk Flyout', () => { + it('renders', () => { + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('topHostScoreContributors')).toBeInTheDocument(); + }); + + it('renders sorted items', () => { + useHostsRiskScoreMock.mockReturnValueOnce({ + loading: true, + isModuleEnabled: true, + result: [ + { + risk_stats: { + rule_risks: [ + { + rule_name: 'third', + rule_risk: '10', + }, + { + rule_name: 'first', + rule_risk: '99', + }, + { + rule_name: 'second', + rule_risk: '55', + }, + ], + }, + }, + ], + }); + + const { queryAllByRole } = render( + + + + ); + + expect(queryAllByRole('row')[1]).toHaveTextContent('first'); + expect(queryAllByRole('row')[2]).toHaveTextContent('second'); + expect(queryAllByRole('row')[3]).toHaveTextContent('third'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx new file mode 100644 index 000000000000000..84892da3b6e3cb3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/index.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiInMemoryTable, + EuiTableFieldDataColumnType, +} from '@elastic/eui'; + +import { HeaderSection } from '../../../common/components/header_section'; +import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; +import * as i18n from './translations'; +import { useHostsRiskScore } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; +import { Direction } from '../../../../../timelines/common'; +import { HostRiskScoreQueryId } from '../../../common/containers/hosts_risk/types'; + +export interface TopHostScoreContributorsProps { + hostName: string; + from: string; + to: string; +} + +interface TableItem { + rank: number; + name: string; +} + +const columns: Array> = [ + { + name: i18n.RANK_TITLE, + field: 'rank', + width: '45px', + align: 'right', + }, + { + name: i18n.RULE_NAME_TITLE, + field: 'name', + sortable: true, + truncateText: true, + }, +]; + +const PAGE_SIZE = 5; + +const TopHostScoreContributorsComponent: React.FC = ({ + hostName, + from, + to, +}) => { + const timerange = useMemo( + () => ({ + from, + to, + }), + [from, to] + ); + + const hostRisk = useHostsRiskScore({ + hostName, + timerange, + onlyLatest: false, + queryId: HostRiskScoreQueryId.TOP_HOST_SCORE_CONTRIBUTORS, + limit: 1, + sortOrder: Direction.desc, + }); + + const result = hostRisk?.result; + + const items = useMemo(() => { + const rules = result && result.length > 0 ? result[0].risk_stats.rule_risks : []; + return rules + .sort((a, b) => b.rule_risk - a.rule_risk) + .map(({ rule_name: name }, i) => ({ rank: i + 1, name })); + }, [result]); + + const pagination = useMemo( + () => ({ + hidePerPageOptions: true, + pageSize: PAGE_SIZE, + totalItemCount: items.length, + }), + [items.length] + ); + + return ( + + + + + + + + + + + + + + + + + + + + ); +}; + +export const TopHostScoreContributors = React.memo(TopHostScoreContributorsComponent); +TopHostScoreContributors.displayName = 'TopHostScoreContributors'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/translations.ts new file mode 100644 index 000000000000000..02017bf33d2da0a --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/top_host_score_contributors/translations.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const TOP_RISK_SCORE_CONTRIBUTORS = i18n.translate( + 'xpack.securitySolution.hosts.topRiskScoreContributors.title', + { + defaultMessage: 'Top risk score contributors', + } +); + +export const RANK_TITLE = i18n.translate( + 'xpack.securitySolution.hosts.topRiskScoreContributors.rankColumnTitle', + { + defaultMessage: 'Rank', + } +); + +export const RULE_NAME_TITLE = i18n.translate( + 'xpack.securitySolution.hosts.topRiskScoreContributors.ruleNameColumnTitle', + { + defaultMessage: 'Rule name', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx index dc537f2f6ffe35c..891db470161d426 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/details_tabs.tsx @@ -25,6 +25,7 @@ import { UncommonProcessQueryTabBody, EventsQueryTabBody, HostAlertsQueryTabBody, + HostRiskTabBody, } from '../navigation'; export const HostDetailsTabs = React.memo( @@ -102,6 +103,9 @@ export const HostDetailsTabs = React.memo( + + + ); } diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 2f89efa56b4d069..40ba3990ee9a7ec 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -53,6 +53,7 @@ import { ID, useHostDetails } from '../../containers/hosts/details'; import { manageQuery } from '../../../common/components/page/manage_query'; import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; const HostOverviewManage = manageQuery(HostOverview); @@ -119,6 +120,8 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta dispatch(setHostDetailsTablesActivePageToZero()); }, [dispatch, detailName]); + const riskyHostsFeatureEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); + return ( <> {indicesExist ? ( @@ -195,7 +198,11 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.test.tsx index 872afc4e82440d8..90f3c223c5501f9 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.test.tsx @@ -11,18 +11,29 @@ import { navTabsHostDetails } from './nav_tabs'; describe('navTabsHostDetails', () => { const mockHostName = 'mockHostName'; test('it should skip anomalies tab if without mlUserPermission', () => { - const tabs = navTabsHostDetails(mockHostName, false); + const tabs = navTabsHostDetails(mockHostName, false, false); expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); expect(tabs).not.toHaveProperty(HostsTableType.anomalies); expect(tabs).toHaveProperty(HostsTableType.events); + expect(tabs).not.toHaveProperty(HostsTableType.risk); }); test('it should display anomalies tab if with mlUserPermission', () => { - const tabs = navTabsHostDetails(mockHostName, true); + const tabs = navTabsHostDetails(mockHostName, true, false); expect(tabs).toHaveProperty(HostsTableType.authentications); expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); expect(tabs).toHaveProperty(HostsTableType.anomalies); expect(tabs).toHaveProperty(HostsTableType.events); + expect(tabs).not.toHaveProperty(HostsTableType.risk); + }); + + test('it should display risky hosts tab if when risky hosts is enabled', () => { + const tabs = navTabsHostDetails(mockHostName, false, true); + expect(tabs).toHaveProperty(HostsTableType.authentications); + expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); + expect(tabs).not.toHaveProperty(HostsTableType.anomalies); + expect(tabs).toHaveProperty(HostsTableType.events); + expect(tabs).toHaveProperty(HostsTableType.risk); }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx index 02f8fa740c024bf..c58fbde09aef1d4 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/nav_tabs.tsx @@ -16,8 +16,11 @@ const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => export const navTabsHostDetails = ( hostName: string, - hasMlUserPermissions: boolean + hasMlUserPermissions: boolean, + isRiskyHostsEnabled: boolean ): HostDetailsNavTab => { + const hiddenTabs = []; + const hostDetailsNavTabs = { [HostsTableType.authentications]: { id: HostsTableType.authentications, @@ -49,9 +52,21 @@ export const navTabsHostDetails = ( href: getTabsOnHostDetailsUrl(hostName, HostsTableType.alerts), disabled: false, }, + [HostsTableType.risk]: { + id: HostsTableType.risk, + name: i18n.NAVIGATION_HOST_RISK_TITLE, + href: getTabsOnHostDetailsUrl(hostName, HostsTableType.risk), + disabled: false, + }, }; - return hasMlUserPermissions - ? hostDetailsNavTabs - : omit(HostsTableType.anomalies, hostDetailsNavTabs); + if (!hasMlUserPermissions) { + hiddenTabs.push(HostsTableType.anomalies); + } + + if (!isRiskyHostsEnabled) { + hiddenTabs.push(HostsTableType.risk); + } + + return omit(hiddenTabs, hostDetailsNavTabs); }; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts index 3a584f7fefb5000..19975b6ad7abb71 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/utils.ts @@ -27,6 +27,7 @@ const TabNameMappedToI18nKey: Record = { [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, + [HostsTableType.risk]: i18n.NAVIGATION_HOST_RISK_TITLE, }; export const getBreadcrumbs = ( diff --git a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx index 23be8f09ce1405d..64acbbc666312e3 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/index.tsx @@ -30,6 +30,7 @@ const getHostDetailsTabPath = () => `${HostsTableType.uncommonProcesses}|` + `${HostsTableType.anomalies}|` + `${HostsTableType.events}|` + + `${HostsTableType.risk}|` + `${HostsTableType.alerts})`; export const HostsContainer = React.memo(() => { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx new file mode 100644 index 000000000000000..6982047aa26ada0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/host_risk_tab_body.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; +import { HostRiskScoreOverTime } from '../../components/host_score_over_time'; +import { TopHostScoreContributors } from '../../components/top_host_score_contributors'; +import { HostsComponentsQueryProps } from './types'; +import * as i18n from '../translations'; +import { useRiskyHostsDashboardButtonHref } from '../../../overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'; +import { HostRiskInformationButtonEmpty } from '../../components/host_risk_information'; + +const StyledEuiFlexGroup = styled(EuiFlexGroup)` + margin-top: ${({ theme }) => theme.eui.paddingSizes.l}; +`; + +const HostRiskTabBodyComponent: React.FC< + Pick & { hostName: string } +> = ({ hostName, startDate, endDate }) => { + const { buttonHref } = useRiskyHostsDashboardButtonHref(startDate, endDate); + + return ( + <> + + + + + + + + + + + + {i18n.VIEW_DASHBOARD_BUTTON} + + + + + + + + ); +}; + +HostRiskTabBodyComponent.displayName = 'HostRiskTabBodyComponent'; + +export const HostRiskTabBody = React.memo(HostRiskTabBodyComponent); + +HostRiskTabBody.displayName = 'HostRiskTabBody'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts b/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts index 09adfece7b01ed0..d5961cdc788e4db 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/index.ts @@ -10,3 +10,4 @@ export * from './events_query_tab_body'; export * from './hosts_query_tab_body'; export * from './uncommon_process_query_tab_body'; export * from './alerts_query_tab_body'; +export * from './host_risk_tab_body'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts index 5563dc285ad5a85..337f18ef335034a 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/pages/translations.ts @@ -57,6 +57,13 @@ export const NAVIGATION_ALERTS_TITLE = i18n.translate( } ); +export const NAVIGATION_HOST_RISK_TITLE = i18n.translate( + 'xpack.securitySolution.hosts.navigation.hostRisk', + { + defaultMessage: 'Host risk', + } +); + export const ERROR_FETCHING_AUTHENTICATIONS_DATA = i18n.translate( 'xpack.securitySolution.hosts.navigaton.matrixHistogram.errorFetchingAuthenticationsData', { @@ -76,3 +83,10 @@ export const EVENTS_UNIT = (totalCount: number) => values: { totalCount }, defaultMessage: `{totalCount, plural, =1 {event} other {events}}`, }); + +export const VIEW_DASHBOARD_BUTTON = i18n.translate( + 'xpack.securitySolution.hosts.navigaton.hostRisk.viewDashboardButtonLabel', + { + defaultMessage: 'View source dashboard', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts index 8c3a3e27ffb38bf..0d7df790f8f317c 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/helpers.test.ts @@ -36,6 +36,7 @@ export const mockHostsState: HostsModel = { activePage: 4, limit: DEFAULT_TABLE_LIMIT, }, + [HostsTableType.risk]: null, }, }, details: { @@ -63,6 +64,7 @@ export const mockHostsState: HostsModel = { activePage: 4, limit: DEFAULT_TABLE_LIMIT, }, + [HostsTableType.risk]: null, }, }, }; @@ -94,6 +96,7 @@ describe('Hosts redux store', () => { activePage: 0, limit: 10, }, + [HostsTableType.risk]: null, }); }); @@ -122,6 +125,7 @@ describe('Hosts redux store', () => { activePage: 0, limit: 10, }, + [HostsTableType.risk]: null, }); }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/store/model.ts b/x-pack/plugins/security_solution/public/hosts/store/model.ts index ea168e965fa2335..0a82d3961aa0207 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/model.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/model.ts @@ -20,6 +20,7 @@ export enum HostsTableType { uncommonProcesses = 'uncommonProcesses', anomalies = 'anomalies', alerts = 'externalAlerts', + risk = 'hostRisk', } export interface BasicQueryPaginated { @@ -39,6 +40,7 @@ export interface Queries { [HostsTableType.uncommonProcesses]: BasicQueryPaginated; [HostsTableType.anomalies]: null | undefined; [HostsTableType.alerts]: BasicQueryPaginated; + [HostsTableType.risk]: null | undefined; } export interface GenericHostsModel { diff --git a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts index eebf3ca1684a1de..171431144a746b0 100644 --- a/x-pack/plugins/security_solution/public/hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/hosts/store/reducer.ts @@ -51,6 +51,7 @@ export const initialHostsState: HostsState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, }, + [HostsTableType.risk]: null, }, }, details: { @@ -78,6 +79,7 @@ export const initialHostsState: HostsState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, }, + [HostsTableType.risk]: null, }, }, }; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx index 64829aab7776d0a..add96986d1d079b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx @@ -10,12 +10,16 @@ import React from 'react'; import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module'; import { RiskyHostsDisabledModule } from './risky_hosts_disabled_module'; import { useHostsRiskScore } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; +import { HostRiskScoreQueryId } from '../../../common/containers/hosts_risk/types'; export interface RiskyHostLinksProps { timerange: { to: string; from: string }; } const RiskyHostLinksComponent: React.FC = ({ timerange }) => { - const hostRiskScore = useHostsRiskScore({ timerange }); + const hostRiskScore = useHostsRiskScore({ + timerange, + queryId: HostRiskScoreQueryId.OVERVIEW_RISKY_HOSTS, + }); switch (hostRiskScore?.isModuleEnabled) { case true: diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx index 364b608c6086deb..bc45faf1f05805d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx @@ -57,6 +57,7 @@ describe('RiskyHostsEnabledModule', () => { isModuleEnabled: true, result: [ { + '@timestamp': '1641902481', host: { name: 'a', }, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx index 8a42cedc3be461d..f508da6c1c99119 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx @@ -14,8 +14,8 @@ import { LinkPanelViewProps } from '../link_panel/types'; import { Link } from '../link_panel/link'; import * as i18n from './translations'; import { VIEW_DASHBOARD } from '../overview_cti_links/translations'; -import { QUERY_ID as RiskyHostsQueryId } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; import { NavigateToHost } from './navigate_to_host'; +import { HostRiskScoreQueryId } from '../../../common/containers/hosts_risk/types'; const columns: Array> = [ { @@ -94,7 +94,7 @@ export const RiskyHostsPanelView: React.FC = ({ dataTestSubj: 'risky-hosts-dashboard-links', defaultSortField: 'count', defaultSortOrder: 'desc', - inspectQueryId: isInspectEnabled ? RiskyHostsQueryId : undefined, + inspectQueryId: isInspectEnabled ? HostRiskScoreQueryId.OVERVIEW_RISKY_HOSTS : undefined, listItems, panelTitle: i18n.PANEL_TITLE, splitPanel: splitPanelElement, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json index bf980e370e3a38c..cf03a90dfe72b79 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json @@ -3,7 +3,7 @@ "interval": "1m", "actions": [ { - "id": "1fa31c30-3046-11ec-8971-1f3f7bae65af", + "id": "0cae9900-6e54-11ec-a124-bfe603780ab8", "group": "default", "params": { "message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/query.hosts_kpi_risky_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/query.hosts_kpi_risky_hosts.dsl.ts index 201d73c4ebb1852..7c00b7c0f8193b2 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/query.hosts_kpi_risky_hosts.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/query.hosts_kpi_risky_hosts.dsl.ts @@ -6,15 +6,12 @@ */ import type { HostsKpiRiskyHostsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/kpi/risky_hosts'; -import { createQueryFilterClauses } from '../../../../../../utils/build_query'; export const buildHostsKpiRiskyHostsQuery = ({ - filterQuery, timerange: { from, to }, defaultIndex, }: HostsKpiRiskyHostsRequestOptions) => { const filter = [ - ...createQueryFilterClauses(filterQuery), { range: { '@timestamp': { diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts index 182ad7892204f86..dc3d1e4b5d587c2 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/risk_score/query.hosts_risk.dsl.ts @@ -5,12 +5,16 @@ * 2.0. */ -import { HostsRiskScoreRequestOptions } from '../../../../../../common/search_strategy'; +import { Direction, HostsRiskScoreRequestOptions } from '../../../../../../common/search_strategy'; + +const QUERY_SIZE = 10; export const buildHostsRiskScoreQuery = ({ timerange, hostNames, defaultIndex, + limit = QUERY_SIZE, + sortOrder = Direction.desc, }: HostsRiskScoreRequestOptions) => { const filter = []; @@ -35,12 +39,20 @@ export const buildHostsRiskScoreQuery = ({ allow_no_indices: false, ignore_unavailable: true, track_total_hits: false, + size: limit, body: { query: { bool: { filter, }, }, + sort: [ + { + '@timestamp': { + order: sortOrder, + }, + }, + ], }, }; diff --git a/x-pack/plugins/security_solution/server/usage/collector.ts b/x-pack/plugins/security_solution/server/usage/collector.ts index d091c2a6e5e8ebd..4530dac725c7b14 100644 --- a/x-pack/plugins/security_solution/server/usage/collector.ts +++ b/x-pack/plugins/security_solution/server/usage/collector.ts @@ -10,6 +10,8 @@ import { CollectorFetchContext } from '../../../../../src/plugins/usage_collecti import { CollectorDependencies } from './types'; import { fetchDetectionsMetrics } from './detections'; import { SAVED_OBJECT_TYPES } from '../../../cases/common/constants'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleActionsSavedObjectType } from '../lib/detection_engine/rule_actions/legacy_saved_object_mappings'; export type RegisterCollector = (deps: CollectorDependencies) => void; export interface UsageData { @@ -19,7 +21,11 @@ export interface UsageData { export async function getInternalSavedObjectsClient(core: CoreSetup) { return core.getStartServices().then(async ([coreStart]) => { // note: we include the "cases" and "alert" hidden types here otherwise we would not be able to query them. If at some point cases and alert is not considered a hidden type this can be removed - return coreStart.savedObjects.createInternalRepository(['alert', ...SAVED_OBJECT_TYPES]); + return coreStart.savedObjects.createInternalRepository([ + 'alert', + legacyRuleActionsSavedObjectType, + ...SAVED_OBJECT_TYPES, + ]); }); } @@ -51,6 +57,22 @@ export const registerCollector: RegisterCollector = ({ type: 'long', _meta: { description: 'Number of cases attached to query detection rule alerts' }, }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, }, threshold: { enabled: { @@ -71,6 +93,22 @@ export const registerCollector: RegisterCollector = ({ description: 'Number of cases attached to threshold detection rule alerts', }, }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, }, eql: { enabled: { type: 'long', _meta: { description: 'Number of eql rules enabled' } }, @@ -83,6 +121,22 @@ export const registerCollector: RegisterCollector = ({ type: 'long', _meta: { description: 'Number of cases attached to eql detection rule alerts' }, }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, }, machine_learning: { enabled: { @@ -103,6 +157,22 @@ export const registerCollector: RegisterCollector = ({ description: 'Number of cases attached to machine_learning detection rule alerts', }, }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, }, threat_match: { enabled: { @@ -123,11 +193,21 @@ export const registerCollector: RegisterCollector = ({ description: 'Number of cases attached to threat_match detection rule alerts', }, }, - }, - legacy_notifications: { - total: { + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { type: 'long', - _meta: { description: 'Number of legacy notifications still in use' }, + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, }, }, elastic_total: { @@ -144,6 +224,22 @@ export const registerCollector: RegisterCollector = ({ type: 'long', _meta: { description: 'Number of cases attached to elastic detection rule alerts' }, }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, }, custom_total: { enabled: { type: 'long', _meta: { description: 'Number of custom rules enabled' } }, @@ -156,6 +252,22 @@ export const registerCollector: RegisterCollector = ({ type: 'long', _meta: { description: 'Number of cases attached to custom detection rule alerts' }, }, + legacy_notifications_enabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications enabled' }, + }, + legacy_notifications_disabled: { + type: 'long', + _meta: { description: 'Number of legacy notifications disabled' }, + }, + notifications_enabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, + notifications_disabled: { + type: 'long', + _meta: { description: 'Number of notifications enabled' }, + }, }, }, detection_rule_detail: { @@ -198,6 +310,14 @@ export const registerCollector: RegisterCollector = ({ type: 'long', _meta: { description: 'The number of total cases generated by a rule' }, }, + has_legacy_notification: { + type: 'boolean', + _meta: { description: 'True if this rule has a legacy notification' }, + }, + has_notification: { + type: 'boolean', + _meta: { description: 'True if this rule has a notification' }, + }, }, }, }, diff --git a/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.test.ts index 3c35296bafb46ce..c19e7b18f9e72c3 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.test.ts @@ -8,13 +8,25 @@ import { initialDetectionRulesUsage, updateDetectionRuleUsage } from './detection_rule_helpers'; import { DetectionRuleMetric, DetectionRulesTypeUsage } from './types'; -const createStubRule = ( - ruleType: string, - enabled: boolean, - elasticRule: boolean, - alertCount: number, - caseCount: number -): DetectionRuleMetric => ({ +interface StubRuleOptions { + ruleType: string; + enabled: boolean; + elasticRule: boolean; + alertCount: number; + caseCount: number; + hasLegacyNotification: boolean; + hasNotification: boolean; +} + +const createStubRule = ({ + ruleType, + enabled, + elasticRule, + alertCount, + caseCount, + hasLegacyNotification, + hasNotification, +}: StubRuleOptions): DetectionRuleMetric => ({ rule_name: 'rule-name', rule_id: 'id-123', rule_type: ruleType, @@ -25,12 +37,22 @@ const createStubRule = ( updated_on: '2022-01-06T20:02:45.306Z', alert_count_daily: alertCount, cases_count_total: caseCount, + has_legacy_notification: hasLegacyNotification, + has_notification: hasNotification, }); describe('Detections Usage and Metrics', () => { describe('Update metrics with rule information', () => { it('Should update elastic and eql rule metric total', async () => { - const stubRule = createStubRule('eql', true, true, 1, 1); + const stubRule = createStubRule({ + ruleType: 'eql', + enabled: true, + elasticRule: true, + alertCount: 1, + caseCount: 1, + hasLegacyNotification: false, + hasNotification: false, + }); const usage = updateDetectionRuleUsage(stubRule, initialDetectionRulesUsage); expect(usage).toEqual({ @@ -40,22 +62,70 @@ describe('Detections Usage and Metrics', () => { cases: 1, disabled: 0, enabled: 1, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, eql: { alerts: 1, cases: 1, disabled: 0, enabled: 1, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, }); }); it('Should update based on multiple metrics', async () => { - const stubEqlRule = createStubRule('eql', true, true, 1, 1); - const stubQueryRuleOne = createStubRule('query', true, true, 5, 2); - const stubQueryRuleTwo = createStubRule('query', true, false, 5, 2); - const stubMachineLearningOne = createStubRule('machine_learning', false, false, 0, 10); - const stubMachineLearningTwo = createStubRule('machine_learning', true, true, 22, 44); + const stubEqlRule = createStubRule({ + ruleType: 'eql', + enabled: true, + elasticRule: true, + alertCount: 1, + caseCount: 1, + hasLegacyNotification: false, + hasNotification: false, + }); + const stubQueryRuleOne = createStubRule({ + ruleType: 'query', + enabled: true, + elasticRule: true, + alertCount: 5, + caseCount: 2, + hasLegacyNotification: false, + hasNotification: false, + }); + const stubQueryRuleTwo = createStubRule({ + ruleType: 'query', + enabled: true, + elasticRule: false, + alertCount: 5, + caseCount: 2, + hasLegacyNotification: false, + hasNotification: false, + }); + const stubMachineLearningOne = createStubRule({ + ruleType: 'machine_learning', + enabled: false, + elasticRule: false, + alertCount: 0, + caseCount: 10, + hasLegacyNotification: false, + hasNotification: false, + }); + const stubMachineLearningTwo = createStubRule({ + ruleType: 'machine_learning', + enabled: true, + elasticRule: true, + alertCount: 22, + caseCount: 44, + hasLegacyNotification: false, + hasNotification: false, + }); let usage = updateDetectionRuleUsage(stubEqlRule, initialDetectionRulesUsage); usage = updateDetectionRuleUsage(stubQueryRuleOne, usage); @@ -70,32 +140,152 @@ describe('Detections Usage and Metrics', () => { cases: 12, disabled: 1, enabled: 1, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, elastic_total: { alerts: 28, cases: 47, disabled: 0, enabled: 3, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, eql: { alerts: 1, cases: 1, disabled: 0, enabled: 1, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, machine_learning: { alerts: 22, cases: 54, disabled: 1, enabled: 1, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, query: { alerts: 10, cases: 4, disabled: 0, enabled: 2, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, }); }); + + describe('table tests of "ruleType", "enabled", "elasticRule", and "legacyNotification"', () => { + test.each` + ruleType | enabled | hasLegacyNotification | hasNotification | expectedLegacyNotificationsEnabled | expectedLegacyNotificationsDisabled | expectedNotificationsEnabled | expectedNotificationsDisabled + ${'eql'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} + ${'eql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'eql'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} + ${'eql'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'eql'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} + ${'eql'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} + ${'query'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} + ${'query'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'query'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} + ${'query'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'query'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} + ${'query'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} + ${'threshold'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} + ${'threshold'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'threshold'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} + ${'threshold'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'threshold'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} + ${'threshold'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} + ${'machine_learning'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} + ${'machine_learning'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'machine_learning'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} + ${'machine_learning'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'machine_learning'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} + ${'machine_learning'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} + ${'threat_match'} | ${true} | ${true} | ${false} | ${1} | ${0} | ${0} | ${0} + ${'threat_match'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'threat_match'} | ${false} | ${false} | ${true} | ${0} | ${0} | ${0} | ${1} + ${'threat_match'} | ${true} | ${false} | ${true} | ${0} | ${0} | ${1} | ${0} + ${'threat_match'} | ${false} | ${true} | ${false} | ${0} | ${1} | ${0} | ${0} + ${'threat_match'} | ${false} | ${false} | ${false} | ${0} | ${0} | ${0} | ${0} + `( + 'expect { "ruleType": $ruleType, "enabled": $enabled, "hasLegacyNotification": $hasLegacyNotification, "hasNotification": $hasNotification } to equal { legacy_notifications_enabled: $expectedLegacyNotificationsEnabled, legacy_notifications_disabled: $expectedLegacyNotificationsDisabled, notifications_enabled: $expectedNotificationsEnabled, notifications_disabled, $expectedNotificationsDisabled }', + ({ + ruleType, + enabled, + hasLegacyNotification, + hasNotification, + expectedLegacyNotificationsEnabled, + expectedLegacyNotificationsDisabled, + expectedNotificationsEnabled, + expectedNotificationsDisabled, + }) => { + const rule1 = createStubRule({ + ruleType, + enabled, + elasticRule: false, + hasLegacyNotification, + hasNotification, + alertCount: 0, + caseCount: 0, + }); + const usage = updateDetectionRuleUsage(rule1, initialDetectionRulesUsage) as ReturnType< + typeof updateDetectionRuleUsage + > & { [key: string]: unknown }; + expect(usage[ruleType]).toEqual( + expect.objectContaining({ + legacy_notifications_enabled: expectedLegacyNotificationsEnabled, + legacy_notifications_disabled: expectedLegacyNotificationsDisabled, + notifications_enabled: expectedNotificationsEnabled, + notifications_disabled: expectedNotificationsDisabled, + }) + ); + + // extra test where we add everything by 1 to ensure that the addition happens with the correct rule type + const rule2 = createStubRule({ + ruleType, + enabled, + elasticRule: false, + hasLegacyNotification, + hasNotification, + alertCount: 0, + caseCount: 0, + }); + const usageAddedByOne = updateDetectionRuleUsage(rule2, usage) as ReturnType< + typeof updateDetectionRuleUsage + > & { [key: string]: unknown }; + + expect(usageAddedByOne[ruleType]).toEqual( + expect.objectContaining({ + legacy_notifications_enabled: + expectedLegacyNotificationsEnabled !== 0 + ? expectedLegacyNotificationsEnabled + 1 + : 0, + legacy_notifications_disabled: + expectedLegacyNotificationsDisabled !== 0 + ? expectedLegacyNotificationsDisabled + 1 + : 0, + notifications_enabled: + expectedNotificationsEnabled !== 0 ? expectedNotificationsEnabled + 1 : 0, + notifications_disabled: + expectedNotificationsDisabled !== 0 ? expectedNotificationsDisabled + 1 : 0, + }) + ); + } + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts index 87e484ae2b3d406..8163a736696745e 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts @@ -15,7 +15,6 @@ import { SAVED_QUERY_RULE_TYPE_ID, } from '@kbn/securitysolution-rules'; import { ALERT_RULE_UUID } from '@kbn/rule-data-utils'; -import { LEGACY_NOTIFICATIONS_ID } from '../../../common/constants'; import { CASE_COMMENT_SAVED_OBJECT } from '../../../../cases/common/constants'; import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server'; @@ -30,6 +29,10 @@ import type { RuleSearchResult, DetectionMetrics, } from './types'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleActionsSavedObjectType } from '../../lib/detection_engine/rule_actions/legacy_saved_object_mappings'; +// eslint-disable-next-line no-restricted-imports +import { LegacyIRuleActionsAttributesSavedObjectAttributes } from '../../lib/detection_engine/rule_actions/legacy_types'; /** * Initial detection metrics initialized. @@ -63,45 +66,70 @@ export const initialDetectionRulesUsage: DetectionRulesTypeUsage = { disabled: 0, alerts: 0, cases: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, threshold: { enabled: 0, disabled: 0, alerts: 0, cases: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, eql: { enabled: 0, disabled: 0, alerts: 0, cases: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, machine_learning: { enabled: 0, disabled: 0, alerts: 0, cases: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, threat_match: { enabled: 0, disabled: 0, alerts: 0, cases: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, elastic_total: { enabled: 0, disabled: 0, alerts: 0, cases: 0, - }, - legacy_notifications: { - total: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, custom_total: { enabled: 0, disabled: 0, alerts: 0, cases: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, }; @@ -112,6 +140,16 @@ export const updateDetectionRuleUsage = ( ): DetectionRulesTypeUsage => { let updatedUsage = usage; + const legacyNotificationEnabled = + detectionRuleMetric.has_legacy_notification && detectionRuleMetric.enabled; + + const legacyNotificationDisabled = + detectionRuleMetric.has_legacy_notification && !detectionRuleMetric.enabled; + + const notificationEnabled = detectionRuleMetric.has_notification && detectionRuleMetric.enabled; + + const notificationDisabled = detectionRuleMetric.has_notification && !detectionRuleMetric.enabled; + if (detectionRuleMetric.rule_type === 'query') { updatedUsage = { ...usage, @@ -121,6 +159,18 @@ export const updateDetectionRuleUsage = ( disabled: !detectionRuleMetric.enabled ? usage.query.disabled + 1 : usage.query.disabled, alerts: usage.query.alerts + detectionRuleMetric.alert_count_daily, cases: usage.query.cases + detectionRuleMetric.cases_count_total, + legacy_notifications_enabled: legacyNotificationEnabled + ? usage.query.legacy_notifications_enabled + 1 + : usage.query.legacy_notifications_enabled, + legacy_notifications_disabled: legacyNotificationDisabled + ? usage.query.legacy_notifications_disabled + 1 + : usage.query.legacy_notifications_disabled, + notifications_enabled: notificationEnabled + ? usage.query.notifications_enabled + 1 + : usage.query.notifications_enabled, + notifications_disabled: notificationDisabled + ? usage.query.notifications_disabled + 1 + : usage.query.notifications_disabled, }, }; } else if (detectionRuleMetric.rule_type === 'threshold') { @@ -136,6 +186,18 @@ export const updateDetectionRuleUsage = ( : usage.threshold.disabled, alerts: usage.threshold.alerts + detectionRuleMetric.alert_count_daily, cases: usage.threshold.cases + detectionRuleMetric.cases_count_total, + legacy_notifications_enabled: legacyNotificationEnabled + ? usage.threshold.legacy_notifications_enabled + 1 + : usage.threshold.legacy_notifications_enabled, + legacy_notifications_disabled: legacyNotificationDisabled + ? usage.threshold.legacy_notifications_disabled + 1 + : usage.threshold.legacy_notifications_disabled, + notifications_enabled: notificationEnabled + ? usage.threshold.notifications_enabled + 1 + : usage.threshold.notifications_enabled, + notifications_disabled: notificationDisabled + ? usage.threshold.notifications_disabled + 1 + : usage.threshold.notifications_disabled, }, }; } else if (detectionRuleMetric.rule_type === 'eql') { @@ -147,6 +209,18 @@ export const updateDetectionRuleUsage = ( disabled: !detectionRuleMetric.enabled ? usage.eql.disabled + 1 : usage.eql.disabled, alerts: usage.eql.alerts + detectionRuleMetric.alert_count_daily, cases: usage.eql.cases + detectionRuleMetric.cases_count_total, + legacy_notifications_enabled: legacyNotificationEnabled + ? usage.eql.legacy_notifications_enabled + 1 + : usage.eql.legacy_notifications_enabled, + legacy_notifications_disabled: legacyNotificationDisabled + ? usage.eql.legacy_notifications_disabled + 1 + : usage.eql.legacy_notifications_disabled, + notifications_enabled: notificationEnabled + ? usage.eql.notifications_enabled + 1 + : usage.eql.notifications_enabled, + notifications_disabled: notificationDisabled + ? usage.eql.notifications_disabled + 1 + : usage.eql.notifications_disabled, }, }; } else if (detectionRuleMetric.rule_type === 'machine_learning') { @@ -162,6 +236,18 @@ export const updateDetectionRuleUsage = ( : usage.machine_learning.disabled, alerts: usage.machine_learning.alerts + detectionRuleMetric.alert_count_daily, cases: usage.machine_learning.cases + detectionRuleMetric.cases_count_total, + legacy_notifications_enabled: legacyNotificationEnabled + ? usage.machine_learning.legacy_notifications_enabled + 1 + : usage.machine_learning.legacy_notifications_enabled, + legacy_notifications_disabled: legacyNotificationDisabled + ? usage.machine_learning.legacy_notifications_disabled + 1 + : usage.machine_learning.legacy_notifications_disabled, + notifications_enabled: notificationEnabled + ? usage.machine_learning.notifications_enabled + 1 + : usage.machine_learning.notifications_enabled, + notifications_disabled: notificationDisabled + ? usage.machine_learning.notifications_disabled + 1 + : usage.machine_learning.notifications_disabled, }, }; } else if (detectionRuleMetric.rule_type === 'threat_match') { @@ -177,6 +263,18 @@ export const updateDetectionRuleUsage = ( : usage.threat_match.disabled, alerts: usage.threat_match.alerts + detectionRuleMetric.alert_count_daily, cases: usage.threat_match.cases + detectionRuleMetric.cases_count_total, + legacy_notifications_enabled: legacyNotificationEnabled + ? usage.threat_match.legacy_notifications_enabled + 1 + : usage.threat_match.legacy_notifications_enabled, + legacy_notifications_disabled: legacyNotificationDisabled + ? usage.threat_match.legacy_notifications_disabled + 1 + : usage.threat_match.legacy_notifications_disabled, + notifications_enabled: notificationEnabled + ? usage.threat_match.notifications_enabled + 1 + : usage.threat_match.notifications_enabled, + notifications_disabled: notificationDisabled + ? usage.threat_match.notifications_disabled + 1 + : usage.threat_match.notifications_disabled, }, }; } @@ -194,6 +292,18 @@ export const updateDetectionRuleUsage = ( : updatedUsage.elastic_total.disabled, alerts: updatedUsage.elastic_total.alerts + detectionRuleMetric.alert_count_daily, cases: updatedUsage.elastic_total.cases + detectionRuleMetric.cases_count_total, + legacy_notifications_enabled: legacyNotificationEnabled + ? updatedUsage.elastic_total.legacy_notifications_enabled + 1 + : updatedUsage.elastic_total.legacy_notifications_enabled, + legacy_notifications_disabled: legacyNotificationDisabled + ? updatedUsage.elastic_total.legacy_notifications_disabled + 1 + : updatedUsage.elastic_total.legacy_notifications_disabled, + notifications_enabled: notificationEnabled + ? updatedUsage.elastic_total.notifications_enabled + 1 + : updatedUsage.elastic_total.notifications_enabled, + notifications_disabled: notificationDisabled + ? updatedUsage.elastic_total.notifications_disabled + 1 + : updatedUsage.elastic_total.notifications_disabled, }, }; } else { @@ -209,6 +319,18 @@ export const updateDetectionRuleUsage = ( : updatedUsage.custom_total.disabled, alerts: updatedUsage.custom_total.alerts + detectionRuleMetric.alert_count_daily, cases: updatedUsage.custom_total.cases + detectionRuleMetric.cases_count_total, + legacy_notifications_enabled: legacyNotificationEnabled + ? updatedUsage.custom_total.legacy_notifications_enabled + 1 + : updatedUsage.custom_total.legacy_notifications_enabled, + legacy_notifications_disabled: legacyNotificationDisabled + ? updatedUsage.custom_total.legacy_notifications_disabled + 1 + : updatedUsage.custom_total.legacy_notifications_disabled, + notifications_enabled: notificationEnabled + ? updatedUsage.custom_total.notifications_enabled + 1 + : updatedUsage.custom_total.notifications_enabled, + notifications_disabled: notificationDisabled + ? updatedUsage.custom_total.notifications_disabled + 1 + : updatedUsage.custom_total.notifications_disabled, }, }; } @@ -287,18 +409,28 @@ export const getDetectionRuleMetrics = async ( filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: alert`, }); - // We get just 1 per a single page so we can get the total count to add to the rulesUsage. // Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function. - const legacyNotificationsCount = ( - await savedObjectClient.find({ - type: 'alert', + const legacyRuleActions = + await savedObjectClient.find({ + type: legacyRuleActionsSavedObjectType, page: 1, + perPage: MAX_RESULTS_WINDOW, namespaces: ['*'], - perPage: 1, - filter: `alert.attributes.alertTypeId: ${LEGACY_NOTIFICATIONS_ID}`, - }) - ).total; - rulesUsage = { ...rulesUsage, legacy_notifications: { total: legacyNotificationsCount } }; + }); + + const legacyNotificationRuleIds = legacyRuleActions.saved_objects.reduce( + (cache, legacyNotificationsObject) => { + const ruleRef = legacyNotificationsObject.references.find( + (reference) => reference.name === 'alert_0' && reference.type === 'alert' + ); + if (ruleRef != null) { + const enabled = legacyNotificationsObject.attributes.ruleThrottle !== 'no_actions'; + cache.set(ruleRef.id, { enabled }); + } + return cache; + }, + new Map() + ); const casesCache = cases.saved_objects.reduce((cache, { attributes: casesObject }) => { const ruleId = casesObject.rule.id; @@ -320,6 +452,17 @@ export const getDetectionRuleMetrics = async ( const ruleObjects = ruleResults.hits.hits.map((hit) => { const ruleId = hit._id.split(':')[1]; const isElastic = isElasticRule(hit._source?.alert.tags); + + // Even if the legacy notification is set to "no_actions" we still count the rule as having a legacy notification that is not migrated yet. + const hasLegacyNotification = legacyNotificationRuleIds.get(ruleId) != null; + + // We only count a rule as having a notification and being "enabled" if it is _not_ set to "no_actions"/"muteAll" and it has at least one action within its array. + const hasNotification = + !hasLegacyNotification && + hit._source?.alert.actions != null && + hit._source?.alert.actions.length > 0 && + hit._source?.alert.muteAll !== true; + return { rule_name: hit._source?.alert.name, rule_id: hit._source?.alert.params.ruleId, @@ -331,6 +474,8 @@ export const getDetectionRuleMetrics = async ( updated_on: hit._source?.alert.updatedAt, alert_count_daily: alertsCache.get(ruleId) || 0, cases_count_total: casesCache.get(ruleId) || 0, + has_legacy_notification: hasLegacyNotification, + has_notification: hasNotification, } as DetectionRuleMetric; }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts index 7793552510c759f..d08f915e4428f56 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts @@ -70,6 +70,8 @@ describe('Detections Usage and Metrics', () => { rule_type: 'query', rule_version: 4, updated_on: '2021-03-23T17:15:59.634Z', + has_legacy_notification: false, + has_notification: false, }, ], detection_rule_usage: { @@ -79,15 +81,20 @@ describe('Detections Usage and Metrics', () => { disabled: 1, alerts: 3400, cases: 1, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, elastic_total: { alerts: 3400, cases: 1, disabled: 1, enabled: 0, - }, - legacy_notifications: { - total: 4, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, }, }, @@ -118,15 +125,20 @@ describe('Detections Usage and Metrics', () => { cases: 1, disabled: 1, enabled: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, query: { alerts: 800, cases: 1, disabled: 1, enabled: 0, - }, - legacy_notifications: { - total: 4, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, }, }, @@ -144,7 +156,7 @@ describe('Detections Usage and Metrics', () => { savedObjectsClient.find.mockResolvedValue(getMockAlertCasesResponse()); const result = await fetchDetectionsMetrics('', '', esClientMock, savedObjectsClient, mlMock); - expect(result).toEqual({ + expect(result).toEqual({ ...getInitialDetectionMetrics(), detection_rules: { detection_rule_detail: [ @@ -159,6 +171,8 @@ describe('Detections Usage and Metrics', () => { rule_type: 'query', rule_version: 4, updated_on: '2021-03-23T17:15:59.634Z', + has_legacy_notification: false, + has_notification: false, }, ], detection_rule_usage: { @@ -168,15 +182,20 @@ describe('Detections Usage and Metrics', () => { cases: 1, disabled: 1, enabled: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, query: { alerts: 0, cases: 1, disabled: 1, enabled: 0, - }, - legacy_notifications: { - total: 4, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, }, }, }, diff --git a/x-pack/plugins/security_solution/server/usage/detections/types.ts b/x-pack/plugins/security_solution/server/usage/detections/types.ts index ed0e8a4e5e99f21..b2a9cf7af486136 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/types.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/types.ts @@ -30,7 +30,9 @@ export interface RuleSearchResult { tags: string[]; createdAt: string; updatedAt: string; + muteAll: boolean | undefined | null; params: DetectionRuleParms; + actions: unknown[]; }; } @@ -55,8 +57,11 @@ interface FeatureTypeUsage { disabled: number; alerts: number; cases: number; + legacy_notifications_enabled: number; + legacy_notifications_disabled: number; + notifications_enabled: number; + notifications_disabled: number; } - export interface DetectionRulesTypeUsage { query: FeatureTypeUsage; threshold: FeatureTypeUsage; @@ -65,7 +70,6 @@ export interface DetectionRulesTypeUsage { threat_match: FeatureTypeUsage; elastic_total: FeatureTypeUsage; custom_total: FeatureTypeUsage; - legacy_notifications: LegacyNotifications; } export interface MlJobsUsage { @@ -129,6 +133,8 @@ export interface DetectionRuleMetric { updated_on: string; alert_count_daily: number; cases_count_total: number; + has_legacy_notification: boolean; + has_notification: boolean; } export interface AlertsAggregationResponse { @@ -162,11 +168,3 @@ export interface DetectionRuleAdoption { detection_rule_detail: DetectionRuleMetric[]; detection_rule_usage: DetectionRulesTypeUsage; } - -/** - * The legacy notifications that are still in use. - * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function - */ -export interface LegacyNotifications { - total: number; -} diff --git a/x-pack/plugins/task_manager/server/task_running/ephemeral_task_runner.ts b/x-pack/plugins/task_manager/server/task_running/ephemeral_task_runner.ts index 0085329cd66e613..a9d1d8b6f2f1689 100644 --- a/x-pack/plugins/task_manager/server/task_running/ephemeral_task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/ephemeral_task_runner.ts @@ -304,6 +304,7 @@ export class EphemeralTaskManagerRunner implements TaskRunner { public async cancel() { const { task } = this; if (task?.cancel) { + // it will cause the task state of "running" to be cleared this.task = undefined; return task.cancel(); } diff --git a/x-pack/plugins/task_manager/server/task_running/task_runner.ts b/x-pack/plugins/task_manager/server/task_running/task_runner.ts index e21e418c9046153..48927435c4bdf91 100644 --- a/x-pack/plugins/task_manager/server/task_running/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_running/task_runner.ts @@ -431,6 +431,7 @@ export class TaskManagerRunner implements TaskRunner { public async cancel() { const { task } = this; if (task?.cancel) { + // it will cause the task state of "running" to be cleared this.task = undefined; return task.cancel(); } diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 02ad35bd0916cf4..799b183f7bbd64c 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -6986,6 +6986,30 @@ "_meta": { "description": "Number of cases attached to query detection rule alerts" } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } } } }, @@ -7014,6 +7038,30 @@ "_meta": { "description": "Number of cases attached to threshold detection rule alerts" } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } } } }, @@ -7042,6 +7090,30 @@ "_meta": { "description": "Number of cases attached to eql detection rule alerts" } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } } } }, @@ -7070,6 +7142,30 @@ "_meta": { "description": "Number of cases attached to machine_learning detection rule alerts" } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } } } }, @@ -7098,15 +7194,29 @@ "_meta": { "description": "Number of cases attached to threat_match detection rule alerts" } - } - } - }, - "legacy_notifications": { - "properties": { - "total": { + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { "type": "long", "_meta": { - "description": "Number of legacy notifications still in use" + "description": "Number of notifications enabled" } } } @@ -7136,6 +7246,30 @@ "_meta": { "description": "Number of cases attached to elastic detection rule alerts" } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } } } }, @@ -7164,6 +7298,30 @@ "_meta": { "description": "Number of cases attached to custom detection rule alerts" } + }, + "legacy_notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications enabled" + } + }, + "legacy_notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of legacy notifications disabled" + } + }, + "notifications_enabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } + }, + "notifications_disabled": { + "type": "long", + "_meta": { + "description": "Number of notifications enabled" + } } } } @@ -7232,6 +7390,18 @@ "_meta": { "description": "The number of total cases generated by a rule" } + }, + "has_legacy_notification": { + "type": "boolean", + "_meta": { + "description": "True if this rule has a legacy notification" + } + }, + "has_notification": { + "type": "boolean", + "_meta": { + "description": "True if this rule has a notification" + } } } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 9d110c64447e45a..6a1c42d37f35bde 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6231,7 +6231,6 @@ "xpack.apm.transactionsTable.cardinalityWarning.body": "一意のトランザクション名の数が構成された値{bucketSize}を超えています。エージェントを再構成し、類似したトランザクションをグループ化するか、{codeBlock}の値を増やしてください。", "xpack.apm.transactionsTable.cardinalityWarning.docsLink": "詳細はドキュメントをご覧ください", "xpack.apm.transactionsTable.cardinalityWarning.title": "このビューには、報告されたトランザクションのサブセットが表示されます。", - "xpack.apm.transactionsTable.errorMessage": "取得できませんでした", "xpack.apm.transactionsTable.linkText": "トランザクションを表示", "xpack.apm.transactionsTable.loading": "読み込み中...", "xpack.apm.transactionsTable.noResults": "トランザクショングループが見つかりません", @@ -14911,12 +14910,10 @@ "xpack.maps.embeddableDisplayName": "マップ", "xpack.maps.emsFileSelect.selectPlaceholder": "EMSレイヤーを選択", "xpack.maps.emsSource.tooltipsTitle": "ツールチップフィールド", - "xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "GeometryCollectionを convertESShapeToGeojsonGeometryに渡さないでください", "xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "{geometryType} ジオメトリから Geojson に変換できません。サポートされていません", "xpack.maps.es_geo_utils.distanceFilterAlias": "{pointLabel}の{distanceKm} km以内", "xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage": "サポートされていないフィールドタイプ、期待値:{expectedTypes}、提供された値:{fieldType}", "xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "サポートされていないジオメトリタイプ、期待値:{expectedTypes}、提供された値:{geometryType}", - "xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "{wkt} を Geojson に変換できません。有効な WKT が必要です。", "xpack.maps.esGeoLine.areEntitiesTrimmedMsg": "結果は ~{totalEntities} 中最初の {entityCount} トラックに制限されます。", "xpack.maps.esGeoLine.tracksCountMsg": "{entityCount} 個のトラックが見つかりました。", "xpack.maps.esGeoLine.tracksTrimmedMsg": "{entityCount} 中 {numTrimmedTracks} 個のトラックが不完全です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3508f2c4bc61469..b12fa0ba6eb1c6e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6275,7 +6275,6 @@ "xpack.apm.transactionsTable.cardinalityWarning.body": "唯一事务名称的数目超过 {bucketSize} 的已配置值。尝试重新配置您的代理以对类似的事务分组或增大 {codeBlock} 的值", "xpack.apm.transactionsTable.cardinalityWarning.docsLink": "在文档中了解详情", "xpack.apm.transactionsTable.cardinalityWarning.title": "此视图显示已报告事务的子集。", - "xpack.apm.transactionsTable.errorMessage": "无法提取", "xpack.apm.transactionsTable.linkText": "查看事务", "xpack.apm.transactionsTable.loading": "正在加载……", "xpack.apm.transactionsTable.noResults": "未找到事务组", @@ -15106,12 +15105,10 @@ "xpack.maps.embeddableDisplayName": "地图", "xpack.maps.emsFileSelect.selectPlaceholder": "选择 EMS 图层", "xpack.maps.emsSource.tooltipsTitle": "工具提示字段", - "xpack.maps.es_geo_utils.convert.invalidGeometryCollectionErrorMessage": "不应将 GeometryCollection 传递给 convertESShapeToGeojsonGeometry", "xpack.maps.es_geo_utils.convert.unsupportedGeometryTypeErrorMessage": "无法将 {geometryType} 几何图形转换成 geojson,不支持", "xpack.maps.es_geo_utils.distanceFilterAlias": "{pointLabel} {distanceKm}km 内", "xpack.maps.es_geo_utils.unsupportedFieldTypeErrorMessage": "字段类型不受支持,应为 {expectedTypes},而提供的是 {fieldType}", "xpack.maps.es_geo_utils.unsupportedGeometryTypeErrorMessage": "几何类型不受支持,应为 {expectedTypes},而提供的是 {geometryType}", - "xpack.maps.es_geo_utils.wkt.invalidWKTErrorMessage": "无法将 {wkt} 转换成 geojson。需要有效的 WKT。", "xpack.maps.esGeoLine.areEntitiesTrimmedMsg": "结果限制为 ~{totalEntities} 条轨迹中的前 {entityCount} 条。", "xpack.maps.esGeoLine.tracksCountMsg": "找到 {entityCount} 条轨迹。", "xpack.maps.esGeoLine.tracksTrimmedMsg": "{entityCount} 条轨迹中有 {numTrimmedTracks} 条不完整。", diff --git a/x-pack/plugins/uptime/common/constants/ui.ts b/x-pack/plugins/uptime/common/constants/ui.ts index 7a9cd4943bc5796..155c6ea80242fcf 100644 --- a/x-pack/plugins/uptime/common/constants/ui.ts +++ b/x-pack/plugins/uptime/common/constants/ui.ts @@ -11,7 +11,7 @@ export const MONITOR_ADD_ROUTE = '/add-monitor'; export const MONITOR_EDIT_ROUTE = '/edit-monitor/:monitorId'; -export const MONITOR_MANAGEMENT = '/manage-monitors'; +export const MONITOR_MANAGEMENT_ROUTE = '/manage-monitors'; export const OVERVIEW_ROUTE = '/'; diff --git a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts index efc47c44765c8c5..e5b0cd352ac5bf4 100644 --- a/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/monitor_management.journey.ts @@ -134,3 +134,63 @@ journey('Monitor Management', async ({ page, params }: { page: Page; params: any await deleteMonitor(); }); }); + +journey('Monitor Management breadcrumbs', async ({ page, params }: { page: Page; params: any }) => { + const uptime = monitorManagementPageProvider({ page, kibanaUrl: params.kibanaUrl }); + const basicMonitorDetails = { + name: 'Sample monitor', + location: 'US Central', + schedule: '@every 3m', + apmServiceName: 'service', + }; + + before(async () => { + await uptime.waitForLoadingToFinish(); + }); + + step('Go to monitor-management', async () => { + await uptime.navigateToMonitorManagement(); + }); + + step('login to Kibana', async () => { + await uptime.loginToKibana(); + }); + + step('Check breadcrumb', async () => { + const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); + expect(lastBreadcrumb).toEqual('Monitor management'); + }); + + step('check breadcrumbs', async () => { + await uptime.clickAddMonitor(); + const breadcrumbs = await page.$$('[data-test-subj="breadcrumb"]'); + expect(await breadcrumbs[1].textContent()).toEqual('Monitor management'); + const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); + expect(lastBreadcrumb).toEqual('Add monitor'); + }); + + step('create monitor http monitor', async () => { + const monitorDetails = { + ...basicMonitorDetails, + url: 'https://elastic.co', + locations: [basicMonitorDetails.location], + }; + await uptime.createBasicHTTPMonitorDetails(monitorDetails); + const isSuccessful = await uptime.confirmAndSave(); + expect(isSuccessful).toBeTruthy(); + }); + + step('edit http monitor and check breadcrumb', async () => { + await uptime.editMonitor(); + const breadcrumbs = await page.$$('[data-test-subj=breadcrumb]'); + expect(await breadcrumbs[1].textContent()).toEqual('Monitor management'); + const lastBreadcrumb = await (await uptime.findByTestSubj('"breadcrumb last"')).textContent(); + expect(lastBreadcrumb).toEqual('Edit monitor'); + }); + + step('delete monitor', async () => { + await uptime.navigateToMonitorManagement(); + const isSuccessful = await uptime.deleteMonitor(); + expect(isSuccessful).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx index 51f476a7a174294..e12b7fdf40bc379 100644 --- a/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx +++ b/x-pack/plugins/uptime/e2e/page_objects/monitor_management.tsx @@ -44,6 +44,10 @@ export function monitorManagementPageProvider({ return await this.findByTestSubj('uptimeDeleteMonitorSuccess'); }, + async editMonitor() { + await this.clickByTestSubj('monitorManagementEditMonitor'); + }, + async findMonitorConfiguration(monitorConfig: Record) { const values = Object.values(monitorConfig); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 0600a629ad3e2bd..985b1ae9146f249 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -16,7 +16,11 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; -import { MONITOR_MANAGEMENT, MONITOR_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; +import { + MONITOR_MANAGEMENT_ROUTE, + MONITOR_ROUTE, + SETTINGS_ROUTE, +} from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; import { InspectorHeaderLink } from './inspector_header_link'; import { monitorStatusSelector } from '../../../state/selectors'; @@ -81,7 +85,7 @@ export function ActionMenuContent({ config }: { config: UptimeConfig }): React.R color="text" data-test-subj="management-page-link" href={history.createHref({ - pathname: MONITOR_MANAGEMENT, + pathname: MONITOR_MANAGEMENT_ROUTE, })} > { }, [data, status, notifications.toasts, isSaving, isValid, monitorId]); return status === FETCH_STATUS.SUCCESS ? ( - + ) : ( {!isValid && hasBeenSubmitted && VALIDATION_ERROR_LABEL} @@ -92,7 +92,7 @@ export const ActionBar = ({ monitor, isValid, onSave }: Props) => { color="ghost" size="s" iconType="cross" - href={`${basePath}/app/uptime/${MONITOR_MANAGEMENT}`} + href={`${basePath}/app/uptime/${MONITOR_MANAGEMENT_ROUTE}`} > {DISCARD_LABEL} diff --git a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx index c3ef3610f8c7e30..d86117df555ad32 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx +++ b/x-pack/plugins/uptime/public/components/monitor_management/monitor_list/actions.tsx @@ -61,6 +61,7 @@ export const Actions = ({ id, setRefresh }: Props) => { iconType="pencil" href={`${basePath}/app/uptime/edit-monitor/${Buffer.from(id, 'utf8').toString('base64')}`} aria-label={EDIT_MONITOR_LABEL} + data-test-subj="monitorManagementEditMonitor" /> diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx index 749a109dffda2b4..cc474b065464394 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/add_monitor.tsx @@ -13,6 +13,7 @@ import { SyntheticsProviders } from '../../components/fleet_package/contexts'; import { Loader } from '../../components/monitor_management/loader/loader'; import { MonitorConfig } from '../../components/monitor_management/monitor_config/monitor_config'; import { useLocations } from '../../components/monitor_management/hooks/use_locations'; +import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs'; export const AddMonitorPage: React.FC = () => { useTrackPageview({ app: 'uptime', path: 'add-monitor' }); @@ -20,6 +21,8 @@ export const AddMonitorPage: React.FC = () => { const { error, loading } = useLocations(); + useMonitorManagementBreadcrumbs({ isAddMonitor: true }); + return ( { useTrackPageview({ app: 'uptime', path: 'edit-monitor' }); useTrackPageview({ app: 'uptime', path: 'edit-monitor', delay: 15000 }); + useMonitorManagementBreadcrumbs({ isEditMonitor: true }); const { monitorId } = useParams<{ monitorId: string }>(); const { data, status } = useFetcher>(() => { diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx index 0619f4d4bed1cf2..cb43dc9c90d7d05 100644 --- a/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx +++ b/x-pack/plugins/uptime/public/pages/monitor_management/monitor_management.tsx @@ -11,6 +11,7 @@ import { useTrackPageview } from '../../../../observability/public'; import { getMonitors } from '../../state/actions'; import { monitorManagementListSelector } from '../../state/selectors'; import { MonitorManagementList } from '../../components/monitor_management/monitor_list/monitor_list'; +import { useMonitorManagementBreadcrumbs } from './use_monitor_management_breadcrumbs'; export const MonitorManagementPage: React.FC = () => { const [refresh, setRefresh] = useState(true); @@ -18,6 +19,7 @@ export const MonitorManagementPage: React.FC = () => { const [pageSize, setPageSize] = useState(10); // saved objects page index is base 1 useTrackPageview({ app: 'uptime', path: 'manage-monitors' }); useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 }); + useMonitorManagementBreadcrumbs(); const dispatch = useDispatch(); const monitorList = useSelector(monitorManagementListSelector); diff --git a/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx b/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx new file mode 100644 index 000000000000000..e5784591a00fc42 --- /dev/null +++ b/x-pack/plugins/uptime/public/pages/monitor_management/use_monitor_management_breadcrumbs.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; +import { MONITOR_MANAGEMENT_ROUTE } from '../../../common/constants'; +import { PLUGIN } from '../../../common/constants/plugin'; + +export const useMonitorManagementBreadcrumbs = ({ + isAddMonitor, + isEditMonitor, + monitorId, +}: { + isAddMonitor?: boolean; + isEditMonitor?: boolean; + monitorId?: string; +} = {}) => { + const kibana = useKibana(); + const appPath = kibana.services.application?.getUrlForApp(PLUGIN.ID) ?? ''; + + useBreadcrumbs([ + { + text: MONITOR_MANAGEMENT_CRUMB, + href: isAddMonitor || isEditMonitor ? `${appPath}/${MONITOR_MANAGEMENT_ROUTE}` : undefined, + }, + ...(isAddMonitor + ? [ + { + text: ADD_MONITOR_CRUMB, + }, + ] + : []), + ...(isEditMonitor + ? [ + { + text: EDIT_MONITOR_CRUMB, + }, + ] + : []), + ]); +}; + +export const MONITOR_MANAGEMENT_CRUMB = i18n.translate( + 'xpack.uptime.monitorManagement.monitorManagementCrumb', + { + defaultMessage: 'Monitor management', + } +); + +export const ADD_MONITOR_CRUMB = i18n.translate('xpack.uptime.monitorManagement.addMonitorCrumb', { + defaultMessage: 'Add monitor', +}); + +export const EDIT_MONITOR_CRUMB = i18n.translate( + 'xpack.uptime.monitorManagement.editMonitorCrumb', + { + defaultMessage: 'Edit monitor', + } +); diff --git a/x-pack/plugins/uptime/public/routes.tsx b/x-pack/plugins/uptime/public/routes.tsx index d462d35a15f5666..bcb942250c6f138 100644 --- a/x-pack/plugins/uptime/public/routes.tsx +++ b/x-pack/plugins/uptime/public/routes.tsx @@ -16,7 +16,7 @@ import { MONITOR_ROUTE, MONITOR_ADD_ROUTE, MONITOR_EDIT_ROUTE, - MONITOR_MANAGEMENT, + MONITOR_MANAGEMENT_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE, STEP_DETAIL_ROUTE, @@ -231,7 +231,7 @@ const getRoutes = (config: UptimeConfig): RouteProps[] => { defaultMessage: 'Manage Monitors | {baseTitle}', values: { baseTitle }, }), - path: MONITOR_MANAGEMENT, + path: MONITOR_MANAGEMENT_ROUTE, component: MonitorManagementPage, dataTestSubj: 'uptimeMonitorManagementListPage', telemetryId: UptimePage.MonitorManagement, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md index cfcf13e1d9f707b..2760001a20626ab 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/README.md @@ -1,8 +1,6 @@ These are tests for the telemetry rules within "security_solution/server/usage" * detection_rules -* legacy_notifications -Detection rules are tests around each of the rule types to affirm they work such as query, eql, etc... -Legacy notifications are tests around the legacy notification telemetry. Once legacy notifications are removed, -these tests can be removed too. +Detection rules are tests around each of the rule types to affirm they work such as query, eql, etc... This includes +legacy notifications. Once legacy notifications are moved, those tests can be removed too. diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts index bafc36c329eab33..561c8bc35647661 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/detection_rules.ts @@ -8,26 +8,30 @@ import expect from '@kbn/expect'; import { DetectionMetrics } from '../../../../../plugins/security_solution/server/usage/detections/types'; import { - EqlCreateSchema, - QueryCreateSchema, ThreatMatchCreateSchema, ThresholdCreateSchema, } from '../../../../../plugins/security_solution/common/detection_engine/schemas/request'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { + createLegacyRuleAction, + createNewAction, createRule, createSignalsIndex, deleteAllAlerts, deleteSignalsIndex, getEqlRuleForSignalTesting, + getRule, getRuleForSignalTesting, + getRuleWithWebHookAction, getSimpleMlRule, + getSimpleRule, getSimpleThreatMatch, getStats, getThresholdRuleForSignalTesting, installPrePackagedRules, waitForRuleSuccessOrStatus, waitForSignalsToBePresent, + updateRule, } from '../../../utils'; import { getInitialDetectionMetrics } from '../../../../../plugins/security_solution/server/usage/detections/detection_rule_helpers'; @@ -64,11 +68,769 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"kql" rule type', () => { - it('should show stats for active rule', async () => { - const rule: QueryCreateSchema = getRuleForSignalTesting(['telemetry']); + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "disabled"/"in-active" rule that does not have any actions', async () => { + const rule = getRuleForSignalTesting(['telemetry'], 'rule-1', false); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "enabled"/"active" rule that does not have any actions', async () => { + const rule = getRuleForSignalTesting(['telemetry']); + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_disabled" to be "1" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { + const rule = getRuleForSignalTesting(['telemetry']); + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, false, rule); + await createRule(supertest, log, ruleToCreate); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + notifications_disabled: 1, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + notifications_disabled: 1, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_enabled" to be "1" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { + const rule = getRuleForSignalTesting(['telemetry']); + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, true, rule); + const { id } = await createRule(supertest, log, ruleToCreate); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "legacy_notifications_disabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "disabled"/"in-active"', async () => { + const rule = getRuleForSignalTesting(['telemetry'], 'rule-1', false); + const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + disabled: 1, + legacy_notifications_disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + legacy_notifications_disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "legacy_notifications_enabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "enabled"/"active"', async () => { + const rule = getRuleForSignalTesting(['telemetry']); + const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + query: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + }); + + describe('"eql" rule type', () => { + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "disabled"/"in-active" rule that does not have any actions', async () => { + const rule = getEqlRuleForSignalTesting(['telemetry'], 'rule-1', false); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "enabled"/"active" rule that does not have any actions', async () => { + const rule = getEqlRuleForSignalTesting(['telemetry']); + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_disabled" to be "1" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { + const rule = getEqlRuleForSignalTesting(['telemetry']); + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, false, rule); + await createRule(supertest, log, ruleToCreate); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql, + notifications_disabled: 1, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + notifications_disabled: 1, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_enabled" to be "1" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { + const rule = getEqlRuleForSignalTesting(['telemetry']); + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, true, rule); + const { id } = await createRule(supertest, log, ruleToCreate); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "legacy_notifications_disabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "disabled"/"in-active"', async () => { + const rule = getEqlRuleForSignalTesting(['telemetry'], 'rule-1', false); + const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql, + disabled: 1, + legacy_notifications_disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + legacy_notifications_disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "legacy_notifications_enabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "enabled"/"active"', async () => { + const rule = getEqlRuleForSignalTesting(['telemetry']); + const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + eql: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.eql, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + }); + + describe('"threshold" rule type', () => { + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "disabled"/"in-active" rule that does not have any actions', async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry'], 'rule-1', false), + threshold: { + field: 'keyword', + value: 1, + }, + }; + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "enabled"/"active" rule that does not have any actions', async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry']), + threshold: { + field: 'keyword', + value: 1, + }, + }; + const { id } = await createRule(supertest, log, rule); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_disabled" to be "1" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry'], 'rule-1', false), + threshold: { + field: 'keyword', + value: 1, + }, + }; + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, false, rule); + await createRule(supertest, log, ruleToCreate); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold, + notifications_disabled: 1, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + notifications_disabled: 1, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_enabled" to be "1" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry']), + threshold: { + field: 'keyword', + value: 1, + }, + }; + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, true, rule); + const { id } = await createRule(supertest, log, ruleToCreate); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + alerts: 4, + notifications_enabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "legacy_notifications_disabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "disabled"/"in-active"', async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry'], 'rule-1', false), + threshold: { + field: 'keyword', + value: 1, + }, + }; + const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold, + disabled: 1, + legacy_notifications_disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + legacy_notifications_disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "legacy_notifications_enabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "enabled"/"active"', async () => { + const rule: ThresholdCreateSchema = { + ...getThresholdRuleForSignalTesting(['telemetry']), + threshold: { + field: 'keyword', + value: 1, + }, + }; const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); await waitForRuleSuccessOrStatus(supertest, log, id); await waitForSignalsToBePresent(supertest, log, 4, [id]); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + threshold: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threshold, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + }); + + // Note: We don't actually find signals with these tests as we don't have a good way of signal finding with ML rules. + describe('"ml" rule type', () => { + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "disabled"/"in-active" rule that does not have any actions', async () => { + const rule = getSimpleMlRule(); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "enabled"/"active" rule that does not have any actions', async () => { + const rule = getSimpleMlRule('rule-1', true); + await createRule(supertest, log, rule); + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning, + enabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + enabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_disabled" to be "1" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { + const rule = getSimpleMlRule(); + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, false, rule); + await createRule(supertest, log, ruleToCreate); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + const expected: DetectionMetrics = { + ...getInitialDetectionMetrics(), + detection_rules: { + ...getInitialDetectionMetrics().detection_rules, + detection_rule_usage: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning, + notifications_disabled: 1, + disabled: 1, + }, + custom_total: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + notifications_disabled: 1, + disabled: 1, + }, + }, + }, + }; + expect(stats).to.eql(expected); + }); + }); + + it('should show "notifications_enabled" to be "1" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { + const rule = getSimpleMlRule('rule-1', true); + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, true, rule); + await createRule(supertest, log, ruleToCreate); + await retry.try(async () => { const stats = await getStats(supertest, log); const expected: DetectionMetrics = { @@ -77,15 +839,16 @@ export default ({ getService }: FtrProviderContext) => { ...getInitialDetectionMetrics().detection_rules, detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - query: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning, enabled: 1, - alerts: 4, + notifications_enabled: 1, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, - alerts: 4, + notifications_enabled: 1, }, }, }, @@ -94,9 +857,12 @@ export default ({ getService }: FtrProviderContext) => { }); }); - it('should show stats for in-active rule', async () => { - const rule: QueryCreateSchema = getRuleForSignalTesting(['telemetry'], 'rule-1', false); - await createRule(supertest, log, rule); + it('should show "legacy_notifications_disabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "disabled"/"in-active"', async () => { + const rule = getSimpleMlRule(); + const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); + await retry.try(async () => { const stats = await getStats(supertest, log); const expected: DetectionMetrics = { @@ -105,13 +871,16 @@ export default ({ getService }: FtrProviderContext) => { ...getInitialDetectionMetrics().detection_rules, detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - query: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning, disabled: 1, + legacy_notifications_disabled: 1, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, + legacy_notifications_disabled: 1, }, }, }, @@ -119,14 +888,13 @@ export default ({ getService }: FtrProviderContext) => { expect(stats).to.eql(expected); }); }); - }); - describe('"eql" rule type', () => { - it('should show stats for active rule', async () => { - const rule: EqlCreateSchema = getEqlRuleForSignalTesting(['telemetry']); + it('should show "legacy_notifications_enabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "enabled"/"active"', async () => { + const rule = getSimpleMlRule('rule-1', true); const { id } = await createRule(supertest, log, rule); - await waitForRuleSuccessOrStatus(supertest, log, id); - await waitForSignalsToBePresent(supertest, log, 4, [id]); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); + await retry.try(async () => { const stats = await getStats(supertest, log); const expected: DetectionMetrics = { @@ -135,15 +903,16 @@ export default ({ getService }: FtrProviderContext) => { ...getInitialDetectionMetrics().detection_rules, detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - eql: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + machine_learning: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage + .machine_learning, enabled: 1, - alerts: 4, + legacy_notifications_enabled: 1, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, - alerts: 4, + legacy_notifications_enabled: 1, }, }, }, @@ -151,9 +920,11 @@ export default ({ getService }: FtrProviderContext) => { expect(stats).to.eql(expected); }); }); + }); - it('should show stats for in-active rule', async () => { - const rule: EqlCreateSchema = getEqlRuleForSignalTesting(['telemetry'], 'rule-1', false); + describe('"indicator_match/threat_match" rule type', () => { + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "disabled"/"in-active" rule that does not have any actions', async () => { + const rule = getSimpleThreatMatch(); await createRule(supertest, log, rule); await retry.try(async () => { const stats = await getStats(supertest, log); @@ -163,13 +934,21 @@ export default ({ getService }: FtrProviderContext) => { ...getInitialDetectionMetrics().detection_rules, detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - eql: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threat_match, disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, }, }, }, @@ -177,16 +956,23 @@ export default ({ getService }: FtrProviderContext) => { expect(stats).to.eql(expected); }); }); - }); - describe('"threshold" rule type', () => { - it('should show stats for active rule', async () => { - const rule: ThresholdCreateSchema = { - ...getThresholdRuleForSignalTesting(['telemetry']), - threshold: { - field: 'keyword', - value: 1, - }, + it('should show "notifications_enabled", "notifications_disabled" "legacy_notifications_enabled", "legacy_notifications_disabled", all to be "0" for "enabled"/"active" rule that does not have any actions', async () => { + const rule: ThreatMatchCreateSchema = { + ...getSimpleThreatMatch('rule-1', true), + index: ['telemetry'], + threat_index: ['telemetry'], + threat_mapping: [ + { + entries: [ + { + field: 'keyword', + value: 'keyword', + type: 'mapping', + }, + ], + }, + ], }; const { id } = await createRule(supertest, log, rule); await waitForRuleSuccessOrStatus(supertest, log, id); @@ -199,15 +985,23 @@ export default ({ getService }: FtrProviderContext) => { ...getInitialDetectionMetrics().detection_rules, detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - threshold: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threat_match, enabled: 1, alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, alerts: 4, + notifications_enabled: 0, + notifications_disabled: 0, + legacy_notifications_disabled: 0, + legacy_notifications_enabled: 0, }, }, }, @@ -216,15 +1010,12 @@ export default ({ getService }: FtrProviderContext) => { }); }); - it('should show stats for in-active rule', async () => { - const rule: ThresholdCreateSchema = { - ...getThresholdRuleForSignalTesting(['telemetry'], 'rule-1', false), - threshold: { - field: 'keyword', - value: 1, - }, - }; - await createRule(supertest, log, rule); + it('should show "notifications_disabled" to be "1" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { + const rule = getSimpleThreatMatch(); + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, false, rule); + await createRule(supertest, log, ruleToCreate); + await retry.try(async () => { const stats = await getStats(supertest, log); const expected: DetectionMetrics = { @@ -233,12 +1024,14 @@ export default ({ getService }: FtrProviderContext) => { ...getInitialDetectionMetrics().detection_rules, detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - threshold: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threat_match, + notifications_disabled: 1, disabled: 1, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, + notifications_disabled: 1, disabled: 1, }, }, @@ -247,13 +1040,30 @@ export default ({ getService }: FtrProviderContext) => { expect(stats).to.eql(expected); }); }); - }); - describe('"ml" rule type', () => { - // Note: We don't actually find signals with this test as we don't have a good way of signal finding with ML rules. - it('should show stats for active rule', async () => { - const rule = getSimpleMlRule('rule-1', true); - await createRule(supertest, log, rule); + it('should show "notifications_enabled" to be "1" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { + const rule: ThreatMatchCreateSchema = { + ...getSimpleThreatMatch('rule-1', true), + index: ['telemetry'], + threat_index: ['telemetry'], + threat_mapping: [ + { + entries: [ + { + field: 'keyword', + value: 'keyword', + type: 'mapping', + }, + ], + }, + ], + }; + const hookAction = await createNewAction(supertest, log); + const ruleToCreate = getRuleWithWebHookAction(hookAction.id, true, rule); + const { id } = await createRule(supertest, log, ruleToCreate); + await waitForRuleSuccessOrStatus(supertest, log, id); + await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { const stats = await getStats(supertest, log); const expected: DetectionMetrics = { @@ -262,13 +1072,17 @@ export default ({ getService }: FtrProviderContext) => { ...getInitialDetectionMetrics().detection_rules, detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - machine_learning: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threat_match, enabled: 1, + alerts: 4, + notifications_enabled: 1, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, + alerts: 4, + notifications_enabled: 1, }, }, }, @@ -277,9 +1091,12 @@ export default ({ getService }: FtrProviderContext) => { }); }); - it('should show stats for in-active rule', async () => { - const rule = getSimpleMlRule(); - await createRule(supertest, log, rule); + it('should show "legacy_notifications_disabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "disabled"/"in-active"', async () => { + const rule = getSimpleThreatMatch(); + const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); + await retry.try(async () => { const stats = await getStats(supertest, log); const expected: DetectionMetrics = { @@ -288,13 +1105,15 @@ export default ({ getService }: FtrProviderContext) => { ...getInitialDetectionMetrics().detection_rules, detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - machine_learning: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, + threat_match: { + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threat_match, disabled: 1, + legacy_notifications_disabled: 1, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, disabled: 1, + legacy_notifications_disabled: 1, }, }, }, @@ -302,10 +1121,8 @@ export default ({ getService }: FtrProviderContext) => { expect(stats).to.eql(expected); }); }); - }); - describe('"indicator_match/threat_match" rule type', () => { - it('should show stats for active rule', async () => { + it('should show "legacy_notifications_enabled" to be "1" for rule that has at least "1" legacy action(s) and the alert is "enabled"/"active"', async () => { const rule: ThreatMatchCreateSchema = { ...getSimpleThreatMatch('rule-1', true), index: ['telemetry'], @@ -323,8 +1140,11 @@ export default ({ getService }: FtrProviderContext) => { ], }; const { id } = await createRule(supertest, log, rule); + const hookAction = await createNewAction(supertest, log); + await createLegacyRuleAction(supertest, id, hookAction.id); await waitForRuleSuccessOrStatus(supertest, log, id); await waitForSignalsToBePresent(supertest, log, 4, [id]); + await retry.try(async () => { const stats = await getStats(supertest, log); const expected: DetectionMetrics = { @@ -334,40 +1154,16 @@ export default ({ getService }: FtrProviderContext) => { detection_rule_usage: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, threat_match: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, - enabled: 1, + ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.threat_match, alerts: 4, - }, - custom_total: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, enabled: 1, - alerts: 4, - }, - }, - }, - }; - expect(stats).to.eql(expected); - }); - }); - - it('should show stats for in-active rule', async () => { - const rule = getSimpleThreatMatch(); - await createRule(supertest, log, rule); - await retry.try(async () => { - const stats = await getStats(supertest, log); - const expected: DetectionMetrics = { - ...getInitialDetectionMetrics(), - detection_rules: { - ...getInitialDetectionMetrics().detection_rules, - detection_rule_usage: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage, - threat_match: { - ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.query, - disabled: 1, + legacy_notifications_enabled: 1, }, custom_total: { ...getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total, - disabled: 1, + alerts: 4, + enabled: 1, + legacy_notifications_enabled: 1, }, }, }, @@ -377,7 +1173,7 @@ export default ({ getService }: FtrProviderContext) => { }); }); - describe('"pre-packaged" rules', async () => { + describe('"pre-packaged"/"immutable" rules', async () => { it('should show stats for totals for in-active pre-packaged rules', async () => { await installPrePackagedRules(supertest, log); await retry.try(async () => { @@ -385,12 +1181,33 @@ export default ({ getService }: FtrProviderContext) => { expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); expect(stats.detection_rules.detection_rule_usage.elastic_total.disabled).above(0); expect(stats.detection_rules.detection_rule_usage.elastic_total.enabled).above(0); - expect(stats.detection_rules.detection_rule_usage.custom_total.enabled).equal(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_enabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_disabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_enabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_disabled + ).to.eql(0); expect(stats.detection_rules.detection_rule_detail.length).above(0); + expect(stats.detection_rules.detection_rule_usage.custom_total).to.eql({ + enabled: 0, + disabled: 0, + alerts: 0, + cases: 0, + legacy_notifications_enabled: 0, + legacy_notifications_disabled: 0, + notifications_enabled: 0, + notifications_disabled: 0, + }); }); }); - it('should show stats for the detection_rule_details for pre-packaged rules', async () => { + it('should show stats for the detection_rule_details for a specific pre-packaged rule', async () => { await installPrePackagedRules(supertest, log); await retry.try(async () => { const stats = await getStats(supertest, log); @@ -402,7 +1219,7 @@ export default ({ getService }: FtrProviderContext) => { (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' ); if (foundRule == null) { - throw new Error('Found rule should not be null'); + throw new Error('Found rule should not be null. Please change this end to end test.'); } const { created_on: createdOn, @@ -418,7 +1235,227 @@ export default ({ getService }: FtrProviderContext) => { elastic_rule: true, alert_count_daily: 0, cases_count_total: 0, + has_notification: false, + has_legacy_notification: false, + }); + }); + }); + + it('should show "notifications_disabled" to be "1" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { + await installPrePackagedRules(supertest, log); + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json + const immutableRule = await getRule(supertest, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + const hookAction = await createNewAction(supertest, log); + const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, false, newRuleToUpdate); + await updateRule(supertest, log, ruleToUpdate); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id + const foundRule = stats.detection_rules.detection_rule_detail.find( + (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + ); + if (foundRule == null) { + throw new Error('Found rule should not be null. Please change this end to end test.'); + } + const { + created_on: createdOn, + updated_on: updatedOn, + rule_id: ruleId, + ...omittedFields + } = foundRule; + expect(omittedFields).to.eql({ + rule_name: 'Simple Rule Query', + rule_type: 'query', + rule_version: 3, + enabled: false, + elastic_rule: true, + alert_count_daily: 0, + cases_count_total: 0, + has_notification: true, + has_legacy_notification: false, + }); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_disabled + ).to.eql(1); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_enabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_disabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_enabled + ).to.eql(0); + expect(stats.detection_rules.detection_rule_usage.custom_total).to.eql( + getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total + ); + }); + }); + + it('should show "notifications_enabled" to be "1" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { + await installPrePackagedRules(supertest, log); + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json + const immutableRule = await getRule(supertest, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + const hookAction = await createNewAction(supertest, log); + const newRuleToUpdate = getSimpleRule(immutableRule.rule_id); + const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, true, newRuleToUpdate); + await updateRule(supertest, log, ruleToUpdate); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id + const foundRule = stats.detection_rules.detection_rule_detail.find( + (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + ); + if (foundRule == null) { + throw new Error('Found rule should not be null. Please change this end to end test.'); + } + const { + created_on: createdOn, + updated_on: updatedOn, + rule_id: ruleId, + ...omittedFields + } = foundRule; + expect(omittedFields).to.eql({ + rule_name: 'Simple Rule Query', + rule_type: 'query', + rule_version: 3, + enabled: true, + elastic_rule: true, + alert_count_daily: 0, + cases_count_total: 0, + has_notification: true, + has_legacy_notification: false, + }); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_disabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_enabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_disabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_enabled + ).to.eql(1); + expect(stats.detection_rules.detection_rule_usage.custom_total).to.eql( + getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total + ); + }); + }); + + it('should show "legacy_notifications_disabled" to be "1" for rule that has at least "1" action(s) and the alert is "disabled"/"in-active"', async () => { + await installPrePackagedRules(supertest, log); + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json + const immutableRule = await getRule(supertest, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + const hookAction = await createNewAction(supertest, log); + const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, false); + await updateRule(supertest, log, newRuleToUpdate); + await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id + const foundRule = stats.detection_rules.detection_rule_detail.find( + (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + ); + if (foundRule == null) { + throw new Error('Found rule should not be null. Please change this end to end test.'); + } + const { + created_on: createdOn, + updated_on: updatedOn, + rule_id: ruleId, + ...omittedFields + } = foundRule; + expect(omittedFields).to.eql({ + rule_name: 'Simple Rule Query', + rule_type: 'query', + rule_version: 3, + enabled: false, + elastic_rule: true, + alert_count_daily: 0, + cases_count_total: 0, + has_notification: false, + has_legacy_notification: true, + }); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_disabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_enabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_disabled + ).to.eql(1); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_enabled + ).to.eql(0); + expect(stats.detection_rules.detection_rule_usage.custom_total).to.eql( + getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total + ); + }); + }); + + it('should show "legacy_notifications_enabled" to be "1" for rule that has at least "1" action(s) and the alert is "enabled"/"active"', async () => { + await installPrePackagedRules(supertest, log); + // Rule id of "9a1a2dae-0b5f-4c3d-8305-a268d404c306" is from the file: + // x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint_security.json + const immutableRule = await getRule(supertest, log, '9a1a2dae-0b5f-4c3d-8305-a268d404c306'); + const hookAction = await createNewAction(supertest, log); + const newRuleToUpdate = getSimpleRule(immutableRule.rule_id, true); + await updateRule(supertest, log, newRuleToUpdate); + await createLegacyRuleAction(supertest, immutableRule.id, hookAction.id); + + await retry.try(async () => { + const stats = await getStats(supertest, log); + // We have to search by "rule_name" since the "rule_id" it is storing is the Saved Object ID and not the rule_id + const foundRule = stats.detection_rules.detection_rule_detail.find( + (rule) => rule.rule_id === '9a1a2dae-0b5f-4c3d-8305-a268d404c306' + ); + if (foundRule == null) { + throw new Error('Found rule should not be null. Please change this end to end test.'); + } + const { + created_on: createdOn, + updated_on: updatedOn, + rule_id: ruleId, + ...omittedFields + } = foundRule; + expect(omittedFields).to.eql({ + rule_name: 'Simple Rule Query', + rule_type: 'query', + rule_version: 3, + enabled: true, + elastic_rule: true, + alert_count_daily: 0, + cases_count_total: 0, + has_notification: false, + has_legacy_notification: true, }); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_disabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_enabled + ).to.eql(1); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.legacy_notifications_disabled + ).to.eql(0); + expect( + stats.detection_rules.detection_rule_usage.elastic_total.notifications_enabled + ).to.eql(0); + expect(stats.detection_rules.detection_rule_usage.custom_total).to.eql( + getInitialDetectionMetrics().detection_rules.detection_rule_usage.custom_total + ); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts index 21676d614bb2064..cf9db6373033afb 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/index.ts @@ -13,7 +13,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { describe('', function () { this.tags('ciGroup11'); loadTestFile(require.resolve('./detection_rules')); - loadTestFile(require.resolve('./legacy_notifications')); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/legacy_notifications.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/legacy_notifications.ts deleted file mode 100644 index aa406519e24439b..000000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/telemetry/legacy_notifications.ts +++ /dev/null @@ -1,75 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import { - createRule, - createSignalsIndex, - deleteAllAlerts, - deleteSignalsIndex, - getSimpleRule, - getStats, - getWebHookAction, -} from '../../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext) => { - const supertest = getService('supertest'); - const log = getService('log'); - const retry = getService('retry'); - - describe('legacy notification telemetry', async () => { - beforeEach(async () => { - await createSignalsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - }); - - it('should have 1 legacy notification when there is a rule on the default', async () => { - // create an connector/action - const { body: hookAction } = await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'true') - .send(getWebHookAction()) - .expect(200); - - // create a rule without actions - const createRuleBody = await createRule(supertest, log, getSimpleRule('rule-1')); - - // attach the legacy notification - await supertest - .post(`/internal/api/detection/legacy/notifications?alert_id=${createRuleBody.id}`) - .set('kbn-xsrf', 'true') - .send({ - name: 'Legacy notification with one action', - interval: '1h', - actions: [ - { - id: hookAction.id, - group: 'default', - params: { - message: - 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', - }, - actionTypeId: hookAction.actionTypeId, - }, - ], - }) - .expect(200); - - await retry.try(async () => { - const stats = await getStats(supertest, log); - // NOTE: We have to do "above 0" until this bug is fixed: https://github.com/elastic/kibana/issues/122456 because other tests are accumulating non-cleaned up legacy actions/notifications and this number isn't reliable at the moment - expect(stats.detection_rules.detection_rule_usage.legacy_notifications.total).to.above(0); - }); - }); - }); -}; diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/agent/stream/stream.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/agent/stream/stream.yml.hbs new file mode 100644 index 000000000000000..2870385f21f9591 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/agent/stream/stream.yml.hbs @@ -0,0 +1 @@ +config.version: "2" diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/fields/fields.yml new file mode 100644 index 000000000000000..6e003ed0ad14769 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/manifest.yml new file mode 100644 index 000000000000000..0b1ae9c6cb9959a --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/data_stream/log/manifest.yml @@ -0,0 +1,15 @@ +title: Test stream +type: logs +streams: + - input: test_input + vars: + - name: test_var_required + type: string + title: Test Var + required: true + show_user: true + - name: test_var + type: string + title: Test Var + required: false + show_user: true diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/docs/README.md new file mode 100644 index 000000000000000..d6cfcce90527cb0 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/docs/README.md @@ -0,0 +1,5 @@ +# filetest + +This package contains randomly collected files from other packages to be used in API integration tests. + +It also serves as an example how to serve a package from the fixtures directory with the package registry docker container. For this, also see the `x-pack/test/fleet_api_integration/config.ts` how the `test_packages` directory is mounted into the docker container, and `x-pack/test/fleet_api_integration/apis/fixtures/package_registry_config.yml` how to pass the directory to the registry. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/img/logo.svg b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/img/logo.svg new file mode 100644 index 000000000000000..15b49bcf28aec26 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/img/logo.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/img/screenshots/metricbeat_dashboard.png b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/img/screenshots/metricbeat_dashboard.png new file mode 100644 index 000000000000000..76d414b86c4ab44 Binary files /dev/null and b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/img/screenshots/metricbeat_dashboard.png differ diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/manifest.yml new file mode 100644 index 000000000000000..f1ed5a8a5a78bab --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/with_required_variables/0.1.0/manifest.yml @@ -0,0 +1,41 @@ +format_version: 1.0.0 +name: with_required_variables +title: Package with variables +description: This is a package. +version: 0.1.0 +categories: [] +# Options are experimental, beta, ga +release: beta +# The package type. The options for now are [integration, solution], more type might be added in the future. +# The default type is integration and will be set if empty. +type: integration +license: basic +# This package can be removed +removable: true + +requirement: + elasticsearch: + versions: ">7.7.0" + kibana: + versions: ">7.7.0" + +screenshots: +- src: "/img/screenshots/metricbeat_dashboard.png" + title: "metricbeat dashboard" + size: "1855x949" + type: "image/png" +icons: + - src: "/img/logo.svg" + size: "16x16" + type: "image/svg+xml" + + +policy_templates: + - name: with_required_variables + title: Package Policy Upgrade + description: Test Package for Upgrading Package Policies + inputs: + - type: test_input + title: Test Input + description: Test Input + enabled: true \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index 1815ab91b53165f..75d5c58d8e37590 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -324,5 +324,80 @@ export default function (providerContext: FtrProviderContext) { }) .expect(400); }); + + it('should return a 400 with required variables not provided', async function () { + const { body } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'pacakge-policy-required-variables-test-456', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [ + { + enabled: true, + streams: [ + { + data_stream: { + dataset: 'with_required_variables.log', + type: 'logs', + }, + enabled: true, + vars: {}, + }, + ], + type: 'test_input', + }, + ], + package: { + name: 'with_required_variables', + version: '0.1.0', + }, + }) + .expect(400); + expect(body.message).contain('Package policy is invalid'); + }); + + it('should work with required variables provided', async function () { + await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'pacakge-policy-required-variables-test-123', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [ + { + enabled: true, + streams: [ + { + data_stream: { + dataset: 'with_required_variables.log', + type: 'logs', + }, + enabled: true, + vars: { + test_var_required: { + value: 'I am required', + }, + }, + }, + ], + type: 'test_input', + }, + ], + package: { + name: 'with_required_variables', + version: '0.1.0', + }, + }) + .expect(200); + }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index 727a779178bb35a..0ccbb913f0d10c4 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -841,6 +841,7 @@ export default function (providerContext: FtrProviderContext) { policy_template: 'package_policy_upgrade', type: 'test_input_new_2', enabled: true, + vars: {}, streams: [ { id: 'test-package_policy_upgrade-xxxx', @@ -850,6 +851,12 @@ export default function (providerContext: FtrProviderContext) { dataset: 'package_policy_upgrade.test_stream_new_2', }, vars: { + test_input_new_2_var_1: { + value: 'Test input value 1', + }, + test_input_new_2_var_2: { + value: 'Test input value 2', + }, test_var_new_2_var_1: { value: 'Test value 1', }, @@ -867,7 +874,6 @@ export default function (providerContext: FtrProviderContext) { version: '0.5.0-restructure-inputs', }, }); - packagePolicyId = packagePolicyResponse.item.id; }); diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/mvt_scaling.js index 3e0e8924a6417ee..2be9606d11dea51 100644 --- a/x-pack/test/functional/apps/maps/mvt_scaling.js +++ b/x-pack/test/functional/apps/maps/mvt_scaling.js @@ -52,7 +52,7 @@ export default function ({ getPageObjects, getService }) { geometryFieldName: 'geometry', index: 'geo_shapes*', requestBody: - '(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))', + '(_source:!f,docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))', }); }); diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json index f9972f21eeb6429..cde819a836b0a31 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json @@ -22,3 +22,28 @@ } } } + +{ + "type":"doc", + "value":{ + "id":"a4cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f", + "index":"ml_host_risk_score_default", + "source":{ + "@timestamp":"2021-03-10T14:51:05.766Z", + "risk_stats": { + "risk_score": 21, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, + "host":{ + "name":"siem-kibana" + }, + "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", + "risk":"Low" + } + } +} diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json index 97d3288bf07b69c..02ceb5b5ebcccca 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json @@ -54,3 +54,61 @@ } } } + + +{ + "type": "index", + "value": { + "index": "ml_host_risk_score_default", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "host": { + "properties": { + "name": { + "type": "keyword" + } + } + }, + "ingest_timestamp": { + "type": "date" + }, + "risk": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "risk_stats": { + "properties": { + "risk_score": { + "type": "long" + } + } + } + } + }, + "settings": { + "index": { + "lifecycle": { + "name": "ml_host_risk_score_latest_default", + "rollover_alias": "ml_host_risk_score_latest_default" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "1", + "number_of_shards": "1", + "refresh_interval": "5s" + } + } + } +} diff --git a/yarn.lock b/yarn.lock index d2fcdbb0a5b0fb1..47a01e6cb8aeebd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10311,15 +10311,6 @@ concat-stream@1.6.2, concat-stream@^1.4.7, concat-stream@^1.5.0, concat-stream@^ readable-stream "^2.2.2" typedarray "^0.0.6" -concat-stream@~1.5.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" - integrity sha1-cIl4Yk2FavQaWnQd790mHadSwmY= - dependencies: - inherits "~2.0.1" - readable-stream "~2.0.0" - typedarray "~0.0.5" - concat-stream@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" @@ -22651,11 +22642,6 @@ process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= - process-on-spawn@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/process-on-spawn/-/process-on-spawn-1.0.0.tgz#95b05a23073d30a17acfdc92a440efd2baefdc93" @@ -24032,18 +24018,6 @@ readable-stream@~1.1.9: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@~2.0.0: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" - integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - readdir-glob@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4" @@ -27877,7 +27851,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typedarray@^0.0.6, typedarray@~0.0.5: +typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= @@ -29518,14 +29492,6 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== -wellknown@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/wellknown/-/wellknown-0.5.0.tgz#09ae9871fa826cf0a6ec1537ef00c379d78d7101" - integrity sha1-Ca6YcfqCbPCm7BU37wDDedeNcQE= - dependencies: - concat-stream "~1.5.0" - minimist "~1.2.0" - wgs84@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/wgs84/-/wgs84-0.0.0.tgz#34fdc555917b6e57cf2a282ed043710c049cdc76"