From 42c3efdcaba4f476ef54f190f639e8180bccc5a7 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 15 Jul 2020 01:26:58 -0700 Subject: [PATCH 1/8] [tests] Temporarily skipped to promote snapshot Will be re-enabled in #71727 Signed-off-by: Tyler Smalley --- .../apis/package_config/create.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 cae4ff79bdef6c..27581550ac2bc4 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 () { From fc5bc6b6a2770903148f35e083cb75b52d467118 Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 15 Jul 2020 10:29:57 +0200 Subject: [PATCH 2/8] Add @elastic/safer-lodash-set as an alternative to lodash.set (#67452) --- .eslintrc.js | 231 +++++++++- package.json | 1 + packages/elastic-safer-lodash-set/.gitignore | 2 + packages/elastic-safer-lodash-set/.npmignore | 3 + packages/elastic-safer-lodash-set/LICENSE | 34 ++ packages/elastic-safer-lodash-set/README.md | 113 +++++ .../elastic-safer-lodash-set/fp/assoc.d.ts | 9 + packages/elastic-safer-lodash-set/fp/assoc.js | 8 + .../fp/assocPath.d.ts | 9 + .../elastic-safer-lodash-set/fp/assocPath.js | 8 + .../elastic-safer-lodash-set/fp/index.d.ts | 225 ++++++++++ packages/elastic-safer-lodash-set/fp/index.js | 9 + packages/elastic-safer-lodash-set/fp/set.d.ts | 9 + packages/elastic-safer-lodash-set/fp/set.js | 13 + .../elastic-safer-lodash-set/fp/setWith.d.ts | 9 + .../elastic-safer-lodash-set/fp/setWith.js | 13 + packages/elastic-safer-lodash-set/index.d.ts | 64 +++ packages/elastic-safer-lodash-set/index.js | 9 + .../lodash/_baseSet.js | 61 +++ .../elastic-safer-lodash-set/lodash/set.js | 44 ++ .../lodash/setWith.js | 41 ++ .../elastic-safer-lodash-set/package.json | 49 +++ .../scripts/_get_lodash.sh | 15 + .../scripts/license-header.txt | 7 + .../scripts/patches/_baseSet.js.patch | 31 ++ .../scripts/save_state.sh | 18 + .../elastic-safer-lodash-set/scripts/tsd.sh | 17 + .../scripts/update.sh | 37 ++ packages/elastic-safer-lodash-set/set.d.ts | 9 + packages/elastic-safer-lodash-set/set.js | 8 + .../elastic-safer-lodash-set/setWith.d.ts | 9 + packages/elastic-safer-lodash-set/setWith.js | 8 + .../test/fp.test-d.ts | 85 ++++ .../test/fp_assoc.test-d.ts | 25 ++ .../test/fp_assocPath.test-d.ts | 25 ++ .../test/fp_patch_test.js | 290 +++++++++++++ .../test/fp_set.test-d.ts | 25 ++ .../test/fp_setWith.test-d.ts | 40 ++ .../test/index.test-d.ts | 37 ++ .../test/patch_test.js | 174 ++++++++ .../test/set.test-d.ts | 14 + .../test/setWith.test-d.ts | 32 ++ .../elastic-safer-lodash-set/tsconfig.json | 9 + .../tools/check_collector__integrity.test.ts | 12 +- .../src/tools/check_collector_integrity.ts | 6 +- .../src/tools/tasks/generate_schemas_task.ts | 1 - .../kbn-telemetry-tools/src/tools/utils.ts | 29 +- src/cli/command.js | 3 +- src/cli/serve/read_keystore.js | 2 +- src/cli/serve/serve.js | 3 +- .../saved_objects/simple_saved_object.ts | 3 +- .../config/deprecation/deprecation_factory.ts | 3 +- .../server/config/object_to_config_adapter.ts | 3 +- src/core/server/config/read_config.ts | 3 +- .../legacy/config/get_unused_config_keys.ts | 3 +- .../migrations/core/document_migrator.test.ts | 9 +- .../migrations/core/document_migrator.ts | 3 +- .../migrations/core/migrate_raw_docs.test.ts | 5 +- .../saved_objects/service/lib/filter_utils.ts | 3 +- src/dev/file.ts | 4 +- src/dev/precommit_hook/casing_check_config.js | 3 + src/fixtures/mock_ui_state.js | 5 +- src/legacy/deprecation/deprecations/rename.js | 3 +- src/legacy/server/config/config.js | 5 +- .../state_management/state_monitor_factory.ts | 3 +- .../build_tabular_inspector_data.ts | 2 +- .../search/search_source/search_source.ts | 14 +- .../context/api/context.predecessors.test.js | 14 +- .../context/api/context.successors.test.js | 14 +- .../lexer_rules/x_json_highlight_rules.ts | 4 +- .../static/forms/hook_form_lib/lib/utils.ts | 2 +- .../public/angular/angular_config.tsx | 3 +- .../object_view/components/form.tsx | 3 +- .../components/lib/convert_series_to_vars.js | 5 +- .../lib/vis_data/helpers/bucket_transform.js | 3 +- .../public/vislib/lib/axis/axis_config.js | 3 +- .../public/vislib/lib/chart_grid.js | 3 +- .../public/vislib/lib/vis_config.js | 3 +- .../public/legacy/vis_update_state.js | 5 +- .../public/persisted_state/persisted_state.ts | 13 +- tasks/config/run.js | 6 + tasks/jenkins.js | 1 + .../apis/saved_objects/migrations.js | 23 +- .../lib/check_license/check_license.test.js | 2 +- .../__tests__/is_es_error_factory.js | 2 +- .../legacy/server/lib/parse_kibana_state.js | 3 +- x-pack/package.json | 1 + .../aggregate-latency-metrics/index.ts | 3 +- .../public/lib/configuration_blocks.ts | 3 +- .../functions/common/plot/index.ts | 3 +- .../public/components/asset_manager/index.ts | 3 +- .../public/expression_types/arg_types/font.js | 3 +- .../event_log/scripts/create_schemas.js | 5 +- ...ith_metrics_explorer_options_url_state.tsx | 3 +- .../components/helpers/create_tsvb_link.ts | 2 +- .../routes/metadata/lib/get_node_info.ts | 3 +- .../metrics_explorer/lib/get_groupings.ts | 3 +- .../server/utils/create_afterkey_handler.ts | 2 +- .../public/components/table/storage.js | 3 +- .../public/lib/calculate_shard_stats.js | 3 +- .../server/lib/__tests__/create_query.js | 2 +- .../cluster/__tests__/get_clusters_state.js | 2 +- .../lib/cluster/flag_supported_clusters.js | 3 +- .../lib/cluster/get_clusters_from_request.js | 3 +- .../elasticsearch/__tests__/get_ml_jobs.js | 2 +- .../nodes/__tests__/calculate_node_type.js | 2 +- .../telemetry_collection/create_query.test.ts | 2 +- .../telemetry_collection/get_all_stats.ts | 3 +- .../server/browsers/network_policy.ts | 4 +- .../export_types/common/validate_urls.ts | 4 +- .../generate_csv/check_cells_for_formulas.ts | 8 +- .../server/routes/lib/get_document_payload.ts | 6 +- .../public/cases/containers/utils.ts | 3 +- .../components/event_details/json_view.tsx | 2 +- .../common/components/search_bar/index.tsx | 3 +- .../common/components/toasters/index.test.tsx | 3 +- .../public/common/containers/source/index.tsx | 3 +- .../components/flyout/index.test.tsx | 2 +- .../components/open_timeline/helpers.ts | 3 +- .../timelines/store/timeline/reducer.test.ts | 3 +- .../server/lib/hosts/elasticsearch_adapter.ts | 3 +- .../lib/timeline/routes/utils/common.ts | 2 +- .../server/test/helpers/router_mock.ts | 2 +- .../public/application/components/tabs.tsx | 3 +- .../checkup/deprecations/reindex/button.tsx | 2 +- .../__tests__/get_monitor_charts.test.ts | 2 +- .../lib/requests/__tests__/get_pings.test.ts | 2 +- .../requests/search/find_potential_matches.ts | 3 +- .../serialization_helpers/build_input.js | 2 +- .../lib/serialization/serialize_json_watch.js | 2 +- .../watcher/common/models/action/action.js | 2 +- .../application/models/action/action.js | 3 +- .../public/application/models/watch/watch.js | 3 +- .../__tests__/fetch_all_from_scroll.js | 2 +- .../watcher/server/models/watch/watch.js | 2 +- x-pack/test/functional/apps/maps/joins.js | 6 +- yarn.lock | 406 +++++++++++++++++- 137 files changed, 2475 insertions(+), 196 deletions(-) create mode 100644 packages/elastic-safer-lodash-set/.gitignore create mode 100644 packages/elastic-safer-lodash-set/.npmignore create mode 100644 packages/elastic-safer-lodash-set/LICENSE create mode 100644 packages/elastic-safer-lodash-set/README.md create mode 100644 packages/elastic-safer-lodash-set/fp/assoc.d.ts create mode 100644 packages/elastic-safer-lodash-set/fp/assoc.js create mode 100644 packages/elastic-safer-lodash-set/fp/assocPath.d.ts create mode 100644 packages/elastic-safer-lodash-set/fp/assocPath.js create mode 100644 packages/elastic-safer-lodash-set/fp/index.d.ts create mode 100644 packages/elastic-safer-lodash-set/fp/index.js create mode 100644 packages/elastic-safer-lodash-set/fp/set.d.ts create mode 100644 packages/elastic-safer-lodash-set/fp/set.js create mode 100644 packages/elastic-safer-lodash-set/fp/setWith.d.ts create mode 100644 packages/elastic-safer-lodash-set/fp/setWith.js create mode 100644 packages/elastic-safer-lodash-set/index.d.ts create mode 100644 packages/elastic-safer-lodash-set/index.js create mode 100644 packages/elastic-safer-lodash-set/lodash/_baseSet.js create mode 100644 packages/elastic-safer-lodash-set/lodash/set.js create mode 100644 packages/elastic-safer-lodash-set/lodash/setWith.js create mode 100644 packages/elastic-safer-lodash-set/package.json create mode 100755 packages/elastic-safer-lodash-set/scripts/_get_lodash.sh create mode 100644 packages/elastic-safer-lodash-set/scripts/license-header.txt create mode 100644 packages/elastic-safer-lodash-set/scripts/patches/_baseSet.js.patch create mode 100755 packages/elastic-safer-lodash-set/scripts/save_state.sh create mode 100755 packages/elastic-safer-lodash-set/scripts/tsd.sh create mode 100755 packages/elastic-safer-lodash-set/scripts/update.sh create mode 100644 packages/elastic-safer-lodash-set/set.d.ts create mode 100644 packages/elastic-safer-lodash-set/set.js create mode 100644 packages/elastic-safer-lodash-set/setWith.d.ts create mode 100644 packages/elastic-safer-lodash-set/setWith.js create mode 100644 packages/elastic-safer-lodash-set/test/fp.test-d.ts create mode 100644 packages/elastic-safer-lodash-set/test/fp_assoc.test-d.ts create mode 100644 packages/elastic-safer-lodash-set/test/fp_assocPath.test-d.ts create mode 100644 packages/elastic-safer-lodash-set/test/fp_patch_test.js create mode 100644 packages/elastic-safer-lodash-set/test/fp_set.test-d.ts create mode 100644 packages/elastic-safer-lodash-set/test/fp_setWith.test-d.ts create mode 100644 packages/elastic-safer-lodash-set/test/index.test-d.ts create mode 100644 packages/elastic-safer-lodash-set/test/patch_test.js create mode 100644 packages/elastic-safer-lodash-set/test/set.test-d.ts create mode 100644 packages/elastic-safer-lodash-set/test/setWith.test-d.ts create mode 100644 packages/elastic-safer-lodash-set/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 4425ad3a12659d..a9ffe2850aa726 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/package.json b/package.json index 55a099b4e5c0c2..190eb6d7d94b43 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 00000000000000..b152df746bf260 --- /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 00000000000000..c2c910c637c015 --- /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 00000000000000..049225c0b6647c --- /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 00000000000000..aae17b35ac1306 --- /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 00000000000000..57fe84d0b07f2a --- /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 00000000000000..851e11690ea35d --- /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 00000000000000..76df38e98ff284 --- /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 00000000000000..851e11690ea35d --- /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 00000000000000..fcd7ff01e3cc88 --- /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 00000000000000..7d9cdb099dfd76 --- /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 00000000000000..16bc98658bdcd5 --- /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 00000000000000..0fb48694d736df --- /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 00000000000000..556e702f59f0f0 --- /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 00000000000000..e477d4b4bc7bab --- /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 00000000000000..aaff01f11a7af9 --- /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 00000000000000..d9edb25476c126 --- /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 00000000000000..9cbf19808edd70 --- /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 00000000000000..740f7c926ee408 --- /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 00000000000000..0ac4f4c9cf39f0 --- /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 00000000000000..f0f425661f605a --- /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 00000000000000..50d3edaf347170 --- /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 00000000000000..4d0aedf74bb0ff --- /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 00000000000000..c7cf2041355d07 --- /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 00000000000000..ead99c3d1de48f --- /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 00000000000000..4572367df415df --- /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 00000000000000..58fd89eb43e33c --- /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 00000000000000..16bc98658bdcd5 --- /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 00000000000000..69770629085496 --- /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 00000000000000..556e702f59f0f0 --- /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 00000000000000..aafa8a4db4be64 --- /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 00000000000000..7a1d6601b5e263 --- /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 00000000000000..8244458cd11803 --- /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 00000000000000..abbfa57eeb9635 --- /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 00000000000000..362ecf6f9d8660 --- /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 00000000000000..a5dbb24d33a052 --- /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 00000000000000..70a5197f72176d --- /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 00000000000000..ab29d7de5a03fb --- /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 00000000000000..03dfe260009e9f --- /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 00000000000000..9829ac3f04ce53 --- /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 00000000000000..b3ed93443c4fbb --- /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 00000000000000..bc1d1a3a7e413d --- /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 6083593431d9b3..dbdda3f38afd5b 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 824132b05732ce..3205edb87aa296 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 f6d15c7127d4eb..5ff7d2dd8ef6ea 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 f5cf74ae35e456..212b06a4c98954 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 f4781fcab1e20c..671e053b9550e5 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 cfe02735630f22..962c708c0d8df9 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 8bc65f3da7111e..972bcdba6b4032 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 165ef98be91d41..5bd339fbd7c965 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 0b19a996243119..cbc9984924c5dd 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 d4c2f733640609..50b31722dceeb2 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 eac3535c9d4ed6..806366dc3e0623 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 8e53178142180e..354bf9af042cf7 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 6287d47f99f623..4fc94d1992869b 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 07675bb0a6819e..c50f755fda9943 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 6e4dd9615d4230..4c9d2e870a7bb3 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 4c31f37f63dad7..5fbe62a074b29b 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 29e7cdc9669095..32998d3e776ef5 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 b8eacdd6a38972..6b1f1dfaeabb43 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 919274390d4d0a..9252fcf2a7dd84 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 b47a745519b1e7..c96b9146b4e2c0 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 d32ec29e6d701d..7805296258d9f5 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 454fefd4f8253f..968ececfe3be5d 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/public/search/expressions/build_tabular_inspector_data.ts b/src/plugins/data/public/search/expressions/build_tabular_inspector_data.ts index c4846a98f124fd..75a4464a8e61ee 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_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index 6260b92e1c11aa..c97a5d0638a6ac 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/discover/public/application/angular/context/api/context.predecessors.test.js b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js index fcde2ade0b2c6d..4987c77f4bf256 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 0f84aa82a989a6..ebf6e78585962d 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 951cf5fa279b5b..138284b5fece02 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 65cd7792a0189e..7d506e28794fd9 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 25cbb0631a652c..eafcbfda3db003 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 d273ffb4c1052c..adf54297c3133b 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 4d48095898b80e..f969778bbc6159 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 0e4d2ce2a926ca..f033a43806312e 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 faf270877217ba..1861fa621ecd1d 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 aac019a98e7907..0cd0c8391995bb 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 03547247032082..6490dfe252b292 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 edaf388e210606..8d80db4e4be1dc 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 c926c456da2190..3799a5b03ce461 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 32adf4f1f87c22..98a1226834bc69 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 b40bb8156098d3..eece5df61a7d10 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 9ea3cf087be909..ed259ccec0114c 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 0545e1a2d16f4c..65b599ed4a5f6c 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 5f2141cce93954..ef6fbaf9c53d03 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 7e81cb2736fc34..a6c9bfbb511c17 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 29264f8920e5d8..6715fa132c1b5f 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/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts index 6bc370be903df9..28b095335e93d8 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/beats_management/public/lib/configuration_blocks.ts b/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts index 5579c70e15017f..b486ba82689e81 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 4ffd2ff3e0c968..9dc7ee8da6d73b 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 b07857f13f6c6e..9b4406f607867d 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 3e88d60b40d5f5..5d0e6b3dd688e4 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/event_log/scripts/create_schemas.js b/x-pack/plugins/event_log/scripts/create_schemas.js index 2432a27e5c70d2..709096393471f9 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 22f7d3d3cd50a1..35fb66b2620d60 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 a81e11418cd6ae..3afc0d050e7364 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 8a21a97631fbbb..d0f0bd18b5d56c 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 f4f877c188d0d9..fdecb5f3d93153 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 2b65c42410723d..cdfb9d7cc99f30 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/monitoring/public/components/table/storage.js b/x-pack/plugins/monitoring/public/components/table/storage.js index 037839a2654c1a..1be8528d5ab232 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 83a79a30069f02..6aee89a9817d54 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 7d5661ccd7560e..e8862c47d4bf2c 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 d1bc3a0a7e3816..cc62e59986f1de 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 03de24916a6db0..8e0d125d122aa0 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 50a4df8a3ff57e..18db738bba38e6 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 58fc2e30972e5a..c2cf19471ecb2e 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 b9adcb725f0b8e..9b4f1d586a319a 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 a85d084f83d83b..ae5ae9320f0f41 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 45fdf1997d2149..726db1706758d9 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 158362cee3c7e9..77458a7d61e087 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 58e63a522e6096..651c6a0347c464 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 d89eb45ead75e3..83a73c53a0b604 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 93f79bfd892b99..d384cbb878a0ed 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/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index ef4e1ff05118b2..313c71375111cc 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/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx index 788ca95e2022ea..1b8177b2038ae5 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx @@ -5,7 +5,7 @@ */ import { EuiCodeEditor } from '@elastic/eui'; -import { set } from 'lodash/fp'; +import { set } from '@elastic/safer-lodash-set/fp'; import React from 'react'; import styled from 'styled-components'; 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 a182102329f050..de60bca73cedf6 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/toasters/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/toasters/index.test.tsx index 35036ef4b16b57..d366da1df9fd3f 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/containers/source/index.tsx b/x-pack/plugins/security_solution/public/common/containers/source/index.tsx index 9b7dfe84277c6e..8c03ab7b9f5086 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/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index 50578ef0a8e42e..9f550f87068bee 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/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 04aef6f07c60af..9899b38f445f97 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/store/timeline/reducer.test.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.test.ts index 0197ccc7eec05a..55451882d96faf 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/hosts/elasticsearch_adapter.ts b/x-pack/plugins/security_solution/server/lib/hosts/elasticsearch_adapter.ts index 796338e189d601..142d2a68faed0d 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 6eefdb0bfc5eca..fc25f1a48194e2 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 5f15d7ea08c549..b71dea96ec6627 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/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index 77ee3448cd06de..146cebabbb3826 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 d88abc9c9c9ea5..a20f4117f693d5 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 45be1df3e8d3bf..2ebe670bc43c1d 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 fd890a30cf7428..a52bf86499396c 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 8bdf7faf380e86..6c229cf30e165f 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 d9d02f4af882ef..1aeec518545a02 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 70b00070447a4f..9b8ce90d7fa825 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 0375b6ebf5d475..78e3fa2fc2582e 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 43874c9ee1dd1a..d2393e327e5ffe 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 934d1e338ed0c6..64ec8db37b1799 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 1000b6369ae3c1..4a77324da18be9 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 febf9c20b07a65..4e7ecf7feae096 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/functional/apps/maps/joins.js b/x-pack/test/functional/apps/maps/joins.js index 7534a1b09cc230..e447996a08dfe1 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/yarn.lock b/yarn.lock index b8aa559bc1d40d..0f144078ff46fa 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== From 25d143fdf79939b2fe4c37336edc235dadec80ff Mon Sep 17 00:00:00 2001 From: Lukas Olson Date: Wed, 15 Jul 2020 01:49:34 -0700 Subject: [PATCH 3/8] [Search] Add telemetry for data plugin search service (#70677) * [search] Refactor the way search strategies are registered/retrieved on the server * Fix types and tests and update docs * Fix failing test * Fix build of example plugin * Fix functional test * Make server strategies sync * Move strategy name into options * docs * Remove FE strategies * TypeScript of hell delete search explorer * Fix search interceptor OSS tests * typos * test cleanup * Update search interceptor tests and abort utils * [Search] Add telemetry for data plugin search service * Add tracking of average query time * Add tests and rename to collectors * Fix TS * Fixed interceptor jest tests * Add to kibana json * docs * Properly use observables rather than only during setup * Update or create * Swallow version conflict errors Co-authored-by: Liza K Co-authored-by: Elastic Machine --- ...plugin-plugins-data-public.plugin.setup.md | 4 +- ...ugins-data-public.searchinterceptordeps.md | 1 + ...ic.searchinterceptordeps.usagecollector.md | 11 ++ ...plugin-plugins-data-server.isearchsetup.md | 3 +- ...-plugins-data-server.isearchsetup.usage.md | 13 +++ src/plugins/data/kibana.json | 1 + src/plugins/data/public/plugin.ts | 3 +- src/plugins/data/public/public.api.md | 14 ++- .../collectors/create_usage_collector.test.ts | 107 ++++++++++++++++++ .../collectors/create_usage_collector.ts | 92 +++++++++++++++ .../data/public/search/collectors/index.ts | 21 ++++ .../data/public/search/collectors/types.ts | 36 ++++++ .../data/public/search/search_interceptor.ts | 14 ++- .../data/public/search/search_service.ts | 14 ++- src/plugins/data/public/search/types.ts | 21 +++- src/plugins/data/public/types.ts | 2 + src/plugins/data/server/plugin.ts | 2 +- .../data/server/saved_objects/index.ts | 3 +- .../{kql_telementry.ts => kql_telemetry.ts} | 0 .../server/saved_objects/search_telemetry.ts | 29 +++++ .../data/server/search/collectors/fetch.ts | 45 ++++++++ .../data/server/search/collectors/register.ts | 49 ++++++++ .../data/server/search/collectors/routes.ts | 50 ++++++++ .../data/server/search/collectors/usage.ts | 77 +++++++++++++ .../data/server/search/search_service.test.ts | 2 +- .../data/server/search/search_service.ts | 20 +++- src/plugins/data/server/search/types.ts | 6 + src/plugins/data/server/server.api.md | 2 + x-pack/plugins/data_enhanced/public/plugin.ts | 1 + .../public/search/search_interceptor.test.ts | 32 ++++++ .../public/search/search_interceptor.ts | 10 +- 31 files changed, 668 insertions(+), 17 deletions(-) create mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md create mode 100644 docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md create mode 100644 src/plugins/data/public/search/collectors/create_usage_collector.test.ts create mode 100644 src/plugins/data/public/search/collectors/create_usage_collector.ts create mode 100644 src/plugins/data/public/search/collectors/index.ts create mode 100644 src/plugins/data/public/search/collectors/types.ts rename src/plugins/data/server/saved_objects/{kql_telementry.ts => kql_telemetry.ts} (100%) create mode 100644 src/plugins/data/server/saved_objects/search_telemetry.ts create mode 100644 src/plugins/data/server/search/collectors/fetch.ts create mode 100644 src/plugins/data/server/search/collectors/register.ts create mode 100644 src/plugins/data/server/search/collectors/routes.ts create mode 100644 src/plugins/data/server/search/collectors/usage.ts 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 51bc46bbdccc83..7bae595e75ad08 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 abd57f3a9568bf..1291af5359887d 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 00000000000000..21afce19276769 --- /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 ca8ad8fdc06eac..3afba80064f084 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 00000000000000..85abd9d9dba980 --- /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/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index 2ffd0688b134ee..b4f20ec6225e2c 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 4040781bb2f01a..323a32ea362ac7 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 00000000000000..cb1b2b65c17c84 --- /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 00000000000000..afe127c00b5dd5 --- /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 00000000000000..bb85532fd3ab59 --- /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/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index 8edbfd94deb383..84e24114a9e6c4 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 a27eba21714bbe..064e16014cb70a 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/types.ts b/src/plugins/data/public/search/types.ts index 5c4bb42a5948d0..ec74275f35c041 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 aaef403979de6a..6d671272514244 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 bcf1f4f8ab60bb..8fa32f9bd564f9 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 00000000000000..ab0ea93edd49e2 --- /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 00000000000000..38fb517e3c3f66 --- /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 00000000000000..c43c572c2edbb9 --- /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 25143fa09e6bff..8c2ed96503003e 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 20f9a7488893f7..5686023e9a667a 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 12f1a1a508bd23..25dc890e0257db 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 4dc60056ed9184..c5d19fef9531ea 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/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 231f1d434b8922..bdf3f6a0acf90c 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 9f018f5b718c73..9bd1ffddeaca8b 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 c0e2a6bd113eb4..d1ed410065248b 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 From a282af7ca3453f616395063cbd20fb00be9f66b0 Mon Sep 17 00:00:00 2001 From: Ross Wolf <31489089+rw-access@users.noreply.github.com> Date: Wed, 15 Jul 2020 02:53:02 -0600 Subject: [PATCH 4/8] [Detection Rules] Add 7.9 rules (#71808) Co-authored-by: Elastic Machine --- .../prepackaged_rules/elastic_endpoint.json | 7 +++++ .../rules/prepackaged_rules/index.ts | 10 +++++++ .../ml_cloudtrail_error_message_spike.json | 29 +++++++++++++++++++ .../ml_cloudtrail_rare_error_code.json | 29 +++++++++++++++++++ .../ml_cloudtrail_rare_method_by_city.json | 29 +++++++++++++++++++ .../ml_cloudtrail_rare_method_by_country.json | 29 +++++++++++++++++++ .../ml_cloudtrail_rare_method_by_user.json | 29 +++++++++++++++++++ .../ml_linux_anomalous_network_activity.json | 5 +--- ...linux_anomalous_network_port_activity.json | 2 +- .../ml_linux_anomalous_network_service.json | 2 +- ..._linux_anomalous_network_url_activity.json | 2 +- .../ml_linux_anomalous_process_all_hosts.json | 4 +-- .../ml_linux_anomalous_user_name.json | 2 +- .../ml_packetbeat_dns_tunneling.json | 2 +- .../ml_packetbeat_rare_dns_question.json | 2 +- .../ml_packetbeat_rare_server_domain.json | 2 +- .../ml_packetbeat_rare_urls.json | 2 +- .../ml_packetbeat_rare_user_agent.json | 2 +- .../ml_rare_process_by_host_linux.json | 4 +-- .../ml_rare_process_by_host_windows.json | 4 +-- .../ml_suspicious_login_activity.json | 2 +- ...ml_windows_anomalous_network_activity.json | 4 +-- .../ml_windows_anomalous_path_activity.json | 2 +- ...l_windows_anomalous_process_all_hosts.json | 4 +-- ...ml_windows_anomalous_process_creation.json | 2 +- .../ml_windows_anomalous_script.json | 2 +- .../ml_windows_anomalous_service.json | 2 +- .../ml_windows_anomalous_user_name.json | 2 +- ...windows_rare_user_type10_remote_login.json | 2 +- 29 files changed, 189 insertions(+), 30 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_error_message_spike.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_error_code.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_city.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_country.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/ml_cloudtrail_rare_method_by_user.json 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 6d2f198c9b9432..396803086552ee 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 880caca03cb7de..f2e2137eec41b3 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 00000000000000..0730c421cf5f2e --- /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 00000000000000..8003cdd7504c76 --- /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 00000000000000..2c54dbd03daba5 --- /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 00000000000000..68cbf4979a933f --- /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 00000000000000..e4ec651e719346 --- /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 3ef426af909ff2..bf86f78fe3e72e 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 add1c2941970e0..a588a6f5bcb0a5 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 af5b331f4cb04d..5c56845024eb29 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 89a6955fd1781f..3b3f751dfc60b0 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 6e73e4dd6dc94f..8475410735f34f 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 c910fb552f9669..3e4b1f15fdce4d 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 b78c4d3459b851..1352fde91b59b7 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 970962dd75eed6..b16e67052a2122 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 f9465a329e9735..a8971300fe11b7 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 e22f9975b54e44..469f5d741ef6e4 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 2ce6f44d90593c..ebcf4f987e9ded 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 c62666134c84e3..385158dd6b65de 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 5d86637553eab8..d0a99b32d47132 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 93413f8d0a8a86..f309debcdffe95 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 a24e1c1c9eb0bb..0ab591097f975c 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 9be69a6bfdcbed..a7b309e6d7fcdb 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 79792d2fd328b4..bc6346f457b651 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 c031e7177abe6e..97351a1f517b35 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 7d05a0286ea97f..d0dc8d7e40fa21 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 7870f75b3d075e..b7e7a0357e1185 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 42e6740beaa0cb..26bd6837cbde5b 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 2043af2b8dcb4e..b69e759120ce4d 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" ], From e4f7acb90fce13b846391d08c09907798bd407d3 Mon Sep 17 00:00:00 2001 From: Pedro Jaramillo Date: Wed, 15 Jul 2020 11:35:08 +0200 Subject: [PATCH 5/8] [Security Solution][Exception Modal] Create endpoint exception list if it doesn't already exist (#71807) * use createEndpointList api * fix lint * update list id constant * add schema test * add api test --- .../create_endpoint_list_schema.test.ts | 58 +++++++++++++++++ .../response/create_endpoint_list_schema.ts | 15 +++++ .../lists/common/schemas/response/index.ts | 1 + x-pack/plugins/lists/common/shared_exports.ts | 3 + .../lists/public/exceptions/api.test.ts | 36 +++++++++++ x-pack/plugins/lists/public/exceptions/api.ts | 35 +++++++++++ .../plugins/lists/public/exceptions/types.ts | 5 ++ x-pack/plugins/lists/public/shared_exports.ts | 1 + .../common/shared_imports.ts | 2 + ...tch_or_create_rule_exception_list.test.tsx | 9 ++- ...se_fetch_or_create_rule_exception_list.tsx | 63 ++++++++++++------- .../public/shared_imports.ts | 1 + 12 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/create_endpoint_list_schema.ts 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 00000000000000..1f51140005e597 --- /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 00000000000000..4653b73347f72e --- /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 fb6f17a896ddb5..deca06ad99feac 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 7bb565792969cb..dc0a9aa5926ef1 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 cd54c24e95e2f6..1414d828fa6d4f 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 a581cfd08ecc19..4d9397ec0adc6c 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 1b4e09b07f1de0..f99323b3847814 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 57fb2f90b64045..56341035f839fd 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/security_solution/common/shared_imports.ts b/x-pack/plugins/security_solution/common/shared_imports.ts index a607906e1b92ab..7fb94cea7b6129 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/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 afc3568fd6c655..7bef771d367f30 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 245ce192b3cfa6..b238e25f6de592 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/shared_imports.ts b/x-pack/plugins/security_solution/public/shared_imports.ts index 5d4579b427f18f..9939345324f110 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'; From 0c0aaf0e6a0b5ad18902b6573664270b59ede10f Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Wed, 15 Jul 2020 04:12:34 -0600 Subject: [PATCH 6/8] [Security Solution] Full screen timeline, Collapse event (#71786) ## Full screen Timeline & Timeline-based views - Adds a _Full screen_ mode to Timeline, and all Timeline-based views, including: - Detections - Detections > Rule details - Hosts > Events - Hosts > External alerts - Network > External alerts - Timeline - Enter full screen from any Resolver - Adds a `Collapse event` action for quickly collapsing an expanded Timeline event - Hides the `Add to case action` in timeline-based Resolver views, so those actions are only enabled in Timeline (a `TODO` from https://github.com/elastic/kibana/pull/70111) ### Full screen detections ![full-screen-detections](https://user-images.githubusercontent.com/4459398/87493332-d348f280-c609-11ea-9399-126d2259daa2.gif) ### Enter full screen from any Resolver ![full-screen-resolver](https://user-images.githubusercontent.com/4459398/87493348-de038780-c609-11ea-86a3-52ab24055e38.gif) ### Full screen Timeline ![full-screen-timeline](https://user-images.githubusercontent.com/4459398/87493394-f4114800-c609-11ea-8d62-4add291d937a.gif) ### Collapse event ![collapse-event](https://user-images.githubusercontent.com/4459398/87493408-fa9fbf80-c609-11ea-88c8-fa87d82d1eb1.gif) ### Sort tooltip ![sort-tooltip](https://user-images.githubusercontent.com/4459398/87493417-012e3700-c60a-11ea-9905-44e3b7cfe60f.gif) --- .../security_solution/common/constants.ts | 2 + .../public/app/home/index.tsx | 2 +- .../components/all_cases/columns.test.tsx | 1 + .../cases/components/all_cases/index.test.tsx | 2 + .../components/all_cases_modal/index.test.tsx | 1 + .../cases/components/case_view/index.test.tsx | 1 + .../configure_cases/button.test.tsx | 1 + .../use_push_to_service/index.test.tsx | 2 + .../components/alerts_viewer/alerts_table.tsx | 3 + .../common/components/alerts_viewer/index.tsx | 47 +- .../components/autocomplete/helpers.test.ts | 1 + .../components/charts/barchart.test.tsx | 1 + .../charts/draggable_legend.test.tsx | 1 + .../charts/draggable_legend_item.test.tsx | 1 + .../drag_and_drop/draggable_wrapper.test.tsx | 1 + .../draggable_wrapper_hover_content.test.tsx | 1 + .../components/draggables/index.test.tsx | 1 + .../__snapshots__/event_details.test.tsx.snap | 27 + .../event_details/event_details.test.tsx | 30 + .../event_details/event_details.tsx | 54 +- .../event_fields_browser.test.tsx | 1 + .../event_details/stateful_event_details.tsx | 13 +- .../events_viewer/events_viewer.test.tsx | 1 + .../events_viewer/events_viewer.tsx | 64 +- .../components/events_viewer/index.test.tsx | 1 + .../common/components/events_viewer/index.tsx | 5 + .../components/exit_full_screen/index.tsx | 49 + .../exit_full_screen/translations.ts | 11 + .../filters_global/filters_global.tsx | 2 + .../common/components/header_global/index.tsx | 9 +- .../header_page/editable_title.test.tsx | 1 + .../components/header_page/index.test.tsx | 1 + .../components/header_page/title.test.tsx | 1 + .../__snapshots__/index.test.tsx.snap | 4 +- .../components/header_section/index.tsx | 12 +- .../components/ml/entity_draggable.test.tsx | 2 + .../ml/score/anomaly_score.test.tsx | 2 + .../ml/score/anomaly_scores.test.tsx | 2 + .../ml/score/draggable_score.test.tsx | 4 +- .../get_anomalies_host_table_columns.test.tsx | 4 +- ...t_anomalies_network_table_columns.test.tsx | 1 + .../public/common/components/page/index.tsx | 8 +- .../common/components/tables/helpers.test.tsx | 6 +- .../common/components/top_n/index.test.tsx | 1 + .../common/components/top_n/top_n.test.tsx | 1 + .../common/components/wrapper_page/index.tsx | 8 +- .../containers/use_full_screen/index.tsx | 39 + .../public/common/store/inputs/actions.ts | 5 + .../public/common/store/inputs/helpers.ts | 16 + .../public/common/store/inputs/model.ts | 1 + .../public/common/store/inputs/reducer.ts | 9 + .../public/common/store/inputs/selectors.ts | 7 + .../alerts_histogram.test.tsx | 1 + .../alerts_histogram_panel/index.test.tsx | 1 + .../components/alerts_table/index.test.tsx | 1 + .../components/alerts_table/index.tsx | 3 + .../index.test.tsx | 1 + .../rules/all_rules_tables/index.test.tsx | 1 + .../load_empty_prompt.test.tsx | 1 + .../detection_engine.test.tsx | 51 +- .../detection_engine/detection_engine.tsx | 103 +- .../rules/all/columns.test.tsx | 1 + .../detection_engine/rules/all/index.test.tsx | 1 + .../rules/create/index.test.tsx | 1 + .../rules/details/index.test.tsx | 51 +- .../detection_engine/rules/details/index.tsx | 261 ++-- .../rules/edit/index.test.tsx | 1 + .../detection_engine/rules/index.test.tsx | 1 + .../authentications_table/index.test.tsx | 1 + .../components/hosts_table/index.test.tsx | 1 + .../hosts/components/kpi_hosts/index.test.tsx | 1 + .../uncommon_process_table/index.test.tsx | 1 + .../hosts/pages/details/details_tabs.test.tsx | 1 + .../public/hosts/pages/display.tsx | 13 + .../public/hosts/pages/hosts.tsx | 77 +- .../navigation/events_query_tab_body.tsx | 49 +- .../components/direction/direction.test.tsx | 1 + .../embeddables/embedded_map.test.tsx | 1 + .../line_tool_tip_content.test.tsx | 2 + .../map_tool_tip/map_tool_tip.test.tsx | 1 + .../point_tool_tip_content.test.tsx | 2 + .../index.test.tsx | 1 + .../network/components/ip/index.test.tsx | 1 + .../components/ip_overview/index.test.tsx | 1 + .../components/kpi_network/index.test.tsx | 1 + .../network_dns_table/index.test.tsx | 1 + .../network_http_table/index.test.tsx | 1 + .../index.test.tsx | 1 + .../network_top_n_flow_table/index.test.tsx | 1 + .../network/components/port/index.test.tsx | 1 + .../source_destination/index.test.tsx | 1 + .../source_destination_ip.test.tsx | 1 + .../components/tls_table/index.test.tsx | 1 + .../components/users_table/index.test.tsx | 1 + .../public/network/pages/network.tsx | 95 +- .../alerts_by_category/index.test.tsx | 1 + .../components/event_counts/index.test.tsx | 1 + .../endpoint_overview/index.test.tsx | 2 + .../components/host_overview/index.test.tsx | 1 + .../components/overview_host/index.test.tsx | 1 + .../overview_network/index.test.tsx | 1 + .../certificate_fingerprint/index.test.tsx | 1 + .../components/duration/index.test.tsx | 1 + .../field_renderers/field_renderers.test.tsx | 1 + .../fields_browser/category.test.tsx | 1 + .../fields_browser/field_browser.test.tsx | 1 + .../fields_browser/field_items.test.tsx | 1 + .../fields_browser/field_name.test.tsx | 1 + .../fields_browser/fields_pane.test.tsx | 1 + .../components/fields_browser/index.test.tsx | 1 + .../header_with_close_button/index.test.tsx | 1 + .../components/flyout/pane/index.tsx | 16 +- .../flyout/pane/timeline_resize_handle.tsx | 14 +- .../components/graph_overlay/index.tsx | 99 +- .../components/ja3_fingerprint/index.test.tsx | 1 + .../components/netflow/index.test.tsx | 1 + .../components/open_timeline/index.test.tsx | 1 + .../open_timeline/open_timeline.test.tsx | 1 + .../open_timeline_modal_body.test.tsx | 1 + .../timelines_table/actions_columns.test.tsx | 1 + .../timelines_table/common_columns.test.tsx | 1 + .../timelines_table/extended_columns.test.tsx | 1 + .../icon_header_columns.test.tsx | 1 + .../timelines_table/index.test.tsx | 1 + .../__snapshots__/index.test.tsx.snap | 1046 +++++++++-------- .../body/column_headers/helpers.test.ts | 7 +- .../body/column_headers/index.test.tsx | 35 +- .../timeline/body/column_headers/index.tsx | 64 +- .../body/column_headers/translations.ts | 4 + .../components/timeline/body/constants.ts | 4 +- .../body/data_driven_columns/index.test.tsx | 1 + .../timeline/body/events/stateful_event.tsx | 1 + .../components/timeline/body/helpers.ts | 35 + .../components/timeline/body/index.test.tsx | 1 + .../components/timeline/body/index.tsx | 48 +- .../timeline/body/renderers/args.test.tsx | 1 + .../renderers/auditd/generic_details.test.tsx | 1 + .../auditd/generic_file_details.test.tsx | 1 + .../primary_secondary_user_info.test.tsx | 1 + .../session_user_host_working_dir.test.tsx | 1 + .../body/renderers/bytes/index.test.tsx | 1 + .../dns/dns_request_event_details.test.tsx | 1 + .../dns_request_event_details_line.test.tsx | 2 +- .../renderers/empty_column_renderer.test.tsx | 1 + .../endgame_security_event_details.test.tsx | 1 + ...dgame_security_event_details_line.test.tsx | 1 + .../renderers/exit_code_draggable.test.tsx | 1 + .../body/renderers/file_draggable.test.tsx | 1 + .../body/renderers/formatted_field.test.tsx | 1 + .../renderers/get_column_renderer.test.tsx | 1 + .../body/renderers/get_row_renderer.test.tsx | 1 + .../body/renderers/host_working_dir.test.tsx | 1 + .../netflow/netflow_row_renderer.test.tsx | 1 + .../parent_process_draggable.test.tsx | 1 + .../renderers/plain_column_renderer.test.tsx | 1 + .../body/renderers/process_draggable.test.tsx | 1 + .../body/renderers/process_hash.test.tsx | 1 + .../suricata/suricata_details.test.tsx | 1 + .../suricata/suricata_row_renderer.test.tsx | 1 + .../suricata/suricata_signature.test.tsx | 1 + .../body/renderers/system/auth_ssh.test.tsx | 1 + .../renderers/system/generic_details.test.tsx | 1 + .../system/generic_file_details.test.tsx | 1 + .../body/renderers/system/package.test.tsx | 1 + .../renderers/user_host_working_dir.test.tsx | 1 + .../body/renderers/zeek/zeek_details.test.tsx | 1 + .../renderers/zeek/zeek_row_renderer.test.tsx | 1 + .../renderers/zeek/zeek_signature.test.tsx | 1 + .../sort_indicator.test.tsx.snap | 15 +- .../body/sort/sort_indicator.test.tsx | 43 +- .../timeline/body/sort/sort_indicator.tsx | 26 +- .../components/timeline/body/translations.ts | 21 + .../timeline/expandable_event/index.tsx | 3 + .../components/timeline/index.test.tsx | 1 + .../timeline/properties/index.test.tsx | 1 + .../properties/use_create_timeline.test.tsx | 20 +- .../properties/use_create_timeline.tsx | 19 +- .../components/timeline/timeline.test.tsx | 1 + .../timeline/epic_local_storage.test.tsx | 1 + 179 files changed, 1927 insertions(+), 870 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/exit_full_screen/translations.ts create mode 100644 x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx create mode 100644 x-pack/plugins/security_solution/public/hosts/pages/display.tsx diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e5dd109007eab4..b39a038c4cc3cb 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/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 8f03945df437c8..41b9252c67b8a6 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 9db8adbf9346fa..654a5f5c4a599b 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 d8acda8ec4f330..23cabd6778cc04 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 f4fd7cc67224ff..b93de014f5c188 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 2832a28fbb7cd5..b93df325b5a8be 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 8d14b2357f4501..6fb693e47560d6 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 d17a2bd2159108..eb80eaff578f57 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/common/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/alerts_table.tsx index 841a1ef09ede6c..e30560f6c81477 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 a31cb4f2a8bfd8..832b14f00159a6 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 c2e8e560844525..cfe23b9391ec0a 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 49c421c5680ba7..8617388f4ffb5d 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 a11fdda3d1b3a8..8fd2fa1fdef12b 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 8ff75c8ca0780a..9f6e614c3c2852 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 d1b3b671307d1c..da68280ed760cb 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 432e369cdd0f63..3f06a8168b5ce6 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 3d80a2605418e8..ff1679875865cd 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 9ca9cd6cce3893..ebaf60e7078f01 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 c28757a90c7026..53ec14380d5bc6 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 674eb3325efc2c..8c1f69279d31cd 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 6e6ba4911be26c..3f474da102ca46 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/exit_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx new file mode 100644 index 00000000000000..8c5ad95a8de0e9 --- /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 00000000000000..72d451cfdfc143 --- /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 b4d8c790002b2f..65901ec589daf2 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 ba4f782499802c..3a8f2f0c16b963 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 1e9a2e06474b92..30e992380e7c6a 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 30f510509913ab..15711663116f93 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 5187a32ac9721a..fd7a0a5d96e009 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 53b41e2240de23..f2d2d23d60fb13 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 43245121dd3936..f49001bd5d7af9 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 c48a5590b49cfd..e9940d088e606b 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 f7fa0ac0a8be15..434cbd8ada88e2 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 d0b923002d6d4b..a900c3e49f912a 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 f7759bb74c3abc..673d1a1cdb72e3 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 b90946c534f3ac..d370a901a62627 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 79277c46e1c9d0..69a4e383413f28 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 f539bb7831c1c1..9a5654ed6475fa 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/tables/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/tables/helpers.test.tsx index 7ceb34755648e7..b28c7e70b8ae85 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/top_n/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/index.test.tsx index b393e9ae6319b2..1e93fdb936728d 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 e5a1fb61202850..667d1816e8f079 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 3223c5058fa7f3..03f9b43678003a 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/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx new file mode 100644 index 00000000000000..b8050034d34a6c --- /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 efad0638b2971b..5d00882f778c07 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 1883f05dc9e9d5..82a2072056d9f9 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 358124405c1469..a8db48c7b31bba 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 40d9ad777acdeb..a94f0f6ca24ee5 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 0eee5ebbfbf772..9feb2f87d7e087 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 09883e342f998e..692d22b115b488 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 4cbfa59aac5826..533f13e6781a6d 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 cc3a47017a835a..d5688d84e97597 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 405ba0719a9106..30cfe2d02354fb 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 a2685017f86d69..efce1dc0263530 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 d841af69a7537d..59334b53faa173 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 89f6399071dd35..a41da908085bc6 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 f4004a66c8f80c..e7a8c4854fa9e7 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 aef9f2adcbcc80..acafb15db3448b 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 2e7ef1180f4e34..7eb5c3a5353772 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 b37d91cc2be3b0..a3885eac5377c2 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 8d004829a34f07..63126da0b9bb5c 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 c4a941d845f167..8268a550257c9c 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 8e221445a95d3d..fee38ad3c62892 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 71cf056f3eb626..6bd0390d014a37 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 5140137ce1b99b..30874e88747604 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 d2d823f6256900..9ac4f7125f34dc 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/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/certificate_fingerprint/index.test.tsx index a5edffc2a099ad..b31094b07a829e 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 94123000888aab..c38eb23195c064 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 cf12740d93a18d..c3b67e3300459c 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 16174e92b3c37e..62306046c7b8cc 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 7c4e3d435e1ed5..9340ee8cf0c7f8 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 e4c9621c2f71c7..f4f8adc9f04196 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 1f917c664e813a..44e4818830acd2 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 b55bbfc0237745..c2ddba6bd88c32 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 ed3f957ad11a8c..a3c7440bece24f 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 9b7d4c3266c56f..cfdca8950d314f 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/pane/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/pane/index.tsx index 1616738897b0ac..f41d318ba95877 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 741ed0a09ebf6b..7192580f2426d3 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 085f0863c7b278..9f20c7f6c1571a 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 113c2dca975064..899a6d7486f945 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 24f8d910b4febb..c2026a71ac6ff4 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/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index e2def46b936be8..e671244d97b57d 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 f42914c86f46b2..57a6431a06b90a 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 1d08f0296ce0de..12df17ceba6669 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 9bec06e5ed917f..eddfdf6e01df26 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 112329ac1738d8..b8b2630e09c6e0 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 390ce8c0b69400..0f2b3cdea4eec8 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 f1df605c072dde..6e3f0037003b13 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 f230a964c3c2a9..649e38865f9071 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 a5610cabc1774b..13c2b14d26eca7 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 588f4074168031..21e135218c8710 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 6a7734ce3161d4..6685ce7d7a018f 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 b139aa1a7a9a60..a3e177604fbd47 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 becdece2c7612e..1ebfa957b654ff 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 6b6ae3c3467b54..576dedfc28b1b7 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 07ef165a6d911f..28a4bf6d8ac51e 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 344fbb59bbe575..3236482e6bc273 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 317f1ed20119b6..067cea175c99bd 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 2df6a39f1a3dfa..b36f1dcc03261c 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 83e44b77802b70..e971dc6c8e1e2c 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 e7e7d1d47f4783..d1e8c8aacca47b 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 b4c95d383593af..726273bc90ad8d 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 0990280879a140..750fbc0014464a 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 41e35427ae2542..54af8c89b15d7a 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 d1e67c25bd79c5..ef3e2f72d04732 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 0160c62ea40acf..4a0eff1ecf1b2a 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 ba77709459c28e..e2dff4e13b80d1 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 1d46e4c3eb02d7..de3eb01612b2af 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 1c7eaef8936510..6c9dd5092e7c1a 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 e84cb93b871781..47064fa02458af 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 b2b4b021e5db53..6d4b2b518b5824 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 4471c26ef8fd7b..98a706d5836a0a 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 70e0e74675cd2f..a038ceab15b446 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 3e055682d27a46..867cf421464851 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 12b093bd517c8c..d1ed5e86e72e56 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 0b3ea0ce6e4303..0c7fbd08ba98c8 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 85a000bbcaf638..2dadbabd0ae16a 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 5140b9abc60ef3..8a8b40198bdba0 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 0a173f766ae19b..86d39da478c6d5 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 b7c2cb7032cc24..9199278c57f7a0 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 91ae94940f7f4f..7a7715c86b5c53 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 55cc61edb064e1..e46a5abc6a9fd0 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 14f147c61fca35..3b9752224e2c16 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 d36d24f41224c7..7d700732a64099 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 a0cad2b059a4bd..61e1a28cc7d7d4 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 4e4e1a0b7bf6fa..791ae8aadc69ce 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 8efd8e19443311..2f2fe2606d1320 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 6c7a74d840d016..52c232f377f79a 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 56f9452ba40b8d..36b69790726e90 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 7f460d30d709c0..d09837e344d7b5 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 04b0e6e5fcfae4..434be7b23aeee8 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 2eed6aaf20335f..23c38f83b89d40 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 a0c5b3a8e8c65c..3b1ce431bfc874 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 5674c18010f678..ebe6bfcbc2e9a5 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 1467813eaf1545..dcaedb90e7252e 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 c148e2f6c6295d..8b842dfa2197ed 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 20467af290b195..c57002023b79de 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 b08c6afcaf4a69..269cd14b5973cc 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 ce96e4e50dea07..8b75f8b398ac1c 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 ce99304c676eeb..efb19275336db4 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 68a3362b721d83..8f548f16cf1d6b 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 fb05b056cdf820..f418491ac4e477 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 58c46af5606f47..555b22eff0c91e 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 bd1fac9b054742..1e0e85d4a48d91 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, From 3c9fa99d685b75150f1c6012fd27ab5eac50a5ba Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Wed, 15 Jul 2020 07:26:24 -0400 Subject: [PATCH 7/8] [Security Solution][Detection Engine] - Update exceptions logic (#71512) Co-authored-by: Elastic Machine Co-authored-by: Yara Tercero --- .../scripts/lists/new/items/ip_item.json | 2 +- .../scripts/lists/new/items/keyword_item.json | 2 +- .../build_exceptions_query.test.ts | 976 +++++------------- .../build_exceptions_query.ts | 118 +-- .../detection_engine/get_query_filter.test.ts | 130 +-- .../detection_engine/get_query_filter.ts | 16 +- .../common/detection_engine/utils.test.ts | 105 ++ .../common/detection_engine/utils.ts | 17 + .../signals/filter_events_with_list.ts | 20 +- .../signals/get_filter.test.ts | 85 +- .../signals/single_search_after.ts | 1 + .../detection_engine/signals/utils.test.ts | 49 - .../lib/detection_engine/signals/utils.ts | 8 +- 13 files changed, 562 insertions(+), 967 deletions(-) create mode 100644 x-pack/plugins/security_solution/common/detection_engine/utils.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/utils.ts 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 563139c40c0cad..c2238890496bbd 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 96d925c157490d..0848dc4c1bd94f 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/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 26a219507c3aee..caf2dfb761ed0d 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 a70e6a66385899..fc4fbae02b8fbd 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 c19ef45605f83f..a8eb4e7bbb15bb 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 6584373b806d8e..a41589b5d0231d 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 00000000000000..99680ffe41d444 --- /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 00000000000000..fa1812235f897c --- /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/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 8af08a02f41527..654ace290f85fe 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 f34879781e0b02..a5740d7719f475 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 5667f2e47b6d71..92ce7a28361155 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 a6130a20f9c520..a610970907bf8f 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 0b95ff6786b013..1c59a4b7ea5d0f 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, From f69edbd89bb5a3b3b4f4325156c9a4174f4787d7 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 15 Jul 2020 07:17:54 -0500 Subject: [PATCH 8/8] [APM] Add error rates to Service Map popovers (#69520) Make the `getErrorRate` function used in the error rate charts additionally take `service.environment` as a filter and have it return the `average` of the values. Call that function in the API for the service map metrics. Fixes #68160. Co-authored-by: cauemarcondes --- x-pack/plugins/apm/common/service_map.ts | 4 +- .../app/ServiceMap/Popover/Contents.tsx | 4 +- .../app/ServiceMap/Popover/Info.tsx | 4 +- .../ServiceMap/Popover/Popover.stories.tsx | 156 +++++-- ...ricFetcher.tsx => ServiceStatsFetcher.tsx} | 31 +- ...iceMetricList.tsx => ServiceStatsList.tsx} | 36 +- .../get_parsed_ui_filters.ts | 23 + .../get_service_map_service_node_info.test.ts | 81 ++++ .../get_service_map_service_node_info.ts | 100 ++-- .../lib/transaction_groups/get_error_rate.ts | 11 +- .../plugins/apm/server/routes/service_map.ts | 17 +- .../apm/server/routes/transaction_groups.ts | 10 +- .../translations/translations/ja-JP.json | 7 - .../translations/translations/zh-CN.json | 7 - .../trial/tests/service_maps.ts | 428 ++++++++++-------- 15 files changed, 568 insertions(+), 351 deletions(-) rename x-pack/plugins/apm/public/components/app/ServiceMap/Popover/{ServiceMetricFetcher.tsx => ServiceStatsFetcher.tsx} (78%) rename x-pack/plugins/apm/public/components/app/ServiceMap/Popover/{ServiceMetricList.tsx => ServiceStatsList.tsx} (75%) create mode 100644 x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_parsed_ui_filters.ts create mode 100644 x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index b50db270ef5442..7f46fc685d9ca9 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 c696a93773cebf..78466b2659bb74 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 223d342e6799f3..094cf032c4c9d3 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 ccf147ed1d90d4..20f6f92f9995f2 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 957678877a134d..9e8f1f7a0171e6 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 f82f434e7ded11..4a1a291249f50e 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/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 00000000000000..324da199807c7d --- /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 00000000000000..1e0d001340edf4 --- /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 dd5d19b620c513..0f7136d6d74a4d 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 5b66f7d7a45e72..6a1ee8daad7c71 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 50123131a42e76..971e247d98986e 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 dca2fb1d9b2955..813d757c7c33ea 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/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b54f88f83fbe00..a4100ae914b25e 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 389e0083d5a9f8..69e37f3f9f9f07 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/test/apm_api_integration/trial/tests/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps.ts index cf265c3fb67375..0b370f6a30a8bd 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, }, - ], + }); }); }); });