diff --git a/.eslintrc.js b/.eslintrc.js index 4425ad3a12659..a9ffe2850aa72 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -49,6 +49,31 @@ const ELASTIC_LICENSE_HEADER = ` */ `; +const SAFER_LODASH_SET_HEADER = ` +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See \`packages/elastic-safer-lodash-set/LICENSE\` for more information. + */ +`; + +const SAFER_LODASH_SET_LODASH_HEADER = ` +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See \`packages/elastic-safer-lodash-set/LICENSE\` for more information. + */ +`; + +const SAFER_LODASH_SET_DEFINITELYTYPED_HEADER = ` +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See \`packages/elastic-safer-lodash-set/LICENSE\` for more information. + */ +`; + const allMochaRulesOff = {}; Object.keys(require('eslint-plugin-mocha').rules).forEach((k) => { allMochaRulesOff['mocha/' + k] = 'off'; @@ -143,7 +168,12 @@ module.exports = { '@kbn/eslint/disallow-license-headers': [ 'error', { - licenses: [ELASTIC_LICENSE_HEADER], + licenses: [ + ELASTIC_LICENSE_HEADER, + SAFER_LODASH_SET_HEADER, + SAFER_LODASH_SET_LODASH_HEADER, + SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + ], }, ], }, @@ -174,7 +204,82 @@ module.exports = { '@kbn/eslint/disallow-license-headers': [ 'error', { - licenses: [APACHE_2_0_LICENSE_HEADER], + licenses: [ + APACHE_2_0_LICENSE_HEADER, + SAFER_LODASH_SET_HEADER, + SAFER_LODASH_SET_LODASH_HEADER, + SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + ], + }, + ], + }, + }, + + /** + * safer-lodash-set package requires special license headers + */ + { + files: ['packages/elastic-safer-lodash-set/**/*.{js,mjs,ts,tsx}'], + rules: { + '@kbn/eslint/require-license-header': [ + 'error', + { + license: SAFER_LODASH_SET_LODASH_HEADER, + }, + ], + '@kbn/eslint/disallow-license-headers': [ + 'error', + { + licenses: [ + ELASTIC_LICENSE_HEADER, + APACHE_2_0_LICENSE_HEADER, + SAFER_LODASH_SET_HEADER, + SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + ], + }, + ], + }, + }, + { + files: ['packages/elastic-safer-lodash-set/test/*.{js,mjs,ts,tsx}'], + rules: { + '@kbn/eslint/require-license-header': [ + 'error', + { + license: SAFER_LODASH_SET_HEADER, + }, + ], + '@kbn/eslint/disallow-license-headers': [ + 'error', + { + licenses: [ + ELASTIC_LICENSE_HEADER, + APACHE_2_0_LICENSE_HEADER, + SAFER_LODASH_SET_LODASH_HEADER, + SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + ], + }, + ], + }, + }, + { + files: ['packages/elastic-safer-lodash-set/**/*.d.ts'], + rules: { + '@kbn/eslint/require-license-header': [ + 'error', + { + license: SAFER_LODASH_SET_DEFINITELYTYPED_HEADER, + }, + ], + '@kbn/eslint/disallow-license-headers': [ + 'error', + { + licenses: [ + ELASTIC_LICENSE_HEADER, + APACHE_2_0_LICENSE_HEADER, + SAFER_LODASH_SET_HEADER, + SAFER_LODASH_SET_LODASH_HEADER, + ], }, ], }, @@ -541,9 +646,129 @@ module.exports = { * Harden specific rules */ { - files: ['test/harden/*.js'], + files: ['test/harden/*.js', 'packages/elastic-safer-lodash-set/test/*.js'], rules: allMochaRulesOff, }, + { + files: ['**/*.{js,mjs,ts,tsx}'], + rules: { + 'no-restricted-imports': [ + 2, + { + paths: [ + { + name: 'lodash', + importNames: ['set', 'setWith'], + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash.set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash.setwith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/setWith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp', + importNames: ['set', 'setWith', 'assoc', 'assocPath'], + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp/set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp/setWith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp/assoc', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/fp/assocPath', + message: 'Please use @elastic/safer-lodash-set instead', + }, + ], + }, + ], + 'no-restricted-modules': [ + 2, + { + paths: [ + { + name: 'lodash.set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash.setwith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + name: 'lodash/setWith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + ], + }, + ], + 'no-restricted-properties': [ + 2, + { + object: 'lodash', + property: 'set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + object: '_', + property: 'set', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + object: 'lodash', + property: 'setWith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + object: '_', + property: 'setWith', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + object: 'lodash', + property: 'assoc', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + object: '_', + property: 'assoc', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + object: 'lodash', + property: 'assocPath', + message: 'Please use @elastic/safer-lodash-set instead', + }, + { + object: '_', + property: 'assocPath', + message: 'Please use @elastic/safer-lodash-set instead', + }, + ], + }, + }, /** * APM overrides diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md index 51bc46bbdccc8..7bae595e75ad0 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md @@ -7,7 +7,7 @@ Signature: ```typescript -setup(core: CoreSetup, { expressions, uiActions }: DataSetupDependencies): DataPublicPluginSetup; +setup(core: CoreSetup, { expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup; ``` ## Parameters @@ -15,7 +15,7 @@ setup(core: CoreSetup, { expressions, uiActions }: DataSetupDependencies): DataP | Parameter | Type | Description | | --- | --- | --- | | core | CoreSetup | | -| { expressions, uiActions } | DataSetupDependencies | | +| { expressions, uiActions, usageCollection } | DataSetupDependencies | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md index abd57f3a9568b..1291af5359887 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md @@ -18,4 +18,5 @@ export interface SearchInterceptorDeps | [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreStart['http'] | | | [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsStart | | | [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreStart['uiSettings'] | | +| [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md) | SearchUsageCollector | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md new file mode 100644 index 0000000000000..21afce1927676 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md) + +## SearchInterceptorDeps.usageCollector property + +Signature: + +```typescript +usageCollector?: SearchUsageCollector; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md index ca8ad8fdc06ea..3afba80064f08 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md @@ -14,5 +14,6 @@ export interface ISearchSetup | Property | Type | Description | | --- | --- | --- | -| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | (name: string, strategy: ISearchStrategy) => void | Extension point exposed for other plugins to register their own search strategies. | +| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | TRegisterSearchStrategy | Extension point exposed for other plugins to register their own search strategies. | +| [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md) | SearchUsage | Used internally for telemetry | diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md new file mode 100644 index 0000000000000..85abd9d9dba98 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) > [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md) + +## ISearchSetup.usage property + +Used internally for telemetry + +Signature: + +```typescript +usage: SearchUsage; +``` diff --git a/package.json b/package.json index 55a099b4e5c0c..190eb6d7d94b4 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "@elastic/good": "8.1.1-kibana2", "@elastic/numeral": "^2.5.0", "@elastic/request-crypto": "1.1.4", + "@elastic/safer-lodash-set": "0.0.0", "@elastic/ui-ace": "0.2.3", "@hapi/good-squeeze": "5.2.1", "@hapi/wreck": "^15.0.2", diff --git a/packages/elastic-safer-lodash-set/.gitignore b/packages/elastic-safer-lodash-set/.gitignore new file mode 100644 index 0000000000000..b152df746bf26 --- /dev/null +++ b/packages/elastic-safer-lodash-set/.gitignore @@ -0,0 +1,2 @@ +.tmp +node_modules diff --git a/packages/elastic-safer-lodash-set/.npmignore b/packages/elastic-safer-lodash-set/.npmignore new file mode 100644 index 0000000000000..c2c910c637c01 --- /dev/null +++ b/packages/elastic-safer-lodash-set/.npmignore @@ -0,0 +1,3 @@ +tsconfig.json +scripts +test diff --git a/packages/elastic-safer-lodash-set/LICENSE b/packages/elastic-safer-lodash-set/LICENSE new file mode 100644 index 0000000000000..049225c0b6647 --- /dev/null +++ b/packages/elastic-safer-lodash-set/LICENSE @@ -0,0 +1,34 @@ +The MIT License (MIT) + +Copyright (c) Elasticsearch BV +Copyright (c) Brian Zengel , Ilya Mochalov +Copyright (c) JS Foundation and other contributors + +Lodash is based on Underscore.js, copyright Jeremy Ashkenas, +DocumentCloud and Investigative Reporters & Editors + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at the following locations: + - https://github.com/lodash/lodash + - https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/lodash + - https://github.com/elastic/kibana/tree/master/packages/elastic-safer-lodash-set + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/elastic-safer-lodash-set/README.md b/packages/elastic-safer-lodash-set/README.md new file mode 100644 index 0000000000000..aae17b35ac130 --- /dev/null +++ b/packages/elastic-safer-lodash-set/README.md @@ -0,0 +1,113 @@ +# @elastic/safer-lodash-set + +This module adds protection against prototype pollution to the [`set`] +and [`setWith`] functions from [Lodash] and are API compatible with +Lodash v4.x. + +## Example Usage + +```js +const { set } = require('@elastic/safer-loadsh-set'); + +const object = { a: [{ b: { c: 3 } }] }; + +set(object, 'a[0].b.c', 4); +console.log(object.a[0].b.c); // => 4 + +set(object, ['x', '0', 'y', 'z'], 5); +console.log(object.x[0].y.z); // => 5 +``` + +## API + +The main module exposes two functions, `set` and `setWith`: + +```js +const { set, setWith } = require('@elastic/safer-lodash-set'); +``` + +Besides the main module, it's also possible to require each function +individually: + +```js +const set = require('@elastic/safer-lodash-set/set'); +const setWith = require('@elastic/safer-lodash-set/setWith'); +``` + +The APIs of these functions are identical to the equivalent Lodash +[`set`] and [`setWith`] functions. Please refer to the Lodash +documentation for the respective functions for details. + +### Functional Programming support (fp) + +This module also supports the `lodash/fp` api and hence exposes the +following fp compatible functions: + +```js +const { set, setWith } = require('@elastic/safer-lodash-set/fp'); +``` + +Besides the main fp module, it's also possible to require each function +individually: + +```js +const set = require('@elastic/safer-lodash-set/fp/set'); +const setWith = require('@elastic/safer-lodash-set/fp/setWith'); +``` + +## Limitations + +The safety improvements in this module is achieved by adding the +following limitations to the algorithm used to walk the `path` given as +the 2nd argument to the `set` and `setWith` functions: + +### Only own properties are followed when walking the `path` + +```js +const parent = { foo: 1 }; +const child = { bar: 2 }; + +Object.setPrototypeOf(child, parent); + +// Now `child` can access `foo` through prototype inheritance +console.log(child.foo); // 1 + +set(child, 'foo', 3); + +// A different `foo` property has now been added directly to the `child` +// object and the `parent` object has not been modified: +console.log(child.foo); // 3 +console.log(parent.foo); // 1 +console.log(Object.prototype.hasOwnProperty.call(child, 'foo')); // true +``` + +### The `path` must not access function prototypes + +```js +const object = { + fn1: function () {}, + fn2: () => {}, +}; + +// Attempting to access any function prototype will result in an +// exception being thrown: +assert.throws(() => { + // Throws: Illegal access of function prototype + set(object, 'fn1.prototype.toString', 'bang!'); +}); + +// This also goes for arrow functions even though they don't have a +// prototype property. This is just to keep things consistent: +assert.throws(() => { + // Throws: Illegal access of function prototype + set(object, 'fn2.prototype.toString', 'bang!'); +}); +``` + +## License + +[MIT](LICENSE) + +[`set`]: https://lodash.com/docs/4.17.15#set +[`setwith`]: https://lodash.com/docs/4.17.15#setWith +[lodash]: https://lodash.com/ diff --git a/packages/elastic-safer-lodash-set/fp/assoc.d.ts b/packages/elastic-safer-lodash-set/fp/assoc.d.ts new file mode 100644 index 0000000000000..57fe84d0b07f2 --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/assoc.d.ts @@ -0,0 +1,9 @@ +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { assoc } from './index'; +export = assoc; diff --git a/packages/elastic-safer-lodash-set/fp/assoc.js b/packages/elastic-safer-lodash-set/fp/assoc.js new file mode 100644 index 0000000000000..851e11690ea35 --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/assoc.js @@ -0,0 +1,8 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +module.exports = require('./set'); diff --git a/packages/elastic-safer-lodash-set/fp/assocPath.d.ts b/packages/elastic-safer-lodash-set/fp/assocPath.d.ts new file mode 100644 index 0000000000000..76df38e98ff28 --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/assocPath.d.ts @@ -0,0 +1,9 @@ +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { assocPath } from './index'; +export = assocPath; diff --git a/packages/elastic-safer-lodash-set/fp/assocPath.js b/packages/elastic-safer-lodash-set/fp/assocPath.js new file mode 100644 index 0000000000000..851e11690ea35 --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/assocPath.js @@ -0,0 +1,8 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +module.exports = require('./set'); diff --git a/packages/elastic-safer-lodash-set/fp/index.d.ts b/packages/elastic-safer-lodash-set/fp/index.d.ts new file mode 100644 index 0000000000000..fcd7ff01e3cc8 --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/index.d.ts @@ -0,0 +1,225 @@ +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import lodash = require('lodash'); + +export = SaferLodashSet; +export as namespace SaferLodashSet; + +declare const SaferLodashSet: SaferLodashSet.SaferLoDashStaticFp; +declare namespace SaferLodashSet { + interface LodashSet { + (path: lodash.PropertyPath): LodashSet1x1; + (path: lodash.__, value: any): LodashSet1x2; + (path: lodash.PropertyPath, value: any): LodashSet1x3; + (path: lodash.__, value: lodash.__, object: T): LodashSet1x4; + (path: lodash.PropertyPath, value: lodash.__, object: T): LodashSet1x5; + (path: lodash.__, value: any, object: T): LodashSet1x6; + (path: lodash.PropertyPath, value: any, object: T): T; + (path: lodash.__, value: lodash.__, object: object): LodashSet2x4; + (path: lodash.PropertyPath, value: lodash.__, object: object): LodashSet2x5; + (path: lodash.__, value: any, object: object): LodashSet2x6; + (path: lodash.PropertyPath, value: any, object: object): TResult; + } + interface LodashSet1x1 { + (value: any): LodashSet1x3; + (value: lodash.__, object: T): LodashSet1x5; + (value: any, object: T): T; + (value: lodash.__, object: object): LodashSet2x5; + (value: any, object: object): TResult; + } + interface LodashSet1x2 { + (path: lodash.PropertyPath): LodashSet1x3; + (path: lodash.__, object: T): LodashSet1x6; + (path: lodash.PropertyPath, object: T): T; + (path: lodash.__, object: object): LodashSet2x6; + (path: lodash.PropertyPath, object: object): TResult; + } + interface LodashSet1x3 { + (object: T): T; + (object: object): TResult; + } + interface LodashSet1x4 { + (path: lodash.PropertyPath): LodashSet1x5; + (path: lodash.__, value: any): LodashSet1x6; + (path: lodash.PropertyPath, value: any): T; + } + type LodashSet1x5 = (value: any) => T; + type LodashSet1x6 = (path: lodash.PropertyPath) => T; + interface LodashSet2x4 { + (path: lodash.PropertyPath): LodashSet2x5; + (path: lodash.__, value: any): LodashSet2x6; + (path: lodash.PropertyPath, value: any): TResult; + } + type LodashSet2x5 = (value: any) => TResult; + type LodashSet2x6 = (path: lodash.PropertyPath) => TResult; + + interface LodashSetWith { + (customizer: lodash.SetWithCustomizer): LodashSetWith1x1; + (customizer: lodash.__, path: lodash.PropertyPath): LodashSetWith1x2; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.PropertyPath + ): LodashSetWith1x3; + (customizer: lodash.__, path: lodash.__, value: any): LodashSetWith1x4; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.__, + value: any + ): LodashSetWith1x5; + (customizer: lodash.__, path: lodash.PropertyPath, value: any): LodashSetWith1x6; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.PropertyPath, + value: any + ): LodashSetWith1x7; + ( + customizer: lodash.__, + path: lodash.__, + value: lodash.__, + object: T + ): LodashSetWith1x8; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.__, + value: lodash.__, + object: T + ): LodashSetWith1x9; + ( + customizer: lodash.__, + path: lodash.PropertyPath, + value: lodash.__, + object: T + ): LodashSetWith1x10; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.PropertyPath, + value: lodash.__, + object: T + ): LodashSetWith1x11; + ( + customizer: lodash.__, + path: lodash.__, + value: any, + object: T + ): LodashSetWith1x12; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.__, + value: any, + object: T + ): LodashSetWith1x13; + ( + customizer: lodash.__, + path: lodash.PropertyPath, + value: any, + object: T + ): LodashSetWith1x14; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.PropertyPath, + value: any, + object: T + ): T; + } + interface LodashSetWith1x1 { + (path: lodash.PropertyPath): LodashSetWith1x3; + (path: lodash.__, value: any): LodashSetWith1x5; + (path: lodash.PropertyPath, value: any): LodashSetWith1x7; + (path: lodash.__, value: lodash.__, object: T): LodashSetWith1x9; + (path: lodash.PropertyPath, value: lodash.__, object: T): LodashSetWith1x11; + (path: lodash.__, value: any, object: T): LodashSetWith1x13; + (path: lodash.PropertyPath, value: any, object: T): T; + } + interface LodashSetWith1x2 { + (customizer: lodash.SetWithCustomizer): LodashSetWith1x3; + (customizer: lodash.__, value: any): LodashSetWith1x6; + (customizer: lodash.SetWithCustomizer, value: any): LodashSetWith1x7; + (customizer: lodash.__, value: lodash.__, object: T): LodashSetWith1x10; + ( + customizer: lodash.SetWithCustomizer, + value: lodash.__, + object: T + ): LodashSetWith1x11; + (customizer: lodash.__, value: any, object: T): LodashSetWith1x14; + (customizer: lodash.SetWithCustomizer, value: any, object: T): T; + } + interface LodashSetWith1x3 { + (value: any): LodashSetWith1x7; + (value: lodash.__, object: T): LodashSetWith1x11; + (value: any, object: T): T; + } + interface LodashSetWith1x4 { + (customizer: lodash.SetWithCustomizer): LodashSetWith1x5; + (customizer: lodash.__, path: lodash.PropertyPath): LodashSetWith1x6; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.PropertyPath + ): LodashSetWith1x7; + (customizer: lodash.__, path: lodash.__, object: T): LodashSetWith1x12; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.__, + object: T + ): LodashSetWith1x13; + ( + customizer: lodash.__, + path: lodash.PropertyPath, + object: T + ): LodashSetWith1x14; + ( + customizer: lodash.SetWithCustomizer, + path: lodash.PropertyPath, + object: T + ): T; + } + interface LodashSetWith1x5 { + (path: lodash.PropertyPath): LodashSetWith1x7; + (path: lodash.__, object: T): LodashSetWith1x13; + (path: lodash.PropertyPath, object: T): T; + } + interface LodashSetWith1x6 { + (customizer: lodash.SetWithCustomizer): LodashSetWith1x7; + (customizer: lodash.__, object: T): LodashSetWith1x14; + (customizer: lodash.SetWithCustomizer, object: T): T; + } + type LodashSetWith1x7 = (object: T) => T; + interface LodashSetWith1x8 { + (customizer: lodash.SetWithCustomizer): LodashSetWith1x9; + (customizer: lodash.__, path: lodash.PropertyPath): LodashSetWith1x10; + (customizer: lodash.SetWithCustomizer, path: lodash.PropertyPath): LodashSetWith1x11; + (customizer: lodash.__, path: lodash.__, value: any): LodashSetWith1x12; + (customizer: lodash.SetWithCustomizer, path: lodash.__, value: any): LodashSetWith1x13; + (customizer: lodash.__, path: lodash.PropertyPath, value: any): LodashSetWith1x14; + (customizer: lodash.SetWithCustomizer, path: lodash.PropertyPath, value: any): T; + } + interface LodashSetWith1x9 { + (path: lodash.PropertyPath): LodashSetWith1x11; + (path: lodash.__, value: any): LodashSetWith1x13; + (path: lodash.PropertyPath, value: any): T; + } + interface LodashSetWith1x10 { + (customizer: lodash.SetWithCustomizer): LodashSetWith1x11; + (customizer: lodash.__, value: any): LodashSetWith1x14; + (customizer: lodash.SetWithCustomizer, value: any): T; + } + type LodashSetWith1x11 = (value: any) => T; + interface LodashSetWith1x12 { + (customizer: lodash.SetWithCustomizer): LodashSetWith1x13; + (customizer: lodash.__, path: lodash.PropertyPath): LodashSetWith1x14; + (customizer: lodash.SetWithCustomizer, path: lodash.PropertyPath): T; + } + type LodashSetWith1x13 = (path: lodash.PropertyPath) => T; + type LodashSetWith1x14 = (customizer: lodash.SetWithCustomizer) => T; + + interface SaferLoDashStaticFp { + assoc: LodashSet; + assocPath: LodashSet; + set: LodashSet; + setWith: LodashSetWith; + } +} diff --git a/packages/elastic-safer-lodash-set/fp/index.js b/packages/elastic-safer-lodash-set/fp/index.js new file mode 100644 index 0000000000000..7d9cdb099dfd7 --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/index.js @@ -0,0 +1,9 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +exports.set = exports.assoc = exports.assocPath = require('./set'); +exports.setWith = require('./setWith'); diff --git a/packages/elastic-safer-lodash-set/fp/set.d.ts b/packages/elastic-safer-lodash-set/fp/set.d.ts new file mode 100644 index 0000000000000..16bc98658bdcd --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/set.d.ts @@ -0,0 +1,9 @@ +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { set } from './index'; +export = set; diff --git a/packages/elastic-safer-lodash-set/fp/set.js b/packages/elastic-safer-lodash-set/fp/set.js new file mode 100644 index 0000000000000..0fb48694d736d --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/set.js @@ -0,0 +1,13 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +/*eslint no-var:0 */ +var convert = require('lodash/fp/convert'); +var func = convert('set', require('../set')); + +func.placeholder = require('lodash/fp/placeholder'); +module.exports = func; diff --git a/packages/elastic-safer-lodash-set/fp/setWith.d.ts b/packages/elastic-safer-lodash-set/fp/setWith.d.ts new file mode 100644 index 0000000000000..556e702f59f0f --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/setWith.d.ts @@ -0,0 +1,9 @@ +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { setWith } from './index'; +export = setWith; diff --git a/packages/elastic-safer-lodash-set/fp/setWith.js b/packages/elastic-safer-lodash-set/fp/setWith.js new file mode 100644 index 0000000000000..e477d4b4bc7ba --- /dev/null +++ b/packages/elastic-safer-lodash-set/fp/setWith.js @@ -0,0 +1,13 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +/*eslint no-var:0 */ +var convert = require('lodash/fp/convert'); +var func = convert('setWith', require('../setWith')); + +func.placeholder = require('lodash/fp/placeholder'); +module.exports = func; diff --git a/packages/elastic-safer-lodash-set/index.d.ts b/packages/elastic-safer-lodash-set/index.d.ts new file mode 100644 index 0000000000000..aaff01f11a7af --- /dev/null +++ b/packages/elastic-safer-lodash-set/index.d.ts @@ -0,0 +1,64 @@ +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +export = SaferLodashSet; +export as namespace SaferLodashSet; + +type Many = T | readonly T[]; +type PropertyName = string | number | symbol; +type PropertyPath = Many; +type SetWithCustomizer = (nsValue: any, key: string, nsObject: T) => any; + +declare const SaferLodashSet: SaferLodashSet.SaferLoDashStatic; +declare namespace SaferLodashSet { + interface SaferLoDashStatic { + /** + * Sets the value at path of object. If a portion of path doesn’t exist it’s + * created. Arrays are created for missing index properties while objects + * are created for all other missing properties. Use SaferLodashSet.setWith + * to customize path creation. + * + * @param object The object to modify. + * @param path The path of the property to set. + * @param value The value to set. + * @return Returns object. + */ + set(object: T, path: PropertyPath, value: any): T; + /** + * @see SaferLodashSet.set + */ + set(object: object, path: PropertyPath, value: any): TResult; + + /** + * This method is like SaferLodashSet.set except that it accepts customizer + * which is invoked to produce the objects of path. If customizer returns + * undefined path creation is handled by the method instead. The customizer + * is invoked with three arguments: (nsValue, key, nsObject). + * + * @param object The object to modify. + * @param path The path of the property to set. + * @param value The value to set. + * @param customizer The function to customize assigned values. + * @return Returns object. + */ + setWith( + object: T, + path: PropertyPath, + value: any, + customizer?: SetWithCustomizer + ): T; + /** + * @see SaferLodashSet.setWith + */ + setWith( + object: T, + path: PropertyPath, + value: any, + customizer?: SetWithCustomizer + ): TResult; + } +} diff --git a/packages/elastic-safer-lodash-set/index.js b/packages/elastic-safer-lodash-set/index.js new file mode 100644 index 0000000000000..d9edb25476c12 --- /dev/null +++ b/packages/elastic-safer-lodash-set/index.js @@ -0,0 +1,9 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +exports.set = require('./lodash/set'); +exports.setWith = require('./lodash/setWith'); diff --git a/packages/elastic-safer-lodash-set/lodash/_baseSet.js b/packages/elastic-safer-lodash-set/lodash/_baseSet.js new file mode 100644 index 0000000000000..9cbf19808edd7 --- /dev/null +++ b/packages/elastic-safer-lodash-set/lodash/_baseSet.js @@ -0,0 +1,61 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +/* eslint-disable */ + +var assignValue = require('lodash/_assignValue'), + castPath = require('lodash/_castPath'), + isFunction = require('lodash/isFunction'), + isIndex = require('lodash/_isIndex'), + isObject = require('lodash/isObject'), + toKey = require('lodash/_toKey'); + +/** + * The base implementation of `_.set`. + * + * @private + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @param {Function} [customizer] The function to customize path creation. + * @returns {Object} Returns `object`. + */ +function baseSet(object, path, value, customizer) { + if (!isObject(object)) { + return object; + } + path = castPath(path, object); + + var index = -1, + length = path.length, + lastIndex = length - 1, + nested = object; + + while (nested != null && ++index < length) { + var key = toKey(path[index]), + newValue = value; + + if (key == 'prototype' && isFunction(nested)) { + throw new Error('Illegal access of function prototype') + } + + if (index != lastIndex) { + var objValue = hasOwnProperty.call(nested, key) ? nested[key] : undefined + newValue = customizer ? customizer(objValue, key, nested) : undefined; + if (newValue === undefined) { + newValue = isObject(objValue) + ? objValue + : (isIndex(path[index + 1]) ? [] : {}); + } + } + assignValue(nested, key, newValue); + nested = nested[key]; + } + return object; +} + +module.exports = baseSet; diff --git a/packages/elastic-safer-lodash-set/lodash/set.js b/packages/elastic-safer-lodash-set/lodash/set.js new file mode 100644 index 0000000000000..740f7c926ee40 --- /dev/null +++ b/packages/elastic-safer-lodash-set/lodash/set.js @@ -0,0 +1,44 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +/* eslint-disable */ + +var baseSet = require('./_baseSet'); + +/** + * Sets the value at `path` of `object`. If a portion of `path` doesn't exist, + * it's created. Arrays are created for missing index properties while objects + * are created for all other missing properties. Use `_.setWith` to customize + * `path` creation. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 3.7.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @returns {Object} Returns `object`. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.set(object, 'a[0].b.c', 4); + * console.log(object.a[0].b.c); + * // => 4 + * + * _.set(object, ['x', '0', 'y', 'z'], 5); + * console.log(object.x[0].y.z); + * // => 5 + */ +function set(object, path, value) { + return object == null ? object : baseSet(object, path, value); +} + +module.exports = set; diff --git a/packages/elastic-safer-lodash-set/lodash/setWith.js b/packages/elastic-safer-lodash-set/lodash/setWith.js new file mode 100644 index 0000000000000..0ac4f4c9cf39f --- /dev/null +++ b/packages/elastic-safer-lodash-set/lodash/setWith.js @@ -0,0 +1,41 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +/* eslint-disable */ + +var baseSet = require('./_baseSet'); + +/** + * This method is like `_.set` except that it accepts `customizer` which is + * invoked to produce the objects of `path`. If `customizer` returns `undefined` + * path creation is handled by the method instead. The `customizer` is invoked + * with three arguments: (nsValue, key, nsObject). + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @since 4.0.0 + * @category Object + * @param {Object} object The object to modify. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @param {Function} [customizer] The function to customize assigned values. + * @returns {Object} Returns `object`. + * @example + * + * var object = {}; + * + * _.setWith(object, '[0][1]', 'a', Object); + * // => { '0': { '1': 'a' } } + */ +function setWith(object, path, value, customizer) { + customizer = typeof customizer == 'function' ? customizer : undefined; + return object == null ? object : baseSet(object, path, value, customizer); +} + +module.exports = setWith; diff --git a/packages/elastic-safer-lodash-set/package.json b/packages/elastic-safer-lodash-set/package.json new file mode 100644 index 0000000000000..f0f425661f605 --- /dev/null +++ b/packages/elastic-safer-lodash-set/package.json @@ -0,0 +1,49 @@ +{ + "name": "@elastic/safer-lodash-set", + "version": "0.0.0", + "description": "A safer version of the lodash set and setWith functions", + "main": "index.js", + "types": "index.d.ts", + "dependencies": {}, + "devDependencies": { + "dependency-check": "^4.1.0", + "tape": "^5.0.1", + "tsd": "^0.13.1" + }, + "peerDependencies": { + "lodash": "4.x" + }, + "scripts": { + "lint": "dependency-check --no-dev package.json set.js setWith.js fp/*.js", + "test": "npm run lint && tape test/*.js && npm run test:types", + "test:types": "./scripts/tsd.sh", + "update": "./scripts/update.sh", + "save_state": "./scripts/save_state.sh" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/elastic/kibana.git" + }, + "keywords": [ + "lodash", + "security", + "set", + "setWith", + "prototype", + "pollution" + ], + "author": "Thomas Watson (https://twitter.com/wa7son)", + "license": "MIT", + "bugs": { + "url": "https://github.com/elastic/kibana/issues" + }, + "homepage": "https://github.com/elastic/kibana/tree/master/packages/safer-lodash-set#readme", + "standard": { + "ignore": [ + "/lodash/" + ] + }, + "tsd": { + "directory": "test" + } +} diff --git a/packages/elastic-safer-lodash-set/scripts/_get_lodash.sh b/packages/elastic-safer-lodash-set/scripts/_get_lodash.sh new file mode 100755 index 0000000000000..50d3edaf34717 --- /dev/null +++ b/packages/elastic-safer-lodash-set/scripts/_get_lodash.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Elasticsearch B.V licenses this file to you under the MIT License. +# See `packages/elastic-safer-lodash-set/LICENSE` for more information. + +clean_up () { + exit_code=$? + rm -fr .tmp + exit $exit_code +} +trap clean_up EXIT + +# Get a temporary copy of the latest v4 lodash +rm -fr .tmp +npm install --no-fund --ignore-scripts --no-audit --loglevel error --prefix ./.tmp lodash@4 > /dev/null diff --git a/packages/elastic-safer-lodash-set/scripts/license-header.txt b/packages/elastic-safer-lodash-set/scripts/license-header.txt new file mode 100644 index 0000000000000..4d0aedf74bb0f --- /dev/null +++ b/packages/elastic-safer-lodash-set/scripts/license-header.txt @@ -0,0 +1,7 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + diff --git a/packages/elastic-safer-lodash-set/scripts/patches/_baseSet.js.patch b/packages/elastic-safer-lodash-set/scripts/patches/_baseSet.js.patch new file mode 100644 index 0000000000000..c7cf2041355d0 --- /dev/null +++ b/packages/elastic-safer-lodash-set/scripts/patches/_baseSet.js.patch @@ -0,0 +1,31 @@ +1,5c1,15 +< var assignValue = require('./_assignValue'), +< castPath = require('./_castPath'), +< isIndex = require('./_isIndex'), +< isObject = require('./isObject'), +< toKey = require('./_toKey'); +--- +> /* +> * This file is forked from the lodash project (https://lodash.com/), +> * and may include modifications made by Elasticsearch B.V. +> * Elasticsearch B.V. licenses this file to you under the MIT License. +> * See `packages/elastic-safer-lodash-set/LICENSE` for more information. +> */ +> +> /* eslint-disable */ +> +> var assignValue = require('lodash/_assignValue'), +> castPath = require('lodash/_castPath'), +> isFunction = require('lodash/isFunction'), +> isIndex = require('lodash/_isIndex'), +> isObject = require('lodash/isObject'), +> toKey = require('lodash/_toKey'); +31a42,45 +> if (key == 'prototype' && isFunction(nested)) { +> throw new Error('Illegal access of function prototype') +> } +> +33c47 +< var objValue = nested[key]; +--- +> var objValue = hasOwnProperty.call(nested, key) ? nested[key] : undefined diff --git a/packages/elastic-safer-lodash-set/scripts/save_state.sh b/packages/elastic-safer-lodash-set/scripts/save_state.sh new file mode 100755 index 0000000000000..ead99c3d1de48 --- /dev/null +++ b/packages/elastic-safer-lodash-set/scripts/save_state.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Elasticsearch B.V licenses this file to you under the MIT License. +# See `packages/elastic-safer-lodash-set/LICENSE` for more information. + +set -e + +source ./scripts/_get_lodash.sh + +modified_lodash_files=(_baseSet.js) + +# Create fresh patch files for each of the modified files +for file in "${modified_lodash_files[@]}" +do + diff ".tmp/node_modules/lodash/$file" "lodash/$file" > "scripts/patches/$file.patch" || true +done + +echo "State updated!" diff --git a/packages/elastic-safer-lodash-set/scripts/tsd.sh b/packages/elastic-safer-lodash-set/scripts/tsd.sh new file mode 100755 index 0000000000000..4572367df415d --- /dev/null +++ b/packages/elastic-safer-lodash-set/scripts/tsd.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +# Elasticsearch B.V licenses this file to you under the MIT License. +# See `packages/elastic-safer-lodash-set/LICENSE` for more information. + +# tsd will get confused if it finds a tsconfig.json file in the project +# directory and start to scan the entirety of Kibana. We don't want that. +mv tsconfig.json tsconfig.tmp + +clean_up () { + exit_code=$? + mv tsconfig.tmp tsconfig.json + exit $exit_code +} +trap clean_up EXIT + +./node_modules/.bin/tsd diff --git a/packages/elastic-safer-lodash-set/scripts/update.sh b/packages/elastic-safer-lodash-set/scripts/update.sh new file mode 100755 index 0000000000000..58fd89eb43e33 --- /dev/null +++ b/packages/elastic-safer-lodash-set/scripts/update.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Elasticsearch B.V licenses this file to you under the MIT License. +# See `packages/elastic-safer-lodash-set/LICENSE` for more information. + +set -e + +source ./scripts/_get_lodash.sh + +all_files=$(cd lodash && ls) +modified_lodash_files=(_baseSet.js) + +# Get fresh copies of all the files that was originally copied from lodash, +# expect the ones in the whitelist +for file in $all_files +do + if [[ ! "${modified_lodash_files[@]}" =~ "${file}" ]] + then + cat scripts/license-header.txt > "lodash/$file" + printf "/* eslint-disable */\n\n" >> "lodash/$file" + cat ".tmp/node_modules/lodash/$file" >> "lodash/$file" + fi +done + +# Check if there's changes to the patched files +for file in "${modified_lodash_files[@]}" +do + diff ".tmp/node_modules/lodash/$file" "lodash/$file" > ".tmp/$file.patch" || true + if [[ $(diff ".tmp/$file.patch" "scripts/patches/$file.patch") ]]; then + echo "WARNING: The modified file $file have changed in a newer version of lodash, but was not updated:" + echo "------------------------------------------------------------------------" + diff ".tmp/$file.patch" "scripts/patches/$file.patch" || true + echo "------------------------------------------------------------------------" + fi +done + +echo "Update complete!" diff --git a/packages/elastic-safer-lodash-set/set.d.ts b/packages/elastic-safer-lodash-set/set.d.ts new file mode 100644 index 0000000000000..16bc98658bdcd --- /dev/null +++ b/packages/elastic-safer-lodash-set/set.d.ts @@ -0,0 +1,9 @@ +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { set } from './index'; +export = set; diff --git a/packages/elastic-safer-lodash-set/set.js b/packages/elastic-safer-lodash-set/set.js new file mode 100644 index 0000000000000..6977062908549 --- /dev/null +++ b/packages/elastic-safer-lodash-set/set.js @@ -0,0 +1,8 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +module.exports = require('./lodash/set'); diff --git a/packages/elastic-safer-lodash-set/setWith.d.ts b/packages/elastic-safer-lodash-set/setWith.d.ts new file mode 100644 index 0000000000000..556e702f59f0f --- /dev/null +++ b/packages/elastic-safer-lodash-set/setWith.d.ts @@ -0,0 +1,9 @@ +/* + * This file is forked from the DefinitelyTyped project (https://github.com/DefinitelyTyped/DefinitelyTyped), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { setWith } from './index'; +export = setWith; diff --git a/packages/elastic-safer-lodash-set/setWith.js b/packages/elastic-safer-lodash-set/setWith.js new file mode 100644 index 0000000000000..aafa8a4db4be6 --- /dev/null +++ b/packages/elastic-safer-lodash-set/setWith.js @@ -0,0 +1,8 @@ +/* + * This file is forked from the lodash project (https://lodash.com/), + * and may include modifications made by Elasticsearch B.V. + * Elasticsearch B.V. licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +module.exports = require('./lodash/setWith'); diff --git a/packages/elastic-safer-lodash-set/test/fp.test-d.ts b/packages/elastic-safer-lodash-set/test/fp.test-d.ts new file mode 100644 index 0000000000000..7a1d6601b5e26 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/fp.test-d.ts @@ -0,0 +1,85 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import { set, setWith, assoc, assocPath } from '../fp'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +function customizer(value: any, key: string, obj: object) { + expectType(value); + expectType(key); + expectType(obj); +} + +expectType(set('a.b.c', anyValue, someObj)); +expectType(set('a.b.c')(anyValue, someObj)); +expectType(set('a.b.c')(anyValue)(someObj)); +expectType(set('a.b.c', anyValue)(someObj)); + +expectType(set(['a.b.c'], anyValue, someObj)); +expectType(set(['a.b.c'])(anyValue, someObj)); +expectType(set(['a.b.c'])(anyValue)(someObj)); +expectType(set(['a.b.c'], anyValue)(someObj)); + +expectType(set(['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(set(['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); +expectType(set(['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(set(['a.b.c', 2, Symbol('hep')], anyValue)(someObj)); + +expectType(assoc('a.b.c', anyValue, someObj)); +expectType(assoc('a.b.c')(anyValue, someObj)); +expectType(assoc('a.b.c')(anyValue)(someObj)); +expectType(assoc('a.b.c', anyValue)(someObj)); + +expectType(assoc(['a.b.c'], anyValue, someObj)); +expectType(assoc(['a.b.c'])(anyValue, someObj)); +expectType(assoc(['a.b.c'])(anyValue)(someObj)); +expectType(assoc(['a.b.c'], anyValue)(someObj)); + +expectType(assoc(['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(assoc(['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); +expectType(assoc(['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(assoc(['a.b.c', 2, Symbol('hep')], anyValue)(someObj)); + +expectType(assocPath('a.b.c', anyValue, someObj)); +expectType(assocPath('a.b.c')(anyValue, someObj)); +expectType(assocPath('a.b.c')(anyValue)(someObj)); +expectType(assocPath('a.b.c', anyValue)(someObj)); + +expectType(assocPath(['a.b.c'], anyValue, someObj)); +expectType(assocPath(['a.b.c'])(anyValue, someObj)); +expectType(assocPath(['a.b.c'])(anyValue)(someObj)); +expectType(assocPath(['a.b.c'], anyValue)(someObj)); + +expectType(assocPath(['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(assocPath(['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); +expectType(assocPath(['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(assocPath(['a.b.c', 2, Symbol('hep')], anyValue)(someObj)); + +expectType(setWith(customizer, 'a.b.c', anyValue, someObj)); +expectType(setWith(customizer)('a.b.c', anyValue, someObj)); +expectType(setWith(customizer)('a.b.c')(anyValue, someObj)); +expectType(setWith(customizer)('a.b.c')(anyValue)(someObj)); +expectType(setWith(customizer, 'a.b.c')(anyValue)(someObj)); +expectType(setWith(customizer, 'a.b.c', anyValue)(someObj)); +expectType(setWith(customizer, 'a.b.c')(anyValue, someObj)); + +expectType(setWith(customizer, ['a.b.c'], anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c'])(anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c'], anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c'])(anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c'])(anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c'], anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c'])(anyValue, someObj)); + +expectType(setWith(customizer, ['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c', 2, Symbol('hep')], anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); diff --git a/packages/elastic-safer-lodash-set/test/fp_assoc.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_assoc.test-d.ts new file mode 100644 index 0000000000000..8244458cd1180 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/fp_assoc.test-d.ts @@ -0,0 +1,25 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import assoc from '../fp/assoc'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +expectType(assoc('a.b.c', anyValue, someObj)); +expectType(assoc('a.b.c')(anyValue, someObj)); +expectType(assoc('a.b.c')(anyValue)(someObj)); +expectType(assoc('a.b.c', anyValue)(someObj)); + +expectType(assoc(['a.b.c'], anyValue, someObj)); +expectType(assoc(['a.b.c'])(anyValue, someObj)); +expectType(assoc(['a.b.c'])(anyValue)(someObj)); +expectType(assoc(['a.b.c'], anyValue)(someObj)); + +expectType(assoc(['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(assoc(['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); +expectType(assoc(['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(assoc(['a.b.c', 2, Symbol('hep')], anyValue)(someObj)); diff --git a/packages/elastic-safer-lodash-set/test/fp_assocPath.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_assocPath.test-d.ts new file mode 100644 index 0000000000000..abbfa57eeb963 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/fp_assocPath.test-d.ts @@ -0,0 +1,25 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import assocPath from '../fp/assocPath'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +expectType(assocPath('a.b.c', anyValue, someObj)); +expectType(assocPath('a.b.c')(anyValue, someObj)); +expectType(assocPath('a.b.c')(anyValue)(someObj)); +expectType(assocPath('a.b.c', anyValue)(someObj)); + +expectType(assocPath(['a.b.c'], anyValue, someObj)); +expectType(assocPath(['a.b.c'])(anyValue, someObj)); +expectType(assocPath(['a.b.c'])(anyValue)(someObj)); +expectType(assocPath(['a.b.c'], anyValue)(someObj)); + +expectType(assocPath(['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(assocPath(['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); +expectType(assocPath(['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(assocPath(['a.b.c', 2, Symbol('hep')], anyValue)(someObj)); diff --git a/packages/elastic-safer-lodash-set/test/fp_patch_test.js b/packages/elastic-safer-lodash-set/test/fp_patch_test.js new file mode 100644 index 0000000000000..362ecf6f9d866 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/fp_patch_test.js @@ -0,0 +1,290 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +const test = require('tape'); + +const setFunctions = [ + [testSet, require('../fp').set, 'fp.set'], + [testSet, require('../fp/set'), 'fp/set'], + [testSet, require('../fp').assoc, 'fp.assoc'], + [testSet, require('../fp/assoc'), 'fp/assoc'], + [testSet, require('../fp').assocPath, 'fp.assocPath'], + [testSet, require('../fp/assocPath'), 'fp/assocPath'], + [testSetWithAsSet, require('../fp').setWith, 'fp.setWith'], + [testSetWithAsSet, require('../fp/setWith'), 'fp/setWith'], +]; +const setWithFunctions = [ + [testSetWith, require('../fp').setWith, 'fp.setWith'], + [testSetWith, require('../fp/setWith'), 'fp/setWith'], +]; + +function testSet(fn, args, onCall) { + const [a, b, c] = args; + onCall(fn(b, c, a)); + onCall(fn(b, c)(a)); + onCall(fn(b)(c, a)); + onCall(fn(b)(c)(a)); +} +testSet.assertionCalls = 4; + +function testSetWith(fn, args, onCall) { + const [a, b, c, d] = args; + onCall(fn(d, b, c, a)); + onCall(fn(d)(b, c, a)); + onCall(fn(d)(b)(c, a)); + onCall(fn(d)(b)(c)(a)); + onCall(fn(d, b)(c)(a)); + onCall(fn(d, b, c)(a)); + onCall(fn(d)(b, c)(a)); +} +testSetWith.assertionCalls = 7; + +// use `fp.setWith` with the same API as `fp.set` by injecting a noop function as the first argument +function testSetWithAsSet(fn, args, onCall) { + args.push(() => {}); + testSetWith(fn, args, onCall); +} +testSetWithAsSet.assertionCalls = testSetWith.assertionCalls; + +setFunctions.forEach(([testPermutations, set, testName]) => { + /** + * GENERAL USAGE TESTS + */ + + const isSetWith = testPermutations.name === 'testSetWithAsSet'; + + test(`${testName}: No side-effects`, (t) => { + t.plan(testPermutations.assertionCalls * 5); + const o1 = { + a: { b: 1 }, + c: { d: 2 }, + }; + testPermutations(set, [o1, 'a.b', 3], (o2) => { + t.notStrictEqual(o1, o2); // clone touched paths + t.notStrictEqual(o1.a, o2.a); // clone touched paths + t.deepEqual(o1.c, o2.c); // do not clone untouched paths + t.deepEqual(o1, { a: { b: 1 }, c: { d: 2 } }); + t.deepEqual(o2, { a: { b: 3 }, c: { d: 2 } }); + }); + }); + + test(`${testName}: Non-objects`, (t) => { + const nonObjects = [null, undefined, NaN, 42]; + t.plan(testPermutations.assertionCalls * nonObjects.length * 3); + nonObjects.forEach((nonObject) => { + t.comment(String(nonObject)); + testPermutations(set, [nonObject, 'a.b', 'foo'], (result) => { + if (Number.isNaN(nonObject)) { + t.ok(result instanceof Number); + t.strictEqual(result.toString(), 'NaN'); + t.deepEqual(result, Object.assign(NaN, { a: { b: 'foo' } })); // will produce new object due to cloning + } else if (nonObject === 42) { + t.ok(result instanceof Number); + t.strictEqual(result.toString(), '42'); + t.deepEqual(result, Object.assign(42, { a: { b: 'foo' } })); // will produce new object due to cloning + } else { + t.ok(result instanceof Object); + t.strictEqual(result.toString(), '[object Object]'); + t.deepEqual(result, { a: { b: 'foo' } }); // will produce new object due to cloning + } + }); + }); + }); + + test(`${testName}: Overwrites existing object properties`, (t) => { + t.plan(testPermutations.assertionCalls); + testPermutations(set, [{ a: { b: { c: 3 } } }, 'a.b', 'foo'], (result) => { + t.deepEqual(result, { a: { b: 'foo' } }); + }); + }); + + test(`${testName}: Adds missing properties without touching other areas`, (t) => { + t.plan(testPermutations.assertionCalls); + testPermutations( + set, + [{ a: [{ aa: { aaa: 3, aab: 4 } }, { ab: 2 }], b: 1 }, 'a[0].aa.aaa.aaaa', 'foo'], + (result) => { + t.deepEqual(result, { + a: [{ aa: { aaa: Object.assign(3, { aaaa: 'foo' }), aab: 4 } }, { ab: 2 }], + b: 1, + }); + } + ); + }); + + test(`${testName}: Overwrites existing elements in array`, (t) => { + t.plan(testPermutations.assertionCalls); + testPermutations(set, [{ a: [1, 2, 3] }, 'a[1]', 'foo'], (result) => { + t.deepEqual(result, { a: [1, 'foo', 3] }); + }); + }); + + test(`${testName}: Create new array`, (t) => { + t.plan(testPermutations.assertionCalls); + testPermutations(set, [{}, ['x', '0', 'y', 'z'], 'foo'], (result) => { + t.deepEqual(result, { x: [{ y: { z: 'foo' } }] }); + }); + }); + + /** + * PROTOTYPE POLLUTION PROTECTION TESTS + */ + + const testCases = [ + ['__proto__', { ['__proto__']: 'foo' }], + ['.__proto__', { '': { ['__proto__']: 'foo' } }], + ['o.__proto__', { o: { ['__proto__']: 'foo' } }], + ['a[0].__proto__', { a: [{ ['__proto__']: 'foo' }] }], + + ['constructor', { constructor: 'foo' }], + ['.constructor', { '': { constructor: 'foo' } }], + ['o.constructor', { o: { constructor: 'foo' } }], + ['a[0].constructor', { a: [{ constructor: 'foo' }] }], + + ['constructor.something', { constructor: { something: 'foo' } }], + ['.constructor.something', { '': { constructor: { something: 'foo' } } }], + ['o.constructor.something', { o: { constructor: { something: 'foo' } } }], + ['a[0].constructor.something', { a: [{ constructor: { something: 'foo' } }] }], + + ['prototype', { prototype: 'foo' }], + ['.prototype', { '': { prototype: 'foo' } }], + ['o.prototype', { o: { prototype: 'foo' } }], + ['a[0].prototype', { a: [{ prototype: 'foo' }] }], + + ['constructor.prototype', { constructor: { prototype: 'foo' } }], + ['.constructor.prototype', { '': { constructor: { prototype: 'foo' } } }], + ['o.constructor.prototype', { o: { constructor: { prototype: 'foo' } } }], + ['a[0].constructor.prototype', { a: [{ constructor: { prototype: 'foo' } }] }], + + ['constructor.something.prototype', { constructor: { something: { prototype: 'foo' } } }], + [ + '.constructor.something.prototype', + { '': { constructor: { something: { prototype: 'foo' } } } }, + ], + [ + 'o.constructor.something.prototype', + { o: { constructor: { something: { prototype: 'foo' } } } }, + ], + [ + 'a[0].constructor.something.prototype', + { a: [{ constructor: { something: { prototype: 'foo' } } }] }, + ], + ]; + + testCases.forEach(([path, expected]) => { + test(`${testName}: Object manipulation, ${path}`, (t) => { + t.plan(testPermutations.assertionCalls); + testPermutations(set, [{}, path, 'foo'], (result) => { + t.deepLooseEqual(result, expected); // Use loose check because the prototype of result isn't Object.prototype + }); + }); + }); + + testCases.forEach(([path, expected]) => { + test(`${testName}: Array manipulation, ${path}`, (t) => { + t.plan(testPermutations.assertionCalls * 4); + const arr = []; + testPermutations(set, [arr, path, 'foo'], (result) => { + t.notStrictEqual(arr, result); + t.ok(Array.isArray(result)); + Object.keys(expected).forEach((key) => { + t.ok(Object.prototype.hasOwnProperty.call(result, key)); + t.deepEqual(result[key], expected[key]); + }); + }); + }); + }); + + test(`${testName}: Function manipulation, object containing function`, (t) => { + const funcTestCases = [ + [{ fn: function () {} }, 'fn.prototype'], + [{ fn: () => {} }, 'fn.prototype'], + ]; + const expected = /Illegal access of function prototype/; + t.plan((isSetWith ? 7 : 4) * funcTestCases.length); + funcTestCases.forEach(([obj, path]) => { + if (isSetWith) { + t.throws(() => set(() => {}, path, 'foo', obj), expected); + t.throws(() => set(() => {})(path, 'foo', obj), expected); + t.throws(() => set(() => {})(path)('foo', obj), expected); + t.throws(() => set(() => {})(path)('foo')(obj), expected); + t.throws(() => set(() => {}, path)('foo')(obj), expected); + t.throws(() => set(() => {}, path, 'foo')(obj), expected); + t.throws(() => set(() => {})(path, 'foo')(obj), expected); + } else { + t.throws(() => set(path, 'foo', obj), expected); + t.throws(() => set(path, 'foo')(obj), expected); + t.throws(() => set(path)('foo', obj), expected); + t.throws(() => set(path)('foo')(obj), expected); + } + }); + }); + test(`${testName}: Function manipulation, arrow function`, (t) => { + // This doesn't really make sense to do with the `fp` variant of lodash, as it will return a regular non-function object + t.plan(testPermutations.assertionCalls * 2); + const obj = () => {}; + testPermutations(set, [obj, 'prototype', 'foo'], (result) => { + t.notStrictEqual(result, obj); + t.strictEqual(result.prototype, 'foo'); + }); + }); + test(`${testName}: Function manipulation, regular function`, (t) => { + // This doesn't really make sense to do with the `fp` variant of lodash, as it will return a regular non-function object + t.plan(testPermutations.assertionCalls * 2); + const obj = function () {}; + testPermutations(set, [obj, 'prototype', 'foo'], (result) => { + t.notStrictEqual(result, obj); + t.strictEqual(result.prototype, 'foo'); + }); + }); +}); + +/** + * setWith specific tests + */ +setWithFunctions.forEach(([testPermutations, setWith, testName]) => { + test(`${testName}: Return undefined`, (t) => { + t.plan(testPermutations.assertionCalls); + testPermutations(setWith, [{}, 'a.b', 'foo', () => {}], (result) => { + t.deepEqual(result, { a: { b: 'foo' } }); + }); + }); + + test(`${testName}: Customizer arguments`, (t) => { + let i = 0; + const expectedCustomizerArgs = [ + [{ b: Object(42) }, 'a', { a: { b: Object(42) } }], + [Object(42), 'b', { b: Object(42) }], + ]; + + t.plan(testPermutations.assertionCalls * (expectedCustomizerArgs.length + 1)); + + testPermutations( + setWith, + [ + { a: { b: 42 } }, + 'a.b.c', + 'foo', + (...args) => { + t.deepEqual( + args, + expectedCustomizerArgs[i++ % 2], + 'customizer args should be as expected' + ); + }, + ], + (result) => { + t.deepEqual(result, { a: { b: Object.assign(42, { c: 'foo' }) } }); + } + ); + }); + + test(`${testName}: Return value`, (t) => { + t.plan(testPermutations.assertionCalls); + testSetWith(setWith, [{}, '[0][1]', 'a', Object], (result) => { + t.deepEqual(result, { 0: { 1: 'a' } }); + }); + }); +}); diff --git a/packages/elastic-safer-lodash-set/test/fp_set.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_set.test-d.ts new file mode 100644 index 0000000000000..a5dbb24d33a05 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/fp_set.test-d.ts @@ -0,0 +1,25 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import set from '../fp/set'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +expectType(set('a.b.c', anyValue, someObj)); +expectType(set('a.b.c')(anyValue, someObj)); +expectType(set('a.b.c')(anyValue)(someObj)); +expectType(set('a.b.c', anyValue)(someObj)); + +expectType(set(['a.b.c'], anyValue, someObj)); +expectType(set(['a.b.c'])(anyValue, someObj)); +expectType(set(['a.b.c'])(anyValue)(someObj)); +expectType(set(['a.b.c'], anyValue)(someObj)); + +expectType(set(['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(set(['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); +expectType(set(['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(set(['a.b.c', 2, Symbol('hep')], anyValue)(someObj)); diff --git a/packages/elastic-safer-lodash-set/test/fp_setWith.test-d.ts b/packages/elastic-safer-lodash-set/test/fp_setWith.test-d.ts new file mode 100644 index 0000000000000..70a5197f72176 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/fp_setWith.test-d.ts @@ -0,0 +1,40 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import setWith from '../fp/setWith'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +function customizer(value: any, key: string, obj: object) { + expectType(value); + expectType(key); + expectType(obj); +} + +expectType(setWith(customizer, 'a.b.c', anyValue, someObj)); +expectType(setWith(customizer)('a.b.c', anyValue, someObj)); +expectType(setWith(customizer)('a.b.c')(anyValue, someObj)); +expectType(setWith(customizer)('a.b.c')(anyValue)(someObj)); +expectType(setWith(customizer, 'a.b.c')(anyValue)(someObj)); +expectType(setWith(customizer, 'a.b.c', anyValue)(someObj)); +expectType(setWith(customizer, 'a.b.c')(anyValue, someObj)); + +expectType(setWith(customizer, ['a.b.c'], anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c'])(anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c'], anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c'])(anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c'])(anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c'], anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c'])(anyValue, someObj)); + +expectType(setWith(customizer, ['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c', 2, Symbol('hep')], anyValue, someObj)); +expectType(setWith(customizer)(['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c', 2, Symbol('hep')])(anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c', 2, Symbol('hep')], anyValue)(someObj)); +expectType(setWith(customizer, ['a.b.c', 2, Symbol('hep')])(anyValue, someObj)); diff --git a/packages/elastic-safer-lodash-set/test/index.test-d.ts b/packages/elastic-safer-lodash-set/test/index.test-d.ts new file mode 100644 index 0000000000000..ab29d7de5a03f --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/index.test-d.ts @@ -0,0 +1,37 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import { set, setWith } from '../'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +expectType(set(someObj, 'a.b.c', anyValue)); +expectType( + setWith(someObj, 'a.b.c', anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); + +expectType(set(someObj, ['a.b.c'], anyValue)); +expectType( + setWith(someObj, ['a.b.c'], anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); + +expectType(set(someObj, ['a.b.c', 2, Symbol('hep')], anyValue)); +expectType( + setWith(someObj, ['a.b.c', 2, Symbol('hep')], anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); diff --git a/packages/elastic-safer-lodash-set/test/patch_test.js b/packages/elastic-safer-lodash-set/test/patch_test.js new file mode 100644 index 0000000000000..03dfe260009e9 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/patch_test.js @@ -0,0 +1,174 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +const test = require('tape'); + +const setFunctions = [ + [require('../').set, 'module.set'], + [require('../set'), 'module/set'], +]; +const setWithFunctions = [ + [require('../').setWith, 'module.setWith'], + [require('../setWith'), 'module/setWith'], +]; +const setAndSetWithFunctions = [].concat(setFunctions, setWithFunctions); + +setAndSetWithFunctions.forEach(([set, testName]) => { + /** + * GENERAL USAGE TESTS + */ + + test(`${testName}: Returns same object`, (t) => { + const o1 = {}; + const o2 = set(o1, 'foo', 'bar'); + t.strictEqual(o1, o2); + t.end(); + }); + + test(`${testName}: Non-objects`, (t) => { + t.strictEqual(set(null, 'a.b', 'foo'), null); + t.strictEqual(set(undefined, 'a.b', 'foo'), undefined); + t.strictEqual(set(NaN, 'a.b', 'foo'), NaN); + t.strictEqual(set(42, 'a.b', 'foo'), 42); + t.end(); + }); + + test(`${testName}: Overwrites existing object properties`, (t) => { + t.deepEqual(set({ a: { b: { c: 3 } } }, 'a.b', 'foo'), { a: { b: 'foo' } }); + t.end(); + }); + + test(`${testName}: Adds missing properties without touching other areas`, (t) => { + t.deepEqual( + set({ a: [{ aa: { aaa: 3, aab: 4 } }, { ab: 2 }], b: 1 }, 'a[0].aa.aaa.aaaa', 'foo'), + { a: [{ aa: { aaa: { aaaa: 'foo' }, aab: 4 } }, { ab: 2 }], b: 1 } + ); + t.end(); + }); + + test(`${testName}: Overwrites existing elements in array`, (t) => { + t.deepEqual(set({ a: [1, 2, 3] }, 'a[1]', 'foo'), { a: [1, 'foo', 3] }); + t.end(); + }); + + test(`${testName}: Create new array`, (t) => { + t.deepEqual(set({}, ['x', '0', 'y', 'z'], 'foo'), { x: [{ y: { z: 'foo' } }] }); + t.end(); + }); + + /** + * PROTOTYPE POLLUTION PROTECTION TESTS + */ + + const testCases = [ + ['__proto__', { ['__proto__']: 'foo' }], + ['.__proto__', { '': { ['__proto__']: 'foo' } }], + ['o.__proto__', { o: { ['__proto__']: 'foo' } }], + ['a[0].__proto__', { a: [{ ['__proto__']: 'foo' }] }], + + ['constructor', { constructor: 'foo' }], + ['.constructor', { '': { constructor: 'foo' } }], + ['o.constructor', { o: { constructor: 'foo' } }], + ['a[0].constructor', { a: [{ constructor: 'foo' }] }], + + ['constructor.something', { constructor: { something: 'foo' } }], + ['.constructor.something', { '': { constructor: { something: 'foo' } } }], + ['o.constructor.something', { o: { constructor: { something: 'foo' } } }], + ['a[0].constructor.something', { a: [{ constructor: { something: 'foo' } }] }], + + ['prototype', { prototype: 'foo' }], + ['.prototype', { '': { prototype: 'foo' } }], + ['o.prototype', { o: { prototype: 'foo' } }], + ['a[0].prototype', { a: [{ prototype: 'foo' }] }], + + ['constructor.prototype', { constructor: { prototype: 'foo' } }], + ['.constructor.prototype', { '': { constructor: { prototype: 'foo' } } }], + ['o.constructor.prototype', { o: { constructor: { prototype: 'foo' } } }], + ['a[0].constructor.prototype', { a: [{ constructor: { prototype: 'foo' } }] }], + + ['constructor.something.prototype', { constructor: { something: { prototype: 'foo' } } }], + [ + '.constructor.something.prototype', + { '': { constructor: { something: { prototype: 'foo' } } } }, + ], + [ + 'o.constructor.something.prototype', + { o: { constructor: { something: { prototype: 'foo' } } } }, + ], + [ + 'a[0].constructor.something.prototype', + { a: [{ constructor: { something: { prototype: 'foo' } } }] }, + ], + ]; + + testCases.forEach(([path, expected]) => { + test(`${testName}: Object manipulation, ${path}`, (t) => { + t.deepEqual(set({}, path, 'foo'), expected); + t.end(); + }); + }); + + testCases.forEach(([path, expected]) => { + test(`${testName}: Array manipulation, ${path}`, (t) => { + const arr = []; + set(arr, path, 'foo'); + Object.keys(expected).forEach((key) => { + t.ok(Object.prototype.hasOwnProperty.call(arr, key)); + t.deepEqual(arr[key], expected[key]); + }); + t.end(); + }); + }); + + test(`${testName}: Function manipulation`, (t) => { + const funcTestCases = [ + [function () {}, 'prototype'], + [() => {}, 'prototype'], + [{ fn: function () {} }, 'fn.prototype'], + [{ fn: () => {} }, 'fn.prototype'], + ]; + funcTestCases.forEach(([obj, path]) => { + t.throws(() => set(obj, path, 'foo'), /Illegal access of function prototype/); + }); + t.end(); + }); +}); + +/** + * setWith specific tests + */ + +setWithFunctions.forEach(([setWith, testName]) => { + test(`${testName}: Return undefined`, (t) => { + t.deepEqual( + setWith({}, 'a.b', 'foo', () => {}), + { a: { b: 'foo' } } + ); + t.end(); + }); + + test(`${testName}: Customizer arguments`, (t) => { + t.plan(3); + + const expectedCustomizerArgs = [ + [{ b: 42 }, 'a', { a: { b: 42 } }], + [42, 'b', { b: 42 }], + ]; + + t.deepEqual( + setWith({ a: { b: 42 } }, 'a.b.c', 'foo', (...args) => { + t.deepEqual(args, expectedCustomizerArgs.shift()); + }), + { a: { b: { c: 'foo' } } } + ); + + t.end(); + }); + + test(`${testName}: Return value`, (t) => { + t.deepEqual(setWith({}, '[0][1]', 'a', Object), { 0: { 1: 'a' } }); + t.end(); + }); +}); diff --git a/packages/elastic-safer-lodash-set/test/set.test-d.ts b/packages/elastic-safer-lodash-set/test/set.test-d.ts new file mode 100644 index 0000000000000..9829ac3f04ce5 --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/set.test-d.ts @@ -0,0 +1,14 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import set from '../set'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +expectType(set(someObj, 'a.b.c', anyValue)); +expectType(set(someObj, ['a.b.c'], anyValue)); +expectType(set(someObj, ['a.b.c', 2, Symbol('hep')], anyValue)); diff --git a/packages/elastic-safer-lodash-set/test/setWith.test-d.ts b/packages/elastic-safer-lodash-set/test/setWith.test-d.ts new file mode 100644 index 0000000000000..b3ed93443c4fb --- /dev/null +++ b/packages/elastic-safer-lodash-set/test/setWith.test-d.ts @@ -0,0 +1,32 @@ +/* + * Elasticsearch B.V licenses this file to you under the MIT License. + * See `packages/elastic-safer-lodash-set/LICENSE` for more information. + */ + +import { expectType } from 'tsd'; +import setWith from '../setWith'; + +const someObj: object = {}; +const anyValue: any = 'any value'; + +expectType( + setWith(someObj, 'a.b.c', anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); +expectType( + setWith(someObj, ['a.b.c'], anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); +expectType( + setWith(someObj, ['a.b.c', 2, Symbol('hep')], anyValue, (value, key, obj) => { + expectType(value); + expectType(key); + expectType(obj); + }) +); diff --git a/packages/elastic-safer-lodash-set/tsconfig.json b/packages/elastic-safer-lodash-set/tsconfig.json new file mode 100644 index 0000000000000..bc1d1a3a7e413 --- /dev/null +++ b/packages/elastic-safer-lodash-set/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "**/*" + ], + "exclude": [ + "**/*.test-d.ts" + ] +} diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts index 6083593431d9b..dbdda3f38afd5 100644 --- a/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts +++ b/packages/kbn-telemetry-tools/src/tools/check_collector__integrity.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as _ from 'lodash'; +import { cloneDeep } from 'lodash'; import * as ts from 'typescript'; import { parsedWorkingCollector } from './__fixture__/parsed_working_collector'; import { checkCompatibleTypeDescriptor, checkMatchingMapping } from './check_collector_integrity'; @@ -42,7 +42,7 @@ describe('checkMatchingMapping', () => { describe('Collector change', () => { it('returns diff on mismatching parsedCollections and stored mapping', async () => { const mockSchema = await parseJsonFile('mock_schema.json'); - const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector); + const malformedParsedCollector = cloneDeep(parsedWorkingCollector); const fieldMapping = { type: 'number' }; malformedParsedCollector[1].schema.value.flat = fieldMapping; @@ -58,7 +58,7 @@ describe('checkMatchingMapping', () => { it('returns diff on unknown parsedCollections', async () => { const mockSchema = await parseJsonFile('mock_schema.json'); - const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector); + const malformedParsedCollector = cloneDeep(parsedWorkingCollector); const collectorName = 'New Collector in town!'; const collectorMapping = { some_usage: { type: 'number' } }; malformedParsedCollector[1].collectorName = collectorName; @@ -84,7 +84,7 @@ describe('checkCompatibleTypeDescriptor', () => { describe('Interface Change', () => { it('returns diff on incompatible type descriptor with mapping', () => { - const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector); + const malformedParsedCollector = cloneDeep(parsedWorkingCollector); malformedParsedCollector[1].fetch.typeDescriptor.flat.kind = ts.SyntaxKind.BooleanKeyword; const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]); expect(incompatibles).toHaveLength(1); @@ -101,14 +101,14 @@ describe('checkCompatibleTypeDescriptor', () => { describe('Mapping change', () => { it('returns no diff when mapping change between text and keyword', () => { - const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector); + const malformedParsedCollector = cloneDeep(parsedWorkingCollector); malformedParsedCollector[1].schema.value.flat.type = 'text'; const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]); expect(incompatibles).toHaveLength(0); }); it('returns diff on incompatible type descriptor with mapping', () => { - const malformedParsedCollector = _.cloneDeep(parsedWorkingCollector); + const malformedParsedCollector = cloneDeep(parsedWorkingCollector); malformedParsedCollector[1].schema.value.flat.type = 'boolean'; const incompatibles = checkCompatibleTypeDescriptor([malformedParsedCollector]); expect(incompatibles).toHaveLength(1); diff --git a/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts b/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts index 824132b05732c..3205edb87aa29 100644 --- a/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts +++ b/packages/kbn-telemetry-tools/src/tools/check_collector_integrity.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as _ from 'lodash'; +import { reduce } from 'lodash'; import { difference, flattenKeys, pickDeep } from './utils'; import { ParsedUsageCollection } from './ts_parser'; import { generateMapping, compatibleSchemaTypes } from './manage_schema'; @@ -44,7 +44,7 @@ export function checkCompatibleTypeDescriptor( const typeDescriptorTypes = flattenKeys( pickDeep(collectorDetails.fetch.typeDescriptor, 'kind') ); - const typeDescriptorKinds = _.reduce( + const typeDescriptorKinds = reduce( typeDescriptorTypes, (acc: any, type: number, key: string) => { try { @@ -58,7 +58,7 @@ export function checkCompatibleTypeDescriptor( ); const schemaTypes = flattenKeys(pickDeep(collectorDetails.schema.value, 'type')); - const transformedMappingKinds = _.reduce( + const transformedMappingKinds = reduce( schemaTypes, (acc: any, type: string, key: string) => { try { diff --git a/packages/kbn-telemetry-tools/src/tools/tasks/generate_schemas_task.ts b/packages/kbn-telemetry-tools/src/tools/tasks/generate_schemas_task.ts index f6d15c7127d4e..5ff7d2dd8ef6e 100644 --- a/packages/kbn-telemetry-tools/src/tools/tasks/generate_schemas_task.ts +++ b/packages/kbn-telemetry-tools/src/tools/tasks/generate_schemas_task.ts @@ -17,7 +17,6 @@ * under the License. */ -import * as _ from 'lodash'; import { TaskContext } from './task_context'; import { generateMapping } from '../manage_schema'; diff --git a/packages/kbn-telemetry-tools/src/tools/utils.ts b/packages/kbn-telemetry-tools/src/tools/utils.ts index f5cf74ae35e45..212b06a4c9895 100644 --- a/packages/kbn-telemetry-tools/src/tools/utils.ts +++ b/packages/kbn-telemetry-tools/src/tools/utils.ts @@ -18,7 +18,7 @@ */ import * as ts from 'typescript'; -import * as _ from 'lodash'; +import { pick, isObject, each, isArray, reduce, isEmpty, merge, transform, isEqual } from 'lodash'; import * as path from 'path'; import glob from 'glob'; import { readFile, writeFile } from 'fs'; @@ -178,17 +178,17 @@ export function getPropertyValue( } export function pickDeep(collection: any, identity: any, thisArg?: any) { - const picked: any = _.pick(collection, identity, thisArg); - const collections = _.pick(collection, _.isObject, thisArg); + const picked: any = pick(collection, identity, thisArg); + const collections = pick(collection, isObject, thisArg); - _.each(collections, function (item, key) { + each(collections, function (item, key) { let object; - if (_.isArray(item)) { - object = _.reduce( + if (isArray(item)) { + object = reduce( item, function (result, value) { const pickedDeep = pickDeep(value, identity, thisArg); - if (!_.isEmpty(pickedDeep)) { + if (!isEmpty(pickedDeep)) { result.push(pickedDeep); } return result; @@ -199,7 +199,7 @@ export function pickDeep(collection: any, identity: any, thisArg?: any) { object = pickDeep(item, identity, thisArg); } - if (!_.isEmpty(object)) { + if (!isEmpty(object)) { picked[key || ''] = object; } }); @@ -208,12 +208,12 @@ export function pickDeep(collection: any, identity: any, thisArg?: any) { } export const flattenKeys = (obj: any, keyPath: any[] = []): any => { - if (_.isObject(obj)) { - return _.reduce( + if (isObject(obj)) { + return reduce( obj, (cum, next, key) => { const keys = [...keyPath, key]; - return _.merge(cum, flattenKeys(next, keys)); + return merge(cum, flattenKeys(next, keys)); }, {} ); @@ -223,10 +223,9 @@ export const flattenKeys = (obj: any, keyPath: any[] = []): any => { export function difference(actual: any, expected: any) { function changes(obj: any, base: any) { - return _.transform(obj, function (result, value, key) { - if (key && !_.isEqual(value, base[key])) { - result[key] = - _.isObject(value) && _.isObject(base[key]) ? changes(value, base[key]) : value; + return transform(obj, function (result, value, key) { + if (key && !isEqual(value, base[key])) { + result[key] = isObject(value) && isObject(base[key]) ? changes(value, base[key]) : value; } }); } diff --git a/src/cli/command.js b/src/cli/command.js index f4781fcab1e20..671e053b9550e 100644 --- a/src/cli/command.js +++ b/src/cli/command.js @@ -17,6 +17,7 @@ * under the License. */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import Chalk from 'chalk'; @@ -86,7 +87,7 @@ Command.prototype.collectUnknownOptions = function () { val = opt[1]; } - _.set(opts, opt[0].slice(2), val); + set(opts, opt[0].slice(2), val); } return opts; diff --git a/src/cli/serve/read_keystore.js b/src/cli/serve/read_keystore.js index cfe02735630f2..962c708c0d8df 100644 --- a/src/cli/serve/read_keystore.js +++ b/src/cli/serve/read_keystore.js @@ -18,7 +18,7 @@ */ import path from 'path'; -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { Keystore } from '../../legacy/server/keystore'; import { getDataPath } from '../../core/server/path'; diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index 8bc65f3da7111..972bcdba6b403 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -17,6 +17,7 @@ * under the License. */ +import { set as lodashSet } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { statSync } from 'fs'; import { resolve } from 'path'; @@ -65,7 +66,7 @@ const pluginDirCollector = pathCollector(); const pluginPathCollector = pathCollector(); function applyConfigOverrides(rawConfig, opts, extraCliOptions) { - const set = _.partial(_.set, rawConfig); + const set = _.partial(lodashSet, rawConfig); const get = _.partial(_.get, rawConfig); const has = _.partial(_.has, rawConfig); const merge = _.partial(_.merge, rawConfig); diff --git a/src/core/public/saved_objects/simple_saved_object.ts b/src/core/public/saved_objects/simple_saved_object.ts index 165ef98be91d4..5bd339fbd7c96 100644 --- a/src/core/public/saved_objects/simple_saved_object.ts +++ b/src/core/public/saved_objects/simple_saved_object.ts @@ -17,7 +17,8 @@ * under the License. */ -import { get, has, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get, has } from 'lodash'; import { SavedObject as SavedObjectType } from '../../server'; import { SavedObjectsClientContract } from './saved_objects_client'; diff --git a/src/core/server/config/deprecation/deprecation_factory.ts b/src/core/server/config/deprecation/deprecation_factory.ts index 0b19a99624311..cbc9984924c5d 100644 --- a/src/core/server/config/deprecation/deprecation_factory.ts +++ b/src/core/server/config/deprecation/deprecation_factory.ts @@ -17,7 +17,8 @@ * under the License. */ -import { get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get } from 'lodash'; import { ConfigDeprecation, ConfigDeprecationLogger, ConfigDeprecationFactory } from './types'; import { unset } from '../../../utils'; diff --git a/src/core/server/config/object_to_config_adapter.ts b/src/core/server/config/object_to_config_adapter.ts index d4c2f73364060..50b31722dceeb 100644 --- a/src/core/server/config/object_to_config_adapter.ts +++ b/src/core/server/config/object_to_config_adapter.ts @@ -17,7 +17,8 @@ * under the License. */ -import { cloneDeep, get, has, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { cloneDeep, get, has } from 'lodash'; import { getFlattenedObject } from '../../utils'; import { Config, ConfigPath } from './'; diff --git a/src/core/server/config/read_config.ts b/src/core/server/config/read_config.ts index eac3535c9d4ed..806366dc3e062 100644 --- a/src/core/server/config/read_config.ts +++ b/src/core/server/config/read_config.ts @@ -20,7 +20,8 @@ import { readFileSync } from 'fs'; import { safeLoad } from 'js-yaml'; -import { isPlainObject, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { isPlainObject } from 'lodash'; import { ensureDeepObject } from './ensure_deep_object'; const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8')); diff --git a/src/core/server/legacy/config/get_unused_config_keys.ts b/src/core/server/legacy/config/get_unused_config_keys.ts index 8e53178142180..354bf9af042cf 100644 --- a/src/core/server/legacy/config/get_unused_config_keys.ts +++ b/src/core/server/legacy/config/get_unused_config_keys.ts @@ -17,7 +17,8 @@ * under the License. */ -import { difference, get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { difference, get } from 'lodash'; // @ts-expect-error import { getTransform } from '../../../../legacy/deprecation/index'; import { unset } from '../../../../legacy/utils'; diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 6287d47f99f62..4fc94d1992869 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { SavedObjectUnsanitizedDoc } from '../../serialization'; import { DocumentMigrator } from './document_migrator'; @@ -132,7 +133,7 @@ describe('DocumentMigrator', () => { name: 'user', migrations: { '1.2.3': (doc) => { - _.set(doc, 'attributes.name', 'Mike'); + set(doc, 'attributes.name', 'Mike'); return doc; }, }, @@ -639,7 +640,7 @@ describe('DocumentMigrator', () => { typeRegistry: createRegistry({ name: 'aaa', migrations: { - '2.3.4': (d) => _.set(d, 'attributes.counter', 42), + '2.3.4': (d) => set(d, 'attributes.counter', 42), }, }), validateDoc: (d) => { @@ -657,12 +658,12 @@ describe('DocumentMigrator', () => { function renameAttr(path: string, newPath: string) { return (doc: SavedObjectUnsanitizedDoc) => - _.omit(_.set(doc, newPath, _.get(doc, path)) as {}, path) as SavedObjectUnsanitizedDoc; + _.omit(set(doc, newPath, _.get(doc, path)) as {}, path) as SavedObjectUnsanitizedDoc; } function setAttr(path: string, value: any) { return (doc: SavedObjectUnsanitizedDoc) => - _.set( + set( doc, path, _.isFunction(value) ? value(_.get(doc, path)) : value diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index 07675bb0a6819..c50f755fda994 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -61,6 +61,7 @@ */ import Boom from 'boom'; +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import Semver from 'semver'; import { Logger } from '../../../logging'; @@ -291,7 +292,7 @@ function markAsUpToDate(doc: SavedObjectUnsanitizedDoc, migrations: ActiveMigrat ...doc, migrationVersion: props(doc).reduce((acc, prop) => { const version = propVersion(migrations, prop); - return version ? _.set(acc, prop, version) : acc; + return version ? set(acc, prop, version) : acc; }, {}), }; } diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 6e4dd9615d423..4c9d2e870a7bb 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -17,6 +17,7 @@ * under the License. */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsSerializer } from '../../serialization'; @@ -25,7 +26,7 @@ import { createSavedObjectsMigrationLoggerMock } from '../../migrations/mocks'; describe('migrateRawDocs', () => { test('converts raw docs to saved objects', async () => { - const transform = jest.fn((doc: any) => _.set(doc, 'attributes.name', 'HOI!')); + const transform = jest.fn((doc: any) => set(doc, 'attributes.name', 'HOI!')); const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), transform, @@ -53,7 +54,7 @@ describe('migrateRawDocs', () => { test('passes invalid docs through untouched and logs error', async () => { const logger = createSavedObjectsMigrationLoggerMock(); const transform = jest.fn((doc: any) => - _.set(_.cloneDeep(doc), 'attributes.name', 'TADA') + set(_.cloneDeep(doc), 'attributes.name', 'TADA') ); const result = await migrateRawDocs( new SavedObjectsSerializer(new SavedObjectTypeRegistry()), diff --git a/src/core/server/saved_objects/service/lib/filter_utils.ts b/src/core/server/saved_objects/service/lib/filter_utils.ts index 4c31f37f63dad..5fbe62a074b29 100644 --- a/src/core/server/saved_objects/service/lib/filter_utils.ts +++ b/src/core/server/saved_objects/service/lib/filter_utils.ts @@ -17,7 +17,8 @@ * under the License. */ -import { get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get } from 'lodash'; import { SavedObjectsErrorHelpers } from './errors'; import { IndexMapping } from '../../mappings'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths diff --git a/src/dev/file.ts b/src/dev/file.ts index 29e7cdc966909..32998d3e776ef 100644 --- a/src/dev/file.ts +++ b/src/dev/file.ts @@ -55,7 +55,9 @@ export class File { } public isFixture() { - return this.relativePath.split(sep).includes('__fixtures__'); + return ( + this.relativePath.split(sep).includes('__fixtures__') || this.path.endsWith('.test-d.ts') + ); } public getRelativeParentDirs() { diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index b8eacdd6a3897..6b1f1dfaeabb4 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -61,6 +61,9 @@ export const IGNORE_FILE_GLOBS = [ // filename required by api-extractor 'api-documenter.json', + // filename must match upstream filenames from lodash + 'packages/elastic-safer-lodash-set/**/*', + // TODO fix file names in APM to remove these 'x-pack/plugins/apm/public/**/*', 'x-pack/plugins/apm/scripts/**/*', diff --git a/src/fixtures/mock_ui_state.js b/src/fixtures/mock_ui_state.js index 919274390d4d0..9252fcf2a7dd8 100644 --- a/src/fixtures/mock_ui_state.js +++ b/src/fixtures/mock_ui_state.js @@ -17,6 +17,7 @@ * under the License. */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; let values = {}; export default { @@ -24,11 +25,11 @@ export default { return _.get(values, path, def); }, set: function (path, val) { - _.set(values, path, val); + set(values, path, val); return val; }, setSilent: function (path, val) { - _.set(values, path, val); + set(values, path, val); return val; }, emit: _.noop, diff --git a/src/legacy/deprecation/deprecations/rename.js b/src/legacy/deprecation/deprecations/rename.js index b47a745519b1e..c96b9146b4e2c 100644 --- a/src/legacy/deprecation/deprecations/rename.js +++ b/src/legacy/deprecation/deprecations/rename.js @@ -17,7 +17,8 @@ * under the License. */ -import { get, isUndefined, noop, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get, isUndefined, noop } from 'lodash'; import { unset } from '../../utils'; export function rename(oldKey, newKey) { diff --git a/src/legacy/server/config/config.js b/src/legacy/server/config/config.js index d32ec29e6d701..7805296258d9f 100644 --- a/src/legacy/server/config/config.js +++ b/src/legacy/server/config/config.js @@ -18,6 +18,7 @@ */ import Joi from 'joi'; +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { override } from './override'; import createDefaultSchema from './schema'; @@ -56,7 +57,7 @@ export class Config { throw new Error(`Config schema already has key: ${key}`); } - _.set(this[schemaExts], key, extension); + set(this[schemaExts], key, extension); this[schema] = null; this.set(key, settings); @@ -82,7 +83,7 @@ export class Config { if (_.isPlainObject(key)) { config = override(config, key); } else { - _.set(config, key, value); + set(config, key, value); } // attempt to validate the config value diff --git a/src/legacy/ui/public/state_management/state_monitor_factory.ts b/src/legacy/ui/public/state_management/state_monitor_factory.ts index 454fefd4f8253..968ececfe3be5 100644 --- a/src/legacy/ui/public/state_management/state_monitor_factory.ts +++ b/src/legacy/ui/public/state_management/state_monitor_factory.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { cloneDeep, isEqual, isPlainObject, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { cloneDeep, isEqual, isPlainObject } from 'lodash'; import { State } from './state'; export const stateMonitorFactory = { diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 2ffd0688b134e..b4f20ec6225e2 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -10,6 +10,7 @@ "optionalPlugins": ["usageCollection"], "extraPublicDirs": ["common", "common/utils/abort_utils"], "requiredBundles": [ + "usageCollection", "kibanaUtils", "kibanaReact", "kibanaLegacy", diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 4040781bb2f01..323a32ea362ac 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -111,7 +111,7 @@ export class DataPublicPlugin implements Plugin { + let mockCoreSetup: MockedKeys; + let mockUsageCollectionSetup: Setup; + let usageCollector: SearchUsageCollector; + + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + (mockCoreSetup as any).getStartServices.mockResolvedValue([ + { + application: { + currentAppId$: from(['foo/bar']), + }, + } as jest.Mocked, + {} as any, + {} as any, + ]); + mockUsageCollectionSetup = usageCollectionPluginMock.createSetupContract(); + usageCollector = createUsageCollector(mockCoreSetup, mockUsageCollectionSetup); + }); + + test('tracks query timeouts', async () => { + await usageCollector.trackQueryTimedOut(); + expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled(); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][0]).toBe('foo/bar'); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe( + SEARCH_EVENT_TYPE.QUERY_TIMED_OUT + ); + }); + + test('tracks query cancellation', async () => { + await usageCollector.trackQueriesCancelled(); + expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled(); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe( + SEARCH_EVENT_TYPE.QUERIES_CANCELLED + ); + }); + + test('tracks long popups', async () => { + await usageCollector.trackLongQueryPopupShown(); + expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled(); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe( + SEARCH_EVENT_TYPE.LONG_QUERY_POPUP_SHOWN + ); + }); + + test('tracks long popups dismissed', async () => { + await usageCollector.trackLongQueryDialogDismissed(); + expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled(); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.CLICK); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe( + SEARCH_EVENT_TYPE.LONG_QUERY_DIALOG_DISMISSED + ); + }); + + test('tracks run query beyond timeout', async () => { + await usageCollector.trackLongQueryRunBeyondTimeout(); + expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled(); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.CLICK); + expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe( + SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT + ); + }); + + test('tracks response errors', async () => { + const duration = 10; + await usageCollector.trackError(duration); + expect(mockCoreSetup.http.post).toBeCalled(); + expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage'); + }); + + test('tracks response duration', async () => { + const duration = 5; + await usageCollector.trackSuccess(duration); + expect(mockCoreSetup.http.post).toBeCalled(); + expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage'); + }); +}); diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.ts b/src/plugins/data/public/search/collectors/create_usage_collector.ts new file mode 100644 index 0000000000000..cb1b2b65c17c8 --- /dev/null +++ b/src/plugins/data/public/search/collectors/create_usage_collector.ts @@ -0,0 +1,92 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { first } from 'rxjs/operators'; +import { CoreSetup } from '../../../../../core/public'; +import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public'; +import { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types'; + +export const createUsageCollector = ( + core: CoreSetup, + usageCollection?: UsageCollectionSetup +): SearchUsageCollector => { + const getCurrentApp = async () => { + const [{ application }] = await core.getStartServices(); + return application.currentAppId$.pipe(first()).toPromise(); + }; + + return { + trackQueryTimedOut: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiStats( + currentApp!, + METRIC_TYPE.LOADED, + SEARCH_EVENT_TYPE.QUERY_TIMED_OUT + ); + }, + trackQueriesCancelled: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiStats( + currentApp!, + METRIC_TYPE.LOADED, + SEARCH_EVENT_TYPE.QUERIES_CANCELLED + ); + }, + trackLongQueryPopupShown: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiStats( + currentApp!, + METRIC_TYPE.LOADED, + SEARCH_EVENT_TYPE.LONG_QUERY_POPUP_SHOWN + ); + }, + trackLongQueryDialogDismissed: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiStats( + currentApp!, + METRIC_TYPE.CLICK, + SEARCH_EVENT_TYPE.LONG_QUERY_DIALOG_DISMISSED + ); + }, + trackLongQueryRunBeyondTimeout: async () => { + const currentApp = await getCurrentApp(); + return usageCollection?.reportUiStats( + currentApp!, + METRIC_TYPE.CLICK, + SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT + ); + }, + trackError: async (duration: number) => { + return core.http.post('/api/search/usage', { + body: JSON.stringify({ + eventType: 'error', + duration, + }), + }); + }, + trackSuccess: async (duration: number) => { + return core.http.post('/api/search/usage', { + body: JSON.stringify({ + eventType: 'success', + duration, + }), + }); + }, + }; +}; diff --git a/src/plugins/data/public/search/collectors/index.ts b/src/plugins/data/public/search/collectors/index.ts new file mode 100644 index 0000000000000..afe127c00b5dd --- /dev/null +++ b/src/plugins/data/public/search/collectors/index.ts @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { createUsageCollector } from './create_usage_collector'; +export { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types'; diff --git a/src/plugins/data/public/search/collectors/types.ts b/src/plugins/data/public/search/collectors/types.ts new file mode 100644 index 0000000000000..bb85532fd3ab5 --- /dev/null +++ b/src/plugins/data/public/search/collectors/types.ts @@ -0,0 +1,36 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export enum SEARCH_EVENT_TYPE { + QUERY_TIMED_OUT = 'queryTimedOut', + QUERIES_CANCELLED = 'queriesCancelled', + LONG_QUERY_POPUP_SHOWN = 'longQueryPopupShown', + LONG_QUERY_DIALOG_DISMISSED = 'longQueryDialogDismissed', + LONG_QUERY_RUN_BEYOND_TIMEOUT = 'longQueryRunBeyondTimeout', +} + +export interface SearchUsageCollector { + trackQueryTimedOut: () => Promise; + trackQueriesCancelled: () => Promise; + trackLongQueryPopupShown: () => Promise; + trackLongQueryDialogDismissed: () => Promise; + trackLongQueryRunBeyondTimeout: () => Promise; + trackError: (duration: number) => Promise; + trackSuccess: (duration: number) => Promise; +} diff --git a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts b/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts index c4846a98f124f..75a4464a8e61e 100644 --- a/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts +++ b/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { FormattedData } from '../../../../../plugins/inspector/public'; import { FormatFactory } from '../../../common/field_formats/utils'; import { TabbedTable } from '../tabify'; diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 8edbfd94deb38..84e24114a9e6c 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -18,12 +18,13 @@ */ import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs'; -import { finalize, filter } from 'rxjs/operators'; +import { finalize, filter, tap } from 'rxjs/operators'; import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public'; import { getCombinedSignal, AbortError } from '../../common/utils'; import { IEsSearchRequest, IEsSearchResponse } from '../../common/search'; import { ISearchOptions } from './types'; import { getLongQueryNotification } from './long_query_notification'; +import { SearchUsageCollector } from './collectors'; const LONG_QUERY_NOTIFICATION_DELAY = 10000; @@ -32,6 +33,7 @@ export interface SearchInterceptorDeps { application: ApplicationStart; http: CoreStart['http']; uiSettings: CoreStart['uiSettings']; + usageCollector?: SearchUsageCollector; } export class SearchInterceptor { @@ -121,6 +123,13 @@ export class SearchInterceptor { this.pendingCount$.next(++this.pendingCount); return this.runSearch(request, combinedSignal).pipe( + tap({ + next: (e) => { + if (this.deps.usageCollector) { + this.deps.usageCollector.trackSuccess(e.rawResponse.took); + } + }, + }), finalize(() => { this.pendingCount$.next(--this.pendingCount); cleanup(); @@ -185,6 +194,9 @@ export class SearchInterceptor { if (this.longRunningToast) { this.deps.toasts.remove(this.longRunningToast); delete this.longRunningToast; + if (this.deps.usageCollector) { + this.deps.usageCollector.trackLongQueryDialogDismissed(); + } } }; } diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index a27eba21714bb..064e16014cb70 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -37,9 +37,12 @@ import { getCalculateAutoTimeExpression, } from './aggs'; import { ISearchGeneric } from './types'; +import { SearchUsageCollector, createUsageCollector } from './collectors'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; interface SearchServiceSetupDependencies { expressions: ExpressionsSetup; + usageCollection?: UsageCollectionSetup; getInternalStartServices: GetInternalStartServicesFn; packageInfo: PackageInfo; } @@ -52,6 +55,7 @@ export class SearchService implements Plugin { private esClient?: LegacyApiCaller; private readonly aggTypesRegistry = new AggTypesRegistry(); private searchInterceptor!: SearchInterceptor; + private usageCollector?: SearchUsageCollector; /** * getForceNow uses window.location, so we must have a separate implementation @@ -62,8 +66,14 @@ export class SearchService implements Plugin { public setup( core: CoreSetup, - { expressions, packageInfo, getInternalStartServices }: SearchServiceSetupDependencies + { + expressions, + usageCollection, + packageInfo, + getInternalStartServices, + }: SearchServiceSetupDependencies ): ISearchSetup { + this.usageCollector = createUsageCollector(core, usageCollection); this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo); const aggTypesSetup = this.aggTypesRegistry.setup(); @@ -102,6 +112,7 @@ export class SearchService implements Plugin { application: core.application, http: core.http, uiSettings: core.uiSettings, + usageCollector: this.usageCollector!, }, core.injectedMetadata.getInjectedVar('esRequestTimeout') as number ); @@ -134,6 +145,7 @@ export class SearchService implements Plugin { types: aggTypesStart, }, search, + usageCollector: this.usageCollector!, searchSource: { create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies), createEmpty: () => { diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 6260b92e1c11a..c97a5d0638a6a 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -69,18 +69,8 @@ * `appSearchSource`. */ -import { - uniqueId, - uniq, - extend, - pick, - difference, - omit, - setWith, - isObject, - keys, - isFunction, -} from 'lodash'; +import { setWith } from '@elastic/safer-lodash-set'; +import { uniqueId, uniq, extend, pick, difference, omit, isObject, keys, isFunction } from 'lodash'; import { map } from 'rxjs/operators'; import { CoreStart } from 'kibana/public'; import { normalizeSortRequest } from './normalize_sort_request'; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index 5c4bb42a5948d..ec74275f35c04 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -18,17 +18,22 @@ */ import { Observable } from 'rxjs'; +import { PackageInfo } from 'kibana/server'; import { SearchAggsSetup, SearchAggsStart } from './aggs'; import { LegacyApiCaller } from './legacy/es_client'; import { SearchInterceptor } from './search_interceptor'; import { ISearchSource, SearchSourceFields } from './search_source'; - +import { SearchUsageCollector } from './collectors'; import { IKibanaSearchRequest, IKibanaSearchResponse, IEsSearchRequest, IEsSearchResponse, } from '../../common/search'; +import { IndexPatternsContract } from '../../common/index_patterns/index_patterns'; +import { ExpressionsSetup } from '../../../expressions/public'; +import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { GetInternalStartServicesFn } from '../types'; export interface ISearchOptions { signal?: AbortSignal; @@ -69,5 +74,19 @@ export interface ISearchStart { create: (fields?: SearchSourceFields) => Promise; createEmpty: () => ISearchSource; }; + usageCollector?: SearchUsageCollector; __LEGACY: ISearchStartLegacy; } + +export { SEARCH_EVENT_TYPE } from './collectors'; + +export interface SearchServiceSetupDependencies { + expressions: ExpressionsSetup; + usageCollection?: UsageCollectionSetup; + getInternalStartServices: GetInternalStartServicesFn; + packageInfo: PackageInfo; +} + +export interface SearchServiceStartDependencies { + indexPatterns: IndexPatternsContract; +} diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index aaef403979de6..6d67127251424 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -30,10 +30,12 @@ import { QuerySetup, QueryStart } from './query'; import { IndexPatternSelectProps } from './ui/index_pattern_select'; import { IndexPatternsContract } from './index_patterns'; import { StatefulSearchBarProps } from './ui/search_bar/create_search_bar'; +import { UsageCollectionSetup } from '../../usage_collection/public'; export interface DataSetupDependencies { expressions: ExpressionsSetup; uiActions: UiActionsSetup; + usageCollection?: UsageCollectionSetup; } export interface DataStartDependencies { diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index bcf1f4f8ab60b..8fa32f9bd564f 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -82,7 +82,7 @@ export class DataServerPlugin implements Plugin) { + return async (callCluster: LegacyAPICaller): Promise => { + const config = await config$.pipe(first()).toPromise(); + + const response = await callCluster('search', { + index: config.kibana.index, + body: { + query: { term: { type: { value: 'search-telemetry' } } }, + }, + ignore: [404], + }); + + return response.hits.hits.length + ? (response.hits.hits[0]._source as Usage) + : { + successCount: 0, + errorCount: 0, + averageDuration: null, + }; + }; +} diff --git a/src/plugins/data/server/search/collectors/register.ts b/src/plugins/data/server/search/collectors/register.ts new file mode 100644 index 0000000000000..ab0ea93edd49e --- /dev/null +++ b/src/plugins/data/server/search/collectors/register.ts @@ -0,0 +1,49 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'kibana/server'; +import { UsageCollectionSetup } from '../../../../usage_collection/server'; +import { fetchProvider } from './fetch'; + +export interface Usage { + successCount: number; + errorCount: number; + averageDuration: number | null; +} + +export async function registerUsageCollector( + usageCollection: UsageCollectionSetup, + context: PluginInitializerContext +) { + try { + const collector = usageCollection.makeUsageCollector({ + type: 'search', + isReady: () => true, + fetch: fetchProvider(context.config.legacy.globalConfig$), + schema: { + successCount: { type: 'number' }, + errorCount: { type: 'number' }, + averageDuration: { type: 'long' }, + }, + }); + usageCollection.registerCollector(collector); + } catch (err) { + return; // kibana plugin is not enabled (test environment) + } +} diff --git a/src/plugins/data/server/search/collectors/routes.ts b/src/plugins/data/server/search/collectors/routes.ts new file mode 100644 index 0000000000000..38fb517e3c3f6 --- /dev/null +++ b/src/plugins/data/server/search/collectors/routes.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { schema } from '@kbn/config-schema'; +import { CoreSetup } from '../../../../../core/server'; +import { DataPluginStart } from '../../plugin'; +import { SearchUsage } from './usage'; + +export function registerSearchUsageRoute( + core: CoreSetup, + usage: SearchUsage +): void { + const router = core.http.createRouter(); + + router.post( + { + path: '/api/search/usage', + validate: { + body: schema.object({ + eventType: schema.string(), + duration: schema.number(), + }), + }, + }, + async (context, request, res) => { + const { eventType, duration } = request.body; + + if (eventType === 'success') usage.trackSuccess(duration); + if (eventType === 'error') usage.trackError(duration); + + return res.ok(); + } + ); +} diff --git a/src/plugins/data/server/search/collectors/usage.ts b/src/plugins/data/server/search/collectors/usage.ts new file mode 100644 index 0000000000000..c43c572c2edbb --- /dev/null +++ b/src/plugins/data/server/search/collectors/usage.ts @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreSetup } from 'kibana/server'; +import { DataPluginStart } from '../../plugin'; +import { Usage } from './register'; + +const SAVED_OBJECT_ID = 'search-telemetry'; + +export interface SearchUsage { + trackError(duration: number): Promise; + trackSuccess(duration: number): Promise; +} + +export function usageProvider(core: CoreSetup): SearchUsage { + const getTracker = (eventType: keyof Usage) => { + return async (duration: number) => { + const repository = await core + .getStartServices() + .then(([coreStart]) => coreStart.savedObjects.createInternalRepository()); + + let attributes: Usage; + let doesSavedObjectExist: boolean = true; + + try { + const response = await repository.get(SAVED_OBJECT_ID, SAVED_OBJECT_ID); + attributes = response.attributes; + } catch (e) { + doesSavedObjectExist = false; + attributes = { + successCount: 0, + errorCount: 0, + averageDuration: 0, + }; + } + + attributes[eventType]++; + + const averageDuration = + (duration + (attributes.averageDuration ?? 0)) / + ((attributes.errorCount ?? 0) + (attributes.successCount ?? 0)); + + const newAttributes = { ...attributes, averageDuration }; + + try { + if (doesSavedObjectExist) { + await repository.update(SAVED_OBJECT_ID, SAVED_OBJECT_ID, newAttributes); + } else { + await repository.create(SAVED_OBJECT_ID, newAttributes, { id: SAVED_OBJECT_ID }); + } + } catch (e) { + // Version conflict error, swallow + } + }; + }; + + return { + trackError: getTracker('errorCount'), + trackSuccess: getTracker('successCount'), + }; +} diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index 25143fa09e6bf..8c2ed96503003 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -34,7 +34,7 @@ describe('Search service', () => { describe('setup()', () => { it('exposes proper contract', async () => { - const setup = plugin.setup(mockCoreSetup); + const setup = plugin.setup(mockCoreSetup, {}); expect(setup).toHaveProperty('registerSearchStrategy'); }); }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 20f9a7488893f..5686023e9a667 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -27,6 +27,11 @@ import { ISearchSetup, ISearchStart, ISearchStrategy } from './types'; import { registerSearchRoute } from './routes'; import { ES_SEARCH_STRATEGY, esSearchStrategyProvider } from './es_search'; import { DataPluginStart } from '../plugin'; +import { UsageCollectionSetup } from '../../../usage_collection/server'; +import { registerUsageCollector } from './collectors/register'; +import { usageProvider } from './collectors/usage'; +import { searchTelemetry } from '../saved_objects'; +import { registerSearchUsageRoute } from './collectors/routes'; import { IEsSearchRequest } from '../../common'; interface StrategyMap { @@ -38,15 +43,26 @@ export class SearchService implements Plugin { constructor(private initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup): ISearchSetup { + public setup( + core: CoreSetup, + { usageCollection }: { usageCollection?: UsageCollectionSetup } + ): ISearchSetup { this.registerSearchStrategy( ES_SEARCH_STRATEGY, esSearchStrategyProvider(this.initializerContext.config.legacy.globalConfig$) ); + core.savedObjects.registerType(searchTelemetry); + if (usageCollection) { + registerUsageCollector(usageCollection, this.initializerContext); + } + + const usage = usageProvider(core); + registerSearchRoute(core); + registerSearchUsageRoute(core, usage); - return { registerSearchStrategy: this.registerSearchStrategy }; + return { registerSearchStrategy: this.registerSearchStrategy, usage }; } private search(context: RequestHandlerContext, searchRequest: IEsSearchRequest, options: any) { diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 12f1a1a508bd2..25dc890e0257d 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -19,6 +19,7 @@ import { RequestHandlerContext } from '../../../../core/server'; import { IKibanaSearchResponse, IKibanaSearchRequest } from '../../common/search'; +import { SearchUsage } from './collectors/usage'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; export interface ISearchOptions { @@ -35,6 +36,11 @@ export interface ISearchSetup { * strategies. */ registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; + + /** + * Used internally for telemetry + */ + usage: SearchUsage; } export interface ISearchStart { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 4dc60056ed918..c5d19fef9531e 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -532,6 +532,8 @@ export interface ISearchOptions { // @public (undocumented) export interface ISearchSetup { registerSearchStrategy: (name: string, strategy: ISearchStrategy) => void; + // Warning: (ae-forgotten-export) The symbol "SearchUsage" needs to be exported by the entry point index.d.ts + usage: SearchUsage; } // Warning: (ae-missing-release-tag) "ISearchStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js index fcde2ade0b2c6..4987c77f4bf25 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js @@ -18,7 +18,7 @@ */ import moment from 'moment'; -import * as _ from 'lodash'; +import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { fetchContextProvider } from './context'; import { setServices } from '../../../../kibana_services'; @@ -124,9 +124,7 @@ describe('context app', function () { ).then((hits) => { const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') - .map(([, { query }]) => - _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) - ); + .map(([, { query }]) => get(query, ['constant_score', 'filter', 'range', '@timestamp'])); expect( intervals.every(({ gte, lte }) => (gte && lte ? moment(gte).isBefore(lte) : true)) @@ -134,7 +132,7 @@ describe('context app', function () { // should have started at the given time expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have ended with a half-open interval - expect(Object.keys(_.last(intervals))).toEqual(['format', 'gte']); + expect(Object.keys(last(intervals))).toEqual(['format', 'gte']); expect(intervals.length).toBeGreaterThan(1); expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); @@ -162,14 +160,12 @@ describe('context app', function () { ).then((hits) => { const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') - .map(([, { query }]) => - _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) - ); + .map(([, { query }]) => get(query, ['constant_score', 'filter', 'range', '@timestamp'])); // should have started at the given time expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 1000).toISOString()); // should have stopped before reaching MS_PER_DAY * 1700 - expect(moment(_.last(intervals).lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); + expect(moment(last(intervals).lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); expect(intervals.length).toBeGreaterThan(1); expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); }); diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.js b/src/plugins/discover/public/application/angular/context/api/context.successors.test.js index 0f84aa82a989a..ebf6e78585962 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.js +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.js @@ -18,7 +18,7 @@ */ import moment from 'moment'; -import * as _ from 'lodash'; +import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { setServices } from '../../../../kibana_services'; @@ -125,9 +125,7 @@ describe('context app', function () { ).then((hits) => { const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') - .map(([, { query }]) => - _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) - ); + .map(([, { query }]) => get(query, ['constant_score', 'filter', 'range', '@timestamp'])); expect( intervals.every(({ gte, lte }) => (gte && lte ? moment(gte).isBefore(lte) : true)) @@ -135,7 +133,7 @@ describe('context app', function () { // should have started at the given time expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have ended with a half-open interval - expect(Object.keys(_.last(intervals))).toEqual(['format', 'lte']); + expect(Object.keys(last(intervals))).toEqual(['format', 'lte']); expect(intervals.length).toBeGreaterThan(1); expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); @@ -165,14 +163,12 @@ describe('context app', function () { ).then((hits) => { const intervals = mockSearchSource.setField.args .filter(([property]) => property === 'query') - .map(([, { query }]) => - _.get(query, ['constant_score', 'filter', 'range', '@timestamp']) - ); + .map(([, { query }]) => get(query, ['constant_score', 'filter', 'range', '@timestamp'])); // should have started at the given time expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have stopped before reaching MS_PER_DAY * 2200 - expect(moment(_.last(intervals).gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); + expect(moment(last(intervals).gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); expect(intervals.length).toBeGreaterThan(1); expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 4)); diff --git a/src/plugins/es_ui_shared/public/console_lang/ace/modes/lexer_rules/x_json_highlight_rules.ts b/src/plugins/es_ui_shared/public/console_lang/ace/modes/lexer_rules/x_json_highlight_rules.ts index 951cf5fa279b5..138284b5fece0 100644 --- a/src/plugins/es_ui_shared/public/console_lang/ace/modes/lexer_rules/x_json_highlight_rules.ts +++ b/src/plugins/es_ui_shared/public/console_lang/ace/modes/lexer_rules/x_json_highlight_rules.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as _ from 'lodash'; +import { defaultsDeep } from 'lodash'; import ace from 'brace'; import 'brace/mode/json'; @@ -176,7 +176,7 @@ export function XJsonHighlightRules(this: any) { oop.inherits(XJsonHighlightRules, JsonHighlightRules); export function addToRules(otherRules: any, embedUnder: any) { - otherRules.$rules = _.defaultsDeep(otherRules.$rules, jsonRules(embedUnder)); + otherRules.$rules = defaultsDeep(otherRules.$rules, jsonRules(embedUnder)); otherRules.embedRules(ScriptHighlightRules, 'script-', [ { token: 'punctuation.end_triple_quote', diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts index 65cd7792a0189..7d506e28794fd 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/lib/utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { FieldHook } from '../types'; export const unflattenObject = (object: any) => diff --git a/src/plugins/kibana_legacy/public/angular/angular_config.tsx b/src/plugins/kibana_legacy/public/angular/angular_config.tsx index 25cbb0631a652..eafcbfda3db00 100644 --- a/src/plugins/kibana_legacy/public/angular/angular_config.tsx +++ b/src/plugins/kibana_legacy/public/angular/angular_config.tsx @@ -26,7 +26,8 @@ import { IRootScopeService, } from 'angular'; import $ from 'jquery'; -import { cloneDeep, forOwn, get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { cloneDeep, forOwn, get } from 'lodash'; import * as Rx from 'rxjs'; import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; import { History } from 'history'; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx index d273ffb4c1052..adf54297c3133 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/form.tsx @@ -26,7 +26,8 @@ import { EuiButtonEmpty, EuiSpacer, } from '@elastic/eui'; -import { cloneDeep, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { SimpleSavedObject, SavedObjectsClientContract } from '../../../../../../core/public'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js index 4d48095898b80..f969778bbc615 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/convert_series_to_vars.js @@ -17,6 +17,7 @@ * under the License. */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { getLastValue } from '../../../../../../plugins/vis_type_timeseries/common/get_last_value'; import { createTickFormatter } from './tick_formatter'; @@ -51,8 +52,8 @@ export const convertSeriesToVars = (series, model, dateFormat = 'lll', getConfig }), }, }; - _.set(variables, varName, data); - _.set(variables, `${_.snakeCase(row.label)}.label`, row.label); + set(variables, varName, data); + set(variables, `${_.snakeCase(row.label)}.label`, row.label); }); }); return variables; diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js index 0e4d2ce2a926c..f033a43806312 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/bucket_transform.js @@ -19,7 +19,8 @@ import { getBucketsPath } from './get_buckets_path'; import { parseInterval } from './parse_interval'; -import { set, isEmpty } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { MODEL_SCRIPTS } from './moving_fn_scripts'; diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js index faf270877217b..1861fa621ecd1 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/axis/axis_config.js @@ -17,6 +17,7 @@ * under the License. */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import d3 from 'd3'; import { SCALE_MODES } from './scale_modes'; @@ -220,7 +221,7 @@ export class AxisConfig { } set(property, value) { - return _.set(this._values, property, value); + return set(this._values, property, value); } isHorizontal() { diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/chart_grid.js b/src/plugins/vis_type_vislib/public/vislib/lib/chart_grid.js index aac019a98e790..0cd0c8391995b 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/chart_grid.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/chart_grid.js @@ -18,6 +18,7 @@ */ import d3 from 'd3'; +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; const defaults = { @@ -102,6 +103,6 @@ export class ChartGrid { } set(property, value) { - return _.set(this._values, property, value); + return set(this._values, property, value); } } diff --git a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js index 0354724703208..6490dfe252b29 100644 --- a/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js +++ b/src/plugins/vis_type_vislib/public/vislib/lib/vis_config.js @@ -20,6 +20,7 @@ /** * Provides vislib configuration, throws error if invalid property is accessed without providing defaults */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { vislibTypesConfig as visTypes } from './types'; import { Data } from './data'; @@ -54,6 +55,6 @@ export class VisConfig { } set(property, value) { - return _.set(this._values, property, value); + return set(this._values, property, value); } } diff --git a/src/plugins/visualizations/public/legacy/vis_update_state.js b/src/plugins/visualizations/public/legacy/vis_update_state.js index edaf388e21060..8d80db4e4be1d 100644 --- a/src/plugins/visualizations/public/legacy/vis_update_state.js +++ b/src/plugins/visualizations/public/legacy/vis_update_state.js @@ -17,6 +17,7 @@ * under the License. */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; /** @@ -31,7 +32,7 @@ function convertHeatmapLabelColor(visState) { if (visState.type === 'heatmap' && visState.params && !hasOverwriteColorParam) { const showLabels = _.get(visState, 'params.valueAxes[0].labels.show', false); const color = _.get(visState, 'params.valueAxes[0].labels.color', '#555'); - _.set(visState, 'params.valueAxes[0].labels.overwriteColor', showLabels && color !== '#555'); + set(visState, 'params.valueAxes[0].labels.overwriteColor', showLabels && color !== '#555'); } } @@ -167,7 +168,7 @@ export const updateOldState = (visState) => { if (visState.type === 'gauge' && visState.fontSize) { delete newState.fontSize; - _.set(newState, 'gauge.style.fontSize', visState.fontSize); + set(newState, 'gauge.style.fontSize', visState.fontSize); } // update old metric to the new one diff --git a/src/plugins/visualizations/public/persisted_state/persisted_state.ts b/src/plugins/visualizations/public/persisted_state/persisted_state.ts index c926c456da219..3799a5b03ce46 100644 --- a/src/plugins/visualizations/public/persisted_state/persisted_state.ts +++ b/src/plugins/visualizations/public/persisted_state/persisted_state.ts @@ -19,17 +19,8 @@ import { EventEmitter } from 'events'; -import { - isPlainObject, - cloneDeep, - get, - set, - isEqual, - isString, - merge, - mergeWith, - toPath, -} from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { isPlainObject, cloneDeep, get, isEqual, isString, merge, mergeWith, toPath } from 'lodash'; function prepSetParams(key: PersistedStateKey, value: any, path: PersistedStatePath) { // key must be the value, set the entire state using it diff --git a/tasks/config/run.js b/tasks/config/run.js index 32adf4f1f87c2..98a1226834bc6 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -223,6 +223,12 @@ module.exports = function (grunt) { args: ['scripts/test_hardening.js'], }), + test_package_safer_lodash_set: scriptWithGithubChecks({ + title: '@elastic/safer-lodash-set tests', + cmd: YARN, + args: ['--cwd', 'packages/elastic-safer-lodash-set', 'test'], + }), + apiIntegrationTests: scriptWithGithubChecks({ title: 'API integration tests', cmd: NODE, diff --git a/tasks/jenkins.js b/tasks/jenkins.js index b40bb8156098d..eece5df61a7d1 100644 --- a/tasks/jenkins.js +++ b/tasks/jenkins.js @@ -39,6 +39,7 @@ module.exports = function (grunt) { 'run:test_projects', 'run:test_karma_ci', 'run:test_hardening', + 'run:test_package_safer_lodash_set', 'run:apiIntegrationTests', ]); }; diff --git a/test/api_integration/apis/saved_objects/migrations.js b/test/api_integration/apis/saved_objects/migrations.js index 9ea3cf087be90..ed259ccec0114 100644 --- a/test/api_integration/apis/saved_objects/migrations.js +++ b/test/api_integration/apis/saved_objects/migrations.js @@ -21,6 +21,7 @@ * Smokescreen tests for core migration logic */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; import { assert } from 'chai'; import { @@ -56,12 +57,12 @@ export default ({ getService }) => { const migrations = { foo: { - '1.0.0': (doc) => _.set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), + '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), }, bar: { - '1.0.0': (doc) => _.set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), - '1.3.0': (doc) => _.set(doc, 'attributes', { mynum: doc.attributes.nomnom }), - '1.9.0': (doc) => _.set(doc, 'attributes.mynum', doc.attributes.mynum * 2), + '1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), + '1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }), + '1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2), }, }; @@ -172,12 +173,12 @@ export default ({ getService }) => { const migrations = { foo: { - '1.0.0': (doc) => _.set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), + '1.0.0': (doc) => set(doc, 'attributes.name', doc.attributes.name.toUpperCase()), }, bar: { - '1.0.0': (doc) => _.set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), - '1.3.0': (doc) => _.set(doc, 'attributes', { mynum: doc.attributes.nomnom }), - '1.9.0': (doc) => _.set(doc, 'attributes.mynum', doc.attributes.mynum * 2), + '1.0.0': (doc) => set(doc, 'attributes.nomnom', doc.attributes.nomnom + 1), + '1.3.0': (doc) => set(doc, 'attributes', { mynum: doc.attributes.nomnom }), + '1.9.0': (doc) => set(doc, 'attributes.mynum', doc.attributes.mynum * 2), }, }; @@ -187,8 +188,8 @@ export default ({ getService }) => { await migrateIndex({ callCluster, index, migrations, mappingProperties }); mappingProperties.bar.properties.name = { type: 'keyword' }; - migrations.foo['2.0.1'] = (doc) => _.set(doc, 'attributes.name', `${doc.attributes.name}v2`); - migrations.bar['2.3.4'] = (doc) => _.set(doc, 'attributes.name', `NAME ${doc.id}`); + migrations.foo['2.0.1'] = (doc) => set(doc, 'attributes.name', `${doc.attributes.name}v2`); + migrations.bar['2.3.4'] = (doc) => set(doc, 'attributes.name', `NAME ${doc.id}`); await migrateIndex({ callCluster, index, migrations, mappingProperties }); @@ -267,7 +268,7 @@ export default ({ getService }) => { const migrations = { foo: { - '1.0.0': (doc) => _.set(doc, 'attributes.name', 'LOTR'), + '1.0.0': (doc) => set(doc, 'attributes.name', 'LOTR'), }, }; diff --git a/x-pack/legacy/server/lib/check_license/check_license.test.js b/x-pack/legacy/server/lib/check_license/check_license.test.js index 0545e1a2d16f4..65b599ed4a5f6 100644 --- a/x-pack/legacy/server/lib/check_license/check_license.test.js +++ b/x-pack/legacy/server/lib/check_license/check_license.test.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { checkLicense } from './check_license'; import { LICENSE_STATUS_UNAVAILABLE, diff --git a/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js index 5f2141cce9395..ef6fbaf9c53d0 100644 --- a/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js +++ b/x-pack/legacy/server/lib/create_router/is_es_error_factory/__tests__/is_es_error_factory.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; class MockAbstractEsError {} diff --git a/x-pack/legacy/server/lib/parse_kibana_state.js b/x-pack/legacy/server/lib/parse_kibana_state.js index 7e81cb2736fc3..a6c9bfbb511c1 100644 --- a/x-pack/legacy/server/lib/parse_kibana_state.js +++ b/x-pack/legacy/server/lib/parse_kibana_state.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isPlainObject, omit, get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { isPlainObject, omit, get } from 'lodash'; import rison from 'rison-node'; const stateTypeKeys = { diff --git a/x-pack/package.json b/x-pack/package.json index 29264f8920e5d..6715fa132c1b5 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -201,6 +201,7 @@ "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", "@elastic/numeral": "^2.5.0", + "@elastic/safer-lodash-set": "0.0.0", "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/i18n": "1.0.0", diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index b50db270ef544..7f46fc685d9ca 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -36,14 +36,14 @@ export interface Connection { destination: ConnectionNode; } -export interface ServiceNodeMetrics { +export interface ServiceNodeStats { avgMemoryUsage: number | null; avgCpuUsage: number | null; transactionStats: { avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }; - avgErrorsPerMinute: number | null; + avgErrorRate: number | null; } export function isValidPlatinumLicense(license: ILicense) { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx index c696a93773ceb..78466b2659bb7 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx @@ -14,7 +14,7 @@ import cytoscape from 'cytoscape'; import React, { MouseEvent } from 'react'; import { Buttons } from './Buttons'; import { Info } from './Info'; -import { ServiceMetricFetcher } from './ServiceMetricFetcher'; +import { ServiceStatsFetcher } from './ServiceStatsFetcher'; import { popoverWidth } from '../cytoscapeOptions'; interface ContentsProps { @@ -70,7 +70,7 @@ export function Contents({ {isService ? ( - diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx index 223d342e6799f..094cf032c4c9d 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Info.tsx @@ -38,13 +38,13 @@ export function Info(data: InfoProps) { const listItems = [ { - title: i18n.translate('xpack.apm.serviceMap.typePopoverMetric', { + title: i18n.translate('xpack.apm.serviceMap.typePopoverStat', { defaultMessage: 'Type', }), description: type, }, { - title: i18n.translate('xpack.apm.serviceMap.subtypePopoverMetric', { + title: i18n.translate('xpack.apm.serviceMap.subtypePopoverStat', { defaultMessage: 'Subtype', }), description: subtype, diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx index ccf147ed1d90d..20f6f92f9995f 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx @@ -5,40 +5,128 @@ */ import { storiesOf } from '@storybook/react'; +import cytoscape from 'cytoscape'; +import { HttpSetup } from 'kibana/public'; import React from 'react'; -import { ServiceMetricList } from './ServiceMetricList'; +import { EuiThemeProvider } from '../../../../../../observability/public'; +import { MockApmPluginContextWrapper } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import { MockUrlParamsContextProvider } from '../../../../context/UrlParamsContext/MockUrlParamsContextProvider'; +import { createCallApmApi } from '../../../../services/rest/createCallApmApi'; +import { CytoscapeContext } from '../Cytoscape'; +import { Popover } from './'; +import { ServiceStatsList } from './ServiceStatsList'; -storiesOf('app/ServiceMap/Popover/ServiceMetricList', module) - .add('example', () => ( - { + const node = { + data: { id: 'example service', 'service.name': 'example service' }, + }; + const cy = cytoscape({ elements: [node] }); + const httpMock = ({ + get: async () => ({ + avgCpuUsage: 0.32809666568309237, + avgErrorRate: 0.556068173242986, + avgMemoryUsage: 0.5504868173242986, avgRequestsPerMinute: 164.47222031860858, - }} - avgCpuUsage={0.32809666568309237} - avgMemoryUsage={0.5504868173242986} - /> - )) - .add('some null values', () => ( - - )) - .add('all null values', () => ( - - )); + avgTransactionDuration: 61634.38905590272, + }), + } as unknown) as HttpSetup; + + createCallApmApi(httpMock); + + setImmediate(() => { + cy.$('example service').select(); + }); + + return ( + + + + +
{storyFn()}
+
+
+
+
+ ); + }) + .add( + 'example', + () => { + return ; + }, + { + info: { + propTablesExclude: [ + CytoscapeContext.Provider, + MockApmPluginContextWrapper, + MockUrlParamsContextProvider, + EuiThemeProvider, + ], + source: false, + }, + } + ); + +storiesOf('app/ServiceMap/Popover/ServiceStatsList', module) + .addDecorator((storyFn) => {storyFn()}) + .add( + 'example', + () => ( + + ), + { info: { propTablesExclude: [EuiThemeProvider] } } + ) + .add( + 'loading', + () => ( + + ), + { info: { propTablesExclude: [EuiThemeProvider] } } + ) + .add( + 'some null values', + () => ( + + ), + { info: { propTablesExclude: [EuiThemeProvider] } } + ) + .add( + 'all null values', + () => ( + + ), + { info: { propTablesExclude: [EuiThemeProvider] } } + ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx similarity index 78% rename from x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx index 957678877a134..9e8f1f7a0171e 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsFetcher.tsx @@ -13,39 +13,44 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; -import { ServiceNodeMetrics } from '../../../../../common/service_map'; +import { ServiceNodeStats } from '../../../../../common/service_map'; +import { ServiceStatsList } from './ServiceStatsList'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; -import { ServiceMetricList } from './ServiceMetricList'; import { AnomalyDetection } from './AnomalyDetection'; import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; -interface ServiceMetricFetcherProps { +interface ServiceStatsFetcherProps { + environment?: string; serviceName: string; serviceAnomalyStats: ServiceAnomalyStats | undefined; } -export function ServiceMetricFetcher({ +export function ServiceStatsFetcher({ serviceName, serviceAnomalyStats, -}: ServiceMetricFetcherProps) { +}: ServiceStatsFetcherProps) { const { - urlParams: { start, end, environment }, + urlParams: { start, end }, + uiFilters, } = useUrlParams(); const { - data = { transactionStats: {} } as ServiceNodeMetrics, + data = { transactionStats: {} } as ServiceNodeStats, status, } = useFetcher( (callApmApi) => { if (serviceName && start && end) { return callApmApi({ pathname: '/api/apm/service-map/service/{serviceName}', - params: { path: { serviceName }, query: { start, end, environment } }, + params: { + path: { serviceName }, + query: { start, end, uiFilters: JSON.stringify(uiFilters) }, + }, }); } }, - [serviceName, start, end, environment], + [serviceName, start, end, uiFilters], { preservePreviousData: false, } @@ -60,20 +65,20 @@ export function ServiceMetricFetcher({ const { avgCpuUsage, - avgErrorsPerMinute, + avgErrorRate, avgMemoryUsage, transactionStats: { avgRequestsPerMinute, avgTransactionDuration }, } = data; const hasServiceData = [ avgCpuUsage, - avgErrorsPerMinute, + avgErrorRate, avgMemoryUsage, avgRequestsPerMinute, avgTransactionDuration, ].some((stat) => isNumber(stat)); - if (environment && !hasServiceData) { + if (!hasServiceData) { return ( {i18n.translate('xpack.apm.serviceMap.popoverMetrics.noDataText', { @@ -93,7 +98,7 @@ export function ServiceMetricFetcher({ )} - + ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx index f82f434e7ded1..4a1a291249f50 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceStatsList.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import { isNumber } from 'lodash'; import React from 'react'; import styled from 'styled-components'; -import { ServiceNodeMetrics } from '../../../../../common/service_map'; +import { ServiceNodeStats } from '../../../../../common/service_map'; import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters'; export const ItemRow = styled('tr')` @@ -24,18 +24,18 @@ export const ItemDescription = styled('td')` text-align: right; `; -type ServiceMetricListProps = ServiceNodeMetrics; +type ServiceStatsListProps = ServiceNodeStats; -export function ServiceMetricList({ - avgErrorsPerMinute, +export function ServiceStatsList({ + transactionStats, + avgErrorRate, avgCpuUsage, avgMemoryUsage, - transactionStats, -}: ServiceMetricListProps) { +}: ServiceStatsListProps) { const listItems = [ { title: i18n.translate( - 'xpack.apm.serviceMap.avgTransDurationPopoverMetric', + 'xpack.apm.serviceMap.avgTransDurationPopoverStat', { defaultMessage: 'Trans. duration (avg.)', } @@ -58,27 +58,21 @@ export function ServiceMetricList({ : null, }, { - title: i18n.translate( - 'xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric', - { - defaultMessage: 'Errors per minute (avg.)', - } - ), - description: avgErrorsPerMinute?.toFixed(2), + title: i18n.translate('xpack.apm.serviceMap.errorRatePopoverStat', { + defaultMessage: 'Error rate (avg.)', + }), + description: isNumber(avgErrorRate) ? asPercent(avgErrorRate, 1) : null, }, { - title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverMetric', { + title: i18n.translate('xpack.apm.serviceMap.avgCpuUsagePopoverStat', { defaultMessage: 'CPU usage (avg.)', }), description: isNumber(avgCpuUsage) ? asPercent(avgCpuUsage, 1) : null, }, { - title: i18n.translate( - 'xpack.apm.serviceMap.avgMemoryUsagePopoverMetric', - { - defaultMessage: 'Memory usage (avg.)', - } - ), + title: i18n.translate('xpack.apm.serviceMap.avgMemoryUsagePopoverStat', { + defaultMessage: 'Memory usage (avg.)', + }), description: isNumber(avgMemoryUsage) ? asPercent(avgMemoryUsage, 1) : null, diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts index 6bc370be903df..28b095335e93d 100644 --- a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts +++ b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts @@ -9,7 +9,8 @@ import { argv } from 'yargs'; import pLimit from 'p-limit'; import pRetry from 'p-retry'; import { parse, format } from 'url'; -import { unique, without, set, merge, flatten } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { unique, without, merge, flatten } from 'lodash'; import * as histogram from 'hdr-histogram-js'; import { ESSearchResponse } from '../../typings/elasticsearch'; import { diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_parsed_ui_filters.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_parsed_ui_filters.ts new file mode 100644 index 0000000000000..324da199807c7 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_parsed_ui_filters.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from 'src/core/server'; +import { UIFilters } from '../../../../typings/ui_filters'; + +export function getParsedUiFilters({ + uiFilters, + logger, +}: { + uiFilters: string; + logger: Logger; +}): UIFilters { + try { + return JSON.parse(uiFilters); + } catch (error) { + logger.error(error); + } + return {}; +} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts new file mode 100644 index 0000000000000..1e0d001340edf --- /dev/null +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getServiceMapServiceNodeInfo } from './get_service_map_service_node_info'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import * as getErrorRateModule from '../transaction_groups/get_error_rate'; + +describe('getServiceMapServiceNodeInfo', () => { + describe('with no results', () => { + it('returns null data', async () => { + const setup = ({ + client: { + search: () => + Promise.resolve({ + hits: { total: { value: 0 } }, + }), + }, + indices: {}, + } as unknown) as Setup & SetupTimeRange; + const environment = 'test environment'; + const serviceName = 'test service name'; + const result = await getServiceMapServiceNodeInfo({ + uiFilters: { environment }, + setup, + serviceName, + }); + + expect(result).toEqual({ + avgCpuUsage: null, + avgErrorRate: null, + avgMemoryUsage: null, + transactionStats: { + avgRequestsPerMinute: null, + avgTransactionDuration: null, + }, + }); + }); + }); + + describe('with some results', () => { + it('returns data', async () => { + jest.spyOn(getErrorRateModule, 'getErrorRate').mockResolvedValueOnce({ + average: 0.5, + erroneousTransactionsRate: [], + noHits: false, + }); + + const setup = ({ + client: { + search: () => + Promise.resolve({ + hits: { total: { value: 1 } }, + }), + }, + indices: {}, + start: 1593460053026000, + end: 1593497863217000, + } as unknown) as Setup & SetupTimeRange; + const environment = 'test environment'; + const serviceName = 'test service name'; + const result = await getServiceMapServiceNodeInfo({ + uiFilters: { environment }, + setup, + serviceName, + }); + + expect(result).toEqual({ + avgCpuUsage: null, + avgErrorRate: 0.5, + avgMemoryUsage: null, + transactionStats: { + avgRequestsPerMinute: 0.000001586873761097901, + avgTransactionDuration: null, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index dd5d19b620c51..0f7136d6d74a4 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -4,23 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { ESFilter } from '../../../typings/elasticsearch'; -import { rangeFilter } from '../../../common/utils/range_filter'; +import { UIFilters } from '../../../typings/ui_filters'; import { - PROCESSOR_EVENT, - SERVICE_NAME, - TRANSACTION_DURATION, TRANSACTION_TYPE, METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_DURATION, } from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeFilter } from '../../../common/utils/range_filter'; +import { ESFilter } from '../../../typings/elasticsearch'; +import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; import { TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD, } from '../../../common/transaction_types'; +import { getErrorRate } from '../transaction_groups/get_error_rate'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; interface Options { @@ -30,69 +33,72 @@ interface Options { } interface TaskParameters { - setup: Setup; - minutes: number; + environment?: string; filter: ESFilter[]; + minutes: number; + serviceName?: string; + setup: Setup; } export async function getServiceMapServiceNodeInfo({ serviceName, - environment, setup, -}: Options & { serviceName: string; environment?: string }) { + uiFilters, +}: Options & { serviceName: string; uiFilters: UIFilters }) { const { start, end } = setup; const filter: ESFilter[] = [ { range: rangeFilter(start, end) }, { term: { [SERVICE_NAME]: serviceName } }, - ...getEnvironmentUiFilterES(environment), + ...getEnvironmentUiFilterES(uiFilters.environment), ]; const minutes = Math.abs((end - start) / (1000 * 60)); - const taskParams = { setup, minutes, filter }; + const taskParams = { + environment: uiFilters.environment, + filter, + minutes, + serviceName, + setup, + }; const [ - errorMetrics, + errorStats, transactionStats, - cpuMetrics, - memoryMetrics, + cpuStats, + memoryStats, ] = await Promise.all([ - getErrorMetrics(taskParams), + getErrorStats(taskParams), getTransactionStats(taskParams), - getCpuMetrics(taskParams), - getMemoryMetrics(taskParams), + getCpuStats(taskParams), + getMemoryStats(taskParams), ]); - return { - ...errorMetrics, + ...errorStats, transactionStats, - ...cpuMetrics, - ...memoryMetrics, + ...cpuStats, + ...memoryStats, }; } -async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) { - const { client, indices } = setup; - - const response = await client.search({ - index: indices['apm_oss.errorIndices'], - body: { - size: 0, - query: { - bool: { - filter: filter.concat({ term: { [PROCESSOR_EVENT]: 'error' } }), - }, - }, - track_total_hits: true, - }, - }); - - return { - avgErrorsPerMinute: - response.hits.total.value > 0 - ? response.hits.total.value / minutes - : null, +async function getErrorStats({ + setup, + serviceName, + environment, +}: { + setup: Options['setup']; + serviceName: string; + environment?: string; +}) { + const setupWithBlankUiFilters = { + ...setup, + uiFiltersES: getEnvironmentUiFilterES(environment), }; + const { noHits, average } = await getErrorRate({ + setup: setupWithBlankUiFilters, + serviceName, + }); + return { avgErrorRate: noHits ? null : average }; } async function getTransactionStats({ @@ -113,7 +119,7 @@ async function getTransactionStats({ bool: { filter: [ ...filter, - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { terms: { [TRANSACTION_TYPE]: [ @@ -137,7 +143,7 @@ async function getTransactionStats({ }; } -async function getCpuMetrics({ +async function getCpuStats({ setup, filter, }: TaskParameters): Promise<{ avgCpuUsage: number | null }> { @@ -150,7 +156,7 @@ async function getCpuMetrics({ query: { bool: { filter: filter.concat([ - { term: { [PROCESSOR_EVENT]: 'metric' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, { exists: { field: METRIC_SYSTEM_CPU_PERCENT } }, ]), }, @@ -162,7 +168,7 @@ async function getCpuMetrics({ return { avgCpuUsage: response.aggregations?.avgCpuUsage.value ?? null }; } -async function getMemoryMetrics({ +async function getMemoryStats({ setup, filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 5b66f7d7a45e7..6a1ee8daad7c7 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -3,11 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { mean } from 'lodash'; import { PROCESSOR_EVENT, HTTP_RESPONSE_STATUS_CODE, TRANSACTION_NAME, TRANSACTION_TYPE, + SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; @@ -39,6 +41,7 @@ export async function getErrorRate({ : []; const filter = [ + { term: { [SERVICE_NAME]: serviceName } }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: rangeFilter(start, end) }, { exists: { field: HTTP_RESPONSE_STATUS_CODE } }, @@ -82,5 +85,11 @@ export async function getErrorRate({ } ) || []; - return { noHits, erroneousTransactionsRate }; + const average = mean( + erroneousTransactionsRate + .map((errorRate) => errorRate.y) + .filter((y) => isFinite(y)) + ); + + return { noHits, erroneousTransactionsRate, average }; } diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 50123131a42e7..971e247d98986 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -14,8 +14,9 @@ import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; -import { rangeRt } from './default_api_types'; +import { rangeRt, uiFiltersRt } from './default_api_types'; import { APM_SERVICE_MAPS_FEATURE_NAME } from '../feature'; +import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const serviceMapRoute = createRoute(() => ({ path: '/api/apm/service-map', @@ -52,12 +53,7 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ path: t.type({ serviceName: t.string, }), - query: t.intersection([ - rangeRt, - t.partial({ - environment: t.string, - }), - ]), + query: t.intersection([rangeRt, uiFiltersRt]), }, handler: async ({ context, request }) => { if (!context.config['xpack.apm.serviceMapEnabled']) { @@ -66,17 +62,20 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ if (!isValidPlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } + const logger = context.logger; const setup = await setupRequest(context, request); const { - query: { environment }, + query: { uiFilters: uiFiltersJson }, path: { serviceName }, } = context.params; + const uiFilters = getParsedUiFilters({ uiFilters: uiFiltersJson, logger }); + return getServiceMapServiceNodeInfo({ setup, serviceName, - environment, + uiFilters, }); }, })); diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index dca2fb1d9b295..813d757c7c33e 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -15,7 +15,7 @@ import { uiFiltersRt, rangeRt } from './default_api_types'; import { getTransactionAvgDurationByBrowser } from '../lib/transactions/avg_duration_by_browser'; import { getTransactionAvgDurationByCountry } from '../lib/transactions/avg_duration_by_country'; import { getErrorRate } from '../lib/transaction_groups/get_error_rate'; -import { UIFilters } from '../../typings/ui_filters'; +import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const transactionGroupsRoute = createRoute(() => ({ path: '/api/apm/services/{serviceName}/transaction_groups', @@ -71,12 +71,8 @@ export const transactionGroupsChartsRoute = createRoute(() => ({ transactionName, uiFilters: uiFiltersJson, } = context.params.query; - let uiFilters: UIFilters = {}; - try { - uiFilters = JSON.parse(uiFiltersJson); - } catch (error) { - logger.error(error); - } + + const uiFilters = getParsedUiFilters({ uiFilters: uiFiltersJson, logger }); return getTransactionCharts({ serviceName, diff --git a/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts b/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts index 5579c70e15017..b486ba82689e8 100644 --- a/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts +++ b/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts @@ -5,7 +5,8 @@ */ import yaml from 'js-yaml'; -import { get, has, omit, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get, has, omit } from 'lodash'; import { ConfigBlockSchema, ConfigurationBlock, diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts index 4ffd2ff3e0c96..9dc7ee8da6d73 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/plot/index.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { groupBy, get, keyBy, set, map, sortBy } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { groupBy, get, keyBy, map, sortBy } from 'lodash'; import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions'; // @ts-expect-error untyped local import { getColorsFromPalette } from '../../../../common/lib/get_colors_from_palette'; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/index.ts b/x-pack/plugins/canvas/public/components/asset_manager/index.ts index b07857f13f6c6..9b4406f607867 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/index.ts +++ b/x-pack/plugins/canvas/public/components/asset_manager/index.ts @@ -6,7 +6,8 @@ import { connect } from 'react-redux'; import { compose, withProps } from 'recompose'; -import { set, get } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get } from 'lodash'; import { fromExpression, toExpression } from '@kbn/interpreter/common'; import { getAssets } from '../../state/selectors/assets'; // @ts-expect-error untyped local diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/font.js b/x-pack/plugins/canvas/public/expression_types/arg_types/font.js index 3e88d60b40d5f..5d0e6b3dd688e 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/font.js +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/font.js @@ -6,7 +6,8 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { get, mapValues, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get, mapValues } from 'lodash'; import { openSans } from '../../../common/lib/fonts'; import { templateFromReactComponent } from '../../lib/template_from_react_component'; import { TextStylePicker } from '../../components/text_style_picker'; diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 231f1d434b892..bdf3f6a0acf90 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -41,6 +41,7 @@ export class DataEnhancedPlugin application: core.application, http: core.http, uiSettings: core.uiSettings, + usageCollector: plugins.data.search.usageCollector, }, core.injectedMetadata.getInjectedVar('esRequestTimeout') as number ); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 9f018f5b718c7..9bd1ffddeaca8 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -36,12 +36,25 @@ function mockFetchImplementation(responses: any[]) { } describe('EnhancedSearchInterceptor', () => { + let mockUsageCollector: any; + beforeEach(() => { mockCoreStart = coreMock.createStart(); next.mockClear(); error.mockClear(); complete.mockClear(); + jest.clearAllTimers(); + + mockUsageCollector = { + trackQueryTimedOut: jest.fn(), + trackQueriesCancelled: jest.fn(), + trackLongQueryPopupShown: jest.fn(), + trackLongQueryDialogDismissed: jest.fn(), + trackLongQueryRunBeyondTimeout: jest.fn(), + trackError: jest.fn(), + trackSuccess: jest.fn(), + }; searchInterceptor = new EnhancedSearchInterceptor( { @@ -49,6 +62,7 @@ describe('EnhancedSearchInterceptor', () => { application: mockCoreStart.application, http: mockCoreStart.http, uiSettings: mockCoreStart.uiSettings, + usageCollector: mockUsageCollector, }, 1000 ); @@ -63,6 +77,9 @@ describe('EnhancedSearchInterceptor', () => { is_partial: false, is_running: false, id: 1, + rawResponse: { + took: 1, + }, }, }, ]; @@ -87,6 +104,9 @@ describe('EnhancedSearchInterceptor', () => { is_partial: false, is_running: true, id: 1, + rawResponse: { + took: 1, + }, }, }, { @@ -95,6 +115,9 @@ describe('EnhancedSearchInterceptor', () => { is_partial: false, is_running: false, id: 1, + rawResponse: { + took: 1, + }, }, }, ]; @@ -350,6 +373,7 @@ describe('EnhancedSearchInterceptor', () => { ([{ signal }]) => signal?.aborted ); expect(areAllRequestsAborted).toBe(true); + expect(mockUsageCollector.trackQueriesCancelled).toBeCalledTimes(1); }); }); @@ -361,6 +385,9 @@ describe('EnhancedSearchInterceptor', () => { is_partial: true, is_running: true, id: 1, + rawResponse: { + took: 1, + }, }, }, { @@ -369,6 +396,9 @@ describe('EnhancedSearchInterceptor', () => { is_partial: false, is_running: false, id: 1, + rawResponse: { + took: 1, + }, }, }, ]; @@ -427,6 +457,8 @@ describe('EnhancedSearchInterceptor', () => { expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value); expect(next.mock.calls[1][0]).toStrictEqual(timedResponses[1].value); expect(error).not.toHaveBeenCalled(); + expect(mockUsageCollector.trackLongQueryRunBeyondTimeout).toBeCalledTimes(1); + expect(mockUsageCollector.trackSuccess).toBeCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index c0e2a6bd113eb..d1ed410065248 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -35,6 +35,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { this.hideToast(); this.abortController.abort(); this.abortController = new AbortController(); + if (this.deps.usageCollector) this.deps.usageCollector.trackQueriesCancelled(); }; /** @@ -43,6 +44,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { public runBeyondTimeout = () => { this.hideToast(); this.timeoutSubscriptions.unsubscribe(); + if (this.deps.usageCollector) this.deps.usageCollector.trackLongQueryRunBeyondTimeout(); }; protected showToast = () => { @@ -59,6 +61,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { toastLifeTimeMs: 1000000, } ); + if (this.deps.usageCollector) this.deps.usageCollector.trackLongQueryPopupShown(); }; public search( @@ -85,7 +88,12 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { } // If the response indicates it is complete, stop polling and complete the observable - if (!response.is_running) return EMPTY; + if (!response.is_running) { + if (this.deps.usageCollector && response.rawResponse) { + this.deps.usageCollector.trackSuccess(response.rawResponse.took); + } + return EMPTY; + } id = response.id; // Delay by the given poll interval diff --git a/x-pack/plugins/event_log/scripts/create_schemas.js b/x-pack/plugins/event_log/scripts/create_schemas.js index 2432a27e5c70d..709096393471f 100755 --- a/x-pack/plugins/event_log/scripts/create_schemas.js +++ b/x-pack/plugins/event_log/scripts/create_schemas.js @@ -8,6 +8,7 @@ const fs = require('fs'); const path = require('path'); +const { set } = require('@elastic/safer-lodash-set'); const lodash = require('lodash'); const LineWriter = require('./lib/line_writer'); @@ -49,7 +50,7 @@ function getEventLogMappings(ecsSchema, exportedProperties) { // copy the leaf values of the properties for (const prop of leafProperties) { const value = lodash.get(ecsSchema.mappings.properties, prop); - lodash.set(result.mappings.properties, prop, value); + set(result.mappings.properties, prop, value); } // set the non-leaf values as appropriate @@ -118,7 +119,7 @@ function augmentMappings(mappings, multiValuedProperties) { const metaPropName = `${fullProp}.meta`; const meta = lodash.get(mappings.properties, metaPropName) || {}; meta.isArray = 'true'; - lodash.set(mappings.properties, metaPropName, meta); + set(mappings.properties, metaPropName, meta); } } diff --git a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx index 22f7d3d3cd50a..35fb66b2620d6 100644 --- a/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx +++ b/x-pack/plugins/infra/public/containers/metrics_explorer/with_metrics_explorer_options_url_state.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set, values } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { values } from 'lodash'; import React, { useContext, useMemo } from 'react'; import * as t from 'io-ts'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index a81e11418cd6a..3afc0d050e736 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -6,7 +6,7 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { colorTransformer, MetricsExplorerColor } from '../../../../../../common/color_palette'; import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; import { diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts index 8a21a97631fbb..d0f0bd18b5d56 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { first, set, startsWith } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { first, startsWith } from 'lodash'; import { RequestHandlerContext } from 'src/core/server'; import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts index f4f877c188d0d..fdecb5f3d9315 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_groupings.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isObject, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { isObject } from 'lodash'; import { i18n } from '@kbn/i18n'; import { InfraDatabaseSearchResponse } from '../../../lib/adapters/framework'; import { diff --git a/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts b/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts index 2b65c42410723..cdfb9d7cc99f3 100644 --- a/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts +++ b/x-pack/plugins/infra/server/utils/create_afterkey_handler.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { InfraDatabaseSearchResponse } from '../lib/adapters/framework'; export const createAfterKeyHandler = ( diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts new file mode 100644 index 0000000000000..1f51140005e59 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../siem_common_deps'; + +import { getExceptionListSchemaMock } from './exception_list_schema.mock'; +import { CreateEndpointListSchema, createEndpointListSchema } from './create_endpoint_list_schema'; + +describe('create_endpoint_list_schema', () => { + test('it should validate a typical endpoint list response', () => { + const payload = getExceptionListSchemaMock(); + const decoded = createEndpointListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should accept an empty object when an endpoint list already exists', () => { + const payload = {}; + const decoded = createEndpointListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT allow missing fields', () => { + const payload = getExceptionListSchemaMock(); + delete payload.list_id; + const decoded = createEndpointListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors)).length).toEqual(1); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: CreateEndpointListSchema & { + extraKey?: string; + } = getExceptionListSchemaMock(); + payload.extraKey = 'some new value'; + const decoded = createEndpointListSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.ts b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.ts new file mode 100644 index 0000000000000..4653b73347f72 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable @typescript-eslint/camelcase */ + +import * as t from 'io-ts'; + +import { exceptionListSchema } from './exception_list_schema'; + +export const createEndpointListSchema = t.union([exceptionListSchema, t.exact(t.type({}))]); + +export type CreateEndpointListSchema = t.TypeOf; diff --git a/x-pack/plugins/lists/common/schemas/response/index.ts b/x-pack/plugins/lists/common/schemas/response/index.ts index fb6f17a896ddb..deca06ad99fea 100644 --- a/x-pack/plugins/lists/common/schemas/response/index.ts +++ b/x-pack/plugins/lists/common/schemas/response/index.ts @@ -5,6 +5,7 @@ */ export * from './acknowledge_schema'; +export * from './create_endpoint_list_schema'; export * from './exception_list_schema'; export * from './exception_list_item_schema'; export * from './found_exception_list_item_schema'; diff --git a/x-pack/plugins/lists/common/shared_exports.ts b/x-pack/plugins/lists/common/shared_exports.ts index 7bb565792969c..dc0a9aa5926ef 100644 --- a/x-pack/plugins/lists/common/shared_exports.ts +++ b/x-pack/plugins/lists/common/shared_exports.ts @@ -12,6 +12,7 @@ export { CreateComments, ExceptionListSchema, ExceptionListItemSchema, + CreateExceptionListSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, Entry, @@ -41,3 +42,5 @@ export { ExceptionListType, Type, } from './schemas'; + +export { ENDPOINT_LIST_ID } from './constants'; diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index cd54c24e95e2f..1414d828fa6d4 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -19,6 +19,7 @@ import { } from '../../common/schemas'; import { + addEndpointExceptionList, addExceptionList, addExceptionListItem, deleteExceptionListById, @@ -738,4 +739,39 @@ describe('Exceptions Lists API', () => { ).rejects.toEqual('Invalid value "undefined" supplied to "id"'); }); }); + + describe('#addEndpointExceptionList', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(getExceptionListSchemaMock()); + }); + + test('it invokes "addEndpointExceptionList" with expected url and body values', async () => { + await addEndpointExceptionList({ + http: mockKibanaHttpService(), + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/endpoint_list', { + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('it returns expected exception list on success', async () => { + const exceptionResponse = await addEndpointExceptionList({ + http: mockKibanaHttpService(), + signal: abortCtrl.signal, + }); + expect(exceptionResponse).toEqual(getExceptionListSchemaMock()); + }); + + test('it returns an empty object when list already exists', async () => { + fetchMock.mockResolvedValue({}); + const exceptionResponse = await addEndpointExceptionList({ + http: mockKibanaHttpService(), + signal: abortCtrl.signal, + }); + expect(exceptionResponse).toEqual({}); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/api.ts b/x-pack/plugins/lists/public/exceptions/api.ts index a581cfd08ecc1..4d9397ec0adc6 100644 --- a/x-pack/plugins/lists/public/exceptions/api.ts +++ b/x-pack/plugins/lists/public/exceptions/api.ts @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ import { + ENDPOINT_LIST_URL, EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_NAMESPACE, EXCEPTION_LIST_NAMESPACE_AGNOSTIC, EXCEPTION_LIST_URL, } from '../../common/constants'; import { + CreateEndpointListSchema, ExceptionListItemSchema, ExceptionListSchema, FoundExceptionListItemSchema, + createEndpointListSchema, createExceptionListItemSchema, createExceptionListSchema, deleteExceptionListItemSchema, @@ -29,6 +32,7 @@ import { import { validate } from '../../common/siem_common_deps'; import { + AddEndpointExceptionListProps, AddExceptionListItemProps, AddExceptionListProps, ApiCallByIdProps, @@ -440,3 +444,34 @@ export const deleteExceptionListItemById = async ({ return Promise.reject(errorsRequest); } }; + +/** + * Add new Endpoint ExceptionList + * + * @param http Kibana http service + * @param signal to cancel request + * + * @throws An error if response is not OK + * + */ +export const addEndpointExceptionList = async ({ + http, + signal, +}: AddEndpointExceptionListProps): Promise => { + try { + const response = await http.fetch(ENDPOINT_LIST_URL, { + method: 'POST', + signal, + }); + + const [validatedResponse, errorsResponse] = validate(response, createEndpointListSchema); + + if (errorsResponse != null || validatedResponse == null) { + return Promise.reject(errorsResponse); + } else { + return Promise.resolve(validatedResponse); + } + } catch (error) { + return Promise.reject(error); + } +}; diff --git a/x-pack/plugins/lists/public/exceptions/types.ts b/x-pack/plugins/lists/public/exceptions/types.ts index 1b4e09b07f1de..f99323b384781 100644 --- a/x-pack/plugins/lists/public/exceptions/types.ts +++ b/x-pack/plugins/lists/public/exceptions/types.ts @@ -110,3 +110,8 @@ export interface UpdateExceptionListItemProps { listItem: UpdateExceptionListItemSchema; signal: AbortSignal; } + +export interface AddEndpointExceptionListProps { + http: HttpStart; + signal: AbortSignal; +} diff --git a/x-pack/plugins/lists/public/shared_exports.ts b/x-pack/plugins/lists/public/shared_exports.ts index 57fb2f90b6404..56341035f839f 100644 --- a/x-pack/plugins/lists/public/shared_exports.ts +++ b/x-pack/plugins/lists/public/shared_exports.ts @@ -24,6 +24,7 @@ export { updateExceptionListItem, fetchExceptionListById, addExceptionList, + addEndpointExceptionList, } from './exceptions/api'; export { ExceptionList, diff --git a/x-pack/plugins/lists/server/scripts/lists/new/items/ip_item.json b/x-pack/plugins/lists/server/scripts/lists/new/items/ip_item.json index 563139c40c0ca..c2238890496bb 100644 --- a/x-pack/plugins/lists/server/scripts/lists/new/items/ip_item.json +++ b/x-pack/plugins/lists/server/scripts/lists/new/items/ip_item.json @@ -1,5 +1,5 @@ { "id": "ip_item", "list_id": "ip_list", - "value": "10.4.2.140" + "value": "127.0.0.1" } diff --git a/x-pack/plugins/lists/server/scripts/lists/new/items/keyword_item.json b/x-pack/plugins/lists/server/scripts/lists/new/items/keyword_item.json index 96d925c157490..0848dc4c1bd94 100644 --- a/x-pack/plugins/lists/server/scripts/lists/new/items/keyword_item.json +++ b/x-pack/plugins/lists/server/scripts/lists/new/items/keyword_item.json @@ -1,4 +1,4 @@ { "list_id": "keyword_list", - "value": "kibana" + "value": "zeek" } diff --git a/x-pack/plugins/monitoring/public/components/table/storage.js b/x-pack/plugins/monitoring/public/components/table/storage.js index 037839a2654c1..1be8528d5ab23 100644 --- a/x-pack/plugins/monitoring/public/components/table/storage.js +++ b/x-pack/plugins/monitoring/public/components/table/storage.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get } from 'lodash'; import { STORAGE_KEY } from '../../../common/constants'; export const tableStorageGetter = (keyPrefix) => { diff --git a/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js b/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js index 83a79a30069f0..6aee89a9817d5 100644 --- a/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js +++ b/x-pack/plugins/monitoring/public/lib/calculate_shard_stats.js @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { set } from '@elastic/safer-lodash-set'; import _ from 'lodash'; function addOne(obj, key) { let value = _.get(obj, key); - _.set(obj, key, ++value); + set(obj, key, ++value); } export function calculateShardStats(state) { diff --git a/x-pack/plugins/monitoring/server/lib/__tests__/create_query.js b/x-pack/plugins/monitoring/server/lib/__tests__/create_query.js index 7d5661ccd7560..e8862c47d4bf2 100644 --- a/x-pack/plugins/monitoring/server/lib/__tests__/create_query.js +++ b/x-pack/plugins/monitoring/server/lib/__tests__/create_query.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { MissingRequiredError } from '../error_missing_required'; import { ElasticsearchMetric } from '../metrics'; import { createQuery } from '../create_query.js'; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_state.js b/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_state.js index d1bc3a0a7e381..cc62e59986f1d 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_state.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/__tests__/get_clusters_state.js @@ -7,7 +7,7 @@ import { handleResponse } from '../get_clusters_state'; import expect from '@kbn/expect'; import moment from 'moment'; -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; const clusters = [ { diff --git a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js index 03de24916a6db..8e0d125d122aa 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/flag_supported_clusters.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set, find } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get, find } from 'lodash'; import { checkParam } from '../error_missing_required'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 50a4df8a3ff57..18db738bba38e 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -5,7 +5,8 @@ */ import { notFound } from 'boom'; -import { set, findIndex } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { findIndex } from 'lodash'; import { getClustersStats } from './get_clusters_stats'; import { flagSupportedClusters } from './flag_supported_clusters'; import { getMlJobsForCluster } from '../elasticsearch'; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_ml_jobs.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_ml_jobs.js index 58fc2e30972e5..c2cf19471ecb2 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_ml_jobs.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/__tests__/get_ml_jobs.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import expect from '@kbn/expect'; import { handleResponse } from '../get_ml_jobs'; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/__tests__/calculate_node_type.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/__tests__/calculate_node_type.js index b9adcb725f0b8..9b4f1d586a319 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/__tests__/calculate_node_type.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/__tests__/calculate_node_type.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import expect from '@kbn/expect'; import { calculateNodeType } from '../calculate_node_type.js'; diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/create_query.test.ts b/x-pack/plugins/monitoring/server/telemetry_collection/create_query.test.ts index a85d084f83d83..ae5ae9320f0f4 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/create_query.test.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/create_query.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { createTypeFilter, createQuery } from './create_query'; describe('Create Type Filter', () => { diff --git a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts index 45fdf1997d214..726db1706758d 100644 --- a/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts +++ b/x-pack/plugins/monitoring/server/telemetry_collection/get_all_stats.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set, merge } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get, merge } from 'lodash'; import { StatsGetter } from 'src/plugins/telemetry_collection_manager/server'; import { LOGSTASH_SYSTEM_ID, KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID } from '../../common/constants'; diff --git a/x-pack/plugins/reporting/server/browsers/network_policy.ts b/x-pack/plugins/reporting/server/browsers/network_policy.ts index 158362cee3c7e..77458a7d61e08 100644 --- a/x-pack/plugins/reporting/server/browsers/network_policy.ts +++ b/x-pack/plugins/reporting/server/browsers/network_policy.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as _ from 'lodash'; +import { every } from 'lodash'; import { parse } from 'url'; interface NetworkPolicyRule { @@ -22,7 +22,7 @@ const isHostMatch = (actualHost: string, ruleHost: string) => { const hostParts = actualHost.split('.').reverse(); const ruleParts = ruleHost.split('.').reverse(); - return _.every(ruleParts, (part, idx) => part === hostParts[idx]); + return every(ruleParts, (part, idx) => part === hostParts[idx]); }; export const allowRequest = (url: string, rules: NetworkPolicyRule[]) => { diff --git a/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts b/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts index 58e63a522e609..651c6a0347c46 100644 --- a/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts +++ b/x-pack/plugins/reporting/server/export_types/common/validate_urls.ts @@ -5,7 +5,7 @@ */ import { parse } from 'url'; -import * as _ from 'lodash'; +import { filter } from 'lodash'; /* * isBogusUrl @@ -21,7 +21,7 @@ const isBogusUrl = (url: string) => { }; export const validateUrls = (urls: string[]): void => { - const badUrls = _.filter(urls, (url) => isBogusUrl(url)); + const badUrls = filter(urls, (url) => isBogusUrl(url)); if (badUrls.length) { throw new Error(`Found invalid URL(s), all URLs must be relative: ${badUrls.join(' ')}`); diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts index d89eb45ead75e..83a73c53a0b60 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/check_cells_for_formulas.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as _ from 'lodash'; +import { pick, keys, values, some } from 'lodash'; import { cellHasFormulas } from './cell_has_formula'; interface IFlattened { @@ -12,8 +12,8 @@ interface IFlattened { } export const checkIfRowsHaveFormulas = (flattened: IFlattened, fields: string[]) => { - const pruned = _.pick(flattened, fields); - const cells = [..._.keys(pruned), ...(_.values(pruned) as string[])]; + const pruned = pick(flattened, fields); + const cells = [...keys(pruned), ...(values(pruned) as string[])]; - return _.some(cells, (cell) => cellHasFormulas(cell)); + return some(cells, (cell) => cellHasFormulas(cell)); }; diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index 93f79bfd892b9..d384cbb878a0e 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -6,7 +6,7 @@ // @ts-ignore import contentDisposition from 'content-disposition'; -import * as _ from 'lodash'; +import { get } from 'lodash'; import { CSV_JOB_TYPE } from '../../../common/constants'; import { statuses } from '../../lib/esqueue/constants/statuses'; import { ExportTypesRegistry } from '../../lib/export_types_registry'; @@ -35,8 +35,8 @@ const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeType) const metaDataHeaders: Record = {}; if (exportType.jobType === CSV_JOB_TYPE) { - const csvContainsFormulas = _.get(output, 'csv_contains_formulas', false); - const maxSizedReach = _.get(output, 'max_size_reached', false); + const csvContainsFormulas = get(output, 'csv_contains_formulas', false); + const maxSizedReach = get(output, 'max_size_reached', false); metaDataHeaders['kbn-csv-contains-formulas'] = csvContainsFormulas; metaDataHeaders['kbn-max-size-reached'] = maxSizedReach; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e5dd109007eab..b39a038c4cc3c 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -32,6 +32,8 @@ export const DEFAULT_INTERVAL_PAUSE = true; export const DEFAULT_INTERVAL_TYPE = 'manual'; export const DEFAULT_INTERVAL_VALUE = 300000; // ms export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges'; +export const FILTERS_GLOBAL_HEIGHT = 109; // px +export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled'; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51'; export const ENDPOINT_METADATA_INDEX = 'metrics-endpoint.metadata-*'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts index 26a219507c3ae..caf2dfb761ed0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.test.ts @@ -113,226 +113,97 @@ describe('build_exceptions_query', () => { }); describe('operatorBuilder', () => { - describe("when 'exclude' is true", () => { - describe('and langauge is kuery', () => { - test('it returns "not " when operator is "included"', () => { - const operator = operatorBuilder({ operator: 'included', language: 'kuery', exclude }); - expect(operator).toEqual('not '); - }); - test('it returns empty string when operator is "excluded"', () => { - const operator = operatorBuilder({ operator: 'excluded', language: 'kuery', exclude }); - expect(operator).toEqual(''); - }); + describe('and language is kuery', () => { + test('it returns empty string when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'kuery' }); + expect(operator).toEqual(''); }); - - describe('and language is lucene', () => { - test('it returns "NOT " when operator is "included"', () => { - const operator = operatorBuilder({ operator: 'included', language: 'lucene', exclude }); - expect(operator).toEqual('NOT '); - }); - test('it returns empty string when operator is "excluded"', () => { - const operator = operatorBuilder({ operator: 'excluded', language: 'lucene', exclude }); - expect(operator).toEqual(''); - }); + test('it returns "not " when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'kuery' }); + expect(operator).toEqual('not '); }); }); - describe("when 'exclude' is false", () => { - beforeEach(() => { - exclude = false; - }); - describe('and language is kuery', () => { - test('it returns empty string when operator is "included"', () => { - const operator = operatorBuilder({ operator: 'included', language: 'kuery', exclude }); - expect(operator).toEqual(''); - }); - test('it returns "not " when operator is "excluded"', () => { - const operator = operatorBuilder({ operator: 'excluded', language: 'kuery', exclude }); - expect(operator).toEqual('not '); - }); + describe('and language is lucene', () => { + test('it returns empty string when operator is "included"', () => { + const operator = operatorBuilder({ operator: 'included', language: 'lucene' }); + expect(operator).toEqual(''); }); - - describe('and language is lucene', () => { - test('it returns empty string when operator is "included"', () => { - const operator = operatorBuilder({ operator: 'included', language: 'lucene', exclude }); - expect(operator).toEqual(''); - }); - test('it returns "NOT " when operator is "excluded"', () => { - const operator = operatorBuilder({ operator: 'excluded', language: 'lucene', exclude }); - expect(operator).toEqual('NOT '); - }); + test('it returns "NOT " when operator is "excluded"', () => { + const operator = operatorBuilder({ operator: 'excluded', language: 'lucene' }); + expect(operator).toEqual('NOT '); }); }); }); describe('buildExists', () => { - describe("when 'exclude' is true", () => { - describe('kuery', () => { - test('it returns formatted wildcard string when operator is "excluded"', () => { - const query = buildExists({ - item: existsEntryWithExcluded, - language: 'kuery', - exclude, - }); - expect(query).toEqual('host.name:*'); - }); - test('it returns formatted wildcard string when operator is "included"', () => { - const query = buildExists({ - item: existsEntryWithIncluded, - language: 'kuery', - exclude, - }); - expect(query).toEqual('not host.name:*'); + describe('kuery', () => { + test('it returns formatted wildcard string when operator is "excluded"', () => { + const query = buildExists({ + item: existsEntryWithExcluded, + language: 'kuery', }); + expect(query).toEqual('not host.name:*'); }); - - describe('lucene', () => { - test('it returns formatted wildcard string when operator is "excluded"', () => { - const query = buildExists({ - item: existsEntryWithExcluded, - language: 'lucene', - exclude, - }); - expect(query).toEqual('_exists_host.name'); - }); - test('it returns formatted wildcard string when operator is "included"', () => { - const query = buildExists({ - item: existsEntryWithIncluded, - language: 'lucene', - exclude, - }); - expect(query).toEqual('NOT _exists_host.name'); + test('it returns formatted wildcard string when operator is "included"', () => { + const query = buildExists({ + item: existsEntryWithIncluded, + language: 'kuery', }); + expect(query).toEqual('host.name:*'); }); }); - describe("when 'exclude' is false", () => { - beforeEach(() => { - exclude = false; - }); - - describe('kuery', () => { - test('it returns formatted wildcard string when operator is "excluded"', () => { - const query = buildExists({ - item: existsEntryWithExcluded, - language: 'kuery', - exclude, - }); - expect(query).toEqual('not host.name:*'); - }); - test('it returns formatted wildcard string when operator is "included"', () => { - const query = buildExists({ - item: existsEntryWithIncluded, - language: 'kuery', - exclude, - }); - expect(query).toEqual('host.name:*'); + describe('lucene', () => { + test('it returns formatted wildcard string when operator is "excluded"', () => { + const query = buildExists({ + item: existsEntryWithExcluded, + language: 'lucene', }); + expect(query).toEqual('NOT _exists_host.name'); }); - - describe('lucene', () => { - test('it returns formatted wildcard string when operator is "excluded"', () => { - const query = buildExists({ - item: existsEntryWithExcluded, - language: 'lucene', - exclude, - }); - expect(query).toEqual('NOT _exists_host.name'); - }); - test('it returns formatted wildcard string when operator is "included"', () => { - const query = buildExists({ - item: existsEntryWithIncluded, - language: 'lucene', - exclude, - }); - expect(query).toEqual('_exists_host.name'); + test('it returns formatted wildcard string when operator is "included"', () => { + const query = buildExists({ + item: existsEntryWithIncluded, + language: 'lucene', }); + expect(query).toEqual('_exists_host.name'); }); }); }); describe('buildMatch', () => { - describe("when 'exclude' is true", () => { - describe('kuery', () => { - test('it returns formatted string when operator is "included"', () => { - const query = buildMatch({ - item: matchEntryWithIncluded, - language: 'kuery', - exclude, - }); - expect(query).toEqual('not host.name:suricata'); - }); - test('it returns formatted string when operator is "excluded"', () => { - const query = buildMatch({ - item: matchEntryWithExcluded, - language: 'kuery', - exclude, - }); - expect(query).toEqual('host.name:suricata'); + describe('kuery', () => { + test('it returns formatted string when operator is "included"', () => { + const query = buildMatch({ + item: matchEntryWithIncluded, + language: 'kuery', }); + expect(query).toEqual('host.name:"suricata"'); }); - - describe('lucene', () => { - test('it returns formatted string when operator is "included"', () => { - const query = buildMatch({ - item: matchEntryWithIncluded, - language: 'lucene', - exclude, - }); - expect(query).toEqual('NOT host.name:suricata'); - }); - test('it returns formatted string when operator is "excluded"', () => { - const query = buildMatch({ - item: matchEntryWithExcluded, - language: 'lucene', - exclude, - }); - expect(query).toEqual('host.name:suricata'); + test('it returns formatted string when operator is "excluded"', () => { + const query = buildMatch({ + item: matchEntryWithExcluded, + language: 'kuery', }); + expect(query).toEqual('not host.name:"suricata"'); }); }); - describe("when 'exclude' is false", () => { - beforeEach(() => { - exclude = false; - }); - - describe('kuery', () => { - test('it returns formatted string when operator is "included"', () => { - const query = buildMatch({ - item: matchEntryWithIncluded, - language: 'kuery', - exclude, - }); - expect(query).toEqual('host.name:suricata'); - }); - test('it returns formatted string when operator is "excluded"', () => { - const query = buildMatch({ - item: matchEntryWithExcluded, - language: 'kuery', - exclude, - }); - expect(query).toEqual('not host.name:suricata'); + describe('lucene', () => { + test('it returns formatted string when operator is "included"', () => { + const query = buildMatch({ + item: matchEntryWithIncluded, + language: 'lucene', }); + expect(query).toEqual('host.name:"suricata"'); }); - - describe('lucene', () => { - test('it returns formatted string when operator is "included"', () => { - const query = buildMatch({ - item: matchEntryWithIncluded, - language: 'lucene', - exclude, - }); - expect(query).toEqual('host.name:suricata'); - }); - test('it returns formatted string when operator is "excluded"', () => { - const query = buildMatch({ - item: matchEntryWithExcluded, - language: 'lucene', - exclude, - }); - expect(query).toEqual('NOT host.name:suricata'); + test('it returns formatted string when operator is "excluded"', () => { + const query = buildMatch({ + item: matchEntryWithExcluded, + language: 'lucene', }); + expect(query).toEqual('NOT host.name:"suricata"'); }); }); }); @@ -352,152 +223,83 @@ describe('build_exceptions_query', () => { operator: 'excluded', }); - describe("when 'exclude' is true", () => { - describe('kuery', () => { - test('it returns empty string if given an empty array for "values"', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithIncludedAndNoValues, - language: 'kuery', - exclude, - }); - expect(exceptionSegment).toEqual(''); - }); - test('it returns formatted string when "values" includes only one item', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithIncludedAndOneValue, - language: 'kuery', - exclude, - }); - expect(exceptionSegment).toEqual('not host.name:(suricata)'); - }); - test('it returns formatted string when operator is "included"', () => { - const exceptionSegment = buildMatchAny({ - item: matchAnyEntryWithIncludedAndTwoValues, - language: 'kuery', - exclude, - }); - expect(exceptionSegment).toEqual('not host.name:(suricata or auditd)'); + describe('kuery', () => { + test('it returns empty string if given an empty array for "values"', () => { + const exceptionSegment = buildMatchAny({ + item: entryWithIncludedAndNoValues, + language: 'kuery', }); + expect(exceptionSegment).toEqual(''); + }); - test('it returns formatted string when operator is "excluded"', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithExcludedAndTwoValues, - language: 'kuery', - exclude, - }); - expect(exceptionSegment).toEqual('host.name:(suricata or auditd)'); + test('it returns formatted string when "values" includes only one item', () => { + const exceptionSegment = buildMatchAny({ + item: entryWithIncludedAndOneValue, + language: 'kuery', }); + + expect(exceptionSegment).toEqual('host.name:("suricata")'); }); - describe('lucene', () => { - test('it returns formatted string when operator is "included"', () => { - const exceptionSegment = buildMatchAny({ - item: matchAnyEntryWithIncludedAndTwoValues, - language: 'lucene', - exclude, - }); - expect(exceptionSegment).toEqual('NOT host.name:(suricata OR auditd)'); - }); - test('it returns formatted string when operator is "excluded"', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithExcludedAndTwoValues, - language: 'lucene', - exclude, - }); - expect(exceptionSegment).toEqual('host.name:(suricata OR auditd)'); - }); - test('it returns formatted string when "values" includes only one item', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithIncludedAndOneValue, - language: 'lucene', - exclude, - }); - expect(exceptionSegment).toEqual('NOT host.name:(suricata)'); + test('it returns formatted string when operator is "included"', () => { + const exceptionSegment = buildMatchAny({ + item: matchAnyEntryWithIncludedAndTwoValues, + language: 'kuery', }); - }); - }); - describe("when 'exclude' is false", () => { - beforeEach(() => { - exclude = false; + expect(exceptionSegment).toEqual('host.name:("suricata" or "auditd")'); }); - describe('kuery', () => { - test('it returns empty string if given an empty array for "values"', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithIncludedAndNoValues, - language: 'kuery', - exclude, - }); - expect(exceptionSegment).toEqual(''); - }); - test('it returns formatted string when "values" includes only one item', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithIncludedAndOneValue, - language: 'kuery', - exclude, - }); - expect(exceptionSegment).toEqual('host.name:(suricata)'); - }); - test('it returns formatted string when operator is "included"', () => { - const exceptionSegment = buildMatchAny({ - item: matchAnyEntryWithIncludedAndTwoValues, - language: 'kuery', - exclude, - }); - expect(exceptionSegment).toEqual('host.name:(suricata or auditd)'); + test('it returns formatted string when operator is "excluded"', () => { + const exceptionSegment = buildMatchAny({ + item: entryWithExcludedAndTwoValues, + language: 'kuery', }); - test('it returns formatted string when operator is "excluded"', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithExcludedAndTwoValues, - language: 'kuery', - exclude, - }); - expect(exceptionSegment).toEqual('not host.name:(suricata or auditd)'); - }); + expect(exceptionSegment).toEqual('not host.name:("suricata" or "auditd")'); }); + }); - describe('lucene', () => { - test('it returns formatted string when operator is "included"', () => { - const exceptionSegment = buildMatchAny({ - item: matchAnyEntryWithIncludedAndTwoValues, - language: 'lucene', - exclude, - }); - expect(exceptionSegment).toEqual('host.name:(suricata OR auditd)'); + describe('lucene', () => { + test('it returns formatted string when operator is "included"', () => { + const exceptionSegment = buildMatchAny({ + item: matchAnyEntryWithIncludedAndTwoValues, + language: 'lucene', }); - test('it returns formatted string when operator is "excluded"', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithExcludedAndTwoValues, - language: 'lucene', - exclude, - }); - expect(exceptionSegment).toEqual('NOT host.name:(suricata OR auditd)'); + + expect(exceptionSegment).toEqual('host.name:("suricata" OR "auditd")'); + }); + test('it returns formatted string when operator is "excluded"', () => { + const exceptionSegment = buildMatchAny({ + item: entryWithExcludedAndTwoValues, + language: 'lucene', }); - test('it returns formatted string when "values" includes only one item', () => { - const exceptionSegment = buildMatchAny({ - item: entryWithIncludedAndOneValue, - language: 'lucene', - exclude, - }); - expect(exceptionSegment).toEqual('host.name:(suricata)'); + + expect(exceptionSegment).toEqual('NOT host.name:("suricata" OR "auditd")'); + }); + test('it returns formatted string when "values" includes only one item', () => { + const exceptionSegment = buildMatchAny({ + item: entryWithIncludedAndOneValue, + language: 'lucene', }); + + expect(exceptionSegment).toEqual('host.name:("suricata")'); }); }); }); describe('buildNested', () => { + // NOTE: Only KQL supports nested describe('kuery', () => { test('it returns formatted query when one item in nested entry', () => { const item: EntryNested = { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'nestedField', operator: 'excluded' })], + entries: [makeMatchEntry({ field: 'nestedField', operator: 'included' })], }; const result = buildNested({ item, language: 'kuery' }); - expect(result).toEqual('parent:{ nestedField:value-1 }'); + expect(result).toEqual('parent:{ nestedField:"value-1" }'); }); test('it returns formatted query when multiple items in nested entry', () => { @@ -505,206 +307,128 @@ describe('build_exceptions_query', () => { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded' }), - makeMatchEntry({ field: 'nestedFieldB', operator: 'excluded', value: 'value-2' }), + makeMatchEntry({ field: 'nestedField', operator: 'included' }), + makeMatchEntry({ field: 'nestedFieldB', operator: 'included', value: 'value-2' }), ], }; const result = buildNested({ item, language: 'kuery' }); - expect(result).toEqual('parent:{ nestedField:value-1 and nestedFieldB:value-2 }'); - }); - }); - - // TODO: Does lucene support nested query syntax? - describe.skip('lucene', () => { - test('it returns formatted query when one item in nested entry', () => { - const item: EntryNested = { - field: 'parent', - type: 'nested', - entries: [makeMatchEntry({ field: 'nestedField', operator: 'excluded' })], - }; - const result = buildNested({ item, language: 'lucene' }); - - expect(result).toEqual('parent:{ nestedField:value-1 }'); - }); - - test('it returns formatted query when multiple items in nested entry', () => { - const item: EntryNested = { - field: 'parent', - type: 'nested', - entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded' }), - makeMatchEntry({ field: 'nestedFieldB', operator: 'excluded', value: 'value-2' }), - ], - }; - const result = buildNested({ item, language: 'lucene' }); - - expect(result).toEqual('parent:{ nestedField:value-1 AND nestedFieldB:value-2 }'); + expect(result).toEqual('parent:{ nestedField:"value-1" and nestedFieldB:"value-2" }'); }); }); }); describe('evaluateValues', () => { - describe("when 'exclude' is true", () => { - describe('kuery', () => { - test('it returns formatted wildcard string when "type" is "exists"', () => { - const result = evaluateValues({ - item: existsEntryWithIncluded, - language: 'kuery', - exclude, - }); - expect(result).toEqual('not host.name:*'); - }); - test('it returns formatted string when "type" is "match"', () => { - const result = evaluateValues({ - item: matchEntryWithIncluded, - language: 'kuery', - exclude, - }); - expect(result).toEqual('not host.name:suricata'); - }); - test('it returns formatted string when "type" is "match_any"', () => { - const result = evaluateValues({ - item: matchAnyEntryWithIncludedAndTwoValues, - language: 'kuery', - exclude, - }); - expect(result).toEqual('not host.name:(suricata or auditd)'); + describe('kuery', () => { + test('it returns formatted wildcard string when "type" is "exists"', () => { + const result = evaluateValues({ + item: existsEntryWithIncluded, + language: 'kuery', }); + expect(result).toEqual('host.name:*'); }); - describe('lucene', () => { - describe('kuery', () => { - test('it returns formatted wildcard string when "type" is "exists"', () => { - const result = evaluateValues({ - item: existsEntryWithIncluded, - language: 'lucene', - exclude, - }); - expect(result).toEqual('NOT _exists_host.name'); - }); - test('it returns formatted string when "type" is "match"', () => { - const result = evaluateValues({ - item: matchEntryWithIncluded, - language: 'lucene', - exclude, - }); - expect(result).toEqual('NOT host.name:suricata'); - }); - test('it returns formatted string when "type" is "match_any"', () => { - const result = evaluateValues({ - item: matchAnyEntryWithIncludedAndTwoValues, - language: 'lucene', - exclude, - }); - expect(result).toEqual('NOT host.name:(suricata OR auditd)'); - }); + test('it returns formatted string when "type" is "match"', () => { + const result = evaluateValues({ + item: matchEntryWithIncluded, + language: 'kuery', }); + expect(result).toEqual('host.name:"suricata"'); }); - }); - describe("when 'exclude' is false", () => { - beforeEach(() => { - exclude = false; + test('it returns formatted string when "type" is "match_any"', () => { + const result = evaluateValues({ + item: matchAnyEntryWithIncludedAndTwoValues, + language: 'kuery', + }); + expect(result).toEqual('host.name:("suricata" or "auditd")'); }); + }); + describe('lucene', () => { describe('kuery', () => { test('it returns formatted wildcard string when "type" is "exists"', () => { const result = evaluateValues({ item: existsEntryWithIncluded, - language: 'kuery', - exclude, + language: 'lucene', }); - expect(result).toEqual('host.name:*'); + expect(result).toEqual('_exists_host.name'); }); + test('it returns formatted string when "type" is "match"', () => { const result = evaluateValues({ item: matchEntryWithIncluded, - language: 'kuery', - exclude, + language: 'lucene', }); - expect(result).toEqual('host.name:suricata'); + expect(result).toEqual('host.name:"suricata"'); }); + test('it returns formatted string when "type" is "match_any"', () => { const result = evaluateValues({ item: matchAnyEntryWithIncludedAndTwoValues, - language: 'kuery', - exclude, - }); - expect(result).toEqual('host.name:(suricata or auditd)'); - }); - }); - - describe('lucene', () => { - describe('kuery', () => { - test('it returns formatted wildcard string when "type" is "exists"', () => { - const result = evaluateValues({ - item: existsEntryWithIncluded, - language: 'lucene', - exclude, - }); - expect(result).toEqual('_exists_host.name'); - }); - test('it returns formatted string when "type" is "match"', () => { - const result = evaluateValues({ - item: matchEntryWithIncluded, - language: 'lucene', - exclude, - }); - expect(result).toEqual('host.name:suricata'); - }); - test('it returns formatted string when "type" is "match_any"', () => { - const result = evaluateValues({ - item: matchAnyEntryWithIncludedAndTwoValues, - language: 'lucene', - exclude, - }); - expect(result).toEqual('host.name:(suricata OR auditd)'); + language: 'lucene', }); + expect(result).toEqual('host.name:("suricata" OR "auditd")'); }); }); }); }); describe('formatQuery', () => { - describe('when query is empty string', () => { - test('it returns query if "exceptions" is empty array', () => { - const formattedQuery = formatQuery({ exceptions: [], query: '', language: 'kuery' }); - expect(formattedQuery).toEqual(''); + describe('exclude is true', () => { + describe('when query is empty string', () => { + test('it returns empty string if "exceptions" is empty array', () => { + const formattedQuery = formatQuery({ exceptions: [], language: 'kuery', exclude: true }); + expect(formattedQuery).toEqual(''); + }); + + test('it returns expected query string when single exception in array', () => { + const formattedQuery = formatQuery({ + exceptions: ['b:("value-1" or "value-2") and not c:*'], + language: 'kuery', + exclude: true, + }); + expect(formattedQuery).toEqual('not ((b:("value-1" or "value-2") and not c:*))'); + }); }); - test('it returns expected query string when single exception in array', () => { + + test('it returns expected query string when multiple exceptions in array', () => { const formattedQuery = formatQuery({ - exceptions: ['b:(value-1 or value-2) and not c:*'], - query: '', + exceptions: ['b:("value-1" or "value-2") and not c:*', 'not d:*'], language: 'kuery', + exclude: true, }); - expect(formattedQuery).toEqual('(b:(value-1 or value-2) and not c:*)'); + expect(formattedQuery).toEqual( + 'not ((b:("value-1" or "value-2") and not c:*) or (not d:*))' + ); }); }); - test('it returns query if "exceptions" is empty array', () => { - const formattedQuery = formatQuery({ exceptions: [], query: 'a:*', language: 'kuery' }); - expect(formattedQuery).toEqual('a:*'); - }); + describe('exclude is false', () => { + describe('when query is empty string', () => { + test('it returns empty string if "exceptions" is empty array', () => { + const formattedQuery = formatQuery({ exceptions: [], language: 'kuery', exclude: false }); + expect(formattedQuery).toEqual(''); + }); - test('it returns expected query string when single exception in array', () => { - const formattedQuery = formatQuery({ - exceptions: ['b:(value-1 or value-2) and not c:*'], - query: 'a:*', - language: 'kuery', + test('it returns expected query string when single exception in array', () => { + const formattedQuery = formatQuery({ + exceptions: ['b:("value-1" or "value-2") and not c:*'], + language: 'kuery', + exclude: false, + }); + expect(formattedQuery).toEqual('(b:("value-1" or "value-2") and not c:*)'); + }); }); - expect(formattedQuery).toEqual('(a:* and b:(value-1 or value-2) and not c:*)'); - }); - test('it returns expected query string when multiple exceptions in array', () => { - const formattedQuery = formatQuery({ - exceptions: ['b:(value-1 or value-2) and not c:*', 'not d:*'], - query: 'a:*', - language: 'kuery', + test('it returns expected query string when multiple exceptions in array', () => { + const formattedQuery = formatQuery({ + exceptions: ['b:("value-1" or "value-2") and not c:*', 'not d:*'], + language: 'kuery', + exclude: false, + }); + expect(formattedQuery).toEqual('(b:("value-1" or "value-2") and not c:*) or (not d:*)'); }); - expect(formattedQuery).toEqual( - '(a:* and b:(value-1 or value-2) and not c:*) or (a:* and not d:*)' - ); }); }); @@ -712,81 +436,69 @@ describe('build_exceptions_query', () => { test('it returns empty string if empty lists array passed in', () => { const query = buildExceptionItemEntries({ language: 'kuery', - lists: [], - exclude, + entries: [], }); expect(query).toEqual(''); }); - test('it returns expected query when more than one item in list', () => { - // Equal to query && !(b && !c) -> (query AND NOT b) OR (query AND c) - // https://www.dcode.fr/boolean-expressions-calculator + test('it returns expected query when more than one item in exception item', () => { const payload: EntriesArray = [ makeMatchAnyEntry({ field: 'b' }), makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-3' }), ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists: payload, - exclude, + entries: payload, }); - const expectedQuery = 'not b:(value-1 or value-2) and c:value-3'; + const expectedQuery = 'b:("value-1" or "value-2") and not c:"value-3"'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list item includes nested value', () => { - // Equal to query && !(b || !c) -> (query AND NOT b AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + test('it returns expected query when exception item includes nested value', () => { + const entries: EntriesArray = [ makeMatchAnyEntry({ field: 'b' }), { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), + makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), ], }, ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'not b:(value-1 or value-2) and parent:{ nestedField:value-3 }'; + const expectedQuery = 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" }'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes multiple items and nested "and" values', () => { - // Equal to query && !((b || !c) && d) -> (query AND NOT b AND c) OR (query AND NOT d) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + test('it returns expected query when exception item includes multiple items and nested "and" values', () => { + const entries: EntriesArray = [ makeMatchAnyEntry({ field: 'b' }), { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), + makeMatchEntry({ field: 'nestedField', operator: 'included', value: 'value-3' }), ], }, makeExistsEntry({ field: 'd' }), ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); const expectedQuery = - 'not b:(value-1 or value-2) and parent:{ nestedField:value-3 } and not d:*'; + 'b:("value-1" or "value-2") and parent:{ nestedField:"value-3" } and d:*'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when language is "lucene"', () => { - // Equal to query && !((b || !c) && !d) -> (query AND NOT b AND c) OR (query AND d) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + const entries: EntriesArray = [ makeMatchAnyEntry({ field: 'b' }), { field: 'parent', @@ -799,170 +511,56 @@ describe('build_exceptions_query', () => { ]; const query = buildExceptionItemEntries({ language: 'lucene', - lists, - exclude, + entries, }); const expectedQuery = - 'NOT b:(value-1 OR value-2) AND parent:{ nestedField:value-3 } AND _exists_e'; + 'b:("value-1" OR "value-2") AND parent:{ nestedField:"value-3" } AND NOT _exists_e'; expect(query).toEqual(expectedQuery); }); - describe('when "exclude" is false', () => { - beforeEach(() => { - exclude = false; - }); - - test('it returns empty string if empty lists array passed in', () => { - const query = buildExceptionItemEntries({ - language: 'kuery', - lists: [], - exclude, - }); - - expect(query).toEqual(''); - }); - test('it returns expected query when more than one item in list', () => { - // Equal to query && !(b && !c) -> (query AND NOT b) OR (query AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const payload: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-3' }), - ]; - const query = buildExceptionItemEntries({ - language: 'kuery', - lists: payload, - exclude, - }); - const expectedQuery = 'b:(value-1 or value-2) and not c:value-3'; - - expect(query).toEqual(expectedQuery); - }); - - test('it returns expected query when list item includes nested value', () => { - // Equal to query && !(b || !c) -> (query AND NOT b AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - { - field: 'parent', - type: 'nested', - entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), - ], - }, - ]; - const query = buildExceptionItemEntries({ - language: 'kuery', - lists, - exclude, - }); - const expectedQuery = 'b:(value-1 or value-2) and parent:{ nestedField:value-3 }'; - - expect(query).toEqual(expectedQuery); - }); - - test('it returns expected query when list includes multiple items and nested "and" values', () => { - // Equal to query && !((b || !c) && d) -> (query AND NOT b AND c) OR (query AND NOT d) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - { - field: 'parent', - type: 'nested', - entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), - ], - }, - makeExistsEntry({ field: 'd' }), - ]; - const query = buildExceptionItemEntries({ - language: 'kuery', - lists, - exclude, - }); - const expectedQuery = 'b:(value-1 or value-2) and parent:{ nestedField:value-3 } and d:*'; - expect(query).toEqual(expectedQuery); - }); - - test('it returns expected query when language is "lucene"', () => { - // Equal to query && !((b || !c) && !d) -> (query AND NOT b AND c) OR (query AND d) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ - makeMatchAnyEntry({ field: 'b' }), - { - field: 'parent', - type: 'nested', - entries: [ - makeMatchEntry({ field: 'nestedField', operator: 'excluded', value: 'value-3' }), - ], - }, - makeExistsEntry({ field: 'e', operator: 'excluded' }), - ]; - const query = buildExceptionItemEntries({ - language: 'lucene', - lists, - exclude, - }); - const expectedQuery = - 'b:(value-1 OR value-2) AND parent:{ nestedField:value-3 } AND NOT _exists_e'; - expect(query).toEqual(expectedQuery); - }); - }); - describe('exists', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - // Equal to query && !(b) -> (query AND NOT b) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [makeExistsEntry({ field: 'b' })]; + const entries: EntriesArray = [makeExistsEntry({ field: 'b' })]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'not b:*'; + const expectedQuery = 'b:*'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - // Equal to query && !(!b) -> (query AND b) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [makeExistsEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [makeExistsEntry({ field: 'b', operator: 'excluded' })]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'b:*'; + const expectedQuery = 'not b:*'; expect(query).toEqual(expectedQuery); }); - test('it returns expected query when list includes list item with "and" values', () => { - // Equal to query && !(!b || !c) -> (query AND b AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + test('it returns expected query when exception item includes entry item with "and" values', () => { + const entries: EntriesArray = [ makeExistsEntry({ field: 'b', operator: 'excluded' }), { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'excluded', value: 'value-1' })], + entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'value-1' })], }, ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'b:* and parent:{ c:value-1 }'; + const expectedQuery = 'not b:* and parent:{ c:"value-1" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes multiple items', () => { - // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + const entries: EntriesArray = [ makeExistsEntry({ field: 'b' }), { field: 'parent', @@ -976,10 +574,9 @@ describe('build_exceptions_query', () => { ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'not b:* and parent:{ c:value-1 and d:value-2 } and not e:*'; + const expectedQuery = 'b:* and parent:{ c:"value-1" and d:"value-2" } and e:*'; expect(query).toEqual(expectedQuery); }); @@ -987,60 +584,49 @@ describe('build_exceptions_query', () => { describe('match', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - // Equal to query && !(b) -> (query AND NOT b) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [makeMatchEntry({ field: 'b', value: 'value' })]; + const entries: EntriesArray = [makeMatchEntry({ field: 'b', value: 'value' })]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'not b:value'; + const expectedQuery = 'b:"value"'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - // Equal to query && !(!b) -> (query AND b) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + const entries: EntriesArray = [ makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'b:value'; + const expectedQuery = 'not b:"value"'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes list item with "and" values', () => { - // Equal to query && !(!b || !c) -> (query AND b AND c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + const entries: EntriesArray = [ makeMatchEntry({ field: 'b', operator: 'excluded', value: 'value' }), { field: 'parent', type: 'nested', - entries: [makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' })], + entries: [makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' })], }, ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'b:value and parent:{ c:valueC }'; + const expectedQuery = 'not b:"value" and parent:{ c:"valueC" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes multiple items', () => { - // Equal to query && !((b || !c || d) && e) -> (query AND NOT b AND c AND NOT d) OR (query AND NOT e) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + const entries: EntriesArray = [ makeMatchEntry({ field: 'b', value: 'value' }), { field: 'parent', @@ -1054,10 +640,9 @@ describe('build_exceptions_query', () => { ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'not b:value and parent:{ c:valueC and d:valueD } and not e:valueE'; + const expectedQuery = 'b:"value" and parent:{ c:"valueC" and d:"valueD" } and e:"valueE"'; expect(query).toEqual(expectedQuery); }); @@ -1065,37 +650,29 @@ describe('build_exceptions_query', () => { describe('match_any', () => { test('it returns expected query when list includes single list item with operator of "included"', () => { - // Equal to query && !(b) -> (query AND NOT b) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [makeMatchAnyEntry({ field: 'b' })]; + const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b' })]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'not b:(value-1 or value-2)'; + const expectedQuery = 'b:("value-1" or "value-2")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes single list item with operator of "excluded"', () => { - // Equal to query && !(!b) -> (query AND b) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [makeMatchAnyEntry({ field: 'b', operator: 'excluded' })]; + const entries: EntriesArray = [makeMatchAnyEntry({ field: 'b', operator: 'excluded' })]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'b:(value-1 or value-2)'; + const expectedQuery = 'not b:("value-1" or "value-2")'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes list item with nested values', () => { - // Equal to query && !(!b || c) -> (query AND b AND NOT c) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + const entries: EntriesArray = [ makeMatchAnyEntry({ field: 'b', operator: 'excluded' }), { field: 'parent', @@ -1105,27 +682,23 @@ describe('build_exceptions_query', () => { ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'b:(value-1 or value-2) and parent:{ c:valueC }'; + const expectedQuery = 'not b:("value-1" or "value-2") and parent:{ c:"valueC" }'; expect(query).toEqual(expectedQuery); }); test('it returns expected query when list includes multiple items', () => { - // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) - // https://www.dcode.fr/boolean-expressions-calculator - const lists: EntriesArray = [ + const entries: EntriesArray = [ makeMatchAnyEntry({ field: 'b' }), makeMatchAnyEntry({ field: 'c' }), ]; const query = buildExceptionItemEntries({ language: 'kuery', - lists, - exclude, + entries, }); - const expectedQuery = 'not b:(value-1 or value-2) and not c:(value-1 or value-2)'; + const expectedQuery = 'b:("value-1" or "value-2") and c:("value-1" or "value-2")'; expect(query).toEqual(expectedQuery); }); @@ -1133,16 +706,19 @@ describe('build_exceptions_query', () => { }); describe('buildQueryExceptions', () => { - test('it returns original query if lists is empty array', () => { - const query = buildQueryExceptions({ query: 'host.name: *', language: 'kuery', lists: [] }); - const expectedQuery = 'host.name: *'; + test('it returns empty array if lists is empty array', () => { + const query = buildQueryExceptions({ language: 'kuery', lists: [] }); - expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + expect(query).toEqual([]); + }); + + test('it returns empty array if lists is undefined', () => { + const query = buildQueryExceptions({ language: 'kuery', lists: undefined }); + + expect(query).toEqual([]); }); test('it returns expected query when lists exist and language is "kuery"', () => { - // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) - // https://www.dcode.fr/boolean-expressions-calculator const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ @@ -1151,47 +727,33 @@ describe('build_exceptions_query', () => { field: 'parent', type: 'nested', entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), + makeMatchEntry({ field: 'c', operator: 'included', value: 'valueC' }), + makeMatchEntry({ field: 'd', operator: 'included', value: 'valueD' }), ], }, - makeMatchAnyEntry({ field: 'e' }), + makeMatchAnyEntry({ field: 'e', operator: 'excluded' }), ]; const query = buildQueryExceptions({ - query: 'a:*', language: 'kuery', lists: [payload, payload2], }); const expectedQuery = - '(a:* and some.parentField:{ nested.field:some value } and not some.not.nested.field:some value) or (a:* and not b:(value-1 or value-2) and parent:{ c:valueC and d:valueD } and not e:(value-1 or value-2))'; + 'not ((some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value") or (b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and not e:("value-1" or "value-2")))'; expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); }); test('it returns expected query when lists exist and language is "lucene"', () => { - // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) - // https://www.dcode.fr/boolean-expressions-calculator const payload = getExceptionListItemSchemaMock(); + payload.entries = [makeMatchAnyEntry({ field: 'a' }), makeMatchAnyEntry({ field: 'b' })]; const payload2 = getExceptionListItemSchemaMock(); - payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), - { - field: 'parent', - type: 'nested', - entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), - ], - }, - makeMatchAnyEntry({ field: 'e' }), - ]; + payload2.entries = [makeMatchAnyEntry({ field: 'c' }), makeMatchAnyEntry({ field: 'd' })]; const query = buildQueryExceptions({ - query: 'a:*', language: 'lucene', lists: [payload, payload2], }); const expectedQuery = - '(a:* AND some.parentField:{ nested.field:some value } AND NOT some.not.nested.field:some value) OR (a:* AND NOT b:(value-1 OR value-2) AND parent:{ c:valueC AND d:valueD } AND NOT e:(value-1 OR value-2))'; + 'NOT ((a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")) OR (c:("value-1" OR "value-2") AND d:("value-1" OR "value-2")))'; expect(query).toEqual([{ query: expectedQuery, language: 'lucene' }]); }); @@ -1201,21 +763,23 @@ describe('build_exceptions_query', () => { exclude = false; }); - test('it returns original query if lists is empty array', () => { + test('it returns empty array if lists is empty array', () => { const query = buildQueryExceptions({ - query: 'host.name: *', language: 'kuery', lists: [], exclude, }); - const expectedQuery = 'host.name: *'; - expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); + expect(query).toEqual([]); + }); + + test('it returns empty array if lists is undefined', () => { + const query = buildQueryExceptions({ language: 'kuery', lists: undefined, exclude }); + + expect(query).toEqual([]); }); test('it returns expected query when lists exist and language is "kuery"', () => { - // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) - // https://www.dcode.fr/boolean-expressions-calculator const payload = getExceptionListItemSchemaMock(); const payload2 = getExceptionListItemSchemaMock(); payload2.entries = [ @@ -1231,42 +795,28 @@ describe('build_exceptions_query', () => { makeMatchAnyEntry({ field: 'e' }), ]; const query = buildQueryExceptions({ - query: 'a:*', language: 'kuery', lists: [payload, payload2], exclude, }); const expectedQuery = - '(a:* and some.parentField:{ nested.field:some value } and some.not.nested.field:some value) or (a:* and b:(value-1 or value-2) and parent:{ c:valueC and d:valueD } and e:(value-1 or value-2))'; + '(some.parentField:{ nested.field:"some value" } and some.not.nested.field:"some value") or (b:("value-1" or "value-2") and parent:{ c:"valueC" and d:"valueD" } and e:("value-1" or "value-2"))'; expect(query).toEqual([{ query: expectedQuery, language: 'kuery' }]); }); test('it returns expected query when lists exist and language is "lucene"', () => { - // Equal to query && !((b || !c || d) && e) -> ((query AND NOT b AND c AND NOT d) OR (query AND NOT e) - // https://www.dcode.fr/boolean-expressions-calculator const payload = getExceptionListItemSchemaMock(); + payload.entries = [makeMatchAnyEntry({ field: 'a' }), makeMatchAnyEntry({ field: 'b' })]; const payload2 = getExceptionListItemSchemaMock(); - payload2.entries = [ - makeMatchAnyEntry({ field: 'b' }), - { - field: 'parent', - type: 'nested', - entries: [ - makeMatchEntry({ field: 'c', operator: 'excluded', value: 'valueC' }), - makeMatchEntry({ field: 'd', operator: 'excluded', value: 'valueD' }), - ], - }, - makeMatchAnyEntry({ field: 'e' }), - ]; + payload2.entries = [makeMatchAnyEntry({ field: 'c' }), makeMatchAnyEntry({ field: 'd' })]; const query = buildQueryExceptions({ - query: 'a:*', language: 'lucene', lists: [payload, payload2], exclude, }); const expectedQuery = - '(a:* AND some.parentField:{ nested.field:some value } AND some.not.nested.field:some value) OR (a:* AND b:(value-1 OR value-2) AND parent:{ c:valueC AND d:valueD } AND e:(value-1 OR value-2))'; + '(a:("value-1" OR "value-2") AND b:("value-1" OR "value-2")) OR (c:("value-1" OR "value-2") AND d:("value-1" OR "value-2"))'; expect(query).toEqual([{ query: expectedQuery, language: 'lucene' }]); }); diff --git a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts index a70e6a6638589..fc4fbae02b8fb 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/build_exceptions_query.ts @@ -19,7 +19,8 @@ import { ExceptionListItemSchema, CreateExceptionListItemSchema, } from '../shared_imports'; -import { Language, Query } from './schemas/common/schemas'; +import { Language } from './schemas/common/schemas'; +import { hasLargeValueList } from './utils'; type Operators = 'and' | 'or' | 'not'; type LuceneOperators = 'AND' | 'OR' | 'NOT'; @@ -46,18 +47,16 @@ export const getLanguageBooleanOperator = ({ export const operatorBuilder = ({ operator, language, - exclude, }: { operator: Operator; language: Language; - exclude: boolean; }): string => { const not = getLanguageBooleanOperator({ language, value: 'not', }); - if ((exclude && operator === 'included') || (!exclude && operator === 'excluded')) { + if (operator === 'excluded') { return `${not} `; } else { return ''; @@ -67,14 +66,12 @@ export const operatorBuilder = ({ export const buildExists = ({ item, language, - exclude, }: { item: EntryExists; language: Language; - exclude: boolean; }): string => { const { operator, field } = item; - const exceptionOperator = operatorBuilder({ operator, language, exclude }); + const exceptionOperator = operatorBuilder({ operator, language }); switch (language) { case 'kuery': @@ -89,26 +86,22 @@ export const buildExists = ({ export const buildMatch = ({ item, language, - exclude, }: { item: EntryMatch; language: Language; - exclude: boolean; }): string => { const { value, operator, field } = item; - const exceptionOperator = operatorBuilder({ operator, language, exclude }); + const exceptionOperator = operatorBuilder({ operator, language }); - return `${exceptionOperator}${field}:${value}`; + return `${exceptionOperator}${field}:"${value}"`; }; export const buildMatchAny = ({ item, language, - exclude, }: { item: EntryMatchAny; language: Language; - exclude: boolean; }): string => { const { value, operator, field } = item; @@ -117,8 +110,8 @@ export const buildMatchAny = ({ return ''; default: const or = getLanguageBooleanOperator({ language, value: 'or' }); - const exceptionOperator = operatorBuilder({ operator, language, exclude }); - const matchAnyValues = value.map((v) => v); + const exceptionOperator = operatorBuilder({ operator, language }); + const matchAnyValues = value.map((v) => `"${v}"`); return `${exceptionOperator}${field}:(${matchAnyValues.join(` ${or} `)})`; } @@ -133,7 +126,7 @@ export const buildNested = ({ }): string => { const { field, entries } = item; const and = getLanguageBooleanOperator({ language, value: 'and' }); - const values = entries.map((entry) => `${entry.field}:${entry.value}`); + const values = entries.map((entry) => `${entry.field}:"${entry.value}"`); return `${field}:{ ${values.join(` ${and} `)} }`; }; @@ -141,18 +134,16 @@ export const buildNested = ({ export const evaluateValues = ({ item, language, - exclude, }: { item: Entry | EntryNested; language: Language; - exclude: boolean; }): string => { if (entriesExists.is(item)) { - return buildExists({ item, language, exclude }); + return buildExists({ item, language }); } else if (entriesMatch.is(item)) { - return buildMatch({ item, language, exclude }); + return buildMatch({ item, language }); } else if (entriesMatchAny.is(item)) { - return buildMatchAny({ item, language, exclude }); + return buildMatchAny({ item, language }); } else if (entriesNested.is(item)) { return buildNested({ item, language }); } else { @@ -162,78 +153,79 @@ export const evaluateValues = ({ export const formatQuery = ({ exceptions, - query, language, + exclude, }: { exceptions: string[]; - query: string; language: Language; + exclude: boolean; }): string => { - if (exceptions.length > 0) { - const or = getLanguageBooleanOperator({ language, value: 'or' }); - const and = getLanguageBooleanOperator({ language, value: 'and' }); - const formattedExceptions = exceptions.map((exception) => { - if (query === '') { - return `(${exception})`; - } else { - return `(${query} ${and} ${exception})`; - } - }); - - return formattedExceptions.join(` ${or} `); - } else { - return query; + if (exceptions == null || (exceptions != null && exceptions.length === 0)) { + return ''; } + + const or = getLanguageBooleanOperator({ language, value: 'or' }); + const not = getLanguageBooleanOperator({ language, value: 'not' }); + const formattedExceptionItems = exceptions.map((exceptionItem, index) => { + if (index === 0) { + return `(${exceptionItem})`; + } + + return `${or} (${exceptionItem})`; + }); + + const exceptionItemsQuery = formattedExceptionItems.join(' '); + return exclude ? `${not} (${exceptionItemsQuery})` : exceptionItemsQuery; }; export const buildExceptionItemEntries = ({ - lists, + entries, language, - exclude, }: { - lists: EntriesArray; + entries: EntriesArray; language: Language; - exclude: boolean; }): string => { const and = getLanguageBooleanOperator({ language, value: 'and' }); - const exceptionItem = lists - .filter(({ type }) => type !== 'list') - .reduce((accum, listItem) => { - const exceptionSegment = evaluateValues({ item: listItem, language, exclude }); - return [...accum, exceptionSegment]; - }, []); - - return exceptionItem.join(` ${and} `); + const exceptionItemEntries = entries.reduce((accum, listItem) => { + const exceptionSegment = evaluateValues({ item: listItem, language }); + return [...accum, exceptionSegment]; + }, []); + + return exceptionItemEntries.join(` ${and} `); }; export const buildQueryExceptions = ({ - query, language, lists, exclude = true, }: { - query: Query; language: Language; lists: Array | undefined; exclude?: boolean; }): DataQuery[] => { - if (lists != null) { - const exceptions = lists.reduce((acc, exceptionItem) => { - return [ - ...acc, - ...(exceptionItem.entries !== undefined - ? [buildExceptionItemEntries({ lists: exceptionItem.entries, language, exclude })] - : []), - ]; - }, []); - const formattedQuery = formatQuery({ exceptions, language, query }); + if (lists == null || (lists != null && lists.length === 0)) { + return []; + } + + const exceptionItems = lists.reduce((acc, exceptionItem) => { + const { entries } = exceptionItem; + + if (entries != null && entries.length > 0 && !hasLargeValueList(entries)) { + return [...acc, buildExceptionItemEntries({ entries, language })]; + } else { + return acc; + } + }, []); + + if (exceptionItems.length === 0) { + return []; + } else { + const formattedQuery = formatQuery({ exceptions: exceptionItems, language, exclude }); return [ { query: formattedQuery, language, }, ]; - } else { - return [{ query, language }]; } }; diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts index c19ef45605f83..a8eb4e7bbb15b 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.test.ts @@ -362,62 +362,45 @@ describe('get_filter', () => { expect(esQuery).toEqual({ bool: { filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, { bool: { - filter: [ - { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'host.name': 'linux', - }, - }, - ], - }, - }, - { - bool: { - filter: [ - { - nested: { - path: 'some.parentField', - query: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'some.parentField.nested.field': 'some value', - }, + must_not: { + bool: { + filter: [ + { + nested: { + path: 'some.parentField', + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'some.parentField.nested.field': 'some value', }, - ], - }, + }, + ], }, - score_mode: 'none', }, + score_mode: 'none', }, - { - bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'some.not.nested.field': 'some value', - }, - }, - ], + }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'some.not.nested.field': 'some value', }, }, - }, + ], }, - ], - }, + }, + ], }, - ], + }, }, }, ], @@ -469,52 +452,35 @@ describe('get_filter', () => { expect(esQuery).toEqual({ bool: { filter: [ + { bool: { minimum_should_match: 1, should: [{ match: { 'host.name': 'linux' } }] } }, { bool: { filter: [ { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'host.name': 'linux', - }, + nested: { + path: 'some.parentField', + query: { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'some.parentField.nested.field': 'some value', + }, + }, + ], }, - ], + }, + score_mode: 'none', }, }, { bool: { - filter: [ - { - nested: { - path: 'some.parentField', - query: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'some.parentField.nested.field': 'some value', - }, - }, - ], - }, - }, - score_mode: 'none', - }, - }, + minimum_should_match: 1, + should: [ { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'some.not.nested.field': 'some value', - }, - }, - ], + match_phrase: { + 'some.not.nested.field': 'some value', }, }, ], diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 6584373b806d8..a41589b5d0231 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -31,12 +31,16 @@ export const getQueryFilter = ( title: index.join(), }; - const queries: DataQuery[] = buildQueryExceptions({ - query, - language, - lists, - exclude: excludeExceptions, - }); + const initialQuery = [{ query, language }]; + /* + * Pinning exceptions to 'kuery' because lucene + * does not support nested queries, while our exceptions + * UI does, since we can pass both lucene and kql into + * buildEsQuery, this allows us to offer nested queries + * regardless + */ + const exceptions = buildQueryExceptions({ language: 'kuery', lists, exclude: excludeExceptions }); + const queries: DataQuery[] = [...initialQuery, ...exceptions]; const config = { allowLeadingWildcards: true, diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts new file mode 100644 index 0000000000000..99680ffe41d44 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { hasLargeValueList, hasNestedEntry } from './utils'; +import { EntriesArray } from '../shared_imports'; + +describe('#hasLargeValueList', () => { + test('it returns false if empty array', () => { + const hasLists = hasLargeValueList([]); + + expect(hasLists).toBeFalsy(); + }); + + test('it returns true if item of type EntryList exists', () => { + const entries: EntriesArray = [ + { + field: 'actingProcess.file.signer', + type: 'list', + operator: 'included', + list: { id: 'some id', type: 'ip' }, + }, + { + field: 'file.signature.signer', + type: 'match', + operator: 'excluded', + value: 'Global Signer', + }, + ]; + const hasLists = hasLargeValueList(entries); + + expect(hasLists).toBeTruthy(); + }); + + test('it returns false if item of type EntryList does not exist', () => { + const entries: EntriesArray = [ + { + field: 'actingProcess.file.signer', + type: 'match', + operator: 'included', + value: 'Elastic, N.V.', + }, + { + field: 'file.signature.signer', + type: 'match', + operator: 'excluded', + value: 'Global Signer', + }, + ]; + const hasLists = hasLargeValueList(entries); + + expect(hasLists).toBeFalsy(); + }); +}); + +describe('#hasNestedEntry', () => { + test('it returns false if empty array', () => { + const hasLists = hasNestedEntry([]); + + expect(hasLists).toBeFalsy(); + }); + + test('it returns true if item of type EntryNested exists', () => { + const entries: EntriesArray = [ + { + field: 'actingProcess.file.signer', + type: 'nested', + entries: [ + { field: 'some field', type: 'match', operator: 'included', value: 'some value' }, + ], + }, + { + field: 'file.signature.signer', + type: 'match', + operator: 'excluded', + value: 'Global Signer', + }, + ]; + const hasLists = hasNestedEntry(entries); + + expect(hasLists).toBeTruthy(); + }); + + test('it returns false if item of type EntryNested does not exist', () => { + const entries: EntriesArray = [ + { + field: 'actingProcess.file.signer', + type: 'match', + operator: 'included', + value: 'Elastic, N.V.', + }, + { + field: 'file.signature.signer', + type: 'match', + operator: 'excluded', + value: 'Global Signer', + }, + ]; + const hasLists = hasNestedEntry(entries); + + expect(hasLists).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/utils.ts b/x-pack/plugins/security_solution/common/detection_engine/utils.ts new file mode 100644 index 0000000000000..fa1812235f897 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/utils.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EntriesArray } from '../shared_imports'; + +export const hasLargeValueList = (entries: EntriesArray): boolean => { + const found = entries.filter(({ type }) => type === 'list'); + return found.length > 0; +}; + +export const hasNestedEntry = (entries: EntriesArray): boolean => { + const found = entries.filter(({ type }) => type === 'nested'); + return found.length > 0; +}; diff --git a/x-pack/plugins/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index a607906e1b92a..7fb94cea7b612 100644 --- a/x-pack/plugins/security_solution/common/shared_imports.ts +++ b/x-pack/plugins/security_solution/common/shared_imports.ts @@ -12,6 +12,7 @@ export { CreateComments, ExceptionListSchema, ExceptionListItemSchema, + CreateExceptionListSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, Entry, @@ -40,4 +41,5 @@ export { namespaceType, ExceptionListType, Type, + ENDPOINT_LIST_ID, } from '../../lists/common'; diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 8f03945df437c..41b9252c67b8a 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -32,7 +32,7 @@ Main.displayName = 'Main'; const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) /** the global Kibana navigation at the top of every page */ -const globalHeaderHeightPx = 48; +export const globalHeaderHeightPx = 48; const calculateFlyoutHeight = ({ globalHeaderSize, diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.test.tsx index 9db8adbf9346f..654a5f5c4a599 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/columns.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; +import '../../../common/mock/match_media'; import { ExternalServiceColumn } from './columns'; import { useGetCasesMockState } from '../../containers/mock'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx index d8acda8ec4f33..23cabd6778cc0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases/index.test.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { mount } from 'enzyme'; import moment from 'moment-timezone'; + +import '../../../common/mock/match_media'; import { AllCases } from '.'; import { TestProviders } from '../../../common/mock'; import { useGetCasesMockState } from '../../containers/mock'; diff --git a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx index f4fd7cc67224f..b93de014f5c18 100644 --- a/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/all_cases_modal/index.test.tsx @@ -5,6 +5,7 @@ */ import { mount } from 'enzyme'; import React from 'react'; +import '../../../common/mock/match_media'; import { AllCasesModal } from '.'; import { TestProviders } from '../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 2832a28fbb7cd..b93df325b5a8b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; +import '../../../common/mock/match_media'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; import { CaseComponent, CaseProps, CaseView } from '.'; import { basicCase, basicCaseClosed, caseUserActions } from '../../containers/mock'; diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx index 8d14b2357f450..6fb693e47560d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/button.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { EuiText } from '@elastic/eui'; +import '../../../common/mock/match_media'; import { ConfigureCaseButton, ConfigureCaseButtonProps } from './button'; import { TestProviders } from '../../../common/mock'; import { searchURL } from './__mock__'; diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx index d17a2bd215910..eb80eaff578f5 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx @@ -6,6 +6,8 @@ /* eslint-disable react/display-name */ import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; + +import '../../../common/mock/match_media'; import { usePushToService, ReturnUsePushToService, UsePushToService } from '.'; import { TestProviders } from '../../../common/mock'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index ef4e1ff05118b..313c71375111c 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { camelCase, isArray, isObject, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { camelCase, isArray, isObject } from 'lodash'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index 841a1ef09ede6..e30560f6c8147 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx @@ -58,6 +58,7 @@ const defaultAlertsFilters: Filter[] = [ interface Props { timelineId: TimelineIdLiteral; endDate: string; + eventsViewerBodyHeight?: number; startDate: string; pageFilters?: Filter[]; } @@ -65,6 +66,7 @@ interface Props { const AlertsTableComponent: React.FC = ({ timelineId, endDate, + eventsViewerBodyHeight, startDate, pageFilters = [], }) => { @@ -91,6 +93,7 @@ const AlertsTableComponent: React.FC = ({ pageFilters={alertsFilter} defaultModel={alertsDefaultModel} end={endDate} + height={eventsViewerBodyHeight} id={timelineId} start={startDate} /> diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index a31cb4f2a8bfd..832b14f00159a 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -5,8 +5,18 @@ */ import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; +import { useWindowSize } from 'react-use'; + +import { globalHeaderHeightPx } from '../../../app/home'; +import { DEFAULT_NUMBER_FORMAT, FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; +import { useFullScreen } from '../../containers/use_full_screen'; +import { EVENTS_VIEWER_HEADER_HEIGHT } from '../events_viewer/events_viewer'; +import { + getEventsViewerBodyHeight, + MIN_EVENTS_VIEWER_BODY_HEIGHT, +} from '../../../timelines/components/timeline/body/helpers'; +import { footerHeight } from '../../../timelines/components/timeline/footer'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; import { AlertsComponentsProps } from './types'; import { AlertsTable } from './alerts_table'; import * as i18n from './translations'; @@ -35,6 +45,8 @@ export const AlertsView = ({ // eslint-disable-next-line react-hooks/exhaustive-deps [] ); + const { height: windowHeight } = useWindowSize(); + const { globalFullScreen } = useFullScreen(); const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( () => ({ ...histogramConfigs, @@ -52,19 +64,32 @@ export const AlertsView = ({ return ( <> - + {!globalFullScreen && ( + + )} diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts index c2e8e56084452..cfe23b9391ec0 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../common/mock/match_media'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; import { diff --git a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx index 49c421c5680ba..8617388f4ffb5 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/barchart.test.tsx @@ -12,6 +12,7 @@ import { ThemeProvider } from 'styled-components'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; import { TestProviders } from '../../mock'; +import '../../mock/match_media'; import { BarChartBaseComponent, BarChartComponent } from './barchart'; import { ChartSeriesData } from './common'; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx index a11fdda3d1b3a..8fd2fa1fdef12 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend.test.tsx @@ -9,6 +9,7 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../mock/match_media'; import { TestProviders } from '../../mock'; import { MIN_LEGEND_HEIGHT, DraggableLegend } from './draggable_legend'; diff --git a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx index 8ff75c8ca0780..9f6e614c3c285 100644 --- a/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/charts/draggable_legend_item.test.tsx @@ -9,6 +9,7 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../mock/match_media'; import { TestProviders } from '../../mock'; import { DraggableLegendItem, LegendItem } from './draggable_legend_item'; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index d1b3b671307d1..da68280ed760c 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { DraggableStateSnapshot, DraggingStyle } from 'react-beautiful-dnd'; +import '../../mock/match_media'; import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers'; diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 432e369cdd0f6..3f06a8168b5ce 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { useWithSource } from '../../containers/source'; import { mockBrowserFields } from '../../containers/source/mock'; +import '../../mock/match_media'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx index 3d80a2605418e..ff1679875865c 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../mock'; +import '../../mock/match_media'; import { getEmptyString } from '../empty_value'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 9ca9cd6cce389..ebaf60e7078f0 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -4,6 +4,33 @@ exports[`EventDetails rendering should match snapshot 1`] = `
+ + + + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={false} + panelPaddingSize="m" + repositionOnScroll={true} + /> + { data={mockDetailItemData} id={mockDetailItemDataId} view="table-view" + onEventToggled={jest.fn()} onUpdateColumns={jest.fn()} onViewSelected={jest.fn()} timelineId="test" @@ -50,6 +52,7 @@ describe('EventDetails', () => { data={mockDetailItemData} id={mockDetailItemDataId} view="table-view" + onEventToggled={jest.fn()} onUpdateColumns={jest.fn()} onViewSelected={jest.fn()} timelineId="test" @@ -76,6 +79,7 @@ describe('EventDetails', () => { data={mockDetailItemData} id={mockDetailItemDataId} view="table-view" + onEventToggled={jest.fn()} onUpdateColumns={jest.fn()} onViewSelected={jest.fn()} timelineId="test" @@ -88,5 +92,31 @@ describe('EventDetails', () => { wrapper.find('[data-test-subj="eventDetails"]').find('.euiTab-isSelected').first().text() ).toEqual('Table'); }); + + test('it invokes `onEventToggled` when the collapse button is clicked', () => { + const onEventToggled = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper.find('[data-test-subj="collapse"]').first().simulate('click'); + wrapper.update(); + + expect(onEventToggled).toHaveBeenCalled(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index c28757a90c702..53ec14380d5bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -4,8 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; -import React from 'react'; +import { noop } from 'lodash/fp'; +import { + EuiButtonIcon, + EuiPopover, + EuiTabbedContent, + EuiTabbedContentTab, + EuiToolTip, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; @@ -15,15 +22,34 @@ import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; +import { COLLAPSE, COLLAPSE_EVENT } from '../../../timelines/components/timeline/body/translations'; export type View = 'table-view' | 'json-view'; +const PopoverContainer = styled.div` + left: -40px; + position: relative; + top: 10px; + + .euiPopover { + position: fixed; + z-index: 10; + } +`; + +const CollapseButton = styled(EuiButtonIcon)` + border: 1px solid; +`; + +CollapseButton.displayName = 'CollapseButton'; + interface Props { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; data: DetailItem[]; id: string; view: View; + onEventToggled: () => void; onUpdateColumns: OnUpdateColumns; onViewSelected: (selected: View) => void; timelineId: string; @@ -43,11 +69,27 @@ export const EventDetails = React.memo( data, id, view, + onEventToggled, onUpdateColumns, onViewSelected, timelineId, toggleColumn, }) => { + const button = useMemo( + () => ( + + + + ), + [onEventToggled] + ); + const tabs: EuiTabbedContentTab[] = [ { id: 'table-view', @@ -73,6 +115,14 @@ export const EventDetails = React.memo( return (
+ + + void; onUpdateColumns: OnUpdateColumns; timelineId: string; toggleColumn: (column: ColumnHeaderOptions) => void; } export const StatefulEventDetails = React.memo( - ({ browserFields, columnHeaders, data, id, onUpdateColumns, timelineId, toggleColumn }) => { + ({ + browserFields, + columnHeaders, + data, + id, + onEventToggled, + onUpdateColumns, + timelineId, + toggleColumn, + }) => { const [view, setView] = useState('table-view'); const handleSetView = useCallback((newView) => setView(newView), []); @@ -34,6 +44,7 @@ export const StatefulEventDetails = React.memo( columnHeaders={columnHeaders} data={data} id={id} + onEventToggled={onEventToggled} onUpdateColumns={onUpdateColumns} onViewSelected={handleSetView} timelineId={timelineId} diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 674eb3325efc2..8c1f69279d31c 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import useResizeObserver from 'use-resize-observer/polyfilled'; +import '../../mock/match_media'; import { mockIndexPattern, TestProviders } from '../../mock'; import { wait } from '../../lib/helpers'; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 6e6ba4911be26..3f474da102ca4 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiPanel } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; import { getOr, isEmpty, union } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { BrowserFields, DocValueFields } from '../../containers/source'; @@ -34,13 +34,40 @@ import { } from '../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; +import { ExitFullScreen } from '../exit_full_screen'; +import { useFullScreen } from '../../containers/use_full_screen'; +import { TimelineId } from '../../../../common/types/timeline'; + +export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px +const UTILITY_BAR_HEIGHT = 19; // px +const COMPACT_HEADER_HEIGHT = EVENTS_VIEWER_HEADER_HEIGHT - UTILITY_BAR_HEIGHT; // px + +const UtilityBar = styled.div` + height: ${UTILITY_BAR_HEIGHT}px; +`; + +const TitleText = styled.span` + margin-right: 12px; +`; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; -const StyledEuiPanel = styled(EuiPanel)` +const StyledEuiPanel = styled(EuiPanel)<{ $isFullScreen: boolean }>` + ${({ $isFullScreen }) => + $isFullScreen && + css` + border: 0; + box-shadow: none; + padding-top: 0; + padding-bottom: 0; + `} max-width: 100%; `; +const TitleFlexGroup = styled(EuiFlexGroup)` + margin-top: 8px; +`; + const EventsContainerLoading = styled.div` width: 100%; overflow: auto; @@ -98,6 +125,7 @@ const EventsViewerComponent: React.FC = ({ utilityBar, graphEventId, }) => { + const { globalFullScreen } = useFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const [isQueryLoading, setIsQueryLoading] = useState(false); @@ -113,6 +141,20 @@ const EventsViewerComponent: React.FC = ({ id, ]); + const justTitle = useMemo(() => {title}, [title]); + + const titleWithExitFullScreen = useMemo( + () => ( + + {justTitle} + + + + + ), + [justTitle] + ); + const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), dataProviders, @@ -153,7 +195,10 @@ const EventsViewerComponent: React.FC = ({ ); return ( - + {canQueryTimeline ? ( = ({ return ( <> - + {headerFilterGroup} - {utilityBar?.(refetch, totalCountMinusDeleted)} + {utilityBar && ( + {utilityBar?.(refetch, totalCountMinusDeleted)} + )} = ({ excludedRowRendererIds, filters, headerFilterGroup, + height, id, isLive, itemsPerPage, @@ -128,6 +130,7 @@ const StatefulEventsViewerComponent: React.FC = ({ isLoadingIndexPattern={isLoadingIndexPattern} filters={globalFilters} headerFilterGroup={headerFilterGroup} + height={height} indexPattern={indexPatterns} isLive={isLive} itemsPerPage={itemsPerPage!} @@ -203,6 +206,7 @@ type PropsFromRedux = ConnectedProps; export const StatefulEventsViewer = connector( React.memo( StatefulEventsViewerComponent, + // eslint-disable-next-line complexity (prevProps, nextProps) => prevProps.id === nextProps.id && deepEqual(prevProps.columns, nextProps.columns) && @@ -212,6 +216,7 @@ export const StatefulEventsViewer = connector( prevProps.deletedEventIds === nextProps.deletedEventIds && prevProps.end === nextProps.end && deepEqual(prevProps.filters, nextProps.filters) && + prevProps.height === nextProps.height && prevProps.isLive === nextProps.isLive && prevProps.itemsPerPage === nextProps.itemsPerPage && deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx index afc3568fd6c65..7bef771d367f3 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx @@ -27,6 +27,9 @@ describe('useFetchOrCreateRuleExceptionList', () => { let fetchRuleById: jest.SpyInstance>; let patchRule: jest.SpyInstance>; let addExceptionList: jest.SpyInstance>; + let addEndpointExceptionList: jest.SpyInstance>; let fetchExceptionListById: jest.SpyInstance>; let render: ( listType?: UseFetchOrCreateRuleExceptionListProps['exceptionListType'] @@ -75,6 +78,10 @@ describe('useFetchOrCreateRuleExceptionList', () => { .spyOn(listsApi, 'addExceptionList') .mockResolvedValue(newDetectionExceptionList); + addEndpointExceptionList = jest + .spyOn(listsApi, 'addEndpointExceptionList') + .mockResolvedValue(newEndpointExceptionList); + fetchExceptionListById = jest .spyOn(listsApi, 'fetchExceptionListById') .mockResolvedValue(detectionExceptionList); @@ -299,7 +306,7 @@ describe('useFetchOrCreateRuleExceptionList', () => { await waitForNextUpdate(); await waitForNextUpdate(); await waitForNextUpdate(); - expect(addExceptionList).toHaveBeenCalledTimes(1); + expect(addEndpointExceptionList).toHaveBeenCalledTimes(1); }); }); it('should update the rule', async () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx index 245ce192b3cfa..b238e25f6de59 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx @@ -7,17 +7,22 @@ import { useEffect, useState } from 'react'; import { HttpStart } from '../../../../../../../src/core/public'; -import { - ExceptionListSchema, - CreateExceptionListSchema, -} from '../../../../../lists/common/schemas'; import { Rule } from '../../../detections/containers/detection_engine/rules/types'; import { List, ListArray } from '../../../../common/detection_engine/schemas/types'; import { fetchRuleById, patchRule, } from '../../../detections/containers/detection_engine/rules/api'; -import { fetchExceptionListById, addExceptionList } from '../../../lists_plugin_deps'; +import { + fetchExceptionListById, + addExceptionList, + addEndpointExceptionList, +} from '../../../lists_plugin_deps'; +import { + ExceptionListSchema, + CreateExceptionListSchema, + ENDPOINT_LIST_ID, +} from '../../../../common/shared_imports'; export type ReturnUseFetchOrCreateRuleExceptionList = [boolean, ExceptionListSchema | null]; @@ -51,27 +56,43 @@ export const useFetchOrCreateRuleExceptionList = ({ const abortCtrl = new AbortController(); async function createExceptionList(ruleResponse: Rule): Promise { - const exceptionListToCreate: CreateExceptionListSchema = { - name: ruleResponse.name, - description: ruleResponse.description, - type: exceptionListType, - namespace_type: exceptionListType === 'endpoint' ? 'agnostic' : 'single', - _tags: undefined, - tags: undefined, - list_id: exceptionListType === 'endpoint' ? 'endpoint_list' : undefined, - meta: undefined, - }; - try { - const newExceptionList = await addExceptionList({ + let newExceptionList: ExceptionListSchema; + if (exceptionListType === 'endpoint') { + const possibleEndpointExceptionList = await addEndpointExceptionList({ + http, + signal: abortCtrl.signal, + }); + if (Object.keys(possibleEndpointExceptionList).length === 0) { + // Endpoint exception list already exists, fetch it + newExceptionList = await fetchExceptionListById({ + http, + id: ENDPOINT_LIST_ID, + namespaceType: 'agnostic', + signal: abortCtrl.signal, + }); + } else { + newExceptionList = possibleEndpointExceptionList as ExceptionListSchema; + } + } else { + const exceptionListToCreate: CreateExceptionListSchema = { + name: ruleResponse.name, + description: ruleResponse.description, + type: exceptionListType, + namespace_type: 'single', + list_id: undefined, + _tags: undefined, + tags: undefined, + meta: undefined, + }; + newExceptionList = await addExceptionList({ http, list: exceptionListToCreate, signal: abortCtrl.signal, }); - return Promise.resolve(newExceptionList); - } catch (error) { - return Promise.reject(error); } + return Promise.resolve(newExceptionList); } + async function createAndAssociateExceptionList( ruleResponse: Rule ): Promise { @@ -133,7 +154,7 @@ export const useFetchOrCreateRuleExceptionList = ({ let exceptionListToUse: ExceptionListSchema; const matchingList = exceptionLists.find((list) => { if (exceptionListType === 'endpoint') { - return list.type === exceptionListType && list.list_id === 'endpoint_list'; + return list.type === exceptionListType && list.list_id === ENDPOINT_LIST_ID; } else { return list.type === exceptionListType; } diff --git a/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx new file mode 100644 index 0000000000000..8c5ad95a8de0e --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiWindowEvent } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { useFullScreen } from '../../../common/containers/use_full_screen'; + +import * as i18n from './translations'; + +export const ExitFullScreen: React.FC = () => { + const { globalFullScreen, setGlobalFullScreen } = useFullScreen(); + + const exitFullScreen = useCallback(() => { + setGlobalFullScreen(false); + }, [setGlobalFullScreen]); + + const onKeyDown = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + + exitFullScreen(); + } + }, + [exitFullScreen] + ); + + if (!globalFullScreen) { + return null; + } + + return ( + <> + + + {i18n.EXIT_FULL_SCREEN} + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/common/components/exit_full_screen/translations.ts b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/translations.ts new file mode 100644 index 0000000000000..72d451cfdfc14 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/translations.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const EXIT_FULL_SCREEN = i18n.translate('xpack.securitySolution.exitFullScreenButton', { + defaultMessage: 'Exit full screen', +}); diff --git a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx index b4d8c790002b2..65901ec589daf 100644 --- a/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx +++ b/x-pack/plugins/security_solution/public/common/components/filters_global/filters_global.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { Sticky } from 'react-sticky'; import styled, { css } from 'styled-components'; +import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; import { gutterTimeline } from '../../lib/helpers'; const offsetChrome = 49; @@ -17,6 +18,7 @@ const disableSticky = `screen and (max-width: ${euiLightVars.euiBreakpoints.s})` const disableStickyMq = window.matchMedia(disableSticky); const Wrapper = styled.aside<{ isSticky?: boolean }>` + height: ${FILTERS_GLOBAL_HEIGHT}px; position: relative; z-index: ${({ theme }) => theme.eui.euiZNavigation}; background: ${({ theme }) => theme.eui.euiColorEmptyShade}; diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index ba4f782499802..3a8f2f0c16b96 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -17,17 +17,19 @@ import { MlPopover } from '../ml_popover/ml_popover'; import { SiemNavigation } from '../navigation'; import * as i18n from './translations'; import { useWithSource } from '../../containers/source'; +import { useFullScreen } from '../../containers/use_full_screen'; import { useGetUrlSearch } from '../navigation/use_get_url_search'; import { useKibana } from '../../lib/kibana'; import { APP_ID, ADD_DATA_PATH, APP_DETECTIONS_PATH } from '../../../../common/constants'; import { LinkAnchor } from '../links'; -const Wrapper = styled.header` - ${({ theme }) => css` +const Wrapper = styled.header<{ show: boolean }>` + ${({ show, theme }) => css` background: ${theme.eui.euiColorEmptyShade}; border-bottom: ${theme.eui.euiBorderThin}; padding: ${theme.eui.paddingSizes.m} ${gutterTimeline} ${theme.eui.paddingSizes.m} ${theme.eui.paddingSizes.l}; + ${show ? '' : 'display: none;'}; `} `; Wrapper.displayName = 'Wrapper'; @@ -42,6 +44,7 @@ interface HeaderGlobalProps { } export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { const { indicesExist } = useWithSource(); + const { globalFullScreen } = useFullScreen(); const search = useGetUrlSearch(navTabs.overview); const { navigateToApp } = useKibana().services.application; const goToOverview = useCallback( @@ -53,7 +56,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine ); return ( - + <> diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/editable_title.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/editable_title.test.tsx index 1e9a2e06474b9..30e992380e7c6 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/editable_title.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/editable_title.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../mock/match_media'; import { TestProviders } from '../../mock'; import { EditableTitle } from './editable_title'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx index 30f510509913a..15711663116f9 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx @@ -8,6 +8,7 @@ import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { shallow } from 'enzyme'; import React from 'react'; +import '../../mock/match_media'; import { TestProviders } from '../../mock'; import { HeaderPage } from './index'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx index 5187a32ac9721..fd7a0a5d96e00 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/title.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../mock/match_media'; import { TestProviders } from '../../mock'; import { Title } from './title'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap index 53b41e2240de2..f2d2d23d60fb1 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_section/__snapshots__/index.test.tsx.snap @@ -1,7 +1,9 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`HeaderSection it renders 1`] = ` -
+
diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx index 43245121dd393..f49001bd5d7af 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.tsx @@ -13,12 +13,18 @@ import { Subtitle } from '../subtitle'; interface HeaderProps { border?: boolean; + height?: number; } const Header = styled.header.attrs(() => ({ className: 'siemHeaderSection', }))` - margin-bottom: ${({ theme }) => theme.eui.euiSizeL}; +${({ height }) => + height && + css` + height: ${height}px; + `} + margin-bottom: ${({ height, theme }) => (height ? 0 : theme.eui.euiSizeL)}; user-select: text; ${({ border }) => @@ -32,6 +38,7 @@ Header.displayName = 'Header'; export interface HeaderSectionProps extends HeaderProps { children?: React.ReactNode; + height?: number; id?: string; split?: boolean; subtitle?: string | React.ReactNode; @@ -43,6 +50,7 @@ export interface HeaderSectionProps extends HeaderProps { const HeaderSectionComponent: React.FC = ({ border, children, + height, id, split, subtitle, @@ -50,7 +58,7 @@ const HeaderSectionComponent: React.FC = ({ titleSize = 'm', tooltip, }) => ( -
+
diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx index c48a5590b49cf..e9940d088e606 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/entity_draggable.test.tsx @@ -6,6 +6,8 @@ import React from 'react'; import { shallow } from 'enzyme'; + +import '../../mock/match_media'; import { EntityDraggableComponent } from './entity_draggable'; import { TestProviders } from '../../mock/test_providers'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx index f7fa0ac0a8be1..434cbd8ada88e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_score.test.tsx @@ -7,6 +7,8 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; + +import '../../../mock/match_media'; import { AnomalyScoreComponent } from './anomaly_score'; import { mockAnomalies } from '../mock'; import { TestProviders } from '../../../mock/test_providers'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx index d0b923002d6d4..a900c3e49f912 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/anomaly_scores.test.tsx @@ -7,6 +7,8 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; + +import '../../../mock/match_media'; import { AnomalyScoresComponent, createJobKey } from './anomaly_scores'; import { mockAnomalies } from '../mock'; import { TestProviders } from '../../../mock/test_providers'; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.test.tsx index f7759bb74c3ab..673d1a1cdb72e 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/draggable_score.test.tsx @@ -5,10 +5,12 @@ */ import React from 'react'; -import { mockAnomalies } from '../mock'; import { cloneDeep } from 'lodash/fp'; import { shallow } from 'enzyme'; + +import '../../../mock/match_media'; import { DraggableScoreComponent } from './draggable_score'; +import { mockAnomalies } from '../mock'; describe('draggable_score', () => { let anomalies = cloneDeep(mockAnomalies); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index b90946c534f3a..d370a901a6262 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; + +import '../../../mock/match_media'; import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns'; import { HostsType } from '../../../../hosts/store/model'; import * as i18n from './translations'; import { AnomaliesByHost, Anomaly } from '../types'; import { Columns } from '../../paginated_table'; import { TestProviders } from '../../../mock'; -import React from 'react'; import { useMountAppended } from '../../../utils/use_mount_appended'; const startDate = new Date(2001).toISOString(); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 79277c46e1c9d..69a4e383413f2 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../../mock/match_media'; import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns'; import { NetworkType } from '../../../../network/store/model'; import * as i18n from './translations'; diff --git a/x-pack/plugins/security_solution/public/common/components/page/index.tsx b/x-pack/plugins/security_solution/public/common/components/page/index.tsx index f539bb7831c1c..9a5654ed6475f 100644 --- a/x-pack/plugins/security_solution/public/common/components/page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/page/index.tsx @@ -7,11 +7,13 @@ import { EuiBadge, EuiDescriptionList, EuiFlexGroup, EuiIcon, EuiPage } from '@elastic/eui'; import styled, { createGlobalStyle } from 'styled-components'; +import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; + /* SIDE EFFECT: the following `createGlobalStyle` overrides default styling in angular code that was not theme-friendly and `EuiPopover`, `EuiToolTip` global styles */ -export const AppGlobalStyle = createGlobalStyle` +export const AppGlobalStyle = createGlobalStyle<{ theme: { eui: { euiColorPrimary: string } } }>` /* dirty hack to fix draggables with tooltip on FF */ body#siem-app { position: static; @@ -57,6 +59,10 @@ export const AppGlobalStyle = createGlobalStyle` z-index: 9950; } + /** applies a "toggled" button style to the Full Screen button */ + .${FULL_SCREEN_TOGGLED_CLASS_NAME} { + ${({ theme }) => `background-color: ${theme.eui.euiColorPrimary} !important`}; + } `; export const DescriptionListStyled = styled(EuiDescriptionList)` diff --git a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx index a182102329f05..de60bca73cedf 100644 --- a/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/search_bar/index.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr, set } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { getOr } from 'lodash/fp'; import React, { memo, useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { Dispatch } from 'redux'; diff --git a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx index 7ceb34755648e..b28c7e70b8ae8 100644 --- a/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; +import { shallow } from 'enzyme'; + +import '../../mock/match_media'; import { getRowItemDraggables, getRowItemOverflow, getRowItemDraggable, OverflowFieldComponent, } from './helpers'; -import React from 'react'; -import { shallow } from 'enzyme'; import { TestProviders } from '../../mock'; import { getEmptyValue } from '../empty_value'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/common/components/toasters/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/index.test.tsx index 35036ef4b16b5..d366da1df9fd3 100644 --- a/x-pack/plugins/security_solution/public/common/components/toasters/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/toasters/index.test.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep, set } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { cloneDeep } from 'lodash/fp'; import { mount } from 'enzyme'; import React, { useEffect } from 'react'; diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index b393e9ae6319b..1e93fdb936728 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx @@ -7,6 +7,7 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; +import '../../mock/match_media'; import { mockBrowserFields } from '../../containers/source/mock'; import { apolloClientObservable, diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx index e5a1fb6120285..667d1816e8f07 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.test.tsx @@ -7,6 +7,7 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; +import '../../mock/match_media'; import { TestProviders, mockIndexPattern } from '../../mock'; import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx index 3223c5058fa7f..03f9b43678003 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx @@ -5,9 +5,10 @@ */ import classNames from 'classnames'; -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; +import { useFullScreen } from '../../containers/use_full_screen'; import { gutterTimeline } from '../../lib/helpers'; import { AppGlobalStyle } from '../page/index'; @@ -45,6 +46,11 @@ const WrapperPageComponent: React.FC = ({ style, noPadding, }) => { + const { setGlobalFullScreen } = useFullScreen(); + useEffect(() => { + setGlobalFullScreen(false); // exit full screen mode on page load + }, [setGlobalFullScreen]); + const classes = classNames(className, { siemWrapperPage: true, 'siemWrapperPage--restrictWidthDefault': diff --git a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 9b7dfe84277c6..8c03ab7b9f508 100644 --- a/x-pack/plugins/security_solution/public/common/containers/source/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx @@ -5,7 +5,8 @@ */ import { isUndefined } from 'lodash'; -import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { get, keyBy, pick, isEmpty } from 'lodash/fp'; import { useEffect, useMemo, useState } from 'react'; import memoizeOne from 'memoize-one'; import { IIndexPattern } from 'src/plugins/data/public'; diff --git a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx new file mode 100644 index 0000000000000..b8050034d34a6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { inputsSelectors } from '../../store'; +import { inputsActions } from '../../store/actions'; + +export const useFullScreen = () => { + const dispatch = useDispatch(); + const globalFullScreen = useSelector(inputsSelectors.globalFullScreenSelector) ?? false; + const timelineFullScreen = useSelector(inputsSelectors.timelineFullScreenSelector) ?? false; + + const setGlobalFullScreen = useCallback( + (fullScreen: boolean) => dispatch(inputsActions.setFullScreen({ id: 'global', fullScreen })), + [dispatch] + ); + + const setTimelineFullScreen = useCallback( + (fullScreen: boolean) => dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen })), + [dispatch] + ); + + const memoizedReturn = useMemo( + () => ({ + globalFullScreen, + setGlobalFullScreen, + setTimelineFullScreen, + timelineFullScreen, + }), + [globalFullScreen, setGlobalFullScreen, setTimelineFullScreen, timelineFullScreen] + ); + + return memoizedReturn; +}; diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts b/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts index efad0638b2971..5d00882f778c0 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts @@ -37,6 +37,11 @@ export const startAutoReload = actionCreator<{ id: InputsModelId }>('START_KQL_A export const stopAutoReload = actionCreator<{ id: InputsModelId }>('STOP_KQL_AUTO_RELOAD'); +export const setFullScreen = actionCreator<{ + id: InputsModelId; + fullScreen: boolean; +}>('SET_FULL_SCREEN'); + export const setQuery = actionCreator<{ inputId: InputsModelId; id: string; diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/helpers.ts b/x-pack/plugins/security_solution/public/common/store/inputs/helpers.ts index 1883f05dc9e9d..82a2072056d9f 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/helpers.ts @@ -9,6 +9,22 @@ import { get } from 'lodash/fp'; import { InputsModel, TimeRange, Refetch, RefetchKql, InspectQuery } from './model'; import { InputsModelId } from './constants'; +export const updateInputFullScreen = ( + inputId: InputsModelId, + fullScreen: boolean, + state: InputsModel +): InputsModel => ({ + ...state, + global: { + ...state.global, + fullScreen: inputId === 'global' ? fullScreen : state.global.fullScreen, + }, + timeline: { + ...state.timeline, + fullScreen: inputId === 'timeline' ? fullScreen : state.timeline.fullScreen, + }, +}); + export const updateInputTimerange = ( inputId: InputsModelId, timerange: TimeRange, diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts index 358124405c146..a8db48c7b31bb 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/model.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/model.ts @@ -80,6 +80,7 @@ export interface InputsRange { query: Query; filters: Filter[]; savedQuery?: SavedQuery; + fullScreen?: boolean; } export interface LinkTo { diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/reducer.ts b/x-pack/plugins/security_solution/public/common/store/inputs/reducer.ts index 40d9ad777acde..a94f0f6ca24ee 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/reducer.ts @@ -12,6 +12,7 @@ import { deleteAllQuery, setAbsoluteRangeDatePicker, setDuration, + setFullScreen, setInspectionParameter, setQuery, setRelativeRangeDatePicker, @@ -38,6 +39,7 @@ import { removeTimelineLink, addTimelineLink, deleteOneQuery as helperDeleteOneQuery, + updateInputFullScreen, } from './helpers'; import { InputsModel, TimeRange } from './model'; @@ -57,6 +59,7 @@ export const initialInputsState: InputsState = { language: 'kuery', }, filters: [], + fullScreen: false, }, timeline: { timerange: { @@ -71,6 +74,7 @@ export const initialInputsState: InputsState = { language: 'kuery', }, filters: [], + fullScreen: false, }, }; @@ -98,6 +102,7 @@ export const createInitialInputsState = (): InputsState => { language: 'kuery', }, filters: [], + fullScreen: false, }, timeline: { timerange: { @@ -118,6 +123,7 @@ export const createInitialInputsState = (): InputsState => { language: 'kuery', }, filters: [], + fullScreen: false, }, }; }; @@ -163,6 +169,9 @@ export const inputsReducer = reducerWithInitialState(initialInputsState) }; return updateInputTimerange(id, timerange, state); }) + .case(setFullScreen, (state, { id, fullScreen }) => { + return updateInputFullScreen(id, fullScreen, state); + }) .case(deleteAllQuery, (state, { id }) => ({ ...state, [id]: { diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts b/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts index 0eee5ebbfbf77..9feb2f87d7e08 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts @@ -44,6 +44,13 @@ export const timelineTimeRangeSelector = createSelector( (timeline) => timeline.timerange ); +export const globalFullScreenSelector = createSelector(selectGlobal, (global) => global.fullScreen); + +export const timelineFullScreenSelector = createSelector( + selectTimeline, + (timeline) => timeline.fullScreen +); + export const globalTimeRangeSelector = createSelector(selectGlobal, (global) => global.timerange); export const globalPolicySelector = createSelector(selectGlobal, (global) => global.policy); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx index 09883e342f998..692d22b115b48 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/alerts_histogram.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import '../../../common/mock/match_media'; import { AlertsHistogram } from './alerts_histogram'; jest.mock('../../../common/lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx index 4cbfa59aac582..533f13e6781a6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import '../../../common/mock/match_media'; import { AlertsHistogramPanel } from './index'; jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx index cc3a47017a835..d5688d84e9759 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import '../../../common/mock/match_media'; import { TimelineId } from '../../../../common/types/timeline'; import { TestProviders } from '../../../common/mock'; import { AlertsTableComponent } from './index'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 405ba0719a910..30cfe2d02354f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -61,6 +61,7 @@ interface OwnProps { timelineId: TimelineIdLiteral; canUserCRUD: boolean; defaultFilters?: Filter[]; + eventsViewerBodyHeight?: number; hasIndexWrite: boolean; from: string; loading: boolean; @@ -86,6 +87,7 @@ export const AlertsTableComponent: React.FC = ({ clearEventsLoading, clearSelected, defaultFilters, + eventsViewerBodyHeight, from, globalFilters, globalQuery, @@ -443,6 +445,7 @@ export const AlertsTableComponent: React.FC = ({ defaultModel={alertsDefaultModel} end={to} headerFilterGroup={headerFilterGroup} + height={eventsViewerBodyHeight} id={timelineId} start={from} utilityBar={utilityBarCallback} diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.test.tsx index a2685017f86d6..efce1dc026353 100644 --- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import '../../../common/mock/match_media'; import { DetectionEngineHeaderPage } from './index'; describe('detection_engine_header_page', () => { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx index d841af69a7537..59334b53faa17 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.test.tsx @@ -7,6 +7,7 @@ import React, { useRef } from 'react'; import { shallow } from 'enzyme'; +import '../../../../common/mock/match_media'; import { AllRulesTables } from './index'; import { AllRulesTabs } from '../../../pages/detection_engine/rules/all'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx index 89f6399071dd3..a41da908085bc 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import '../../../../common/mock/match_media'; import { PrePackagedRulesPrompt } from './load_empty_prompt'; jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index f4004a66c8f80..e7a8c4854fa9e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -5,15 +5,33 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; import { useParams } from 'react-router-dom'; import '../../../common/mock/match_media'; +import { + apolloClientObservable, + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { DetectionEnginePageComponent } from './detection_engine'; import { useUserInfo } from '../../components/user_info'; import { useWithSource } from '../../../common/containers/source'; +import { createStore, State } from '../../../common/store'; +import { mockHistory, Router } from '../../../cases/components/__mock__/router'; +// Test will fail because we will to need to mock some core services to make the test work +// For now let's forget about SiemSearchBar and QueryBar +jest.mock('../../../common/components/search_bar', () => ({ + SiemSearchBar: () => null, +})); +jest.mock('../../../common/components/query_bar', () => ({ + QueryBar: () => null, +})); jest.mock('../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../components/user_info'); jest.mock('../../../common/containers/source'); @@ -36,6 +54,19 @@ jest.mock('react-router-dom', () => { }; }); +const state: State = { + ...mockGlobalState, +}; + +const { storage } = createSecuritySolutionStorageMock(); +const store = createStore( + state, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage +); + describe('DetectionEnginePageComponent', () => { beforeAll(() => { (useParams as jest.Mock).mockReturnValue({}); @@ -47,14 +78,18 @@ describe('DetectionEnginePageComponent', () => { }); it('renders correctly', () => { - const wrapper = shallow( - + const wrapper = mount( + + + + + ); - expect(wrapper.find('FiltersGlobal')).toHaveLength(1); + expect(wrapper.find('FiltersGlobal').exists()).toBe(true); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index aef9f2adcbcc8..acafb15db3448 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -4,12 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useState } from 'react'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; - +import { useWindowSize } from 'react-use'; import { useHistory } from 'react-router-dom'; + +import { globalHeaderHeightPx } from '../../../app/home'; import { SecurityPageName } from '../../../app/types'; import { TimelineId } from '../../../../common/types/timeline'; import { useGlobalTime } from '../../../common/containers/use_global_time'; @@ -31,6 +34,7 @@ import { NoWriteAlertsCallOut } from '../../components/no_write_alerts_callout'; import { AlertsHistogramPanel } from '../../components/alerts_histogram_panel'; import { alertsHistogramOptions } from '../../components/alerts_histogram_panel/config'; import { useUserInfo } from '../../components/user_info'; +import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../common/components/events_viewer/events_viewer'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; @@ -39,6 +43,14 @@ import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unau import * as i18n from './translations'; import { LinkButton } from '../../../common/components/links'; import { useFormatUrl } from '../../../common/components/link_to'; +import { FILTERS_GLOBAL_HEIGHT } from '../../../../common/constants'; +import { useFullScreen } from '../../../common/containers/use_full_screen'; +import { Display } from '../../../hosts/pages/display'; +import { + getEventsViewerBodyHeight, + MIN_EVENTS_VIEWER_BODY_HEIGHT, +} from '../../../timelines/components/timeline/body/helpers'; +import { footerHeight } from '../../../timelines/components/timeline/footer'; import { buildShowBuildingBlockFilter } from '../../components/alerts_table/default_config'; export const DetectionEnginePageComponent: React.FC = ({ @@ -47,6 +59,8 @@ export const DetectionEnginePageComponent: React.FC = ({ setAbsoluteRangeDatePicker, }) => { const { to, from, deleteQuery, setQuery } = useGlobalTime(); + const { height: windowHeight } = useWindowSize(); + const { globalFullScreen } = useFullScreen(); const { loading: userInfoLoading, isSignalIndexExists, @@ -136,51 +150,66 @@ export const DetectionEnginePageComponent: React.FC = ({ {hasIndexWrite != null && !hasIndexWrite && } {indicesExist ? ( + - - - {i18n.LAST_ALERT} - {': '} - {lastAlerts} - - ) - } - title={i18n.PAGE_TITLE} - > - + + + {i18n.LAST_ALERT} + {': '} + {lastAlerts} + + ) + } + title={i18n.PAGE_TITLE} > - {i18n.BUTTON_MANAGE_RULES} - - + + {i18n.BUTTON_MANAGE_RULES} + + + + + - - ({ + SiemSearchBar: () => null, +})); +jest.mock('../../../../../common/components/query_bar', () => ({ + QueryBar: () => null, +})); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); @@ -38,6 +55,18 @@ jest.mock('react-router-dom', () => { }; }); +const state: State = { + ...mockGlobalState, +}; +const { storage } = createSecuritySolutionStorageMock(); +const store = createStore( + state, + SUB_PLUGINS_REDUCER, + apolloClientObservable, + kibanaObservable, + storage +); + describe('RuleDetailsPageComponent', () => { beforeAll(() => { (useUserInfo as jest.Mock).mockReturnValue({}); @@ -49,17 +78,21 @@ describe('RuleDetailsPageComponent', () => { }); it('renders correctly', () => { - const wrapper = shallow( - , + const wrapper = mount( + + + + + , { wrappingComponent: TestProviders, } ); - expect(wrapper.find('DetectionEngineHeaderPage')).toHaveLength(1); + expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 2e7ef1180f4e3..7eb5c3a535377 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -15,13 +15,17 @@ import { EuiTab, EuiTabs, EuiToolTip, + EuiWindowEvent, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { noop } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; +import { useWindowSize } from 'react-use'; +import { globalHeaderHeightPx } from '../../../../../app/home'; import { TimelineId } from '../../../../../../common/types/timeline'; import { UpdateDateRange } from '../../../../../common/components/charts/common'; import { FiltersGlobal } from '../../../../../common/components/filters_global'; @@ -62,6 +66,7 @@ import * as ruleI18n from '../translations'; import * as i18n from './translations'; import { useGlobalTime } from '../../../../../common/containers/use_global_time'; import { alertsHistogramOptions } from '../../../../components/alerts_histogram_panel/config'; +import { EVENTS_VIEWER_HEADER_HEIGHT } from '../../../../../common/components/events_viewer/events_viewer'; import { inputsSelectors } from '../../../../../common/store/inputs'; import { State } from '../../../../../common/store'; import { InputsRange } from '../../../../../common/store/inputs/model'; @@ -76,7 +81,15 @@ import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; +import { FILTERS_GLOBAL_HEIGHT } from '../../../../../../common/constants'; +import { useFullScreen } from '../../../../../common/containers/use_full_screen'; +import { Display } from '../../../../../hosts/pages/display'; import { ExceptionListTypeEnum, ExceptionIdentifiers } from '../../../../../lists_plugin_deps'; +import { + getEventsViewerBodyHeight, + MIN_EVENTS_VIEWER_BODY_HEIGHT, +} from '../../../../../timelines/components/timeline/body/helpers'; +import { footerHeight } from '../../../../../timelines/components/timeline/footer'; enum RuleDetailTabs { alerts = 'alerts', @@ -141,6 +154,8 @@ export const RuleDetailsPageComponent: FC = ({ const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const { height: windowHeight } = useWindowSize(); + const { globalFullScreen } = useFullScreen(); // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = @@ -329,140 +344,156 @@ export const RuleDetailsPageComponent: FC = ({ {userHasNoPermissions(canUserCRUD) && } {indicesExist ? ( + - - - {detectionI18n.LAST_ALERT} - {': '} - {lastAlerts} - , - ] - : []), - , - ]} - title={title} - > - - - - + + + {detectionI18n.LAST_ALERT} + {': '} + {lastAlerts} + , + ] + : []), + , + ]} + title={title} + > + + + - + > + + + + + + + + + {ruleI18n.EDIT_RULE_SETTINGS} + + + + + + + + + + {ruleError} + + + + - - - - - {ruleI18n.EDIT_RULE_SETTINGS} - + + + + + {defineRuleData != null && ( + + )} + - - + + + + {scheduleRuleData != null && ( + + )} + - - {ruleError} - - - - - - - - - - - {defineRuleData != null && ( - - )} - - - - - - {scheduleRuleData != null && ( - - )} - - - - - - - {tabs} - + + {tabs} + + {ruleDetailTab === RuleDetailTabs.alerts && ( <> - - + + + + {ruleId != null && ( ` + ${({ show }) => (show ? '' : 'display: none;')}; +`; + +Display.displayName = 'Display'; diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index b37d91cc2be3b..a3885eac5377c 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer } from '@elastic/eui'; +import { EuiSpacer, EuiWindowEvent } from '@elastic/eui'; +import { noop } from 'lodash/fp'; import React, { useCallback } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; @@ -22,6 +23,7 @@ import { manageQuery } from '../../common/components/page/manage_query'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; import { KpiHostsQuery } from '../containers/kpi_hosts'; +import { useFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { useWithSource } from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; @@ -34,6 +36,7 @@ import { SpyRoute } from '../../common/utils/route/spy_routes'; import { esQuery } from '../../../../../../src/plugins/data/public'; import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; import { OverviewEmpty } from '../../overview/components/overview_empty'; +import { Display } from './display'; import { HostsTabs } from './hosts_tabs'; import { navTabsHosts } from './nav_tabs'; import * as i18n from './translations'; @@ -47,6 +50,7 @@ const KpiHostsComponentManage = manageQuery(KpiHostsComponent); export const HostsComponent = React.memo( ({ filters, query, setAbsoluteRangeDatePicker, hostsPagePath }) => { const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useFullScreen(); const capabilities = useMlCapabilities(); const kibana = useKibana(); const { tabName } = useParams(); @@ -88,44 +92,47 @@ export const HostsComponent = React.memo( <> {indicesExist ? ( + - - } - title={i18n.PAGE_TITLE} - /> - - - {({ kpiHosts, loading, id, inspect, refetch }) => ( - - )} - - - - - - - + + + } + title={i18n.PAGE_TITLE} + /> + + + {({ kpiHosts, loading, id, inspect, refetch }) => ( + + )} + + + + + + + + { const { initializeTimeline } = useManageTimeline(); const dispatch = useDispatch(); - + const { height: windowHeight } = useWindowSize(); + const { globalFullScreen } = useFullScreen(); useEffect(() => { initializeTimeline({ id: TimelineId.hostsPageEvents, @@ -81,19 +93,32 @@ export const EventsQueryTabBody = ({ return ( <> - + {!globalFullScreen && ( + + )} ( capabilitiesFetched, }) => { const { to, from, setQuery, isInitializing } = useGlobalTime(); + const { globalFullScreen } = useFullScreen(); const kibana = useKibana(); const { tabName } = useParams(); @@ -95,56 +99,61 @@ const NetworkComponent = React.memo( <> {indicesExist ? ( + - - } - title={i18n.PAGE_TITLE} - /> - - - - - - - {({ kpiNetwork, loading, id, inspect, refetch }) => ( - - )} - + + + } + title={i18n.PAGE_TITLE} + /> + + + + + + + {({ kpiNetwork, loading, id, inspect, refetch }) => ( + + )} + + {capabilitiesFetched && !isInitializing ? ( <> - + + - + - + + ( ) : ( )} - - ) : ( diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index 8d004829a34f0..63126da0b9bb5 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -11,6 +11,7 @@ import { mount, ReactWrapper } from 'enzyme'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../common/mock/match_media'; import { useQuery } from '../../../common/containers/matrix_histogram'; import { wait } from '../../../common/lib/helpers'; import { mockIndexPattern, TestProviders } from '../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx index c4a941d845f16..8268a550257c9 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { OverviewHostProps } from '../overview_host'; import { OverviewNetworkProps } from '../overview_network'; import { mockIndexPattern, TestProviders } from '../../../common/mock'; +import '../../../common/mock/match_media'; import { EventCounts } from '.'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx index 8e221445a95d3..fee38ad3c6289 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/endpoint_overview/index.test.tsx @@ -6,6 +6,8 @@ import { mount } from 'enzyme'; import React from 'react'; + +import '../../../../common/mock/match_media'; import { TestProviders } from '../../../../common/mock'; import { EndpointOverview } from './index'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx index 71cf056f3eb62..6bd0390d014a3 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.test.tsx @@ -6,6 +6,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock'; import { HostOverview } from './index'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index 5140137ce1b99..30874e8874760 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -9,6 +9,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; +import '../../../common/mock/match_media'; import { apolloClientObservable, mockGlobalState, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index d2d823f625690..9ac4f7125f34d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -8,6 +8,7 @@ import { cloneDeep } from 'lodash/fp'; import { mount } from 'enzyme'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; +import '../../../common/mock/match_media'; import { apolloClientObservable, mockGlobalState, diff --git a/x-pack/plugins/security_solution/public/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 5d4579b427f18..9939345324f11 100644 --- a/x-pack/plugins/security_solution/public/shared_imports.ts +++ b/x-pack/plugins/security_solution/public/shared_imports.ts @@ -49,4 +49,5 @@ export { ExceptionList, Pagination, UseExceptionListSuccess, + addEndpointExceptionList, } from '../../lists/public'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index a5edffc2a099a..b31094b07a829 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { TestProviders } from '../../../common/mock'; +import '../../../common/mock/match_media'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { CertificateFingerprint } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx index 94123000888aa..c38eb23195c06 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/duration/index.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock'; import { ONE_MILLISECOND_AS_NANOSECONDS } from '../formatted_duration/helpers'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx index cf12740d93a18..c3b67e3300459 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { FlowTarget, GetIpOverviewQuery, HostEcsFields } from '../../../graphql/types'; import { TestProviders } from '../../../common/mock'; +import '../../../common/mock/match_media'; import { getEmptyValue } from '../../../common/components/empty_value'; import { diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx index 16174e92b3c37..62306046c7b8c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/category.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import '../../../common/mock/match_media'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { Category } from './category'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx index 7c4e3d435e1ed..9340ee8cf0c7f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_browser.test.tsx @@ -7,6 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; +import '../../../common/mock/match_media'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx index e4c9621c2f71c..f4f8adc9f0419 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; +import '../../../common/mock/match_media'; import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx index 1f917c664e813..44e4818830acd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_name.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; +import '../../../common/mock/match_media'; import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; import { FieldName } from './field_name'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx index b55bbfc023774..c2ddba6bd88c3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/fields_pane.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import '../../../common/mock/match_media'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx index ed3f957ad11a8..a3c7440bece24 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/index.test.tsx @@ -7,6 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; +import '../../../common/mock/match_media'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx index 9b7d4c3266c56..cfdca8950d314 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header_with_close_button/index.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { TimelineType } from '../../../../../common/types/timeline'; import { TestProviders } from '../../../../common/mock'; +import '../../../../common/mock/match_media'; import { FlyoutHeaderWithCloseButton } from '.'; jest.mock('react-router-dom', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index 50578ef0a8e42..9f550f87068be 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx @@ -5,7 +5,7 @@ */ import { mount, shallow } from 'enzyme'; -import { set } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; import React from 'react'; import { ActionCreator } from 'typescript-fsa'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 1616738897b0a..f41d318ba9587 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx @@ -10,11 +10,13 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { Resizable, ResizeCallback } from 're-resizable'; -import { TimelineResizeHandle } from './timeline_resize_handle'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; +import { useFullScreen } from '../../../../common/containers/use_full_screen'; +import { timelineActions } from '../../../store/timeline'; + +import { TimelineResizeHandle } from './timeline_resize_handle'; import * as i18n from './translations'; -import { timelineActions } from '../../../store/timeline'; const minWidthPixels = 550; // do not allow the flyout to shrink below this width (pixels) const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view @@ -44,12 +46,12 @@ const RESIZABLE_ENABLE = { left: true }; const FlyoutPaneComponent: React.FC = ({ children, - flyoutHeight, onClose, timelineId, width, }) => { const dispatch = useDispatch(); + const { timelineFullScreen } = useFullScreen(); const onResizeStop: ResizeCallback = useCallback( (_e, _direction, _ref, delta) => { @@ -80,9 +82,9 @@ const FlyoutPaneComponent: React.FC = ({ ); const resizableHandleComponent = useMemo( () => ({ - left: , + left: , }), - [flyoutHeight] + [] ); return ( @@ -98,8 +100,8 @@ const FlyoutPaneComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/timeline_resize_handle.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/timeline_resize_handle.tsx index 741ed0a09ebf6..7192580f2426d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/timeline_resize_handle.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/timeline_resize_handle.tsx @@ -6,15 +6,17 @@ import styled from 'styled-components'; -export const TIMELINE_RESIZE_HANDLE_WIDTH = 2; // px +export const TIMELINE_RESIZE_HANDLE_WIDTH = 4; // px -export const TimelineResizeHandle = styled.div<{ height: number }>` +export const TimelineResizeHandle = styled.div` + background-color: ${({ theme }) => theme.eui.euiColorLightShade}; cursor: col-resize; - height: 100%; min-height: 20px; - width: 0; - border: ${TIMELINE_RESIZE_HANDLE_WIDTH}px solid ${(props) => props.theme.eui.euiColorLightShade}; + width: ${TIMELINE_RESIZE_HANDLE_WIDTH}px; z-index: 2; - height: ${({ height }) => `${height}px`}; + height: 100vh; position: absolute; + &:hover { + background-color: ${({ theme }) => theme.eui.euiColorPrimary}; + } `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 085f0863c7b27..9f20c7f6c1571 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -4,21 +4,33 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiToolTip, +} from '@elastic/eui'; import { noop } from 'lodash/fp'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { connect, ConnectedProps, useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; import { SecurityPageName } from '../../../app/types'; +import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; import { AllCasesModal } from '../../../cases/components/all_cases_modal'; +import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; +import { APP_ID, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; +import { useFullScreen } from '../../../common/containers/use_full_screen'; import { getCaseDetailsUrl, getCreateCaseUrl } from '../../../common/components/link_to'; -import { APP_ID } from '../../../../common/constants'; import { useKibana } from '../../../common/lib/kibana'; import { State } from '../../../common/store'; +import { TimelineId, TimelineType } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; import { timelineDefaults } from '../../store/timeline/defaults'; import { TimelineModel } from '../../store/timeline/model'; +import { isFullScreen } from '../timeline/body/column_headers'; import { NewCase, ExistingCase } from '../timeline/properties/helpers'; import { UNTITLED_TIMELINE } from '../timeline/properties/translations'; import { @@ -28,7 +40,6 @@ import { import { Resolver } from '../../../resolver/view'; import * as i18n from './translations'; -import { TimelineType } from '../../../../common/types/timeline'; const OverlayContainer = styled.div<{ bodyHeight?: number }>` height: ${({ bodyHeight }) => (bodyHeight ? `${bodyHeight}px` : 'auto')}; @@ -41,6 +52,10 @@ const StyledResolver = styled(Resolver)` height: 100%; `; +const FullScreenButtonIcon = styled(EuiButtonIcon)` + margin: 4px 0 4px 0; +`; + interface OwnProps { bodyHeight?: number; graphEventId?: string; @@ -48,6 +63,46 @@ interface OwnProps { timelineType: TimelineType; } +const Navigation = ({ + fullScreen, + globalFullScreen, + onCloseOverlay, + timelineId, + timelineFullScreen, + toggleFullScreen, +}: { + fullScreen: boolean; + globalFullScreen: boolean; + onCloseOverlay: () => void; + timelineId: string; + timelineFullScreen: boolean; + toggleFullScreen: () => void; +}) => ( + + + + {i18n.BACK_TO_EVENTS} + + + + + + + + +); + const GraphOverlayComponent = ({ bodyHeight, graphEventId, @@ -86,17 +141,45 @@ const GraphOverlayComponent = ({ }, [currentTimeline, dispatch, graphEventId, navigateToApp, onCloseCaseModal, timelineId, title] ); + const { + timelineFullScreen, + setTimelineFullScreen, + globalFullScreen, + setGlobalFullScreen, + } = useFullScreen(); + const fullScreen = useMemo( + () => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }), + [globalFullScreen, timelineId, timelineFullScreen] + ); + const toggleFullScreen = useCallback(() => { + if (timelineId === TimelineId.active) { + setTimelineFullScreen(!timelineFullScreen); + } else { + setGlobalFullScreen(!globalFullScreen); + } + }, [ + timelineId, + setTimelineFullScreen, + timelineFullScreen, + setGlobalFullScreen, + globalFullScreen, + ]); return ( - - {i18n.BACK_TO_EVENTS} - + - {timelineType === TimelineType.default && ( + {timelineId === TimelineId.active && timelineType === TimelineType.default && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx index 113c2dca97506..899a6d7486f94 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { TestProviders } from '../../../common/mock'; +import '../../../common/mock/match_media'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { Ja3Fingerprint } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx index 24f8d910b4feb..c2026a71ac6ff 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/netflow/index.test.tsx @@ -10,6 +10,7 @@ import { shallow } from 'enzyme'; import { asArrayIfExists } from '../../../common/lib/helpers'; import { getMockNetflowData } from '../../../common/mock'; +import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock/test_providers'; import { TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 04aef6f07c60a..9899b38f445f9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -7,7 +7,8 @@ /* eslint-disable complexity */ import ApolloClient from 'apollo-client'; -import { getOr, set, isEmpty } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { getOr, isEmpty } from 'lodash/fp'; import { Action } from 'typescript-fsa'; import uuid from 'uuid'; import { Dispatch } from 'redux'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index e2def46b936be..e671244d97b57 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -9,6 +9,7 @@ import { MockedProvider } from 'react-apollo/test-utils'; import React from 'react'; import { wait } from '../../../common/lib/helpers'; +import '../../../common/mock/match_media'; import { TestProviders, apolloClient } from '../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../common/mock/timeline_results'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx index f42914c86f46b..57a6431a06b90 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -10,6 +10,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../common/mock/match_media'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; import { OpenTimelineResult, OpenTimelineProps } from './types'; import { TimelinesTableProps } from './timelines_table'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index 1d08f0296ce0d..12df17ceba666 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -10,6 +10,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../../common/mock/match_media'; import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines_page'; import { OpenTimelineResult, OpenTimelineProps } from '../types'; import { TimelinesTableProps } from '../timelines_table'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx index 9bec06e5ed917..eddfdf6e01df2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -11,6 +11,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../../common/mock/match_media'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; import { TimelinesTableProps } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx index 112329ac1738d..b8b2630e09c6e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import '../../../../common/mock/match_media'; import { getEmptyValue } from '../../../../common/components/empty_value'; import { OpenTimelineResult } from '../types'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx index 390ce8c0b6940..0f2b3cdea4eec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx @@ -10,6 +10,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../../common/mock/match_media'; import { getEmptyValue } from '../../../../common/components/empty_value'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx index f1df605c072dd..6e3f0037003b1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx @@ -10,6 +10,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../../common/mock/match_media'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { TimelinesTable, TimelinesTableProps } from '.'; import { OpenTimelineResult } from '../types'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx index f230a964c3c2a..649e38865f907 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/timelines_table/index.test.tsx @@ -10,6 +10,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; +import '../../../../common/mock/match_media'; import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; import { TimelinesTable, TimelinesTableProps } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index a5610cabc1774..13c2b14d26eca 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -1,503 +1,591 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` - - - - - + + + + + + - - - - - - - - - - + ] + } + isSelectAllChecked={false} + onColumnRemoved={[MockFunction]} + onColumnResized={[MockFunction]} + onColumnSorted={[MockFunction]} + onSelectAll={[Function]} + onUpdateColumns={[MockFunction]} + showEventsSelect={false} + showSelectAllCheckbox={false} + sort={ + Object { + "columnId": "fooColumn", + "sortDirection": "desc", + } + } + timelineId="test" + toggleColumn={[MockFunction]} + /> + + + + + + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts index 588f407416803..21e135218c871 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/helpers.test.ts @@ -9,9 +9,10 @@ import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, DEFAULT_ACTIONS_COLUMN_WIDTH, + EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, SHOW_CHECK_BOXES_COLUMN_WIDTH, - MINIMUM_ACTIONS_COLUMN_WIDTH, } from '../constants'; +import '../../../../../common/mock/match_media'; describe('helpers', () => { describe('getColumnWidthFromType', () => { @@ -36,12 +37,12 @@ describe('helpers', () => { }); test('returns the events viewer actions column width when isEventViewer is true', () => { - expect(getActionsColumnWidth(true)).toEqual(MINIMUM_ACTIONS_COLUMN_WIDTH); + expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH); }); test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { expect(getActionsColumnWidth(true, true)).toEqual( - MINIMUM_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH + EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx index 6a7734ce3161d..6685ce7d7a018 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../common/mock/match_media'; import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; import { defaultHeaders } from './default_headers'; import { Direction } from '../../../../../graphql/types'; @@ -28,22 +29,24 @@ describe('ColumnHeaders', () => { test('renders correctly against snapshot', () => { const wrapper = shallow( - + + + ); expect(wrapper).toMatchSnapshot(); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index b139aa1a7a9a6..a3e177604fbd4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiCheckbox } from '@elastic/eui'; +import { EuiButtonIcon, EuiCheckbox, EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; @@ -18,6 +18,10 @@ import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix, } from '../../../../../common/components/drag_and_drop/helpers'; +import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations'; +import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../../../common/constants'; +import { useFullScreen } from '../../../../../common/containers/use_full_screen'; +import { TimelineId } from '../../../../../../common/types/timeline'; import { OnColumnRemoved, OnColumnResized, @@ -42,6 +46,8 @@ import { Sort } from '../sort'; import { EventsSelect } from './events_select'; import { ColumnHeader } from './column_header'; +import * as i18n from './translations'; + interface Props { actionsColumnWidth: number; browserFields: BrowserFields; @@ -81,6 +87,18 @@ export const DraggableContainer = React.memo( DraggableContainer.displayName = 'DraggableContainer'; +export const isFullScreen = ({ + globalFullScreen, + timelineId, + timelineFullScreen, +}: { + globalFullScreen: boolean; + timelineId: string; + timelineFullScreen: boolean; +}) => + (timelineId === TimelineId.active && timelineFullScreen) || + (timelineId !== TimelineId.active && globalFullScreen); + /** Renders the timeline header columns */ export const ColumnHeadersComponent = ({ actionsColumnWidth, @@ -101,6 +119,26 @@ export const ColumnHeadersComponent = ({ toggleColumn, }: Props) => { const [draggingIndex, setDraggingIndex] = useState(null); + const { + timelineFullScreen, + setTimelineFullScreen, + globalFullScreen, + setGlobalFullScreen, + } = useFullScreen(); + + const toggleFullScreen = useCallback(() => { + if (timelineId === TimelineId.active) { + setTimelineFullScreen(!timelineFullScreen); + } else { + setGlobalFullScreen(!globalFullScreen); + } + }, [ + timelineId, + setTimelineFullScreen, + timelineFullScreen, + setGlobalFullScreen, + globalFullScreen, + ]); const handleSelectAllChange = useCallback( (event: React.ChangeEvent) => { @@ -165,6 +203,11 @@ export const ColumnHeadersComponent = ({ ] ); + const fullScreen = useMemo( + () => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }), + [globalFullScreen, timelineId, timelineFullScreen] + ); + return ( @@ -206,6 +249,25 @@ export const ColumnHeadersComponent = ({ /> + + + + + + + + {showEventsSelect && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts index becdece2c7612..1ebfa957b654f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/translations.ts @@ -18,6 +18,10 @@ export const FIELD = i18n.translate('xpack.securitySolution.timeline.fieldToolti defaultMessage: 'Field', }); +export const FULL_SCREEN = i18n.translate('xpack.securitySolution.timeline.fullScreenButton', { + defaultMessage: 'Full screen', +}); + export const TYPE = i18n.translate('xpack.securitySolution.timeline.typeTooltip', { defaultMessage: 'Type', }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts index 6b6ae3c3467b5..576dedfc28b1b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/constants.ts @@ -8,12 +8,12 @@ export const MINIMUM_ACTIONS_COLUMN_WIDTH = 50; // px; /** The (fixed) width of the Actions column */ -export const DEFAULT_ACTIONS_COLUMN_WIDTH = 76; // px; +export const DEFAULT_ACTIONS_COLUMN_WIDTH = 24 * 4; // px; /** * The (fixed) width of the Actions column when the timeline body is used as * an events viewer, which has fewer actions than a regular events viewer */ -export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 26; // px; +export const EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH = 24 * 3; // px; /** Additional column width to include when checkboxes are shown **/ export const SHOW_CHECK_BOXES_COLUMN_WIDTH = 24; // px; /** The default minimum width of a column (when a width for the column type is not specified) */ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx index 07ef165a6d911..28a4bf6d8ac51 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../common/mock/match_media'; import { mockTimelineData } from '../../../../../common/mock'; import { defaultHeaders } from '../column_headers/default_headers'; import { columnRenderers } from '../renderers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 344fbb59bbe57..3236482e6bc27 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -248,6 +248,7 @@ const StatefulEventComponent: React.FC = ({ event={detailsData || emptyDetails} forceExpand={!!expanded[event._id] && !loading} id={event._id} + onEventToggled={onToggleExpanded} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts index 317f1ed20119b..067cea175c99b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.ts @@ -128,3 +128,38 @@ export const getInvestigateInResolverAction = ({ dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: eventId })), width: DEFAULT_ICON_BUTTON_WIDTH, }); + +/** + * The minimum height of a timeline-based events viewer body, as seen in several + * views, e.g. `Detections`, `Events`, `External events`, etc + */ +export const MIN_EVENTS_VIEWER_BODY_HEIGHT = 500; // px + +interface GetEventsViewerBodyHeightParams { + /** the height of the header, e.g. the section containing "`Showing n event / alerts`, and `Open` / `In progress` / `Closed` filters" */ + headerHeight: number; + /** the height of the footer, e.g. "`25 of 100 events / alerts`, `Load More`, `Updated n minutes ago`" */ + footerHeight: number; + /** the height of the global Kibana chrome, common throughout the app */ + kibanaChromeHeight: number; + /** the (combined) height of other non-events viewer content, e.g. the global search / filter bar in full screen mode */ + otherContentHeight: number; + /** the full height of the window */ + windowHeight: number; +} + +export const getEventsViewerBodyHeight = ({ + footerHeight, + headerHeight, + kibanaChromeHeight, + otherContentHeight, + windowHeight, +}: GetEventsViewerBodyHeightParams) => { + if (windowHeight === 0 || !isFinite(windowHeight)) { + return MIN_EVENTS_VIEWER_BODY_HEIGHT; + } + + const combinedHeights = kibanaChromeHeight + otherContentHeight + headerHeight + footerHeight; + + return Math.max(MIN_EVENTS_VIEWER_BODY_HEIGHT, windowHeight - combinedHeights); +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index 2df6a39f1a3df..b36f1dcc03261 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -7,6 +7,7 @@ import { ReactWrapper } from '@elastic/eui/node_modules/@types/enzyme'; import React from 'react'; import { useSelector } from 'react-redux'; +import '../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { Direction } from '../../../../graphql/types'; import { defaultHeaders, mockTimelineData, mockTimelineModel } from '../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx index 83e44b77802b7..e971dc6c8e1e2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.tsx @@ -29,11 +29,9 @@ import { Events } from './events'; import { ColumnRenderer } from './renderers/column_renderer'; import { RowRenderer } from './renderers/row_renderer'; import { Sort } from './sort'; -import { useManageTimeline } from '../../manage_timeline'; import { GraphOverlay } from '../../graph_overlay'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../helpers'; -import { TimelineRowAction } from './actions'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineId, TimelineType } from '../../../../../common/types/timeline'; export interface BodyProps { addNoteToEvent: AddNoteToEvent; @@ -70,6 +68,11 @@ export interface BodyProps { updateNote: UpdateNote; } +export const hasAdditonalActions = (id: string): boolean => + id === TimelineId.detectionsPage || id === TimelineId.detectionsRulesDetailsPage; + +const EXTRA_WIDTH = 4; // px + /** Renders the timeline body */ export const Body = React.memo( ({ @@ -107,39 +110,14 @@ export const Body = React.memo( updateNote, }) => { const containerElementRef = useRef(null); - const { getManageTimelineById } = useManageTimeline(); - const timelineActions = useMemo( - () => - data.reduce((acc: TimelineRowAction[], rowData) => { - const rowActions = getManageTimelineById(id).timelineRowActions({ - ecsData: rowData.ecs, - nonEcsData: rowData.data, - }); - return rowActions && - rowActions.filter((v) => v.displayType === 'icon').length > - acc.filter((v) => v.displayType === 'icon').length - ? rowActions - : acc; - }, []), - [data, getManageTimelineById, id] - ); - - const additionalActionWidth = useMemo(() => { - let hasContextMenu = false; - return ( - timelineActions.reduce((acc, v) => { - if (v.displayType === 'icon') { - return acc + (v.width ?? 0); - } - const addWidth = hasContextMenu ? 0 : DEFAULT_ICON_BUTTON_WIDTH; - hasContextMenu = true; - return acc + addWidth; - }, 0) ?? 0 - ); - }, [timelineActions]); const actionsColumnWidth = useMemo( - () => getActionsColumnWidth(isEventViewer, showCheckboxes, additionalActionWidth), - [isEventViewer, showCheckboxes, additionalActionWidth] + () => + getActionsColumnWidth( + isEventViewer, + showCheckboxes, + hasAdditonalActions(id) ? DEFAULT_ICON_BUTTON_WIDTH + EXTRA_WIDTH : 0 + ), + [isEventViewer, showCheckboxes, id] ); const columnWidths = useMemo( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx index e7e7d1d47f478..d1e8c8aacca47 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/args.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../common/mock/match_media'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { TestProviders } from '../../../../../common/mock'; import { ArgsComponent } from './args'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx index b4c95d383593a..726273bc90ad8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx index 0990280879a14..750fbc0014464 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx index 41e35427ae254..54af8c89b15d7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock'; import { PrimarySecondaryUserInfo, nilOrUnSet } from './primary_secondary_user_info'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx index d1e67c25bd79c..ef3e2f72d0473 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx @@ -8,6 +8,7 @@ import { EuiFlexItem } from '@elastic/eui'; import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock'; import { SessionUserHostWorkingDir } from './session_user_host_working_dir'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx index 0160c62ea40ac..4a0eff1ecf1b2 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock'; import { PreferenceFormattedBytes } from '../../../../../../common/components/formatted_bytes'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx index ba77709459c28..e2dff4e13b80d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx @@ -11,6 +11,7 @@ import React from 'react'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockEndgameDnsRequest } from '../../../../../../common/mock/mock_endgame_ecs_data'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx index 1d46e4c3eb02d..de3eb01612b2a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { TestProviders } from '../../../../../../common/mock'; - +import '../../../../../../common/mock/match_media'; import { DnsRequestEventDetailsLine } from './dns_request_event_details_line'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx index 1c7eaef893651..6c9dd5092e7c1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { TimelineNonEcsData } from '../../../../../graphql/types'; import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../../common/mock'; +import '../../../../../common/mock/match_media'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { getEmptyValue } from '../../../../../common/components/empty_value'; import { deleteItemIdx, findItem } from './helpers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx index e84cb93b87178..47064fa02458a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx @@ -11,6 +11,7 @@ import React from 'react'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx index b2b4b021e5db5..6d4b2b518b582 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { TestProviders } from '../../../../../../common/mock'; +import '../../../../../../common/mock/match_media'; import { EndgameSecurityEventDetailsLine } from './endgame_security_event_details_line'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx index 4471c26ef8fd7..98a706d5836a0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { TestProviders } from '../../../../../common/mock'; +import '../../../../../common/mock/match_media'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { ExitCodeDraggable } from './exit_code_draggable'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx index 70e0e74675cd2..a038ceab15b44 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { FileDraggable } from './file_draggable'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx index 3e055682d27a4..867cf42146485 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { get } from 'lodash/fp'; import React from 'react'; +import '../../../../../common/mock/match_media'; import { mockTimelineData, TestProviders } from '../../../../../common/mock'; import { getEmptyValue } from '../../../../../common/components/empty_value'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx index 12b093bd517c8..d1ed5e86e72e5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import '../../../../../common/mock/match_media'; import { TimelineNonEcsData } from '../../../../../graphql/types'; import { mockTimelineData } from '../../../../../common/mock'; import { TestProviders } from '../../../../../common/mock/test_providers'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 0b3ea0ce6e430..0c7fbd08ba98c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; +import '../../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../graphql/types'; import { mockTimelineData } from '../../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx index 85a000bbcaf63..2dadbabd0ae16 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../common/mock/match_media'; import { mockTimelineData, TestProviders } from '../../../../../common/mock'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { HostWorkingDir } from './host_working_dir'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx index 5140b9abc60ef..8a8b40198bdba 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../graphql/types'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx index 0a173f766ae19..86d39da478c6d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import '../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../common/mock'; import { ParentProcessDraggable } from './parent_process_draggable'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx index b7c2cb7032cc2..9199278c57f7a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import '../../../../../common/mock/match_media'; import { TimelineNonEcsData } from '../../../../../graphql/types'; import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../../common/mock'; import { getEmptyValue } from '../../../../../common/components/empty_value'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx index 91ae94940f7f4..7a7715c86b5c5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../../../common/mock'; +import '../../../../../common/mock/match_media'; import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx index 55cc61edb064e..e46a5abc6a9fd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/process_hash.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { TestProviders } from '../../../../../common/mock'; +import '../../../../../common/mock/match_media'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { ProcessHash } from './process_hash'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 14f147c61fca3..3b9752224e2c1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData } from '../../../../../../common/mock'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock/test_providers'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { SuricataDetails } from './suricata_details'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index d36d24f41224c..7d700732a6409 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../graphql/types'; import { mockTimelineData } from '../../../../../../common/mock'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock/test_providers'; import { suricataRowRenderer } from './suricata_row_renderer'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx index a0cad2b059a4b..61e1a28cc7d7d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.test.tsx index 4e4e1a0b7bf6f..791ae8aadc69c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/auth_ssh.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { AuthSsh } from './auth_ssh'; describe('AuthSsh', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx index 8efd8e1944331..2f2fe2606d132 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx index 6c7a74d840d01..52c232f377f79 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { BrowserFields } from '../../../../../../common/containers/source'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import '../../../../../../common/mock/match_media'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { SystemGenericFileDetails, SystemGenericFileLine } from './generic_file_details'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx index 56f9452ba40b8..36b69790726e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/system/package.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { Package } from './package'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx index 7f460d30d709c..d09837e344d7b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../../../../common/mock'; +import '../../../../../common/mock/match_media'; import { UserHostWorkingDir } from './user_host_working_dir'; import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index 04b0e6e5fcfae..434be7b23aeee 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -6,6 +6,7 @@ import React from 'react'; +import '../../../../../../common/mock/match_media'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index 2eed6aaf20335..23c38f83b89d4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -11,6 +11,7 @@ import React from 'react'; import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { Ecs } from '../../../../../../graphql/types'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import '../../../../../../common/mock/match_media'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { zeekRowRenderer } from './zeek_row_renderer'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index a0c5b3a8e8c65..3b1ce431bfc87 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -8,6 +8,7 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; +import '../../../../../../common/mock/match_media'; import { Ecs } from '../../../../../../graphql/types'; import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap index 5674c18010f67..ebe6bfcbc2e9a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap @@ -1,8 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SortIndicator rendering renders correctly against snapshot 1`] = ` - + + + `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx index 1467813eaf154..dcaedb90e7252 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx @@ -8,6 +8,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { Direction } from '../../../../../graphql/types'; +import * as i18n from '../translations'; import { getDirection, SortIndicator } from './sort_indicator'; @@ -18,13 +19,29 @@ describe('SortIndicator', () => { expect(wrapper).toMatchSnapshot(); }); - test('it renders the sort indicator', () => { + test('it renders the expected sort indicator when direction is ascending', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( + 'sortUp' + ); + }); + + test('it renders the expected sort indicator when direction is descending', () => { const wrapper = mount(); expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( 'sortDown' ); }); + + test('it renders the expected sort indicator when direction is `none`', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="sortIndicator"]').first().prop('type')).toEqual( + 'empty' + ); + }); }); describe('getDirection', () => { @@ -40,4 +57,28 @@ describe('SortIndicator', () => { expect(getDirection('none')).toEqual(undefined); }); }); + + describe('sort indicator tooltip', () => { + test('it returns the expected tooltip when the direction is ascending', () => { + const wrapper = mount(); + + expect( + wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content + ).toEqual(i18n.SORTED_ASCENDING); + }); + + test('it returns the expected tooltip when the direction is descending', () => { + const wrapper = mount(); + + expect( + wrapper.find('[data-test-subj="sort-indicator-tooltip"]').first().props().content + ).toEqual(i18n.SORTED_DESCENDING); + }); + + test('it does NOT render a tooltip when sort direction is `none`', () => { + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="sort-indicator-tooltip"]').exists()).toBe(false); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx index c148e2f6c6295..8b842dfa2197e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/sort/sort_indicator.tsx @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiIcon } from '@elastic/eui'; +import { EuiIcon, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { Direction } from '../../../../../graphql/types'; +import * as i18n from '../translations'; import { SortDirection } from '.'; @@ -37,8 +38,25 @@ interface Props { } /** Renders a sort indicator */ -export const SortIndicator = React.memo(({ sortDirection }) => ( - -)); +export const SortIndicator = React.memo(({ sortDirection }) => { + const direction = getDirection(sortDirection); + + if (direction != null) { + return ( + + + + ); + } else { + return ; + } +}); SortIndicator.displayName = 'SortIndicator'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts index 20467af290b19..c57002023b79d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts @@ -45,6 +45,20 @@ export const PINNED_WITH_NOTES = i18n.translate( } ); +export const SORTED_ASCENDING = i18n.translate( + 'xpack.securitySolution.timeline.body.sort.sortedAscendingTooltip', + { + defaultMessage: 'Sorted ascending', + } +); + +export const SORTED_DESCENDING = i18n.translate( + 'xpack.securitySolution.timeline.body.sort.sortedDescendingTooltip', + { + defaultMessage: 'Sorted descending', + } +); + export const DISABLE_PIN = i18n.translate( 'xpack.securitySolution.timeline.body.pinning.disablePinnnedTooltip', { @@ -66,6 +80,13 @@ export const COLLAPSE = i18n.translate( } ); +export const COLLAPSE_EVENT = i18n.translate( + 'xpack.securitySolution.timeline.body.actions.collapseEventTooltip', + { + defaultMessage: 'Collapse event', + } +); + export const ACTION_INVESTIGATE_IN_RESOLVER = i18n.translate( 'xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index b08c6afcaf4a6..269cd14b5973c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -34,6 +34,7 @@ interface Props { event: DetailItem[]; forceExpand?: boolean; hideExpandButton?: boolean; + onEventToggled: () => void; onUpdateColumns: OnUpdateColumns; timelineId: string; toggleColumn: (column: ColumnHeaderOptions) => void; @@ -48,6 +49,7 @@ export const ExpandableEvent = React.memo( id, timelineId, toggleColumn, + onEventToggled, onUpdateColumns, }) => ( @@ -59,6 +61,7 @@ export const ExpandableEvent = React.memo( columnHeaders={columnHeaders} data={event} id={id} + onEventToggled={onEventToggled} onUpdateColumns={onUpdateColumns} timelineId={timelineId} toggleColumn={toggleColumn} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index ce96e4e50dea0..8b75f8b398ac1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -10,6 +10,7 @@ import { MockedProvider } from 'react-apollo/test-utils'; import { act } from 'react-dom/test-utils'; import useResizeObserver from 'use-resize-observer/polyfilled'; +import '../../../common/mock/match_media'; import { useSignalIndex, ReturnSignalIndex, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx index ce99304c676ee..efb19275336db 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/index.test.tsx @@ -16,6 +16,7 @@ import { TestProviders, kibanaObservable, } from '../../../../common/mock'; +import '../../../../common/mock/match_media'; import { createStore, State } from '../../../../common/store'; import { useThrottledResizeObserver } from '../../../../common/components/utils'; import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index 68a3362b721d8..8f548f16cf1d6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -3,10 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; import { shallow } from 'enzyme'; import { TimelineType } from '../../../../../common/types/timeline'; +import { TestProviders } from '../../../../common/mock'; import { useCreateTimelineButton } from './use_create_timeline'; jest.mock('react-redux', () => { @@ -20,11 +22,15 @@ jest.mock('react-redux', () => { describe('useCreateTimelineButton', () => { const mockId = 'mockId'; const timelineType = TimelineType.default; + const wrapperContainer: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( + {children} + ); test('return getButton', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCreateTimelineButton({ timelineId: mockId, timelineType }) + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } ); await waitForNextUpdate(); @@ -34,8 +40,9 @@ describe('useCreateTimelineButton', () => { test('getButton renders correct outline - EuiButton', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCreateTimelineButton({ timelineId: mockId, timelineType }) + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } ); await waitForNextUpdate(); @@ -47,8 +54,9 @@ describe('useCreateTimelineButton', () => { test('getButton renders correct outline - EuiButtonEmpty', async () => { await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useCreateTimelineButton({ timelineId: mockId, timelineType }) + const { result, waitForNextUpdate } = renderHook( + () => useCreateTimelineButton({ timelineId: mockId, timelineType }), + { wrapper: wrapperContainer } ); await waitForNextUpdate(); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index fb05b056cdf82..f418491ac4e47 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -8,7 +8,12 @@ import { useDispatch } from 'react-redux'; import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { timelineActions } from '../../../store/timeline'; -import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline'; +import { useFullScreen } from '../../../../common/containers/use_full_screen'; +import { + TimelineId, + TimelineType, + TimelineTypeLiteral, +} from '../../../../../common/types/timeline'; export const useCreateTimelineButton = ({ timelineId, @@ -20,9 +25,14 @@ export const useCreateTimelineButton = ({ closeGearMenu?: () => void; }) => { const dispatch = useDispatch(); + const { timelineFullScreen, setTimelineFullScreen } = useFullScreen(); const createTimeline = useCallback( - ({ id, show }) => + ({ id, show }) => { + if (id === TimelineId.active && timelineFullScreen) { + setTimelineFullScreen(false); + } + dispatch( timelineActions.createTimeline({ id, @@ -30,8 +40,9 @@ export const useCreateTimelineButton = ({ show, timelineType, }) - ), - [dispatch, timelineType] + ); + }, + [dispatch, setTimelineFullScreen, timelineFullScreen, timelineType] ); const handleButtonClick = useCallback(() => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx index 58c46af5606f4..555b22eff0c91 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/timeline.test.tsx @@ -13,6 +13,7 @@ import { timelineQuery } from '../../containers/index.gql_query'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { Direction } from '../../../graphql/types'; import { defaultHeaders, mockTimelineData, mockIndexPattern } from '../../../common/mock'; +import '../../../common/mock/match_media'; import { TestProviders } from '../../../common/mock/test_providers'; import { diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index bd1fac9b05474..1e0e85d4a48d9 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import '../../../common/mock/match_media'; import { mockGlobalState, SUB_PLUGINS_REDUCER, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 0197ccc7eec05..55451882d96fa 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { cloneDeep, set } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { cloneDeep } from 'lodash/fp'; import { TimelineType, TimelineStatus } from '../../../../common/types/timeline'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json index 6d2f198c9b943..396803086552e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/elastic_endpoint.json @@ -4,6 +4,13 @@ ], "description": "Generates a detection alert each time an Elastic Endpoint alert is received. Enabling this rule allows you to immediately begin investigating your Elastic Endpoint alerts.", "enabled": true, + "exceptions_list": [ + { + "id": "endpoint_list", + "namespace_type": "agnostic", + "type": "endpoint" + } + ], "from": "now-10m", "index": [ "logs-endpoint.alerts-*" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts index 880caca03cb7d..f2e2137eec41b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -205,6 +205,11 @@ import rule193 from './privilege_escalation_root_login_without_mfa.json'; import rule194 from './privilege_escalation_updateassumerolepolicy.json'; import rule195 from './elastic_endpoint.json'; import rule196 from './external_alerts.json'; +import rule197 from './ml_cloudtrail_error_message_spike.json'; +import rule198 from './ml_cloudtrail_rare_error_code.json'; +import rule199 from './ml_cloudtrail_rare_method_by_city.json'; +import rule200 from './ml_cloudtrail_rare_method_by_country.json'; +import rule201 from './ml_cloudtrail_rare_method_by_user.json'; export const rawRules = [ rule1, @@ -403,4 +408,9 @@ export const rawRules = [ rule194, rule195, rule196, + rule197, + rule198, + rule199, + rule200, + rule201, ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_error_message_spike.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_error_message_spike.json new file mode 100644 index 0000000000000..0730c421cf5f2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_error_message_spike.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 50, + "author": [ + "Elastic" + ], + "description": "A machine learning job detected a significant spike in the rate of a particular error in the CloudTrail messages. Spikes in error messages may accompany attempts at privilege escalation, lateral movement, or discovery.", + "false_positives": [ + "Spikes in error message activity can also be due to bugs in cloud automation scripts or workflows; changes to cloud automation scripts or workflows; adoption of new services; changes in the way services are used; or changes to IAM privileges." + ], + "from": "now-60m", + "interval": "15m", + "license": "Elastic License", + "machine_learning_job_id": "high_distinct_count_error_message", + "name": "Spike in AWS Error Messages", + "note": "### Investigating Spikes in CloudTrail Errors ###\nDetection alerts from this rule indicate a large spike in the number of CloudTrail log messages that contain a particular error message. The error message in question was associated with the response to an AWS API command or method call. Here are some possible avenues of investigation:\n- Examine the history of the error. Has it manifested before? If the error, which is visible in the `aws.cloudtrail.error_message` field, manifested only very recently, it might be related to recent changes in an automation module or script.\n- Examine the request parameters. These may provide indications as to the nature of the task being performed when the error occurred. Is the error related to unsuccessful attempts to enumerate or access objects, data or secrets? If so, this can sometimes be a byproduct of discovery, privilege escalation or lateral movement attempts.\n- Consider the user as identified by the user.name field. Is this activity part of an expected workflow for the user context? Examine the user identity in the `aws.cloudtrail.user_identity.arn` field and the access key id in the `aws.cloudtrail.user_identity.access_key_id` field which can help identify the precise user context. The user agent details in the `user_agent.original` field may also indicate what kind of a client made the request.\n- Consider the source IP address and geolocation for the calling user who issued the command. Do they look normal for the calling user? If the source is an EC2 IP address, is it associated with an EC2 instance in one of your accounts or could it be sourcing from an EC2 instance not under your control? If it is an authorized EC2 instance, is the activity associated with normal behavior for the instance role or roles? Are there any other alerts or signs of suspicious activity involving this instance?", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "78d3d8d9-b476-451d-a9e0-7a5addd70670", + "severity": "low", + "tags": [ + "AWS", + "Elastic", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_error_code.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_error_code.json new file mode 100644 index 0000000000000..8003cdd7504c7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_error_code.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 50, + "author": [ + "Elastic" + ], + "description": "A machine learning job detected an unusual error in a CloudTrail message. These can be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection.", + "false_positives": [ + "Rare and unusual errors may indicate an impending service failure state. Rare and unusual user error activity can also be due to manual troubleshooting or reconfiguration attempts by insufficiently privileged users, bugs in cloud automation scripts or workflows, or changes to IAM privileges." + ], + "from": "now-60m", + "interval": "15m", + "license": "Elastic License", + "machine_learning_job_id": "rare_error_code", + "name": "Rare AWS Error Code", + "note": "### Investigating Unusual CloudTrail Error Activity ###\nDetection alerts from this rule indicate a rare and unusual error code that was associated with the response to an AWS API command or method call. Here are some possible avenues of investigation:\n- Examine the history of the error. Has it manifested before? If the error, which is visible in the `aws.cloudtrail.error_code field`, manifested only very recently, it might be related to recent changes in an automation module or script.\n- Examine the request parameters. These may provide indications as to the nature of the task being performed when the error occurred. Is the error related to unsuccessful attempts to enumerate or access objects, data, or secrets? If so, this can sometimes be a byproduct of discovery, privilege escalation, or lateral movement attempts.\n- Consider the user as identified by the `user.name` field. Is this activity part of an expected workflow for the user context? Examine the user identity in the `aws.cloudtrail.user_identity.arn` field and the access key id in the `aws.cloudtrail.user_identity.access_key_id` field which can help identify the precise user context. The user agent details in the `user_agent.original` field may also indicate what kind of a client made the request.\n- Consider the source IP address and geolocation for the calling user who issued the command. Do they look normal for the calling user? If the source is an EC2 IP address, is it associated with an EC2 instance in one of your accounts or could it be sourcing from an EC2 instance not under your control? If it is an authorized EC2 instance, is the activity associated with normal behavior for the instance role or roles? Are there any other alerts or signs of suspicious activity involving this instance?", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "19de8096-e2b0-4bd8-80c9-34a820813fff", + "severity": "low", + "tags": [ + "AWS", + "Elastic", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_city.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_city.json new file mode 100644 index 0000000000000..2c54dbd03daba --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_city.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 50, + "author": [ + "Elastic" + ], + "description": "A machine learning job detected AWS command activity that, while not inherently suspicious or abnormal, is sourcing from a geolocation (city) that is unusual for the command. This can be the result of compromised credentials or keys being used by a threat actor in a different geography then the authorized user(s).", + "false_positives": [ + "New or unusual command and user geolocation activity can be due to manual troubleshooting or reconfiguration; changes in cloud automation scripts or workflows; adoption of new services; expansion into new regions; increased adoption of work from home policies; or users who travel frequently." + ], + "from": "now-60m", + "interval": "15m", + "license": "Elastic License", + "machine_learning_job_id": "rare_method_for_a_city", + "name": "Unusual City For an AWS Command", + "note": "### Investigating an Unusual CloudTrail Event ###\nDetection alerts from this rule indicate an AWS API command or method call that is rare and unusual for the geolocation of the source IP address. Here are some possible avenues of investigation:\n- Consider the source IP address and geolocation for the calling user who issued the command. Do they look normal for the calling user? If the source is an EC2 IP address, is it associated with an EC2 instance in one of your accounts or could it be sourcing from an EC2 instance not under your control? If it is an authorized EC2 instance, is the activity associated with normal behavior for the instance role or roles? Are there any other alerts or signs of suspicious activity involving this instance?\n- Consider the user as identified by the `user.name` field. Is this command part of an expected workflow for the user context? Examine the user identity in the `aws.cloudtrail.user_identity.arn` field and the access key id in the `aws.cloudtrail.user_identity.access_key_id` field which can help identify the precise user context. The user agent details in the `user_agent.original` field may also indicate what kind of a client made the request.\n- Consider the time of day. If the user is a human, not a program or script, did the activity take place during a normal time of day?\n- Examine the history of the command. If the command, which is visible in the `event.action field`, manifested only very recently, it might be part of a new automation module or script. If it has a consistent cadence - for example, if it appears in small numbers on a weekly or monthly cadence it might be part of a housekeeping or maintenance process.\n- Examine the request parameters. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "809b70d3-e2c3-455e-af1b-2626a5a1a276", + "severity": "low", + "tags": [ + "AWS", + "Elastic", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_country.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_country.json new file mode 100644 index 0000000000000..68cbf4979a933 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_country.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 50, + "author": [ + "Elastic" + ], + "description": "A machine learning job detected AWS command activity that, while not inherently suspicious or abnormal, is sourcing from a geolocation (country) that is unusual for the command. This can be the result of compromised credentials or keys being used by a threat actor in a different geography then the authorized user(s).", + "false_positives": [ + "New or unusual command and user geolocation activity can be due to manual troubleshooting or reconfiguration; changes in cloud automation scripts or workflows; adoption of new services; expansion into new regions; increased adoption of work from home policies; or users who travel frequently." + ], + "from": "now-60m", + "interval": "15m", + "license": "Elastic License", + "machine_learning_job_id": "rare_method_for_a_country", + "name": "Unusual Country For an AWS Command", + "note": "### Investigating an Unusual CloudTrail Event ###\nDetection alerts from this rule indicate an AWS API command or method call that is rare and unusual for the geolocation of the source IP address. Here are some possible avenues of investigation:\n- Consider the source IP address and geolocation for the calling user who issued the command. Do they look normal for the calling user? If the source is an EC2 IP address, is it associated with an EC2 instance in one of your accounts or could it be sourcing from an EC2 instance not under your control? If it is an authorized EC2 instance, is the activity associated with normal behavior for the instance role or roles? Are there any other alerts or signs of suspicious activity involving this instance?\n- Consider the user as identified by the `user.name` field. Is this command part of an expected workflow for the user context? Examine the user identity in the `aws.cloudtrail.user_identity.arn` field and the access key id in the `aws.cloudtrail.user_identity.access_key_id` field which can help identify the precise user context. The user agent details in the `user_agent.original` field may also indicate what kind of a client made the request.\n- Consider the time of day. If the user is a human, not a program or script, did the activity take place during a normal time of day?\n- Examine the history of the command. If the command, which is visible in the `event.action field`, manifested only very recently, it might be part of a new automation module or script. If it has a consistent cadence - for example, if it appears in small numbers on a weekly or monthly cadence it might be part of a housekeeping or maintenance process.\n- Examine the request parameters. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "dca28dee-c999-400f-b640-50a081cc0fd1", + "severity": "low", + "tags": [ + "AWS", + "Elastic", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_user.json new file mode 100644 index 0000000000000..e4ec651e71934 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_user.json @@ -0,0 +1,29 @@ +{ + "anomaly_threshold": 75, + "author": [ + "Elastic" + ], + "description": "A machine learning job detected an AWS API command that, while not inherently suspicious or abnormal, is being made by a user context that does not normally use the command. This can be the result of compromised credentials or keys as someone uses a valid account to persist, move laterally, or exfil data.", + "false_positives": [ + "New or unusual user command activity can be due to manual troubleshooting or reconfiguration; changes in cloud automation scripts or workflows; adoption of new services; or changes in the way services are used." + ], + "from": "now-60m", + "interval": "15m", + "license": "Elastic License", + "machine_learning_job_id": "rare_method_for_a_username", + "name": "Unusual AWS Command for a User", + "note": "### Investigating an Unusual CloudTrail Event ###\nDetection alerts from this rule indicate an AWS API command or method call that is rare and unusual for the calling IAM user. Here are some possible avenues of investigation:\n- Consider the user as identified by the `user.name` field. Is this command part of an expected workflow for the user context? Examine the user identity in the `aws.cloudtrail.user_identity.arn` field and the access key id in the `aws.cloudtrail.user_identity.access_key_id` field which can help identify the precise user context. The user agent details in the `user_agent.original` field may also indicate what kind of a client made the request.\n- Consider the source IP address and geolocation for the calling user who issued the command. Do they look normal for the calling user? If the source is an EC2 IP address, is it associated with an EC2 instance in one of your accounts or could it be sourcing from an EC2 instance not under your control? If it is an authorized EC2 instance, is the activity associated with normal behavior for the instance role or roles? Are there any other alerts or signs of suspicious activity involving this instance?\n- Consider the time of day. If the user is a human, not a program or script, did the activity take place during a normal time of day?\n- Examine the history of the command. If the command, which is visible in the `event.action field`, manifested only very recently, it might be part of a new automation module or script. If it has a consistent cadence - for example, if it appears in small numbers on a weekly or monthly cadence it might be part of a housekeeping or maintenance process.\n- Examine the request parameters. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "references": [ + "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" + ], + "risk_score": 21, + "rule_id": "ac706eae-d5ec-4b14-b4fd-e8ba8086f0e1", + "severity": "low", + "tags": [ + "AWS", + "Elastic", + "ML" + ], + "type": "machine_learning", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_activity.json index 3ef426af909ff..bf86f78fe3e72 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_activity.json @@ -4,15 +4,12 @@ "Elastic" ], "description": "Identifies Linux processes that do not usually use the network but have unexpected network activity, which can indicate command-and-control, lateral movement, persistence, or data exfiltration activity. A process with unusual network activity can denote process exploitation or injection, where the process is used to run persistence mechanisms that allow a malicious actor remote access or control of the host, data exfiltration, and execution of unauthorized network applications.", - "false_positives": [ - "A newly installed program or one that rarely uses the network could trigger this signal." - ], "from": "now-45m", "interval": "15m", "license": "Elastic License", "machine_learning_job_id": "linux_anomalous_network_activity_ecs", "name": "Unusual Linux Network Activity", - "note": "### Investigating Unusual Network Activity ###\nSignals from this rule indicate the presence of network activity from a Linux process for which network activity is rare and unusual. Here are some possible avenues of investigation:\n- Consider the IP addresses and ports. Are these used by normal but infrequent network workflows? Are they expected or unexpected? \n- If the destination IP address is remote or external, does it associate with an expected domain, organization or geography? Note: avoid interacting directly with suspected malicious IP addresses.\n- Consider the user as identified by the username field. Is this network activity part of an expected workflow for the user who ran the program?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business or maintenance process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "note": "### Investigating Unusual Network Activity ###\nDetection alerts from this rule indicate the presence of network activity from a Linux process for which network activity is rare and unusual. Here are some possible avenues of investigation:\n- Consider the IP addresses and ports. Are these used by normal but infrequent network workflows? Are they expected or unexpected? \n- If the destination IP address is remote or external, does it associate with an expected domain, organization or geography? Note: avoid interacting directly with suspected malicious IP addresses.\n- Consider the user as identified by the username field. Is this network activity part of an expected workflow for the user who ran the program?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business or maintenance process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json index add1c2941970e..a588a6f5bcb0a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_port_activity.json @@ -5,7 +5,7 @@ ], "description": "Identifies unusual destination port activity that can indicate command-and-control, persistence mechanism, or data exfiltration activity. Rarely used destination port activity is generally unusual in Linux fleets, and can indicate unauthorized access or threat actor activity.", "false_positives": [ - "A newly installed program or one that rarely uses the network could trigger this signal." + "A newly installed program or one that rarely uses the network could trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_service.json index af5b331f4cb04..5c56845024eb2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_service.json @@ -5,7 +5,7 @@ ], "description": "Identifies unusual listening ports on Linux instances that can indicate execution of unauthorized services, backdoors, or persistence mechanisms.", "false_positives": [ - "A newly installed program or one that rarely uses the network could trigger this signal." + "A newly installed program or one that rarely uses the network could trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_url_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_url_activity.json index 89a6955fd1781..3b3f751dfc60b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_url_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_network_url_activity.json @@ -5,7 +5,7 @@ ], "description": "A machine learning job detected an unusual web URL request from a Linux host, which can indicate malware delivery and execution. Wget and cURL are commonly used by Linux programs to download code and data. Most of the time, their usage is entirely normal. Generally, because they use a list of URLs, they repeatedly download from the same locations. However, Wget and cURL are sometimes used to deliver Linux exploit payloads, and threat actors use these tools to download additional software and code. For these reasons, unusual URLs can indicate unauthorized downloads or threat activity.", "false_positives": [ - "A new and unusual program or artifact download in the course of software upgrades, debugging, or troubleshooting could trigger this signal." + "A new and unusual program or artifact download in the course of software upgrades, debugging, or troubleshooting could trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json index 6e73e4dd6dc94..8475410735f34 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_process_all_hosts.json @@ -5,14 +5,14 @@ ], "description": "Searches for rare processes running on multiple Linux hosts in an entire fleet or network. This reduces the detection of false positives since automated maintenance processes usually only run occasionally on a single machine but are common to all or many hosts in a fleet.", "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this alert." ], "from": "now-45m", "interval": "15m", "license": "Elastic License", "machine_learning_job_id": "linux_anomalous_process_all_hosts_ecs", "name": "Anomalous Process For a Linux Population", - "note": "### Investigating an Unusual Linux Process ###\nSignals from this rule indicate the presence of a Linux process that is rare and unusual for all of the monitored Linux hosts for which Auditbeat data is available. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "note": "### Investigating an Unusual Linux Process ###\nDetection alerts from this rule indicate the presence of a Linux process that is rare and unusual for all of the monitored Linux hosts for which Auditbeat data is available. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json index c910fb552f966..3e4b1f15fdce4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_linux_anomalous_user_name.json @@ -12,7 +12,7 @@ "license": "Elastic License", "machine_learning_job_id": "linux_anomalous_user_name_ecs", "name": "Unusual Linux Username", - "note": "### Investigating an Unusual Linux User ###\nSignals from this rule indicate activity for a Linux user name that is rare and unusual. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host? Could this be related to troubleshooting or debugging activity by a developer or site reliability engineer?\n- Examine the history of user activity. If this user manifested only very recently, it might be a service account for a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks that the user is performing.", + "note": "### Investigating an Unusual Linux User ###\nDetection alerts from this rule indicate activity for a Linux user name that is rare and unusual. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host? Could this be related to troubleshooting or debugging activity by a developer or site reliability engineer?\n- Examine the history of user activity. If this user manifested only very recently, it might be a service account for a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks that the user is performing.", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_dns_tunneling.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_dns_tunneling.json index b78c4d3459b85..1352fde91b59b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_dns_tunneling.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_dns_tunneling.json @@ -5,7 +5,7 @@ ], "description": "A machine learning job detected unusually large numbers of DNS queries for a single top-level DNS domain, which is often used for DNS tunneling. DNS tunneling can be used for command-and-control, persistence, or data exfiltration activity. For example, dnscat tends to generate many DNS questions for a top-level domain as it uses the DNS protocol to tunnel data.", "false_positives": [ - "DNS domains that use large numbers of child domains, such as software or content distribution networks, can trigger this signal and such parent domains can be excluded." + "DNS domains that use large numbers of child domains, such as software or content distribution networks, can trigger this alert and such parent domains can be excluded." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_dns_question.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_dns_question.json index 970962dd75eed..b16e67052a212 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_dns_question.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_dns_question.json @@ -5,7 +5,7 @@ ], "description": "A machine learning job detected a rare and unusual DNS query that indicate network activity with unusual DNS domains. This can be due to initial access, persistence, command-and-control, or exfiltration activity. For example, when a user clicks on a link in a phishing email or opens a malicious document, a request may be sent to download and run a payload from an uncommon domain. When malware is already running, it may send requests to an uncommon DNS domain the malware uses for command-and-control communication.", "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal. Network activity that occurs rarely, in small quantities, can trigger this signal. Possible examples are browsing technical support or vendor networks sparsely. A user who visits a new or unique web destination may trigger this signal." + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this alert. Network activity that occurs rarely, in small quantities, can trigger this alert. Possible examples are browsing technical support or vendor networks sparsely. A user who visits a new or unique web destination may trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_server_domain.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_server_domain.json index f9465a329e973..a8971300fe11b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_server_domain.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_server_domain.json @@ -5,7 +5,7 @@ ], "description": "A machine learning job detected an unusual network destination domain name. This can be due to initial access, persistence, command-and-control, or exfiltration activity. For example, when a user clicks on a link in a phishing email or opens a malicious document, a request may be sent to download and run a payload from an uncommon web server name. When malware is already running, it may send requests to an uncommon DNS domain the malware uses for command-and-control communication.", "false_positives": [ - "Web activity that occurs rarely in small quantities can trigger this signal. Possible examples are browsing technical support or vendor URLs that are used very sparsely. A user who visits a new and unique web destination may trigger this signal when the activity is sparse. Web applications that generate URLs unique to a transaction may trigger this when they are used sparsely. Web domains can be excluded in cases such as these." + "Web activity that occurs rarely in small quantities can trigger this alert. Possible examples are browsing technical support or vendor URLs that are used very sparsely. A user who visits a new and unique web destination may trigger this alert when the activity is sparse. Web applications that generate URLs unique to a transaction may trigger this when they are used sparsely. Web domains can be excluded in cases such as these." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_urls.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_urls.json index e22f9975b54e4..469f5d741ef6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_urls.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_urls.json @@ -5,7 +5,7 @@ ], "description": "A machine learning job detected a rare and unusual URL that indicates unusual web browsing activity. This can be due to initial access, persistence, command-and-control, or exfiltration activity. For example, in a strategic web compromise or watering hole attack, when a trusted website is compromised to target a particular sector or organization, targeted users may receive emails with uncommon URLs for trusted websites. These URLs can be used to download and run a payload. When malware is already running, it may send requests to uncommon URLs on trusted websites the malware uses for command-and-control communication. When rare URLs are observed being requested for a local web server by a remote source, these can be due to web scanning, enumeration or attack traffic, or they can be due to bots and web scrapers which are part of common Internet background traffic.", "false_positives": [ - "Web activity that occurs rarely in small quantities can trigger this signal. Possible examples are browsing technical support or vendor URLs that are used very sparsely. A user who visits a new and unique web destination may trigger this signal when the activity is sparse. Web applications that generate URLs unique to a transaction may trigger this when they are used sparsely. Web domains can be excluded in cases such as these." + "Web activity that occurs rarely in small quantities can trigger this alert. Possible examples are browsing technical support or vendor URLs that are used very sparsely. A user who visits a new and unique web destination may trigger this alert when the activity is sparse. Web applications that generate URLs unique to a transaction may trigger this when they are used sparsely. Web domains can be excluded in cases such as these." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_user_agent.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_user_agent.json index 2ce6f44d90593..ebcf4f987e9de 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_user_agent.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_packetbeat_rare_user_agent.json @@ -5,7 +5,7 @@ ], "description": "A machine learning job detected a rare and unusual user agent indicating web browsing activity by an unusual process other than a web browser. This can be due to persistence, command-and-control, or exfiltration activity. Uncommon user agents coming from remote sources to local destinations are often the result of scanners, bots, and web scrapers, which are part of common Internet background traffic. Much of this is noise, but more targeted attacks on websites using tools like Burp or SQLmap can sometimes be discovered by spotting uncommon user agents. Uncommon user agents in traffic from local sources to remote destinations can be any number of things, including harmless programs like weather monitoring or stock-trading programs. However, uncommon user agents from local sources can also be due to malware or scanning activity.", "false_positives": [ - "Web activity that is uncommon, like security scans, may trigger this signal and may need to be excluded. A new or rarely used program that calls web services may trigger this signal." + "Web activity that is uncommon, like security scans, may trigger this alert and may need to be excluded. A new or rarely used program that calls web services may trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json index c62666134c84e..385158dd6b65d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_linux.json @@ -5,14 +5,14 @@ ], "description": "Identifies rare processes that do not usually run on individual hosts, which can indicate execution of unauthorized services, malware, or persistence mechanisms. Processes are considered rare when they only run occasionally as compared with other processes running on the host.", "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this alert." ], "from": "now-45m", "interval": "15m", "license": "Elastic License", "machine_learning_job_id": "rare_process_by_host_linux_ecs", "name": "Unusual Process For a Linux Host", - "note": "### Investigating an Unusual Linux Process ###\nSignals from this rule indicate the presence of a Linux process that is rare and unusual for the host it ran on. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", + "note": "### Investigating an Unusual Linux Process ###\nDetection alerts from this rule indicate the presence of a Linux process that is rare and unusual for the host it ran on. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json index 5d86637553eab..d0a99b32d4713 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_rare_process_by_host_windows.json @@ -5,14 +5,14 @@ ], "description": "Identifies rare processes that do not usually run on individual hosts, which can indicate execution of unauthorized services, malware, or persistence mechanisms. Processes are considered rare when they only run occasionally as compared with other processes running on the host.", "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this alert." ], "from": "now-45m", "interval": "15m", "license": "Elastic License", "machine_learning_job_id": "rare_process_by_host_windows_ecs", "name": "Unusual Process For a Windows Host", - "note": "### Investigating an Unusual Windows Process ###\nSignals from this rule indicate the presence of a Windows process that is rare and unusual for the host it ran on. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process metadata like the values of the Company, Description and Product fields which may indicate whether the program is associated with an expected software vendor or package. \n- Examine arguments and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", + "note": "### Investigating an Unusual Windows Process ###\nDetection alerts from this rule indicate the presence of a Windows process that is rare and unusual for the host it ran on. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process metadata like the values of the Company, Description and Product fields which may indicate whether the program is associated with an expected software vendor or package. \n- Examine arguments and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_suspicious_login_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_suspicious_login_activity.json index 93413f8d0a8a8..f309debcdffe9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_suspicious_login_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_suspicious_login_activity.json @@ -5,7 +5,7 @@ ], "description": "Identifies an unusually high number of authentication attempts.", "false_positives": [ - "Security audits may trigger this signal. Conditions that generate bursts of failed logins, such as misconfigured applications or account lockouts could trigger this signal." + "Security audits may trigger this alert. Conditions that generate bursts of failed logins, such as misconfigured applications or account lockouts could trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json index a24e1c1c9eb0b..0ab591097f975 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_network_activity.json @@ -5,14 +5,14 @@ ], "description": "Identifies Windows processes that do not usually use the network but have unexpected network activity, which can indicate command-and-control, lateral movement, persistence, or data exfiltration activity. A process with unusual network activity can denote process exploitation or injection, where the process is used to run persistence mechanisms that allow a malicious actor remote access or control of the host, data exfiltration, and execution of unauthorized network applications.", "false_positives": [ - "A newly installed program or one that rarely uses the network could trigger this signal." + "A newly installed program or one that rarely uses the network could trigger this alert." ], "from": "now-45m", "interval": "15m", "license": "Elastic License", "machine_learning_job_id": "windows_anomalous_network_activity_ecs", "name": "Unusual Windows Network Activity", - "note": "### Investigating Unusual Network Activity ###\nSignals from this rule indicate the presence of network activity from a Windows process for which network activity is very unusual. Here are some possible avenues of investigation:\n- Consider the IP addresses, protocol and ports. Are these used by normal but infrequent network workflows? Are they expected or unexpected? \n- If the destination IP address is remote or external, does it associate with an expected domain, organization or geography? Note: avoid interacting directly with suspected malicious IP addresses.\n- Consider the user as identified by the username field. Is this network activity part of an expected workflow for the user who ran the program?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools.", + "note": "### Investigating Unusual Network Activity ###\nDetection alerts from this rule indicate the presence of network activity from a Windows process for which network activity is very unusual. Here are some possible avenues of investigation:\n- Consider the IP addresses, protocol and ports. Are these used by normal but infrequent network workflows? Are they expected or unexpected? \n- If the destination IP address is remote or external, does it associate with an expected domain, organization or geography? Note: avoid interacting directly with suspected malicious IP addresses.\n- Consider the user as identified by the username field. Is this network activity part of an expected workflow for the user who ran the program?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools.", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json index 9be69a6bfdcbe..a7b309e6d7fcd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_path_activity.json @@ -5,7 +5,7 @@ ], "description": "Identifies processes started from atypical folders in the file system, which might indicate malware execution or persistence mechanisms. In corporate Windows environments, software installation is centrally managed and it is unusual for programs to be executed from user or temporary directories. Processes executed from these locations can denote that a user downloaded software directly from the Internet or a malicious script or macro executed malware.", "false_positives": [ - "A new and unusual program or artifact download in the course of software upgrades, debugging, or troubleshooting could trigger this signal. Users downloading and running programs from unusual locations, such as temporary directories, browser caches, or profile paths could trigger this signal." + "A new and unusual program or artifact download in the course of software upgrades, debugging, or troubleshooting could trigger this alert. Users downloading and running programs from unusual locations, such as temporary directories, browser caches, or profile paths could trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json index 79792d2fd328b..bc6346f457b65 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_all_hosts.json @@ -5,14 +5,14 @@ ], "description": "Searches for rare processes running on multiple hosts in an entire fleet or network. This reduces the detection of false positives since automated maintenance processes usually only run occasionally on a single machine but are common to all or many hosts in a fleet.", "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this alert." ], "from": "now-45m", "interval": "15m", "license": "Elastic License", "machine_learning_job_id": "windows_anomalous_process_all_hosts_ecs", "name": "Anomalous Process For a Windows Population", - "note": "### Investigating an Unusual Windows Process ###\nSignals from this rule indicate the presence of a Windows process that is rare and unusual for all of the Windows hosts for which Winlogbeat data is available. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process metadata like the values of the Company, Description and Product fields which may indicate whether the program is associated with an expected software vendor or package. \n- Examine arguments and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", + "note": "### Investigating an Unusual Windows Process ###\nDetection alerts from this rule indicate the presence of a Windows process that is rare and unusual for all of the Windows hosts for which Winlogbeat data is available. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host?\n- Examine the history of execution. If this process manifested only very recently, it might be part of a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process metadata like the values of the Company, Description and Product fields which may indicate whether the program is associated with an expected software vendor or package.\n- Examine arguments and working directory. These may provide indications as to the source of the program or the nature of the tasks it is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.\n- If you have file hash values in the event data, and you suspect malware, you can optionally run a search for the file hash to see if the file is identified as malware by anti-malware tools. ", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json index c031e7177abe6..97351a1f517b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_process_creation.json @@ -5,7 +5,7 @@ ], "description": "Identifies unusual parent-child process relationships that can indicate malware execution or persistence mechanisms. Malicious scripts often call on other applications and processes as part of their exploit payload. For example, when a malicious Office document runs scripts as part of an exploit payload, Excel or Word may start a script interpreter process, which, in turn, runs a script that downloads and executes malware. Another common scenario is Outlook running an unusual process when malware is downloaded in an email. Monitoring and identifying anomalous process relationships is a method of detecting new and emerging malware that is not yet recognized by anti-virus scanners.", "false_positives": [ - "Users running scripts in the course of technical support operations of software upgrades could trigger this signal. A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + "Users running scripts in the course of technical support operations of software upgrades could trigger this alert. A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_script.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_script.json index 7d05a0286ea97..d0dc8d7e40fa2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_script.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_script.json @@ -5,7 +5,7 @@ ], "description": "A machine learning job detected a PowerShell script with unusual data characteristics, such as obfuscation, that may be a characteristic of malicious PowerShell script text blocks.", "false_positives": [ - "Certain kinds of security testing may trigger this signal. PowerShell scripts that use high levels of obfuscation or have unusual script block payloads may trigger this signal." + "Certain kinds of security testing may trigger this alert. PowerShell scripts that use high levels of obfuscation or have unusual script block payloads may trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_service.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_service.json index 7870f75b3d075..b7e7a0357e118 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_service.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_service.json @@ -5,7 +5,7 @@ ], "description": "A machine learning job detected an unusual Windows service, This can indicate execution of unauthorized services, malware, or persistence mechanisms. In corporate Windows environments, hosts do not generally run many rare or unique services. This job helps detect malware and persistence mechanisms that have been installed and run as a service.", "false_positives": [ - "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this signal." + "A newly installed program or one that runs rarely as part of a monthly or quarterly workflow could trigger this alert." ], "from": "now-45m", "interval": "15m", diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json index 42e6740beaa0c..26bd6837cbde5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_anomalous_user_name.json @@ -12,7 +12,7 @@ "license": "Elastic License", "machine_learning_job_id": "windows_anomalous_user_name_ecs", "name": "Unusual Windows Username", - "note": "### Investigating an Unusual Windows User ###\nSignals from this rule indicate activity for a Windows user name that is rare and unusual. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host? Could this be related to occasional troubleshooting or support activity?\n- Examine the history of user activity. If this user manifested only very recently, it might be a service account for a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks that the user is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.", + "note": "### Investigating an Unusual Windows User ###\nDetection alerts from this rule indicate activity for a Windows user name that is rare and unusual. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is this program part of an expected workflow for the user who ran this program on this host? Could this be related to occasional troubleshooting or support activity?\n- Examine the history of user activity. If this user manifested only very recently, it might be a service account for a new software package. If it has a consistent cadence - for example if it runs monthly or quarterly - it might be part of a monthly or quarterly business process.\n- Examine the process arguments, title and working directory. These may provide indications as to the source of the program or the nature of the tasks that the user is performing.\n- Consider the same for the parent process. If the parent process is a legitimate system utility or service, this could be related to software updates or system management. If the parent process is something user-facing like an Office application, this process could be more suspicious.", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_type10_remote_login.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_type10_remote_login.json index 2043af2b8dcb4..b69e759120ce4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_type10_remote_login.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_windows_rare_user_type10_remote_login.json @@ -12,7 +12,7 @@ "license": "Elastic License", "machine_learning_job_id": "windows_rare_user_type10_remote_login", "name": "Unusual Windows Remote User", - "note": "### Investigating an Unusual Windows User ###\nSignals from this rule indicate activity for a rare and unusual Windows RDP (remote desktop) user. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is the user part of a group who normally logs into Windows hosts using RDP (remote desktop protocol)? Is this logon activity part of an expected workflow for the user? \n- Consider the source of the login. If the source is remote, could this be related to occasional troubleshooting or support activity by a vendor or an employee working remotely?", + "note": "### Investigating an Unusual Windows User ###\nDetection alerts from this rule indicate activity for a rare and unusual Windows RDP (remote desktop) user. Here are some possible avenues of investigation:\n- Consider the user as identified by the username field. Is the user part of a group who normally logs into Windows hosts using RDP (remote desktop protocol)? Is this logon activity part of an expected workflow for the user? \n- Consider the source of the login. If the source is remote, could this be related to occasional troubleshooting or support activity by a vendor or an employee working remotely?", "references": [ "https://www.elastic.co/guide/en/security/current/prebuilt-ml-jobs.html" ], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts index 8af08a02f4152..654ace290f85f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts @@ -14,6 +14,7 @@ import { EntryList, ExceptionListItemSchema, } from '../../../../../lists/common/schemas'; +import { hasLargeValueList } from '../../../../common/detection_engine/utils'; interface FilterEventsAgainstList { listClient: ListClient; @@ -36,11 +37,28 @@ export const filterEventsAgainstList = async ({ return eventSearchResult; } + const exceptionItemsWithLargeValueLists = exceptionsList.reduce( + (acc, exception) => { + const { entries } = exception; + if (hasLargeValueList(entries)) { + return [...acc, exception]; + } + + return acc; + }, + [] + ); + + if (exceptionItemsWithLargeValueLists.length === 0) { + logger.debug(buildRuleMessage('about to return original search result')); + return eventSearchResult; + } + // narrow unioned type to be single const isStringableType = (val: SearchTypes) => ['string', 'number', 'boolean'].includes(typeof val); // grab the signals with values found in the given exception lists. - const filteredHitsPromises = exceptionsList.map( + const filteredHitsPromises = exceptionItemsWithLargeValueLists.map( async (exceptionItem: ExceptionListItemSchema) => { const { entries } = exceptionItem; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts index f34879781e0b0..a5740d7719f47 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/get_filter.test.ts @@ -192,71 +192,66 @@ describe('get_filter', () => { index: ['auditbeat-*'], lists: [getExceptionListItemSchemaMock()], }); + expect(filter).toEqual({ bool: { + must: [], filter: [ { bool: { - filter: [ + should: [ { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'host.name': 'siem', - }, - }, - ], + match: { + 'host.name': 'siem', }, }, - { - bool: { - filter: [ - { - nested: { - path: 'some.parentField', - query: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'some.parentField.nested.field': 'some value', - }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + must_not: { + bool: { + filter: [ + { + nested: { + path: 'some.parentField', + query: { + bool: { + should: [ + { + match_phrase: { + 'some.parentField.nested.field': 'some value', }, - ], - }, + }, + ], + minimum_should_match: 1, }, - score_mode: 'none', }, + score_mode: 'none', }, - { - bool: { - must_not: { - bool: { - minimum_should_match: 1, - should: [ - { - match: { - 'some.not.nested.field': 'some value', - }, - }, - ], + }, + { + bool: { + should: [ + { + match_phrase: { + 'some.not.nested.field': 'some value', }, }, - }, + ], + minimum_should_match: 1, }, - ], - }, + }, + ], }, - ], + }, }, }, ], - must: [], - must_not: [], should: [], + must_not: [], }, }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts index 5667f2e47b6d7..92ce7a2836115 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_search_after.ts @@ -52,6 +52,7 @@ export const singleSearchAfter = async ({ searchAfterSortId, timestampOverride, }); + const start = performance.now(); const nextSearchAfterResult: SignalSearchResponse = await services.callCluster( 'search', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index a6130a20f9c52..a610970907bf8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -9,7 +9,6 @@ import sinon from 'sinon'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import { listMock } from '../../../../../lists/server/mocks'; -import { EntriesArray } from '../../../../common/shared_imports'; import { buildRuleMessageFactory } from './rule_messages'; import { ExceptionListClient } from '../../../../../lists/server'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; @@ -24,7 +23,6 @@ import { getGapMaxCatchupRatio, errorAggregator, getListsClient, - hasLargeValueList, getSignalTimeTuples, getExceptions, } from './utils'; @@ -585,53 +583,6 @@ describe('utils', () => { }); }); - describe('#hasLargeValueList', () => { - test('it returns false if empty array', () => { - const hasLists = hasLargeValueList([]); - - expect(hasLists).toBeFalsy(); - }); - - test('it returns true if item of type EntryList exists', () => { - const entries: EntriesArray = [ - { - field: 'actingProcess.file.signer', - type: 'list', - operator: 'included', - list: { id: 'some id', type: 'ip' }, - }, - { - field: 'file.signature.signer', - type: 'match', - operator: 'excluded', - value: 'Global Signer', - }, - ]; - const hasLists = hasLargeValueList(entries); - - expect(hasLists).toBeTruthy(); - }); - - test('it returns false if item of type EntryList does not exist', () => { - const entries: EntriesArray = [ - { - field: 'actingProcess.file.signer', - type: 'match', - operator: 'included', - value: 'Elastic, N.V.', - }, - { - field: 'file.signature.signer', - type: 'match', - operator: 'excluded', - value: 'Global Signer', - }, - ]; - const hasLists = hasLargeValueList(entries); - - expect(hasLists).toBeFalsy(); - }); - }); describe('getSignalTimeTuples', () => { test('should return a single tuple if no gap', () => { const someTuples = getSignalTimeTuples({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 0b95ff6786b01..1c59a4b7ea5d0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -10,10 +10,11 @@ import dateMath from '@elastic/datemath'; import { Logger, SavedObjectsClientContract } from '../../../../../../../src/core/server'; import { AlertServices, parseDuration } from '../../../../../alerts/server'; import { ExceptionListClient, ListClient, ListPluginSetup } from '../../../../../lists/server'; -import { EntriesArray, ExceptionListItemSchema } from '../../../../../lists/common/schemas'; +import { ExceptionListItemSchema } from '../../../../../lists/common/schemas'; import { ListArrayOrUndefined } from '../../../../common/detection_engine/schemas/types/lists'; import { BulkResponse, BulkResponseErrorAggregation, isValidUnit } from './types'; import { BuildRuleMessage } from './rule_messages'; +import { hasLargeValueList } from '../../../../common/detection_engine/utils'; interface SortExceptionsReturn { exceptionsWithValueLists: ExceptionListItemSchema[]; @@ -148,11 +149,6 @@ export const getListsClient = async ({ return { listClient, exceptionsClient }; }; -export const hasLargeValueList = (entries: EntriesArray): boolean => { - const found = entries.filter(({ type }) => type === 'list'); - return found.length > 0; -}; - export const getExceptions = async ({ client, lists, diff --git a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts index 796338e189d60..142d2a68faed0 100644 --- a/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, getOr, has, head, set } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; +import { get, getOr, has, head } from 'lodash/fp'; import { FirstLastSeenHost, diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts index 6eefdb0bfc5ec..fc25f1a48194e 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/utils/common.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; import readline from 'readline'; import fs from 'fs'; import { Readable } from 'stream'; diff --git a/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts b/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts index 5f15d7ea08c54..b71dea96ec662 100644 --- a/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts +++ b/x-pack/plugins/snapshot_restore/server/test/helpers/router_mock.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; type RequestHandler = (...params: any[]) => any; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b54f88f83fbe0..a4100ae914b25 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4286,11 +4286,6 @@ "xpack.apm.serviceDetails.metricsTabLabel": "メトリック", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "トランザクション", - "xpack.apm.serviceMap.avgCpuUsagePopoverMetric": "CPU使用状況 (平均)", - "xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric": "1分あたりのエラー(平均)", - "xpack.apm.serviceMap.avgMemoryUsagePopoverMetric": "メモリー使用状況(平均)", - "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "1分あたりのリクエスト(平均)", - "xpack.apm.serviceMap.avgTransDurationPopoverMetric": "トランザクションの長さ(平均)", "xpack.apm.serviceMap.betaBadge": "ベータ", "xpack.apm.serviceMap.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", "xpack.apm.serviceMap.center": "中央", @@ -4300,8 +4295,6 @@ "xpack.apm.serviceMap.focusMapButtonText": "焦点マップ", "xpack.apm.serviceMap.invalidLicenseMessage": "サービスマップを利用するには、Elastic Platinum ライセンスが必要です。これにより、APM データとともにアプリケーションスタック全てを可視化することができるようになります。", "xpack.apm.serviceMap.serviceDetailsButtonText": "サービス詳細", - "xpack.apm.serviceMap.subtypePopoverMetric": "サブタイプ", - "xpack.apm.serviceMap.typePopoverMetric": "タイプ", "xpack.apm.serviceMap.viewFullMap": "サービスの全体マップを表示", "xpack.apm.serviceMap.zoomIn": "ズームイン", "xpack.apm.serviceMap.zoomOut": "ズームアウト", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 389e0083d5a9f..69e37f3f9f9f0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4290,11 +4290,6 @@ "xpack.apm.serviceDetails.metricsTabLabel": "指标", "xpack.apm.serviceDetails.nodesTabLabel": "JVM", "xpack.apm.serviceDetails.transactionsTabLabel": "事务", - "xpack.apm.serviceMap.avgCpuUsagePopoverMetric": "CPU 使用(平均)", - "xpack.apm.serviceMap.avgErrorsPerMinutePopoverMetric": "每分钟错误数(平均)", - "xpack.apm.serviceMap.avgMemoryUsagePopoverMetric": "内存使用(平均)", - "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "每分钟请求数(平均)", - "xpack.apm.serviceMap.avgTransDurationPopoverMetric": "事务持续时间(平均)", "xpack.apm.serviceMap.betaBadge": "公测版", "xpack.apm.serviceMap.betaTooltipMessage": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。", "xpack.apm.serviceMap.center": "中", @@ -4304,8 +4299,6 @@ "xpack.apm.serviceMap.focusMapButtonText": "聚焦地图", "xpack.apm.serviceMap.invalidLicenseMessage": "要访问服务地图,必须订阅 Elastic 白金级许可证。使用该许可证,您将能够可视化整个应用程序堆栈以及 APM 数据。", "xpack.apm.serviceMap.serviceDetailsButtonText": "服务详情", - "xpack.apm.serviceMap.subtypePopoverMetric": "子类型", - "xpack.apm.serviceMap.typePopoverMetric": "类型", "xpack.apm.serviceMap.viewFullMap": "查看完整的服务地图", "xpack.apm.serviceMap.zoomIn": "放大", "xpack.apm.serviceMap.zoomOut": "缩小", diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index 77ee3448cd06d..146cebabbb382 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { findIndex, get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { findIndex, get } from 'lodash'; import React from 'react'; import { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx index d88abc9c9c9ea..a20f4117f693d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import React, { Fragment, ReactNode } from 'react'; import { i18n } from '@kbn/i18n'; import { Subscription } from 'rxjs'; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts index 45be1df3e8d3b..2ebe670bc43c1 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_monitor_charts.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import mockChartsData from './monitor_charts_mock.json'; import { getMonitorDurationChart } from '../get_monitor_duration'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; diff --git a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts index fd890a30cf742..a52bf86499396 100644 --- a/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts +++ b/x-pack/plugins/uptime/server/lib/requests/__tests__/get_pings.test.ts @@ -5,7 +5,7 @@ */ import { getPings } from '../get_pings'; -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants'; describe('getAll', () => { diff --git a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts index 8bdf7faf380e8..6c229cf30e165 100644 --- a/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts +++ b/x-pack/plugins/uptime/server/lib/requests/search/find_potential_matches.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get } from 'lodash'; import { QueryContext } from './query_context'; /** diff --git a/x-pack/plugins/watcher/common/lib/serialization/serialization_helpers/build_input.js b/x-pack/plugins/watcher/common/lib/serialization/serialization_helpers/build_input.js index d9d02f4af882e..1aeec518545a0 100644 --- a/x-pack/plugins/watcher/common/lib/serialization/serialization_helpers/build_input.js +++ b/x-pack/plugins/watcher/common/lib/serialization/serialization_helpers/build_input.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; /* watch.input.search.request.indices diff --git a/x-pack/plugins/watcher/common/lib/serialization/serialize_json_watch.js b/x-pack/plugins/watcher/common/lib/serialization/serialize_json_watch.js index 70b00070447a4..9b8ce90d7fa82 100644 --- a/x-pack/plugins/watcher/common/lib/serialization/serialize_json_watch.js +++ b/x-pack/plugins/watcher/common/lib/serialization/serialize_json_watch.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { WATCH_TYPES } from '../../constants'; export function serializeJsonWatch(name, json) { diff --git a/x-pack/plugins/watcher/common/models/action/action.js b/x-pack/plugins/watcher/common/models/action/action.js index 0375b6ebf5d47..78e3fa2fc2582 100644 --- a/x-pack/plugins/watcher/common/models/action/action.js +++ b/x-pack/plugins/watcher/common/models/action/action.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { getActionType } from '../../lib/get_action_type'; import { ACTION_TYPES } from '../../constants'; import { LoggingAction } from './logging_action'; diff --git a/x-pack/plugins/watcher/public/application/models/action/action.js b/x-pack/plugins/watcher/public/application/models/action/action.js index 43874c9ee1dd1..d2393e327e5ff 100644 --- a/x-pack/plugins/watcher/public/application/models/action/action.js +++ b/x-pack/plugins/watcher/public/application/models/action/action.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get } from 'lodash'; import { ACTION_TYPES } from '../../../../common/constants'; import { EmailAction } from './email_action'; import { LoggingAction } from './logging_action'; diff --git a/x-pack/plugins/watcher/public/application/models/watch/watch.js b/x-pack/plugins/watcher/public/application/models/watch/watch.js index 934d1e338ed0c..64ec8db37b179 100644 --- a/x-pack/plugins/watcher/public/application/models/watch/watch.js +++ b/x-pack/plugins/watcher/public/application/models/watch/watch.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { get, set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; +import { get } from 'lodash'; import { WATCH_TYPES } from '../../../../common/constants'; import { JsonWatch } from './json_watch'; import { ThresholdWatch } from './threshold_watch'; diff --git a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js index 1000b6369ae3c..4a77324da18be 100644 --- a/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js +++ b/x-pack/plugins/watcher/server/lib/fetch_all_from_scroll/__tests__/fetch_all_from_scroll.js @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import sinon from 'sinon'; import { fetchAllFromScroll } from '../fetch_all_from_scroll'; -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; describe('fetch_all_from_scroll', () => { let mockResponse; diff --git a/x-pack/plugins/watcher/server/models/watch/watch.js b/x-pack/plugins/watcher/server/models/watch/watch.js index febf9c20b07a6..4e7ecf7feae09 100644 --- a/x-pack/plugins/watcher/server/models/watch/watch.js +++ b/x-pack/plugins/watcher/server/models/watch/watch.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { badRequest } from 'boom'; import { WATCH_TYPES } from '../../../common/constants'; import { JsonWatch } from './json_watch'; diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps.ts index cf265c3fb6737..0b370f6a30a8b 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import querystring from 'querystring'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -11,159 +12,224 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + describe('Service Maps with a trial license', () => { + describe('/api/apm/service-map', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); - describe('Service Maps', () => { - describe('when there is no data', () => { - it('returns empty list', async () => { - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); - - expect(response.status).to.be(200); - expect(response.body).to.eql({ elements: [] }); + expect(response.status).to.be(200); + expect(response.body).to.eql({ elements: [] }); + }); }); - }); - describe('when there is data', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + describe('when there is data', () => { + before(() => esArchiver.load('8.0.0')); + after(() => esArchiver.unload('8.0.0')); - it('returns service map elements', async () => { - const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); + it('returns service map elements', async () => { + const response = await supertest.get( + '/api/apm/service-map?start=2020-06-28T10%3A24%3A46.055Z&end=2020-06-29T10%3A24%3A46.055Z' + ); - expect(response.status).to.be(200); - expect(response.body).to.eql({ - elements: [ - { - data: { - source: 'client', - target: 'opbeans-node', - id: 'client~opbeans-node', - sourceData: { - id: 'client', - 'service.name': 'client', - 'agent.name': 'rum-js', + expect(response.status).to.be(200); + + expect(response.body).to.eql({ + elements: [ + { + data: { + source: 'client', + target: 'opbeans-node', + id: 'client~opbeans-node', + sourceData: { + id: 'client', + 'service.name': 'client', + 'agent.name': 'rum-js', + }, + targetData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, }, - targetData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', + }, + { + data: { + source: 'opbeans-java', + target: '>opbeans-java:3000', + id: 'opbeans-java~>opbeans-java:3000', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + 'span.subtype': 'http', + 'span.destination.service.resource': 'opbeans-java:3000', + 'span.type': 'external', + id: '>opbeans-java:3000', + label: 'opbeans-java:3000', + }, }, }, - }, - { - data: { - source: 'opbeans-java', - target: '>opbeans-java:3000', - id: 'opbeans-java~>opbeans-java:3000', - sourceData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', + { + data: { + source: 'opbeans-java', + target: '>postgresql', + id: 'opbeans-java~>postgresql', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, }, - targetData: { - 'span.subtype': 'http', - 'span.destination.service.resource': 'opbeans-java:3000', - 'span.type': 'external', - id: '>opbeans-java:3000', - label: 'opbeans-java:3000', + }, + { + data: { + source: 'opbeans-java', + target: 'opbeans-node', + id: 'opbeans-java~opbeans-node', + sourceData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + targetData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + bidirectional: true, }, }, - }, - { - data: { - source: 'opbeans-java', - target: '>postgresql', - id: 'opbeans-java~>postgresql', - sourceData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', + { + data: { + source: 'opbeans-node', + target: '>93.184.216.34:80', + id: 'opbeans-node~>93.184.216.34:80', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'http', + 'span.destination.service.resource': '93.184.216.34:80', + 'span.type': 'external', + id: '>93.184.216.34:80', + label: '93.184.216.34:80', + }, }, - targetData: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', + }, + { + data: { + source: 'opbeans-node', + target: '>postgresql', + id: 'opbeans-node~>postgresql', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', + }, }, }, - }, - { - data: { - source: 'opbeans-java', - target: 'opbeans-node', - id: 'opbeans-java~opbeans-node', - sourceData: { + { + data: { + source: 'opbeans-node', + target: '>redis', + id: 'opbeans-node~>redis', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + 'span.subtype': 'redis', + 'span.destination.service.resource': 'redis', + 'span.type': 'cache', + id: '>redis', + label: 'redis', + }, + }, + }, + { + data: { + source: 'opbeans-node', + target: 'opbeans-java', + id: 'opbeans-node~opbeans-java', + sourceData: { + id: 'opbeans-node', + 'service.environment': 'production', + 'service.name': 'opbeans-node', + 'agent.name': 'nodejs', + }, + targetData: { + id: 'opbeans-java', + 'service.environment': 'production', + 'service.name': 'opbeans-java', + 'agent.name': 'java', + }, + isInverseEdge: true, + }, + }, + { + data: { id: 'opbeans-java', 'service.environment': 'production', 'service.name': 'opbeans-java', 'agent.name': 'java', }, - targetData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - bidirectional: true, }, - }, - { - data: { - source: 'opbeans-node', - target: '>93.184.216.34:80', - id: 'opbeans-node~>93.184.216.34:80', - sourceData: { + { + data: { id: 'opbeans-node', 'service.environment': 'production', 'service.name': 'opbeans-node', 'agent.name': 'nodejs', }, - targetData: { + }, + { + data: { 'span.subtype': 'http', - 'span.destination.service.resource': '93.184.216.34:80', + 'span.destination.service.resource': 'opbeans-java:3000', 'span.type': 'external', - id: '>93.184.216.34:80', - label: '93.184.216.34:80', + id: '>opbeans-java:3000', + label: 'opbeans-java:3000', }, }, - }, - { - data: { - source: 'opbeans-node', - target: '>postgresql', - id: 'opbeans-node~>postgresql', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', + { + data: { + id: 'client', + 'service.name': 'client', + 'agent.name': 'rum-js', }, }, - }, - { - data: { - source: 'opbeans-node', - target: '>redis', - id: 'opbeans-node~>redis', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { + { + data: { 'span.subtype': 'redis', 'span.destination.service.resource': 'redis', 'span.type': 'cache', @@ -171,87 +237,51 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) label: 'redis', }, }, - }, - { - data: { - source: 'opbeans-node', - target: 'opbeans-java', - id: 'opbeans-node~opbeans-java', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', + { + data: { + 'span.subtype': 'postgresql', + 'span.destination.service.resource': 'postgresql', + 'span.type': 'db', + id: '>postgresql', + label: 'postgresql', }, - isInverseEdge: true, - }, - }, - { - data: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', - }, - }, - { - data: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - }, - { - data: { - 'span.subtype': 'http', - 'span.destination.service.resource': 'opbeans-java:3000', - 'span.type': 'external', - id: '>opbeans-java:3000', - label: 'opbeans-java:3000', - }, - }, - { - data: { - id: 'client', - 'service.name': 'client', - 'agent.name': 'rum-js', - }, - }, - { - data: { - 'span.subtype': 'redis', - 'span.destination.service.resource': 'redis', - 'span.type': 'cache', - id: '>redis', - label: 'redis', - }, - }, - { - data: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', }, - }, - { - data: { - 'span.subtype': 'http', - 'span.destination.service.resource': '93.184.216.34:80', - 'span.type': 'external', - id: '>93.184.216.34:80', - label: '93.184.216.34:80', + { + data: { + 'span.subtype': 'http', + 'span.destination.service.resource': '93.184.216.34:80', + 'span.type': 'external', + id: '>93.184.216.34:80', + label: '93.184.216.34:80', + }, }, + ], + }); + }); + }); + }); + + describe('/api/apm/service-map/service/{serviceName}', () => { + describe('when there is no data', () => { + it('returns an object with nulls', async () => { + const q = querystring.stringify({ + start: '2020-06-28T10:24:46.055Z', + end: '2020-06-29T10:24:46.055Z', + uiFilters: {}, + }); + const response = await supertest.get(`/api/apm/service-map/service/opbeans-node?${q}`); + + expect(response.status).to.be(200); + + expect(response.body).to.eql({ + avgCpuUsage: null, + avgErrorRate: null, + avgMemoryUsage: null, + transactionStats: { + avgRequestsPerMinute: null, + avgTransactionDuration: null, }, - ], + }); }); }); }); diff --git a/x-pack/test/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 7534a1b09cc23..e447996a08dfe 100644 --- a/x-pack/test/functional/apps/maps/joins.js +++ b/x-pack/test/functional/apps/maps/joins.js @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import _ from 'lodash'; +import { set } from '@elastic/safer-lodash-set'; import { MAPBOX_STYLES } from './mapbox_styles'; @@ -99,7 +99,7 @@ export default function ({ getPageObjects, getService }) { //circle layer for points expect(layersForVectorSource[CIRCLE_STYLE_LAYER_INDEX]).to.eql( - _.set(MAPBOX_STYLES.POINT_LAYER, 'paint.circle-stroke-color', dynamicColor) + set(MAPBOX_STYLES.POINT_LAYER, 'paint.circle-stroke-color', dynamicColor) ); //fill layer @@ -107,7 +107,7 @@ export default function ({ getPageObjects, getService }) { //line layer for borders expect(layersForVectorSource[LINE_STYLE_LAYER_INDEX]).to.eql( - _.set(MAPBOX_STYLES.LINE_LAYER, 'paint.line-color', dynamicColor) + set(MAPBOX_STYLES.LINE_LAYER, 'paint.line-color', dynamicColor) ); }); diff --git a/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts index cae4ff79bdef6..27581550ac2bc 100644 --- a/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts +++ b/x-pack/test/ingest_manager_api_integration/apis/package_config/create.ts @@ -18,7 +18,9 @@ export default function ({ getService }: FtrProviderContext) { // because `this` has to point to the Mocha context // see https://mochajs.org/#arrow-functions - describe('Package Config - create', async function () { + // Temporarily skipped to promote snapshot + // Re-enabled in https://github.com/elastic/kibana/pull/71727 + describe.skip('Package Config - create', async function () { let agentConfigId: string; before(async function () { diff --git a/yarn.lock b/yarn.lock index b8aa559bc1d40..0f144078ff46f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5420,6 +5420,11 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" integrity sha1-UALhT3Xi1x5WQoHfBDHIwbSio2o= +"@types/minimist@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" + integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= + "@types/minipass@*": version "2.2.0" resolved "https://registry.yarnpkg.com/@types/minipass/-/minipass-2.2.0.tgz#51ad404e8eb1fa961f75ec61205796807b6f9651" @@ -6605,7 +6610,7 @@ acorn-jsx@^5.1.0: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.1.0.tgz#294adb71b57398b0680015f0a38c563ee1db5384" integrity sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw== -acorn-node@^1.3.0: +acorn-node@^1.3.0, acorn-node@^1.6.1: version "1.8.2" resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== @@ -7870,6 +7875,13 @@ autoprefixer@^9.4.9, autoprefixer@^9.7.4: postcss "^7.0.26" postcss-value-parser "^4.0.2" +available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" + integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ== + dependencies: + array-filter "^1.0.0" + await-event@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/await-event/-/await-event-2.1.0.tgz#78e9f92684bae4022f9fa0b5f314a11550f9aa76" @@ -9498,6 +9510,15 @@ camelcase-keys@^4.0.0: map-obj "^2.0.0" quick-lru "^1.0.0" +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + camelcase@5.0.0, camelcase@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42" @@ -9528,6 +9549,11 @@ camelcase@^4.0.0, camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= +camelcase@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" + integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== + camelize@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" @@ -9686,7 +9712,7 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^4.1.0: +chalk@^4.0.0, chalk@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== @@ -11898,7 +11924,7 @@ debuglog@^1.0.1: resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= -decamelize-keys@^1.0.0: +decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= @@ -12024,6 +12050,26 @@ deep-equal@^1.0.1, deep-equal@~1.0.1: resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= +deep-equal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0" + integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA== + dependencies: + es-abstract "^1.17.5" + es-get-iterator "^1.1.0" + is-arguments "^1.0.4" + is-date-object "^1.0.2" + is-regex "^1.0.5" + isarray "^2.0.5" + object-is "^1.1.2" + object-keys "^1.1.1" + object.assign "^4.1.0" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.2" + which-boxed-primitive "^1.0.1" + which-collection "^1.0.1" + which-typed-array "^1.1.2" + deep-extend@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -12132,7 +12178,7 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" -defined@~1.0.0: +defined@^1.0.0, defined@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= @@ -12224,6 +12270,21 @@ depd@~1.1.1, depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +dependency-check@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/dependency-check/-/dependency-check-4.1.0.tgz#d45405cabb50298f8674fe28ab594c8a5530edff" + integrity sha512-nlw+PvhVQwg0gSNNlVUiuRv0765gah9pZEXdQlIFzeSnD85Eex0uM0bkrAWrHdeTzuMGZnR9daxkup/AqqgqzA== + dependencies: + debug "^4.0.0" + detective "^5.0.2" + globby "^10.0.1" + is-relative "^1.0.0" + micromatch "^4.0.2" + minimist "^1.2.0" + pkg-up "^3.1.0" + read-package-json "^2.0.10" + resolve "^1.1.7" + dependency-tree@^7.0.2: version "7.0.2" resolved "https://registry.yarnpkg.com/dependency-tree/-/dependency-tree-7.0.2.tgz#01df8bbdc51e41438f5bb93f4a53e1a9cf8301a1" @@ -12391,6 +12452,15 @@ detective-typescript@^5.1.1: node-source-walk "^4.2.0" typescript "^3.4.5" +detective@^5.0.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.0.tgz#feb2a77e85b904ecdea459ad897cc90a99bd2a7b" + integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== + dependencies: + acorn-node "^1.6.1" + defined "^1.0.0" + minimist "^1.1.1" + dezalgo@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" @@ -12695,7 +12765,7 @@ dotenv@^8.1.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== -dotignore@~0.1.2: +dotignore@^0.1.2, dotignore@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/dotignore/-/dotignore-0.1.2.tgz#f942f2200d28c3a76fbdd6f0ee9f3257c8a2e905" integrity sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw== @@ -13299,6 +13369,36 @@ es-abstract@^1.15.0, es-abstract@^1.17.0-next.1: string.prototype.trimleft "^2.1.1" string.prototype.trimright "^2.1.1" +es-abstract@^1.17.4, es-abstract@^1.17.5: + version "1.17.6" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" + integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.0" + is-regex "^1.1.0" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-get-iterator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" + integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== + dependencies: + es-abstract "^1.17.4" + has-symbols "^1.0.1" + is-arguments "^1.0.4" + is-map "^2.0.1" + is-set "^2.0.1" + is-string "^1.0.5" + isarray "^2.0.5" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -13522,6 +13622,19 @@ eslint-formatter-pretty@^1.3.0: plur "^2.1.2" string-width "^2.0.0" +eslint-formatter-pretty@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-formatter-pretty/-/eslint-formatter-pretty-4.0.0.tgz#dc15f3bf4fb51b7ba5fbedb77f57ba8841140ce2" + integrity sha512-QgdeZxQwWcN0TcXXNZJiS6BizhAANFhCzkE7Yl9HKB7WjElzwED6+FbbZB2gji8ofgJTGPqKm6VRCNT3OGCeEw== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + eslint-rule-docs "^1.1.5" + log-symbols "^4.0.0" + plur "^4.0.0" + string-width "^4.2.0" + supports-hyperlinks "^2.0.0" + eslint-import-resolver-node@0.3.2, eslint-import-resolver-node@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" @@ -13695,6 +13808,11 @@ eslint-rule-composer@^0.3.0: resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== +eslint-rule-docs@^1.1.5: + version "1.1.199" + resolved "https://registry.yarnpkg.com/eslint-rule-docs/-/eslint-rule-docs-1.1.199.tgz#f4e0befb6907101399624964ce4726f684415630" + integrity sha512-0jXhQ2JLavUsV/8HVFrBSHL4EM17cl0veZHAVcF1HOEoPdrr09huADK9/L7CbsqP4tMJy9FG23neUEDH8W/Mmg== + eslint-scope@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" @@ -15026,7 +15144,7 @@ for-each@^0.3.2: dependencies: is-function "~1.0.0" -for-each@~0.3.3: +for-each@^0.3.3, for-each@~0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== @@ -15057,6 +15175,11 @@ for-own@^1.0.0: dependencies: for-in "^1.0.1" +foreach@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" + integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= + foreachasync@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6" @@ -16737,6 +16860,11 @@ har-validator@~5.1.0, har-validator@~5.1.3: ajv "^6.5.5" har-schema "^2.0.0" +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + has-ansi@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e" @@ -17651,7 +17779,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@~2.0.3, inherits@~2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -18014,6 +18142,11 @@ irregular-plurals@^1.0.0: resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.4.0.tgz#2ca9b033651111855412f16be5d77c62a458a766" integrity sha1-LKmwM2UREYVUEvFr5dd8YqRYp2Y= +irregular-plurals@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-3.2.0.tgz#b19c490a0723798db51b235d7e39add44dab0822" + integrity sha512-YqTdPLfwP7YFN0SsD3QUVCkm9ZG2VzOXv3DOrw5G5mkMbVwptTwVcFv7/C0vOpBmgTxAeTG19XpUs1E522LW9Q== + is-absolute-url@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" @@ -18069,6 +18202,11 @@ is-arrayish@^0.3.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.1.tgz#c2dfc386abaa0c3e33c48db3fe87059e69065efd" integrity sha1-wt/DhquqDD4zxI2z/ocFnmkGXv0= +is-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" + integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== + is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" @@ -18090,7 +18228,7 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-boolean-object@^1.0.1: +is-boolean-object@^1.0.0, is-boolean-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== @@ -18117,6 +18255,11 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.1.5: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== +is-callable@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" + integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== + is-ci@2.0.0, is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" @@ -18150,6 +18293,11 @@ is-date-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= +is-date-object@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + is-decimal@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.1.tgz#f5fb6a94996ad9e8e3761fbfbd091f1fca8c4e82" @@ -18332,6 +18480,11 @@ is-lower-case@^1.1.0: dependencies: lower-case "^1.1.0" +is-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" + integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== + is-my-ip-valid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz#7b351b8e8edd4d3995d4d066680e664d94696824" @@ -18381,7 +18534,7 @@ is-npm@^4.0.0: resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== -is-number-object@^1.0.4: +is-number-object@^1.0.3, is-number-object@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== @@ -18521,6 +18674,13 @@ is-regex@^1.0.3, is-regex@^1.0.4, is-regex@^1.0.5, is-regex@~1.0.5: dependencies: has "^1.0.3" +is-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.0.tgz#ece38e389e490df0dc21caea2bd596f987f767ff" + integrity sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw== + dependencies: + has-symbols "^1.0.1" + is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" @@ -18570,6 +18730,11 @@ is-secret@^1.0.0: resolved "https://registry.yarnpkg.com/is-secret/-/is-secret-1.2.1.tgz#04b9ca1880ea763049606cfe6c2a08a93f33abe3" integrity sha512-VtBantcgKL2a64fDeCmD1JlkHToh3v0bVOhyJZ5aGTjxtCgrdNcjaC9GaaRFXi19gA4/pYFpnuyoscIgQCFSMQ== +is-set@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" + integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== + is-ssh@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.1.tgz#f349a8cadd24e65298037a522cf7520f2e81a0f3" @@ -18587,7 +18752,7 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== -is-string@^1.0.5: +is-string@^1.0.4, is-string@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== @@ -18609,6 +18774,16 @@ is-symbol@^1.0.2: dependencies: has-symbols "^1.0.0" +is-typed-array@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d" + integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ== + dependencies: + available-typed-arrays "^1.0.0" + es-abstract "^1.17.4" + foreach "^2.0.5" + has-symbols "^1.0.1" + is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -18650,6 +18825,16 @@ is-valid-path@0.1.1, is-valid-path@^0.1.1: dependencies: is-invalid-path "^0.1.0" +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakset@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" + integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== + is-whitespace-character@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.1.tgz#9ae0176f3282b65457a1992cdb084f8a5f833e3b" @@ -18704,6 +18889,11 @@ isarray@2.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isbinaryfile@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.2.tgz#bfc45642da645681c610cca831022e30af426488" @@ -20122,7 +20312,7 @@ kind-of@^5.0.0, kind-of@^5.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -kind-of@^6.0.0, kind-of@^6.0.2: +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== @@ -20941,6 +21131,13 @@ log-symbols@^1.0.1, log-symbols@^1.0.2: dependencies: chalk "^1.0.0" +log-symbols@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" + integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== + dependencies: + chalk "^4.0.0" + log-update@2.3.0, log-update@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" @@ -21224,6 +21421,11 @@ map-obj@^2.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= +map-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" + integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== + map-or-similar@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08" @@ -21513,6 +21715,25 @@ meow@^5.0.0: trim-newlines "^2.0.0" yargs-parser "^10.0.0" +meow@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-7.0.1.tgz#1ed4a0a50b3844b451369c48362eb0515f04c1dc" + integrity sha512-tBKIQqVrAHqwit0vfuFPY3LlzJYkEOFyKa3bPgxzNl6q/RtN8KQ+ALYEASYuFayzSAsjlhXj/JZ10rH85Q6TUw== + dependencies: + "@types/minimist" "^1.2.0" + arrify "^2.0.1" + camelcase "^6.0.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "^4.0.2" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.13.1" + yargs-parser "^18.1.3" + merge-deep@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" @@ -21753,6 +21974,15 @@ minimist-options@^3.0.1: arrify "^1.0.1" is-plain-obj "^1.1.0" +minimist-options@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + minimist@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.5.tgz#d7aa327bcecf518f9106ac6b8f003fa3bcea8566" @@ -22821,7 +23051,7 @@ npm-keyword@^5.0.0: got "^7.1.0" registry-url "^3.0.3" -npm-normalize-package-bin@^1.0.1: +npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== @@ -23034,6 +23264,14 @@ object-is@^1.0.1, object-is@^1.0.2: resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== +object-is@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6" + integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -24290,6 +24528,13 @@ plur@^2.1.2: dependencies: irregular-plurals "^1.0.0" +plur@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/plur/-/plur-4.0.0.tgz#729aedb08f452645fe8c58ef115bf16b0a73ef84" + integrity sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg== + dependencies: + irregular-plurals "^3.2.0" + pluralize@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-3.1.0.tgz#84213d0a12356069daa84060c559242633161368" @@ -25125,6 +25370,11 @@ quick-lru@^1.0.0: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + quickselect@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" @@ -26106,6 +26356,18 @@ read-package-json@^2.0.0: optionalDependencies: graceful-fs "^4.1.2" +read-package-json@^2.0.10: + version "2.1.1" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.1.1.tgz#16aa66c59e7d4dad6288f179dd9295fd59bb98f1" + integrity sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A== + dependencies: + glob "^7.1.1" + json-parse-better-errors "^1.0.1" + normalize-package-data "^2.0.0" + npm-normalize-package-bin "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.2" + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -26147,7 +26409,7 @@ read-pkg-up@^6.0.0: read-pkg "^5.1.1" type-fest "^0.5.0" -read-pkg-up@^7.0.1: +read-pkg-up@^7.0.0, read-pkg-up@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== @@ -26633,6 +26895,14 @@ regexp.prototype.flags@^1.2.0: dependencies: define-properties "^1.1.2" +regexp.prototype.flags@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -27258,7 +27528,7 @@ restructure@^0.5.3: dependencies: browserify-optional "^1.0.0" -resumer@~0.0.0: +resumer@^0.0.0, resumer@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" integrity sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k= @@ -28190,6 +28460,14 @@ shot@4.x.x: hoek "5.x.x" joi "13.x.x" +side-channel@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" + integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA== + dependencies: + es-abstract "^1.17.0-next.1" + object-inspect "^1.7.0" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -29111,6 +29389,14 @@ string.prototype.trim@~1.1.2: es-abstract "^1.5.0" function-bind "^1.0.2" +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string.prototype.trimleft@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" @@ -29127,6 +29413,14 @@ string.prototype.trimright@^2.1.1: define-properties "^1.1.3" function-bind "^1.1.1" +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + string_decoder@0.10, string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" @@ -29690,6 +29984,29 @@ tape@^4.5.1: string.prototype.trim "~1.1.2" through "~2.3.8" +tape@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/tape/-/tape-5.0.1.tgz#0d70ce90a586387c4efda4393e72872672a416a3" + integrity sha512-wVsOl2shKPcjdJdc8a+PwacvrOdJZJ57cLUXlxW4TQ2R6aihXwG0m0bKm4mA4wjtQNTaLMCrYNEb4f9fjHKUYQ== + dependencies: + deep-equal "^2.0.3" + defined "^1.0.0" + dotignore "^0.1.2" + for-each "^0.3.3" + function-bind "^1.1.1" + glob "^7.1.6" + has "^1.0.3" + inherits "^2.0.4" + is-regex "^1.0.5" + minimist "^1.2.5" + object-inspect "^1.7.0" + object-is "^1.1.2" + object.assign "^4.1.0" + resolve "^1.17.0" + resumer "^0.0.0" + string.prototype.trim "^1.2.1" + through "^2.3.8" + tar-fs@^1.16.3: version "1.16.3" resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" @@ -29995,7 +30312,7 @@ through2@~2.0.3: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3.4, through@~2.3.6, through@~2.3.8: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3.4, through@~2.3.6, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -30394,6 +30711,11 @@ trim-newlines@^2.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= +trim-newlines@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" + integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== + trim-repeated@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" @@ -30490,6 +30812,18 @@ ts-pnp@^1.1.2: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.4.tgz#ae27126960ebaefb874c6d7fa4729729ab200d90" integrity sha512-1J/vefLC+BWSo+qe8OnJQfWTYRS6ingxjwqmHMqaMxXMj7kFtKLgAaYW3JeX3mktjgUL+etlU8/B4VUAUI9QGw== +tsd@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.13.1.tgz#d2a8baa80b8319dafea37fbeb29fef3cec86e92b" + integrity sha512-+UYM8LRG/M4H8ISTg2ow8SWi65PS7Os+4DUnyiQLbJysXBp2DEmws9SMgBH+m8zHcJZqUJQ+mtDWJXP1IAvB2A== + dependencies: + eslint-formatter-pretty "^4.0.0" + globby "^11.0.1" + meow "^7.0.1" + path-exists "^4.0.0" + read-pkg-up "^7.0.0" + update-notifier "^4.1.0" + tsd@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.7.4.tgz#d9aba567f1394641821a6800dcee60746c87bd03" @@ -31022,6 +31356,11 @@ type-fest@^0.10.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.10.0.tgz#7f06b2b9fbfc581068d1341ffabd0349ceafc642" integrity sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw== +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + type-fest@^0.3.0, type-fest@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -31574,7 +31913,7 @@ update-notifier@^2.5.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" -update-notifier@^4.0.0: +update-notifier@^4.0.0, update-notifier@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3" integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew== @@ -32784,6 +33123,27 @@ whatwg-url@^7.0.0: tr46 "^1.0.1" webidl-conversions "^4.0.2" +which-boxed-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" + integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== + dependencies: + is-bigint "^1.0.0" + is-boolean-object "^1.0.0" + is-number-object "^1.0.3" + is-string "^1.0.4" + is-symbol "^1.0.2" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" @@ -32794,6 +33154,18 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +which-typed-array@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2" + integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ== + dependencies: + available-typed-arrays "^1.0.2" + es-abstract "^1.17.5" + foreach "^2.0.5" + function-bind "^1.1.1" + has-symbols "^1.0.1" + is-typed-array "^1.1.3" + which@1, which@1.3.1, which@^1.2.9, which@^1.3.1, which@~1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -33357,7 +33729,7 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^18.1.1, yargs-parser@^18.1.2: +yargs-parser@^18.1.1, yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==