From 7a13bb22b6cabe55a2707b6cc409fd7b8cfd37ed Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 19 Mar 2020 17:34:20 +0100 Subject: [PATCH 01/22] [Cross Cluster Replication] NP Shim (#60121) (#60635) * Public in WiP state, removed all 'ui/' imports * First iteration of public shimmed and working * A whole lotta WIP server side * Server-side to using the NP router + client side changes Updated the client code to properly encode requests to the server. Did first E2E test. Route tests are probably broken, need to fix them. * Removed unused error wrapping code * Update client Jest tests * Add breadcrumbs service mock * Fix server side Jest tests * Add helper functions file for server side Jest tests * Fix API integration tests * Fixed boolean logic mistake in due to refactor in index mgmt ext. Also migrated to the a more NP friendly version of index mgmt extension. * Remove unused import * Clean up some cruft and refactor URL variable names * Fix stringification of body and fix boolean server logic * Fix mocha Folder called __tests__ with Jest tests was breaking mocha. * Refactor to Jest test * Fix types issues in jest test * Migrate to new config-schema API Co-authored-by: Elastic Machine Co-authored-by: Elastic Machine --- .../auto_follow_pattern_add.test.js | 2 +- .../auto_follow_pattern_edit.test.js | 3 +- .../auto_follow_pattern_list.test.js | 10 +- .../follower_index_add.test.js | 3 +- .../follower_index_edit.test.js | 3 +- .../follower_indices_list.test.js | 9 - .../auto_follow_pattern_add.helpers.js | 6 +- .../auto_follow_pattern_edit.helpers.js | 6 +- .../auto_follow_pattern_list.helpers.js | 6 +- .../helpers/follower_index_add.helpers.js | 6 +- .../helpers/follower_index_edit.helpers.js | 6 +- .../helpers/follower_index_list.helpers.js | 6 +- .../helpers/home.helpers.js | 6 +- .../helpers/http_requests.js | 12 +- .../helpers/setup_environment.js | 11 +- .../__jest__/client_integration/home.test.js | 1 + .../common/constants/{app.js => app.ts} | 0 .../constants/{base_path.js => base_path.ts} | 0 .../common/constants/{index.js => index.ts} | 0 .../common/constants/{plugin.js => plugin.ts} | 0 .../constants/{settings.js => settings.ts} | 0 .../cross_cluster_replication/index.js | 20 +- .../app/services/documentation_links.js | 14 - .../cross_cluster_replication/public/index.js | 1 - .../public/index.scss | 2 +- .../public/{ => np_ready}/app/_app.scss | 0 .../public/{ => np_ready}/app/app.js | 8 +- .../auto_follow_pattern_form.test.js.snap | 0 ...to_follow_pattern_action_menu.container.ts | 0 .../auto_follow_pattern_action_menu.tsx | 0 .../auto_follow_pattern_action_menu/index.ts | 0 .../auto_follow_pattern_delete_provider.d.ts | 0 .../auto_follow_pattern_delete_provider.js | 2 +- .../components/auto_follow_pattern_form.js | 4 +- .../auto_follow_pattern_form.test.js | 0 .../auto_follow_pattern_indices_preview.js | 0 .../auto_follow_pattern_page_title.js | 4 +- .../auto_follow_pattern_request_flyout.js | 2 +- .../follower_index_form.test.js.snap | 0 .../advanced_settings_fields.js | 6 +- .../follower_index_form.js | 28 +- .../follower_index_form.test.js | 0 .../follower_index_request_flyout.js | 2 +- .../components/follower_index_form/index.js | 0 .../components/follower_index_page_title.js | 4 +- .../follower_index_pause_provider.js | 2 +- .../follower_index_resume_provider.js | 2 +- .../follower_index_unfollow_provider.js | 2 +- .../app/components/form_entry_row.js | 0 .../{ => np_ready}/app/components/index.js | 0 .../components/remote_clusters_form_field.js | 2 +- .../components/remote_clusters_provider.js | 0 .../app/components/section_error.js | 8 +- .../app/components/section_loading.js | 0 .../app/components/section_unauthorized.js | 0 .../{ => np_ready}/app/constants/api.js | 0 .../{ => np_ready}/app/constants/index.js | 0 .../{ => np_ready}/app/constants/sections.js | 0 .../{ => np_ready}/app/constants/ui_metric.js | 0 .../public/{ => np_ready}/app/index.js | 3 +- .../auto_follow_pattern_add.container.js | 0 .../auto_follow_pattern_add.js | 6 +- .../sections/auto_follow_pattern_add/index.js | 0 .../auto_follow_pattern_edit.container.js | 0 .../auto_follow_pattern_edit.js | 6 +- .../auto_follow_pattern_edit/index.js | 0 .../follower_index_add.container.js | 0 .../follower_index_add/follower_index_add.js | 6 +- .../app/sections/follower_index_add/index.js | 0 .../follower_index_edit.container.js | 0 .../follower_index_edit.js | 6 +- .../app/sections/follower_index_edit/index.js | 0 .../auto_follow_pattern_list.container.js | 0 .../auto_follow_pattern_list.js | 0 .../auto_follow_pattern_table.container.js | 0 .../auto_follow_pattern_table.js | 0 .../auto_follow_pattern_table/index.js | 0 .../detail_panel/detail_panel.container.js | 0 .../components/detail_panel/detail_panel.js | 2 +- .../components/detail_panel/index.js | 0 .../components/index.js | 0 .../home/auto_follow_pattern_list/index.js | 0 .../components/context_menu/context_menu.js | 0 .../components/context_menu/index.js | 0 .../detail_panel/detail_panel.container.js | 0 .../components/detail_panel/detail_panel.js | 2 +- .../components/detail_panel/index.js | 0 .../follower_indices_table.container.js | 0 .../follower_indices_table.js | 0 .../follower_indices_table/index.js | 0 .../follower_indices_list/components/index.js | 0 .../follower_indices_list.container.js | 0 .../follower_indices_list.js | 0 .../home/follower_indices_list/index.js | 0 .../app/sections/home/home.container.js | 0 .../{ => np_ready}/app/sections/home/home.js | 8 +- .../{ => np_ready}/app/sections/home/index.js | 0 .../{ => np_ready}/app/sections/index.js | 0 ...uto_follow_pattern_validators.test.js.snap | 0 .../public/{ => np_ready}/app/services/api.js | 111 +++--- .../app/services/auto_follow_errors.js | 0 .../app/services/auto_follow_errors.test.js | 0 .../app/services/auto_follow_pattern.js | 0 .../app/services/auto_follow_pattern.test.js | 0 .../auto_follow_pattern_validators.js | 4 +- .../auto_follow_pattern_validators.test.js | 0 .../np_ready/app/services/breadcrumbs.mock.ts | 10 + .../app/services/breadcrumbs.ts} | 24 +- .../app/services/documentation_links.ts | 22 ++ .../follower_index_default_settings.js | 2 +- .../app/services/get_remote_cluster_name.js | 0 .../app/services/input_validation.js | 2 +- .../np_ready/app/services/notifications.ts | 20 + .../app/services/query_params.js | 0 .../{ => np_ready}/app/services/routing.js | 2 +- .../app/services/track_ui_metric.js | 2 +- .../{ => np_ready}/app/services/utils.js | 0 .../{ => np_ready}/app/services/utils.test.js | 0 .../{ => np_ready}/app/store/action_types.js | 0 .../{ => np_ready}/app/store/actions/api.js | 0 .../app/store/actions/auto_follow_pattern.js | 16 +- .../{ => np_ready}/app/store/actions/ccr.js | 0 .../app/store/actions/follower_index.js | 19 +- .../{ => np_ready}/app/store/actions/index.js | 0 .../public/{ => np_ready}/app/store/index.js | 0 .../{ => np_ready}/app/store/reducers/api.js | 0 .../app/store/reducers/api.test.js | 0 .../app/store/reducers/auto_follow_pattern.js | 0 .../app/store/reducers/follower_index.js | 0 .../app/store/reducers/index.js | 0 .../app/store/reducers/stats.js | 0 .../app/store/selectors/index.js | 0 .../public/{ => np_ready}/app/store/store.js | 0 .../extend_index_management.ts} | 13 +- .../public/np_ready/index.ts | 11 + .../public/np_ready/plugin.ts | 44 +++ .../public/register_routes.js | 29 +- .../__tests__/wrap_custom_error.js | 21 -- .../__tests__/wrap_unknown_error.js | 19 - .../lib/error_wrappers/wrap_custom_error.js | 18 - .../__tests__/license_pre_routing_factory.js | 66 ---- .../license_pre_routing_factory.js | 28 -- .../client/elasticsearch_ccr.js | 0 .../cross_cluster_replication_data.ts} | 14 +- .../server/np_ready/index.ts | 11 + .../ccr_stats_serialization.test.js.snap | 0 .../call_with_request_factory.js | 0 .../lib/call_with_request_factory/index.js | 0 .../lib/ccr_stats_serialization.js | 0 .../lib/ccr_stats_serialization.test.js | 0 .../lib/check_license/check_license.js | 0 .../{ => np_ready}/lib/check_license/index.js | 0 .../__tests__/wrap_es_error.test.js} | 18 +- .../lib/error_wrappers/index.ts} | 2 - .../lib/error_wrappers/wrap_es_error.ts} | 34 +- .../lib/is_es_error.ts} | 14 +- .../__tests__/is_es_error_factory.js | 0 .../lib/is_es_error_factory/index.ts} | 0 .../is_es_error_factory.ts} | 6 +- .../license_pre_routing_factory.test.ts | 64 ++++ .../lib/license_pre_routing_factory/index.ts} | 0 .../license_pre_routing_factory.ts | 32 ++ .../lib/register_license_checker/index.js | 0 .../register_license_checker.js | 10 +- .../server/np_ready/plugin.ts | 38 ++ .../api/__jest__}/auto_follow_pattern.test.js | 141 +++---- .../api/__jest__}/follower_index.test.js | 119 +++--- .../np_ready/routes/api/__jest__/helpers.ts | 37 ++ .../routes/api/auto_follow_pattern.ts | 301 +++++++++++++++ .../server/np_ready/routes/api/ccr.ts | 112 ++++++ .../np_ready/routes/api/follower_index.ts | 345 ++++++++++++++++++ .../routes/map_to_kibana_http_error.ts | 26 ++ .../routes/register_routes.ts} | 9 +- .../server/np_ready/routes/types.ts | 13 + .../server/routes/api/auto_follow_pattern.js | 256 ------------- .../server/routes/api/ccr.js | 107 ------ .../server/routes/api/follower_index.js | 328 ----------------- .../auto_follow_pattern.js | 5 +- .../follower_indices.js | 6 +- 179 files changed, 1518 insertions(+), 1261 deletions(-) rename x-pack/legacy/plugins/cross_cluster_replication/common/constants/{app.js => app.ts} (100%) rename x-pack/legacy/plugins/cross_cluster_replication/common/constants/{base_path.js => base_path.ts} (100%) rename x-pack/legacy/plugins/cross_cluster_replication/common/constants/{index.js => index.ts} (100%) rename x-pack/legacy/plugins/cross_cluster_replication/common/constants/{plugin.js => plugin.ts} (100%) rename x-pack/legacy/plugins/cross_cluster_replication/common/constants/{settings.js => settings.ts} (100%) delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/public/app/services/documentation_links.js rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/_app.scss (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/app.js (97%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_action_menu/index.ts (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_delete_provider.d.ts (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_delete_provider.js (98%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_form.js (99%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_form.test.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_indices_preview.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_page_title.js (92%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/auto_follow_pattern_request_flyout.js (96%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_form/__snapshots__/follower_index_form.test.js.snap (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_form/advanced_settings_fields.js (98%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_form/follower_index_form.js (97%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_form/follower_index_form.test.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_form/follower_index_request_flyout.js (96%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_form/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_page_title.js (92%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_pause_provider.js (98%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_resume_provider.js (98%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/follower_index_unfollow_provider.js (98%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/form_entry_row.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/remote_clusters_form_field.js (99%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/remote_clusters_provider.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/section_error.js (79%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/section_loading.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/components/section_unauthorized.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/constants/api.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/constants/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/constants/sections.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/constants/ui_metric.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/index.js (88%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js (91%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/auto_follow_pattern_add/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js (96%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/auto_follow_pattern_edit/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/follower_index_add/follower_index_add.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/follower_index_add/follower_index_add.js (91%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/follower_index_add/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/follower_index_edit/follower_index_edit.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/follower_index_edit/follower_index_edit.js (97%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/follower_index_edit/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js (99%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/components/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/auto_follow_pattern_list/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/context_menu/context_menu.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/context_menu/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js (99%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/detail_panel/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/follower_indices_table/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/components/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/follower_indices_list.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/follower_indices_list.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/follower_indices_list/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/home.container.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/home.js (91%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/home/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/sections/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/api.js (52%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/auto_follow_errors.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/auto_follow_errors.test.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/auto_follow_pattern.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/auto_follow_pattern.test.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/auto_follow_pattern_validators.js (97%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/auto_follow_pattern_validators.test.js (100%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts rename x-pack/legacy/plugins/cross_cluster_replication/public/{app/services/breadcrumbs.js => np_ready/app/services/breadcrumbs.ts} (56%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/follower_index_default_settings.js (89%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/get_remote_cluster_name.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/input_validation.js (97%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/query_params.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/routing.js (99%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/track_ui_metric.js (92%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/utils.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/services/utils.test.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/action_types.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/actions/api.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/actions/auto_follow_pattern.js (95%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/actions/ccr.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/actions/follower_index.js (95%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/actions/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/reducers/api.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/reducers/api.test.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/reducers/auto_follow_pattern.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/reducers/follower_index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/reducers/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/reducers/stats.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/selectors/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{ => np_ready}/app/store/store.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/public/{extend_index_management/index.js => np_ready/extend_index_management.ts} (67%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_custom_error.js delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_unknown_error.js delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_custom_error.js delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/license_pre_routing_factory.js rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/client/elasticsearch_ccr.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/{cross_cluster_replication_data.js => server/np_ready/cross_cluster_replication_data.ts} (59%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/__snapshots__/ccr_stats_serialization.test.js.snap (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/call_with_request_factory/call_with_request_factory.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/call_with_request_factory/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/ccr_stats_serialization.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/ccr_stats_serialization.test.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/check_license/check_license.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/check_license/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{lib/error_wrappers/__tests__/wrap_es_error.js => np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js} (55%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{lib/error_wrappers/index.js => np_ready/lib/error_wrappers/index.ts} (72%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{lib/error_wrappers/wrap_es_error.js => np_ready/lib/error_wrappers/wrap_es_error.ts} (66%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{lib/error_wrappers/wrap_unknown_error.js => np_ready/lib/is_es_error.ts} (50%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/is_es_error_factory/__tests__/is_es_error_factory.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{lib/is_es_error_factory/index.js => np_ready/lib/is_es_error_factory/index.ts} (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{lib/is_es_error_factory/is_es_error_factory.js => np_ready/lib/is_es_error_factory/is_es_error_factory.ts} (76%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts rename x-pack/legacy/plugins/cross_cluster_replication/server/{lib/license_pre_routing_factory/index.js => np_ready/lib/license_pre_routing_factory/index.ts} (100%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/register_license_checker/index.js (100%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{ => np_ready}/lib/register_license_checker/register_license_checker.js (66%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts rename x-pack/legacy/plugins/cross_cluster_replication/server/{routes/api => np_ready/routes/api/__jest__}/auto_follow_pattern.test.js (68%) rename x-pack/legacy/plugins/cross_cluster_replication/server/{routes/api => np_ready/routes/api/__jest__}/follower_index.test.js (72%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts rename x-pack/legacy/plugins/cross_cluster_replication/server/{routes/register_routes.js => np_ready/routes/register_routes.ts} (67%) create mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js delete mode 100644 x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js index 06a7c2f1ec45e..2be00e70f6f84 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js @@ -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 '../../public/np_ready/app/services/breadcrumbs.mock'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; import { indexPatterns } from '../../../../../../src/plugins/data/public'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js index 04e80deaf8276..abc3e5dc9def2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AutoFollowPatternForm } from '../../public/app/components/auto_follow_pattern_form'; +import '../../public/np_ready/app/services/breadcrumbs.mock'; +import { AutoFollowPatternForm } from '../../public/np_ready/app/components/auto_follow_pattern_form'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { AUTO_FOLLOW_PATTERN_EDIT } from './helpers/constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js index 88d8f98b973bd..20e982856dc19 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -4,21 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../public/np_ready/app/services/breadcrumbs.mock'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; import { getAutoFollowPatternClientMock } from '../../fixtures/auto_follow_pattern'; jest.mock('ui/new_platform'); -jest.mock('ui/chrome', () => ({ - addBasePath: () => 'api/cross_cluster_replication', - breadcrumbs: { set: () => {} }, - getUiSettingsClient: () => ({ - get: x => x, - getUpdate$: () => ({ subscribe: jest.fn() }), - }), -})); - const { setup } = pageHelpers.autoFollowPatternList; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js index 8d4523ca26de2..7680be9d858a4 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../public/np_ready/app/services/breadcrumbs.mock'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { RemoteClustersFormField } from '../../public/app/components'; +import { RemoteClustersFormField } from '../../public/np_ready/app/components'; import { indexPatterns } from '../../../../../../src/plugins/data/public'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js index 5e2810ae882fb..cfa37ff2e0358 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../public/np_ready/app/services/breadcrumbs.mock'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { FollowerIndexForm } from '../../public/app/components/follower_index_form/follower_index_form'; +import { FollowerIndexForm } from '../../public/np_ready/app/components/follower_index_form/follower_index_form'; import { FOLLOWER_INDEX_EDIT } from './helpers/constants'; jest.mock('ui/new_platform'); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js index 9fd5756a7febf..dde31d1d166f9 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js @@ -10,15 +10,6 @@ import { getFollowerIndexMock } from '../../fixtures/follower_index'; jest.mock('ui/new_platform'); -jest.mock('ui/chrome', () => ({ - addBasePath: () => 'api/cross_cluster_replication', - breadcrumbs: { set: () => {} }, - getUiSettingsClient: () => ({ - get: x => x, - getUpdate$: () => ({ subscribe: jest.fn() }), - }), -})); - const { setup } = pageHelpers.followerIndexList; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js index 3eb195bac7ed1..1f64e589bc4c1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { AutoFollowPatternAdd } from '../../../public/app/sections/auto_follow_pattern_add'; -import { ccrStore } from '../../../public/app/store'; -import routing from '../../../public/app/services/routing'; +import { AutoFollowPatternAdd } from '../../../public/np_ready/app/sections/auto_follow_pattern_add'; +import { ccrStore } from '../../../public/np_ready/app/store'; +import routing from '../../../public/np_ready/app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js index 94a94554b9105..2b110c6552072 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { AutoFollowPatternEdit } from '../../../public/app/sections/auto_follow_pattern_edit'; -import { ccrStore } from '../../../public/app/store'; -import routing from '../../../public/app/services/routing'; +import { AutoFollowPatternEdit } from '../../../public/np_ready/app/sections/auto_follow_pattern_edit'; +import { ccrStore } from '../../../public/np_ready/app/store'; +import routing from '../../../public/np_ready/app/services/routing'; import { AUTO_FOLLOW_PATTERN_EDIT_NAME } from './constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js index c0d29e8af2549..1d3e8ad6dff83 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed, findTestSubject } from '../../../../../../test_utils'; -import { AutoFollowPatternList } from '../../../public/app/sections/home/auto_follow_pattern_list'; -import { ccrStore } from '../../../public/app/store'; -import routing from '../../../public/app/services/routing'; +import { AutoFollowPatternList } from '../../../public/np_ready/app/sections/home/auto_follow_pattern_list'; +import { ccrStore } from '../../../public/np_ready/app/store'; +import routing from '../../../public/np_ready/app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js index 785330049cb0c..f74baa1b2ad0a 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { FollowerIndexAdd } from '../../../public/app/sections/follower_index_add'; -import { ccrStore } from '../../../public/app/store'; -import routing from '../../../public/app/services/routing'; +import { FollowerIndexAdd } from '../../../public/np_ready/app/sections/follower_index_add'; +import { ccrStore } from '../../../public/np_ready/app/store'; +import routing from '../../../public/np_ready/app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js index 56cbe5b47229c..47f8539bb593b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { FollowerIndexEdit } from '../../../public/app/sections/follower_index_edit'; -import { ccrStore } from '../../../public/app/store'; -import routing from '../../../public/app/services/routing'; +import { FollowerIndexEdit } from '../../../public/np_ready/app/sections/follower_index_edit'; +import { ccrStore } from '../../../public/np_ready/app/store'; +import routing from '../../../public/np_ready/app/services/routing'; import { FOLLOWER_INDEX_EDIT_NAME } from './constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js index 02b64cd7f306c..2154e11e17b1f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed, findTestSubject } from '../../../../../../test_utils'; -import { FollowerIndicesList } from '../../../public/app/sections/home/follower_indices_list'; -import { ccrStore } from '../../../public/app/store'; -import routing from '../../../public/app/services/routing'; +import { FollowerIndicesList } from '../../../public/np_ready/app/sections/home/follower_indices_list'; +import { ccrStore } from '../../../public/np_ready/app/store'; +import routing from '../../../public/np_ready/app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js index db30e4fe1dbe7..664ad909ba8e7 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { CrossClusterReplicationHome } from '../../../public/app/sections/home/home'; -import { ccrStore } from '../../../public/app/store'; -import routing from '../../../public/app/services/routing'; +import { CrossClusterReplicationHome } from '../../../public/np_ready/app/sections/home/home'; +import { ccrStore } from '../../../public/np_ready/app/store'; +import routing from '../../../public/np_ready/app/services/routing'; import { BASE_PATH } from '../../../common/constants'; const testBedConfig = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js index 9bd88a08a5a61..e2bd54a92a1f1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js @@ -19,7 +19,7 @@ const registerHttpRequestMockHelpers = server => { server.respondWith( 'GET', - 'api/cross_cluster_replication/follower_indices', + '/api/cross_cluster_replication/follower_indices', mockResponse(defaultResponse, response) ); }; @@ -29,7 +29,7 @@ const registerHttpRequestMockHelpers = server => { server.respondWith( 'GET', - 'api/cross_cluster_replication/auto_follow_patterns', + '/api/cross_cluster_replication/auto_follow_patterns', mockResponse(defaultResponse, response) ); }; @@ -39,7 +39,7 @@ const registerHttpRequestMockHelpers = server => { server.respondWith( 'DELETE', - /api\/cross_cluster_replication\/auto_follow_patterns/, + /\/api\/cross_cluster_replication\/auto_follow_patterns/, mockResponse(defaultResponse, response) ); }; @@ -61,7 +61,7 @@ const registerHttpRequestMockHelpers = server => { server.respondWith( 'GET', - 'api/cross_cluster_replication/stats/auto_follow', + '/api/cross_cluster_replication/stats/auto_follow', mockResponse(defaultResponse, response) ); }; @@ -87,7 +87,7 @@ const registerHttpRequestMockHelpers = server => { server.respondWith( 'GET', - /api\/cross_cluster_replication\/auto_follow_patterns\/.+/, + /\/api\/cross_cluster_replication\/auto_follow_patterns\/.+/, mockResponse(defaultResponse, response) ); }; @@ -105,7 +105,7 @@ const registerHttpRequestMockHelpers = server => { server.respondWith( 'GET', - /api\/cross_cluster_replication\/follower_indices\/.+/, + /\/api\/cross_cluster_replication\/follower_indices\/.+/, mockResponse(defaultResponse, response) ); }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js index 8bd86067d8513..3562ad0df5b51 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js @@ -7,14 +7,15 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { setHttpClient } from '../../../public/app/services/api'; +import { setHttpClient } from '../../../public/np_ready/app/services/api'; import { init as initHttpRequests } from './http_requests'; export const setupEnvironment = () => { - // Mock Angular $q - const $q = { defer: () => ({ resolve() {} }) }; - // axios has a $http like interface so using it to simulate $http - setHttpClient(axios.create({ adapter: axiosXhrAdapter }), $q); + // axios has a similar interface to HttpSetup, but we + // flatten out the response. + const client = axios.create({ adapter: axiosXhrAdapter }); + client.interceptors.response.use(({ data }) => data); + setHttpClient(client); const { server, httpRequestsMockHelpers } = initHttpRequests(); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js index 2afa9c44a7b1c..2c536d069ef53 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import '../../public/np_ready/app/services/breadcrumbs.mock'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; jest.mock('ui/new_platform'); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.js b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.js b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.js b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.js b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.js b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.js rename to x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/index.js b/x-pack/legacy/plugins/cross_cluster_replication/index.js index cdb867972fcf5..aff4cc5b56738 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/index.js @@ -6,9 +6,7 @@ import { resolve } from 'path'; import { PLUGIN } from './common/constants'; -import { registerLicenseChecker } from './server/lib/register_license_checker'; -import { registerRoutes } from './server/routes/register_routes'; -import { ccrDataEnricher } from './cross_cluster_replication_data'; +import { plugin } from './server/np_ready'; export function crossClusterReplication(kibana) { return new kibana.Plugin({ @@ -47,15 +45,13 @@ export function crossClusterReplication(kibana) { ); }, init: function initCcrPlugin(server) { - registerLicenseChecker(server); - registerRoutes(server); - if ( - server.config().get('xpack.ccr.ui.enabled') && - server.newPlatform.setup.plugins.indexManagement && - server.newPlatform.setup.plugins.indexManagement.indexDataEnricher - ) { - server.newPlatform.setup.plugins.indexManagement.indexDataEnricher.add(ccrDataEnricher); - } + plugin({}).setup(server.newPlatform.setup.core, { + indexManagement: server.newPlatform.setup.plugins.indexManagement, + __LEGACY: { + server, + ccrUIEnabled: server.config().get('xpack.ccr.ui.enabled'), + }, + }); }, }); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/documentation_links.js b/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/documentation_links.js deleted file mode 100644 index 585ca7e0f5cf1..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/documentation_links.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; - -const esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; - -export const autoFollowPatternUrl = `${esBase}/ccr-put-auto-follow-pattern.html`; -export const followerIndexUrl = `${esBase}/ccr-put-follow.html`; -export const byteUnitsUrl = `${esBase}/common-options.html#byte-units`; -export const timeUnitsUrl = `${esBase}/common-options.html#time-units`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/index.js index 4ec268f0de7f2..e92c44da34474 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/index.js @@ -5,4 +5,3 @@ */ import './register_routes'; -import './extend_index_management'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss b/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss index 6f65dc04d4427..31317e16e3e9f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss @@ -10,4 +10,4 @@ // ccrChart__legend--small // ccrChart__legend-isLoading -@import 'app/app'; +@import 'np_ready/app/app'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/_app.scss b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/_app.scss rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js index 31626750a7f37..968646a4bd1b0 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/app.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js @@ -7,7 +7,6 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { Route, Switch, Redirect, withRouter } from 'react-router-dom'; -import { fatalError } from 'ui/notify'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -21,7 +20,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { BASE_PATH } from '../../common/constants'; +import { BASE_PATH } from '../../../common/constants'; +import { getFatalErrors } from './services/notifications'; import { SectionError } from './components'; import routing from './services/routing'; import { loadPermissions } from './services/api'; @@ -81,7 +81,7 @@ class AppComponent extends Component { }); } catch (error) { // Expect an error in the shape provided by Angular's $http service. - if (error && error.data) { + if (error && error.body) { return this.setState({ isFetchingPermissions: false, fetchPermissionError: error, @@ -90,7 +90,7 @@ class AppComponent extends Component { // This error isn't an HTTP error, so let the fatal error screen tell the user something // unexpected happened. - fatalError( + getFatalErrors().add( error, i18n.translate('xpack.crossClusterReplication.app.checkPermissionsFatalErrorTitle', { defaultMessage: 'Cross-Cluster Replication app', diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/__snapshots__/auto_follow_pattern_form.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.container.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/auto_follow_pattern_action_menu.tsx diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_action_menu/index.ts rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_action_menu/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.d.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.d.ts rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.d.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js index f9c03165dcf97..7803b329e6258 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_delete_provider.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_delete_provider.js @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { deleteAutoFollowPattern } from '../store/actions'; -import { arrify } from '../../../common/services/utils'; +import { arrify } from '../../../../common/services/utils'; class AutoFollowPatternDeleteProviderUi extends PureComponent { state = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js index d4e418a964c8f..5bc5d8ba6e402 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.js @@ -29,8 +29,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; -import { indexPatterns } from '../../../../../../../src/plugins/data/public'; +import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; import routing from '../services/routing'; import { extractQueryParams } from '../services/query_params'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_form.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_form.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_indices_preview.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_indices_preview.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_indices_preview.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_page_title.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_page_title.js index 43cc0a39e6e57..9880e8c983a8e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/auto_follow_pattern_page_title.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/auto_follow_pattern_page_title.js @@ -17,7 +17,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { autoFollowPatternUrl } from '../services/documentation_links'; +import { getAutoFollowPatternUrl } from '../services/documentation_links'; export const AutoFollowPatternPageTitle = ({ title }) => ( @@ -35,7 +35,7 @@ export const AutoFollowPatternPageTitle = ({ title }) => ( + + { @@ -223,18 +223,24 @@ export class FollowerIndexForm extends PureComponent { isValidatingIndexName: false, }); } catch (error) { - // Expect an error in the shape provided by Angular's $http service. - if (error && error.data) { - // All validation does is check for a name collision, so we can just let the user attempt - // to save the follower index and get an error back from the API. - return this.setState({ - isValidatingIndexName: false, - }); + if (error) { + if (error.name === 'AbortError') { + // Ignore aborted requests + return; + } + // This could be an HTTP error + if (error.body) { + // All validation does is check for a name collision, so we can just let the user attempt + // to save the follower index and get an error back from the API. + return this.setState({ + isValidatingIndexName: false, + }); + } } // This error isn't an HTTP error, so let the fatal error screen tell the user something // unexpected happened. - fatalError( + getFatalErrors().add( error, i18n.translate( 'xpack.crossClusterReplication.followerIndexForm.indexNameValidationFatalErrorTitle', diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_form.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_form.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_form.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_request_flyout.js similarity index 96% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_request_flyout.js index cba1c104e45d9..cb02a929b16f8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/follower_index_request_flyout.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/follower_index_request_flyout.js @@ -26,7 +26,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; +import { serializeFollowerIndex } from '../../../../../common/services/follower_index_serialization'; export class FollowerIndexRequestFlyout extends PureComponent { static propTypes = { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_form/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_form/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_page_title.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_page_title.js index a77059b5fe084..d72038096b72a 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/follower_index_page_title.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/follower_index_page_title.js @@ -17,7 +17,7 @@ import { EuiTitle, } from '@elastic/eui'; -import { followerIndexUrl } from '../services/documentation_links'; +import { getFollowerIndexUrl } from '../services/documentation_links'; export const FollowerIndexPageTitle = ({ title }) => ( @@ -35,7 +35,7 @@ export const FollowerIndexPageTitle = ({ title }) => ( ( diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/section_error.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js similarity index 79% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/section_error.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js index 8aaf89b30f0e7..a2c782a0e8e58 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/section_error.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js @@ -9,21 +9,21 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; export function SectionError(props) { const { title, error, ...rest } = props; - const data = error.data ? error.data : error; + const data = error.body ? error.body : error; const { error: errorString, - cause, // wrapEsError() on the server add a "cause" array + attributes, // wrapEsError() on the server add a "cause" array message, } = data; return (
{message || errorString}
- {cause && ( + {attributes && attributes.cause && (
    - {cause.map((message, i) => ( + {attributes.cause.map((message, i) => (
  • {message}
  • ))}
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/section_loading.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/section_loading.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/constants/api.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/constants/api.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/constants/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/constants/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/constants/sections.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/constants/sections.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/constants/ui_metric.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/constants/ui_metric.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js similarity index 88% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js index 928d37558adb7..cc81fce4eebe7 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { I18nContext } from 'ui/i18n'; import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; @@ -12,7 +11,7 @@ import { HashRouter } from 'react-router-dom'; import { App } from './app'; import { ccrStore } from './store'; -export const renderReact = async elem => { +export const renderReact = async (elem, I18nContext) => { render( diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js similarity index 91% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js index f55b9e4bceb0b..60a6cc79376e5 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js @@ -7,12 +7,10 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; import { EuiPageContent } from '@elastic/eui'; -import { listBreadcrumb, addBreadcrumb } from '../../services/breadcrumbs'; +import { listBreadcrumb, addBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs'; import { AutoFollowPatternForm, AutoFollowPatternPageTitle, @@ -29,7 +27,7 @@ export class AutoFollowPatternAdd extends PureComponent { }; componentDidMount() { - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb, addBreadcrumb]); + setBreadcrumbs([listBreadcrumb, addBreadcrumb]); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js similarity index 96% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js index a64c9566502f1..4cd3617abd989 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js @@ -8,12 +8,10 @@ import React, { PureComponent, Fragment } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer } from '@elastic/eui'; -import { listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs'; +import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs'; import routing from '../../services/routing'; import { AutoFollowPatternForm, @@ -56,7 +54,7 @@ export class AutoFollowPatternEdit extends PureComponent { selectAutoFollowPattern(decodedId); - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb, editBreadcrumb]); + setBreadcrumbs([listBreadcrumb, editBreadcrumb]); } componentDidUpdate(prevProps, prevState) { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js similarity index 91% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js index 26b5d8d6bb880..003e27777652b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js @@ -7,12 +7,10 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; import { EuiPageContent } from '@elastic/eui'; -import { listBreadcrumb, addBreadcrumb } from '../../services/breadcrumbs'; +import { setBreadcrumbs, listBreadcrumb, addBreadcrumb } from '../../services/breadcrumbs'; import { FollowerIndexForm, FollowerIndexPageTitle, @@ -29,7 +27,7 @@ export class FollowerIndexAdd extends PureComponent { }; componentDidMount() { - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb, addBreadcrumb]); + setBreadcrumbs([listBreadcrumb, addBreadcrumb]); } componentWillUnmount() { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js index 7dc45e88f4106..21493602c12a7 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js @@ -8,8 +8,6 @@ import React, { PureComponent, Fragment } from 'react'; import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; import { EuiButtonEmpty, @@ -21,7 +19,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import { listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs'; +import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs'; import routing from '../../services/routing'; import { FollowerIndexForm, @@ -76,7 +74,7 @@ export class FollowerIndexEdit extends PureComponent { selectFollowerIndex(decodedId); - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb, editBreadcrumb]); + setBreadcrumbs([listBreadcrumb, editBreadcrumb]); } componentDidUpdate(prevProps, prevState) { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js index 7b31ffa5024b7..1a6d5e6efe35a 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js @@ -7,7 +7,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getIndexListUri } from '../../../../../../../../../../plugins/index_management/public'; +import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public'; import { EuiButtonEmpty, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js index 2ad118d28f38d..3e8cf6d3e2f78 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js @@ -31,7 +31,7 @@ import { } from '@elastic/eui'; import 'brace/theme/textmate'; -import { getIndexListUri } from '../../../../../../../../../../plugins/index_management/public'; +import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public'; import { API_STATUS } from '../../../../../constants'; import { ContextMenu } from '../context_menu'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/follower_indices_table.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/home.container.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/home.container.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/home.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js similarity index 91% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/home.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js index f89d287540ebd..88db909612245 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/home.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js @@ -7,13 +7,11 @@ import React, { PureComponent } from 'react'; import { Route, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; -import chrome from 'ui/chrome'; -import { MANAGEMENT_BREADCRUMB } from 'ui/management'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; -import { BASE_PATH } from '../../../../common/constants'; -import { listBreadcrumb } from '../../services/breadcrumbs'; +import { BASE_PATH } from '../../../../../common/constants'; +import { setBreadcrumbs, listBreadcrumb } from '../../services/breadcrumbs'; import routing from '../../services/routing'; import { AutoFollowPatternList } from './auto_follow_pattern_list'; import { FollowerIndicesList } from './follower_indices_list'; @@ -47,7 +45,7 @@ export class CrossClusterReplicationHome extends PureComponent { ]; componentDidMount() { - chrome.breadcrumbs.set([MANAGEMENT_BREADCRUMB, listBreadcrumb]); + setBreadcrumbs([listBreadcrumb]); } static getDerivedStateFromProps(props) { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/home/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/sections/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/api.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js similarity index 52% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/api.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js index 52576387444fd..b50c36aa8df9f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/api.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js @@ -3,14 +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 chrome from 'ui/chrome'; import { API_BASE_PATH, API_REMOTE_CLUSTERS_BASE_PATH, API_INDEX_MANAGEMENT_BASE_PATH, -} from '../../../common/constants'; -import { arrify } from '../../../common/services/utils'; +} from '../../../../common/constants'; +import { arrify } from '../../../../common/services/utils'; import { UIM_FOLLOWER_INDEX_CREATE, UIM_FOLLOWER_INDEX_UPDATE, @@ -33,22 +31,10 @@ import { import { trackUserRequest } from './track_ui_metric'; import { areAllSettingsDefault } from './follower_index_default_settings'; -const apiPrefix = chrome.addBasePath(API_BASE_PATH); -const apiPrefixRemoteClusters = chrome.addBasePath(API_REMOTE_CLUSTERS_BASE_PATH); -const apiPrefixIndexManagement = chrome.addBasePath(API_INDEX_MANAGEMENT_BASE_PATH); - -// This is an Angular service, which is why we use this provider pattern -// to access it within our React app. let httpClient; -// The deferred AngularJS api allows us to create a deferred promise -// to be resolved later. This allows us to cancel in-flight http Requests. -// https://docs.angularjs.org/api/ng/service/$q#the-deferred-api -let $q; - -export function setHttpClient(client, $deffered) { +export function setHttpClient(client) { httpClient = client; - $q = $deffered; } export const getHttpClient = () => { @@ -57,67 +43,65 @@ export const getHttpClient = () => { // --- -const extractData = response => response.data; - const createIdString = ids => ids.map(id => encodeURIComponent(id)).join(','); /* Auto Follow Pattern */ -export const loadAutoFollowPatterns = () => - httpClient.get(`${apiPrefix}/auto_follow_patterns`).then(extractData); +export const loadAutoFollowPatterns = () => httpClient.get(`${API_BASE_PATH}/auto_follow_patterns`); export const getAutoFollowPattern = id => - httpClient.get(`${apiPrefix}/auto_follow_patterns/${encodeURIComponent(id)}`).then(extractData); + httpClient.get(`${API_BASE_PATH}/auto_follow_patterns/${encodeURIComponent(id)}`); -export const loadRemoteClusters = () => httpClient.get(apiPrefixRemoteClusters).then(extractData); +export const loadRemoteClusters = () => httpClient.get(API_REMOTE_CLUSTERS_BASE_PATH); export const createAutoFollowPattern = autoFollowPattern => { - const request = httpClient.post(`${apiPrefix}/auto_follow_patterns`, autoFollowPattern); - return trackUserRequest(request, UIM_AUTO_FOLLOW_PATTERN_CREATE).then(extractData); + const request = httpClient.post(`${API_BASE_PATH}/auto_follow_patterns`, { + body: JSON.stringify(autoFollowPattern), + }); + return trackUserRequest(request, UIM_AUTO_FOLLOW_PATTERN_CREATE); }; export const updateAutoFollowPattern = (id, autoFollowPattern) => { const request = httpClient.put( - `${apiPrefix}/auto_follow_patterns/${encodeURIComponent(id)}`, - autoFollowPattern + `${API_BASE_PATH}/auto_follow_patterns/${encodeURIComponent(id)}`, + { body: JSON.stringify(autoFollowPattern) } ); - return trackUserRequest(request, UIM_AUTO_FOLLOW_PATTERN_UPDATE).then(extractData); + return trackUserRequest(request, UIM_AUTO_FOLLOW_PATTERN_UPDATE); }; export const deleteAutoFollowPattern = id => { const ids = arrify(id); const idString = ids.map(_id => encodeURIComponent(_id)).join(','); - const request = httpClient.delete(`${apiPrefix}/auto_follow_patterns/${idString}`); + const request = httpClient.delete(`${API_BASE_PATH}/auto_follow_patterns/${idString}`); const uiMetric = ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_DELETE_MANY : UIM_AUTO_FOLLOW_PATTERN_DELETE; - return trackUserRequest(request, uiMetric).then(extractData); + return trackUserRequest(request, uiMetric); }; export const pauseAutoFollowPattern = id => { const ids = arrify(id); const idString = ids.map(encodeURIComponent).join(','); - const request = httpClient.post(`${apiPrefix}/auto_follow_patterns/${idString}/pause`); + const request = httpClient.post(`${API_BASE_PATH}/auto_follow_patterns/${idString}/pause`); const uiMetric = ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_PAUSE_MANY : UIM_AUTO_FOLLOW_PATTERN_PAUSE; - return trackUserRequest(request, uiMetric).then(extractData); + return trackUserRequest(request, uiMetric); }; export const resumeAutoFollowPattern = id => { const ids = arrify(id); const idString = ids.map(encodeURIComponent).join(','); - const request = httpClient.post(`${apiPrefix}/auto_follow_patterns/${idString}/resume`); + const request = httpClient.post(`${API_BASE_PATH}/auto_follow_patterns/${idString}/resume`); const uiMetric = ids.length > 1 ? UIM_AUTO_FOLLOW_PATTERN_RESUME_MANY : UIM_AUTO_FOLLOW_PATTERN_RESUME; - return trackUserRequest(request, uiMetric).then(extractData); + return trackUserRequest(request, uiMetric); }; /* Follower Index */ -export const loadFollowerIndices = () => - httpClient.get(`${apiPrefix}/follower_indices`).then(extractData); +export const loadFollowerIndices = () => httpClient.get(`${API_BASE_PATH}/follower_indices`); export const getFollowerIndex = id => - httpClient.get(`${apiPrefix}/follower_indices/${encodeURIComponent(id)}`).then(extractData); + httpClient.get(`${API_BASE_PATH}/follower_indices/${encodeURIComponent(id)}`); export const createFollowerIndex = followerIndex => { const uiMetrics = [UIM_FOLLOWER_INDEX_CREATE]; @@ -125,32 +109,34 @@ export const createFollowerIndex = followerIndex => { if (isUsingAdvancedSettings) { uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS); } - const request = httpClient.post(`${apiPrefix}/follower_indices`, followerIndex); - return trackUserRequest(request, uiMetrics).then(extractData); + const request = httpClient.post(`${API_BASE_PATH}/follower_indices`, { + body: JSON.stringify(followerIndex), + }); + return trackUserRequest(request, uiMetrics); }; export const pauseFollowerIndex = id => { const ids = arrify(id); const idString = createIdString(ids); - const request = httpClient.put(`${apiPrefix}/follower_indices/${idString}/pause`); + const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${idString}/pause`); const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_PAUSE_MANY : UIM_FOLLOWER_INDEX_PAUSE; - return trackUserRequest(request, uiMetric).then(extractData); + return trackUserRequest(request, uiMetric); }; export const resumeFollowerIndex = id => { const ids = arrify(id); const idString = createIdString(ids); - const request = httpClient.put(`${apiPrefix}/follower_indices/${idString}/resume`); + const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${idString}/resume`); const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_RESUME_MANY : UIM_FOLLOWER_INDEX_RESUME; - return trackUserRequest(request, uiMetric).then(extractData); + return trackUserRequest(request, uiMetric); }; export const unfollowLeaderIndex = id => { const ids = arrify(id); const idString = createIdString(ids); - const request = httpClient.put(`${apiPrefix}/follower_indices/${idString}/unfollow`); + const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${idString}/unfollow`); const uiMetric = ids.length > 1 ? UIM_FOLLOWER_INDEX_UNFOLLOW_MANY : UIM_FOLLOWER_INDEX_UNFOLLOW; - return trackUserRequest(request, uiMetric).then(extractData); + return trackUserRequest(request, uiMetric); }; export const updateFollowerIndex = (id, followerIndex) => { @@ -159,31 +145,28 @@ export const updateFollowerIndex = (id, followerIndex) => { if (isUsingAdvancedSettings) { uiMetrics.push(UIM_FOLLOWER_INDEX_USE_ADVANCED_OPTIONS); } - const request = httpClient.put( - `${apiPrefix}/follower_indices/${encodeURIComponent(id)}`, - followerIndex - ); - return trackUserRequest(request, uiMetrics).then(extractData); + const request = httpClient.put(`${API_BASE_PATH}/follower_indices/${encodeURIComponent(id)}`, { + body: JSON.stringify(followerIndex), + }); + return trackUserRequest(request, uiMetrics); }; /* Stats */ -export const loadAutoFollowStats = () => - httpClient.get(`${apiPrefix}/stats/auto_follow`).then(extractData); +export const loadAutoFollowStats = () => httpClient.get(`${API_BASE_PATH}/stats/auto_follow`); /* Indices */ -let canceler = null; +let abortController = null; export const loadIndices = () => { - if (canceler) { - // If there is a previous request in flight we cancel it by resolving the canceler - canceler.resolve(); + if (abortController) { + abortController.abort(); + abortController = null; } - canceler = $q.defer(); - return httpClient - .get(`${apiPrefixIndexManagement}/indices`, { timeout: canceler.promise }) - .then(response => { - canceler = null; - return extractData(response); - }); + abortController = new AbortController(); + const { signal } = abortController; + return httpClient.get(`${API_INDEX_MANAGEMENT_BASE_PATH}/indices`, { signal }).then(response => { + abortController = null; + return response; + }); }; -export const loadPermissions = () => httpClient.get(`${apiPrefix}/permissions`).then(extractData); +export const loadPermissions = () => httpClient.get(`${API_BASE_PATH}/permissions`); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js index 5186a02383d33..1b5a39658ee46 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js @@ -8,8 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; -import { indexPatterns } from '../../../../../../../src/plugins/data/public'; +import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; const { indexNameBeginsWithPeriod, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts new file mode 100644 index 0000000000000..b7c75108d4ef0 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +jest.mock('./breadcrumbs', () => ({ + ...jest.requireActual('./breadcrumbs'), + setBreadcrumbs: jest.fn(), +})); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/breadcrumbs.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts similarity index 56% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/breadcrumbs.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts index f8c8cc710964a..dc64cdee07f7d 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/breadcrumbs.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts @@ -3,9 +3,27 @@ * 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'; -import { BASE_PATH } from '../../../common/constants'; +import { ChromeBreadcrumb } from 'src/core/public'; + +import { ManagementAppMountParams } from '../../../../../../../../src/plugins/management/public'; + +import { BASE_PATH } from '../../../../common/constants'; + +let setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; + +export const setBreadcrumbSetter = ({ + __LEGACY, +}: { + __LEGACY: { + chrome: any; + MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; + }; +}): void => { + setBreadcrumbs = (crumbs: ChromeBreadcrumb[]) => { + __LEGACY.chrome.breadcrumbs.set([__LEGACY.MANAGEMENT_BREADCRUMB, ...crumbs]); + }; +}; export const listBreadcrumb = { text: i18n.translate('xpack.crossClusterReplication.homeBreadcrumbTitle', { @@ -25,3 +43,5 @@ export const editBreadcrumb = { defaultMessage: 'Edit', }), }; + +export { setBreadcrumbs }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts new file mode 100644 index 0000000000000..f17926d2bee10 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +let esBase: string; + +export const setDocLinks = ({ + DOC_LINK_VERSION, + ELASTIC_WEBSITE_URL, +}: { + ELASTIC_WEBSITE_URL: string; + DOC_LINK_VERSION: string; +}) => { + esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; +}; + +export const getAutoFollowPatternUrl = () => `${esBase}/ccr-put-auto-follow-pattern.html`; +export const getFollowerIndexUrl = () => `${esBase}/ccr-put-follow.html`; +export const getByteUnitsUrl = () => `${esBase}/common-options.html#byte-units`; +export const getTimeUnitsUrl = () => `${esBase}/common-options.html#time-units`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js similarity index 89% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js index 118a54887d404..d20fa76ef5451 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../common/constants'; +import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../common/constants'; export const getSettingDefault = name => { if (!FOLLOWER_INDEX_ADVANCED_SETTINGS[name]) { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js index 981b3f5929751..64c3e8412437e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/input_validation.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../../src/plugins/es_ui_shared/public'; +import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; const isEmpty = value => { return !value || !value.trim().length; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts new file mode 100644 index 0000000000000..5e1c3e9e99437 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts @@ -0,0 +1,20 @@ +/* + * 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 { NotificationsSetup, IToasts, FatalErrorsSetup } from 'src/core/public'; + +let _notifications: IToasts; +let _fatalErrors: FatalErrorsSetup; + +export const setNotifications = ( + notifications: NotificationsSetup, + fatalErrorsSetup: FatalErrorsSetup +) => { + _notifications = notifications.toasts; + _fatalErrors = fatalErrorsSetup; +}; + +export const getNotifications = () => _notifications; +export const getFatalErrors = () => _fatalErrors; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/query_params.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js index 487b1068794f9..965aeaaad22ad 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/routing.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js @@ -10,7 +10,7 @@ import { createLocation } from 'history'; import { stringify } from 'query-string'; -import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; +import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../../common/constants'; const isModifiedEvent = event => !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js index bd618f6a59e5c..36b9c185b487d 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/track_ui_metric.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js @@ -7,7 +7,7 @@ import { createUiStatsReporter, METRIC_TYPE, -} from '../../../../../../../src/legacy/core_plugins/ui_metric/public'; +} from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; import { UIM_APP_NAME } from '../constants'; export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/utils.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/utils.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/services/utils.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/services/utils.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/action_types.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/action_types.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/api.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/api.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js index 439858ad98ba3..b81cd30f3977a 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; +import { getNotifications } from '../../services/notifications'; import { SECTIONS, API_STATUS } from '../../constants'; import { loadAutoFollowPatterns as loadAutoFollowPatternsRequest, @@ -75,7 +75,7 @@ export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) } ); - toastNotifications.addSuccess(successMessage); + getNotifications().addSuccess(successMessage); routing.navigate(`/auto_follow_patterns`, undefined, { pattern: encodeURIComponent(id), }); @@ -111,7 +111,7 @@ export const deleteAutoFollowPattern = id => } ); - toastNotifications.addDanger(errorMessage); + getNotifications().addDanger(errorMessage); } if (response.itemsDeleted.length) { @@ -133,7 +133,7 @@ export const deleteAutoFollowPattern = id => } ); - toastNotifications.addSuccess(successMessage); + getNotifications().addSuccess(successMessage); // If we've just deleted a pattern we were looking at, we need to close the panel. const autoFollowPatternId = getSelectedAutoFollowPatternId('detail')(getState()); @@ -173,7 +173,7 @@ export const pauseAutoFollowPattern = id => } ); - toastNotifications.addDanger(errorMessage); + getNotifications().addDanger(errorMessage); } if (response.itemsPaused.length) { @@ -195,7 +195,7 @@ export const pauseAutoFollowPattern = id => } ); - toastNotifications.addSuccess(successMessage); + getNotifications().addSuccess(successMessage); } }, }); @@ -229,7 +229,7 @@ export const resumeAutoFollowPattern = id => } ); - toastNotifications.addDanger(errorMessage); + getNotifications().addDanger(errorMessage); } if (response.itemsResumed.length) { @@ -251,7 +251,7 @@ export const resumeAutoFollowPattern = id => } ); - toastNotifications.addSuccess(successMessage); + getNotifications().addSuccess(successMessage); } }, }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/ccr.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/ccr.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js index da1c259974498..ebdee067ced75 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js @@ -4,8 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; + import routing from '../../services/routing'; +import { getNotifications } from '../../services/notifications'; import { SECTIONS, API_STATUS } from '../../constants'; import { loadFollowerIndices as loadFollowerIndicesRequest, @@ -75,7 +76,7 @@ export const saveFollowerIndex = (name, followerIndex, isUpdating = false) => } ); - toastNotifications.addSuccess(successMessage); + getNotifications().addSuccess(successMessage); routing.navigate(`/follower_indices`, undefined, { name: encodeURIComponent(name), }); @@ -111,7 +112,7 @@ export const pauseFollowerIndex = id => } ); - toastNotifications.addDanger(errorMessage); + getNotifications().addDanger(errorMessage); } if (response.itemsPaused.length) { @@ -133,7 +134,7 @@ export const pauseFollowerIndex = id => } ); - toastNotifications.addSuccess(successMessage); + getNotifications().addSuccess(successMessage); // Refresh list dispatch(loadFollowerIndices(true)); @@ -170,7 +171,7 @@ export const resumeFollowerIndex = id => } ); - toastNotifications.addDanger(errorMessage); + getNotifications().addDanger(errorMessage); } if (response.itemsResumed.length) { @@ -192,7 +193,7 @@ export const resumeFollowerIndex = id => } ); - toastNotifications.addSuccess(successMessage); + getNotifications().addSuccess(successMessage); } // Refresh list @@ -229,7 +230,7 @@ export const unfollowLeaderIndex = id => } ); - toastNotifications.addDanger(errorMessage); + getNotifications().addDanger(errorMessage); } if (response.itemsUnfollowed.length) { @@ -251,7 +252,7 @@ export const unfollowLeaderIndex = id => } ); - toastNotifications.addSuccess(successMessage); + getNotifications().addSuccess(successMessage); } if (response.itemsNotOpen.length) { @@ -273,7 +274,7 @@ export const unfollowLeaderIndex = id => } ); - toastNotifications.addWarning(warningMessage); + getNotifications().addWarning(warningMessage); } // If we've just unfollowed a follower index we were looking at, we need to close the panel. diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/actions/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/api.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/api.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/stats.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/reducers/stats.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/selectors/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/selectors/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/app/store/store.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/app/store/store.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/extend_index_management/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts similarity index 67% rename from x-pack/legacy/plugins/cross_cluster_replication/public/extend_index_management/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts index c44918c500849..01c6250383fb8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/extend_index_management/index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts @@ -3,14 +3,15 @@ * 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'; -import { npSetup } from 'ui/new_platform'; import { get } from 'lodash'; +import { IndexMgmtSetup } from '../../../../../plugins/index_management/public'; const propertyPath = 'isFollowerIndex'; const followerBadgeExtension = { - matchIndex: index => { + matchIndex: (index: any) => { return get(index, propertyPath); }, label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', { @@ -20,6 +21,8 @@ const followerBadgeExtension = { filterExpression: 'isFollowerIndex:true', }; -if (npSetup.plugins.indexManagement) { - npSetup.plugins.indexManagement.extensionsService.addBadge(followerBadgeExtension); -} +export const extendIndexManagement = (indexManagement?: IndexMgmtSetup) => { + if (indexManagement) { + indexManagement.extensionsService.addBadge(followerBadgeExtension); + } +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts new file mode 100644 index 0000000000000..11aea6b7b5de4 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.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 { PluginInitializerContext } from 'src/core/public'; + +import { CrossClusterReplicationUIPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => new CrossClusterReplicationUIPlugin(ctx); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts new file mode 100644 index 0000000000000..f7651cbb210a7 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts @@ -0,0 +1,44 @@ +/* + * 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 { + ChromeBreadcrumb, + CoreSetup, + Plugin, + PluginInitializerContext, + DocLinksStart, +} from 'src/core/public'; + +import { IndexMgmtSetup } from '../../../../../plugins/index_management/public'; + +// @ts-ignore; +import { setHttpClient } from './app/services/api'; +import { setBreadcrumbSetter } from './app/services/breadcrumbs'; +import { setDocLinks } from './app/services/documentation_links'; +import { setNotifications } from './app/services/notifications'; +import { extendIndexManagement } from './extend_index_management'; + +interface PluginDependencies { + indexManagement: IndexMgmtSetup; + __LEGACY: { + chrome: any; + MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; + docLinks: DocLinksStart; + }; +} + +export class CrossClusterReplicationUIPlugin implements Plugin { + // @ts-ignore + constructor(private readonly ctx: PluginInitializerContext) {} + setup({ http, notifications, fatalErrors }: CoreSetup, deps: PluginDependencies) { + setHttpClient(http); + setBreadcrumbSetter(deps); + setDocLinks(deps.__LEGACY.docLinks); + setNotifications(notifications, fatalErrors); + extendIndexManagement(deps.indexManagement); + } + + start() {} +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js b/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js index 7b9ba07f46c18..838939f46e523 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js @@ -6,15 +6,21 @@ import { unmountComponentAtNode } from 'react-dom'; import chrome from 'ui/chrome'; -import { management } from 'ui/management'; +import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; +import { npSetup, npStart } from 'ui/new_platform'; import routes from 'ui/routes'; import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { i18n } from '@kbn/i18n'; import template from './main.html'; import { BASE_PATH } from '../common/constants'; -import { renderReact } from './app'; -import { setHttpClient } from './app/services/api'; + +import { plugin } from './np_ready'; + +/** + * TODO: When this file is deleted, use the management section for rendering + */ +import { renderReact } from './np_ready/app'; const isAvailable = xpackInfo.get('features.crossClusterReplication.isAvailable'); const isActive = xpackInfo.get('features.crossClusterReplication.isActive'); @@ -37,26 +43,31 @@ if (isLicenseOK && isCcrUiEnabled) { const CCR_REACT_ROOT = 'ccrReactRoot'; + plugin({}).setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + chrome, + docLinks: npStart.core.docLinks, + MANAGEMENT_BREADCRUMB, + }, + }); + const unmountReactApp = () => elem && unmountComponentAtNode(elem); routes.when(`${BASE_PATH}/:section?/:subsection?/:view?/:id?`, { template, controllerAs: 'ccr', controller: class CrossClusterReplicationController { - constructor($scope, $route, $http, $q) { + constructor($scope, $route) { // React-router's does not play well with the angular router. It will cause this controller // to re-execute without the $destroy handler being called. This means that the app will be mounted twice // creating a memory leak when leaving (only 1 app will be unmounted). // To avoid this, we unmount the React app each time we enter the controller. unmountReactApp(); - // NOTE: We depend upon Angular's $http service because it's decorated with interceptors, - // e.g. to check license status per request. - setHttpClient($http, $q); - $scope.$$postDigest(() => { elem = document.getElementById(CCR_REACT_ROOT); - renderReact(elem); + renderReact(elem, npStart.core.i18n.Context); // Angular Lifecycle const appRoute = $route.current; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_custom_error.js b/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_custom_error.js deleted file mode 100644 index f9c102be7a1ff..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_custom_error.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapCustomError } from '../wrap_custom_error'; - -describe('wrap_custom_error', () => { - describe('#wrapCustomError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const statusCode = 404; - const wrappedError = wrapCustomError(originalError, statusCode); - - expect(wrappedError.isBoom).to.be(true); - expect(wrappedError.output.statusCode).to.equal(statusCode); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_unknown_error.js b/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_unknown_error.js deleted file mode 100644 index 85e0b2b3033ad..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_unknown_error.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { wrapUnknownError } from '../wrap_unknown_error'; - -describe('wrap_unknown_error', () => { - describe('#wrapUnknownError', () => { - it('should return a Boom object', () => { - const originalError = new Error('I am an error'); - const wrappedError = wrapUnknownError(originalError); - - expect(wrappedError.isBoom).to.be(true); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_custom_error.js b/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_custom_error.js deleted file mode 100644 index 3295113d38ee5..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_custom_error.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -/** - * Wraps a custom error into a Boom error response and returns it - * - * @param err Object error - * @param statusCode Error status code - * @return Object Boom error response - */ -export function wrapCustomError(err, statusCode) { - return Boom.boomify(err, { statusCode }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js deleted file mode 100644 index a73aa96209c26..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/__tests__/license_pre_routing_factory.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import expect from '@kbn/expect'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockServer; - let mockLicenseCheckResults; - - beforeEach(() => { - mockServer = { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }; - }); - - it('only instantiates one instance per server', () => { - const firstInstance = licensePreRoutingFactory(mockServer); - const secondInstance = licensePreRoutingFactory(mockServer); - - expect(firstInstance).to.be(secondInstance); - }); - - describe('isAvailable is false', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: false, - }; - }); - - it('replies with 403', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const response = licensePreRouting(); - expect(response).to.be.an(Error); - expect(response.isBoom).to.be(true); - expect(response.output.statusCode).to.be(403); - }); - }); - - describe('isAvailable is true', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: true, - }; - }); - - it('replies with nothing', () => { - const licensePreRouting = licensePreRoutingFactory(mockServer); - const response = licensePreRouting(); - expect(response).to.be(null); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/license_pre_routing_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/license_pre_routing_factory.js deleted file mode 100644 index 548ad7ca02104..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/license_pre_routing_factory.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { once } from 'lodash'; -import { wrapCustomError } from '../error_wrappers'; -import { PLUGIN } from '../../../common/constants'; - -export const licensePreRoutingFactory = once(server => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - function licensePreRouting() { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - if (!licenseCheckResults.isAvailable) { - const error = new Error(licenseCheckResults.message); - const statusCode = 403; - const wrappedError = wrapCustomError(error, statusCode); - return wrappedError; - } else { - return null; - } - } - - return licensePreRouting; -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/cross_cluster_replication_data.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts similarity index 59% rename from x-pack/legacy/plugins/cross_cluster_replication/cross_cluster_replication_data.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts index 2944c3e6bc2ec..ae15073b979e1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/cross_cluster_replication_data.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts @@ -3,9 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { APICaller } from 'src/core/server'; +import { Index } from '../../../../../plugins/index_management/server'; -export const ccrDataEnricher = async (indicesList, callWithRequest) => { - if (!indicesList || !indicesList.length) { +export const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => { + if (!indicesList?.length) { return indicesList; } const params = { @@ -18,9 +20,11 @@ export const ccrDataEnricher = async (indicesList, callWithRequest) => { params ); return indicesList.map(index => { - const isFollowerIndex = !!followerIndices.find(followerIndex => { - return followerIndex.follower_index === index.name; - }); + const isFollowerIndex = !!followerIndices.find( + (followerIndex: { follower_index: string }) => { + return followerIndex.follower_index === index.name; + } + ); return { ...index, isFollowerIndex, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts new file mode 100644 index 0000000000000..7a38d024d99a2 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.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 { PluginInitializerContext } from 'src/core/server'; +import { CrossClusterReplicationServerPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => + new CrossClusterReplicationServerPlugin(ctx); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.js.snap rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/call_with_request_factory/call_with_request_factory.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/call_with_request_factory/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/check_license/check_license.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/check_license/check_license.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/check_license/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/check_license/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_es_error.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js similarity index 55% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_es_error.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js index 8241dc4329137..11a6fd4e1d816 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/__tests__/wrap_es_error.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js @@ -16,24 +16,18 @@ describe('wrap_es_error', () => { originalError.response = '{}'; }); - it('should return a Boom object', () => { + it('should return the correct object', () => { const wrappedError = wrapEsError(originalError); - expect(wrappedError.isBoom).to.be(true); + expect(wrappedError.statusCode).to.be(originalError.statusCode); + expect(wrappedError.message).to.be(originalError.message); }); - it('should return the correct Boom object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be(originalError.message); - }); - - it('should return the correct Boom object with custom message', () => { + it('should return the correct object with custom message', () => { const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - expect(wrappedError.output.statusCode).to.be(originalError.statusCode); - expect(wrappedError.output.payload.message).to.be('No encontrado!'); + expect(wrappedError.statusCode).to.be(originalError.statusCode); + expect(wrappedError.message).to.be('No encontrado!'); }); }); }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts similarity index 72% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts index f275f15637091..3756b0c74fb10 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/index.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { wrapCustomError } from './wrap_custom_error'; export { wrapEsError } from './wrap_es_error'; -export { wrapUnknownError } from './wrap_unknown_error'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_es_error.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts similarity index 66% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_es_error.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts index 5f4884a3f2d26..8afd5f1a018eb 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_es_error.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts @@ -4,16 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - -function extractCausedByChain(causedBy = {}, accumulator = []) { - const { reason, caused_by } = causedBy; // eslint-disable-line camelcase +function extractCausedByChain( + causedBy: Record = {}, + accumulator: string[] = [] +): string[] { + const { reason, caused_by } = causedBy; // eslint-disable-line @typescript-eslint/camelcase if (reason) { accumulator.push(reason); } - // eslint-disable-next-line camelcase + // eslint-disable-next-line @typescript-eslint/camelcase if (caused_by) { return extractCausedByChain(caused_by, accumulator); } @@ -26,34 +27,39 @@ function extractCausedByChain(causedBy = {}, accumulator = []) { * * @param err Object Error thrown by ES JS client * @param statusCodeToMessageMap Object Optional map of HTTP status codes => error messages - * @return Object Boom error response */ -export function wrapEsError(err, statusCodeToMessageMap = {}) { +export function wrapEsError( + err: any, + statusCodeToMessageMap: Record = {} +): { message: string; body?: { cause?: string[] }; statusCode: number } { const { statusCode, response } = err; const { error: { - root_cause = [], // eslint-disable-line camelcase - caused_by, // eslint-disable-line camelcase + root_cause = [], // eslint-disable-line @typescript-eslint/camelcase + caused_by = undefined, // eslint-disable-line @typescript-eslint/camelcase } = {}, } = JSON.parse(response); // If no custom message if specified for the error's status code, just // wrap the error as a Boom error response and return it if (!statusCodeToMessageMap[statusCode]) { - const boomError = Boom.boomify(err, { statusCode }); - // The caused_by chain has the most information so use that if it's available. If not then // settle for the root_cause. const causedByChain = extractCausedByChain(caused_by); const defaultCause = root_cause.length ? extractCausedByChain(root_cause[0]) : undefined; - boomError.output.payload.cause = causedByChain.length ? causedByChain : defaultCause; - return boomError; + return { + message: err.message, + statusCode, + body: { + cause: causedByChain.length ? causedByChain : defaultCause, + }, + }; } // Otherwise, use the custom message to create a Boom error response and // return it const message = statusCodeToMessageMap[statusCode]; - return new Boom(message, { statusCode }); + return { message, statusCode }; } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_unknown_error.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts similarity index 50% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_unknown_error.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts index ffd915c513362..4137293cf39c0 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/error_wrappers/wrap_unknown_error.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts @@ -4,14 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; +import * as legacyElasticsearch from 'elasticsearch'; -/** - * Wraps an unknown error into a Boom error response and returns it - * - * @param err Object Unknown error - * @return Object Boom error response - */ -export function wrapUnknownError(err) { - return Boom.boomify(err); +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/__tests__/is_es_error_factory.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/is_es_error_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts similarity index 76% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/is_es_error_factory.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts index 6c17554385ef8..fc6405b8e7513 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/is_es_error_factory/is_es_error_factory.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts @@ -6,13 +6,13 @@ import { memoize } from 'lodash'; -const esErrorsFactory = memoize(server => { +const esErrorsFactory = memoize((server: any) => { return server.plugins.elasticsearch.getCluster('admin').errors; }); -export function isEsErrorFactory(server) { +export function isEsErrorFactory(server: any) { const esErrors = esErrorsFactory(server); - return function isEsError(err) { + return function isEsError(err: any) { return err instanceof esErrors._Abstract; }; } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts new file mode 100644 index 0000000000000..d22505f0e315a --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; +import { licensePreRoutingFactory } from '../license_pre_routing_factory'; + +describe('license_pre_routing_factory', () => { + describe('#reportingFeaturePreRoutingFactory', () => { + let mockDeps: any; + let mockLicenseCheckResults: any; + + const anyContext: any = {}; + const anyRequest: any = {}; + + beforeEach(() => { + mockDeps = { + __LEGACY: { + server: { + plugins: { + xpack_main: { + info: { + feature: () => ({ + getLicenseCheckResults: () => mockLicenseCheckResults, + }), + }, + }, + }, + }, + }, + requestHandler: jest.fn(), + }; + }); + + describe('isAvailable is false', () => { + beforeEach(() => { + mockLicenseCheckResults = { + isAvailable: false, + }; + }); + + it('replies with 403', async () => { + const licensePreRouting = licensePreRoutingFactory(mockDeps); + const response = await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory); + expect(response.status).toBe(403); + }); + }); + + describe('isAvailable is true', () => { + beforeEach(() => { + mockLicenseCheckResults = { + isAvailable: true, + }; + }); + + it('it calls the wrapped handler', async () => { + const licensePreRouting = licensePreRoutingFactory(mockDeps); + await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory); + expect(mockDeps.requestHandler).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/license_pre_routing_factory/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts new file mode 100644 index 0000000000000..c47faa940a650 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts @@ -0,0 +1,32 @@ +/* + * 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 { RequestHandler } from 'src/core/server'; +import { PLUGIN } from '../../../../common/constants'; + +export const licensePreRoutingFactory = ({ + __LEGACY, + requestHandler, +}: { + __LEGACY: { server: any }; + requestHandler: RequestHandler; +}) => { + const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; + + // License checking and enable/disable logic + const licensePreRouting: RequestHandler = (ctx, request, response) => { + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); + if (!licenseCheckResults.isAvailable) { + return response.forbidden({ + body: licenseCheckResults.message, + }); + } else { + return requestHandler(ctx, request, response); + } + }; + + return licensePreRouting; +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/index.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js similarity index 66% rename from x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/register_license_checker.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js index dbd99efd95573..b9bb34a80ce79 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/lib/register_license_checker/register_license_checker.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mirrorPluginStatus } from '../../../../../server/lib/mirror_plugin_status'; -import { PLUGIN } from '../../../common/constants'; +import { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; +import { PLUGIN } from '../../../../common/constants'; import { checkLicense } from '../check_license'; -export function registerLicenseChecker(server) { - const xpackMainPlugin = server.plugins.xpack_main; - const ccrPluggin = server.plugins[PLUGIN.ID]; +export function registerLicenseChecker(__LEGACY) { + const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; + const ccrPluggin = __LEGACY.server.plugins[PLUGIN.ID]; mirrorPluginStatus(xpackMainPlugin, ccrPluggin); xpackMainPlugin.status.once('green', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts new file mode 100644 index 0000000000000..1012c07af3d2a --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts @@ -0,0 +1,38 @@ +/* + * 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 { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server'; + +import { IndexMgmtSetup } from '../../../../../plugins/index_management/server'; + +// @ts-ignore +import { registerLicenseChecker } from './lib/register_license_checker'; +// @ts-ignore +import { registerRoutes } from './routes/register_routes'; +import { ccrDataEnricher } from './cross_cluster_replication_data'; + +interface PluginDependencies { + indexManagement: IndexMgmtSetup; + __LEGACY: { + server: any; + ccrUIEnabled: boolean; + }; +} + +export class CrossClusterReplicationServerPlugin implements Plugin { + // @ts-ignore + constructor(private readonly ctx: PluginInitializerContext) {} + setup({ http }: CoreSetup, { indexManagement, __LEGACY }: PluginDependencies) { + registerLicenseChecker(__LEGACY); + + const router = http.createRouter(); + registerRoutes({ router, __LEGACY }); + if (__LEGACY.ccrUIEnabled && indexManagement && indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(ccrDataEnricher); + } + } + start() {} +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js similarity index 68% rename from x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js index c610039cfd2ac..f3024515c7213 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js @@ -3,23 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { deserializeAutoFollowPattern } from '../../../../../common/services/auto_follow_pattern_serialization'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; +import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../../../fixtures'; +import { registerAutoFollowPatternRoutes } from '../auto_follow_pattern'; -import { deserializeAutoFollowPattern } from '../../../common/services/auto_follow_pattern_serialization'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../fixtures'; -import { registerAutoFollowPatternRoutes } from './auto_follow_pattern'; +import { createRouter, callRoute } from './helpers'; -jest.mock('../../lib/call_with_request_factory'); -jest.mock('../../lib/is_es_error_factory'); -jest.mock('../../lib/license_pre_routing_factory'); +jest.mock('../../../lib/call_with_request_factory'); +jest.mock('../../../lib/is_es_error_factory'); +jest.mock('../../../lib/license_pre_routing_factory', () => ({ + licensePreRoutingFactory: ({ requestHandler }) => requestHandler, +})); const DESERIALIZED_KEYS = Object.keys(deserializeAutoFollowPattern(getAutoFollowPatternMock())); -/** - * Hashtable to save the route handlers - */ -const routeHandlers = {}; +let routeRegistry; /** * Helper to extract all the different server route handler so we can easily call them in our tests. @@ -28,8 +28,6 @@ const routeHandlers = {}; * if a "server.route()" call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here. */ const registerHandlers = () => { - let index = 0; - const HANDLER_INDEX_TO_ACTION = { 0: 'list', 1: 'create', @@ -40,15 +38,12 @@ const registerHandlers = () => { 6: 'resume', }; - const server = { - route({ handler }) { - // Save handler and increment index - routeHandlers[HANDLER_INDEX_TO_ACTION[index]] = handler; - index++; - }, - }; + routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION); - registerAutoFollowPatternRoutes(server); + registerAutoFollowPatternRoutes({ + __LEGACY: {}, + router: routeRegistry.router, + }); }; /** @@ -94,14 +89,16 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { describe('list()', () => { beforeEach(() => { - routeHandler = routeHandlers.list; + routeHandler = routeRegistry.getRoutes().list; }); it('should deserialize the response from Elasticsearch', async () => { const totalResult = 2; setHttpRequestResponse(null, getAutoFollowPatternListMock(totalResult)); - const response = await routeHandler(); + const { + options: { body: response }, + } = await callRoute(routeHandler); const autoFollowPattern = response.patterns[0]; expect(response.patterns.length).toEqual(totalResult); @@ -112,21 +109,25 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { describe('create()', () => { beforeEach(() => { resetHttpRequestResponses(); - routeHandler = routeHandlers.create; + routeHandler = routeRegistry.getRoutes().create; }); it('should throw a 409 conflict error if id already exists', async () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ - payload: { - id: 'some-id', - foo: 'bar', - }, - }).catch(err => err); // return the error - - expect(response.output.statusCode).toEqual(409); + const response = await callRoute( + routeHandler, + {}, + { + body: { + id: 'some-id', + foo: 'bar', + }, + } + ); + + expect(response.status).toEqual(409); }); it('should return 200 status when the id does not exist', async () => { @@ -135,12 +136,18 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { setHttpRequestResponse(error); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ - payload: { - id: 'some-id', - foo: 'bar', - }, - }); + const { + options: { body: response }, + } = await callRoute( + routeHandler, + {}, + { + body: { + id: 'some-id', + foo: 'bar', + }, + } + ); expect(response).toEqual({ acknowledge: true }); }); @@ -148,7 +155,7 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { describe('update()', () => { beforeEach(() => { - routeHandler = routeHandlers.update; + routeHandler = routeRegistry.getRoutes().update; }); it('should serialize the payload before sending it to Elasticsearch', async () => { @@ -156,16 +163,16 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { const request = { params: { id: 'foo' }, - payload: { + body: { remoteCluster: 'bar1', leaderIndexPatterns: ['bar2'], followIndexPattern: 'bar3', }, }; - const response = await routeHandler(request); + const response = await callRoute(routeHandler, {}, request); - expect(response).toEqual({ + expect(response.options.body).toEqual({ id: 'foo', body: { remote_cluster: 'bar1', @@ -178,7 +185,7 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { describe('get()', () => { beforeEach(() => { - routeHandler = routeHandlers.get; + routeHandler = routeRegistry.getRoutes().get; }); it('should return a single resource even though ES return an array with 1 item', async () => { @@ -187,21 +194,23 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { setHttpRequestResponse(null, esResponse); - const response = await routeHandler({ params: { id: 1 } }); - expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS); + const response = await callRoute(routeHandler, {}, { params: { id: 1 } }); + expect(Object.keys(response.options.body)).toEqual(DESERIALIZED_KEYS); }); }); describe('delete()', () => { beforeEach(() => { resetHttpRequestResponses(); - routeHandler = routeHandlers.delete; + routeHandler = routeRegistry.getRoutes().delete; }); it('should delete a single item', async () => { setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: 'a' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); expect(response.itemsDeleted).toEqual(['a']); expect(response.errors).toEqual([]); @@ -212,9 +221,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: 'a,b,c' } }); + const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - expect(response.itemsDeleted).toEqual(['a', 'b', 'c']); + expect(response.options.body.itemsDeleted).toEqual(['a', 'b', 'c']); }); it('should catch error and return them in array', async () => { @@ -224,7 +233,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(error); - const response = await routeHandler({ params: { id: 'a,b' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); expect(response.itemsDeleted).toEqual(['a']); expect(response.errors[0].id).toEqual('b'); @@ -234,13 +245,15 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { describe('pause()', () => { beforeEach(() => { resetHttpRequestResponses(); - routeHandler = routeHandlers.pause; + routeHandler = routeRegistry.getRoutes().pause; }); it('accept a single item', async () => { setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: 'a' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); expect(response.itemsPaused).toEqual(['a']); expect(response.errors).toEqual([]); @@ -251,9 +264,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: 'a,b,c' } }); + const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - expect(response.itemsPaused).toEqual(['a', 'b', 'c']); + expect(response.options.body.itemsPaused).toEqual(['a', 'b', 'c']); }); it('should catch error and return them in array', async () => { @@ -263,7 +276,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(error); - const response = await routeHandler({ params: { id: 'a,b' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); expect(response.itemsPaused).toEqual(['a']); expect(response.errors[0].id).toEqual('b'); @@ -273,13 +288,15 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { describe('resume()', () => { beforeEach(() => { resetHttpRequestResponses(); - routeHandler = routeHandlers.resume; + routeHandler = routeRegistry.getRoutes().resume; }); it('accept a single item', async () => { setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: 'a' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); expect(response.itemsResumed).toEqual(['a']); expect(response.errors).toEqual([]); @@ -290,9 +307,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: 'a,b,c' } }); + const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - expect(response.itemsResumed).toEqual(['a', 'b', 'c']); + expect(response.options.body.itemsResumed).toEqual(['a', 'b', 'c']); }); it('should catch error and return them in array', async () => { @@ -302,7 +319,9 @@ describe('[CCR API Routes] Auto Follow Pattern', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(error); - const response = await routeHandler({ params: { id: 'a,b' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); expect(response.itemsResumed).toEqual(['a']); expect(response.errors[0].id).toEqual('b'); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js similarity index 72% rename from x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js index 7e363c2758a4c..f0139e5bd7011 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.test.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js @@ -3,21 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import { deserializeFollowerIndex } from '../../../common/services/follower_index_serialization'; +import { deserializeFollowerIndex } from '../../../../../common/services/follower_index_serialization'; import { getFollowerIndexStatsMock, getFollowerIndexListStatsMock, getFollowerIndexInfoMock, getFollowerIndexListInfoMock, -} from '../../../fixtures'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { registerFollowerIndexRoutes } from './follower_index'; - -jest.mock('../../lib/call_with_request_factory'); -jest.mock('../../lib/is_es_error_factory'); -jest.mock('../../lib/license_pre_routing_factory'); +} from '../../../../../fixtures'; +import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; +import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; +import { registerFollowerIndexRoutes } from '../follower_index'; +import { createRouter, callRoute } from './helpers'; + +jest.mock('../../../lib/call_with_request_factory'); +jest.mock('../../../lib/is_es_error_factory'); +jest.mock('../../../lib/license_pre_routing_factory', () => ({ + licensePreRoutingFactory: ({ requestHandler }) => requestHandler, +})); const DESERIALIZED_KEYS = Object.keys( deserializeFollowerIndex({ @@ -26,10 +28,7 @@ const DESERIALIZED_KEYS = Object.keys( }) ); -/** - * Hashtable to save the route handlers - */ -const routeHandlers = {}; +let routeRegistry; /** * Helper to extract all the different server route handler so we can easily call them in our tests. @@ -38,8 +37,6 @@ const routeHandlers = {}; * if a 'server.route()' call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here. */ const registerHandlers = () => { - let index = 0; - const HANDLER_INDEX_TO_ACTION = { 0: 'list', 1: 'get', @@ -50,15 +47,11 @@ const registerHandlers = () => { 6: 'unfollow', }; - const server = { - route({ handler }) { - // Save handler and increment index - routeHandlers[HANDLER_INDEX_TO_ACTION[index]] = handler; - index++; - }, - }; - - registerFollowerIndexRoutes(server); + routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION); + registerFollowerIndexRoutes({ + __LEGACY: {}, + router: routeRegistry.router, + }); }; /** @@ -104,7 +97,7 @@ describe('[CCR API Routes] Follower Index', () => { describe('list()', () => { beforeEach(() => { - routeHandler = routeHandlers.list; + routeHandler = routeRegistry.getRoutes().list; }); it('deserializes the response from Elasticsearch', async () => { @@ -117,7 +110,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, infoResult); setHttpRequestResponse(null, statsResult); - const response = await routeHandler(); + const { + options: { body: response }, + } = await callRoute(routeHandler); const followerIndex = response.indices[0]; expect(response.indices.length).toEqual(totalResult); @@ -127,7 +122,7 @@ describe('[CCR API Routes] Follower Index', () => { describe('get()', () => { beforeEach(() => { - routeHandler = routeHandlers.get; + routeHandler = routeRegistry.getRoutes().get; }); it('should return a single resource even though ES return an array with 1 item', async () => { @@ -138,7 +133,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, { follower_indices: [followerIndexInfo] }); setHttpRequestResponse(null, { indices: [followerIndexStats] }); - const response = await routeHandler({ params: { id: mockId } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: mockId } }); expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS); }); }); @@ -146,34 +143,40 @@ describe('[CCR API Routes] Follower Index', () => { describe('create()', () => { beforeEach(() => { resetHttpRequestResponses(); - routeHandler = routeHandlers.create; + routeHandler = routeRegistry.getRoutes().create; }); it('should return 200 status when follower index is created', async () => { setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ - payload: { - name: 'follower_index', - remoteCluster: 'remote_cluster', - leaderIndex: 'leader_index', - }, - }); + const response = await callRoute( + routeHandler, + {}, + { + body: { + name: 'follower_index', + remoteCluster: 'remote_cluster', + leaderIndex: 'leader_index', + }, + } + ); - expect(response).toEqual({ acknowledge: true }); + expect(response.options.body).toEqual({ acknowledge: true }); }); }); describe('pause()', () => { beforeEach(() => { resetHttpRequestResponses(); - routeHandler = routeHandlers.pause; + routeHandler = routeRegistry.getRoutes().pause; }); it('should pause a single item', async () => { setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: '1' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: '1' } }); expect(response.itemsPaused).toEqual(['1']); expect(response.errors).toEqual([]); @@ -184,9 +187,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: '1,2,3' } }); + const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - expect(response.itemsPaused).toEqual(['1', '2', '3']); + expect(response.options.body.itemsPaused).toEqual(['1', '2', '3']); }); it('should catch error and return them in array', async () => { @@ -196,7 +199,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(error); - const response = await routeHandler({ params: { id: '1,2' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); expect(response.itemsPaused).toEqual(['1']); expect(response.errors[0].id).toEqual('2'); @@ -206,13 +211,15 @@ describe('[CCR API Routes] Follower Index', () => { describe('resume()', () => { beforeEach(() => { resetHttpRequestResponses(); - routeHandler = routeHandlers.resume; + routeHandler = routeRegistry.getRoutes().resume; }); it('should resume a single item', async () => { setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: '1' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: '1' } }); expect(response.itemsResumed).toEqual(['1']); expect(response.errors).toEqual([]); @@ -223,9 +230,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: '1,2,3' } }); + const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - expect(response.itemsResumed).toEqual(['1', '2', '3']); + expect(response.options.body.itemsResumed).toEqual(['1', '2', '3']); }); it('should catch error and return them in array', async () => { @@ -235,7 +242,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(error); - const response = await routeHandler({ params: { id: '1,2' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); expect(response.itemsResumed).toEqual(['1']); expect(response.errors[0].id).toEqual('2'); @@ -245,7 +254,7 @@ describe('[CCR API Routes] Follower Index', () => { describe('unfollow()', () => { beforeEach(() => { resetHttpRequestResponses(); - routeHandler = routeHandlers.unfollow; + routeHandler = routeRegistry.getRoutes().unfollow; }); it('should unfollow await single item', async () => { @@ -254,7 +263,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: '1' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: '1' } }); expect(response.itemsUnfollowed).toEqual(['1']); expect(response.errors).toEqual([]); @@ -274,9 +285,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(null, { acknowledge: true }); - const response = await routeHandler({ params: { id: '1,2,3' } }); + const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - expect(response.itemsUnfollowed).toEqual(['1', '2', '3']); + expect(response.options.body.itemsUnfollowed).toEqual(['1', '2', '3']); }); it('should catch error and return them in array', async () => { @@ -290,7 +301,9 @@ describe('[CCR API Routes] Follower Index', () => { setHttpRequestResponse(null, { acknowledge: true }); setHttpRequestResponse(error); - const response = await routeHandler({ params: { id: '1,2' } }); + const { + options: { body: response }, + } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); expect(response.itemsUnfollowed).toEqual(['1']); expect(response.errors[0].id).toEqual('2'); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts new file mode 100644 index 0000000000000..555fc0937c0ad --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; + +export const callRoute = ( + route: RequestHandler, + ctx = {}, + request = {}, + response = kibanaResponseFactory +) => { + return route(ctx as any, request as any, response); +}; + +export const createRouter = (indexToActionMap: Record) => { + let index = 0; + const routeHandlers: Record> = {}; + const addHandler = (ignoreCtxForNow: any, handler: RequestHandler) => { + // Save handler and increment index + routeHandlers[indexToActionMap[index]] = handler; + index++; + }; + + return { + getRoutes: () => routeHandlers, + router: { + get: addHandler, + post: addHandler, + put: addHandler, + delete: addHandler, + }, + }; +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts new file mode 100644 index 0000000000000..d458f1ccb354b --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts @@ -0,0 +1,301 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +// @ts-ignore +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { isEsError } from '../../lib/is_es_error'; +// @ts-ignore +import { + deserializeAutoFollowPattern, + deserializeListAutoFollowPatterns, + serializeAutoFollowPattern, + // @ts-ignore +} from '../../../../common/services/auto_follow_pattern_serialization'; + +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; +import { API_BASE_PATH } from '../../../../common/constants'; + +import { RouteDependencies } from '../types'; +import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; + +export const registerAutoFollowPatternRoutes = ({ router, __LEGACY }: RouteDependencies) => { + /** + * Returns a list of all auto-follow patterns + */ + router.get( + { + path: `${API_BASE_PATH}/auto_follow_patterns`, + validate: false, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + + try { + const result = await callWithRequest('ccr.autoFollowPatterns'); + return response.ok({ + body: { + patterns: deserializeListAutoFollowPatterns(result.patterns), + }, + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Create an auto-follow pattern + */ + router.post( + { + path: `${API_BASE_PATH}/auto_follow_patterns`, + validate: { + body: schema.object( + { + id: schema.string(), + }, + { unknowns: 'allow' } + ), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id, ...rest } = request.body; + const body = serializeAutoFollowPattern(rest); + + /** + * First let's make sur that an auto-follow pattern with + * the same id does not exist. + */ + try { + await callWithRequest('ccr.autoFollowPattern', { id }); + // If we get here it means that an auto-follow pattern with the same id exists + return response.conflict({ + body: `An auto-follow pattern with the name "${id}" already exists.`, + }); + } catch (err) { + if (err.statusCode !== 404) { + return mapErrorToKibanaHttpResponse(err); + } + } + + try { + return response.ok({ + body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }), + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Update an auto-follow pattern + */ + router.put( + { + path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + body: schema.object({}, { unknowns: 'allow' }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + const body = serializeAutoFollowPattern(request.body); + + try { + return response.ok({ + body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }), + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Returns a single auto-follow pattern + */ + router.get( + { + path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + + try { + const result = await callWithRequest('ccr.autoFollowPattern', { id }); + const autoFollowPattern = result.patterns[0]; + + return response.ok({ + body: deserializeAutoFollowPattern(autoFollowPattern), + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Delete an auto-follow pattern + */ + router.delete( + { + path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + const ids = id.split(','); + + const itemsDeleted: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + await Promise.all( + ids.map(_id => + callWithRequest('ccr.deleteAutoFollowPattern', { id: _id }) + .then(() => itemsDeleted.push(_id)) + .catch((err: Error) => { + if (isEsError(err)) { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + } else { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + } + }) + ) + ); + + return response.ok({ + body: { + itemsDeleted, + errors, + }, + }); + }, + }) + ); + + /** + * Pause auto-follow pattern(s) + */ + router.post( + { + path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + const ids = id.split(','); + + const itemsPaused: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + await Promise.all( + ids.map(_id => + callWithRequest('ccr.pauseAutoFollowPattern', { id: _id }) + .then(() => itemsPaused.push(_id)) + .catch((err: Error) => { + if (isEsError(err)) { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + } else { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + } + }) + ) + ); + + return response.ok({ + body: { + itemsPaused, + errors, + }, + }); + }, + }) + ); + + /** + * Resume auto-follow pattern(s) + */ + router.post( + { + path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + const ids = id.split(','); + + const itemsResumed: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + await Promise.all( + ids.map(_id => + callWithRequest('ccr.resumeAutoFollowPattern', { id: _id }) + .then(() => itemsResumed.push(_id)) + .catch((err: Error) => { + if (isEsError(err)) { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + } else { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + } + }) + ) + ); + + return response.ok({ + body: { + itemsResumed, + errors, + }, + }); + }, + }) + ); +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts new file mode 100644 index 0000000000000..b08b056ad2c8a --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts @@ -0,0 +1,112 @@ +/* + * 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 { API_BASE_PATH } from '../../../../common/constants'; +// @ts-ignore +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +// @ts-ignore +import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; + +import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; +import { RouteDependencies } from '../types'; + +export const registerCcrRoutes = ({ router, __LEGACY }: RouteDependencies) => { + /** + * Returns Auto-follow stats + */ + router.get( + { + path: `${API_BASE_PATH}/stats/auto_follow`, + validate: false, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + + try { + const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats'); + + return response.ok({ + body: deserializeAutoFollowStats(autoFollowStats), + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Returns whether the user has CCR permissions + */ + router.get( + { + path: `${API_BASE_PATH}/permissions`, + validate: false, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; + const xpackInfo = xpackMainPlugin && xpackMainPlugin.info; + + if (!xpackInfo) { + // xpackInfo is updated via poll, so it may not be available until polling has begun. + // In this rare situation, tell the client the service is temporarily unavailable. + return response.customError({ + statusCode: 503, + body: 'Security info unavailable', + }); + } + + const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); + if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { + // If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR. + return response.ok({ + body: { + hasPermission: true, + missingClusterPrivileges: [], + }, + }); + } + + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + + try { + const { has_all_requested: hasPermission, cluster } = await callWithRequest( + 'ccr.permissions', + { + body: { + cluster: ['manage', 'manage_ccr'], + }, + } + ); + + const missingClusterPrivileges = Object.keys(cluster).reduce( + (permissions: any, permissionName: any) => { + if (!cluster[permissionName]) { + permissions.push(permissionName); + return permissions; + } + }, + [] as any[] + ); + + return response.ok({ + body: { + hasPermission, + missingClusterPrivileges, + }, + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts new file mode 100644 index 0000000000000..3896e1c02c915 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts @@ -0,0 +1,345 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { + deserializeFollowerIndex, + deserializeListFollowerIndices, + serializeFollowerIndex, + serializeAdvancedSettings, + // @ts-ignore +} from '../../../../common/services/follower_index_serialization'; +import { API_BASE_PATH } from '../../../../common/constants'; +// @ts-ignore +import { removeEmptyFields } from '../../../../common/services/utils'; +// @ts-ignore +import { callWithRequestFactory } from '../../lib/call_with_request_factory'; +import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; + +import { RouteDependencies } from '../types'; +import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; + +export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependencies) => { + /** + * Returns a list of all follower indices + */ + router.get( + { + path: `${API_BASE_PATH}/follower_indices`, + validate: false, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + + try { + const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { + id: '_all', + }); + + const { + follow_stats: { indices: followerIndicesStats }, + } = await callWithRequest('ccr.stats'); + + const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { + map[stats.index] = stats; + return map; + }, {}); + + const collatedFollowerIndices = followerIndices.map((followerIndex: any) => { + return { + ...followerIndex, + ...followerIndicesStatsMap[followerIndex.follower_index], + }; + }); + + return response.ok({ + body: { + indices: deserializeListFollowerIndices(collatedFollowerIndices), + }, + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Returns a single follower index pattern + */ + router.get( + { + path: `${API_BASE_PATH}/follower_indices/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + + try { + const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); + + const followerIndexInfo = followerIndices && followerIndices[0]; + + if (!followerIndexInfo) { + return response.notFound({ + body: `The follower index "${id}" does not exist.`, + }); + } + + // If this follower is paused, skip call to ES stats api since it will return 404 + if (followerIndexInfo.status === 'paused') { + return response.ok({ + body: deserializeFollowerIndex({ + ...followerIndexInfo, + }), + }); + } else { + const { + indices: followerIndicesStats, + } = await callWithRequest('ccr.followerIndexStats', { id }); + + return response.ok({ + body: deserializeFollowerIndex({ + ...followerIndexInfo, + ...(followerIndicesStats ? followerIndicesStats[0] : {}), + }), + }); + } + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Create a follower index + */ + router.post( + { + path: `${API_BASE_PATH}/follower_indices`, + validate: { + body: schema.object( + { + name: schema.string(), + }, + { unknowns: 'allow' } + ), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { name, ...rest } = request.body; + const body = removeEmptyFields(serializeFollowerIndex(rest)); + + try { + return response.ok({ + body: await callWithRequest('ccr.saveFollowerIndex', { name, body }), + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Edit a follower index + */ + router.put( + { + path: `${API_BASE_PATH}/follower_indices/{id}`, + validate: { + params: schema.object({ id: schema.string() }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + + // We need to first pause the follower and then resume it passing the advanced settings + try { + const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); + const followerIndexInfo = followerIndices && followerIndices[0]; + if (!followerIndexInfo) { + return response.notFound({ body: `The follower index "${id}" does not exist.` }); + } + + // Retrieve paused state instead of pulling it from the payload to ensure it's not stale. + const isPaused = followerIndexInfo.status === 'paused'; + // Pause follower if not already paused + if (!isPaused) { + await callWithRequest('ccr.pauseFollowerIndex', { id }); + } + + // Resume follower + const body = removeEmptyFields(serializeAdvancedSettings(request.body)); + return response.ok({ + body: await callWithRequest('ccr.resumeFollowerIndex', { id, body }), + }); + } catch (err) { + return mapErrorToKibanaHttpResponse(err); + } + }, + }) + ); + + /** + * Pauses a follower index + */ + router.put( + { + path: `${API_BASE_PATH}/follower_indices/{id}/pause`, + validate: { + params: schema.object({ id: schema.string() }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + const ids = id.split(','); + + const itemsPaused: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + await Promise.all( + ids.map(_id => + callWithRequest('ccr.pauseFollowerIndex', { id: _id }) + .then(() => itemsPaused.push(_id)) + .catch((err: Error) => { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsPaused, + errors, + }, + }); + }, + }) + ); + + /** + * Resumes a follower index + */ + router.put( + { + path: `${API_BASE_PATH}/follower_indices/{id}/resume`, + validate: { + params: schema.object({ id: schema.string() }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + const ids = id.split(','); + + const itemsResumed: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + await Promise.all( + ids.map(_id => + callWithRequest('ccr.resumeFollowerIndex', { id: _id }) + .then(() => itemsResumed.push(_id)) + .catch((err: Error) => { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsResumed, + errors, + }, + }); + }, + }) + ); + + /** + * Unfollow follower index's leader index + */ + router.put( + { + path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`, + validate: { + params: schema.object({ id: schema.string() }), + }, + }, + licensePreRoutingFactory({ + __LEGACY, + requestHandler: async (ctx, request, response) => { + const callWithRequest = callWithRequestFactory(__LEGACY.server, request); + const { id } = request.params; + const ids = id.split(','); + + const itemsUnfollowed: string[] = []; + const itemsNotOpen: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + await Promise.all( + ids.map(async _id => { + try { + // Try to pause follower, let it fail silently since it may already be paused + try { + await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); + } catch (e) { + // Swallow errors + } + + // Close index + await callWithRequest('indices.close', { index: _id }); + + // Unfollow leader + await callWithRequest('ccr.unfollowLeaderIndex', { id: _id }); + + // Try to re-open the index, store failures in a separate array to surface warnings in the UI + // This will allow users to query their index normally after unfollowing + try { + await callWithRequest('indices.open', { index: _id }); + } catch (e) { + itemsNotOpen.push(_id); + } + + // Push success + itemsUnfollowed.push(_id); + } catch (err) { + errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); + } + }) + ); + + return response.ok({ + body: { + itemsUnfollowed, + itemsNotOpen, + errors, + }, + }); + }, + }) + ); +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts new file mode 100644 index 0000000000000..6a81bd26dc47d --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts @@ -0,0 +1,26 @@ +/* + * 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 { kibanaResponseFactory } from '../../../../../../../src/core/server'; +// @ts-ignore +import { wrapEsError } from '../lib/error_wrappers'; +import { isEsError } from '../lib/is_es_error'; + +export const mapErrorToKibanaHttpResponse = (err: any) => { + if (isEsError(err)) { + const { statusCode, message, body } = wrapEsError(err); + return kibanaResponseFactory.customError({ + statusCode, + body: { + message, + attributes: { + cause: body?.cause, + }, + }, + }); + } + return kibanaResponseFactory.internalError(err); +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/register_routes.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts similarity index 67% rename from x-pack/legacy/plugins/cross_cluster_replication/server/routes/register_routes.js rename to x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts index 6e4088ec8600f..7e59417550691 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/register_routes.js +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts @@ -7,9 +7,10 @@ import { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern'; import { registerFollowerIndexRoutes } from './api/follower_index'; import { registerCcrRoutes } from './api/ccr'; +import { RouteDependencies } from './types'; -export function registerRoutes(server) { - registerAutoFollowPatternRoutes(server); - registerFollowerIndexRoutes(server); - registerCcrRoutes(server); +export function registerRoutes(deps: RouteDependencies) { + registerAutoFollowPatternRoutes(deps); + registerFollowerIndexRoutes(deps); + registerCcrRoutes(deps); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts new file mode 100644 index 0000000000000..7f57c20c536e0 --- /dev/null +++ b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IRouter } from 'src/core/server'; + +export interface RouteDependencies { + router: IRouter; + __LEGACY: { + server: any; + }; +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js deleted file mode 100644 index 4667f0a110c1f..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern.js +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import Boom from 'boom'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; -import { - deserializeAutoFollowPattern, - deserializeListAutoFollowPatterns, - serializeAutoFollowPattern, -} from '../../../common/services/auto_follow_pattern_serialization'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../common/constants'; - -export const registerAutoFollowPatternRoutes = server => { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - /** - * Returns a list of all auto-follow patterns - */ - server.route({ - path: `${API_BASE_PATH}/auto_follow_patterns`, - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const response = await callWithRequest('ccr.autoFollowPatterns'); - return { - patterns: deserializeListAutoFollowPatterns(response.patterns), - }; - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Create an auto-follow pattern - */ - server.route({ - path: `${API_BASE_PATH}/auto_follow_patterns`, - method: 'POST', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id, ...rest } = request.payload; - const body = serializeAutoFollowPattern(rest); - - /** - * First let's make sur that an auto-follow pattern with - * the same id does not exist. - */ - try { - await callWithRequest('ccr.autoFollowPattern', { id }); - // If we get here it means that an auto-follow pattern with the same id exists - const error = Boom.conflict(`An auto-follow pattern with the name "${id}" already exists.`); - throw error; - } catch (err) { - if (err.statusCode !== 404) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - } - - try { - return await callWithRequest('ccr.saveAutoFollowPattern', { id, body }); - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Update an auto-follow pattern - */ - server.route({ - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - method: 'PUT', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - const body = serializeAutoFollowPattern(request.payload); - - try { - return await callWithRequest('ccr.saveAutoFollowPattern', { id, body }); - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Returns a single auto-follow pattern - */ - server.route({ - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - - try { - const response = await callWithRequest('ccr.autoFollowPattern', { id }); - const autoFollowPattern = response.patterns[0]; - - return deserializeAutoFollowPattern(autoFollowPattern); - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Delete an auto-follow pattern - */ - server.route({ - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - method: 'DELETE', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsDeleted = []; - const errors = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.deleteAutoFollowPattern', { id: _id }) - .then(() => itemsDeleted.push(_id)) - .catch(err => { - if (isEsError(err)) { - errors.push({ id: _id, error: wrapEsError(err) }); - } else { - errors.push({ id: _id, error: wrapUnknownError(err) }); - } - }) - ) - ); - - return { - itemsDeleted, - errors, - }; - }, - }); - - /** - * Pause auto-follow pattern(s) - */ - server.route({ - path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`, - method: 'POST', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsPaused = []; - const errors = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.pauseAutoFollowPattern', { id: _id }) - .then(() => itemsPaused.push(_id)) - .catch(err => { - if (isEsError(err)) { - errors.push({ id: _id, error: wrapEsError(err) }); - } else { - errors.push({ id: _id, error: wrapUnknownError(err) }); - } - }) - ) - ); - - return { - itemsPaused, - errors, - }; - }, - }); - - /** - * Resume auto-follow pattern(s) - */ - server.route({ - path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`, - method: 'POST', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsResumed = []; - const errors = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.resumeAutoFollowPattern', { id: _id }) - .then(() => itemsResumed.push(_id)) - .catch(err => { - if (isEsError(err)) { - errors.push({ id: _id, error: wrapEsError(err) }); - } else { - errors.push({ id: _id, error: wrapUnknownError(err) }); - } - }) - ) - ); - - return { - itemsResumed, - errors, - }; - }, - }); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js deleted file mode 100644 index 8255eb6e86b07..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/ccr.js +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -import { API_BASE_PATH } from '../../../common/constants'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; -import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; - -export const registerCcrRoutes = server => { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - /** - * Returns Auto-follow stats - */ - server.route({ - path: `${API_BASE_PATH}/stats/auto_follow`, - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats'); - - return deserializeAutoFollowStats(autoFollowStats); - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Returns whether the user has CCR permissions - */ - server.route({ - path: `${API_BASE_PATH}/permissions`, - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const xpackMainPlugin = server.plugins.xpack_main; - const xpackInfo = xpackMainPlugin && xpackMainPlugin.info; - - if (!xpackInfo) { - // xpackInfo is updated via poll, so it may not be available until polling has begun. - // In this rare situation, tell the client the service is temporarily unavailable. - throw new Boom('Security info unavailable', { statusCode: 503 }); - } - - const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); - if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { - // If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR. - return { - hasPermission: true, - missingClusterPrivileges: [], - }; - } - - const callWithRequest = callWithRequestFactory(server, request); - - try { - const { has_all_requested: hasPermission, cluster } = await callWithRequest( - 'ccr.permissions', - { - body: { - cluster: ['manage', 'manage_ccr'], - }, - } - ); - - const missingClusterPrivileges = Object.keys(cluster).reduce( - (permissions, permissionName) => { - if (!cluster[permissionName]) { - permissions.push(permissionName); - return permissions; - } - }, - [] - ); - - return { - hasPermission, - missingClusterPrivileges, - }; - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js deleted file mode 100644 index e532edaa39636..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/routes/api/follower_index.js +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import Boom from 'boom'; - -import { - deserializeFollowerIndex, - deserializeListFollowerIndices, - serializeFollowerIndex, - serializeAdvancedSettings, -} from '../../../common/services/follower_index_serialization'; -import { API_BASE_PATH } from '../../../common/constants'; -import { removeEmptyFields } from '../../../common/services/utils'; -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../lib/is_es_error_factory'; -import { wrapEsError, wrapUnknownError } from '../../lib/error_wrappers'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; - -export const registerFollowerIndexRoutes = server => { - const isEsError = isEsErrorFactory(server); - const licensePreRouting = licensePreRoutingFactory(server); - - /** - * Returns a list of all follower indices - */ - server.route({ - path: `${API_BASE_PATH}/follower_indices`, - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { - id: '_all', - }); - - const { - follow_stats: { indices: followerIndicesStats }, - } = await callWithRequest('ccr.stats'); - - const followerIndicesStatsMap = followerIndicesStats.reduce((map, stats) => { - map[stats.index] = stats; - return map; - }, {}); - - const collatedFollowerIndices = followerIndices.map(followerIndex => { - return { - ...followerIndex, - ...followerIndicesStatsMap[followerIndex.follower_index], - }; - }); - - return { - indices: deserializeListFollowerIndices(collatedFollowerIndices), - }; - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Returns a single follower index pattern - */ - server.route({ - path: `${API_BASE_PATH}/follower_indices/{id}`, - method: 'GET', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); - - const followerIndexInfo = followerIndices && followerIndices[0]; - - if (!followerIndexInfo) { - const error = Boom.notFound(`The follower index "${id}" does not exist.`); - throw error; - } - - // If this follower is paused, skip call to ES stats api since it will return 404 - if (followerIndexInfo.status === 'paused') { - return deserializeFollowerIndex({ - ...followerIndexInfo, - }); - } else { - const { indices: followerIndicesStats } = await callWithRequest( - 'ccr.followerIndexStats', - { id } - ); - - return deserializeFollowerIndex({ - ...followerIndexInfo, - ...(followerIndicesStats ? followerIndicesStats[0] : {}), - }); - } - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Create a follower index - */ - server.route({ - path: `${API_BASE_PATH}/follower_indices`, - method: 'POST', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { name, ...rest } = request.payload; - const body = removeEmptyFields(serializeFollowerIndex(rest)); - - try { - return await callWithRequest('ccr.saveFollowerIndex', { name, body }); - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Edit a follower index - */ - server.route({ - path: `${API_BASE_PATH}/follower_indices/{id}`, - method: 'PUT', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - - async function isFollowerIndexPaused() { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); - - const followerIndexInfo = followerIndices && followerIndices[0]; - - if (!followerIndexInfo) { - const error = Boom.notFound(`The follower index "${id}" does not exist.`); - throw error; - } - - return followerIndexInfo.status === 'paused'; - } - - // We need to first pause the follower and then resume it passing the advanced settings - try { - // Retrieve paused state instead of pulling it from the payload to ensure it's not stale. - const isPaused = await isFollowerIndexPaused(); - // Pause follower if not already paused - if (!isPaused) { - await callWithRequest('ccr.pauseFollowerIndex', { id }); - } - - // Resume follower - const body = removeEmptyFields(serializeAdvancedSettings(request.payload)); - return await callWithRequest('ccr.resumeFollowerIndex', { id, body }); - } catch (err) { - if (isEsError(err)) { - throw wrapEsError(err); - } - throw wrapUnknownError(err); - } - }, - }); - - /** - * Pauses a follower index - */ - server.route({ - path: `${API_BASE_PATH}/follower_indices/{id}/pause`, - method: 'PUT', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsPaused = []; - const errors = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.pauseFollowerIndex', { id: _id }) - .then(() => itemsPaused.push(_id)) - .catch(err => { - if (isEsError(err)) { - errors.push({ id: _id, error: wrapEsError(err) }); - } else { - errors.push({ id: _id, error: wrapUnknownError(err) }); - } - }) - ) - ); - - return { - itemsPaused, - errors, - }; - }, - }); - - /** - * Resumes a follower index - */ - server.route({ - path: `${API_BASE_PATH}/follower_indices/{id}/resume`, - method: 'PUT', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsResumed = []; - const errors = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.resumeFollowerIndex', { id: _id }) - .then(() => itemsResumed.push(_id)) - .catch(err => { - if (isEsError(err)) { - errors.push({ id: _id, error: wrapEsError(err) }); - } else { - errors.push({ id: _id, error: wrapUnknownError(err) }); - } - }) - ) - ); - - return { - itemsResumed, - errors, - }; - }, - }); - - /** - * Unfollow follower index's leader index - */ - server.route({ - path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`, - method: 'PUT', - config: { - pre: [licensePreRouting], - }, - handler: async request => { - const callWithRequest = callWithRequestFactory(server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsUnfollowed = []; - const itemsNotOpen = []; - const errors = []; - - await Promise.all( - ids.map(async _id => { - try { - // Try to pause follower, let it fail silently since it may already be paused - try { - await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); - } catch (e) { - // Swallow errors - } - - // Close index - await callWithRequest('indices.close', { index: _id }); - - // Unfollow leader - await callWithRequest('ccr.unfollowLeaderIndex', { id: _id }); - - // Try to re-open the index, store failures in a separate array to surface warnings in the UI - // This will allow users to query their index normally after unfollowing - try { - await callWithRequest('indices.open', { index: _id }); - } catch (e) { - itemsNotOpen.push(_id); - } - - // Push success - itemsUnfollowed.push(_id); - } catch (err) { - if (isEsError(err)) { - errors.push({ id: _id, error: wrapEsError(err) }); - } else { - errors.push({ id: _id, error: wrapUnknownError(err) }); - } - } - }) - ); - - return { - itemsUnfollowed, - itemsNotOpen, - errors, - }; - }, - }); -}; diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js index 30459be6ee1dd..3efb4d6600f7f 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js @@ -41,7 +41,7 @@ export default function({ getService }) { payload.remoteCluster = 'unknown-cluster'; const { body } = await createAutoFollowPattern(undefined, payload).expect(404); - expect(body.cause[0]).to.contain('no such remote cluster'); + expect(body.attributes.cause[0]).to.contain('no such remote cluster'); }); }); @@ -52,6 +52,7 @@ export default function({ getService }) { it('should create an auto-follow pattern when cluster is known', async () => { const name = getRandomString(); const { body } = await createAutoFollowPattern(name).expect(200); + console.log(body); expect(body.acknowledged).to.eql(true); }); @@ -62,7 +63,7 @@ export default function({ getService }) { const name = getRandomString(); const { body } = await getAutoFollowPattern(name).expect(404); - expect(body.cause).not.to.be(undefined); + expect(body.attributes.cause).not.to.be(undefined); }); it('should return an auto-follow pattern that was created', async () => { diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js index a5b12668ad9b9..5f9ebbd2a0a3f 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js @@ -47,13 +47,13 @@ export default function({ getService }) { payload.remoteCluster = 'unknown-cluster'; const { body } = await createFollowerIndex(undefined, payload).expect(404); - expect(body.cause[0]).to.contain('no such remote cluster'); + expect(body.attributes.cause[0]).to.contain('no such remote cluster'); }); it('should throw a 404 error trying to follow an unknown index', async () => { const payload = getFollowerIndexPayload(); const { body } = await createFollowerIndex(undefined, payload).expect(404); - expect(body.cause[0]).to.contain('no such index'); + expect(body.attributes.cause[0]).to.contain('no such index'); }); it('should create a follower index that follows an existing remote index', async () => { @@ -75,7 +75,7 @@ export default function({ getService }) { const name = getRandomString(); const { body } = await getFollowerIndex(name).expect(404); - expect(body.cause[0]).to.contain('no such index'); + expect(body.attributes.cause[0]).to.contain('no such index'); }); it('should return a follower index that was created', async () => { From 19fa69df5f2b1a22486ab26da7ca39cd45df1adf Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Thu, 19 Mar 2020 12:00:08 -0500 Subject: [PATCH 02/22] [SIEM] Create ML Rules (#58053) (#60585) * Remove unnecessary linter exceptions Not sure what was causing issues here, but it's gone now. * WIP: Simple form to test creation of ML rules This will be integrated into the regular rule creation workflow, but for now this simple form should allow us to exercise the full ML rule workflow. * WIP: Adds POST to backend, and type/payload changes necessary to make that work * Simplify logic with Math.min * WIP: Failed spike of making an http call * WIP: Hacking together an ML client The rest of this is going to be easier if I have actual data. For now this is mostly copy/pasted and simplified ML code. I've hardcoded time ranges to a period I know has data for a particular job. * Threading through our new ML Rule params It's a bummer that we normalize our rule alert params across all rule types currently, but that's the deal. * Retrieve our anomalies during rule execution Next step: generate signals * WIP: Generate ECS-compatible ML Signals This uses as much of the existing signal-creation code as possible. I skipped the search_after stuff for now because it would require us recreating the anomalies query which we really shouldn't own. For now, here's how it works: * Adds a separate branch of the rule executor for machine_learning rules * In that branch, we call our new bulkCreateMlSignal function * This function first transforms the anomaly document into ECS fields * We then pass the transformed documents to singleBulkCreate, which does the rest * After both branches, we update the rule's status appropriately. We need to do some more work on the anomaly transformation, but this works! * Extract setting of rule failure to helper function We were doing this identically in three places. * Remove unused import * Define a field for our Rule Type selection This adds most of the markup and logic to allow an ML rule type to be selected. We still need to add things like license-checking and showing/hiding of fields based on type. * Hide Query Fields when ML is selected These are still getting set on the form. We'll need to filter these fields before we send off the data, and not show them on the readonly display either. ALso, edit is majorly broken. * Add input field for anomaly threshold * Display numberic values in the readonly view of a step TIL that isEmpty returns false for numbers and other non-iterable values. I don't think it's exactly what we want here, but until I figure out the intention this gets our anomalyThreshold showing up without a separate logic branch here. Removes the unnecessary branch that was redundant with the 'else' clause. * Add field for selecting an ML job This is not the same as the mockups and lacks some functionality, but it'll allow us to select a job for now. * Format our new ML Fields when sending them to the server So that we don't get rejected due to snake case vs camelcase. * Put back code that respects a rule's schedule It was previously hardcoded to a time period I knew had anomalies. * ML fields are optional in our creation step In that we don't initialize them like we do the query (default) fields. * Only send along type-specific Rule fields from form This makes any query- or ML-specific fields optional on a Rule, and performs some logic on the frontend to group and include these fieldsets conditionally based on the user's selection. The one place we don't handle this well is on the readonly view of a completed step in the rules creation, but we'll address that. * Rename anomalies query It's no longer tabular data. If we need that, we can use the ML client. * Remove spike page with simple form * Remove unneeded ES option This response isn't going to HTTP, which is where this option would matter. * Fix bulk create logic I made a happy accident and flipped the logic here, which meant we weren't capping the signals we created. * Rename argument Value is a little more ambiguous than data, here: this is our step data. * Create Rule form stores all values, but filters by type for use When sending off to the backend, or displaying on the readonly view, we inspect which rule type we've currently selected, and filter our form values appropriately. * Fix editing of ML fields on Rule Create We need to inherit the field value from our form on initial render, and everything works as expected. * Clear form errors when switching between rule types Validation errors prevent us from moving to the next step, so it was previously possible to get an error for Query fields, switch to an ML rule, and be unable to continue because the form had Query errors. This also adds a helper for checking whether a ruleType is ML, to prevent having to change all these references if the type string changes. * Validate the selection of an ML Job * Fix type errors on frontend According to the types, this is essentially the opposite of formatRule, so we need to reinflate all potential form values from the rule. * Don't set defaults for query-specific rules For ML rules these types should not be included. * Return ML Fields in Rule responses This adds these fields to our rule serialization, and then adds conditional validation around those fields if the rule type is ML. Conversely, we moved the 'language' and 'query' fields to be conditionally validated if the rule is a query/saved_query rule. * Fix editing of ML rules by changing who controls the field values The source of truth for their state is the parent form object; these inputs should not have local state. * Fix type errors related to new ML fields In adding the new ML fields, some other fields (e.g. `query` and `index`) that were previously required but implicitly part of Query Rules are now marked as optional. Consequently, any downstream code that actually required these fields started to complain. In general, the fix was to verify that those fields exist, and throw an error otherwise as to appease the linter. Runtime-wise, the new ML rules/signals follow a separate code path and both branches should be unaffected by these changes; the issue is simply that our conditional types don't work well with Typescript. * Fix failing route tests Error message changed. * Fix integration tests We were not sending required properties when creating a rule(index and language). * Fix non-ML Rule creation I was accidentally dropping this parameter for our POST payload. Whoops. * More informative logging during ML signal generation The messaging diverged from the normal path here because we don't have index patterns to display. However, we have the rest of the rule context, and should report it appropriately. * Prefer keyof for string union types * Tidy up our new form components * Type them as React.FCs * Remove unnecessary use of styled-components * Prefer destructuring to lodash's omit * Fix mock params for helper functions These were updated to take simpler parameters. * Remove any type This could have been a boolean all along, whoops * Fix mock types * Update outdated tests These were added on master, but behavior has been changed on my branch. * Add some tests around our helper function I need to refactor it, so this is as good a time as any to pin down the behavior. * Remove uses of any in favor of actual types Mainly leverages ML typings instead of our placeholder types. This required handling a null case in our formatting of anomalies. * Annotate our anomalies with @timestamp field We were notably lacking this ECS field in our post-conversion anomalies, and typescript was rightly complaining about it. * ml_job_id -> machine_learning_job_id * PR Feedback * Stricter threshold type * More robust date parsing * More informative log/error messages * Remove redundant runtime checks * Cleaning up our new ML types * Fix types on our Rest types * Use less ambiguous machineLearningJobId over mlJobId * Declare our ML params as required keys, and ensure we pass them around everywhere we might need them (creating, importing, updating rules). * Use implicit type to avoid the need for a ts-ignore FormSchema has a very generic index signature such that our filterRuleFieldsForType helper cannot infer that it has our necessary rule fields (when in fact it does). By removing the FormSchema hint we get the actual keys of our schema, and things work as expected. All other uses of schema continue to work because they're expecting FormSchema, which is effectively { [key: string]: any }. * New ML params are not nullable Rather than setting a null and then never using it, let's just make it truly optional in terms of default values. * Query and language are conditional based on rule type For ML Rules, we don't use them. * Remove defaulted parameter in API test We don't need to specify this, and we should continue not to for backwards compatibility. * Use explicit types over implicit ones The concern is that not typing our schemae as FormSchema could break our form if there are upstream changes. For now, we simply use the intersection of FormSchema and our generic parameter to satisfy our use within the function. * Add integration test for creation of ML Rule * Add ML fields to route schemae * threshold and job id are conditional on type * makes query and language mutually exclusive with above * Fix router test for creating an ML rule We were sending invalid parameters. * Remove null check against index for query rules We support not having an index here, as getInputIndex will return the current UI setting if none is specified. * Add regression test for API compatibility We were previously able to create a rule without an input index; we should continue to support that, as verified by this test! * Respect the index pattern determined at runtime when performing search_after If a rule does not specify an input index pattern on creation, we use the current UI default when the rule is evaluated. This ensures that any subsequent searches use that same index. We're not currently persisting that runtime index to the generated signal, but we should. * Fix type errors in our bulk create tests We added a new argument, but didn't update the tests. --- .../detection_engine/rules/types.ts | 31 ++-- .../rules/all/__mocks__/mock.ts | 3 + .../anomaly_threshold_slider/index.tsx | 45 ++++++ .../description_step/helpers.test.tsx | 27 +--- .../components/description_step/helpers.tsx | 4 +- .../components/description_step/index.tsx | 39 +++-- .../components/description_step/types.ts | 3 +- .../rules/components/ml_job_select/index.tsx | 58 ++++++++ .../rules/components/query_bar/index.tsx | 2 +- .../components/select_rule_type/index.tsx | 60 ++++++++ .../select_rule_type/translations.ts | 42 ++++++ .../components/step_define_rule/index.tsx | 133 +++++++++++------- .../components/step_define_rule/schema.tsx | 94 +++++++++++-- .../rules/create/helpers.test.ts | 9 +- .../detection_engine/rules/create/helpers.ts | 78 ++++++---- .../detection_engine/rules/create/index.tsx | 3 - .../detection_engine/rules/edit/index.tsx | 12 +- .../detection_engine/rules/helpers.test.tsx | 13 +- .../pages/detection_engine/rules/helpers.tsx | 20 +-- .../pages/detection_engine/rules/types.ts | 17 ++- .../routes/__mocks__/request_responses.ts | 17 +++ .../rules/create_rules_bulk_route.test.ts | 2 +- .../routes/rules/create_rules_bulk_route.ts | 4 + .../routes/rules/create_rules_route.test.ts | 10 +- .../routes/rules/create_rules_route.ts | 4 + .../routes/rules/import_rules_route.ts | 5 + .../rules/patch_rules_bulk_route.test.ts | 2 +- .../routes/rules/patch_rules_route.test.ts | 2 +- .../rules/update_rules_bulk_route.test.ts | 2 +- .../routes/rules/update_rules_bulk_route.ts | 4 + .../routes/rules/update_rules_route.test.ts | 2 +- .../routes/rules/update_rules_route.ts | 4 + .../routes/rules/utils.test.ts | 30 +++- .../detection_engine/routes/rules/utils.ts | 2 + .../schemas/add_prepackaged_rules_schema.ts | 24 +++- .../routes/schemas/create_rules_schema.ts | 24 +++- .../routes/schemas/import_rules_schema.ts | 24 +++- .../routes/schemas/patch_rules_schema.ts | 4 + .../schemas/response/__mocks__/utils.ts | 12 ++ .../response/check_type_dependents.test.ts | 68 ++++++++- .../schemas/response/check_type_dependents.ts | 26 ++++ .../routes/schemas/response/rules_schema.ts | 12 +- .../routes/schemas/response/schemas.ts | 4 +- .../routes/schemas/schemas.ts | 7 +- .../routes/schemas/update_rules_schema.ts | 24 +++- .../detection_engine/rules/create_rules.ts | 4 + .../rules/install_prepacked_rules.ts | 4 + .../signals/__mocks__/es_results.ts | 2 + .../detection_engine/signals/build_rule.ts | 2 + .../signals/bulk_create_ml_signals.test.ts | 90 ++++++++++++ .../signals/bulk_create_ml_signals.ts | 80 +++++++++++ .../signals/find_ml_signals.ts | 29 ++++ .../detection_engine/signals/get_filter.ts | 5 + .../signals/search_after_bulk_create.test.ts | 10 ++ .../signals/search_after_bulk_create.ts | 8 +- .../signals/signal_params_schema.ts | 2 + .../signals/signal_rule_alert_type.ts | 129 ++++++++++------- .../signals/single_search_after.test.ts | 16 ++- .../signals/single_search_after.ts | 15 +- .../lib/detection_engine/signals/types.ts | 2 +- .../siem/server/lib/detection_engine/types.ts | 12 +- .../siem/server/lib/machine_learning/index.ts | 90 ++++++++++++ .../security_and_spaces/tests/create_rules.ts | 27 ++++ .../security_and_spaces/tests/utils.ts | 31 ++++ 64 files changed, 1297 insertions(+), 273 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index f962204c6b1b4..5466ba2203714 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -6,26 +6,35 @@ import * as t from 'io-ts'; +export const RuleTypeSchema = t.keyof({ + query: null, + saved_query: null, + machine_learning: null, +}); +export type RuleType = t.TypeOf; + export const NewRuleSchema = t.intersection([ t.type({ description: t.string, enabled: t.boolean, - filters: t.array(t.unknown), - index: t.array(t.string), interval: t.string, - language: t.string, name: t.string, - query: t.string, risk_score: t.number, severity: t.string, - type: t.union([t.literal('query'), t.literal('saved_query')]), + type: RuleTypeSchema, }), t.partial({ + anomaly_threshold: t.number, created_by: t.string, false_positives: t.array(t.string), + filters: t.array(t.unknown), from: t.string, id: t.string, + index: t.array(t.string), + language: t.string, + machine_learning_job_id: t.string, max_signals: t.number, + query: t.string, references: t.array(t.string), rule_id: t.string, saved_id: t.string, @@ -56,32 +65,34 @@ export const RuleSchema = t.intersection([ description: t.string, enabled: t.boolean, false_positives: t.array(t.string), - filters: t.array(t.unknown), from: t.string, id: t.string, - index: t.array(t.string), interval: t.string, immutable: t.boolean, - language: t.string, name: t.string, max_signals: t.number, - query: t.string, references: t.array(t.string), risk_score: t.number, rule_id: t.string, severity: t.string, tags: t.array(t.string), - type: t.string, + type: RuleTypeSchema, to: t.string, threat: t.array(t.unknown), updated_at: t.string, updated_by: t.string, }), t.partial({ + anomaly_threshold: t.number, + filters: t.array(t.unknown), + index: t.array(t.string), + language: t.string, last_failure_at: t.string, last_failure_message: t.string, meta: MetaRule, + machine_learning_job_id: t.string, output_index: t.string, + query: t.string, saved_id: t.string, status: t.string, status_date: t.string, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index 5627d33818500..011a2614c1af9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -181,6 +181,9 @@ export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ isNew, + ruleType: 'query', + anomalyThreshold: 50, + machineLearningJobId: '', index: ['filebeat-'], queryBar: mockQueryBar, }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx new file mode 100644 index 0000000000000..18970ff935b8d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { EuiFlexGrid, EuiFlexItem, EuiRange, EuiFormRow } from '@elastic/eui'; + +import { FieldHook } from '../../../../../shared_imports'; + +interface AnomalyThresholdSliderProps { + field: FieldHook; +} +type Event = React.ChangeEvent; +type EventArg = Event | React.MouseEvent; + +export const AnomalyThresholdSlider: React.FC = ({ field }) => { + const threshold = field.value as number; + const onThresholdChange = useCallback( + (event: EventArg) => { + const thresholdValue = Number((event as Event).target.value); + field.setValue(thresholdValue); + }, + [field] + ); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx index 56c9d6da15607..7a3f0105d3d15 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx @@ -38,10 +38,7 @@ setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); const mockFilterManager = new FilterManager(setupMock.uiSettings); const mockQueryBar = { - query: { - query: 'test query', - language: 'kuery', - }, + query: 'test query', filters: [ { $state: { @@ -93,10 +90,7 @@ describe('helpers', () => { describe('buildQueryBarDescription', () => { test('returns empty array if no filters, query or savedId exist', () => { const emptyMockQueryBar = { - query: { - query: '', - language: 'kuery', - }, + query: '', filters: [], saved_id: '', }; @@ -113,10 +107,7 @@ describe('helpers', () => { test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { const mockQueryBarWithFilters = { ...mockQueryBar, - query: { - query: '', - language: 'kuery', - }, + query: '', saved_id: '', }; const result: ListItems[] = buildQueryBarDescription({ @@ -135,10 +126,7 @@ describe('helpers', () => { test('returns expected array of ListItems when filters AND indexPatterns exist', () => { const mockQueryBarWithFilters = { ...mockQueryBar, - query: { - query: '', - language: 'kuery', - }, + query: '', saved_id: '', }; const result: ListItems[] = buildQueryBarDescription({ @@ -171,16 +159,13 @@ describe('helpers', () => { savedId: mockQueryBarWithQuery.saved_id, }); expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query.query} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} ); }); test('returns expected array of ListItems when "savedId" exists', () => { const mockQueryBarWithSavedId = { ...mockQueryBar, - query: { - query: '', - language: 'kuery', - }, + query: '', filters: [], }; const result: ListItems[] = buildQueryBarDescription({ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index bc454ecb1134a..7b22078c89d1b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -77,12 +77,12 @@ export const buildQueryBarDescription = ({ }, ]; } - if (!isEmpty(query.query)) { + if (!isEmpty(query)) { items = [ ...items, { title: <>{i18n.QUERY_LABEL} , - description: <>{query.query} , + description: <>{query} , }, ]; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx index 1d58ef8014899..43b4a5f781b89 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx @@ -5,7 +5,7 @@ */ import { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEmpty, chunk, get, pick } from 'lodash/fp'; +import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; import React, { memo, useState } from 'react'; import styled from 'styled-components'; @@ -14,7 +14,6 @@ import { Filter, esFilters, FilterManager, - Query, } from '../../../../../../../../../../src/plugins/data/public'; import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; import { useKibana } from '../../../../../lib/kibana'; @@ -133,14 +132,14 @@ export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { export const getDescriptionItem = ( field: string, label: string, - value: unknown, + data: unknown, filterManager: FilterManager, indexPatterns?: IIndexPattern ): ListItems[] => { if (field === 'queryBar') { - const filters = addFilterStateIfNotThere(get('queryBar.filters', value) ?? []); - const query = get('queryBar.query', value) as Query; - const savedId = get('queryBar.saved_id', value); + const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []); + const query = get('queryBar.query.query', data); + const savedId = get('queryBar.saved_id', data); return buildQueryBarDescription({ field, filters, @@ -150,31 +149,24 @@ export const getDescriptionItem = ( indexPatterns, }); } else if (field === 'threat') { - const threat: IMitreEnterpriseAttack[] = get(field, value).filter( + const threat: IMitreEnterpriseAttack[] = get(field, data).filter( (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' ); return buildThreatDescription({ label, threat }); } else if (field === 'references') { - const urls: string[] = get(field, value); + const urls: string[] = get(field, data); return buildUrlsDescription(label, urls); } else if (field === 'falsePositives') { - const values: string[] = get(field, value); + const values: string[] = get(field, data); return buildUnorderedListArrayDescription(label, field, values); - } else if (Array.isArray(get(field, value))) { - const values: string[] = get(field, value); + } else if (Array.isArray(get(field, data))) { + const values: string[] = get(field, data); return buildStringArrayDescription(label, field, values); } else if (field === 'severity') { - const val: string = get(field, value); + const val: string = get(field, data); return buildSeverityDescription(label, val); - } else if (field === 'riskScore') { - return [ - { - title: label, - description: get(field, value), - }, - ]; } else if (field === 'timeline') { - const timeline = get(field, value) as FieldValueTimeline; + const timeline = get(field, data) as FieldValueTimeline; return [ { title: label, @@ -182,11 +174,12 @@ export const getDescriptionItem = ( }, ]; } else if (field === 'note') { - const val: string = get(field, value); + const val: string = get(field, data); return buildNoteDescription(label, val); } - const description: string = get(field, value); - if (!isEmpty(description)) { + + const description: string = get(field, data); + if (isNumber(description) || !isEmpty(description)) { return [ { title: label, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts index ab73c52ae9070..bfca6b2068443 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts @@ -9,7 +9,6 @@ import { IIndexPattern, Filter, FilterManager, - Query, } from '../../../../../../../../../../src/plugins/data/public'; import { IMitreEnterpriseAttack } from '../../types'; @@ -22,7 +21,7 @@ export interface BuildQueryBarDescription { field: string; filters: Filter[]; filterManager: FilterManager; - query: Query; + query: string; savedId: string; indexPatterns?: IIndexPattern; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx new file mode 100644 index 0000000000000..627fa21cc2f61 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx @@ -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 React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSuperSelect, EuiText } from '@elastic/eui'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; +import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; + +const JobDisplay = ({ title, description }: { title: string; description: string }) => ( + <> + {title} + +

{description}

+
+ +); + +interface MlJobSelectProps { + field: FieldHook; +} + +export const MlJobSelect: React.FC = ({ field }) => { + const jobId = field.value as string; + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [isLoading, siemJobs] = useSiemJobs(false); + const handleJobChange = useCallback( + (machineLearningJobId: string) => { + field.setValue(machineLearningJobId); + }, + [field] + ); + + const options = siemJobs.map(job => ({ + value: job.id, + inputDisplay: job.id, + dropdownDisplay: , + })); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx index 5886a76182eec..d232c86c19e6f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx @@ -35,7 +35,7 @@ import * as i18n from './translations'; export interface FieldValueQueryBar { filters: Filter[]; query: Query; - saved_id: string | null; + saved_id?: string; } interface QueryBarDefineRuleProps { browserFields: BrowserFields; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx new file mode 100644 index 0000000000000..b3b35699914f6 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { EuiCard, EuiFlexGrid, EuiFlexItem, EuiIcon, EuiFormRow } from '@elastic/eui'; + +import { FieldHook } from '../../../../../shared_imports'; +import { RuleType } from '../../../../../containers/detection_engine/rules/types'; +import * as i18n from './translations'; +import { isMlRule } from '../../helpers'; + +interface SelectRuleTypeProps { + field: FieldHook; +} + +export const SelectRuleType: React.FC = ({ field }) => { + const ruleType = field.value as RuleType; + const setType = useCallback( + (type: RuleType) => { + field.setValue(type); + }, + [field] + ); + const setMl = useCallback(() => setType('machine_learning'), [setType]); + const setQuery = useCallback(() => setType('query'), [setType]); + const license = true; // TODO + + return ( + + + + } + selectable={{ + onClick: setQuery, + isSelected: !isMlRule(ruleType), + }} + /> + + + } + selectable={{ + onClick: setMl, + isSelected: isMlRule(ruleType), + }} + /> + + + + ); +}; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts new file mode 100644 index 0000000000000..32b860e8f703e --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts @@ -0,0 +1,42 @@ +/* + * 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 QUERY_TYPE_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeTitle', + { + defaultMessage: 'Custom query', + } +); + +export const QUERY_TYPE_DESCRIPTION = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.queryTypeDescription', + { + defaultMessage: 'Use KQL or Lucene to detect issues across indices.', + } +); + +export const ML_TYPE_TITLE = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeTitle', + { + defaultMessage: 'Machine Learning', + } +); + +export const ML_TYPE_DESCRIPTION = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDescription', + { + defaultMessage: 'Select ML job to detect anomalous activity.', + } +); + +export const ML_TYPE_DISABLED_DESCRIPTION = i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.ruleTypeField.mlTypeDisabledDescription', + { + defaultMessage: 'Access to ML requires a Platinum subscription.', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 2327ac36a5906..6b1a9a828d950 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -9,6 +9,7 @@ import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, + EuiFormRow, EuiButton, } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; @@ -20,11 +21,14 @@ import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/pu import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; import { useUiSetting$ } from '../../../../../lib/kibana'; -import { setFieldValue } from '../../helpers'; +import { setFieldValue, isMlRule } from '../../helpers'; import * as RuleI18n from '../../translations'; import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; import { QueryBarDefineRule } from '../query_bar'; +import { SelectRuleType } from '../select_rule_type'; +import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; +import { MlJobSelect } from '../ml_job_select'; import { StepContentWrapper } from '../step_content_wrapper'; import { Field, @@ -33,9 +37,11 @@ import { getUseField, UseField, useForm, + FormSchema, } from '../../../../../shared_imports'; import { schema } from './schema'; import * as i18n from './translations'; +import { filterRuleFieldsForType, RuleFields } from '../../create/helpers'; const CommonUseField = getUseField({ component: Field }); @@ -43,13 +49,16 @@ interface StepDefineRuleProps extends RuleStepProps { defaultValues?: DefineStepRule | null; } -const stepDefineDefaultValue = { +const stepDefineDefaultValue: DefineStepRule = { + anomalyThreshold: 50, index: [], isNew: true, + machineLearningJobId: '', + ruleType: 'query', queryBar: { query: { query: '', language: 'kuery' }, filters: [], - saved_id: null, + saved_id: undefined, }, }; @@ -96,6 +105,7 @@ const StepDefineRuleComponent: FC = ({ }) => { const [openTimelineSearch, setOpenTimelineSearch] = useState(false); const [localUseIndicesConfig, setLocalUseIndicesConfig] = useState(false); + const [localIsMlRule, setIsMlRule] = useState(false); const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); const [mylocalIndicesConfig, setMyLocalIndicesConfig] = useState( defaultValues != null ? defaultValues.index : indicesConfig ?? [] @@ -112,6 +122,7 @@ const StepDefineRuleComponent: FC = ({ options: { stripEmptyFields: false }, schema, }); + const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); const onSubmit = useCallback(async () => { if (setStepData) { @@ -154,64 +165,75 @@ const StepDefineRuleComponent: FC = ({ setOpenTimelineSearch(false); }, []); - return isReadOnlyView && myStepData?.queryBar != null ? ( + return isReadOnlyView ? ( ) : ( <>
- - {i18n.RESET_DEFAULT_INDEX} - - ) : null, - }} - componentProps={{ - idAria: 'detectionEngineStepDefineRuleIndices', - 'data-test-subj': 'detectionEngineStepDefineRuleIndices', - euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, - placeholder: '', - }, - }} - /> - - {i18n.IMPORT_TIMELINE_QUERY} - - ), - }} - component={QueryBarDefineRule} - componentProps={{ - browserFields, - loading: indexPatternLoadingQueryBar, - idAria: 'detectionEngineStepDefineRuleQueryBar', - indexPattern: indexPatternQueryBar, - isDisabled: isLoading, - isLoading: indexPatternLoadingQueryBar, - dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', - openTimelineSearch, - onCloseTimelineSearch: handleCloseTimelineSearch, - }} - /> - - {({ index }) => { + + + <> + + {i18n.RESET_DEFAULT_INDEX} + + ) : null, + }} + componentProps={{ + idAria: 'detectionEngineStepDefineRuleIndices', + 'data-test-subj': 'detectionEngineStepDefineRuleIndices', + euiFieldProps: { + fullWidth: true, + isDisabled: isLoading, + placeholder: '', + }, + }} + /> + + {i18n.IMPORT_TIMELINE_QUERY} + + ), + }} + component={QueryBarDefineRule} + componentProps={{ + browserFields, + loading: indexPatternLoadingQueryBar, + idAria: 'detectionEngineStepDefineRuleQueryBar', + indexPattern: indexPatternQueryBar, + isDisabled: isLoading, + isLoading: indexPatternLoadingQueryBar, + dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', + openTimelineSearch, + onCloseTimelineSearch: handleCloseTimelineSearch, + }} + /> + + + + <> + + + + + + {({ index, ruleType }) => { if (index != null) { if (deepEqual(index, indicesConfig) && !localUseIndicesConfig) { setLocalUseIndicesConfig(true); @@ -223,6 +245,15 @@ const StepDefineRuleComponent: FC = ({ setMyLocalIndicesConfig(index); } } + + if (isMlRule(ruleType) && !localIsMlRule) { + setIsMlRule(true); + clearErrors(); + } else if (!isMlRule(ruleType) && localIsMlRule) { + setIsMlRule(false); + clearErrors(); + } + return null; }} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx index e202ff030cd90..bcfcd4f4ee09d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx @@ -19,8 +19,7 @@ import { ValidationFunc, } from '../../../../../shared_imports'; import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; - -const { emptyField } = fieldValidators; +import { isMlRule } from '../../helpers'; export const schema: FormSchema = { index: { @@ -34,14 +33,25 @@ export const schema: FormSchema = { helpText: {INDEX_HELPER_TEXT}, validations: [ { - validator: emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', - { - defaultMessage: 'A minimum of one index pattern is required.', - } - ) - ), + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = !isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'A minimum of one index pattern is required.', + } + ) + )(...args); + }, }, ], }, @@ -57,8 +67,13 @@ export const schema: FormSchema = { validator: ( ...args: Parameters ): ReturnType> | undefined => { - const [{ value, path }] = args; + const [{ value, path, formData }] = args; const { query, filters } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + return isEmpty(query.query as string) && isEmpty(filters) ? { code: 'ERR_FIELD_MISSING', @@ -72,8 +87,13 @@ export const schema: FormSchema = { validator: ( ...args: Parameters ): ReturnType> | undefined => { - const [{ value, path }] = args; + const [{ value, path, formData }] = args; const { query } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + if (!isEmpty(query.query as string) && query.language === 'kuery') { try { esKuery.fromKueryExpression(query.query); @@ -85,7 +105,55 @@ export const schema: FormSchema = { }; } } - return undefined; + }, + }, + ], + }, + ruleType: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', + { + defaultMessage: 'Rule type', + } + ), + validations: [], + }, + anomalyThreshold: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', + { + defaultMessage: 'Anomaly score threshold', + } + ), + validations: [], + }, + machineLearningJobId: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', + { + defaultMessage: 'Machine Learning job', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', + { + defaultMessage: 'A Machine Learning job is required.', + } + ) + )(...args); }, }, ], diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts index dbc5dd9bbe29a..ea6b02924cb3e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts @@ -87,6 +87,7 @@ describe('helpers', () => { query: 'test query', saved_id: 'test123', index: ['filebeat-'], + type: 'saved_query', }; expect(result).toEqual(expected); @@ -106,6 +107,8 @@ describe('helpers', () => { filters: mockQueryBar.filters, query: 'test query', index: ['filebeat-'], + saved_id: '', + type: 'query', }; expect(result).toEqual(expected); @@ -574,12 +577,6 @@ describe('helpers', () => { expect(result.type).toEqual('query'); }); - test('returns NewRule with id set to ruleId if ruleId exists', () => { - const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, 'query-with-rule-id'); - - expect(result.id).toEqual('query-with-rule-id'); - }); - test('returns NewRule without id if ruleId does not exist', () => { const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts index 07578e870bf2b..1f3379bf681bb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { isEmpty } from 'lodash/fp'; +import { has, isEmpty } from 'lodash/fp'; import moment from 'moment'; -import { NewRule } from '../../../../containers/detection_engine/rules'; +import { NewRule, RuleType } from '../../../../containers/detection_engine/rules'; import { AboutStepRule, @@ -16,8 +16,8 @@ import { DefineStepRuleJson, ScheduleStepRuleJson, AboutStepRuleJson, - FormatRuleType, } from '../types'; +import { isMlRule } from '../helpers'; export const getTimeTypeValue = (time: string): { unit: string; value: number } => { const timeObj = { @@ -39,16 +39,52 @@ export const getTimeTypeValue = (time: string): { unit: string; value: number } return timeObj; }; +export interface RuleFields { + anomalyThreshold: unknown; + machineLearningJobId: unknown; + queryBar: unknown; + index: unknown; + ruleType: unknown; +} +type QueryRuleFields = Omit; +type MlRuleFields = Omit; + +const isMlFields = (fields: QueryRuleFields | MlRuleFields): fields is MlRuleFields => + has('anomalyThreshold', fields); + +export const filterRuleFieldsForType = (fields: T, type: RuleType) => { + if (isMlRule(type)) { + const { index, queryBar, ...mlRuleFields } = fields; + return mlRuleFields; + } else { + const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; + return queryRuleFields; + } +}; + export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const { queryBar, isNew, ...rest } = defineStepData; - const { filters, query, saved_id: savedId } = queryBar; - return { - ...rest, - language: query.language, - filters, - query: query.query as string, - ...(savedId != null && savedId !== '' ? { saved_id: savedId } : {}), - }; + const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); + + if (isMlFields(ruleFields)) { + const { anomalyThreshold, machineLearningJobId, isNew, ruleType, ...rest } = ruleFields; + return { + ...rest, + type: ruleType, + anomaly_threshold: anomalyThreshold, + machine_learning_job_id: machineLearningJobId, + }; + } else { + const { queryBar, isNew, ruleType, ...rest } = ruleFields; + return { + ...rest, + type: ruleType, + filters: queryBar?.filters, + language: queryBar?.query?.language, + query: queryBar?.query?.query as string, + saved_id: queryBar?.saved_id, + ...(ruleType === 'query' && queryBar?.saved_id ? { type: 'saved_query' as RuleType } : {}), + }; + } }; export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { @@ -110,15 +146,9 @@ export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRule export const formatRule = ( defineStepData: DefineStepRule, aboutStepData: AboutStepRule, - scheduleData: ScheduleStepRule, - ruleId?: string -): NewRule => { - const type: FormatRuleType = !isEmpty(defineStepData.queryBar.saved_id) ? 'saved_query' : 'query'; - const persistData = { - type, - ...formatDefineStepData(defineStepData), - ...formatAboutStepData(aboutStepData), - ...formatScheduleStepData(scheduleData), - }; - return ruleId != null ? { id: ruleId, ...persistData } : persistData; -}; + scheduleData: ScheduleStepRule +): NewRule => ({ + ...formatDefineStepData(defineStepData), + ...formatAboutStepData(aboutStepData), + ...formatScheduleStepData(scheduleData), +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index c9f44ab0048f9..67aaabfe70fda 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -98,7 +98,6 @@ const CreateRulePageComponent: React.FC = () => { const userHasNoPermissions = canUserCRUD != null && hasManageApiKey != null ? !canUserCRUD || !hasManageApiKey : false; - // eslint-disable-next-line react-hooks/rules-of-hooks const setStepData = useCallback( (step: RuleStep, data: unknown, isValid: boolean) => { stepsData.current[step] = { ...stepsData.current[step], data, isValid }; @@ -138,12 +137,10 @@ const CreateRulePageComponent: React.FC = () => { [isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule] ); - // eslint-disable-next-line react-hooks/rules-of-hooks const setStepsForm = useCallback((step: RuleStep, form: FormHook) => { stepsForm.current[step] = form; }, []); - // eslint-disable-next-line react-hooks/rules-of-hooks const getAccordionType = useCallback( (accordionId: RuleStep) => { if (accordionId === openAccordionId) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index 5e0e4223e3e27..8618bf9504861 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -195,8 +195,8 @@ const EditRulePageComponent: FC = () => { if (invalidForms.length === 0 && activeForm != null) { setTabHasError([]); - setRule( - formatRule( + setRule({ + ...formatRule( (activeFormId === RuleStep.defineRule ? activeForm.data : myDefineRuleForm.data) as DefineStepRule, @@ -205,10 +205,10 @@ const EditRulePageComponent: FC = () => { : myAboutRuleForm.data) as AboutStepRule, (activeFormId === RuleStep.scheduleRule ? activeForm.data - : myScheduleRuleForm.data) as ScheduleStepRule, - ruleId - ) - ); + : myScheduleRuleForm.data) as ScheduleStepRule + ), + ...(ruleId ? { id: ruleId } : {}), + }); } else { setTabHasError(invalidForms); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx index 0c29bc31cdebc..ee43ae5f1d6e2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx @@ -32,7 +32,10 @@ describe('rule helpers', () => { }); const defineRuleStepData = { isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, index: ['auditbeat-*'], + machineLearningJobId: '', queryBar: { query: { query: 'user.name: root or user.name: admin', @@ -180,6 +183,9 @@ describe('rule helpers', () => { const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); const expected = { isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', index: ['auditbeat-*'], queryBar: { query: { @@ -194,7 +200,7 @@ describe('rule helpers', () => { expect(result).toEqual(expected); }); - test('returns with saved_id of null if value does not exist on rule', () => { + test('returns with saved_id of undefined if value does not exist on rule', () => { const mockedRule = { ...mockRule('test-id'), }; @@ -202,6 +208,9 @@ describe('rule helpers', () => { const result: DefineStepRule = getDefineStepsData(mockedRule); const expected = { isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', index: ['auditbeat-*'], queryBar: { query: { @@ -209,7 +218,7 @@ describe('rule helpers', () => { language: 'kuery', }, filters: [], - saved_id: null, + saved_id: undefined, }, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index 1fc8a86a476f2..e59ca5e7e14e5 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -10,7 +10,7 @@ import moment from 'moment'; import { useLocation } from 'react-router-dom'; import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../containers/detection_engine/rules'; +import { Rule, RuleType } from '../../../containers/detection_engine/rules'; import { FormData, FormHook, FormSchema } from '../../../shared_imports'; import { AboutStepRule, @@ -43,18 +43,16 @@ export const getStepsData = ({ }; export const getDefineStepsData = (rule: Rule): DefineStepRule => { - const { index, query, language, filters, saved_id: savedId } = rule; - return { isNew: false, - index, + ruleType: rule.type, + anomalyThreshold: rule.anomaly_threshold ?? 50, + machineLearningJobId: rule.machine_learning_job_id ?? '', + index: rule.index ?? [], queryBar: { - query: { - query, - language, - }, - filters: filters as Filter[], - saved_id: savedId ?? null, + query: { query: rule.query ?? '', language: rule.language ?? '' }, + filters: (rule.filters ?? []) as Filter[], + saved_id: rule.saved_id, }, }; }; @@ -195,6 +193,8 @@ export const setFieldValue = ( } }); +export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; + export const redirectToDetections = ( isSignalIndexExists: boolean | null, isAuthenticated: boolean | null, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts index aa50626a1231a..447b5dc6325ee 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/types.ts @@ -5,6 +5,7 @@ */ import { Filter } from '../../../../../../../../src/plugins/data/common'; +import { RuleType } from '../../../containers/detection_engine/rules/types'; import { FieldValueQueryBar } from './components/query_bar'; import { FormData, FormHook } from '../../../shared_imports'; import { FieldValueTimeline } from './components/pick_timeline'; @@ -67,8 +68,11 @@ export interface AboutStepRuleDetails { } export interface DefineStepRule extends StepRuleData { + anomalyThreshold: number; index: string[]; + machineLearningJobId: string; queryBar: FieldValueQueryBar; + ruleType: RuleType; } export interface ScheduleStepRule extends StepRuleData { @@ -79,11 +83,14 @@ export interface ScheduleStepRule extends StepRuleData { } export interface DefineStepRuleJson { - index: string[]; - filters: Filter[]; + anomaly_threshold?: number; + index?: string[]; + filters?: Filter[]; + machine_learning_job_id?: string; saved_id?: string; - query: string; - language: string; + query?: string; + language?: string; + type: RuleType; } export interface AboutStepRuleJson { @@ -112,8 +119,6 @@ export type MyRule = Omit body: typicalPayload(), }); +export const createMlRuleRequest = () => { + const { query, language, index, ...mlParams } = typicalPayload(); + + return requestMock.create({ + method: 'post', + path: DETECTION_ENGINE_RULES_URL, + body: { + ...mlParams, + type: 'machine_learning', + anomaly_threshold: 50, + machine_learning_job_id: 'some-uuid', + }, + }); +}; + export const getSetSignalStatusByIdsRequest = () => requestMock.create({ method: 'post', @@ -349,6 +364,7 @@ export const getResult = (): RuleAlertType => ({ alertTypeId: 'siem.signals', consumer: 'siem', params: { + anomalyThreshold: undefined, description: 'Detecting root and admin users', ruleId: 'rule-1', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -357,6 +373,7 @@ export const getResult = (): RuleAlertType => ({ immutable: false, query: 'user.name: root or user.name: admin', language: 'kuery', + machineLearningJobId: undefined, outputIndex: '.siem-signals', timelineId: 'some-timeline-id', timelineTitle: 'some-timeline-title', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 6ad9efebce2dd..2b31d37dddddb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -137,7 +137,7 @@ describe('create_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query]]]' + '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index d727bbb953d2a..b819bc6919274 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -56,12 +56,14 @@ export const createRulesBulkRoute = (router: IRouter) => { .filter(rule => rule.rule_id == null || !dupes.includes(rule.rule_id)) .map(async payloadRule => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, query, language, + machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, meta, @@ -107,6 +109,7 @@ export const createRulesBulkRoute = (router: IRouter) => { const createdRule = await createRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -114,6 +117,7 @@ export const createRulesBulkRoute = (router: IRouter) => { immutable: false, query, language, + machineLearningJobId, outputIndex: finalIndex, savedId, timelineId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index d019668e2a8d1..976f371c6b1a6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -14,6 +14,7 @@ import { getNonEmptyIndex, getEmptyIndex, getFindResultWithSingleHit, + createMlRuleRequest, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; @@ -48,6 +49,13 @@ describe('create_rules', () => { }); }); + describe('creating an ML Rule', () => { + it('is successful', async () => { + const response = await server.inject(createMlRuleRequest(), context); + expect(response.status).toEqual(200); + }); + }); + describe('unhappy paths', () => { test('it returns a 400 if the index does not exist', async () => { clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); @@ -111,7 +119,7 @@ describe('create_rules', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query]]' + 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index fcfcee99f369e..42bade1ba0855 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -31,6 +31,7 @@ export const createRulesRoute = (router: IRouter): void => { }, async (context, request, response) => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, @@ -42,6 +43,7 @@ export const createRulesRoute = (router: IRouter): void => { timeline_id: timelineId, timeline_title: timelineTitle, meta, + machine_learning_job_id: machineLearningJobId, filters, rule_id: ruleId, index, @@ -93,6 +95,7 @@ export const createRulesRoute = (router: IRouter): void => { const createdRule = await createRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -105,6 +108,7 @@ export const createRulesRoute = (router: IRouter): void => { timelineId, timelineTitle, meta, + machineLearningJobId, filters, ruleId: ruleId ?? uuid.v4(), index, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index ec4e707f46e50..d92ef316aef0c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -111,6 +111,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config return null; } const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, @@ -118,6 +119,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config immutable, query, language, + machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, meta, @@ -139,6 +141,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config timeline_title: timelineTitle, version, } = parsedRule; + try { const signalsIndex = siemClient.signalsIndex; const indexExists = await getIndexExists( @@ -159,6 +162,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config await createRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -166,6 +170,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config immutable, query, language, + machineLearningJobId, outputIndex: signalsIndex, savedId, timelineId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index 19bcd2e7f0596..967fd46f7e3da 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -89,7 +89,7 @@ describe('patch_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query]]]' + '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 1658de77e3390..0c2ca882a5590 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -112,7 +112,7 @@ describe('patch_rules', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query]]' + 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 7a9159ecc852b..46639e1fe3380 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -110,7 +110,7 @@ describe('update_rules_bulk', () => { const result = server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query]]]' + '"value" at position 0 fails because [child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 777b9f3cc7a9d..859935d851126 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -47,12 +47,14 @@ export const updateRulesBulkRoute = (router: IRouter) => { const rules = await Promise.all( request.body.map(async payloadRule => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, query, language, + machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, @@ -81,6 +83,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { const rule = await updateRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, immutable: false, @@ -88,6 +91,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { from, query, language, + machineLearningJobId, outputIndex: finalIndex, savedId, savedObjectsClient, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 6ef508b817713..a6da8cd56ec17 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -115,7 +115,7 @@ describe('update_rules', () => { const result = await server.validate(request); expect(result.badRequest).toHaveBeenCalledWith( - 'child "type" fails because ["type" must be one of [query, saved_query]]' + 'child "type" fails because ["type" must be one of [query, saved_query, machine_learning]]' ); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 1393de8c725cb..a9982a9896633 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -30,12 +30,14 @@ export const updateRulesRoute = (router: IRouter) => { }, async (context, request, response) => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, from, query, language, + machine_learning_job_id: machineLearningJobId, output_index: outputIndex, saved_id: savedId, timeline_id: timelineId, @@ -77,6 +79,7 @@ export const updateRulesRoute = (router: IRouter) => { const rule = await updateRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -84,6 +87,7 @@ export const updateRulesRoute = (router: IRouter) => { immutable: false, query, language, + machineLearningJobId, outputIndex: finalIndex, savedId, savedObjectsClient, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 70fcbb2c163ca..3243ccb14f89c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -36,7 +36,7 @@ describe('utils', () => { test('should work with a full data set', () => { const fullRule = getResult(); const rule = transformAlertToRule(fullRule); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -358,7 +358,7 @@ describe('utils', () => { const fullRule = getResult(); fullRule.enabled = false; const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -424,7 +424,7 @@ describe('utils', () => { const fullRule = getResult(); fullRule.params.immutable = false; const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -490,7 +490,7 @@ describe('utils', () => { const fullRule = getResult(); fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; const rule = transformAlertToRule(fullRule); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', created_by: 'elastic', @@ -551,6 +551,22 @@ describe('utils', () => { }; expect(rule).toEqual(expected); }); + + it('transforms ML Rule fields', () => { + const mlRule = getResult(); + mlRule.params.anomalyThreshold = 55; + mlRule.params.machineLearningJobId = 'some_job_id'; + mlRule.params.type = 'machine_learning'; + + const rule = transformAlertToRule(mlRule); + expect(rule).toEqual( + expect.objectContaining({ + anomaly_threshold: 55, + machine_learning_job_id: 'some_job_id', + type: 'machine_learning', + }) + ); + }); }); describe('getIdError', () => { @@ -640,7 +656,7 @@ describe('utils', () => { total: 0, data: [getResult()], }); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -722,7 +738,7 @@ describe('utils', () => { describe('transform', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transform(getResult()); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', @@ -895,7 +911,7 @@ describe('utils', () => { describe('transformOrBulkError', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transformOrBulkError('rule-1', getResult()); - const expected: OutputRuleAlertRest = { + const expected: Partial = { created_by: 'elastic', created_at: '2019-12-13T16:40:33.400Z', updated_at: '2019-12-13T16:40:33.400Z', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index ecf669b0106c3..abd8dd7e87f03 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -106,6 +106,7 @@ export const transformAlertToRule = ( created_by: alert.createdBy, description: alert.params.description, enabled: alert.enabled, + anomaly_threshold: alert.params.anomalyThreshold, false_positives: alert.params.falsePositives, filters: alert.params.filters, from: alert.params.from, @@ -117,6 +118,7 @@ export const transformAlertToRule = ( language: alert.params.language, output_index: alert.params.outputIndex, max_signals: alert.params.maxSignals, + machine_learning_job_id: alert.params.machineLearningJobId, risk_score: alert.params.riskScore, name: alert.name, query: alert.params.query, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index 974ddcf35eeb4..ec0a8e7871b5b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -34,6 +34,8 @@ import { references, note, version, + anomaly_threshold, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -49,6 +51,11 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; * - index is a required field that must exist */ export const addPrepackagedRulesSchema = Joi.object({ + anomaly_threshold: anomaly_threshold.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), description: description.required(), enabled: enabled.default(false), false_positives: false_positives.default([]), @@ -61,8 +68,21 @@ export const addPrepackagedRulesSchema = Joi.object({ .valid(true), index: index.required(), interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), + query: query.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: query.allow('').default(''), + }), + language: language.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: language.default('kuery'), + }), + machine_learning_job_id: machine_learning_job_id.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.required(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index c9b380d3c67e1..e86963fd4594c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -8,6 +8,7 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ import { + anomaly_threshold, enabled, description, false_positives, @@ -34,12 +35,18 @@ import { references, note, version, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; export const createRulesSchema = Joi.object({ + anomaly_threshold: anomaly_threshold.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), description: description.required(), enabled: enabled.default(true), false_positives: false_positives.default([]), @@ -48,8 +55,16 @@ export const createRulesSchema = Joi.object({ rule_id, index, interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), + query: query.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: query.allow('').default(''), + }), + language: language.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: language.default('kuery'), + }), output_index, saved_id: saved_id.when('type', { is: 'saved_query', @@ -59,6 +74,11 @@ export const createRulesSchema = Joi.object({ timeline_id, timeline_title, meta, + machine_learning_job_id: machine_learning_job_id.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), risk_score: risk_score.required(), max_signals: max_signals.default(DEFAULT_MAX_SIGNALS), name: name.required(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts index bd12872c4dc72..92718b7ae71ba 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts @@ -40,6 +40,8 @@ import { references, note, version, + anomaly_threshold, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -55,6 +57,11 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; * - updated_by is optional (but ignored in the import code) */ export const importRulesSchema = Joi.object({ + anomaly_threshold: anomaly_threshold.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), id, description: description.required(), enabled: enabled.default(true), @@ -65,9 +72,22 @@ export const importRulesSchema = Joi.object({ immutable: immutable.default(false).valid(false), index, interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), + query: query.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: query.allow('').default(''), + }), + language: language.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: language.default('kuery'), + }), output_index, + machine_learning_job_id: machine_learning_job_id.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), saved_id: saved_id.when('type', { is: 'saved_query', then: Joi.required(), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts index 4d1b73fb69e5b..4496a808f6869 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts @@ -35,10 +35,13 @@ import { note, id, version, + anomaly_threshold, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ export const patchRulesSchema = Joi.object({ + anomaly_threshold, description, enabled, false_positives, @@ -50,6 +53,7 @@ export const patchRulesSchema = Joi.object({ interval, query: query.allow(''), language, + machine_learning_job_id, output_index, saved_id, timeline_id, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts index 05b85ffab7263..dd88bd80d5787 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -67,6 +67,18 @@ export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesS export const getRulesBulkPayload = (): RulesBulkSchema => [getBaseResponsePayload()]; +export const getMlRuleResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesSchema => { + const basePayload = getBaseResponsePayload(anchorDate); + const { filters, index, query, language, ...rest } = basePayload; + + return { + ...rest, + type: 'machine_learning', + anomaly_threshold: 59, + machine_learning_job_id: 'some_machine_learning_job_id', + }; +}; + export const getErrorPayload = ( id: string = '819eded6-e9c8-445b-a647-519aea39e063' ): ErrorSchema => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts index fc1c019ff97b5..1a5ee793a25da 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts @@ -12,8 +12,15 @@ import { getDependents, addSavedId, addTimelineTitle, + addQueryFields, + addMlFields, } from './check_type_dependents'; -import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils'; +import { + foldLeftRight, + getBaseResponsePayload, + getPaths, + getMlRuleResponsePayload, +} from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { exactCheck } from './exact_check'; import { RulesSchema } from './rules_schema'; @@ -375,6 +382,34 @@ describe('check_type_dependents', () => { ]); expect(message.schema).toEqual({}); }); + + test('it validates an ML rule response', () => { + const payload = getMlRuleResponsePayload(); + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + const expected = getMlRuleResponsePayload(); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(expected); + }); + + test('it rejects a response with both ML and query properties', () => { + const payload = { + ...getBaseResponsePayload(), + ...getMlRuleResponsePayload(), + }; + + const dependents = getDependents(payload); + const decoded = dependents.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['invalid keys "query,language"']); + expect(message.schema).toEqual({}); + }); }); describe('addSavedId', () => { @@ -402,4 +437,35 @@ describe('check_type_dependents', () => { expect(array.length).toEqual(2); }); }); + + describe('addQueryFields', () => { + test('should return empty array if type is not "query"', () => { + const fields = addQueryFields({ type: 'machine_learning' }); + const expected: t.Mixed[] = []; + expect(fields).toEqual(expected); + }); + + test('should return two fields for a rule of type "query"', () => { + const fields = addQueryFields({ type: 'query' }); + expect(fields.length).toEqual(2); + }); + + test('should return two fields for a rule of type "saved_query"', () => { + const fields = addQueryFields({ type: 'saved_query' }); + expect(fields.length).toEqual(2); + }); + }); + + describe('addMlFields', () => { + test('should return empty array if type is not "machine_learning"', () => { + const fields = addMlFields({ type: 'query' }); + const expected: t.Mixed[] = []; + expect(fields).toEqual(expected); + }); + + test('should return two fields for a rule of type "machine_learning"', () => { + const fields = addMlFields({ type: 'machine_learning' }); + expect(fields.length).toEqual(2); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts index 09142c8568b2d..b5a01e3e5c6df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts @@ -35,12 +35,38 @@ export const addTimelineTitle = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mi } }; +export const addQueryFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'query' || typeAndTimelineOnly.type === 'saved_query') { + return [ + t.exact(t.type({ query: dependentRulesSchema.props.query })), + t.exact(t.type({ language: dependentRulesSchema.props.language })), + ]; + } else { + return []; + } +}; + +export const addMlFields = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed[] => { + if (typeAndTimelineOnly.type === 'machine_learning') { + return [ + t.exact(t.type({ anomaly_threshold: dependentRulesSchema.props.anomaly_threshold })), + t.exact( + t.type({ machine_learning_job_id: dependentRulesSchema.props.machine_learning_job_id }) + ), + ]; + } else { + return []; + } +}; + export const getDependents = (typeAndTimelineOnly: TypeAndTimelineOnly): t.Mixed => { const dependents: t.Mixed[] = [ t.exact(requiredRulesSchema), t.exact(partialRulesSchema), ...addSavedId(typeAndTimelineOnly), ...addTimelineTitle(typeAndTimelineOnly), + ...addQueryFields(typeAndTimelineOnly), + ...addMlFields(typeAndTimelineOnly), ]; if (dependents.length > 1) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts index 945b5651be066..28b588a86aeb0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts @@ -11,6 +11,7 @@ import { Either } from 'fp-ts/lib/Either'; import { checkTypeDependents } from './check_type_dependents'; import { + anomaly_threshold, description, enabled, false_positives, @@ -24,6 +25,7 @@ import { name, output_index, max_signals, + machine_learning_job_id, query, references, severity, @@ -65,12 +67,10 @@ export const requiredRulesSchema = t.type({ immutable, interval, rule_id, - language, output_index, max_signals, risk_score, name, - query, references, severity, updated_by, @@ -91,12 +91,20 @@ export type RequiredRulesSchema = t.TypeOf; * check_type_dependents file for whichever REST flow it is going through. */ export const dependentRulesSchema = t.partial({ + // query fields + language, + query, + // when type = saved_query, saved_is is required saved_id, // These two are required together or not at all. timeline_id, timeline_title, + + // ML fields + anomaly_threshold, + machine_learning_job_id, }); /** diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts index 16f6c0fd6b8b4..072e3f5beefe2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -45,6 +45,8 @@ export const output_index = t.string; export const saved_id = t.string; export const timeline_id = t.string; export const timeline_title = t.string; +export const anomaly_threshold = PositiveInteger; +export const machine_learning_job_id = t.string; /** * Note that this is a plain unknown object because we allow the UI @@ -64,7 +66,7 @@ export const job_status = t.keyof({ succeeded: null, failed: null, 'going to run // TODO: Create a regular expression type or custom date math part type here export const to = t.string; -export const type = t.keyof({ query: null, saved_query: null }); +export const type = t.keyof({ machine_learning: null, query: null, saved_query: null }); export const queryFilter = t.string; export const references = t.array(t.string); export const per_page = PositiveInteger; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index 2ba9ec7f83253..ad7050e8dd65c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -7,6 +7,10 @@ import Joi from 'joi'; /* eslint-disable @typescript-eslint/camelcase */ +export const anomaly_threshold = Joi.number() + .integer() + .greater(-1) + .less(101); export const description = Joi.string(); export const enabled = Joi.boolean(); export const exclude_export_details = Joi.boolean(); @@ -48,7 +52,8 @@ export const risk_score = Joi.number() export const severity = Joi.string().valid('low', 'medium', 'high', 'critical'); export const status = Joi.string().valid('open', 'closed'); export const to = Joi.string(); -export const type = Joi.string().valid('query', 'saved_query'); +export const type = Joi.string().valid('query', 'saved_query', 'machine_learning'); +export const machine_learning_job_id = Joi.string(); export const queryFilter = Joi.string(); export const references = Joi.array() .items(Joi.string()) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index a72105142d287..f7a53385200df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -35,6 +35,8 @@ import { id, note, version, + anomaly_threshold, + machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ @@ -48,6 +50,11 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; * - id is on here because you can pass in an id to update using it instead of rule_id. */ export const updateRulesSchema = Joi.object({ + anomaly_threshold: anomaly_threshold.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), description: description.required(), enabled: enabled.default(true), id, @@ -57,8 +64,21 @@ export const updateRulesSchema = Joi.object({ rule_id, index, interval: interval.default('5m'), - query: query.allow('').default(''), - language: language.default('kuery'), + query: query.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: query.allow('').default(''), + }), + language: language.when('type', { + is: 'machine_learning', + then: Joi.forbidden(), + otherwise: language.default('kuery'), + }), + machine_learning_job_id: machine_learning_job_id.when('type', { + is: 'machine_learning', + then: Joi.required(), + otherwise: Joi.forbidden(), + }), output_index, saved_id: saved_id.when('type', { is: 'saved_query', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index ea87950a59b78..1b4c06fb5d828 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -12,6 +12,7 @@ import { addTags } from './add_tags'; export const createRules = ({ alertsClient, actionsClient, // TODO: Use this actionsClient once we have actions such as email, etc... + anomalyThreshold, description, enabled, falsePositives, @@ -22,6 +23,7 @@ export const createRules = ({ timelineId, timelineTitle, meta, + machineLearningJobId, filters, ruleId, immutable, @@ -47,6 +49,7 @@ export const createRules = ({ alertTypeId: SIGNALS_ID, consumer: APP_ID, params: { + anomalyThreshold, description, ruleId, index, @@ -60,6 +63,7 @@ export const createRules = ({ timelineId, timelineTitle, meta, + machineLearningJobId, filters, maxSignals, riskScore, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 3b5ef57d3dcb6..dc71ae3678f2e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -18,6 +18,7 @@ export const installPrepackagedRules = ( ): Array> => rules.reduce>>((acc, rule) => { const { + anomaly_threshold: anomalyThreshold, description, enabled, false_positives: falsePositives, @@ -25,6 +26,7 @@ export const installPrepackagedRules = ( immutable, query, language, + machine_learning_job_id: machineLearningJobId, saved_id: savedId, timeline_id: timelineId, timeline_title: timelineTitle, @@ -50,6 +52,7 @@ export const installPrepackagedRules = ( createRules({ alertsClient, actionsClient, + anomalyThreshold, description, enabled, falsePositives, @@ -57,6 +60,7 @@ export const installPrepackagedRules = ( immutable, query, language, + machineLearningJobId, outputIndex, savedId, timelineId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 922651edc4082..010f6b2ee98ff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -29,6 +29,8 @@ export const sampleRuleAlertParams = ( riskScore: riskScore ? riskScore : 50, maxSignals: maxSignals ? maxSignals : 10000, note: '', + anomalyThreshold: undefined, + machineLearningJobId: undefined, filters: undefined, savedId: undefined, timelineId: undefined, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index 9baf6a55b7f48..a9ccda2efe99c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -65,5 +65,7 @@ export const buildRule = ({ version: ruleParams.version, created_at: createdAt, updated_at: updatedAt, + machine_learning_job_id: ruleParams.machineLearningJobId, + anomaly_threshold: ruleParams.anomalyThreshold, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts new file mode 100644 index 0000000000000..d9fb9d4bbabde --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.test.ts @@ -0,0 +1,90 @@ +/* + * 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 { transformAnomalyFieldsToEcs } from './bulk_create_ml_signals'; + +const buildMockAnomaly = () => ({ + job_id: 'rare_process_by_host_linux_ecs', + result_type: 'record', + probability: 0.03406145177566593, + multi_bucket_impact: -0.0, + record_score: 10.86784984522809, + initial_record_score: 10.86784984522809, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1584482400000, + by_field_name: 'process.name', + by_field_value: 'gzip', + partition_field_name: 'host.name', + partition_field_value: 'rock01', + function: 'rare', + function_description: 'rare', + typical: [0.03406145177566593], + actual: [1.0], + influencers: [ + { + influencer_field_name: 'user.name', + influencer_field_values: ['root'], + }, + { + influencer_field_name: 'process.pid', + influencer_field_values: ['123'], + }, + { + influencer_field_name: 'host.name', + influencer_field_values: ['rock01'], + }, + ], + 'process.name': ['gzip'], + 'process.pid': ['123'], + 'user.name': ['root'], + 'host.name': ['rock01'], +}); + +describe('transformAnomalyFieldsToEcs', () => { + it('adds a @timestamp field based on timestamp', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + const expectedTime = '2020-03-17T22:00:00.000Z'; + + expect(result['@timestamp']).toEqual(expectedTime); + }); + + it('deletes dotted influencer fields', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + + const ecsKeys = Object.keys(result); + expect(ecsKeys).not.toContain('user.name'); + expect(ecsKeys).not.toContain('process.pid'); + expect(ecsKeys).not.toContain('host.name'); + }); + + it('deletes dotted entity field', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + + const ecsKeys = Object.keys(result); + expect(ecsKeys).not.toContain('process.name'); + }); + + it('creates nested influencer fields', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + + expect(result.process.pid).toEqual(['123']); + expect(result.user.name).toEqual(['root']); + expect(result.host.name).toEqual(['rock01']); + }); + + it('creates nested entity field', () => { + const anomaly = buildMockAnomaly(); + const result = transformAnomalyFieldsToEcs(anomaly); + + expect(result.process.name).toEqual(['gzip']); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts new file mode 100644 index 0000000000000..1ab34f26d4b70 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/bulk_create_ml_signals.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { flow, set, omit } from 'lodash/fp'; +import { SearchResponse } from 'elasticsearch'; + +import { Logger } from '../../../../../../../../src/core/server'; +import { AlertServices } from '../../../../../../../plugins/alerting/server'; +import { RuleTypeParams } from '../types'; +import { singleBulkCreate } from './single_bulk_create'; +import { AnomalyResults, Anomaly } from '../../machine_learning'; + +interface BulkCreateMlSignalsParams { + someResult: AnomalyResults; + ruleParams: RuleTypeParams; + services: AlertServices; + logger: Logger; + id: string; + signalsIndex: string; + name: string; + createdAt: string; + createdBy: string; + updatedAt: string; + updatedBy: string; + interval: string; + enabled: boolean; + tags: string[]; +} + +interface EcsAnomaly extends Anomaly { + '@timestamp': string; +} + +export const transformAnomalyFieldsToEcs = (anomaly: Anomaly): EcsAnomaly => { + const { + by_field_name: entityName, + by_field_value: entityValue, + influencers, + timestamp, + } = anomaly; + let errantFields = (influencers ?? []).map(influencer => ({ + name: influencer.influencer_field_name, + value: influencer.influencer_field_values, + })); + + if (entityName && entityValue) { + errantFields = [...errantFields, { name: entityName, value: [entityValue] }]; + } + + const omitDottedFields = omit(errantFields.map(field => field.name)); + const setNestedFields = errantFields.map(field => set(field.name, field.value)); + const setTimestamp = set('@timestamp', new Date(timestamp).toISOString()); + + return flow(omitDottedFields, setNestedFields, setTimestamp)(anomaly); +}; + +const transformAnomalyResultsToEcs = (results: AnomalyResults): SearchResponse => { + const transformedHits = results.hits.hits.map(({ _source, ...rest }) => ({ + ...rest, + _source: transformAnomalyFieldsToEcs(_source), + })); + + return { + ...results, + hits: { + ...results.hits, + hits: transformedHits, + }, + }; +}; + +export const bulkCreateMlSignals = async (params: BulkCreateMlSignalsParams) => { + const anomalyResults = params.someResult; + const ecsResults = transformAnomalyResultsToEcs(anomalyResults); + + return singleBulkCreate({ ...params, someResult: ecsResults }); +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts new file mode 100644 index 0000000000000..b7f752e6ba5e0 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/find_ml_signals.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; + +import { AlertServices } from '../../../../../../../plugins/alerting/server'; + +import { getAnomalies } from '../../machine_learning'; + +export const findMlSignals = async ( + jobId: string, + anomalyThreshold: number, + from: string, + to: string, + callCluster: AlertServices['callCluster'] +) => { + const params = { + jobIds: [jobId], + threshold: anomalyThreshold, + earliestMs: dateMath.parse(from)?.valueOf() ?? 0, + latestMs: dateMath.parse(to)?.valueOf() ?? 0, + }; + const relevantAnomalies = await getAnomalies(params, callCluster); + + return relevantAnomalies; +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts index 9c3e15de7ce90..82a50222dc351 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/get_filter.ts @@ -107,6 +107,11 @@ export const getFilter = async ({ throw new BadRequestError('savedId parameter should be defined'); } } + case 'machine_learning': { + throw new BadRequestError( + 'Unsupported Rule of type "machine_learning" supplied to getFilter' + ); + } } return assertUnreachable(type); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index bf7a97a29aef3..09daae8485381 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -26,8 +26,10 @@ export const mockService = { }; describe('searchAfterAndBulkCreate', () => { + let inputIndexPattern: string[] = []; beforeEach(() => { jest.clearAllMocks(); + inputIndexPattern = ['auditbeat-*']; }); test('if successful with empty search results', async () => { @@ -38,6 +40,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -93,6 +96,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -119,6 +123,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -152,6 +157,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -185,6 +191,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -220,6 +227,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -255,6 +263,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', @@ -292,6 +301,7 @@ describe('searchAfterAndBulkCreate', () => { services: mockService, logger: mockLogger, id: sampleRuleGuid, + inputIndexPattern, signalsIndex: DEFAULT_SIGNALS_INDEX, name: 'rule-name', createdAt: '2020-01-28T15:58:34.810Z', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts index 1cfd2f812a195..f54ad67af4a48 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -17,6 +17,7 @@ interface SearchAfterAndBulkCreateParams { services: AlertServices; logger: Logger; id: string; + inputIndexPattern: string[]; signalsIndex: string; name: string; createdAt: string; @@ -37,6 +38,7 @@ export const searchAfterAndBulkCreate = async ({ services, logger, id, + inputIndexPattern, signalsIndex, filter, name, @@ -77,7 +79,7 @@ export const searchAfterAndBulkCreate = async ({ // If the total number of hits for the overall search result is greater than // maxSignals, default to requesting a total of maxSignals, otherwise use the // totalHits in the response from the searchAfter query. - const maxTotalHitsSize = totalHits >= ruleParams.maxSignals ? ruleParams.maxSignals : totalHits; + const maxTotalHitsSize = Math.min(totalHits, ruleParams.maxSignals); // number of docs in the current search result let hitsSize = someResult.hits.hits.length; @@ -98,7 +100,9 @@ export const searchAfterAndBulkCreate = async ({ logger.debug(`sortIds: ${sortIds}`); const searchAfterResult: SignalSearchResponse = await singleSearchAfter({ searchAfterSortId: sortId, - ruleParams, + index: inputIndexPattern, + from: ruleParams.from, + to: ruleParams.to, services, logger, filter, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts index adbb5fa618957..7b0546f56dd15 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts @@ -14,6 +14,7 @@ import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; */ export const signalParamsSchema = () => schema.object({ + anomalyThreshold: schema.maybe(schema.number()), description: schema.string(), note: schema.nullable(schema.string()), falsePositives: schema.arrayOf(schema.string(), { defaultValue: [] }), @@ -27,6 +28,7 @@ export const signalParamsSchema = () => timelineId: schema.nullable(schema.string()), timelineTitle: schema.nullable(schema.string()), meta: schema.nullable(schema.object({}, { unknowns: 'allow' })), + machineLearningJobId: schema.maybe(schema.string()), query: schema.nullable(schema.string()), filters: schema.nullable(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), maxSignals: schema.number({ defaultValue: DEFAULT_MAX_SIGNALS }), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index e3ea121a9ebb1..7a4dcf68e0ca9 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -20,6 +20,8 @@ import { writeGapErrorToSavedObject } from './write_gap_error_to_saved_object'; import { getRuleStatusSavedObjects } from './get_rule_status_saved_objects'; import { getCurrentStatusSavedObject } from './get_current_status_saved_object'; import { writeCurrentStatusSucceeded } from './write_current_status_succeeded'; +import { findMlSignals } from './find_ml_signals'; +import { bulkCreateMlSignals } from './bulk_create_ml_signals'; export const signalRulesAlertType = ({ logger, @@ -38,11 +40,13 @@ export const signalRulesAlertType = ({ }, async executor({ previousStartedAt, alertId, services, params }) { const { + anomalyThreshold, from, ruleId, index, filters, language, + machineLearningJobId, outputIndex, savedId, query, @@ -86,33 +90,70 @@ export const signalRulesAlertType = ({ ruleStatusSavedObjects, name, }); - // set searchAfter page size to be the lesser of default page size or maxSignals. - const searchAfterSize = - DEFAULT_SEARCH_AFTER_PAGE_SIZE <= params.maxSignals - ? DEFAULT_SEARCH_AFTER_PAGE_SIZE - : params.maxSignals; + + const searchAfterSize = Math.min(params.maxSignals, DEFAULT_SEARCH_AFTER_PAGE_SIZE); + let creationSucceeded = false; + try { - const inputIndex = await getInputIndex(services, version, index); - const esFilter = await getFilter({ - type, - filters, - language, - query, - savedId, - services, - index: inputIndex, - }); + if (type === 'machine_learning') { + if (machineLearningJobId == null || anomalyThreshold == null) { + throw new Error( + `Attempted to execute machine learning rule, but it is missing job id and/or anomaly threshold for rule id: "${ruleId}", name: "${name}", signals index: "${outputIndex}", job id: "${machineLearningJobId}", anomaly threshold: "${anomalyThreshold}"` + ); + } - const noReIndex = buildEventsSearchQuery({ - index: inputIndex, - from, - to, - filter: esFilter, - size: searchAfterSize, - searchAfterSortId: undefined, - }); + const anomalyResults = await findMlSignals( + machineLearningJobId, + anomalyThreshold, + from, + to, + services.callCluster + ); + + const anomalyCount = anomalyResults.hits.hits.length; + if (anomalyCount) { + logger.info( + `Found ${anomalyCount} signals from ML anomalies for signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", pushing signals to index "${outputIndex}"` + ); + } + + creationSucceeded = await bulkCreateMlSignals({ + someResult: anomalyResults, + ruleParams: params, + services, + logger, + id: alertId, + signalsIndex: outputIndex, + name, + createdBy, + createdAt, + updatedBy, + updatedAt, + interval, + enabled, + tags, + }); + } else { + const inputIndex = await getInputIndex(services, version, index); + const esFilter = await getFilter({ + type, + filters, + language, + query, + savedId, + services, + index: inputIndex, + }); + + const noReIndex = buildEventsSearchQuery({ + index: inputIndex, + from, + to, + filter: esFilter, + size: searchAfterSize, + searchAfterSortId: undefined, + }); - try { logger.debug( `Starting signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` ); @@ -130,12 +171,13 @@ export const signalRulesAlertType = ({ ); } - const bulkIndexResult = await searchAfterAndBulkCreate({ + creationSucceeded = await searchAfterAndBulkCreate({ someResult: noReIndexResult, ruleParams: params, services, logger, id: alertId, + inputIndexPattern: inputIndex, signalsIndex: outputIndex, filter: esFilter, name, @@ -148,46 +190,35 @@ export const signalRulesAlertType = ({ pageSize: searchAfterSize, tags, }); + } - if (bulkIndexResult) { - logger.debug( - `Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}"` - ); - await writeCurrentStatusSucceeded({ - services, - currentStatusSavedObject, - }); - } else { - await writeSignalRuleExceptionToSavedObject({ - name, - alertId, - currentStatusSavedObject, - logger, - message: `Bulk Indexing signals failed. Check logs for further details \nRule name: "${name}"\nid: "${alertId}"\nrule_id: "${ruleId}"\n`, - services, - ruleStatusSavedObjects, - ruleId: ruleId ?? '(unknown rule id)', - }); - } - } catch (err) { + if (creationSucceeded) { + logger.debug( + `Finished signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}", output_index: "${outputIndex}"` + ); + await writeCurrentStatusSucceeded({ + services, + currentStatusSavedObject, + }); + } else { await writeSignalRuleExceptionToSavedObject({ name, alertId, currentStatusSavedObject, logger, - message: err?.message ?? '(no error message given)', + message: `Bulk Indexing signals failed. Check logs for further details Rule name: "${name}" id: "${alertId}" rule_id: "${ruleId}" output_index: "${outputIndex}"`, services, ruleStatusSavedObjects, ruleId: ruleId ?? '(unknown rule id)', }); } - } catch (exception) { + } catch (error) { await writeSignalRuleExceptionToSavedObject({ name, alertId, currentStatusSavedObject, logger, - message: exception?.message ?? '(no error message given)', + message: error?.message ?? '(no error message given)', services, ruleStatusSavedObjects, ruleId: ruleId ?? '(unknown rule id)', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts index a5d1f66d3089e..1685c6518def3 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.test.ts @@ -6,7 +6,6 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { - sampleRuleAlertParams, sampleDocSearchResultsNoSortId, mockLogger, sampleDocSearchResultsWithSortId, @@ -26,12 +25,13 @@ describe('singleSearchAfter', () => { test('if singleSearchAfter works without a given sort id', async () => { let searchAfterSortId; - const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsNoSortId); await expect( singleSearchAfter({ searchAfterSortId, - ruleParams: sampleParams, + index: [], + from: 'now-360s', + to: 'now', services: mockService, logger: mockLogger, pageSize: 1, @@ -41,11 +41,12 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter works with a given sort id', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockReturnValue(sampleDocSearchResultsWithSortId); const searchAfterResult = await singleSearchAfter({ searchAfterSortId, - ruleParams: sampleParams, + index: [], + from: 'now-360s', + to: 'now', services: mockService, logger: mockLogger, pageSize: 1, @@ -55,14 +56,15 @@ describe('singleSearchAfter', () => { }); test('if singleSearchAfter throws error', async () => { const searchAfterSortId = '1234567891111'; - const sampleParams = sampleRuleAlertParams(); mockService.callCluster.mockImplementation(async () => { throw Error('Fake Error'); }); await expect( singleSearchAfter({ searchAfterSortId, - ruleParams: sampleParams, + index: [], + from: 'now-360s', + to: 'now', services: mockService, logger: mockLogger, pageSize: 1, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts index a0e7047ad1cd6..bb12b5a802f8f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/single_search_after.ts @@ -5,14 +5,15 @@ */ import { AlertServices } from '../../../../../../../plugins/alerting/server'; -import { RuleTypeParams } from '../types'; import { Logger } from '../../../../../../../../src/core/server'; import { SignalSearchResponse } from './types'; import { buildEventsSearchQuery } from './build_events_query'; interface SingleSearchAfterParams { searchAfterSortId: string | undefined; - ruleParams: RuleTypeParams; + index: string[]; + from: string; + to: string; services: AlertServices; logger: Logger; pageSize: number; @@ -22,7 +23,9 @@ interface SingleSearchAfterParams { // utilize search_after for paging results into bulk. export const singleSearchAfter = async ({ searchAfterSortId, - ruleParams, + index, + from, + to, services, filter, logger, @@ -33,9 +36,9 @@ export const singleSearchAfter = async ({ } try { const searchAfterQuery = buildEventsSearchQuery({ - index: ruleParams.index, - from: ruleParams.from, - to: ruleParams.to, + index, + from, + to, filter, size: pageSize, searchAfterSortId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts index eaed3f2ead3a5..1ee3d4f0eb8e4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/types.ts @@ -104,7 +104,7 @@ export interface GetResponse { } export type SignalSearchResponse = SearchResponse; -export type SignalSourceHit = SignalSearchResponse['hits']['hits'][0]; +export type SignalSourceHit = SignalSearchResponse['hits']['hits'][number]; export type RuleExecutorOptions = Omit & { params: RuleAlertParams & { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index fa43ac1debb92..f77924aafadf8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -22,7 +22,10 @@ export interface ThreatParams { technique: IMitreAttack[]; } +export type RuleType = 'query' | 'saved_query' | 'machine_learning'; + export interface RuleAlertParams { + anomalyThreshold: number | undefined; description: string; note: string | undefined | null; enabled: boolean; @@ -30,11 +33,12 @@ export interface RuleAlertParams { filters: PartialFilter[] | undefined | null; from: string; immutable: boolean; - index: string[]; + index: string[] | undefined | null; interval: string; ruleId: string | undefined | null; language: string | undefined | null; maxSignals: number; + machineLearningJobId: string | undefined; riskScore: number; outputIndex: string; name: string; @@ -48,7 +52,7 @@ export interface RuleAlertParams { timelineId: string | undefined | null; timelineTitle: string | undefined | null; threat: ThreatParams[] | undefined | null; - type: 'query' | 'saved_query'; + type: RuleType; version: number; throttle?: string; } @@ -57,10 +61,12 @@ export type RuleTypeParams = Omit & { + anomaly_threshold: RuleAlertParams['anomalyThreshold']; rule_id: RuleAlertParams['ruleId']; false_positives: RuleAlertParams['falsePositives']; saved_id?: RuleAlertParams['savedId']; timeline_id: RuleAlertParams['timelineId']; timeline_title: RuleAlertParams['timelineTitle']; max_signals: RuleAlertParams['maxSignals']; + machine_learning_job_id: RuleAlertParams['machineLearningJobId']; risk_score: RuleAlertParams['riskScore']; output_index: RuleAlertParams['outputIndex']; created_at: string; diff --git a/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts b/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts new file mode 100644 index 0000000000000..aa83df15f68d4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/machine_learning/index.ts @@ -0,0 +1,90 @@ +/* + * 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 { SearchResponse } from 'elasticsearch'; + +import { AlertServices } from '../../../../../../plugins/alerting/server'; +import { AnomalyRecordDoc as Anomaly } from '../../../../../../plugins/ml/common/types/anomalies'; + +export { Anomaly }; +export type AnomalyResults = SearchResponse; + +export interface AnomaliesSearchParams { + jobIds: string[]; + threshold: number; + earliestMs: number; + latestMs: number; + maxRecords?: number; +} + +export const getAnomalies = async ( + params: AnomaliesSearchParams, + callCluster: AlertServices['callCluster'] +): Promise => { + const boolCriteria = buildCriteria(params); + + return callCluster('search', { + index: '.ml-anomalies-*', + size: params.maxRecords || 100, + body: { + query: { + bool: { + filter: [ + { + query_string: { + query: 'result_type:record', + analyze_wildcard: false, + }, + }, + { + bool: { + must: boolCriteria, + }, + }, + ], + }, + }, + sort: [{ record_score: { order: 'desc' } }], + }, + }); +}; + +const buildCriteria = (params: AnomaliesSearchParams): object[] => { + const { earliestMs, jobIds, latestMs, threshold } = params; + const jobIdsFilterable = jobIds.length > 0 && !(jobIds.length === 1 && jobIds[0] === '*'); + + const boolCriteria: object[] = [ + { + range: { + timestamp: { + gte: earliestMs, + lte: latestMs, + format: 'epoch_millis', + }, + }, + }, + { + range: { + record_score: { + gte: threshold, + }, + }, + }, + ]; + + if (jobIdsFilterable) { + const jobIdFilter = jobIds.map(jobId => `job_id:${jobId}`).join(' OR '); + + boolCriteria.push({ + query_string: { + analyze_wildcard: false, + query: jobIdFilter, + }, + }); + } + + return boolCriteria; +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index d6a238e5b0940..91088acb7a51c 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -18,6 +18,8 @@ import { getSimpleRuleWithoutRuleId, removeServerGeneratedProperties, removeServerGeneratedPropertiesIncludingRuleId, + getSimpleMlRule, + getSimpleMlRuleOutput, } from './utils'; // eslint-disable-next-line import/no-default-export @@ -63,6 +65,20 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare).to.eql(getSimpleRuleOutput()); }); + it('should create a single rule without an input index', async () => { + const { index, ...payload } = getSimpleRule(); + const { index: _index, ...expected } = getSimpleRuleOutput(); + + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(payload) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(expected); + }); + it('should create a single rule without a rule_id', async () => { const { body } = await supertest .post(DETECTION_ENGINE_RULES_URL) @@ -74,6 +90,17 @@ export default ({ getService }: FtrProviderContext) => { expect(bodyToCompare).to.eql(getSimpleRuleOutputWithoutRuleId()); }); + it('should create a single Machine Learning rule', async () => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(getSimpleMlRule()) + .expect(200); + + const bodyToCompare = removeServerGeneratedProperties(body); + expect(bodyToCompare).to.eql(getSimpleMlRuleOutput()); + }); + it('should cause a 409 conflict if we attempt to create the same rule_id twice', async () => { await supertest .post(DETECTION_ENGINE_RULES_URL) diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts index 1570124cdb92b..8847a2fdb21af 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -49,10 +49,26 @@ export const getSimpleRule = (ruleId = 'rule-1'): Partial = risk_score: 1, rule_id: ruleId, severity: 'high', + index: ['auditbeat-*'], type: 'query', query: 'user.name: root or user.name: admin', }); +/** + * This is a representative ML rule payload as expected by the server + * @param ruleId + */ +export const getSimpleMlRule = (ruleId = 'rule-1'): Partial => ({ + name: 'Simple ML Rule', + description: 'Simple Machine Learning Rule', + anomaly_threshold: 44, + risk_score: 1, + rule_id: ruleId, + severity: 'high', + machine_learning_job_id: 'some_job_id', + type: 'machine_learning', +}); + export const getSignalStatus = () => ({ aggs: { statuses: { terms: { field: 'signal.status', size: 10 } } }, }); @@ -118,6 +134,7 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial => { + const rule = getSimpleRuleOutput(ruleId); + const { query, language, index, ...rest } = rule; + + return { + ...rest, + name: 'Simple ML Rule', + description: 'Simple Machine Learning Rule', + anomaly_threshold: 44, + machine_learning_job_id: 'some_job_id', + type: 'machine_learning', + }; +}; + /** * Remove all alerts from the .kibana index * @param es The ElasticSearch handle From 8bb1ae8b686fda5342303c762dcbef601e0c71c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 19 Mar 2020 13:20:31 -0400 Subject: [PATCH 03/22] Clear changes when canceling an edit to an alert (#60518) (#60634) * Clear alerting edit flyout after canceling an edit * Add functional test * Fix merge conflicts --- .../alerts_list/components/alerts_list.tsx | 2 +- .../apps/triggers_actions_ui/alerts.ts | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index c409dead7c850..4bcfef78abd71 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -443,7 +443,7 @@ export const AlertsList: React.FunctionComponent = () => { addFlyoutVisible={alertFlyoutVisible} setAddFlyoutVisibility={setAlertFlyoutVisibility} /> - {editedAlertItem ? ( + {editFlyoutVisible && editedAlertItem ? ( { ]); }); + it('should reset alert when canceling an edit', async () => { + const createdAlert = await createAlert({ + alertTypeId: '.index-threshold', + name: generateUniqueKey(), + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000, 5000], + index: ['.kibana_1'], + timeField: 'alert', + }, + }); + await pageObjects.common.navigateToApp('triggersActions'); + await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); + + const editLink = await testSubjects.findAll('alertsTableCell-editLink'); + await editLink[0].click(); + + const updatedAlertName = 'Changed Alert Name'; + const nameInputToUpdate = await testSubjects.find('alertNameInput'); + await nameInputToUpdate.click(); + await nameInputToUpdate.clearValue(); + await nameInputToUpdate.type(updatedAlertName); + + await testSubjects.click('cancelSaveEditedAlertButton'); + await find.waitForDeletedByCssSelector('[data-test-subj="cancelSaveEditedAlertButton"]'); + + const editLinkPostCancel = await testSubjects.findAll('alertsTableCell-editLink'); + await editLinkPostCancel[0].click(); + + const nameInputAfterCancel = await testSubjects.find('alertNameInput'); + const textAfterCancel = await nameInputAfterCancel.getAttribute('value'); + expect(textAfterCancel).to.eql(createdAlert.name); + }); + it('should search for tags', async () => { const createdAlert = await createAlert(); await pageObjects.common.navigateToApp('triggersActions'); From c7b472fb6a9ddf150fd7ab9ddcc02a6a41e33442 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Thu, 19 Mar 2020 14:32:41 -0400 Subject: [PATCH 04/22] Endpoint: Change the input type for @kbn/config-schema to work with more schemas (#60007) (#60629) --- x-pack/plugins/endpoint/common/types.ts | 46 ++++++++++++------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index 2ff99ad97e1c9..aa326c663965d 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -6,7 +6,6 @@ import { SearchResponse } from 'elasticsearch'; import { TypeOf } from '@kbn/config-schema'; -import * as kbnConfigSchemaTypes from '@kbn/config-schema/target/types/types'; import { alertingIndexGetQuerySchema } from './schema/alert_index'; /** @@ -352,16 +351,16 @@ export type PageId = 'alertsPage' | 'managementPage' | 'policyListPage'; * const input: KbnConfigSchemaInputTypeOf = value * schema.validate(input) // should be valid * ``` + * Note that because the types coming from `@kbn/config-schema`'s schemas sometimes have deeply nested + * `Type` types, we process the result of `TypeOf` instead, as this will be consistent. */ -type KbnConfigSchemaInputTypeOf< - T extends kbnConfigSchemaTypes.Type -> = T extends kbnConfigSchemaTypes.ObjectType +type KbnConfigSchemaInputTypeOf = T extends Record ? KbnConfigSchemaInputObjectTypeOf< T > /** `schema.number()` accepts strings, so this type should accept them as well. */ - : kbnConfigSchemaTypes.Type extends T - ? TypeOf | string - : TypeOf; + : number extends T + ? T | string + : T; /** * Works like ObjectResultType, except that 'maybe' schema will create an optional key. @@ -369,20 +368,15 @@ type KbnConfigSchemaInputTypeOf< * * Instead of using this directly, use `InputTypeOf`. */ -type KbnConfigSchemaInputObjectTypeOf< - T extends kbnConfigSchemaTypes.ObjectType -> = T extends kbnConfigSchemaTypes.ObjectType - ? { - /** Use ? to make the field optional if the prop accepts undefined. - * This allows us to avoid writing `field: undefined` for optional fields. - */ - [K in Exclude< - keyof P, - keyof KbnConfigSchemaNonOptionalProps

- >]?: KbnConfigSchemaInputTypeOf; - } & - { [K in keyof KbnConfigSchemaNonOptionalProps

]: KbnConfigSchemaInputTypeOf } - : never; +type KbnConfigSchemaInputObjectTypeOf

> = { + /** Use ? to make the field optional if the prop accepts undefined. + * This allows us to avoid writing `field: undefined` for optional fields. + */ + [K in Exclude>]?: KbnConfigSchemaInputTypeOf< + P[K] + >; +} & + { [K in keyof KbnConfigSchemaNonOptionalProps

]: KbnConfigSchemaInputTypeOf }; /** * Takes the props of a schema.object type, and returns a version that excludes @@ -390,10 +384,14 @@ type KbnConfigSchemaInputObjectTypeOf< * * Instead of using this directly, use `InputTypeOf`. */ -type KbnConfigSchemaNonOptionalProps = Pick< +type KbnConfigSchemaNonOptionalProps> = Pick< Props, { - [Key in keyof Props]: undefined extends TypeOf ? never : Key; + [Key in keyof Props]: undefined extends Props[Key] + ? never + : null extends Props[Key] + ? never + : Key; }[keyof Props] >; @@ -401,7 +399,7 @@ type KbnConfigSchemaNonOptionalProps = * Query params to pass to the alert API when fetching new data. */ export type AlertingIndexGetQueryInput = KbnConfigSchemaInputTypeOf< - typeof alertingIndexGetQuerySchema + TypeOf >; /** From 30cae691eecbd864794981af815f02ff0a981c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20C=C3=B4t=C3=A9?= Date: Thu, 19 Mar 2020 15:09:53 -0400 Subject: [PATCH 05/22] Fix race condition in flaky alerting test (#60438) (#60663) * Fix race condition in flaky test * Fix flakiness in test * Fix more flakiness --- .../common/lib/task_manager_utils.ts | 40 ++++++++++++++- .../tests/alerting/alerts.ts | 51 +++++++++++-------- 2 files changed, 69 insertions(+), 22 deletions(-) diff --git a/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts b/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts index 3a1d035a023c2..8eb0d11bbb569 100644 --- a/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts +++ b/x-pack/test/alerting_api_integration/common/lib/task_manager_utils.ts @@ -13,7 +13,7 @@ export class TaskManagerUtils { this.retry = retry; } - async waitForIdle(taskRunAtFilter: Date) { + async waitForEmpty(taskRunAtFilter: Date) { return await this.retry.try(async () => { const searchResult = await this.es.search({ index: '.kibana_task_manager', @@ -44,6 +44,44 @@ export class TaskManagerUtils { }); } + async waitForAllTasksIdle(taskRunAtFilter: Date) { + return await this.retry.try(async () => { + const searchResult = await this.es.search({ + index: '.kibana_task_manager', + body: { + query: { + bool: { + must: [ + { + terms: { + 'task.scope': ['actions', 'alerting'], + }, + }, + { + range: { + 'task.scheduledAt': { + gte: taskRunAtFilter, + }, + }, + }, + ], + must_not: [ + { + term: { + 'task.status': 'idle', + }, + }, + ], + }, + }, + }, + }); + if (searchResult.hits.total.value) { + throw new Error(`Expected 0 non-idle tasks but received ${searchResult.hits.total.value}`); + } + }); + } + async waitForActionTaskParamsToBeCleanedUp(createdAtFilter: Date): Promise { return await this.retry.try(async () => { const searchResult = await this.es.search({ diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts index 6766705f688a6..6eed28cc381dd 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/alerts.ts @@ -26,9 +26,7 @@ export default function alertTests({ getService }: FtrProviderContext) { const esTestIndexTool = new ESTestIndexTool(es, retry); const taskManagerUtils = new TaskManagerUtils(es, retry); - // FLAKY: https://github.com/elastic/kibana/issues/58643 - // FLAKY: https://github.com/elastic/kibana/issues/58991 - describe.skip('alerts', () => { + describe('alerts', () => { const authorizationIndex = '.kibana-test-authorization'; const objectRemover = new ObjectRemover(supertest); @@ -99,9 +97,11 @@ export default function alertTests({ getService }: FtrProviderContext) { // Wait for the action to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.index-record', reference); + await taskManagerUtils.waitForAllTasksIdle(testStart); + const alertId = response.body.id; await alertUtils.disable(alertId); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Ensure only 1 alert executed with proper params const alertSearchResult = await esTestIndexTool.search( @@ -166,17 +166,23 @@ instanceStateValue: true }); it('should pass updated alert params to executor', async () => { + const testStart = new Date(); // create an alert const reference = alertUtils.generateReference(); - const overwrites = { - throttle: '1s', - schedule: { interval: '1s' }, - }; - const response = await alertUtils.createAlwaysFiringAction({ reference, overwrites }); + const response = await alertUtils.createAlwaysFiringAction({ + reference, + overwrites: { throttle: null }, + }); // only need to test creation success paths if (response.statusCode !== 200) return; + // Wait for the action to index a document before disabling the alert and waiting for tasks to finish + await esTestIndexTool.waitForDocs('action:test.index-record', reference); + + // Avoid invalidating an API key while the alert is executing + await taskManagerUtils.waitForAllTasksIdle(testStart); + // update the alert with super user const alertId = response.body.id; const reference2 = alertUtils.generateReference(); @@ -188,8 +194,8 @@ instanceStateValue: true overwrites: { name: 'def', tags: ['fee', 'fi', 'fo'], - throttle: '1s', - schedule: { interval: '1s' }, + // This will cause the task to re-run on update + schedule: { interval: '59s' }, }, }); @@ -197,6 +203,9 @@ instanceStateValue: true // make sure alert info passed to executor is correct await esTestIndexTool.waitForDocs('alert:test.always-firing', reference2); + + await taskManagerUtils.waitForAllTasksIdle(testStart); + await alertUtils.disable(alertId); const alertSearchResult = await esTestIndexTool.search( 'alert:test.always-firing', @@ -359,7 +368,7 @@ instanceStateValue: true // Wait for test.authorization to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.authorization', reference); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Ensure only 1 document exists with proper params searchResult = await esTestIndexTool.search('alert:test.authorization', reference); @@ -387,7 +396,7 @@ instanceStateValue: true // Wait for test.authorization to index a document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.authorization', reference); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Ensure only 1 document exists with proper params searchResult = await esTestIndexTool.search('alert:test.authorization', reference); @@ -467,7 +476,7 @@ instanceStateValue: true // Ensure test.authorization indexed 1 document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.authorization', reference); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Ensure only 1 document with proper params exists searchResult = await esTestIndexTool.search('action:test.authorization', reference); @@ -495,7 +504,7 @@ instanceStateValue: true // Ensure test.authorization indexed 1 document before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.authorization', reference); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Ensure only 1 document with proper params exists searchResult = await esTestIndexTool.search('action:test.authorization', reference); @@ -544,7 +553,7 @@ instanceStateValue: true // Wait until alerts scheduled actions 3 times before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 3); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Ensure actions only executed once const searchResult = await esTestIndexTool.search( @@ -610,7 +619,7 @@ instanceStateValue: true // Wait for actions to execute twice before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.index-record', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Ensure only 2 actions with proper params exists const searchResult = await esTestIndexTool.search( @@ -660,7 +669,7 @@ instanceStateValue: true // Actions should execute twice before widning things down await esTestIndexTool.waitForDocs('action:test.index-record', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Ensure only 2 actions are executed const searchResult = await esTestIndexTool.search( @@ -705,7 +714,7 @@ instanceStateValue: true // execution once before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Should not have executed any action const executedActionsResult = await esTestIndexTool.search( @@ -750,7 +759,7 @@ instanceStateValue: true // once before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('alert:test.always-firing', reference, 2); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Should not have executed any action const executedActionsResult = await esTestIndexTool.search( @@ -796,7 +805,7 @@ instanceStateValue: true // Ensure actions are executed once before disabling the alert and waiting for tasks to finish await esTestIndexTool.waitForDocs('action:test.index-record', reference, 1); await alertUtils.disable(response.body.id); - await taskManagerUtils.waitForIdle(testStart); + await taskManagerUtils.waitForEmpty(testStart); // Should have one document indexed by the action const searchResult = await esTestIndexTool.search( From 24ffb192b1b30fdac04a137179d9f9ff5b3b47ce Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 19 Mar 2020 19:26:37 +0000 Subject: [PATCH 06/22] [ML] Fixing file data visualizer override arguments (#60627) (#60654) --- .../datavisualizer/file_based/components/utils/utils.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.js b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.js index 3bf128f84aa78..39cd25ba87d8c 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.js +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/components/utils/utils.js @@ -66,6 +66,10 @@ export function createUrlOverrides(overrides, originalSettings) { ) { formattedOverrides.format = originalSettings.format; } + + if (Array.isArray(formattedOverrides.column_names)) { + formattedOverrides.column_names = formattedOverrides.column_names.join(); + } } if (formattedOverrides.format === '' && originalSettings.format === 'semi_structured_text') { @@ -82,11 +86,6 @@ export function createUrlOverrides(overrides, originalSettings) { formattedOverrides.column_names = ''; } - // escape grok pattern as it can contain bad characters - if (formattedOverrides.grok_pattern !== '') { - formattedOverrides.grok_pattern = encodeURIComponent(formattedOverrides.grok_pattern); - } - if (formattedOverrides.lines_to_sample === '') { formattedOverrides.lines_to_sample = overrides.linesToSample; } From 99f4ce5b814a55ed773487c3d76796194c6cfe14 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Thu, 19 Mar 2020 20:29:28 +0100 Subject: [PATCH 07/22] [7.x] [APM] Optimize service map query (#60412) (#60599) * [APM] Optimize service map query Closes #60411. - Chunk trace lookup - Remove pagination, move dedupe logic to server * Fix imports * Fix imports again Co-authored-by: Nathan L Smith Co-authored-by: Nathan L Smith --- .../app/ServiceMap/Cytoscape.stories.tsx | 2 +- .../app/ServiceMap/get_cytoscape_elements.ts | 179 ++++-------------- .../components/app/ServiceMap/index.tsx | 124 +++--------- x-pack/plugins/apm/server/index.ts | 17 ++ .../apm/server/lib/helpers/setup_request.ts | 1 + .../lib/service_map/dedupe_connections.ts | 123 ++++++++++++ .../server/lib/service_map/get_service_map.ts | 61 +++--- .../lib/service_map/get_trace_sample_ids.ts | 89 ++++----- .../plugins/apm/server/routes/service_map.ts | 12 +- 9 files changed, 293 insertions(+), 315 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/service_map/dedupe_connections.ts diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx index 6f7b743d8b779..b18f462b54171 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/Cytoscape.stories.tsx @@ -13,7 +13,7 @@ import { getCytoscapeElements } from './get_cytoscape_elements'; import serviceMapResponse from './cytoscape-layout-test-response.json'; import { iconForNode } from './icons'; -const elementsFromResponses = getCytoscapeElements([serviceMapResponse], ''); +const elementsFromResponses = getCytoscapeElements(serviceMapResponse, ''); storiesOf('app/ServiceMap/Cytoscape', module).add( 'example', diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts index 9ba70646598fc..4017aa2e3cdd9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/get_cytoscape_elements.ts @@ -4,166 +4,63 @@ * you may not use this file except in compliance with the Elastic License. */ import { ValuesType } from 'utility-types'; -import { sortBy, isEqual } from 'lodash'; -import { - Connection, - ConnectionNode -} from '../../../../../../../plugins/apm/common/service_map'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceMapAPIResponse } from '../../../../../../../plugins/apm/server/lib/service_map/get_service_map'; import { getAPMHref } from '../../shared/Links/apm/APMLink'; -function getConnectionNodeId(node: ConnectionNode): string { - if ('destination.address' in node) { - // use a prefix to distinguish exernal destination ids from services - return `>${node['destination.address']}`; - } - return node['service.name']; -} - -function getConnectionId(connection: Connection) { - return `${getConnectionNodeId(connection.source)}~${getConnectionNodeId( - connection.destination - )}`; -} export function getCytoscapeElements( - responses: ServiceMapAPIResponse[], + response: ServiceMapAPIResponse, search: string ) { - const discoveredServices = responses.flatMap( - response => response.discoveredServices - ); - - const serviceNodes = responses - .flatMap(response => response.services) - .map(service => ({ - ...service, - id: service['service.name'] - })); - - // maps destination.address to service.name if possible - function getConnectionNode(node: ConnectionNode) { - let mappedNode: ConnectionNode | undefined; - - if ('destination.address' in node) { - mappedNode = discoveredServices.find(map => isEqual(map.from, node))?.to; - } - - if (!mappedNode) { - mappedNode = node; - } - - return { - ...mappedNode, - id: getConnectionNodeId(mappedNode) - }; - } - - // build connections with mapped nodes - const connections = responses - .flatMap(response => response.connections) - .map(connection => { - const source = getConnectionNode(connection.source); - const destination = getConnectionNode(connection.destination); - - return { - source, - destination, - id: getConnectionId({ source, destination }) - }; - }) - .filter(connection => connection.source.id !== connection.destination.id); - - const nodes = connections - .flatMap(connection => [connection.source, connection.destination]) - .concat(serviceNodes); - - type ConnectionWithId = ValuesType; - type ConnectionNodeWithId = ValuesType; - - const connectionsById = connections.reduce((connectionMap, connection) => { - return { - ...connectionMap, - [connection.id]: connection - }; - }, {} as Record); + const { nodes, connections } = response; const nodesById = nodes.reduce((nodeMap, node) => { return { ...nodeMap, [node.id]: node }; - }, {} as Record); - - const cyNodes = (Object.values(nodesById) as ConnectionNodeWithId[]).map( - node => { - let data = {}; - - if ('service.name' in node) { - data = { - href: getAPMHref( - `/services/${node['service.name']}/service-map`, - search - ), - agentName: node['agent.name'], - frameworkName: node['service.framework.name'], - type: 'service' - }; - } - - if ('span.type' in node) { - data = { - // For nodes with span.type "db", convert it to "database". Otherwise leave it as-is. - type: node['span.type'] === 'db' ? 'database' : node['span.type'], - // Externals should not have a subtype so make it undefined if the type is external. - subtype: node['span.type'] !== 'external' && node['span.subtype'] - }; - } - - return { - group: 'nodes' as const, - data: { - id: node.id, - label: - 'service.name' in node - ? node['service.name'] - : node['destination.address'], - ...data - } + }, {} as Record>); + + const cyNodes = (Object.values(nodesById) as Array< + ValuesType + >).map(node => { + let data = {}; + + if ('service.name' in node) { + data = { + href: getAPMHref( + `/services/${node['service.name']}/service-map`, + search + ), + agentName: node['agent.name'], + frameworkName: node['service.framework.name'], + type: 'service' }; } - ); - - // instead of adding connections in two directions, - // we add a `bidirectional` flag to use in styling - // and hide the inverse edge when rendering - const dedupedConnections = (sortBy( - Object.values(connectionsById), - // make sure that order is stable - 'id' - ) as ConnectionWithId[]).reduce< - Array< - ConnectionWithId & { bidirectional?: boolean; isInverseEdge?: boolean } - > - >((prev, connection) => { - const reversedConnection = prev.find( - c => - c.destination.id === connection.source.id && - c.source.id === connection.destination.id - ); - if (reversedConnection) { - reversedConnection.bidirectional = true; - return prev.concat({ - ...connection, - isInverseEdge: true - }); + if ('span.type' in node) { + data = { + // For nodes with span.type "db", convert it to "database". Otherwise leave it as-is. + type: node['span.type'] === 'db' ? 'database' : node['span.type'], + // Externals should not have a subtype so make it undefined if the type is external. + subtype: node['span.type'] !== 'external' && node['span.subtype'] + }; } - return prev.concat(connection); - }, []); + return { + group: 'nodes' as const, + data: { + id: node.id, + label: + 'service.name' in node + ? node['service.name'] + : node['destination.address'], + ...data + } + }; + }); - const cyEdges = dedupedConnections.map(connection => { + const cyEdges = connections.map(connection => { return { group: 'edges' as const, classes: connection.isInverseEdge ? 'invisible' : undefined, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx index 93aa3d406028c..6222a00a9e888 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,26 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiBetaBadge } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; -import { ElementDefinition } from 'cytoscape'; -import { find, isEqual } from 'lodash'; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState -} from 'react'; -import { EuiBetaBadge } from '@elastic/eui'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { isValidPlatinumLicense } from '../../../../../../../plugins/apm/common/service_map'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceMapAPIResponse } from '../../../../../../../plugins/apm/server/lib/service_map/get_service_map'; -import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; import { useDeepObjectIdentity } from '../../../hooks/useDeepObjectIdentity'; +import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; -import { useLoadingIndicator } from '../../../hooks/useLoadingIndicator'; import { useLocation } from '../../../hooks/useLocation'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; @@ -64,13 +53,11 @@ const BetaBadgeContainer = styled.div` top: ${theme.gutterTypes.gutterSmall}; z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ `; -const MAX_REQUESTS = 5; export function ServiceMap({ serviceName }: ServiceMapProps) { const license = useLicense(); const { search } = useLocation(); const { urlParams, uiFilters } = useUrlParams(); - const { notifications } = useApmPluginContext().core; const params = useDeepObjectIdentity({ start: urlParams.start, end: urlParams.end, @@ -82,95 +69,28 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { } }); - const renderedElements = useRef([]); - - const [responses, setResponses] = useState([]); - - const { setIsLoading } = useLoadingIndicator(); - - const [, _setUnusedState] = useState(false); - - const elements = useMemo(() => getCytoscapeElements(responses, search), [ - responses, - search - ]); - - const forceUpdate = useCallback(() => _setUnusedState(value => !value), []); - - const getNext = useCallback( - async (input: { reset?: boolean; after?: string | undefined }) => { - const { start, end, uiFilters: strippedUiFilters, ...query } = params; - - if (input.reset) { - renderedElements.current = []; - setResponses([]); - } - - if (start && end) { - setIsLoading(true); - try { - const data = await callApmApi({ - pathname: '/api/apm/service-map', - params: { - query: { - ...query, - start, - end, - uiFilters: JSON.stringify(strippedUiFilters), - after: input.after - } - } - }); - setResponses(resp => resp.concat(data)); - - const shouldGetNext = - responses.length + 1 < MAX_REQUESTS && data.after; - - if (shouldGetNext) { - await getNext({ after: data.after }); - } else { - setIsLoading(false); + const { data } = useFetcher(() => { + const { start, end } = params; + if (start && end) { + return callApmApi({ + pathname: '/api/apm/service-map', + params: { + query: { + ...params, + start, + end, + uiFilters: JSON.stringify(params.uiFilters) } - } catch (error) { - setIsLoading(false); - notifications.toasts.addError(error, { - title: i18n.translate('xpack.apm.errorServiceMapData', { - defaultMessage: `Error loading service connections` - }) - }); } - } - }, - [params, setIsLoading, responses.length, notifications.toasts] - ); - - useEffect(() => { - const loadServiceMaps = async () => { - await getNext({ reset: true }); - }; - - loadServiceMaps(); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [params]); - - useEffect(() => { - if (renderedElements.current.length === 0) { - renderedElements.current = elements; - return; + }); } + }, [params]); - const newElements = elements.filter(element => { - return !find(renderedElements.current, el => isEqual(el, element)); - }); - - if (newElements.length > 0 && renderedElements.current.length > 0) { - renderedElements.current = elements; - forceUpdate(); - } - }, [elements, forceUpdate]); + const elements = useMemo(() => { + return data ? getCytoscapeElements(data as any, search) : []; + }, [data, search]); - const { ref: wrapperRef, width, height } = useRefDimensions(); + const { ref, height, width } = useRefDimensions(); if (!license) { return null; @@ -179,10 +99,10 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return isValidPlatinumLicense(license) ? (

${node['destination.address']}`; + } + return node['service.name']; +} + +function getConnectionId(connection: Connection) { + return `${getConnectionNodeId(connection.source)}~${getConnectionNodeId( + connection.destination + )}`; +} + +type ServiceMapResponse = ConnectionsResponse & { services: ServicesResponse }; + +export function dedupeConnections(response: ServiceMapResponse) { + const { discoveredServices, services, connections } = response; + + const serviceNodes = services.map(service => ({ + ...service, + id: service['service.name'] + })); + + // maps destination.address to service.name if possible + function getConnectionNode(node: ConnectionNode) { + let mappedNode: ConnectionNode | undefined; + + if ('destination.address' in node) { + mappedNode = discoveredServices.find(map => isEqual(map.from, node))?.to; + } + + if (!mappedNode) { + mappedNode = node; + } + + return { + ...mappedNode, + id: getConnectionNodeId(mappedNode) + }; + } + + // build connections with mapped nodes + const mappedConnections = connections + .map(connection => { + const source = getConnectionNode(connection.source); + const destination = getConnectionNode(connection.destination); + + return { + source, + destination, + id: getConnectionId({ source, destination }) + }; + }) + .filter(connection => connection.source.id !== connection.destination.id); + + const nodes = mappedConnections + .flatMap(connection => [connection.source, connection.destination]) + .concat(serviceNodes); + + const dedupedNodes: typeof nodes = []; + + nodes.forEach(node => { + if (!dedupedNodes.find(dedupedNode => isEqual(node, dedupedNode))) { + dedupedNodes.push(node); + } + }); + + type ConnectionWithId = ValuesType; + + const connectionsById = mappedConnections.reduce( + (connectionMap, connection) => { + return { + ...connectionMap, + [connection.id]: connection + }; + }, + {} as Record + ); + + // instead of adding connections in two directions, + // we add a `bidirectional` flag to use in styling + const dedupedConnections = (sortBy( + Object.values(connectionsById), + // make sure that order is stable + 'id' + ) as ConnectionWithId[]).reduce< + Array< + ConnectionWithId & { bidirectional?: boolean; isInverseEdge?: boolean } + > + >((prev, connection) => { + const reversedConnection = prev.find( + c => + c.destination.id === connection.source.id && + c.source.id === connection.destination.id + ); + + if (reversedConnection) { + reversedConnection.bidirectional = true; + return prev.concat({ + ...connection, + isInverseEdge: true + }); + } + + return prev.concat(connection); + }, []); + + return { + nodes: dedupedNodes, + connections: dedupedConnections + }; +} diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index 85d71784b55c7..96acfb7986c68 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.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 { chunk } from 'lodash'; import { PromiseReturnType } from '../../../typings/common'; import { Setup, @@ -19,48 +19,61 @@ import { SERVICE_NAME, SERVICE_FRAMEWORK_NAME } from '../../../common/elasticsearch_fieldnames'; +import { dedupeConnections } from './dedupe_connections'; export interface IEnvOptions { setup: Setup & SetupTimeRange & SetupUIFilters; serviceName?: string; environment?: string; - after?: string; } async function getConnectionData({ setup, serviceName, - environment, - after + environment }: IEnvOptions) { - const { traceIds, after: nextAfter } = await getTraceSampleIds({ + const { traceIds } = await getTraceSampleIds({ setup, serviceName, - environment, - after + environment }); - const serviceMapData = traceIds.length - ? await getServiceMapFromTraceIds({ + const chunks = chunk( + traceIds, + setup.config['xpack.apm.serviceMapMaxTracesPerRequest'] + ); + + const init = { + connections: [], + discoveredServices: [] + }; + + if (!traceIds.length) { + return init; + } + + const chunkedResponses = await Promise.all( + chunks.map(traceIdsChunk => + getServiceMapFromTraceIds({ setup, serviceName, environment, - traceIds + traceIds: traceIdsChunk }) - : { connections: [], discoveredServices: [] }; + ) + ); - return { - after: nextAfter, - ...serviceMapData - }; + return chunkedResponses.reduce((prev, current) => { + return { + connections: prev.connections.concat(current.connections), + discoveredServices: prev.discoveredServices.concat( + current.discoveredServices + ) + }; + }); } async function getServicesData(options: IEnvOptions) { - // only return services on the first request for the global service map - if (options.after) { - return []; - } - const { setup } = options; const projection = getServicesProjection({ setup }); @@ -125,15 +138,19 @@ async function getServicesData(options: IEnvOptions) { ); } +export type ConnectionsResponse = PromiseReturnType; +export type ServicesResponse = PromiseReturnType; + export type ServiceMapAPIResponse = PromiseReturnType; + export async function getServiceMap(options: IEnvOptions) { const [connectionData, servicesData] = await Promise.all([ getConnectionData(options), getServicesData(options) ]); - return { + return dedupeConnections({ ...connectionData, services: servicesData - }; + }); } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts index 463fe7f2cf640..f4e12df5d6a66 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_trace_sample_ids.ts @@ -15,27 +15,24 @@ import { PROCESSOR_EVENT, SERVICE_NAME, SERVICE_ENVIRONMENT, - SPAN_TYPE, - SPAN_SUBTYPE, + TRACE_ID, DESTINATION_ADDRESS, - TRACE_ID + SPAN_TYPE, + SPAN_SUBTYPE } from '../../../common/elasticsearch_fieldnames'; -const MAX_CONNECTIONS_PER_REQUEST = 1000; const MAX_TRACES_TO_INSPECT = 1000; export async function getTraceSampleIds({ - after, serviceName, environment, setup }: { - after?: string; serviceName?: string; environment?: string; setup: Setup & SetupTimeRange & SetupUIFilters; }) { - const { start, end, client, indices } = setup; + const { start, end, client, indices, config } = setup; const rangeQuery = { range: rangeFilter(start, end) }; @@ -65,9 +62,15 @@ export async function getTraceSampleIds({ query.bool.filter.push({ term: { [SERVICE_ENVIRONMENT]: environment } }); } - const afterObj = after - ? { after: JSON.parse(Buffer.from(after, 'base64').toString()) } - : {}; + const fingerprintBucketSize = serviceName + ? config['xpack.apm.serviceMapFingerprintBucketSize'] + : config['xpack.apm.serviceMapFingerprintGlobalBucketSize']; + + const traceIdBucketSize = serviceName + ? config['xpack.apm.serviceMapTraceIdBucketSize'] + : config['xpack.apm.serviceMapTraceIdGlobalBucketSize']; + + const samplerShardSize = traceIdBucketSize * 10; const params = { index: [indices['apm_oss.spanIndices']], @@ -77,42 +80,57 @@ export async function getTraceSampleIds({ aggs: { connections: { composite: { - size: MAX_CONNECTIONS_PER_REQUEST, - ...afterObj, sources: [ - { [SERVICE_NAME]: { terms: { field: SERVICE_NAME } } }, { - [SERVICE_ENVIRONMENT]: { - terms: { field: SERVICE_ENVIRONMENT, missing_bucket: true } + [DESTINATION_ADDRESS]: { + terms: { + field: DESTINATION_ADDRESS + } } }, { - [SPAN_TYPE]: { - terms: { field: SPAN_TYPE, missing_bucket: true } + [SERVICE_NAME]: { + terms: { + field: SERVICE_NAME + } } }, { - [SPAN_SUBTYPE]: { - terms: { field: SPAN_SUBTYPE, missing_bucket: true } + [SERVICE_ENVIRONMENT]: { + terms: { + field: SERVICE_ENVIRONMENT, + missing_bucket: true + } } }, { - [DESTINATION_ADDRESS]: { - terms: { field: DESTINATION_ADDRESS } + [SPAN_TYPE]: { + terms: { + field: SPAN_TYPE + } + } + }, + { + [SPAN_SUBTYPE]: { + terms: { + field: SPAN_SUBTYPE, + missing_bucket: true + } } } - ] + ], + size: fingerprintBucketSize }, aggs: { sample: { sampler: { - shard_size: 30 + shard_size: samplerShardSize }, aggs: { trace_ids: { terms: { field: TRACE_ID, - size: 10, + size: traceIdBucketSize, execution_hint: 'map' as const, // remove bias towards large traces by sorting on trace.id // which will be random-esque @@ -129,25 +147,9 @@ export async function getTraceSampleIds({ } }; - const tracesSampleResponse = await client.search< - { trace: { id: string } }, - typeof params - >(params); - - let nextAfter: string | undefined; - - const receivedAfterKey = - tracesSampleResponse.aggregations?.connections.after_key; - - if ( - receivedAfterKey && - (tracesSampleResponse.aggregations?.connections.buckets.length ?? 0) >= - MAX_CONNECTIONS_PER_REQUEST - ) { - nextAfter = Buffer.from(JSON.stringify(receivedAfterKey)).toString( - 'base64' - ); - } + const tracesSampleResponse = await client.search( + params + ); // make sure at least one trace per composite/connection bucket // is queried @@ -167,7 +169,6 @@ export async function getTraceSampleIds({ ); return { - after: nextAfter, traceIds }; } diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index bead0445d6ccc..a61a61e3ccaac 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -20,10 +20,12 @@ export const serviceMapRoute = createRoute(() => ({ path: '/api/apm/service-map', params: { query: t.intersection([ - t.partial({ environment: t.string, serviceName: t.string }), + t.partial({ + environment: t.string, + serviceName: t.string + }), uiFiltersRt, - rangeRt, - t.partial({ after: t.string }) + rangeRt ]) }, handler: async ({ context, request }) => { @@ -36,9 +38,9 @@ export const serviceMapRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { - query: { serviceName, environment, after } + query: { serviceName, environment } } = context.params; - return getServiceMap({ setup, serviceName, environment, after }); + return getServiceMap({ setup, serviceName, environment }); } })); From 4f1fdfbd5e3f9209e7c27f44e4dac5a21264a85f Mon Sep 17 00:00:00 2001 From: Melissa Alvarez Date: Thu, 19 Mar 2020 16:22:30 -0400 Subject: [PATCH 08/22] [ML] Data Visualizer: Replace KqlFilterBar with QueryStringInput (#60544) (#60670) * data visualizer:replace kqlFilterBar * remove unused translation * show syntax error toast --- x-pack/plugins/ml/public/application/app.tsx | 5 + .../content_types/number_content.tsx | 2 +- .../components/search_panel/search_panel.tsx | 126 +++++++++++------- .../datavisualizer/index_based/page.tsx | 10 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 6 files changed, 91 insertions(+), 54 deletions(-) diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index 2597715488399..6269c11fca896 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -9,6 +9,8 @@ import ReactDOM from 'react-dom'; import { AppMountParameters, CoreStart } from 'kibana/public'; +import { Storage } from '../../../../../src/plugins/kibana_utils/public'; + import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { setDependencyCache, clearCache } from './util/dependency_cache'; import { setLicenseCache } from './license'; @@ -24,6 +26,8 @@ interface AppProps { appMountParams: AppMountParameters; } +const localStorage = new Storage(window.localStorage); + const App: FC = ({ coreStart, deps, appMountParams }) => { setDependencyCache({ indexPatterns: deps.data.indexPatterns, @@ -62,6 +66,7 @@ const App: FC = ({ coreStart, deps, appMountParams }) => { appName: 'ML', data: deps.data, security: deps.security, + storage: localStorage, ...coreStart, }; diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx index 29be9d2e1e2a4..e2c156fc66ded 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/content_types/number_content.tsx @@ -157,7 +157,7 @@ export const NumberContent: FC = ({ config }) => { buttonSize="compressed" /> - {detailsMode === DETAILS_MODE.DISTRIBUTION && ( + {distribution && detailsMode === DETAILS_MODE.DISTRIBUTION && ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx index 527cd31ed91d4..50c76725f5245 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/search_panel/search_panel.tsx @@ -4,18 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC } from 'react'; +import React, { FC, useState } from 'react'; -import { - EuiFieldSearch, - EuiFlexItem, - EuiFlexGroup, - EuiForm, - EuiFormRow, - EuiIconTip, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiIconTip, EuiSuperSelect, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -23,18 +14,24 @@ import { i18n } from '@kbn/i18n'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search'; -import { SavedSearchQuery } from '../../../../contexts/ml'; -// @ts-ignore -import { KqlFilterBar } from '../../../../components/kql_filter_bar/index'; +import { + esKuery, + esQuery, + Query, + QueryStringInput, +} from '../../../../../../../../../src/plugins/data/public'; + +import { getToastNotifications } from '../../../../util/dependency_cache'; interface Props { indexPattern: IndexPattern; - searchString: string | SavedSearchQuery; - setSearchString(s: string): void; - searchQuery: string | SavedSearchQuery; - setSearchQuery(q: string | SavedSearchQuery): void; + searchString: Query['query']; + setSearchString(s: Query['query']): void; + searchQuery: Query['query']; + setSearchQuery(q: Query['query']): void; searchQueryLanguage: SEARCH_QUERY_LANGUAGE; + setSearchQueryLanguage(q: any): void; samplerShardSize: number; setSamplerShardSize(s: number): void; totalCount: number; @@ -59,6 +56,20 @@ const searchSizeOptions = [1000, 5000, 10000, 100000, -1].map(v => { }; }); +const kqlSyntaxErrorMessage = i18n.translate( + 'xpack.ml.datavisualizer.invalidKqlSyntaxErrorMessage', + { + defaultMessage: + 'Invalid syntax in search bar. The input must be valid Kibana Query Language (KQL)', + } +); +const luceneSyntaxErrorMessage = i18n.translate( + 'xpack.ml.datavisualizer.invalidLuceneSyntaxErrorMessage', + { + defaultMessage: 'Invalid syntax in search bar. The input must be valid Lucene', + } +); + export const SearchPanel: FC = ({ indexPattern, searchString, @@ -66,44 +77,65 @@ export const SearchPanel: FC = ({ searchQuery, setSearchQuery, searchQueryLanguage, + setSearchQueryLanguage, samplerShardSize, setSamplerShardSize, totalCount, }) => { - const searchHandler = (d: Record) => { - setSearchQuery(d.filterQuery); + // The internal state of the input query bar updated on every key stroke. + const [searchInput, setSearchInput] = useState({ + query: searchString || '', + language: searchQueryLanguage, + }); + + const searchHandler = (query: Query) => { + let filterQuery; + try { + if (query.language === SEARCH_QUERY_LANGUAGE.KUERY) { + filterQuery = esKuery.toElasticsearchQuery( + esKuery.fromKueryExpression(query.query), + indexPattern + ); + } else if (query.language === SEARCH_QUERY_LANGUAGE.LUCENE) { + filterQuery = esQuery.luceneStringToDsl(query.query); + } else { + filterQuery = {}; + } + + setSearchQuery(filterQuery); + setSearchString(query.query); + setSearchQueryLanguage(query.language); + } catch (e) { + console.log('Invalid syntax', e); // eslint-disable-line no-console + const toastNotifications = getToastNotifications(); + const notification = + query.language === SEARCH_QUERY_LANGUAGE.KUERY + ? kqlSyntaxErrorMessage + : luceneSyntaxErrorMessage; + toastNotifications.addDanger(notification); + } }; + const searchChangeHandler = (query: Query) => setSearchInput(query); return ( - {searchQueryLanguage === SEARCH_QUERY_LANGUAGE.KUERY ? ( - - ) : ( - - - - - - )} + diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index b66d12b6c9ebe..3a37274edbc16 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -24,6 +24,7 @@ import { import { IFieldType, KBN_FIELD_TYPES, + Query, esQuery, esKuery, } from '../../../../../../../src/plugins/data/public'; @@ -36,7 +37,7 @@ import { checkPermission } from '../../privilege/check_privilege'; import { mlNodesAvailable } from '../../ml_nodes_check/check_ml_nodes'; import { FullTimeRangeSelector } from '../../components/full_time_range_selector'; import { mlTimefilterRefresh$ } from '../../services/timefilter_refresh_service'; -import { useMlContext, SavedSearchQuery } from '../../contexts/ml'; +import { useMlContext } from '../../contexts/ml'; import { kbnTypeToMLJobType } from '../../util/field_types_utils'; import { useTimefilter } from '../../contexts/kibana'; import { timeBasedIndexCheck, getQueryFromSavedSearch } from '../../util/index_utils'; @@ -49,8 +50,8 @@ import { SearchPanel } from './components/search_panel'; import { DataLoader } from './data_loader'; interface DataVisualizerPageState { - searchQuery: string | SavedSearchQuery; - searchString: string | SavedSearchQuery; + searchQuery: Query['query']; + searchString: Query['query']; searchQueryLanguage: SEARCH_QUERY_LANGUAGE; samplerShardSize: number; overallStats: any; @@ -160,7 +161,7 @@ export const Page: FC = () => { const [searchString, setSearchString] = useState(initSearchString); const [searchQuery, setSearchQuery] = useState(initSearchQuery); - const [searchQueryLanguage] = useState(initQueryLanguage); + const [searchQueryLanguage, setSearchQueryLanguage] = useState(initQueryLanguage); const [samplerShardSize, setSamplerShardSize] = useState(defaults.samplerShardSize); // TODO - type overallStats and stats @@ -676,6 +677,7 @@ export const Page: FC = () => { searchQuery={searchQuery} setSearchQuery={setSearchQuery} searchQueryLanguage={searchQueryLanguage} + setSearchQueryLanguage={setSearchQueryLanguage} samplerShardSize={samplerShardSize} setSamplerShardSize={setSamplerShardSize} totalCount={overallStats.totalCount} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1206fda3f75d0..01665ac27635c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7691,7 +7691,6 @@ "xpack.ml.datavisualizer.page.fieldsPanelTitle": "フィールド", "xpack.ml.datavisualizer.page.metricsPanelTitle": "メトリック", "xpack.ml.datavisualizer.searchPanel.allOptionLabel": "すべて", - "xpack.ml.datavisualizer.searchPanel.kqlEditOnlyLabel": "現在 KQAL で保存された検索のみ編集できます。", "xpack.ml.datavisualizer.searchPanel.queryBarPlaceholder": "小さいサンプルサイズを選択することで、クエリの実行時間を短縮しクラスターへの負荷を軽減できます。", "xpack.ml.datavisualizer.searchPanel.queryBarPlaceholderText": "検索… (例: status:200 AND extension:\"PHP\")", "xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel": "サンプリングするドキュメント数を選択してください", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index e13c80e0044d0..b587dd4fb28bf 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7691,7 +7691,6 @@ "xpack.ml.datavisualizer.page.fieldsPanelTitle": "字段", "xpack.ml.datavisualizer.page.metricsPanelTitle": "指标", "xpack.ml.datavisualizer.searchPanel.allOptionLabel": "全部", - "xpack.ml.datavisualizer.searchPanel.kqlEditOnlyLabel": "当前仅可以编辑 KQL 已保存搜索", "xpack.ml.datavisualizer.searchPanel.queryBarPlaceholder": "选择较小的样例大小将减少查询运行时间和集群上的负载。", "xpack.ml.datavisualizer.searchPanel.queryBarPlaceholderText": "搜索……(例如,status:200 AND extension:\"PHP\")", "xpack.ml.datavisualizer.searchPanel.sampleSizeAriaLabel": "选择要采样的文档数目", From be64c88789434f7213c6502a52270b2b5ca1bd23 Mon Sep 17 00:00:00 2001 From: Steph Milovic Date: Thu, 19 Mar 2020 15:38:27 -0600 Subject: [PATCH 09/22] [SIEM] [Cases] Case closed and add user email (#60463) (#60650) --- .../siem/public/containers/case/types.ts | 7 +- .../public/containers/case/use_get_case.tsx | 2 + .../containers/case/use_update_case.tsx | 4 +- .../components/all_cases/__mock__/index.tsx | 14 +- .../case/components/all_cases/columns.tsx | 50 +++-- .../pages/case/components/all_cases/index.tsx | 18 +- .../case/components/all_cases/translations.ts | 6 - .../case/components/case_status/index.tsx | 105 +++++++++++ .../components/case_view/__mock__/index.tsx | 50 ++--- .../components/case_view/actions.test.tsx | 65 +++++++ .../case/components/case_view/actions.tsx | 75 ++++++++ .../case/components/case_view/index.test.tsx | 95 ++++------ .../pages/case/components/case_view/index.tsx | 171 +++++------------- .../case/components/case_view/translations.ts | 16 ++ .../case/components/user_list/index.test.tsx | 40 ++++ .../pages/case/components/user_list/index.tsx | 28 ++- .../siem/public/pages/case/translations.ts | 10 + x-pack/plugins/case/common/api/cases/case.ts | 2 + x-pack/plugins/case/common/api/user.ts | 1 + .../routes/api/__fixtures__/authc_mock.ts | 6 +- .../api/__fixtures__/mock_saved_objects.ts | 50 +++++ .../api/cases/comments/patch_comment.ts | 4 +- .../api/cases/configure/patch_configure.ts | 4 +- .../api/cases/configure/post_configure.ts | 4 +- .../routes/api/cases/find_cases.test.ts | 2 +- .../routes/api/cases/patch_cases.test.ts | 50 ++++- .../server/routes/api/cases/patch_cases.ts | 30 ++- .../plugins/case/server/routes/api/types.ts | 2 +- .../plugins/case/server/routes/api/utils.ts | 20 +- .../case/server/saved_object_types/cases.ts | 22 +++ .../server/saved_object_types/comments.ts | 6 + 31 files changed, 692 insertions(+), 267 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index 65d94865bf00c..5b6ff8438be8c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -18,6 +18,8 @@ export interface Comment { export interface Case { id: string; + closedAt: string | null; + closedBy: ElasticUser | null; comments: Comment[]; commentIds: string[]; createdAt: string; @@ -59,12 +61,13 @@ export interface AllCases extends CasesStatus { export enum SortFieldCase { createdAt = 'createdAt', - updatedAt = 'updatedAt', + closedAt = 'closedAt', } export interface ElasticUser { - readonly username: string; + readonly email?: string | null; readonly fullName?: string | null; + readonly username: string; } export interface FetchCasesProps { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index a179b6f546b9b..b70195e2c126f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -49,6 +49,8 @@ const dataFetchReducer = (state: CaseState, action: Action): CaseState => { }; const initialData: Case = { id: '', + closedAt: null, + closedBy: null, createdAt: '', comments: [], commentIds: [], diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index afcbe20fa791a..987620469901b 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -5,7 +5,7 @@ */ import { useReducer, useCallback } from 'react'; - +import { cloneDeep } from 'lodash/fp'; import { CaseRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; @@ -47,7 +47,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => ...state, isLoading: false, isError: false, - caseData: action.payload, + caseData: cloneDeep(action.payload), updateKey: null, }; case 'FETCH_FAILURE': diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 0fe8daafcb30a..5d00b770b3ca9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -13,6 +13,8 @@ export const useGetCasesMockState: UseGetCasesState = { countOpenCases: 0, cases: [ { + closedAt: null, + closedBy: null, id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:23.627Z', createdBy: { username: 'elastic' }, @@ -27,6 +29,8 @@ export const useGetCasesMockState: UseGetCasesState = { version: 'WzQ3LDFd', }, { + closedAt: null, + closedBy: null, id: '362a5c10-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:13.328Z', createdBy: { username: 'elastic' }, @@ -41,6 +45,8 @@ export const useGetCasesMockState: UseGetCasesState = { version: 'WzQ3LDFd', }, { + closedAt: null, + closedBy: null, id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:11.328Z', createdBy: { username: 'elastic' }, @@ -55,6 +61,8 @@ export const useGetCasesMockState: UseGetCasesState = { version: 'WzQ3LDFd', }, { + closedAt: '2020-02-13T19:44:13.328Z', + closedBy: { username: 'elastic' }, id: '31890e90-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:05.563Z', createdBy: { username: 'elastic' }, @@ -64,11 +72,13 @@ export const useGetCasesMockState: UseGetCasesState = { status: 'closed', tags: ['phishing'], title: 'Uh oh', - updatedAt: null, - updatedBy: null, + updatedAt: '2020-02-13T19:44:13.328Z', + updatedBy: { username: 'elastic' }, version: 'WzQ3LDFd', }, { + closedAt: null, + closedBy: null, id: '2f5b3210-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:01.901Z', createdBy: { username: 'elastic' }, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index 5859e6bbce263..b9e1113c486ad 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -36,7 +36,8 @@ const Spacer = styled.span` const renderStringField = (field: string, dataTestSubj: string) => field != null ? {field} : getEmptyTagValue(); export const getCasesColumns = ( - actions: Array> + actions: Array>, + filterStatus: string ): CasesColumns[] => [ { name: i18n.NAME, @@ -113,22 +114,39 @@ export const getCasesColumns = ( render: (comments: Case['commentIds']) => renderStringField(`${comments.length}`, `case-table-column-commentCount`), }, - { - field: 'createdAt', - name: i18n.OPENED_ON, - sortable: true, - render: (createdAt: Case['createdAt']) => { - if (createdAt != null) { - return ( - - ); + filterStatus === 'open' + ? { + field: 'createdAt', + name: i18n.OPENED_ON, + sortable: true, + render: (createdAt: Case['createdAt']) => { + if (createdAt != null) { + return ( + + ); + } + return getEmptyTagValue(); + }, } - return getEmptyTagValue(); - }, - }, + : { + field: 'closedAt', + name: i18n.CLOSED_ON, + sortable: true, + render: (closedAt: Case['closedAt']) => { + if (closedAt != null) { + return ( + + ); + } + return getEmptyTagValue(); + }, + }, { name: 'Actions', actions, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 9f836bd043c9d..9a84dd07b0af4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -71,8 +71,8 @@ const ProgressLoader = styled(EuiProgress)` const getSortField = (field: string): SortFieldCase => { if (field === SortFieldCase.createdAt) { return SortFieldCase.createdAt; - } else if (field === SortFieldCase.updatedAt) { - return SortFieldCase.updatedAt; + } else if (field === SortFieldCase.closedAt) { + return SortFieldCase.closedAt; } return SortFieldCase.createdAt; }; @@ -206,17 +206,25 @@ export const AllCases = React.memo(() => { } setQueryParams(newQueryParams); }, - [setQueryParams, queryParams] + [queryParams] ); const onFilterChangedCallback = useCallback( (newFilterOptions: Partial) => { + if (newFilterOptions.status && newFilterOptions.status === 'closed') { + setQueryParams({ ...queryParams, sortField: SortFieldCase.closedAt }); + } else if (newFilterOptions.status && newFilterOptions.status === 'open') { + setQueryParams({ ...queryParams, sortField: SortFieldCase.createdAt }); + } setFilters({ ...filterOptions, ...newFilterOptions }); }, - [filterOptions, setFilters] + [filterOptions, queryParams] ); - const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [actions]); + const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions, filterOptions.status), [ + actions, + filterOptions.status, + ]); const memoizedPagination = useMemo( () => ({ pageIndex: queryParams.page - 1, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index 27532e57166e1..8f79b78ef7568 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -60,9 +60,3 @@ export const CLOSED = i18n.translate('xpack.siem.case.caseTable.closed', { export const DELETE = i18n.translate('xpack.siem.case.caseTable.delete', { defaultMessage: 'Delete', }); -export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase', { - defaultMessage: 'Reopen case', -}); -export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', { - defaultMessage: 'Close case', -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx new file mode 100644 index 0000000000000..9dbd71ea3e34c --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_status/index.tsx @@ -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 React, { useCallback } from 'react'; +import styled, { css } from 'styled-components'; +import { + EuiBadge, + EuiButtonToggle, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import * as i18n from '../case_view/translations'; +import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; +import { CaseViewActions } from '../case_view/actions'; + +const MyDescriptionList = styled(EuiDescriptionList)` + ${({ theme }) => css` + & { + padding-right: ${theme.eui.euiSizeL}; + border-right: ${theme.eui.euiBorderThin}; + } + `} +`; + +interface CaseStatusProps { + 'data-test-subj': string; + badgeColor: string; + buttonLabel: string; + caseId: string; + caseTitle: string; + icon: string; + isLoading: boolean; + isSelected: boolean; + status: string; + title: string; + toggleStatusCase: (status: string) => void; + value: string | null; +} +const CaseStatusComp: React.FC = ({ + 'data-test-subj': dataTestSubj, + badgeColor, + buttonLabel, + caseId, + caseTitle, + icon, + isLoading, + isSelected, + status, + title, + toggleStatusCase, + value, +}) => { + const onChange = useCallback(e => toggleStatusCase(e.target.checked ? 'closed' : 'open'), [ + toggleStatusCase, + ]); + return ( + + + + + + {i18n.STATUS} + + + {status} + + + + + {title} + + + + + + + + + + + + + + + + + + + ); +}; + +export const CaseStatus = React.memo(CaseStatusComp); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx index 53cc1f80b5c10..e11441eac3a9d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -10,6 +10,8 @@ import { Case } from '../../../../../containers/case/types'; export const caseProps: CaseProps = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', initialData: { + closedAt: null, + closedBy: null, id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'], comments: [ @@ -20,6 +22,7 @@ export const caseProps: CaseProps = { createdBy: { fullName: 'Steph Milovic', username: 'smilovic', + email: 'notmyrealemailfool@elastic.co', }, updatedAt: '2020-02-20T23:06:33.798Z', updatedBy: { @@ -29,7 +32,7 @@ export const caseProps: CaseProps = { }, ], createdAt: '2020-02-13T19:44:23.627Z', - createdBy: { fullName: null, username: 'elastic' }, + createdBy: { fullName: null, email: 'testemail@elastic.co', username: 'elastic' }, description: 'Security banana Issue', status: 'open', tags: ['defacement'], @@ -41,35 +44,22 @@ export const caseProps: CaseProps = { version: 'WzQ3LDFd', }, }; - -export const data: Case = { - id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', - commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'], - comments: [ - { - comment: 'Solve this fast!', - id: 'a357c6a0-5435-11ea-b427-fb51a1fcb7b8', - createdAt: '2020-02-20T23:06:33.798Z', - createdBy: { - fullName: 'Steph Milovic', - username: 'smilovic', - }, - updatedAt: '2020-02-20T23:06:33.798Z', - updatedBy: { - username: 'elastic', - }, - version: 'WzQ3LDFd', +export const caseClosedProps: CaseProps = { + ...caseProps, + initialData: { + ...caseProps.initialData, + closedAt: '2020-02-20T23:06:33.798Z', + closedBy: { + username: 'elastic', }, - ], - createdAt: '2020-02-13T19:44:23.627Z', - createdBy: { username: 'elastic', fullName: null }, - description: 'Security banana Issue', - status: 'open', - tags: ['defacement'], - title: 'Another horrible breach!!', - updatedAt: '2020-02-19T15:02:57.995Z', - updatedBy: { - username: 'elastic', + status: 'closed', }, - version: 'WzQ3LDFd', +}; + +export const data: Case = { + ...caseProps.initialData, +}; + +export const dataClosed: Case = { + ...caseClosedProps.initialData, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx new file mode 100644 index 0000000000000..4e1e5ba753c36 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { CaseViewActions } from './actions'; +import { TestProviders } from '../../../../mock'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; +jest.mock('../../../../containers/case/use_delete_cases'); +const useDeleteCasesMock = useDeleteCases as jest.Mock; + +describe('CaseView actions', () => { + const caseTitle = 'Cool title'; + const caseId = 'cool-id'; + const handleOnDeleteConfirm = jest.fn(); + const handleToggleModal = jest.fn(); + const dispatchResetIsDeleted = jest.fn(); + const defaultDeleteState = { + dispatchResetIsDeleted, + handleToggleModal, + handleOnDeleteConfirm, + isLoading: false, + isError: false, + isDeleted: false, + isDisplayConfirmDeleteModal: false, + }; + beforeEach(() => { + jest.resetAllMocks(); + useDeleteCasesMock.mockImplementation(() => defaultDeleteState); + }); + it('clicking trash toggles modal', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); + + wrapper + .find('button[data-test-subj="property-actions-ellipses"]') + .first() + .simulate('click'); + wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click'); + expect(handleToggleModal).toHaveBeenCalled(); + }); + it('toggle delete modal and confirm', () => { + useDeleteCasesMock.mockImplementation(() => ({ + ...defaultDeleteState, + isDisplayConfirmDeleteModal: true, + })); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); + wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([caseId]); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx new file mode 100644 index 0000000000000..88a717ac5fa6a --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/actions.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; + +import { Redirect } from 'react-router-dom'; +import * as i18n from './translations'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; +import { SiemPageName } from '../../../home/types'; +import { PropertyActions } from '../property_actions'; + +interface CaseViewActions { + caseId: string; + caseTitle: string; +} + +const CaseViewActionsComponent: React.FC = ({ caseId, caseTitle }) => { + // Delete case + const { + handleToggleModal, + handleOnDeleteConfirm, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + + const confirmDeleteModal = useMemo( + () => ( + + ), + [isDisplayConfirmDeleteModal] + ); + // TO DO refactor each of these const's into their own components + const propertyActions = useMemo( + () => [ + { + iconType: 'trash', + label: i18n.DELETE_CASE, + onClick: handleToggleModal, + }, + { + iconType: 'popout', + label: 'View ServiceNow incident', + onClick: () => null, + }, + { + iconType: 'importAction', + label: 'Update ServiceNow incident', + onClick: () => null, + }, + ], + [handleToggleModal] + ); + + if (isDeleted) { + return ; + } + return ( + <> + + {confirmDeleteModal} + + ); +}; + +export const CaseViewActions = React.memo(CaseViewActionsComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx index 15d6cf7cf7317..ec18bdb2bf9ab 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.test.tsx @@ -7,15 +7,13 @@ import React from 'react'; import { mount } from 'enzyme'; import { CaseComponent } from './'; -import * as updateHook from '../../../../containers/case/use_update_case'; -import * as deleteHook from '../../../../containers/case/use_delete_cases'; -import { caseProps, data } from './__mock__'; +import { caseProps, caseClosedProps, data, dataClosed } from './__mock__'; import { TestProviders } from '../../../../mock'; +import { useUpdateCase } from '../../../../containers/case/use_update_case'; +jest.mock('../../../../containers/case/use_update_case'); +const useUpdateCaseMock = useUpdateCase as jest.Mock; describe('CaseView ', () => { - const handleOnDeleteConfirm = jest.fn(); - const handleToggleModal = jest.fn(); - const dispatchResetIsDeleted = jest.fn(); const updateCaseProperty = jest.fn(); /* eslint-disable no-console */ // Silence until enzyme fixed to use ReactTestUtils.act() @@ -28,15 +26,17 @@ describe('CaseView ', () => { }); /* eslint-enable no-console */ + const defaultUpdateCaseState = { + caseData: data, + isLoading: false, + isError: false, + updateKey: null, + updateCaseProperty, + }; + beforeEach(() => { jest.resetAllMocks(); - jest.spyOn(updateHook, 'useUpdateCase').mockReturnValue({ - caseData: data, - isLoading: false, - isError: false, - updateKey: null, - updateCaseProperty, - }); + useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState); }); it('should render CaseComponent', () => { @@ -69,6 +69,7 @@ describe('CaseView ', () => { .first() .text() ).toEqual(data.createdBy.username); + expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false); expect( wrapper .find(`[data-test-subj="case-view-createdAt"]`) @@ -82,6 +83,30 @@ describe('CaseView ', () => { .prop('raw') ).toEqual(data.description); }); + it('should show closed indicators in header when case is closed', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + caseData: dataClosed, + })); + const wrapper = mount( + + + + ); + expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false); + expect( + wrapper + .find(`[data-test-subj="case-view-closedAt"]`) + .first() + .prop('value') + ).toEqual(dataClosed.closedAt); + expect( + wrapper + .find(`[data-test-subj="case-view-status"]`) + .first() + .text() + ).toEqual(dataClosed.status); + }); it('should dispatch update state when button is toggled', () => { const wrapper = mount( @@ -92,7 +117,7 @@ describe('CaseView ', () => { wrapper .find('input[data-test-subj="toggle-case-status"]') - .simulate('change', { target: { value: false } }); + .simulate('change', { target: { checked: true } }); expect(updateCaseProperty).toBeCalledWith({ updateKey: 'status', @@ -133,46 +158,4 @@ describe('CaseView ', () => { .prop('source') ).toEqual(data.comments[0].comment); }); - - it('toggle delete modal and cancel', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); - - wrapper - .find( - '[data-test-subj="case-view-actions"] button[data-test-subj="property-actions-ellipses"]' - ) - .first() - .simulate('click'); - wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click'); - expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); - wrapper.find('button[data-test-subj="confirmModalCancelButton"]').simulate('click'); - expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); - }); - - it('toggle delete modal and confirm', () => { - jest.spyOn(deleteHook, 'useDeleteCases').mockReturnValue({ - dispatchResetIsDeleted, - handleToggleModal, - handleOnDeleteConfirm, - isLoading: false, - isError: false, - isDeleted: false, - isDisplayConfirmDeleteModal: true, - }); - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); - wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([caseProps.caseId]); - }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 82216e88a091e..dce7bde2225c9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -5,26 +5,14 @@ */ import React, { useCallback, useMemo } from 'react'; -import { - EuiBadge, - EuiButtonToggle, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import styled, { css } from 'styled-components'; -import { Redirect } from 'react-router-dom'; +import styled from 'styled-components'; import * as i18n from './translations'; import { Case } from '../../../../containers/case/types'; -import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; import { getCaseUrl } from '../../../../components/link_to'; import { HeaderPage } from '../../../../components/header_page'; import { EditableTitle } from '../../../../components/header_page/editable_title'; -import { PropertyActions } from '../property_actions'; import { TagList } from '../tag_list'; import { useGetCase } from '../../../../containers/case/use_get_case'; import { UserActionTree } from '../user_action_tree'; @@ -33,23 +21,13 @@ import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { WrapperPage } from '../../../../components/wrapper_page'; import { getTypedPayload } from '../../../../containers/case/utils'; import { WhitePageWrapper } from '../wrappers'; -import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; -import { SiemPageName } from '../../../home/types'; -import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; +import { useBasePath } from '../../../../lib/kibana'; +import { CaseStatus } from '../case_status'; interface Props { caseId: string; } -const MyDescriptionList = styled(EuiDescriptionList)` - ${({ theme }) => css` - & { - padding-right: ${theme.eui.euiSizeL}; - border-right: ${theme.eui.euiBorderThin}; - } - `} -`; - const MyWrapper = styled(WrapperPage)` padding-bottom: 0; `; @@ -64,6 +42,8 @@ export interface CaseProps { } export const CaseComponent = React.memo(({ caseId, initialData }) => { + const basePath = window.location.origin + useBasePath(); + const caseLink = `${basePath}/app/siem#/case/${caseId}`; const { caseData, isLoading, updateKey, updateCaseProperty } = useUpdateCase(caseId, initialData); // Update Fields @@ -107,58 +87,44 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => return null; } }, - [updateCaseProperty, caseData.status] - ); - const toggleStatusCase = useCallback( - e => onUpdateField('status', e.target.checked ? 'open' : 'closed'), - [onUpdateField] + [caseData.status] ); - const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); - - // Delete case - const { - handleToggleModal, - handleOnDeleteConfirm, - isDeleted, - isDisplayConfirmDeleteModal, - } = useDeleteCases(); - - const confirmDeleteModal = useMemo( - () => ( - - ), - [isDisplayConfirmDeleteModal] + const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); + const toggleStatusCase = useCallback(status => onUpdateField('status', status), [onUpdateField]); + + const caseStatusData = useMemo( + () => + caseData.status === 'open' + ? { + 'data-test-subj': 'case-view-createdAt', + value: caseData.createdAt, + title: i18n.CASE_OPENED, + buttonLabel: i18n.CLOSE_CASE, + status: caseData.status, + icon: 'checkInCircleFilled', + badgeColor: 'secondary', + isSelected: false, + } + : { + 'data-test-subj': 'case-view-closedAt', + value: caseData.closedAt, + title: i18n.CASE_CLOSED, + buttonLabel: i18n.REOPEN_CASE, + status: caseData.status, + icon: 'magnet', + badgeColor: 'danger', + isSelected: true, + }, + [caseData.closedAt, caseData.createdAt, caseData.status] + ); + const emailContent = useMemo( + () => ({ + subject: i18n.EMAIL_SUBJECT(caseData.title), + body: i18n.EMAIL_BODY(caseLink), + }), + [caseData.title] ); - // TO DO refactor each of these const's into their own components - const propertyActions = [ - { - iconType: 'trash', - label: 'Delete case', - onClick: handleToggleModal, - }, - { - iconType: 'popout', - label: 'View ServiceNow incident', - onClick: () => null, - }, - { - iconType: 'importAction', - label: 'Update ServiceNow incident', - onClick: () => null, - }, - ]; - - if (isDeleted) { - return ; - } - return ( <> @@ -177,51 +143,13 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => } title={caseData.title} > - - - - - - {i18n.STATUS} - - - {caseData.status} - - - - - {i18n.CASE_OPENED} - - - - - - - - - - - - - - - - - - + @@ -237,6 +165,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => @@ -250,7 +179,6 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => - {confirmDeleteModal} ); }); @@ -273,4 +201,5 @@ export const CaseView = React.memo(({ caseId }: Props) => { return ; }); +CaseComponent.displayName = 'CaseComponent'; CaseView.displayName = 'CaseView'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts index 82b5e771e2151..e5fa3bff51f85 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/translations.ts @@ -55,3 +55,19 @@ export const STATUS = i18n.translate('xpack.siem.case.caseView.statusLabel', { export const CASE_OPENED = i18n.translate('xpack.siem.case.caseView.caseOpened', { defaultMessage: 'Case opened', }); + +export const CASE_CLOSED = i18n.translate('xpack.siem.case.caseView.caseClosed', { + defaultMessage: 'Case closed', +}); + +export const EMAIL_SUBJECT = (caseTitle: string) => + i18n.translate('xpack.siem.case.caseView.emailSubject', { + values: { caseTitle }, + defaultMessage: 'SIEM Case - {caseTitle}', + }); + +export const EMAIL_BODY = (caseUrl: string) => + i18n.translate('xpack.siem.case.caseView.emailBody', { + values: { caseUrl }, + defaultMessage: 'Case reference: {caseUrl}', + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx new file mode 100644 index 0000000000000..51acb3b810d92 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { UserList } from './'; +import * as i18n from '../case_view/translations'; + +describe('UserList ', () => { + const title = 'Case Title'; + const caseLink = 'http://reddit.com'; + const user = { username: 'username', fullName: 'Full Name', email: 'testemail@elastic.co' }; + const open = jest.fn(); + beforeAll(() => { + window.open = open; + }); + beforeEach(() => { + jest.resetAllMocks(); + }); + it('triggers mailto when email icon clicked', () => { + const wrapper = shallow( + + ); + wrapper.find('[data-test-subj="user-list-email-button"]').simulate('click'); + expect(open).toBeCalledWith( + `mailto:${user.email}?subject=${i18n.EMAIL_SUBJECT(title)}&body=${i18n.EMAIL_BODY(caseLink)}`, + '_blank' + ); + }); +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx index abb49122dc142..74a1b98c29eef 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_list/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiButtonIcon, EuiText, @@ -17,6 +17,10 @@ import styled, { css } from 'styled-components'; import { ElasticUser } from '../../../../containers/case/types'; interface UserListProps { + email: { + subject: string; + body: string; + }; headline: string; users: ElasticUser[]; } @@ -31,8 +35,11 @@ const MyFlexGroup = styled(EuiFlexGroup)` `} `; -const renderUsers = (users: ElasticUser[]) => { - return users.map(({ fullName, username }, key) => ( +const renderUsers = ( + users: ElasticUser[], + handleSendEmail: (emailAddress: string | undefined | null) => void +) => { + return users.map(({ fullName, username, email }, key) => ( @@ -50,7 +57,8 @@ const renderUsers = (users: ElasticUser[]) => { {}} // TO DO + data-test-subj="user-list-email-button" + onClick={handleSendEmail.bind(null, email)} // TO DO iconType="email" aria-label="email" /> @@ -59,12 +67,20 @@ const renderUsers = (users: ElasticUser[]) => { )); }; -export const UserList = React.memo(({ headline, users }: UserListProps) => { +export const UserList = React.memo(({ email, headline, users }: UserListProps) => { + const handleSendEmail = useCallback( + (emailAddress: string | undefined | null) => { + if (emailAddress && emailAddress != null) { + window.open(`mailto:${emailAddress}?subject=${email.subject}&body=${email.body}`, '_blank'); + } + }, + [email.subject] + ); return (

{headline}

- {renderUsers(users)} + {renderUsers(users, handleSendEmail)}
); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index 6ef412d408ae5..341a34240fe49 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -30,6 +30,16 @@ export const OPENED_ON = i18n.translate('xpack.siem.case.caseView.openedOn', { defaultMessage: 'Opened on', }); +export const CLOSED_ON = i18n.translate('xpack.siem.case.caseView.closedOn', { + defaultMessage: 'Closed on', +}); +export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase', { + defaultMessage: 'Reopen case', +}); +export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', { + defaultMessage: 'Close case', +}); + export const REPORTER = i18n.translate('xpack.siem.case.caseView.createdBy', { defaultMessage: 'Reporter', }); diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index 68a222cb656ed..6f58e2702ec5b 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -24,6 +24,8 @@ export const CaseAttributesRt = rt.intersection([ CaseBasicRt, rt.type({ comment_ids: rt.array(rt.string), + closed_at: rt.union([rt.string, rt.null]), + closed_by: rt.union([UserRT, rt.null]), created_at: rt.string, created_by: UserRT, updated_at: rt.union([rt.string, rt.null]), diff --git a/x-pack/plugins/case/common/api/user.ts b/x-pack/plugins/case/common/api/user.ts index ed44791c4e04d..651cd08f08a02 100644 --- a/x-pack/plugins/case/common/api/user.ts +++ b/x-pack/plugins/case/common/api/user.ts @@ -7,6 +7,7 @@ import * as rt from 'io-ts'; export const UserRT = rt.type({ + email: rt.union([rt.undefined, rt.string]), full_name: rt.union([rt.undefined, rt.string]), username: rt.string, }); diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts index 17a2518482637..c08dae1dc18b4 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/authc_mock.ts @@ -13,7 +13,11 @@ function createAuthenticationMock({ authc.getCurrentUser.mockReturnValue( currentUser !== undefined ? currentUser - : ({ username: 'awesome', full_name: 'Awesome D00d' } as AuthenticatedUser) + : ({ + email: 'd00d@awesome.com', + username: 'awesome', + full_name: 'Awesome D00d', + } as AuthenticatedUser) ); return authc; } diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 1e1965f83ff68..5aa8b93f17b08 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -12,10 +12,13 @@ export const mockCases: Array> = [ type: 'cases', id: 'mock-id-1', attributes: { + closed_at: null, + closed_by: null, comment_ids: ['mock-comment-1'], created_at: '2019-11-25T21:54:48.952Z', created_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, description: 'This is a brand new case of a bad meanie defacing data', @@ -25,6 +28,7 @@ export const mockCases: Array> = [ updated_at: '2019-11-25T21:54:48.952Z', updated_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, }, @@ -36,10 +40,13 @@ export const mockCases: Array> = [ type: 'cases', id: 'mock-id-2', attributes: { + closed_at: null, + closed_by: null, comment_ids: [], created_at: '2019-11-25T22:32:00.900Z', created_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, description: 'Oh no, a bad meanie destroying data!', @@ -49,6 +56,7 @@ export const mockCases: Array> = [ updated_at: '2019-11-25T22:32:00.900Z', updated_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, }, @@ -60,10 +68,13 @@ export const mockCases: Array> = [ type: 'cases', id: 'mock-id-3', attributes: { + closed_at: null, + closed_by: null, comment_ids: [], created_at: '2019-11-25T22:32:17.947Z', created_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, description: 'Oh no, a bad meanie going LOLBins all over the place!', @@ -73,6 +84,39 @@ export const mockCases: Array> = [ updated_at: '2019-11-25T22:32:17.947Z', updated_by: { full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + }, + references: [], + updated_at: '2019-11-25T22:32:17.947Z', + version: 'WzUsMV0=', + }, + { + type: 'cases', + id: 'mock-id-4', + attributes: { + closed_at: '2019-11-25T22:32:17.947Z', + closed_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + comment_ids: [], + created_at: '2019-11-25T22:32:17.947Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + description: 'Oh no, a bad meanie going LOLBins all over the place!', + title: 'Another bad one', + status: 'closed', + tags: ['LOLBins'], + updated_at: '2019-11-25T22:32:17.947Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, }, @@ -100,11 +144,13 @@ export const mockCaseComments: Array> = [ created_at: '2019-11-25T21:55:00.177Z', created_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, updated_at: '2019-11-25T21:55:00.177Z', updated_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, }, @@ -126,11 +172,13 @@ export const mockCaseComments: Array> = [ created_at: '2019-11-25T21:55:14.633Z', created_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, updated_at: '2019-11-25T21:55:14.633Z', updated_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, }, @@ -153,11 +201,13 @@ export const mockCaseComments: Array> = [ created_at: '2019-11-25T22:32:30.608Z', created_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, updated_at: '2019-11-25T22:32:30.608Z', updated_by: { full_name: 'elastic', + email: 'testemail@elastic.co', username: 'elastic', }, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts index 0166ba89eb76c..c14a94e84e51c 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.ts @@ -56,14 +56,14 @@ export function initPatchCommentApi({ caseService, router }: RouteDeps) { } const updatedBy = await caseService.getUser({ request, response }); - const { full_name, username } = updatedBy; + const { email, full_name, username } = updatedBy; const updatedComment = await caseService.patchComment({ client: context.core.savedObjects.client, commentId: query.id, updatedAttributes: { comment: query.comment, updated_at: new Date().toISOString(), - updated_by: { full_name, username }, + updated_by: { email, full_name, username }, }, version: query.version, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index 1da1161ab01d1..1542394fc438d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -49,7 +49,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout } const updatedBy = await caseService.getUser({ request, response }); - const { full_name, username } = updatedBy; + const { email, full_name, username } = updatedBy; const updateDate = new Date().toISOString(); const patch = await caseConfigureService.patch({ @@ -58,7 +58,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout updatedAttributes: { ...queryWithoutVersion, updated_at: updateDate, - updated_by: { full_name, username }, + updated_by: { email, full_name, username }, }, }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index a22dd8437e508..c839d36dcf4df 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -43,7 +43,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route ); } const updatedBy = await caseService.getUser({ request, response }); - const { full_name, username } = updatedBy; + const { email, full_name, username } = updatedBy; const creationDate = new Date().toISOString(); const post = await caseConfigureService.post({ @@ -51,7 +51,7 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route attributes: { ...query, created_at: creationDate, - created_by: { full_name, username }, + created_by: { email, full_name, username }, updated_at: null, updated_by: null, }, diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts index 7ce37d2569e57..8fafb1af0eb82 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts @@ -34,6 +34,6 @@ describe('GET all cases', () => { const response = await routeHandler(theContext, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.cases).toHaveLength(3); + expect(response.payload.cases).toHaveLength(4); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts index 7ab7212d2f436..19ff7f0734a77 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts @@ -25,7 +25,7 @@ describe('PATCH cases', () => { toISOString: jest.fn().mockReturnValue('2019-11-25T21:54:48.952Z'), })); }); - it(`Patch a case`, async () => { + it(`Close a case`, async () => { const request = httpServerMock.createKibanaRequest({ path: '/api/cases', method: 'patch', @@ -50,17 +50,61 @@ describe('PATCH cases', () => { expect(response.status).toEqual(200); expect(response.payload).toEqual([ { + closed_at: '2019-11-25T21:54:48.952Z', + closed_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, comment_ids: ['mock-comment-1'], comments: [], created_at: '2019-11-25T21:54:48.952Z', - created_by: { full_name: 'elastic', username: 'elastic' }, + created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, description: 'This is a brand new case of a bad meanie defacing data', id: 'mock-id-1', status: 'closed', tags: ['defacement'], title: 'Super Bad Security Issue', updated_at: '2019-11-25T21:54:48.952Z', - updated_by: { full_name: 'Awesome D00d', username: 'awesome' }, + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, + version: 'WzE3LDFd', + }, + ]); + }); + it(`Open a case`, async () => { + const request = httpServerMock.createKibanaRequest({ + path: '/api/cases', + method: 'patch', + body: { + cases: [ + { + id: 'mock-id-4', + status: 'open', + version: 'WzUsMV0=', + }, + ], + }, + }); + + const theContext = createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + }) + ); + + const response = await routeHandler(theContext, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload).toEqual([ + { + closed_at: null, + closed_by: null, + comment_ids: [], + comments: [], + created_at: '2019-11-25T22:32:17.947Z', + created_by: { email: 'testemail@elastic.co', full_name: 'elastic', username: 'elastic' }, + description: 'Oh no, a bad meanie going LOLBins all over the place!', + id: 'mock-id-4', + status: 'open', + tags: ['LOLBins'], + title: 'Another bad one', + updated_at: '2019-11-25T21:54:48.952Z', + updated_by: { email: 'd00d@awesome.com', full_name: 'Awesome D00d', username: 'awesome' }, version: 'WzE3LDFd', }, ]); diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts index 3fd8c2a1627ab..4aa0d8daf5b34 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.ts @@ -37,10 +37,23 @@ export function initPatchCasesApi({ caseService, router }: RouteDeps) { client: context.core.savedObjects.client, caseIds: query.cases.map(q => q.id), }); + let nonExistingCases: CasePatchRequest[] = []; const conflictedCases = query.cases.filter(q => { const myCase = myCases.saved_objects.find(c => c.id === q.id); + + if (myCase && myCase.error) { + nonExistingCases = [...nonExistingCases, q]; + return false; + } return myCase == null || myCase?.version !== q.version; }); + if (nonExistingCases.length > 0) { + throw Boom.notFound( + `These cases ${nonExistingCases + .map(c => c.id) + .join(', ')} do not exist. Please check you have the correct ids.` + ); + } if (conflictedCases.length > 0) { throw Boom.conflict( `These cases ${conflictedCases @@ -60,18 +73,31 @@ export function initPatchCasesApi({ caseService, router }: RouteDeps) { }); if (updateFilterCases.length > 0) { const updatedBy = await caseService.getUser({ request, response }); - const { full_name, username } = updatedBy; + const { email, full_name, username } = updatedBy; const updatedDt = new Date().toISOString(); const updatedCases = await caseService.patchCases({ client: context.core.savedObjects.client, cases: updateFilterCases.map(thisCase => { const { id: caseId, version, ...updateCaseAttributes } = thisCase; + let closedInfo = {}; + if (updateCaseAttributes.status && updateCaseAttributes.status === 'closed') { + closedInfo = { + closed_at: updatedDt, + closed_by: { email, full_name, username }, + }; + } else if (updateCaseAttributes.status && updateCaseAttributes.status === 'open') { + closedInfo = { + closed_at: null, + closed_by: null, + }; + } return { caseId, updatedAttributes: { ...updateCaseAttributes, + ...closedInfo, updated_at: updatedDt, - updated_by: { full_name, username }, + updated_by: { email, full_name, username }, }, version, }; diff --git a/x-pack/plugins/case/server/routes/api/types.ts b/x-pack/plugins/case/server/routes/api/types.ts index eac259cc69c5a..7af3e7b70d96f 100644 --- a/x-pack/plugins/case/server/routes/api/types.ts +++ b/x-pack/plugins/case/server/routes/api/types.ts @@ -14,7 +14,7 @@ export interface RouteDeps { } export enum SortFieldCase { + closedAt = 'closed_at', createdAt = 'created_at', status = 'status', - updatedAt = 'updated_at', } diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index 27ee6fc58e20a..19dbb024d1e0b 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -26,18 +26,22 @@ import { SortFieldCase } from './types'; export const transformNewCase = ({ createdDate, - newCase, + email, full_name, + newCase, username, }: { createdDate: string; - newCase: CaseRequest; + email?: string; full_name?: string; + newCase: CaseRequest; username: string; }): CaseAttributes => ({ + closed_at: newCase.status === 'closed' ? createdDate : null, + closed_by: newCase.status === 'closed' ? { email, full_name, username } : null, comment_ids: [], created_at: createdDate, - created_by: { full_name, username }, + created_by: { email, full_name, username }, updated_at: null, updated_by: null, ...newCase, @@ -46,18 +50,20 @@ export const transformNewCase = ({ interface NewCommentArgs { comment: string; createdDate: string; + email?: string; full_name?: string; username: string; } export const transformNewComment = ({ comment, createdDate, + email, full_name, username, }: NewCommentArgs): CommentAttributes => ({ comment, created_at: createdDate, - created_by: { full_name, username }, + created_by: { email, full_name, username }, updated_at: null, updated_by: null, }); @@ -133,9 +139,9 @@ export const sortToSnake = (sortField: string): SortFieldCase => { case 'createdAt': case 'created_at': return SortFieldCase.createdAt; - case 'updatedAt': - case 'updated_at': - return SortFieldCase.updatedAt; + case 'closedAt': + case 'closed_at': + return SortFieldCase.closedAt; default: return SortFieldCase.createdAt; } diff --git a/x-pack/plugins/case/server/saved_object_types/cases.ts b/x-pack/plugins/case/server/saved_object_types/cases.ts index 2aa64528739b1..8eab040b9ca9c 100644 --- a/x-pack/plugins/case/server/saved_object_types/cases.ts +++ b/x-pack/plugins/case/server/saved_object_types/cases.ts @@ -14,6 +14,22 @@ export const caseSavedObjectType: SavedObjectsType = { namespaceAgnostic: false, mappings: { properties: { + closed_at: { + type: 'date', + }, + closed_by: { + properties: { + username: { + type: 'keyword', + }, + full_name: { + type: 'keyword', + }, + email: { + type: 'keyword', + }, + }, + }, comment_ids: { type: 'keyword', }, @@ -28,6 +44,9 @@ export const caseSavedObjectType: SavedObjectsType = { full_name: { type: 'keyword', }, + email: { + type: 'keyword', + }, }, }, description: { @@ -53,6 +72,9 @@ export const caseSavedObjectType: SavedObjectsType = { full_name: { type: 'keyword', }, + email: { + type: 'keyword', + }, }, }, }, diff --git a/x-pack/plugins/case/server/saved_object_types/comments.ts b/x-pack/plugins/case/server/saved_object_types/comments.ts index 51c31421fec2f..f52da886e7611 100644 --- a/x-pack/plugins/case/server/saved_object_types/comments.ts +++ b/x-pack/plugins/case/server/saved_object_types/comments.ts @@ -28,6 +28,9 @@ export const caseCommentSavedObjectType: SavedObjectsType = { username: { type: 'keyword', }, + email: { + type: 'keyword', + }, }, }, updated_at: { @@ -41,6 +44,9 @@ export const caseCommentSavedObjectType: SavedObjectsType = { full_name: { type: 'keyword', }, + email: { + type: 'keyword', + }, }, }, }, From 44c23d4769f7b75f8124dc6e5ef353d9b5bd1ca6 Mon Sep 17 00:00:00 2001 From: kqualters-elastic <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 19 Mar 2020 18:22:36 -0400 Subject: [PATCH 10/22] [Endpoint] resolver v1 events (#59233) (#60685) * Unifying the test index name for resolver and alerts * Endpoint isn't sending the agent field so check for it * Update resolver to use either legacy or ecs events * Use correct format for child events api * Adding string or array for category and type * Add return types to process event models * Create a common/models.ts for common event logic * Decrease resolver min height * Update types to match cli tool * Add a smoke test for resolver rendering nodes, remove unused selector * Add common/models/event * Internationalize some strings, address pr comments Co-authored-by: Jonathan Buttner Co-authored-by: Jonathan Buttner --- .../plugins/endpoint/common/models/event.ts | 31 ++++ x-pack/plugins/endpoint/common/types.ts | 5 +- .../endpoint/store/alerts/selectors.ts | 12 -- .../view/alerts/details/overview/index.tsx | 174 ++++++++++-------- .../endpoint/view/alerts/resolver.tsx | 5 +- .../resolver/models/indexed_process_tree.ts | 19 +- .../resolver/models/process_event.ts | 79 +++++--- .../embeddables/resolver/store/actions.ts | 6 +- .../embeddables/resolver/store/data/action.ts | 4 +- .../resolver/store/data/selectors.ts | 10 +- .../embeddables/resolver/store/methods.ts | 4 +- .../embeddables/resolver/store/middleware.ts | 64 +++++-- .../public/embeddables/resolver/types.ts | 27 ++- .../embeddables/resolver/view/index.tsx | 4 +- .../embeddables/resolver/view/panel.tsx | 19 +- .../resolver/view/process_event_dot.tsx | 10 +- .../resolver/view/use_camera.test.tsx | 8 +- .../test/functional/apps/endpoint/alerts.ts | 9 +- 18 files changed, 308 insertions(+), 182 deletions(-) create mode 100644 x-pack/plugins/endpoint/common/models/event.ts diff --git a/x-pack/plugins/endpoint/common/models/event.ts b/x-pack/plugins/endpoint/common/models/event.ts new file mode 100644 index 0000000000000..650486f3c3858 --- /dev/null +++ b/x-pack/plugins/endpoint/common/models/event.ts @@ -0,0 +1,31 @@ +/* + * 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 { EndpointEvent, LegacyEndpointEvent } from '../types'; + +export function isLegacyEvent( + event: EndpointEvent | LegacyEndpointEvent +): event is LegacyEndpointEvent { + return (event as LegacyEndpointEvent).endgame !== undefined; +} + +export function eventTimestamp( + event: EndpointEvent | LegacyEndpointEvent +): string | undefined | number { + if (isLegacyEvent(event)) { + return event.endgame.timestamp_utc; + } else { + return event['@timestamp']; + } +} + +export function eventName(event: EndpointEvent | LegacyEndpointEvent): string { + if (isLegacyEvent(event)) { + return event.endgame.process_name ? event.endgame.process_name : ''; + } else { + return event.process.name; + } +} diff --git a/x-pack/plugins/endpoint/common/types.ts b/x-pack/plugins/endpoint/common/types.ts index aa326c663965d..92bec4f72a387 100644 --- a/x-pack/plugins/endpoint/common/types.ts +++ b/x-pack/plugins/endpoint/common/types.ts @@ -311,8 +311,8 @@ export interface EndpointEvent { version: string; }; event: { - category: string; - type: string; + category: string | string[]; + type: string | string[]; id: string; kind: string; }; @@ -328,6 +328,7 @@ export interface EndpointEvent { name: string; parent?: { entity_id: string; + name?: string; }; }; } diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts index 68731bb3f307f..5e9b08c09c2c7 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts +++ b/x-pack/plugins/endpoint/public/applications/endpoint/store/alerts/selectors.ts @@ -165,15 +165,3 @@ export const hasSelectedAlert: (state: AlertListState) => boolean = createSelect uiQueryParams, ({ selected_alert: selectedAlert }) => selectedAlert !== undefined ); - -/** - * Determine if the alert event is most likely compatible with LegacyEndpointEvent. - */ -export const selectedAlertIsLegacyEndpointEvent: ( - state: AlertListState -) => boolean = createSelector(selectedAlertDetailsData, function(event) { - if (event === undefined) { - return false; - } - return 'endgame' in event; -}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx index 82a4bc00a4396..0ec5a855c8615 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/details/overview/index.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { memo, useMemo } from 'react'; +import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -19,87 +20,104 @@ import * as selectors from '../../../../store/alerts/selectors'; import { MetadataPanel } from './metadata_panel'; import { FormattedDate } from '../../formatted_date'; import { AlertDetailResolver } from '../../resolver'; +import { ResolverEvent } from '../../../../../../../common/types'; import { TakeActionDropdown } from './take_action_dropdown'; -export const AlertDetailsOverview = memo(() => { - const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); - if (alertDetailsData === undefined) { - return null; - } - const selectedAlertIsLegacyEndpointEvent = useAlertListSelector( - selectors.selectedAlertIsLegacyEndpointEvent - ); +export const AlertDetailsOverview = styled( + memo(() => { + const alertDetailsData = useAlertListSelector(selectors.selectedAlertDetailsData); + if (alertDetailsData === undefined) { + return null; + } - const tabs: EuiTabbedContentTab[] = useMemo(() => { - return [ - { - id: 'overviewMetadata', - 'data-test-subj': 'overviewMetadata', - name: i18n.translate( - 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', - { - defaultMessage: 'Overview', - } - ), - content: ( - <> - - - - ), - }, - { - id: 'overviewResolver', - name: i18n.translate( - 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver', - { - defaultMessage: 'Resolver', - } - ), - content: ( - <> - - {selectedAlertIsLegacyEndpointEvent && } - - ), - }, - ]; - }, [selectedAlertIsLegacyEndpointEvent]); + const tabs: EuiTabbedContentTab[] = useMemo(() => { + return [ + { + id: 'overviewMetadata', + 'data-test-subj': 'overviewMetadata', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.overview', + { + defaultMessage: 'Overview', + } + ), + content: ( + <> + + + + ), + }, + { + id: 'overviewResolver', + 'data-test-subj': 'overviewResolverTab', + name: i18n.translate( + 'xpack.endpoint.application.endpoint.alertDetails.overview.tabs.resolver', + { + defaultMessage: 'Resolver', + } + ), + content: ( + <> + + + + ), + }, + ]; + }, [alertDetailsData]); - return ( - <> -
- -

+ return ( + <> +
+ +

+ +

+
+ + +

+ , + }} + /> +

+
+ + + Endpoint Status:{' '} + + {' '} + + + + + {' '} -

-
- - -

- , - }} - /> -

-
- - - Endpoint Status: Online - - Alert Status: Open - - - -
- - - ); -}); + + + + + + + + ); + }) +)` + height: 100%; + width: 100%; +`; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx index 52ef480bbb930..d18bc59a35f52 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/alerts/resolver.tsx @@ -10,12 +10,12 @@ import { Provider } from 'react-redux'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { Resolver } from '../../../../embeddables/resolver/view'; import { EndpointPluginServices } from '../../../../plugin'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; import { storeFactory } from '../../../../embeddables/resolver/store'; export const AlertDetailResolver = styled( React.memo( - ({ className, selectedEvent }: { className?: string; selectedEvent?: LegacyEndpointEvent }) => { + ({ className, selectedEvent }: { className?: string; selectedEvent?: ResolverEvent }) => { const context = useKibana(); const { store } = storeFactory(context); @@ -33,4 +33,5 @@ export const AlertDetailResolver = styled( width: 100%; display: flex; flex-grow: 1; + min-height: 500px; `; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts index 6892bf11ecff2..c9a03f0a47968 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/indexed_process_tree.ts @@ -6,15 +6,15 @@ import { uniquePidForProcess, uniqueParentPidForProcess } from './process_event'; import { IndexedProcessTree } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; import { levelOrder as baseLevelOrder } from '../lib/tree_sequencers'; /** * Create a new IndexedProcessTree from an array of ProcessEvents */ -export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { - const idToChildren = new Map(); - const idToValue = new Map(); +export function factory(processes: ResolverEvent[]): IndexedProcessTree { + const idToChildren = new Map(); + const idToValue = new Map(); for (const process of processes) { idToValue.set(uniquePidForProcess(process), process); @@ -36,10 +36,7 @@ export function factory(processes: LegacyEndpointEvent[]): IndexedProcessTree { /** * Returns an array with any children `ProcessEvent`s of the passed in `process` */ -export function children( - tree: IndexedProcessTree, - process: LegacyEndpointEvent -): LegacyEndpointEvent[] { +export function children(tree: IndexedProcessTree, process: ResolverEvent): ResolverEvent[] { const id = uniquePidForProcess(process); const processChildren = tree.idToChildren.get(id); return processChildren === undefined ? [] : processChildren; @@ -50,8 +47,8 @@ export function children( */ export function parent( tree: IndexedProcessTree, - childProcess: LegacyEndpointEvent -): LegacyEndpointEvent | undefined { + childProcess: ResolverEvent +): ResolverEvent | undefined { const uniqueParentPid = uniqueParentPidForProcess(childProcess); if (uniqueParentPid === undefined) { return undefined; @@ -74,7 +71,7 @@ export function root(tree: IndexedProcessTree) { if (size(tree) === 0) { return null; } - let current: LegacyEndpointEvent = tree.idToProcess.values().next().value; + let current: ResolverEvent = tree.idToProcess.values().next().value; while (parent(tree, current) !== undefined) { current = parent(tree, current)!; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts index 876168d2ed96a..a709d6caf46cb 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/models/process_event.ts @@ -4,36 +4,65 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; +import { ResolverProcessType } from '../types'; /** * Returns true if the process's eventType is either 'processCreated' or 'processRan'. * Resolver will only render 'graphable' process events. */ -export function isGraphableProcess(passedEvent: LegacyEndpointEvent) { +export function isGraphableProcess(passedEvent: ResolverEvent) { return eventType(passedEvent) === 'processCreated' || eventType(passedEvent) === 'processRan'; } +function isValue(field: string | string[], value: string) { + if (field instanceof Array) { + return field.length === 1 && field[0] === value; + } else { + return field === value; + } +} + /** * Returns a custom event type for a process event based on the event's metadata. */ -export function eventType(passedEvent: LegacyEndpointEvent) { - const { - endgame: { event_type_full: type, event_subtype_full: subType }, - } = passedEvent; +export function eventType(passedEvent: ResolverEvent): ResolverProcessType { + if (event.isLegacyEvent(passedEvent)) { + const { + endgame: { event_type_full: type, event_subtype_full: subType }, + } = passedEvent; - if (type === 'process_event') { - if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { - return 'processCreated'; - } else if (subType === 'already_running') { - return 'processRan'; - } else if (subType === 'termination_event') { - return 'processTerminated'; - } else { - return 'unknownProcessEvent'; + if (type === 'process_event') { + if (subType === 'creation_event' || subType === 'fork_event' || subType === 'exec_event') { + return 'processCreated'; + } else if (subType === 'already_running') { + return 'processRan'; + } else if (subType === 'termination_event') { + return 'processTerminated'; + } else { + return 'unknownProcessEvent'; + } + } else if (type === 'alert_event') { + return 'processCausedAlert'; + } + } else { + const { + event: { type, category, kind }, + } = passedEvent; + if (isValue(category, 'process')) { + if (isValue(type, 'start') || isValue(type, 'change') || isValue(type, 'creation')) { + return 'processCreated'; + } else if (isValue(type, 'info')) { + return 'processRan'; + } else if (isValue(type, 'end')) { + return 'processTerminated'; + } else { + return 'unknownProcessEvent'; + } + } else if (kind === 'alert') { + return 'processCausedAlert'; } - } else if (type === 'alert_event') { - return 'processCausedAlert'; } return 'unknownEvent'; } @@ -41,13 +70,21 @@ export function eventType(passedEvent: LegacyEndpointEvent) { /** * Returns the process event's pid */ -export function uniquePidForProcess(event: LegacyEndpointEvent) { - return event.endgame.unique_pid; +export function uniquePidForProcess(passedEvent: ResolverEvent): string { + if (event.isLegacyEvent(passedEvent)) { + return String(passedEvent.endgame.unique_pid); + } else { + return passedEvent.process.entity_id; + } } /** * Returns the process event's parent pid */ -export function uniqueParentPidForProcess(event: LegacyEndpointEvent) { - return event.endgame.unique_ppid; +export function uniqueParentPidForProcess(passedEvent: ResolverEvent): string | undefined { + if (event.isLegacyEvent(passedEvent)) { + return String(passedEvent.endgame.unique_ppid); + } else { + return passedEvent.process.parent?.entity_id; + } } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts index ecba0ec404d44..fec2078cc60c9 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/actions.ts @@ -5,7 +5,7 @@ */ import { CameraAction } from './camera'; import { DataAction } from './data'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; /** * When the user wants to bring a process node front-and-center on the map. @@ -16,7 +16,7 @@ interface UserBroughtProcessIntoView { /** * Used to identify the process node that should be brought into view. */ - readonly process: LegacyEndpointEvent; + readonly process: ResolverEvent; /** * The time (since epoch in milliseconds) when the action was dispatched. */ @@ -33,7 +33,7 @@ interface UserChangedSelectedEvent { /** * Optional because they could have unselected the event. */ - selectedEvent?: LegacyEndpointEvent; + readonly selectedEvent?: ResolverEvent; }; } diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts index f34d7c08ce08c..373afa89921dc 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/action.ts @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; interface ServerReturnedResolverData { readonly type: 'serverReturnedResolverData'; readonly payload: { readonly data: { readonly result: { - readonly search_results: readonly LegacyEndpointEvent[]; + readonly search_results: readonly ResolverEvent[]; }; }; }; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts index 304abbb06880b..e8007f82e30c2 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/data/selectors.ts @@ -14,7 +14,7 @@ import { ProcessWithWidthMetadata, Matrix3, } from '../../types'; -import { LegacyEndpointEvent } from '../../../../../common/types'; +import { ResolverEvent } from '../../../../../common/types'; import { Vector2 } from '../../types'; import { add as vector2Add, applyMatrix3 } from '../../lib/vector2'; import { isGraphableProcess } from '../../models/process_event'; @@ -112,7 +112,7 @@ export const graphableProcesses = createSelector( * */ function widthsOfProcessSubtrees(indexedProcessTree: IndexedProcessTree): ProcessWidths { - const widths = new Map(); + const widths = new Map(); if (size(indexedProcessTree) === 0) { return widths; @@ -313,13 +313,13 @@ function processPositions( indexedProcessTree: IndexedProcessTree, widths: ProcessWidths ): ProcessPositions { - const positions = new Map(); + const positions = new Map(); /** * This algorithm iterates the tree in level order. It keeps counters that are reset for each parent. * By keeping track of the last parent node, we can know when we are dealing with a new set of siblings and * reset the counters. */ - let lastProcessedParentNode: LegacyEndpointEvent | undefined; + let lastProcessedParentNode: ResolverEvent | undefined; /** * Nodes are positioned relative to their siblings. We walk this in level order, so we handle * children left -> right. @@ -424,7 +424,7 @@ export const processNodePositionsAndEdgeLineSegments = createSelector( * Transform the positions of nodes and edges so they seem like they are on an isometric grid. */ const transformedEdgeLineSegments: EdgeLineSegment[] = []; - const transformedPositions = new Map(); + const transformedPositions = new Map(); for (const [processEvent, position] of positions) { transformedPositions.set(processEvent, applyMatrix3(position, isometricTransformMatrix)); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts index 9f06643626f50..f15307a662388 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/methods.ts @@ -7,7 +7,7 @@ import { animatePanning } from './camera/methods'; import { processNodePositionsAndEdgeLineSegments } from './selectors'; import { ResolverState } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; const animationDuration = 1000; @@ -17,7 +17,7 @@ const animationDuration = 1000; export function animateProcessIntoView( state: ResolverState, startTime: number, - process: LegacyEndpointEvent + process: ResolverEvent ): ResolverState { const { processNodePositions } = processNodePositionsAndEdgeLineSegments(state); const position = processNodePositions.get(process); diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts index 900aece60618d..23e4a4fe7d7ed 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/store/middleware.ts @@ -8,6 +8,8 @@ import { Dispatch, MiddlewareAPI } from 'redux'; import { KibanaReactContextValue } from '../../../../../../../src/plugins/kibana_react/public'; import { EndpointPluginServices } from '../../../plugin'; import { ResolverState, ResolverAction } from '../types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; type MiddlewareFactory = ( context?: KibanaReactContextValue @@ -19,22 +21,54 @@ export const resolverMiddlewareFactory: MiddlewareFactory = context => { return api => next => async (action: ResolverAction) => { next(action); if (action.type === 'userChangedSelectedEvent') { - if (context?.services.http) { + /** + * concurrently fetches a process's details, its ancestors, and its related events. + */ + if (context?.services.http && action.payload.selectedEvent) { api.dispatch({ type: 'appRequestedResolverData' }); - const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; - const legacyEndpointID = action.payload.selectedEvent?.agent?.id; - const [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ - context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { - query: { legacyEndpointID }, - }), - context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { - query: { legacyEndpointID }, - }), - ]); - const response = [...lifecycle, ...children, ...relatedEvents]; + let response = []; + let lifecycle: ResolverEvent[]; + let childEvents: ResolverEvent[]; + let relatedEvents: ResolverEvent[]; + let children = []; + const ancestors: ResolverEvent[] = []; + const maxAncestors = 5; + if (event.isLegacyEvent(action.payload.selectedEvent)) { + const uniquePid = action.payload.selectedEvent?.endgame?.unique_pid; + const legacyEndpointID = action.payload.selectedEvent?.agent?.id; + [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${uniquePid}`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`, { + query: { legacyEndpointID }, + }), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`, { + query: { legacyEndpointID }, + }), + ]); + childEvents = children.length > 0 ? children.map((child: any) => child.lifecycle) : []; + } else { + const uniquePid = action.payload.selectedEvent.process.entity_id; + const ppid = action.payload.selectedEvent.process.parent?.entity_id; + async function getAncestors(pid: string | undefined) { + if (ancestors.length < maxAncestors && pid !== undefined) { + const parent = await context?.services.http.get(`/api/endpoint/resolver/${pid}`); + ancestors.push(parent.lifecycle[0]); + if (parent.lifecycle[0].process?.parent?.entity_id) { + await getAncestors(parent.lifecycle[0].process.parent.entity_id); + } + } + } + [{ lifecycle }, { children }, { events: relatedEvents }] = await Promise.all([ + context.services.http.get(`/api/endpoint/resolver/${uniquePid}`), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/children`), + context.services.http.get(`/api/endpoint/resolver/${uniquePid}/related`), + getAncestors(ppid), + ]); + } + childEvents = children.length > 0 ? children.map((child: any) => child.lifecycle) : []; + response = [...lifecycle, ...childEvents, ...relatedEvents, ...ancestors]; api.dispatch({ type: 'serverReturnedResolverData', payload: { data: { result: { search_results: response } } }, diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts index 4c2a1ea5ac21f..4380d3ab98999 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/types.ts @@ -8,7 +8,7 @@ import { Store } from 'redux'; import { ResolverAction } from './store/actions'; export { ResolverAction } from './store/actions'; -import { LegacyEndpointEvent } from '../../../common/types'; +import { ResolverEvent } from '../../../common/types'; /** * Redux state for the Resolver feature. Properties on this interface are populated via multiple reducers using redux's `combineReducers`. @@ -115,7 +115,7 @@ export type CameraState = { * State for `data` reducer which handles receiving Resolver data from the backend. */ export interface DataState { - readonly results: readonly LegacyEndpointEvent[]; + readonly results: readonly ResolverEvent[]; isLoading: boolean; } @@ -184,21 +184,21 @@ export interface IndexedProcessTree { /** * Map of ID to a process's children */ - idToChildren: Map; + idToChildren: Map; /** * Map of ID to process */ - idToProcess: Map; + idToProcess: Map; } /** * A map of ProcessEvents (representing process nodes) to the 'width' of their subtrees as calculated by `widthsOfProcessSubtrees` */ -export type ProcessWidths = Map; +export type ProcessWidths = Map; /** * Map of ProcessEvents (representing process nodes) to their positions. Calculated by `processPositions` */ -export type ProcessPositions = Map; +export type ProcessPositions = Map; /** * An array of vectors2 forming an polyline. Used to connect process nodes in the graph. */ @@ -208,11 +208,11 @@ export type EdgeLineSegment = Vector2[]; * Used to provide precalculated info from `widthsOfProcessSubtrees`. These 'width' values are used in the layout of the graph. */ export type ProcessWithWidthMetadata = { - process: LegacyEndpointEvent; + process: ResolverEvent; width: number; } & ( | { - parent: LegacyEndpointEvent; + parent: ResolverEvent; parentWidth: number; isOnlyChild: boolean; firstChildWidth: number; @@ -275,4 +275,15 @@ export interface SideEffectSimulator { mock: jest.Mocked> & Pick; } +/** + * The internal types of process events used by resolver, mapped from v0 and v1 events. + */ +export type ResolverProcessType = + | 'processCreated' + | 'processRan' + | 'processTerminated' + | 'unknownProcessEvent' + | 'processCausedAlert' + | 'unknownEvent'; + export type ResolverStore = Store; diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx index 52a0872f269f5..eab22f993d0a8 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/index.tsx @@ -15,7 +15,7 @@ import { GraphControls } from './graph_controls'; import { ProcessEventDot } from './process_event_dot'; import { useCamera } from './use_camera'; import { ResolverAction } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; const StyledPanel = styled(Panel)` position: absolute; @@ -39,7 +39,7 @@ export const Resolver = styled( selectedEvent, }: { className?: string; - selectedEvent?: LegacyEndpointEvent; + selectedEvent?: ResolverEvent; }) { const { processNodePositions, edgeLineSegments } = useSelector( selectors.processNodePositionsAndEdgeLineSegments diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx index 84c299698bb32..1250c1106b355 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/panel.tsx @@ -11,7 +11,8 @@ import euiVars from '@elastic/eui/dist/eui_theme_light.json'; import { useSelector } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { SideEffectContext } from './side_effect_context'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as event from '../../../../common/models/event'; import { useResolverDispatch } from './use_resolver_dispatch'; import * as selectors from '../store/selectors'; @@ -38,7 +39,7 @@ export const Panel = memo(function Event({ className }: { className?: string }) interface ProcessTableView { name: string; timestamp?: Date; - event: LegacyEndpointEvent; + event: ResolverEvent; } const { processNodePositions } = useSelector(selectors.processNodePositionsAndEdgeLineSegments); @@ -48,14 +49,16 @@ export const Panel = memo(function Event({ className }: { className?: string }) () => [...processNodePositions.keys()].map(processEvent => { let dateTime; - if (processEvent.endgame.timestamp_utc) { - const date = new Date(processEvent.endgame.timestamp_utc); + const eventTime = event.eventTimestamp(processEvent); + const name = event.eventName(processEvent); + if (eventTime) { + const date = new Date(eventTime); if (isFinite(date.getTime())) { dateTime = date; } } return { - name: processEvent.endgame.process_name ? processEvent.endgame.process_name : '', + name, timestamp: dateTime, event: processEvent, }; @@ -115,9 +118,9 @@ export const Panel = memo(function Event({ className }: { className?: string }) }), dataType: 'date', sortable: true, - render(eventTimestamp?: Date) { - return eventTimestamp ? ( - formatter.format(eventTimestamp) + render(eventDate?: Date) { + return eventDate ? ( + formatter.format(eventDate) ) : ( {i18n.translate('xpack.endpoint.resolver.panel.tabel.row.timestampInvalidLabel', { diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx index 034780c7ba14c..2241df97291ae 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/process_event_dot.tsx @@ -8,7 +8,8 @@ import React from 'react'; import styled from 'styled-components'; import { applyMatrix3 } from '../lib/vector2'; import { Vector2, Matrix3 } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; +import * as eventModel from '../../../../common/models/event'; /** * A placeholder view for a process node. @@ -32,7 +33,7 @@ export const ProcessEventDot = styled( /** * An event which contains details about the process node. */ - event: LegacyEndpointEvent; + event: ResolverEvent; /** * projectionMatrix which can be used to convert `position` to screen coordinates. */ @@ -42,14 +43,13 @@ export const ProcessEventDot = styled( * Convert the position, which is in 'world' coordinates, to screen coordinates. */ const [left, top] = applyMatrix3(position, projectionMatrix); - const style = { left: (left - 20).toString() + 'px', top: (top - 20).toString() + 'px', }; return ( - - name: {event.endgame.process_name} + + name: {eventModel.eventName(event)}
x: {position[0]}
diff --git a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx index 711e4f9a5c537..6e83fc19a922e 100644 --- a/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx +++ b/x-pack/plugins/endpoint/public/embeddables/resolver/view/use_camera.test.tsx @@ -11,7 +11,7 @@ import { Provider } from 'react-redux'; import * as selectors from '../store/selectors'; import { storeFactory } from '../store'; import { Matrix3, ResolverAction, ResolverStore, SideEffectSimulator } from '../types'; -import { LegacyEndpointEvent } from '../../../../common/types'; +import { ResolverEvent } from '../../../../common/types'; import { SideEffectContext } from './side_effect_context'; import { applyMatrix3 } from '../lib/vector2'; import { sideEffectSimulator } from './side_effect_simulator'; @@ -133,9 +133,9 @@ describe('useCamera on an unpainted element', () => { expect(simulator.mock.requestAnimationFrame).not.toHaveBeenCalled(); }); describe('when the camera begins animation', () => { - let process: LegacyEndpointEvent; + let process: ResolverEvent; beforeEach(() => { - const events: LegacyEndpointEvent[] = []; + const events: ResolverEvent[] = []; const numberOfEvents: number = Math.floor(Math.random() * 10 + 1); for (let index = 0; index < numberOfEvents; index++) { @@ -164,7 +164,7 @@ describe('useCamera on an unpainted element', () => { act(() => { store.dispatch(serverResponseAction); }); - const processes: LegacyEndpointEvent[] = [ + const processes: ResolverEvent[] = [ ...selectors .processNodePositionsAndEdgeLineSegments(store.getState()) .processNodePositions.keys(), diff --git a/x-pack/test/functional/apps/endpoint/alerts.ts b/x-pack/test/functional/apps/endpoint/alerts.ts index 1ce7eb41e6690..759574702c0f1 100644 --- a/x-pack/test/functional/apps/endpoint/alerts.ts +++ b/x-pack/test/functional/apps/endpoint/alerts.ts @@ -18,8 +18,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.load('endpoint/alerts/api_feature'); await pageObjects.common.navigateToUrlWithBrowserHistory('endpoint', '/alerts'); }); - - it('loads in the browser', async () => { + it('loads the Alert List Page', async () => { await testSubjects.existOrFail('alertListPage'); }); it('contains the Alert List Page title', async () => { @@ -57,6 +56,12 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { it('loads the Alert List Flyout correctly', async () => { await testSubjects.existOrFail('alertDetailFlyout'); }); + + it('loads the resolver component and renders at least a single node', async () => { + await testSubjects.click('overviewResolverTab'); + await testSubjects.existOrFail('alertResolver'); + await testSubjects.existOrFail('resolverNode'); + }); }); after(async () => { From 69b2082e2c4e3792b05f069db5adfb9787d3853f Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 19 Mar 2020 16:55:54 -0600 Subject: [PATCH 11/22] [SIEM][Detection Engine] Adds lists feature flag and list values to the REST interfaces (#60678) ## Summary * https://github.com/elastic/kibana/issues/60022 * Adds the feature flag for simple list values * Adds the boolean filters of "and", "and not" to further filter based on simple values * Adds unit tests and e2e tests for the values. * Most tests can include the simple list values but some have to be skipped until we move those to more functions or just enable simple list values as a permanent feature. * DOES NOT FILTER ON THE VALUES JUST YET (That will be a follow on PR) ## Testing: To turn on/off the feature flag do this with an env variable (set this in your .bashrc/.zshrc): ```ts export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true ``` Expect to see this error in the console when the environment variable is set: ```ts server log [11:41:16.245] [error][plugins][siem] You have activated the lists feature flag which is NOT currently supported for SIEM! You should turn this feature flag off immediately by un-setting the environment variable: ELASTIC_XPACK_SIEM_LISTS_FEATURE and restarting Kibana ``` Expect create and update to work when the environment variable is set and look like this: ```ts ./update_rule.sh ./rules/updates/update_list.json { "created_at": "2020-03-15T17:42:37.074Z", "updated_at": "2020-03-15T17:54:22.427Z", "created_by": "yo", "description": "Query with a list", "enabled": true, "false_positives": [], "from": "now-6m", "id": "c602e3f6-713b-4f43-9bdd-b60fbfead1c5", "immutable": false, "interval": "5m", "rule_id": "query-with-list", "language": "kuery", "output_index": ".siem-signals-hassanabad-frank-default", "max_signals": 100, "risk_score": 1, "name": "Query with a list", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "yo", "tags": [], "to": "now", "type": "query", "threat": [], "version": 6, "lists": [ { "field": "source.ip", "boolean_operator": "and", "values": [ { "name": "127.0.0.1", "type": "value" } ] }, { "field": "host.name", "boolean_operator": "and not", "values": [ { "name": "rock01", "type": "value" } ] } ], "status": "succeeded", "status_date": "2020-03-15T17:42:40.718Z", "last_success_at": "2020-03-15T17:42:40.718Z", "last_success_message": "succeeded" } ``` ```ts ./post_rule.sh ./rules/queries/query_with_list.json { "created_at": "2020-03-15T17:42:37.074Z", "updated_at": "2020-03-15T17:42:37.116Z", "created_by": "yo", "description": "Query with a list", "enabled": true, "false_positives": [], "from": "now-6m", "id": "c602e3f6-713b-4f43-9bdd-b60fbfead1c5", "immutable": false, "interval": "5m", "rule_id": "query-with-list", "language": "kuery", "output_index": ".siem-signals-hassanabad-frank-default", "max_signals": 100, "risk_score": 1, "name": "Query with a list", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "yo", "tags": [], "to": "now", "type": "query", "threat": [], "version": 1, "lists": [ { "field": "source.ip", "boolean_operator": "and", "values": [ { "name": "127.0.0.1", "type": "value" } ] }, { "field": "host.name", "boolean_operator": "and not", "values": [ { "name": "rock01", "type": "value" }, { "name": "mothra", "type": "value" } ] } ] } ``` ```ts ./patch_rule.sh ./rules/patches/update_list.json { "created_at": "2020-03-15T18:02:52.434Z", "updated_at": "2020-03-15T18:02:57.675Z", "created_by": "yo", "description": "Query with a list", "enabled": true, "false_positives": [], "from": "now-6m", "id": "40b7c2fb-83b4-4820-bf7c-056f3a631126", "immutable": false, "interval": "5m", "rule_id": "query-with-list", "language": "kuery", "output_index": ".siem-signals-hassanabad-frank-default", "max_signals": 100, "risk_score": 1, "name": "Query with a list", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "yo", "tags": [], "to": "now", "type": "query", "threat": [], "version": 1, "lists": [ { "field": "source.ip", "boolean_operator": "and", "values": [ { "name": "127.0.0.1", "type": "value" } ] }, { "field": "host.name", "boolean_operator": "and not", "values": [ { "name": "rock01", "type": "value" }, { "name": "mothra", "type": "value" } ] } ], "status": "succeeded", "status_date": "2020-03-15T18:02:56.426Z", "last_success_at": "2020-03-15T18:02:56.426Z", "last_success_message": "succeeded" } ``` ```ts ./get_rule_by_rule_id.sh query-with-list { "created_at": "2020-03-15T18:10:07.657Z", "updated_at": "2020-03-15T18:10:08.479Z", "created_by": "yo", "description": "Query with a list", "enabled": true, "false_positives": [], "from": "now-6m", "id": "9854162b-003c-47be-af59-8c3c9545aafa", "immutable": false, "interval": "5m", "rule_id": "query-with-list", "language": "kuery", "output_index": ".siem-signals-hassanabad-frank-default", "max_signals": 100, "risk_score": 1, "name": "Query with a list", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "yo", "tags": [], "to": "now", "type": "query", "threat": [], "version": 1, "lists": [ { "field": "source.ip", "boolean_operator": "and", "values": [ { "name": "127.0.0.1", "type": "value" } ] }, { "field": "host.name", "boolean_operator": "and not", "values": [ { "name": "rock01", "type": "value" }, { "name": "mothra", "type": "value" } ] } ], "status": "going to run", "status_date": "2020-03-15T18:10:10.738Z" } ``` Expect these errors when the environment variable is not set: ```ts ./post_rule.sh ./rules/queries/query_with_list.json { "statusCode": 400, "error": "Bad Request", "message": "[request body]: child \"lists\" fails because [\"lists\" is not allowed]" } ``` ```ts ./update_rule.sh ./rules/queries/query_with_list.json { "statusCode": 400, "error": "Bad Request", "message": "[request body]: child \"lists\" fails because [\"lists\" is not allowed]" } ``` ```ts ./patch_rule.sh ./rules/patches/update_list.json { "statusCode": 400, "error": "Bad Request", "message": "[request body]: child \"lists\" fails because [\"lists\" is not allowed]" } ``` Expect that this is _backwards_ compatible with the feature flag but not necessarily _forwards_ compatible. This means: * You can have older data that never had lists and it will show up as an empty list when you query it. (backwards compatible) * You _might_ have lists and remove the env. variable and get back items as if the list was not there for (forwards compatible) * You can export without lists, flip on the env flag and import with newer lists feature (backwards compatible) * You can export lists and it will _not_ work with an older system (not forwards compatible) ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../detection_engine/feature_flags.test.ts | 97 ++ .../lib/detection_engine/feature_flags.ts | 49 + .../index/get_index_exists.test.ts | 9 + .../routes/__mocks__/request_responses.ts | 26 + .../routes/__mocks__/utils.ts | 89 ++ .../rules/add_prepackaged_rules_route.test.ts | 9 + .../rules/create_rules_bulk_route.test.ts | 9 + .../routes/rules/create_rules_bulk_route.ts | 2 + .../routes/rules/create_rules_route.test.ts | 9 + .../routes/rules/create_rules_route.ts | 2 + .../rules/delete_rules_bulk_route.test.ts | 9 + .../routes/rules/delete_rules_route.test.ts | 9 + .../routes/rules/find_rules_route.test.ts | 9 + .../rules/find_rules_status_route.test.ts | 9 + ...get_prepackaged_rules_status_route.test.ts | 9 + .../routes/rules/import_rules_route.test.ts | 9 + .../routes/rules/import_rules_route.ts | 2 + .../rules/patch_rules_bulk_route.test.ts | 9 + .../routes/rules/patch_rules_route.test.ts | 9 + .../routes/rules/read_rules_route.test.ts | 9 + .../rules/update_rules_bulk_route.test.ts | 9 + .../routes/rules/update_rules_bulk_route.ts | 2 + .../routes/rules/update_rules_route.test.ts | 9 + .../routes/rules/update_rules_route.ts | 2 + .../routes/rules/utils.test.ts | 854 ++---------------- .../detection_engine/routes/rules/utils.ts | 3 + .../routes/rules/validate.test.ts | 35 + .../add_prepackaged_rules_schema.test.ts | 121 +++ .../schemas/add_prepackaged_rules_schema.ts | 5 + .../schemas/create_rules_bulk_schema.test.ts | 9 + .../schemas/create_rules_schema.test.ts | 117 +++ .../routes/schemas/create_rules_schema.ts | 5 + .../schemas/export_rules_schema.test.ts | 9 + .../routes/schemas/find_rules_schema.test.ts | 9 + .../schemas/import_rules_schema.test.ts | 117 +++ .../routes/schemas/import_rules_schema.ts | 5 + .../schemas/patch_rules_bulk_schema.test.ts | 9 + .../routes/schemas/patch_rules_schema.test.ts | 151 ++++ .../routes/schemas/patch_rules_schema.ts | 5 + .../schemas/query_rules_bulk_schema.test.ts | 9 + .../routes/schemas/query_rules_schema.test.ts | 9 + .../query_signals_index_schema.test.ts | 9 + .../schemas/response/__mocks__/utils.ts | 26 + .../response/check_type_dependents.test.ts | 9 + .../schemas/response/error_schema.test.ts | 9 + .../schemas/response/exact_check.test.ts | 9 + .../response/find_rules_schema.test.ts | 9 + .../response/import_rules_schema.test.ts | 9 + .../response/prepackaged_rules_schema.test.ts | 9 + .../prepackaged_rules_status_schema.test.ts | 9 + .../response/rules_bulk_schema.test.ts | 9 + .../schemas/response/rules_schema.test.ts | 91 +- .../routes/schemas/response/rules_schema.ts | 27 +- .../routes/schemas/response/schemas.ts | 13 + .../type_timeline_only_schema.test.ts | 9 + .../routes/schemas/response/utils.test.ts | 9 + .../routes/schemas/schemas.ts | 12 + .../schemas/set_signal_status_schema.test.ts | 9 + .../schemas/types/lists_default_array.test.ts | 85 ++ .../schemas/types/lists_default_array.ts | 27 + .../schemas/update_rules_bulk_schema.test.ts | 9 + .../schemas/update_rules_schema.test.ts | 117 +++ .../routes/schemas/update_rules_schema.ts | 5 + .../routes/signals/open_close_signals.test.ts | 9 + .../signals/query_signals_route.test.ts | 9 + .../lib/detection_engine/routes/utils.test.ts | 9 + .../detection_engine/rules/create_rules.ts | 5 + .../create_rules_stream_from_ndjson.test.ts | 10 + .../rules/get_export_all.test.ts | 92 +- .../rules/get_export_by_object_ids.test.ts | 118 ++- .../rules/install_prepacked_rules.ts | 2 + .../lib/detection_engine/rules/patch_rules.ts | 3 + .../detection_engine/rules/update_rules.ts | 6 + .../scripts/rules/patches/update_list.json | 25 + .../rules/queries/query_with_list.json | 35 + .../scripts/rules/updates/update_list.json | 31 + .../signals/__mocks__/es_results.ts | 26 + .../signals/build_bulk_body.test.ts | 104 +++ .../signals/build_rule.test.ts | 78 ++ .../detection_engine/signals/build_rule.ts | 1 + .../signals/signal_params_schema.ts | 1 + .../siem/server/lib/detection_engine/types.ts | 6 + x-pack/legacy/plugins/siem/server/plugin.ts | 7 + .../common/config.ts | 5 + .../security_and_spaces/tests/utils.ts | 2 + 85 files changed, 2172 insertions(+), 810 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json create mode 100644 x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts new file mode 100644 index 0000000000000..920064f9a1b77 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + listsEnvFeatureFlagName, + hasListsFeature, + unSetFeatureFlagsForTestsOnly, + setFeatureFlagsForTestsOnly, +} from './feature_flags'; + +describe('feature_flags', () => { + beforeAll(() => { + delete process.env[listsEnvFeatureFlagName]; + }); + + afterEach(() => { + delete process.env[listsEnvFeatureFlagName]; + }); + + describe('hasListsFeature', () => { + test('hasListsFeature should return false if process.env is not set', () => { + expect(hasListsFeature()).toEqual(false); + }); + + test('hasListsFeature should return true if process.env is set to true', () => { + process.env[listsEnvFeatureFlagName] = 'true'; + expect(hasListsFeature()).toEqual(true); + }); + + test('hasListsFeature should return false if process.env is set to false', () => { + process.env[listsEnvFeatureFlagName] = 'false'; + expect(hasListsFeature()).toEqual(false); + }); + + test('hasListsFeature should return false if process.env is set to a non true value', () => { + process.env[listsEnvFeatureFlagName] = 'something else'; + expect(hasListsFeature()).toEqual(false); + }); + }); + + describe('setFeatureFlagsForTestsOnly', () => { + test('it can be called once and sets the environment variable for tests', () => { + setFeatureFlagsForTestsOnly(); + expect(process.env[listsEnvFeatureFlagName]).toEqual('true'); + unSetFeatureFlagsForTestsOnly(); // This is needed to not pollute other tests since this has to be paired + }); + + test('if it is called twice it throws an exception', () => { + setFeatureFlagsForTestsOnly(); + expect(() => setFeatureFlagsForTestsOnly()).toThrow( + 'In your tests you need to ensure in your afterEach/afterAll blocks you are calling unSetFeatureFlagsForTestsOnly' + ); + unSetFeatureFlagsForTestsOnly(); // This is needed to not pollute other tests since this has to be paired + }); + + test('it can be called twice as long as unSetFeatureFlagsForTestsOnly is called in-between', () => { + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + setFeatureFlagsForTestsOnly(); + expect(process.env[listsEnvFeatureFlagName]).toEqual('true'); + unSetFeatureFlagsForTestsOnly(); // This is needed to not pollute other tests since this has to be paired + }); + }); + + describe('unSetFeatureFlagsForTestsOnly', () => { + test('it can sets the value to undefined', () => { + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + expect(process.env[listsEnvFeatureFlagName]).toEqual(undefined); + }); + + test('it can not be be called before setFeatureFlagsForTestsOnly without throwing', () => { + expect(() => unSetFeatureFlagsForTestsOnly()).toThrow( + 'In your tests you need to ensure in your beforeEach/beforeAll blocks you are calling setFeatureFlagsForTestsOnly' + ); + }); + + test('if it is called twice it throws an exception', () => { + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + expect(() => unSetFeatureFlagsForTestsOnly()).toThrow( + 'In your tests you need to ensure in your beforeEach/beforeAll blocks you are calling setFeatureFlagsForTestsOnly' + ); + }); + + test('it can be called twice as long as setFeatureFlagsForTestsOnly is called in-between', () => { + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + setFeatureFlagsForTestsOnly(); + unSetFeatureFlagsForTestsOnly(); + expect(process.env[listsEnvFeatureFlagName]).toEqual(undefined); + }); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts new file mode 100644 index 0000000000000..4e309faa46e1b --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/feature_flags.ts @@ -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. + */ + +// TODO: (LIST-FEATURE) Delete this file once the lists features are within the product and in a particular version + +// Very temporary file where we put our feature flags for detection lists. +// We need to use an environment variable and CANNOT use a kibana.dev.yml setting because some definitions +// of things are global in the modules are are initialized before the init of the server has a chance to start. +// Set this in your .bashrc/.zshrc to turn on lists feature, export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true + +// NOTE: This feature is forwards and backwards compatible but forwards compatible is not guaranteed. +// Once you enable this and begin using it you might not be able to easily go back back. +// So it's best to not turn it on unless you are developing code. +export const listsEnvFeatureFlagName = 'ELASTIC_XPACK_SIEM_LISTS_FEATURE'; + +// This is for setFeatureFlagsForTestsOnly and unSetFeatureFlagsForTestsOnly only to use +let setFeatureFlagsForTestsOnlyCalled = false; + +// Use this to detect if the lists feature is enabled or not +export const hasListsFeature = (): boolean => { + return process.env[listsEnvFeatureFlagName]?.trim().toLowerCase() === 'true'; +}; + +// This is for tests only to use in your beforeAll() calls +export const setFeatureFlagsForTestsOnly = (): void => { + if (setFeatureFlagsForTestsOnlyCalled) { + throw new Error( + 'In your tests you need to ensure in your afterEach/afterAll blocks you are calling unSetFeatureFlagsForTestsOnly' + ); + } else { + setFeatureFlagsForTestsOnlyCalled = true; + process.env[listsEnvFeatureFlagName] = 'true'; + } +}; + +// This is for tests only to use in your afterAll() calls +export const unSetFeatureFlagsForTestsOnly = (): void => { + if (!setFeatureFlagsForTestsOnlyCalled) { + throw new Error( + 'In your tests you need to ensure in your beforeEach/beforeAll blocks you are calling setFeatureFlagsForTestsOnly' + ); + } else { + delete process.env[listsEnvFeatureFlagName]; + setFeatureFlagsForTestsOnlyCalled = false; + } +}; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts index cb358c15e5fad..25945e72ff179 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/index/get_index_exists.test.ts @@ -5,6 +5,7 @@ */ import { getIndexExists } from './get_index_exists'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; class StatusCode extends Error { status: number = -1; @@ -15,6 +16,14 @@ class StatusCode extends Error { } describe('get_index_exists', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should return a true if you have _shards', async () => { const callWithRequest = jest.fn().mockResolvedValue({ _shards: { total: 1 } }); const indexExists = await getIndexExists(callWithRequest, 'some-index'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index d90c8ea49a53f..01f5c364ae420 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -412,6 +412,32 @@ export const getResult = (): RuleAlertType => ({ references: ['http://www.example.com', 'https://ww.example.com'], note: '# Investigative notes', version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, createdAt: new Date('2019-12-13T16:40:33.400Z'), updatedAt: new Date('2019-12-13T16:40:33.400Z'), diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts index f59370ce481b6..aa9b05eb379a6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -77,3 +77,92 @@ export const buildHapiStream = (string: string, filename = 'file.ndjson'): HapiR return stream; }; + +export const getOutputRuleAlertForRest = (): Omit< + OutputRuleAlertRest, + 'machine_learning_job_id' | 'anomaly_threshold' +> => ({ + created_by: 'elastic', + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + risk_score: 50, + rule_id: 'rule-1', + language: 'kuery', + max_signals: 100, + name: 'Detect Root/Admin Users', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + severity: 'high', + updated_by: 'elastic', + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + filters: [ + { + query: { + match_phrase: { + 'host.name': 'some-host', + }, + }, + }, + ], + meta: { + someMeta: 'someField', + }, + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + to: 'now', + type: 'query', + note: '# Investigative notes', + version: 1, +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts index 2b4fb8fa08a60..f53efc8a3234d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.test.ts @@ -14,6 +14,7 @@ import { import { requestContextMock, serverMock } from '../__mocks__'; import { addPrepackedRulesRoute } from './add_prepackaged_rules_route'; import { PrepackagedRules } from '../../types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -44,6 +45,14 @@ describe('add_prepackaged_rules_route', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index 2b31d37dddddb..e2af678c828e6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -16,11 +16,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index b819bc6919274..e8b1162b06182 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -84,6 +84,7 @@ export const createRulesBulkRoute = (router: IRouter) => { timeline_id: timelineId, timeline_title: timelineTitle, version, + lists, } = payloadRule; const ruleIdOrUuid = ruleId ?? uuid.v4(); try { @@ -138,6 +139,7 @@ export const createRulesBulkRoute = (router: IRouter) => { references, note, version, + lists, }); return transformValidateBulkError(ruleIdOrUuid, createdRule); } catch (err) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index 976f371c6b1a6..1a4e19c2047b5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -18,11 +18,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 42bade1ba0855..3a440178344da 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -58,6 +58,7 @@ export const createRulesRoute = (router: IRouter): void => { type, references, note, + lists, } = request.body; const siemResponse = buildSiemResponse(response); @@ -124,6 +125,7 @@ export const createRulesRoute = (router: IRouter): void => { references, note, version: 1, + lists, }); const ruleStatuses = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts index 16f9a9524df55..f2da3ab4be8f6 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.test.ts @@ -17,11 +17,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { deleteRulesBulkRoute } from './delete_rules_bulk_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('delete_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 0519addb275d6..e30f332ecd1ca 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -15,11 +15,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { deleteRulesRoute } from './delete_rules_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('delete_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts index 57759844c100d..b4591a8141f7b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.test.ts @@ -13,11 +13,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { findRulesRoute } from './find_rules_route'; +import { unSetFeatureFlagsForTestsOnly, setFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('find_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts index 9c86b70b88270..89c9f34027120 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts @@ -8,11 +8,20 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getFindResultStatus, ruleStatusRequest } from '../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { findRulesStatusesRoute } from './find_rules_status_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('find_statuses', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts index 03059ed5ec5cc..2bbd4f78afae1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/get_prepackaged_rules_status_route.test.ts @@ -13,6 +13,7 @@ import { getNonEmptyIndex, } from '../__mocks__/request_responses'; import { requestContextMock, serverMock } from '../__mocks__'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; jest.mock('../../rules/get_prepackaged_rules', () => { return { @@ -38,6 +39,14 @@ jest.mock('../../rules/get_prepackaged_rules', () => { }); describe('get_prepackaged_rule_status_route', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index c224e0f055b85..f6e1cf6e2420c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -23,8 +23,17 @@ import { createMockConfig, requestContextMock, serverMock, requestMock } from '. import { importRulesRoute } from './import_rules_route'; import { DEFAULT_SIGNALS_INDEX } from '../../../../../common/constants'; import * as createRulesStreamFromNdJson from '../../rules/create_rules_stream_from_ndjson'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('import_rules_route', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + let config = createMockConfig(); let server: ReturnType; let request: ReturnType; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index d92ef316aef0c..920cf97d32a7a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -140,6 +140,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config timeline_id: timelineId, timeline_title: timelineTitle, version, + lists, } = parsedRule; try { @@ -191,6 +192,7 @@ export const importRulesRoute = (router: IRouter, config: LegacyServices['config references, note, version, + lists, }); resolve({ rule_id: ruleId, status_code: 200 }); } else if (rule != null && request.query.overwrite) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index 967fd46f7e3da..4c980c8cc60d2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -14,11 +14,20 @@ import { } from '../__mocks__/request_responses'; import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('patch_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index 0c2ca882a5590..b92c18827557c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -16,11 +16,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { patchRulesRoute } from './patch_rules_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('patch_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index 7ebac9b785c82..982e1bb47a53a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -14,11 +14,20 @@ import { getFindResultStatusEmpty, } from '../__mocks__/request_responses'; import { requestMock, requestContextMock, serverMock } from '../__mocks__'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('read_signals', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 46639e1fe3380..d530866edaf0d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -16,11 +16,20 @@ import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('update_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 859935d851126..deb319492258c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -76,6 +76,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { references, note, version, + lists, } = payloadRule; const finalIndex = outputIndex ?? siemClient.signalsIndex; const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; @@ -114,6 +115,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { references, note, version, + lists, }); if (rule != null) { const ruleStatuses = await savedObjectsClient.find< diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index a6da8cd56ec17..a15f1ca9b044e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -16,11 +16,20 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('update_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index a9982a9896633..c47a412c2e9df 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -59,6 +59,7 @@ export const updateRulesRoute = (router: IRouter) => { references, note, version, + lists, } = request.body; const siemResponse = buildSiemResponse(response); @@ -110,6 +111,7 @@ export const updateRulesRoute = (router: IRouter) => { references, note, version, + lists, }); if (rule != null) { const ruleStatuses = await savedObjectsClient.find< diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts index 3243ccb14f89c..3a8d068cad38d 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.test.ts @@ -20,403 +20,88 @@ import { } from './utils'; import { getResult } from '../__mocks__/request_responses'; import { INTERNAL_IDENTIFIER } from '../../../../../common/constants'; -import { OutputRuleAlertRest, ImportRuleAlertRest, RuleAlertParamsRest } from '../../types'; +import { ImportRuleAlertRest, RuleAlertParamsRest, RuleTypeParams } from '../../types'; import { BulkError, ImportSuccessError } from '../utils'; import { sampleRule } from '../../signals/__mocks__/es_results'; -import { getSimpleRule } from '../__mocks__/utils'; +import { getSimpleRule, getOutputRuleAlertForRest } from '../__mocks__/utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { createPromiseFromStreams } from '../../../../../../../../../src/legacy/utils/streams'; import { PartialAlert } from '../../../../../../../../plugins/alerting/server'; import { SanitizedAlert } from '../../../../../../../../plugins/alerting/server/types'; +import { RuleAlertType } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; type PromiseFromStreams = ImportRuleAlertRest | Error; describe('utils', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('transformAlertToRule', () => { test('should work with a full data set', () => { const fullRule = getResult(); const rule = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(rule).toEqual(expected); + expect(rule).toEqual(getOutputRuleAlertForRest()); }); test('should work with a partial data set missing data', () => { const fullRule = getResult(); - const { from, language, ...omitData } = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(omitData).toEqual(expected); + const { from, language, ...omitParams } = fullRule.params; + fullRule.params = omitParams as RuleTypeParams; + const rule = transformAlertToRule(fullRule); + const { + from: from2, + language: language2, + ...expectedWithoutFromWithoutLanguage + } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutFromWithoutLanguage); }); test('should omit query if query is null', () => { const fullRule = getResult(); fullRule.params.query = null; const rule = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(rule).toEqual(expected); + const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutQuery); }); test('should omit query if query is undefined', () => { const fullRule = getResult(); fullRule.params.query = undefined; const rule = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - output_index: '.siem-signals', - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(rule).toEqual(expected); + const { query, ...expectedWithoutQuery } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutQuery); }); test('should omit a mix of undefined, null, and missing fields', () => { const fullRule = getResult(); fullRule.params.query = undefined; fullRule.params.language = null; - const { from, enabled, ...omitData } = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - max_signals: 100, - name: 'Detect Root/Admin Users', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; - expect(omitData).toEqual(expected); + const { from, ...omitParams } = fullRule.params; + fullRule.params = omitParams as RuleTypeParams; + const { enabled, ...omitEnabled } = fullRule; + const rule = transformAlertToRule(omitEnabled as RuleAlertType); + const { + from: from2, + enabled: enabled2, + language, + query, + ...expectedWithoutFromEnabledLanguageQuery + } = getOutputRuleAlertForRest(); + expect(rule).toEqual(expectedWithoutFromEnabledLanguageQuery); }); test('should return enabled is equal to false', () => { const fullRule = getResult(); fullRule.enabled = false; const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: false, - from: 'now-6m', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); + expected.enabled = false; expect(ruleWithEnabledFalse).toEqual(expected); }); @@ -424,65 +109,7 @@ describe('utils', () => { const fullRule = getResult(); fullRule.params.immutable = false; const ruleWithEnabledFalse = transformAlertToRule(fullRule); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - from: 'now-6m', - false_positives: [], - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - risk_score: 50, - rule_id: 'rule-1', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); expect(ruleWithEnabledFalse).toEqual(expected); }); @@ -490,65 +117,8 @@ describe('utils', () => { const fullRule = getResult(); fullRule.tags = ['tag 1', 'tag 2', `${INTERNAL_IDENTIFIER}_some_other_value`]; const rule = transformAlertToRule(fullRule); - const expected: Partial = { - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: ['tag 1', 'tag 2'], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); + expected.tags = ['tag 1', 'tag 2']; expect(rule).toEqual(expected); }); @@ -656,65 +226,7 @@ describe('utils', () => { total: 0, data: [getResult()], }); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); expect(output).toEqual({ page: 1, perPage: 0, @@ -738,65 +250,7 @@ describe('utils', () => { describe('transform', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transform(getResult()); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); expect(output).toEqual(expected); }); @@ -911,65 +365,7 @@ describe('utils', () => { describe('transformOrBulkError', () => { test('outputs 200 if the data is of type siem alert', () => { const output = transformOrBulkError('rule-1', getResult()); - const expected: Partial = { - created_by: 'elastic', - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - output_index: '.siem-signals', - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - rule_id: 'rule-1', - risk_score: 50, - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'query', - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - filters: [ - { - query: { - match_phrase: { - 'host.name': 'some-host', - }, - }, - }, - ], - meta: { - someMeta: 'someField', - }, - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - note: '# Investigative notes', - version: 1, - }; + const expected = getOutputRuleAlertForRest(); expect(output).toEqual(expected); }); @@ -1033,57 +429,8 @@ describe('utils', () => { test('given single alert will return the alert transformed', () => { const result1 = getResult(); const transformed = transformAlertsToRules([result1]); - expect(transformed).toEqual([ - { - created_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { someMeta: 'someField' }, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - risk_score: 50, - rule_id: 'rule-1', - severity: 'high', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - updated_at: '2019-12-13T16:40:33.400Z', - updated_by: 'elastic', - note: '# Investigative notes', - version: 1, - }, - ]); + const expected = getOutputRuleAlertForRest(); + expect(transformed).toEqual([expected]); }); test('given two alerts will return the two alerts transformed', () => { @@ -1093,106 +440,11 @@ describe('utils', () => { result2.params.ruleId = 'some other id'; const transformed = transformAlertsToRules([result1, result2]); - expect(transformed).toEqual([ - { - created_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { someMeta: 'someField' }, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - risk_score: 50, - rule_id: 'rule-1', - severity: 'high', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - updated_at: '2019-12-13T16:40:33.400Z', - updated_by: 'elastic', - note: '# Investigative notes', - version: 1, - }, - { - created_at: '2019-12-13T16:40:33.400Z', - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], - from: 'now-6m', - id: 'some other id', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - language: 'kuery', - max_signals: 100, - meta: { someMeta: 'someField' }, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - risk_score: 50, - rule_id: 'some other id', - severity: 'high', - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: 'TA0040', - name: 'impact', - reference: 'https://attack.mitre.org/tactics/TA0040/', - }, - technique: [ - { - id: 'T1499', - name: 'endpoint denial of service', - reference: 'https://attack.mitre.org/techniques/T1499/', - }, - ], - }, - ], - timeline_id: 'some-timeline-id', - timeline_title: 'some-timeline-title', - to: 'now', - type: 'query', - updated_at: '2019-12-13T16:40:33.400Z', - updated_by: 'elastic', - note: '# Investigative notes', - version: 1, - }, - ]); + const expected1 = getOutputRuleAlertForRest(); + const expected2 = getOutputRuleAlertForRest(); + expected2.id = 'some other id'; + expected2.rule_id = 'some other id'; + expect(transformed).toEqual([expected1, expected2]); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts index abd8dd7e87f03..fe7618bca0c75 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/utils.ts @@ -28,6 +28,7 @@ import { createImportErrorObject, OutputError, } from '../utils'; +import { hasListsFeature } from '../../feature_flags'; type PromiseFromStreams = ImportRuleAlertRest | Error; @@ -141,6 +142,8 @@ export const transformAlertToRule = ( last_success_at: ruleStatus?.attributes.lastSuccessAt, last_failure_message: ruleStatus?.attributes.lastFailureMessage, last_success_message: ruleStatus?.attributes.lastSuccessMessage, + // TODO: (LIST-FEATURE) Remove hasListsFeature() check once we have lists available for a release + lists: hasListsFeature() ? alert.params.lists : null, }); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts index ba6c702e9601b..1dce602f3fcac 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/validate.test.ts @@ -16,6 +16,7 @@ import { getResult } from '../__mocks__/request_responses'; import { FindResult } from '../../../../../../../../plugins/alerting/server'; import { RulesSchema } from '../schemas/response/rules_schema'; import { BulkError } from '../utils'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; export const ruleOutput: RulesSchema = { created_at: '2019-12-13T16:40:33.400Z', @@ -68,6 +69,32 @@ export const ruleOutput: RulesSchema = { }, }, ], + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], meta: { someMeta: 'someField', @@ -78,6 +105,14 @@ export const ruleOutput: RulesSchema = { }; describe('validate', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('validate', () => { test('it should do a validation correctly', () => { const schema = t.exact(t.type({ a: t.number })); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index a002cc9324012..171a34f0d0592 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -6,8 +6,17 @@ import { ThreatParams, PrepackagedRules } from '../../types'; import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('add prepackaged rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate', () => { expect(addPrepackagedRulesSchema.validate>({}).error).toBeTruthy(); }); @@ -1332,4 +1341,116 @@ describe('add prepackaged rules schema', () => { ).toEqual('child "note" fails because ["note" must be a string]'); }); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + addPrepackagedRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + version: 1, + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + expect( + addPrepackagedRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + version: 1, + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + expect( + addPrepackagedRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + version: 1, + }).value.lists + ).toEqual([]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts index ec0a8e7871b5b..4c60a66141250 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.ts @@ -34,12 +34,14 @@ import { references, note, version, + lists, anomaly_threshold, machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import { hasListsFeature } from '../../feature_flags'; /** * Big differences between this schema and the createRulesSchema @@ -102,4 +104,7 @@ export const addPrepackagedRulesSchema = Joi.object({ references: references.default([]), note: note.allow(''), version: version.required(), + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts index 6512bfdc4361f..fa007bba6551a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_bulk_schema.test.ts @@ -6,11 +6,20 @@ import { createRulesBulkSchema } from './create_rules_bulk_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; // only the basics of testing are here. // see: create_rules_schema.test.ts for the bulk of the validation tests // this just wraps createRulesSchema in an array describe('create_rules_bulk_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('can take an empty array and validate it', () => { expect( createRulesBulkSchema.validate>>([]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 3bad87dc1a9ad..db5097a6f25db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -7,8 +7,17 @@ import { createRulesSchema } from './create_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { ThreatParams, RuleAlertParamsRest } from '../../types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate', () => { expect(createRulesSchema.validate>({}).error).toBeTruthy(); }); @@ -1314,5 +1323,113 @@ describe('create rules schema', () => { }).error ).toBeFalsy(); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + createRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + expect( + createRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + expect( + createRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).value.lists + ).toEqual([]); + }); + }); }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts index e86963fd4594c..0aa7317dd8cdc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.ts @@ -35,11 +35,13 @@ import { references, note, version, + lists, machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import { hasListsFeature } from '../../feature_flags'; export const createRulesSchema = Joi.object({ anomaly_threshold: anomaly_threshold.when('type', { @@ -90,4 +92,7 @@ export const createRulesSchema = Joi.object({ references: references.default([]), note: note.allow(''), version: version.default(1), + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts index 621dcd8fa8ed4..0e71237f75232 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/export_rules_schema.test.ts @@ -6,8 +6,17 @@ import { exportRulesSchema, exportRulesQuerySchema } from './export_rules_schema'; import { ExportRulesRequestParams } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('exportRulesSchema', () => { test('null value or absent values validate', () => { expect(exportRulesSchema.validate(null).error).toBeFalsy(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts index 339874e19c33a..ffbfd193873a8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/find_rules_schema.test.ts @@ -6,8 +6,17 @@ import { findRulesSchema } from './find_rules_schema'; import { FindParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('find rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do validate', () => { expect(findRulesSchema.validate>({}).error).toBeFalsy(); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index 9c80ddde9e7b7..bcb24268fc6c7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -11,8 +11,17 @@ import { } from './import_rules_schema'; import { ThreatParams, ImportRuleAlertRest } from '../../types'; import { ImportRulesRequestParams } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('import rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('importRulesSchema', () => { test('empty objects do not validate', () => { expect(importRulesSchema.validate>({}).error).toBeTruthy(); @@ -1535,4 +1544,112 @@ describe('import rules schema', () => { ).toEqual('child "note" fails because ["note" must be a string]'); }); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + importRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate and lists is empty', () => { + expect( + importRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate', () => { + expect( + importRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).value.lists + ).toEqual([]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts index 92718b7ae71ba..469b59a8e08ad 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.ts @@ -40,12 +40,14 @@ import { references, note, version, + lists, anomaly_threshold, machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import { hasListsFeature } from '../../feature_flags'; /** * Differences from this and the createRulesSchema are @@ -111,6 +113,9 @@ export const importRulesSchema = Joi.object({ updated_at, created_by, updated_by, + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }); export const importRulesQuerySchema = Joi.object({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts index 43d1e7ab2aa3b..e87c732e8a2f7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_bulk_schema.test.ts @@ -6,11 +6,20 @@ import { patchRulesBulkSchema } from './patch_rules_bulk_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; // only the basics of testing are here. // see: patch_rules_schema.test.ts for the bulk of the validation tests // this just wraps patchRulesSchema in an array describe('patch_rules_bulk_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('can take an empty array and validate it', () => { expect( patchRulesBulkSchema.validate>>([]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts index ecdba7ccc0091..6fc1a0c3caa9c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.test.ts @@ -7,8 +7,17 @@ import { patchRulesSchema } from './patch_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { ThreatParams } from '../../types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('patch rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate as they require at least id or rule_id', () => { expect(patchRulesSchema.validate>({}).error).toBeTruthy(); }); @@ -1053,4 +1062,146 @@ describe('patch rules schema', () => { ).toEqual('child "note" fails because ["note" must be a string]'); }); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('lists can be patched', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'some id', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + patchRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + expect( + patchRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + expect( + patchRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).value.lists + ).toEqual([]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts index 4496a808f6869..8bb155d83cf44 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/patch_rules_schema.ts @@ -35,9 +35,11 @@ import { note, id, version, + lists, anomaly_threshold, machine_learning_job_id, } from './schemas'; +import { hasListsFeature } from '../../feature_flags'; /* eslint-enable @typescript-eslint/camelcase */ export const patchRulesSchema = Joi.object({ @@ -70,4 +72,7 @@ export const patchRulesSchema = Joi.object({ references, note: note.allow(''), version, + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts index 7ea7fcbd1d86b..389c5ff7ea617 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_bulk_schema.test.ts @@ -6,11 +6,20 @@ import { queryRulesBulkSchema } from './query_rules_bulk_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; // only the basics of testing are here. // see: query_rules_bulk_schema.test.ts for the bulk of the validation tests // this just wraps queryRulesSchema in an array describe('query_rules_bulk_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('can take an empty array and validate it', () => { expect( queryRulesBulkSchema.validate>>([]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts index 0f392e399f36c..68be4c627780c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_rules_schema.test.ts @@ -6,8 +6,17 @@ import { queryRulesSchema } from './query_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('queryRulesSchema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate', () => { expect(queryRulesSchema.validate>({}).error).toBeTruthy(); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts index 5c293f4825b95..4752d1794ff28 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/query_signals_index_schema.test.ts @@ -6,8 +6,17 @@ import { querySignalsSchema } from './query_signals_index_schema'; import { SignalsQueryRestParams } from '../../signals/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('query, aggs, size, _source and track_total_hits on signals index', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('query, aggs, size, _source and track_total_hits simultaneously', () => { expect( querySignalsSchema.validate>({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts index dd88bd80d5787..46cd1b653b5b4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/__mocks__/utils.ts @@ -63,6 +63,32 @@ export const getBaseResponsePayload = (anchorDate: string = ANCHOR_DATE): RulesS language: 'kuery', rule_id: 'query-rule-id', interval: '5m', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }); export const getRulesBulkPayload = (): RulesBulkSchema => [getBaseResponsePayload()]; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts index 1a5ee793a25da..0eda2a7a13d96 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.test.ts @@ -25,8 +25,17 @@ import { left } from 'fp-ts/lib/Either'; import { exactCheck } from './exact_check'; import { RulesSchema } from './rules_schema'; import { TypeAndTimelineOnly } from './type_timeline_only_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('check_type_dependents', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('checkTypeDependents', () => { test('it should validate a type of "query" without anything extra', () => { const payload = getBaseResponsePayload(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts index 9708c928870f5..11d8b85f25920 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/error_schema.test.ts @@ -10,8 +10,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck } from './exact_check'; import { foldLeftRight, getErrorPayload, getPaths } from './__mocks__/utils'; import { errorSchema, ErrorSchema } from './error_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('error_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate an error with a UUID given for id', () => { const error = getErrorPayload(); const decoded = errorSchema.decode(getErrorPayload()); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts index d01c5e19d4322..cae4365d06856 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/exact_check.test.ts @@ -10,8 +10,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from './__mocks__/utils'; import { exactCheck, findDifferencesRecursive } from './exact_check'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('exact_check', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it returns an error if given extra object properties', () => { const someType = t.exact( t.type({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts index 937af223b91ab..f5c1970ee8c55 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/find_rules_schema.test.ts @@ -15,8 +15,17 @@ import { } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { RulesSchema } from './rules_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('find_rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate a typical single find rules response', () => { const payload = getFindResponseSingle(); const decoded = findRulesSchema.decode(payload); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts index 62ffcd527eea8..ce4bbf420a634 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/import_rules_schema.test.ts @@ -10,8 +10,17 @@ import { foldLeftRight, getPaths } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { ImportRulesSchema, importRulesSchema } from './import_rules_schema'; import { ErrorSchema } from './error_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('import_rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate an empty import response with no errors', () => { const payload: ImportRulesSchema = { success: true, success_count: 0, errors: [] }; const decoded = importRulesSchema.decode(payload); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts index 7f9b296e2d466..46667826416e1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_schema.test.ts @@ -9,8 +9,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { foldLeftRight, getPaths } from './__mocks__/utils'; import { left } from 'fp-ts/lib/Either'; import { PrePackagedRulesSchema, prePackagedRulesSchema } from './prepackaged_rules_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('prepackaged_rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate an empty prepackaged response with defaults', () => { const payload: PrePackagedRulesSchema = { rules_installed: 0, rules_updated: 0 }; const decoded = prePackagedRulesSchema.decode(payload); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts index 9d44e09e847a0..1c270ff402f75 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/prepackaged_rules_status_schema.test.ts @@ -12,8 +12,17 @@ import { PrePackagedRulesStatusSchema, prePackagedRulesStatusSchema, } from './prepackaged_rules_status_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('prepackaged_rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate an empty prepackaged response with defaults', () => { const payload: PrePackagedRulesStatusSchema = { rules_installed: 0, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts index c2f346cacc43e..8dc97d727c4d1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_bulk_schema.test.ts @@ -17,8 +17,17 @@ import { import { RulesBulkSchema, rulesBulkSchema } from './rules_bulk_schema'; import { RulesSchema } from './rules_schema'; import { ErrorSchema } from './error_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('prepackaged_rule_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate a regular message and and error together with a uuid', () => { const payload: RulesBulkSchema = [getBaseResponsePayload(), getErrorPayload()]; const decoded = rulesBulkSchema.decode(payload); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts index a2594ffa21c45..fb9ff2c28dc44 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.test.ts @@ -8,12 +8,21 @@ import { left } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck } from './exact_check'; -import { rulesSchema, RulesSchema } from './rules_schema'; +import { rulesSchema, RulesSchema, removeList } from './rules_schema'; import { foldLeftRight, getBaseResponsePayload, getPaths } from './__mocks__/utils'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; export const ANCHOR_DATE = '2020-02-20T03:57:54.037Z'; describe('rules_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate a type of "query" without anything extra', () => { const payload = getBaseResponsePayload(); @@ -196,4 +205,84 @@ describe('rules_schema', () => { ]); expect(message.schema).toEqual({}); }); + + // TODO: (LIST-FEATURE) Remove this test once the feature flag is deployed + test('it should remove lists when we need it to be removed because the feature is off but there exists a list in the data', () => { + const payload = getBaseResponsePayload(); + const decoded = rulesSchema.decode(payload); + const listRemoved = removeList(decoded); + const message = pipe(listRemoved, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: '2020-02-20T03:57:54.037Z', + updated_at: '2020-02-20T03:57:54.037Z', + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + updated_by: 'elastic_kibana', + tags: [], + to: 'now', + type: 'query', + threat: [], + version: 1, + output_index: '.siem-signals-hassanabad-frank-default', + max_signals: 100, + risk_score: 55, + language: 'kuery', + rule_id: 'query-rule-id', + interval: '5m', + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + }); + }); + + test('it should work with lists that are not there and not cause invalidation or errors', () => { + const payload = getBaseResponsePayload(); + const { lists, ...payloadWithoutLists } = payload; + const decoded = rulesSchema.decode(payloadWithoutLists); + const listRemoved = removeList(decoded); + const message = pipe(listRemoved, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual({ + id: '7a7065d7-6e8b-4aae-8d20-c93613dec9f9', + created_at: '2020-02-20T03:57:54.037Z', + updated_at: '2020-02-20T03:57:54.037Z', + created_by: 'elastic', + description: 'some description', + enabled: true, + false_positives: ['false positive 1', 'false positive 2'], + from: 'now-6m', + immutable: false, + name: 'Query with a rule id', + query: 'user.name: root or user.name: admin', + references: ['test 1', 'test 2'], + severity: 'high', + updated_by: 'elastic_kibana', + tags: [], + to: 'now', + type: 'query', + threat: [], + version: 1, + output_index: '.siem-signals-hassanabad-frank-default', + max_signals: 100, + risk_score: 55, + language: 'kuery', + rule_id: 'query-rule-id', + interval: '5m', + status: 'succeeded', + status_date: '2020-02-22T16:47:50.047Z', + last_success_at: '2020-02-22T16:47:50.047Z', + last_success_message: 'succeeded', + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts index 28b588a86aeb0..75de97a55534b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/rules_schema.ts @@ -7,8 +7,9 @@ /* eslint-disable @typescript-eslint/camelcase */ import * as t from 'io-ts'; import { isObject } from 'lodash/fp'; -import { Either } from 'fp-ts/lib/Either'; +import { Either, fold, right, left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; import { checkTypeDependents } from './check_type_dependents'; import { anomaly_threshold, @@ -52,6 +53,8 @@ import { meta, note, } from './schemas'; +import { ListsDefaultArray } from '../types/lists_default_array'; +import { hasListsFeature } from '../../../feature_flags'; /** * This is the required fields for the rules schema response. Put all required properties on @@ -82,6 +85,7 @@ export const requiredRulesSchema = t.type({ updated_at, created_by, version, + lists: ListsDefaultArray, }); export type RequiredRulesSchema = t.TypeOf; @@ -147,11 +151,30 @@ export const rulesSchema = new t.Type< 'RulesSchema', (input: unknown): input is RulesWithoutTypeDependentsSchema => isObject(input), (input): Either => { - return checkTypeDependents(input); + const output = checkTypeDependents(input); + if (!hasListsFeature()) { + // TODO: (LIST-FEATURE) Remove this after the lists feature is an accepted feature for a particular release + return removeList(output); + } else { + return output; + } }, t.identity ); +// TODO: (LIST-FEATURE) Remove this after the lists feature is an accepted feature for a particular release +export const removeList = ( + decoded: Either +): Either => { + const onLeft = (errors: t.Errors): Either => left(errors); + const onRight = (decodedValue: RequiredRulesSchema): Either => { + delete decodedValue.lists; + return right(decodedValue); + }; + const folded = fold(onLeft, onRight); + return pipe(decoded, folded); +}; + /** * This is the correct type you want to use for Rules that are outputted from the * REST interface. This has all base and all optional properties merged together. diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts index 072e3f5beefe2..d90cb7b1f0829 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/schemas.ts @@ -131,3 +131,16 @@ export const rules_custom_installed = PositiveInteger; export const rules_not_installed = PositiveInteger; export const rules_not_updated = PositiveInteger; export const note = t.string; + +// NOTE: Experimental list support not being shipped currently and behind a feature flag +// TODO: Remove this comment once we lists have passed testing and is ready for the release +export const boolean_operator = t.keyof({ and: null, 'and not': null }); +export const list_type = t.keyof({ value: null }); // TODO: (LIST-FEATURE) Eventually this can include "list" when we support lists CRUD +export const list_value = t.exact(t.type({ name: t.string, type: list_type })); +export const list = t.exact( + t.type({ + field: t.string, + boolean_operator, + values: t.array(list_value), + }) +); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts index 219cd68d3a2a1..68a3c8b303823 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/type_timeline_only_schema.test.ts @@ -10,8 +10,17 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { exactCheck } from './exact_check'; import { foldLeftRight, getPaths } from './__mocks__/utils'; import { TypeAndTimelineOnly, typeAndTimelineOnlySchema } from './type_timeline_only_schema'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('prepackaged_rule_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it should validate a a type and timeline_id together', () => { const payload: TypeAndTimelineOnly = { type: 'query', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts index cd223c24792bf..c1eb32be4895c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/response/utils.test.ts @@ -6,8 +6,17 @@ import * as t from 'io-ts'; import { formatErrors } from './utils'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../../feature_flags'; describe('utils', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('returns an empty error message string if there are no errors', () => { const errors: t.Errors = []; const output = formatErrors(errors); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts index ad7050e8dd65c..007294293f59b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/schemas.ts @@ -111,3 +111,15 @@ export const version = Joi.number() .integer() .min(1); export const note = Joi.string(); + +// NOTE: Experimental list support not being shipped currently and behind a feature flag +// TODO: (LIST-FEATURE) Remove this comment once we lists have passed testing and is ready for the release +export const boolean_operator = Joi.string().valid('and', 'and not'); +export const list_type = Joi.string().valid('value'); // TODO: (LIST-FEATURE) Eventually this can be "list" when we support list types +export const list_value = Joi.object({ name: Joi.string().required(), type: list_type.required() }); +export const list = Joi.object({ + field: Joi.string().required(), + boolean_operator: boolean_operator.required(), + values: Joi.array().items(list_value), +}); +export const lists = Joi.array().items(list); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts index a6ba9b19a9d7d..953532a6e1c26 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/set_signal_status_schema.test.ts @@ -6,8 +6,17 @@ import { setSignalsStatusSchema } from './set_signal_status_schema'; import { SignalsStatusRestParams } from '../../signals/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('set signal status schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('signal_ids and status is valid', () => { expect( setSignalsStatusSchema.validate>({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts new file mode 100644 index 0000000000000..14df1c3d8cd55 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.test.ts @@ -0,0 +1,85 @@ +/* + * 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 { ListsDefaultArray } from './lists_default_array'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { foldLeftRight, getPaths } from '../response/__mocks__/utils'; +import { left } from 'fp-ts/lib/Either'; + +describe('lists_default_array', () => { + test('it should validate an empty array', () => { + const payload: string[] = []; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should validate an array of lists', () => { + const payload = [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should not validate an array with a number', () => { + const payload = [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + 5, + ]; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual(['Invalid value "5" supplied to ""']); + expect(message.schema).toEqual({}); + }); + + test('it should return a default array entry', () => { + const payload = null; + const decoded = ListsDefaultArray.decode(payload); + const message = pipe(decoded, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual([]); + }); +}); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts new file mode 100644 index 0000000000000..0e0944a11b416 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/types/lists_default_array.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { Either } from 'fp-ts/lib/Either'; + +import { list } from '../response/schemas'; + +export type ListsDefaultArrayC = t.Type; +type List = t.TypeOf; + +/** + * Types the ListsDefaultArray as: + * - If null or undefined, then a default array will be set for the list + */ +export const ListsDefaultArray: ListsDefaultArrayC = new t.Type( + 'listsWithDefaultArray', + t.array(list).is, + (input): Either => + input == null ? t.success([]) : t.array(list).decode(input), + t.identity +); + +export type ListsDefaultArraySchema = t.TypeOf; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts index e866260662ad7..d329070eaaa0a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_bulk_schema.test.ts @@ -6,11 +6,20 @@ import { updateRulesBulkSchema } from './update_rules_bulk_schema'; import { UpdateRuleAlertParamsRest } from '../../rules/types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; // only the basics of testing are here. // see: update_rules_schema.test.ts for the bulk of the validation tests // this just wraps updateRulesSchema in an array describe('update_rules_bulk_schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('can take an empty array and validate it', () => { expect( updateRulesBulkSchema.validate>>([]).error diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts index e37abf3746ae6..a0689966a8694 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.test.ts @@ -7,8 +7,17 @@ import { updateRulesSchema } from './update_rules_schema'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { ThreatParams, RuleAlertParamsRest } from '../../types'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('create rules schema', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('empty objects do not validate as they require at least id or rule_id', () => { expect(updateRulesSchema.validate>({}).error).toBeTruthy(); }); @@ -1340,4 +1349,112 @@ describe('create rules schema', () => { ).toEqual('child "note" fails because ["note" must be a string]'); }); }); + + // TODO: (LIST-FEATURE) We can enable this once we change the schema's to not be global per module but rather functions that can create the schema + // on demand. Since they are per module, we have a an issue where the ENV variables do not take effect. It is better we change all the + // schema's to be function calls to avoid global side effects or just wait until the feature is available. If you want to test this early, + // you can remove the .skip and set your env variable of export ELASTIC_XPACK_SIEM_LISTS_FEATURE=true locally + describe.skip('lists', () => { + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and lists] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and empty lists] does validate', () => { + expect( + updateRulesSchema.validate>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [], + }).error + ).toBeFalsy(); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and invalid lists] does NOT validate', () => { + expect( + updateRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + lists: [{ invalid_value: 'invalid value' }], + }).error.message + ).toEqual( + 'child "lists" fails because ["lists" at position 0 fails because [child "field" fails because ["field" is required]]]' + ); + }); + + test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, note, and non-existent lists] does validate with empty lists', () => { + expect( + updateRulesSchema.validate>>({ + rule_id: 'rule-1', + description: 'some description', + from: 'now-5m', + to: 'now', + index: ['index-1'], + name: 'some-name', + severity: 'low', + interval: '5m', + type: 'query', + risk_score: 50, + note: '# some markdown', + }).value.lists + ).toEqual([]); + }); + }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts index f7a53385200df..421172cf0b1a1 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/update_rules_schema.ts @@ -35,12 +35,14 @@ import { id, note, version, + lists, anomaly_threshold, machine_learning_job_id, } from './schemas'; /* eslint-enable @typescript-eslint/camelcase */ import { DEFAULT_MAX_SIGNALS } from '../../../../../common/constants'; +import { hasListsFeature } from '../../feature_flags'; /** * This almost identical to the create_rules_schema except for a few details. @@ -99,4 +101,7 @@ export const updateRulesSchema = Joi.object({ references: references.default([]), note: note.allow(''), version, + + // TODO: (LIST-FEATURE) Remove the hasListsFeatures once this is ready for release + lists: hasListsFeature() ? lists.default([]) : lists.forbidden().default([]), }).xor('id', 'rule_id'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts index b189eac186a78..612d08c09785a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals.test.ts @@ -15,8 +15,17 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { setSignalsStatusRoute } from './open_close_signals_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('set signal status', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts index dcbb7b8e1fe44..8d7b171a8537b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.test.ts @@ -15,8 +15,17 @@ import { } from '../__mocks__/request_responses'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { querySignalsRoute } from './query_signals_route'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; describe('query for signal', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + let server: ReturnType; let { clients, context } = requestContextMock.createTools(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 6768e9534a87e..fdb1cd148c7fa 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -21,8 +21,17 @@ import { SiemResponseFactory, } from './utils'; import { responseMock } from './__mocks__'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; describe('utils', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + describe('transformError', () => { test('returns transformed output error from boom object with a 500 and payload of internal server error', () => { const boom = new Boom('some boom message'); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts index 1b4c06fb5d828..0bf9d17d70fdc 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules.ts @@ -8,6 +8,7 @@ import { Alert } from '../../../../../../../plugins/alerting/common'; import { APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { CreateRuleParams } from './types'; import { addTags } from './add_tags'; +import { hasListsFeature } from '../feature_flags'; export const createRules = ({ alertsClient, @@ -41,7 +42,10 @@ export const createRules = ({ references, note, version, + lists, }: CreateRuleParams): Promise => { + // TODO: Remove this and use regular lists once the feature is stable for a release + const listsParam = hasListsFeature() ? { lists } : {}; return alertsClient.create({ data: { name, @@ -74,6 +78,7 @@ export const createRules = ({ references, note, version, + ...listsParam, }, schedule: { interval }, enabled, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts index 8705682f61bcc..3ed4408138833 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/create_rules_stream_from_ndjson.test.ts @@ -65,6 +65,7 @@ describe('create_rules_stream_from_ndjson', () => { immutable: false, query: '', language: 'kuery', + lists: [], max_signals: 100, tags: [], threat: [], @@ -88,6 +89,7 @@ describe('create_rules_stream_from_ndjson', () => { immutable: false, query: '', language: 'kuery', + lists: [], max_signals: 100, tags: [], threat: [], @@ -151,6 +153,7 @@ describe('create_rules_stream_from_ndjson', () => { language: 'kuery', max_signals: 100, tags: [], + lists: [], threat: [], references: [], version: 1, @@ -173,6 +176,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -217,6 +221,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -240,6 +245,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -284,6 +290,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -308,6 +315,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -351,6 +359,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], @@ -377,6 +386,7 @@ describe('create_rules_stream_from_ndjson', () => { query: '', language: 'kuery', max_signals: 100, + lists: [], tags: [], threat: [], references: [], diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts index 39b596dfed855..532bfbaf469ff 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_all.test.ts @@ -11,8 +11,17 @@ import { } from '../routes/__mocks__/request_responses'; import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; import { getExportAll } from './get_export_all'; +import { unSetFeatureFlagsForTestsOnly, setFeatureFlagsForTestsOnly } from '../feature_flags'; describe('getExportAll', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + test('it exports everything from the alerts client', async () => { const alertsClient = alertsClientMock.create(); alertsClient.get.mockResolvedValue(getResult()); @@ -20,9 +29,86 @@ describe('getExportAll', () => { const exports = await getExportAll(alertsClient); expect(exports).toEqual({ - rulesNdjson: - '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"note":"# Investigative notes","version":1}\n', - exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', + rulesNdjson: `${JSON.stringify({ + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals', + max_signals: 100, + risk_score: 50, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + meta: { someMeta: 'someField' }, + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + note: '# Investigative notes', + version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + })}\n`, + exportDetails: `${JSON.stringify({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + })}\n`, }); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 1406c7c9000b2..f27299436c702 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -12,8 +12,17 @@ import { } from '../routes/__mocks__/request_responses'; import * as readRules from './read_rules'; import { alertsClientMock } from '../../../../../../../plugins/alerting/server/mocks'; +import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; describe('get_export_by_object_ids', () => { + beforeAll(() => { + setFeatureFlagsForTestsOnly(); + }); + + afterAll(() => { + unSetFeatureFlagsForTestsOnly(); + }); + beforeEach(() => { jest.resetAllMocks(); jest.restoreAllMocks(); @@ -28,9 +37,86 @@ describe('get_export_by_object_ids', () => { const objects = [{ rule_id: 'rule-1' }]; const exports = await getExportByObjectIds(alertsClient, objects); expect(exports).toEqual({ - rulesNdjson: - '{"created_at":"2019-12-13T16:40:33.400Z","updated_at":"2019-12-13T16:40:33.400Z","created_by":"elastic","description":"Detecting root and admin users","enabled":true,"false_positives":[],"filters":[{"query":{"match_phrase":{"host.name":"some-host"}}}],"from":"now-6m","id":"04128c15-0d1b-4716-a4c5-46997ac7f3bd","immutable":false,"index":["auditbeat-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"rule-1","language":"kuery","output_index":".siem-signals","max_signals":100,"risk_score":50,"name":"Detect Root/Admin Users","query":"user.name: root or user.name: admin","references":["http://www.example.com","https://ww.example.com"],"timeline_id":"some-timeline-id","timeline_title":"some-timeline-title","meta":{"someMeta":"someField"},"severity":"high","updated_by":"elastic","tags":[],"to":"now","type":"query","threat":[{"framework":"MITRE ATT&CK","tactic":{"id":"TA0040","name":"impact","reference":"https://attack.mitre.org/tactics/TA0040/"},"technique":[{"id":"T1499","name":"endpoint denial of service","reference":"https://attack.mitre.org/techniques/T1499/"}]}],"note":"# Investigative notes","version":1}\n', - exportDetails: '{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n', + rulesNdjson: `${JSON.stringify({ + created_at: '2019-12-13T16:40:33.400Z', + updated_at: '2019-12-13T16:40:33.400Z', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: true, + false_positives: [], + filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: '5m', + rule_id: 'rule-1', + language: 'kuery', + output_index: '.siem-signals', + max_signals: 100, + risk_score: 50, + name: 'Detect Root/Admin Users', + query: 'user.name: root or user.name: admin', + references: ['http://www.example.com', 'https://ww.example.com'], + timeline_id: 'some-timeline-id', + timeline_title: 'some-timeline-title', + meta: { someMeta: 'someField' }, + severity: 'high', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'query', + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: 'TA0040', + name: 'impact', + reference: 'https://attack.mitre.org/tactics/TA0040/', + }, + technique: [ + { + id: 'T1499', + name: 'endpoint denial of service', + reference: 'https://attack.mitre.org/techniques/T1499/', + }, + ], + }, + ], + note: '# Investigative notes', + version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], + })}\n`, + exportDetails: `${JSON.stringify({ + exported_count: 1, + missing_rules: [], + missing_rules_count: 0, + })}\n`, }); }); @@ -119,6 +205,32 @@ describe('get_export_by_object_ids', () => { ], note: '# Investigative notes', version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, ], }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index dc71ae3678f2e..bcbe460fb6a66 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -46,6 +46,7 @@ export const installPrepackagedRules = ( references, note, version, + lists, } = rule; return [ ...acc, @@ -81,6 +82,7 @@ export const installPrepackagedRules = ( references, note, version, + lists, }), ]; }, []); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index 628f4033d5665..4fb73235854c0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -45,6 +45,7 @@ export const patchRules = async ({ note, version, throttle, + lists, }: PatchRuleParams): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -77,6 +78,7 @@ export const patchRules = async ({ version, throttle, note, + lists, }); const nextParams = defaults( @@ -106,6 +108,7 @@ export const patchRules = async ({ references, note, version: calculatedVersion, + lists, } ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 3987654589bdd..b2a1d2a6307d2 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -10,6 +10,7 @@ import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './t import { addTags } from './add_tags'; import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion } from './utils'; +import { hasListsFeature } from '../feature_flags'; export const updateRules = async ({ alertsClient, @@ -44,6 +45,7 @@ export const updateRules = async ({ version, throttle, note, + lists, }: UpdateRuleParams): Promise => { const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { @@ -78,6 +80,9 @@ export const updateRules = async ({ note, }); + // TODO: Remove this and use regular lists once the feature is stable for a release + const listsParam = hasListsFeature() ? { lists } : {}; + const update = await alertsClient.update({ id: rule.id, data: { @@ -110,6 +115,7 @@ export const updateRules = async ({ references, note, version: calculatedVersion, + ...listsParam, }, }, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json new file mode 100644 index 0000000000000..8c86f4c85af1d --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/patches/update_list.json @@ -0,0 +1,25 @@ +{ + "rule_id": "query-with-list", + "lists": [ + { + "field": "source.ip", + "boolean_operator": "and", + "values": [ + { + "name": "127.0.0.1", + "type": "value" + } + ] + }, + { + "field": "host.name", + "boolean_operator": "and not", + "values": [ + { + "name": "rock01", + "type": "value" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json new file mode 100644 index 0000000000000..f6856eec59966 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/queries/query_with_list.json @@ -0,0 +1,35 @@ +{ + "name": "Query with a list", + "description": "Query with a list", + "rule_id": "query-with-list", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", + "lists": [ + { + "field": "source.ip", + "boolean_operator": "and", + "values": [ + { + "name": "127.0.0.1", + "type": "value" + } + ] + }, + { + "field": "host.name", + "boolean_operator": "and not", + "values": [ + { + "name": "rock01", + "type": "value" + }, + { + "name": "mothra", + "type": "value" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json new file mode 100644 index 0000000000000..6704c9676fa56 --- /dev/null +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/scripts/rules/updates/update_list.json @@ -0,0 +1,31 @@ +{ + "name": "Query with a list", + "description": "Query with a list", + "rule_id": "query-with-list", + "risk_score": 1, + "severity": "high", + "type": "query", + "query": "user.name: root or user.name: admin", + "lists": [ + { + "field": "source.ip", + "boolean_operator": "and", + "values": [ + { + "name": "127.0.0.1", + "type": "value" + } + ] + }, + { + "field": "host.name", + "boolean_operator": "and not", + "values": [ + { + "name": "rock01", + "type": "value" + } + ] + } + ] +} diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 010f6b2ee98ff..31b922e0067cd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -38,6 +38,32 @@ export const sampleRuleAlertParams = ( meta: undefined, threat: undefined, version: 1, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }); export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts index 30dac114ac506..c30635c9d1490 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -86,6 +86,32 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, }, }; @@ -176,6 +202,32 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, }, }; @@ -264,6 +316,32 @@ describe('buildBulkBody', () => { version: 1, created_at: fakeSignalSourceHit.signal.rule?.created_at, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, }, }; @@ -345,6 +423,32 @@ describe('buildBulkBody', () => { version: 1, updated_at: fakeSignalSourceHit.signal.rule?.updated_at, created_at: fakeSignalSourceHit.signal.rule?.created_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }, }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts index c2900782ed676..499e3e9c88a85 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.test.ts @@ -75,6 +75,32 @@ describe('buildRule', () => { query: 'host.name: Braden', }, ], + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], version: 1, }; expect(rule).toEqual(expected); @@ -122,6 +148,32 @@ describe('buildRule', () => { version: 1, updated_at: rule.updated_at, created_at: rule.created_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }; expect(rule).toEqual(expected); }); @@ -168,6 +220,32 @@ describe('buildRule', () => { version: 1, updated_at: rule.updated_at, created_at: rule.created_at, + lists: [ + { + field: 'source.ip', + boolean_operator: 'and', + values: [ + { + name: '127.0.0.1', + type: 'value', + }, + ], + }, + { + field: 'host.name', + boolean_operator: 'and not', + values: [ + { + name: 'rock01', + type: 'value', + }, + { + name: 'mothra', + type: 'value', + }, + ], + }, + ], }; expect(rule).toEqual(expected); }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts index a9ccda2efe99c..a1bee162c9280 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/build_rule.ts @@ -65,6 +65,7 @@ export const buildRule = ({ version: ruleParams.version, created_at: createdAt, updated_at: updatedAt, + lists: ruleParams.lists, machine_learning_job_id: ruleParams.machineLearningJobId, anomaly_threshold: ruleParams.anomalyThreshold, }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts index 7b0546f56dd15..58dd53b6447c5 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_params_schema.ts @@ -39,4 +39,5 @@ export const signalParamsSchema = () => type: schema.string(), references: schema.arrayOf(schema.string(), { defaultValue: [] }), version: schema.number({ defaultValue: 1 }), + lists: schema.maybe(schema.arrayOf(schema.object({}, { unknowns: 'allow' }))), }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index f77924aafadf8..5973a1dbe5f18 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -7,6 +7,7 @@ import { CallAPIOptions } from '../../../../../../../src/core/server'; import { Filter } from '../../../../../../../src/plugins/data/server'; import { IRuleStatusAttributes } from './rules/types'; +import { ListsDefaultArraySchema } from './routes/schemas/types/lists_default_array'; export type PartialFilter = Partial; @@ -22,6 +23,10 @@ export interface ThreatParams { technique: IMitreAttack[]; } +// Notice below we are using lists: ListsDefaultArraySchema[]; which is coming directly from the response output section. +// TODO: Eventually this whole RuleAlertParams will be replaced with io-ts. For now we can slowly strangle it out and reduce duplicate types +// We don't have the input types defined through io-ts just yet but as we being introducing types from there we will more and more remove +// types and share them between input and output schema but have an input Rule Schema and an output Rule Schema. export type RuleType = 'query' | 'saved_query' | 'machine_learning'; export interface RuleAlertParams { @@ -55,6 +60,7 @@ export interface RuleAlertParams { type: RuleType; version: number; throttle?: string; + lists: ListsDefaultArraySchema | null | undefined; } export type RuleTypeParams = Omit; diff --git a/x-pack/legacy/plugins/siem/server/plugin.ts b/x-pack/legacy/plugins/siem/server/plugin.ts index d9d381498fb56..c505edc79bc76 100644 --- a/x-pack/legacy/plugins/siem/server/plugin.ts +++ b/x-pack/legacy/plugins/siem/server/plugin.ts @@ -34,6 +34,7 @@ import { ruleStatusSavedObjectType, } from './saved_objects'; import { SiemClientFactory } from './client'; +import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; export { CoreSetup, CoreStart }; @@ -66,6 +67,12 @@ export class Plugin { public setup(core: CoreSetup, plugins: SetupPlugins, __legacy: LegacyServices) { this.logger.debug('Shim plugin setup'); + if (hasListsFeature()) { + // TODO: Remove this once we have the lists feature supported + this.logger.error( + `You have activated the lists feature flag which is NOT currently supported for SIEM! You should turn this feature flag off immediately by un-setting the environment variable: ${listsEnvFeatureFlagName} and restarting Kibana` + ); + } const router = core.http.createRouter(); core.http.registerRouteHandlerContext(this.name, (context, request, response) => ({ diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index d2bfeeb6433d3..89ebd902834b9 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -8,6 +8,7 @@ import path from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import { services } from './services'; +import { listsEnvFeatureFlagName } from '../../../legacy/plugins/siem/server/lib/detection_engine/feature_flags'; interface CreateTestConfigOptions { license: string; @@ -31,6 +32,10 @@ const enabledActionTypes = [ 'test.rate-limit', ]; +// Temporary feature flag for the lists feature +// TODO: Remove this once lists land in a Kibana version +process.env[listsEnvFeatureFlagName] = 'true'; + // eslint-disable-next-line import/no-default-export export function createTestConfig(name: string, options: CreateTestConfigOptions) { const { license = 'trial', disabledPlugins = [], ssl = false } = options; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts index 8847a2fdb21af..6e2a391ec14e1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/utils.ts @@ -150,6 +150,7 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1'): Partial Date: Fri, 20 Mar 2020 01:13:54 +0200 Subject: [PATCH 12/22] Return incident's url (#60617) (#60694) --- .../servicenow/action_handlers.test.ts | 25 +++++++++++++++++++ .../servicenow/action_handlers.ts | 15 +++++++---- .../servicenow/index.test.ts | 4 ++- .../servicenow/lib/constants.ts | 3 +++ .../servicenow/lib/index.test.ts | 2 ++ .../servicenow/lib/index.ts | 8 +++++- .../servicenow/lib/types.ts | 1 + .../builtin_action_types/servicenow/mock.ts | 2 ++ .../builtin_action_types/servicenow/types.ts | 7 ++---- .../builtin_action_types/servicenow.ts | 7 +++++- 10 files changed, 61 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts index be687e33e2201..2712b8f6ea9b5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.test.ts @@ -78,11 +78,13 @@ beforeAll(() => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }), updateIncident: jest.fn().mockResolvedValue({ incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }), batchCreateComments: jest .fn() @@ -107,6 +109,7 @@ describe('handleIncident', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', comments: [ { commentId: '456', @@ -129,6 +132,7 @@ describe('handleIncident', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', comments: [ { commentId: '456', @@ -161,6 +165,7 @@ describe('handleCreateIncident', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); @@ -203,6 +208,7 @@ describe('handleCreateIncident', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', comments: [ { commentId: '456', @@ -236,6 +242,7 @@ describe('handleUpdateIncident', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); @@ -326,6 +333,7 @@ describe('handleUpdateIncident', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', comments: [ { commentId: '456', @@ -383,8 +391,10 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); + test('nothing & append', async () => { const { serviceNow } = new ServiceNowMock(); finalMapping.set('title', { @@ -426,8 +436,10 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); + test('append & append', async () => { const { serviceNow } = new ServiceNowMock(); finalMapping.set('title', { @@ -471,8 +483,10 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); + test('nothing & nothing', async () => { const { serviceNow } = new ServiceNowMock(); finalMapping.set('title', { @@ -511,8 +525,10 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); + test('overwrite & nothing', async () => { const { serviceNow } = new ServiceNowMock(); finalMapping.set('title', { @@ -553,8 +569,10 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); + test('overwrite & overwrite', async () => { const { serviceNow } = new ServiceNowMock(); finalMapping.set('title', { @@ -596,8 +614,10 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); + test('nothing & overwrite', async () => { const { serviceNow } = new ServiceNowMock(); finalMapping.set('title', { @@ -638,8 +658,10 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); + test('append & overwrite', async () => { const { serviceNow } = new ServiceNowMock(); finalMapping.set('title', { @@ -682,8 +704,10 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); + test('append & nothing', async () => { const { serviceNow } = new ServiceNowMock(); finalMapping.set('title', { @@ -725,6 +749,7 @@ describe('handleUpdateIncident: different action types', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts index 6439a68813fd5..fb296089e9ec5 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/action_handlers.ts @@ -47,11 +47,11 @@ export const handleCreateIncident = async ({ fields, }); - const { incidentId, number, pushedDate } = await serviceNow.createIncident({ + const createdIncident = await serviceNow.createIncident({ ...incident, }); - const res: HandlerResponse = { incidentId, number, pushedDate }; + const res: HandlerResponse = { ...createdIncident }; if ( comments && @@ -61,7 +61,12 @@ export const handleCreateIncident = async ({ ) { comments = transformComments(comments, params, ['informationAdded']); res.comments = [ - ...(await createComments(serviceNow, incidentId, mapping.get('comments').target, comments)), + ...(await createComments( + serviceNow, + res.incidentId, + mapping.get('comments').target, + comments + )), ]; } @@ -88,11 +93,11 @@ export const handleUpdateIncident = async ({ currentIncident, }); - const { number, pushedDate } = await serviceNow.updateIncident(incidentId, { + const updatedIncident = await serviceNow.updateIncident(incidentId, { ...incident, }); - const res: HandlerResponse = { incidentId, number, pushedDate }; + const res: HandlerResponse = { ...updatedIncident }; if ( comments && diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts index 8ee81c5e76451..67d595cc3ec56 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -231,8 +231,10 @@ describe('execute()', () => { services, }; + handleIncidentMock.mockImplementation(() => incidentResponse); + const actionResponse = await actionType.executor(executorOptions); - expect(actionResponse).toEqual({ actionId, status: 'ok' }); + expect(actionResponse).toEqual({ actionId, status: 'ok', data: incidentResponse }); }); test('should throw an error when failed to update an incident', async () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts index c84e1928e2e5a..3f102ae19f437 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/constants.ts @@ -8,3 +8,6 @@ export const API_VERSION = 'v2'; export const INCIDENT_URL = `api/now/${API_VERSION}/table/incident`; export const USER_URL = `api/now/${API_VERSION}/table/sys_user?user_name=`; export const COMMENT_URL = `api/now/${API_VERSION}/table/incident`; + +// Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html +export const VIEW_INCIDENT_URL = `nav_to.do?uri=incident.do?sys_id=`; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts index 17c8bce651403..40eeb0f920f82 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.test.ts @@ -92,6 +92,7 @@ describe('ServiceNow lib', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); @@ -116,6 +117,7 @@ describe('ServiceNow lib', () => { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts index 2d1d8975c9efc..1acb6c563801c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/index.ts @@ -6,7 +6,7 @@ import axios, { AxiosInstance, Method, AxiosResponse } from 'axios'; -import { INCIDENT_URL, USER_URL, COMMENT_URL } from './constants'; +import { INCIDENT_URL, USER_URL, COMMENT_URL, VIEW_INCIDENT_URL } from './constants'; import { Instance, Incident, IncidentResponse, UpdateIncident, CommentResponse } from './types'; import { Comment } from '../types'; @@ -72,6 +72,10 @@ class ServiceNow { return `[Action][ServiceNow]: ${msg}`; } + private _getIncidentViewURL(id: string) { + return `${this.instance.url}/${VIEW_INCIDENT_URL}${id}`; + } + async getUserID(): Promise { try { const res = await this._request({ url: `${this.userUrl}${this.instance.username}` }); @@ -109,6 +113,7 @@ class ServiceNow { number: res.data.result.number, incidentId: res.data.result.sys_id, pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), + url: this._getIncidentViewURL(res.data.result.sys_id), }; } catch (error) { throw new Error(this._getErrorMessage(`Unable to create incident. Error: ${error.message}`)); @@ -126,6 +131,7 @@ class ServiceNow { number: res.data.result.number, incidentId: res.data.result.sys_id, pushedDate: new Date(this._addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), + url: this._getIncidentViewURL(res.data.result.sys_id), }; } catch (error) { throw new Error( diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts index 3c245bf3f688f..a65e417dbc486 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/lib/types.ts @@ -21,6 +21,7 @@ export interface IncidentResponse { number: string; incidentId: string; pushedDate: string; + url: string; } export interface CommentResponse { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts index b9608511159b6..06c006fb37825 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mock.ts @@ -69,6 +69,8 @@ const params: ExecutorParams = { const incidentResponse = { incidentId: 'c816f79cc0a8016401c5a33be04be441', number: 'INC0010001', + pushedDate: '2020-03-13T08:34:53.450Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', }; const userId = '2e9a0a5e2f79001016ab51172799b670'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 418b78add2429..71b05be8f3e4d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -16,7 +16,7 @@ import { } from './schema'; import { ServiceNow } from './lib'; -import { Incident } from './lib/types'; +import { Incident, IncidentResponse } from './lib/types'; // config definition export type ConfigType = TypeOf; @@ -50,11 +50,8 @@ export type IncidentHandlerArguments = CreateHandlerArguments & { incidentId: string | null; }; -export interface HandlerResponse { - incidentId: string; - number: string; +export interface HandlerResponse extends IncidentResponse { comments?: SimpleComment[]; - pushedDate: string; } export interface SimpleComment { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts index b735dae2ca5b1..48f348e1b834d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts @@ -294,7 +294,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { expect(result).to.eql({ status: 'ok', actionId: simulatedActionId, - data: { incidentId: '123', number: 'INC01', pushedDate: '2020-03-10T12:24:20.000Z' }, + data: { + incidentId: '123', + number: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + }, }); }); From c7df9c83fa0a2e90deebff6f2dd1e578f07b160f Mon Sep 17 00:00:00 2001 From: Patrick Mueller Date: Thu, 19 Mar 2020 20:20:53 -0400 Subject: [PATCH 13/22] [Alerting] add functional tests for index threshold alertType (#60597) (#60707) resolves https://github.com/elastic/kibana/issues/58902 --- .../alert_types/index_threshold/alert_type.ts | 2 + .../common/lib/es_test_index_tool.ts | 23 +- .../index_threshold/alert.ts | 398 ++++++++++++++++++ .../index_threshold/create_test_data.ts | 48 +-- .../index_threshold/index.ts | 1 + .../time_series_query_endpoint.ts | 24 +- 6 files changed, 447 insertions(+), 49 deletions(-) create mode 100644 x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts diff --git a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts index b79321a8803fa..6d27f8a99dd4b 100644 --- a/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts +++ b/x-pack/plugins/alerting_builtins/server/alert_types/index_threshold/alert_type.ts @@ -113,6 +113,7 @@ export function getAlertType(service: Service): AlertType { timeWindowUnit: params.timeWindowUnit, interval: undefined, }; + // console.log(`index_threshold: query: ${JSON.stringify(queryParams, null, 4)}`); const result = await service.indexThreshold.timeSeriesQuery({ logger, callCluster, @@ -121,6 +122,7 @@ export function getAlertType(service: Service): AlertType { logger.debug(`alert ${ID}:${alertId} "${name}" query result: ${JSON.stringify(result)}`); const groupResults = result.results || []; + // console.log(`index_threshold: response: ${JSON.stringify(groupResults, null, 4)}`); for (const groupResult of groupResults) { const instanceId = groupResult.group; const value = groupResult.metrics[0][1]; diff --git a/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts b/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts index ccd7748d9e899..999a8686e0ee7 100644 --- a/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts +++ b/x-pack/test/alerting_api_integration/common/lib/es_test_index_tool.ts @@ -4,20 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export const ES_TEST_INDEX_NAME = '.kibaka-alerting-test-data'; +export const ES_TEST_INDEX_NAME = '.kibana-alerting-test-data'; export class ESTestIndexTool { - private readonly es: any; - private readonly retry: any; - - constructor(es: any, retry: any) { - this.es = es; - this.retry = retry; - } + constructor( + private readonly es: any, + private readonly retry: any, + private readonly index: string = ES_TEST_INDEX_NAME + ) {} async setup() { return await this.es.indices.create({ - index: ES_TEST_INDEX_NAME, + index: this.index, body: { mappings: { properties: { @@ -56,12 +54,13 @@ export class ESTestIndexTool { } async destroy() { - return await this.es.indices.delete({ index: ES_TEST_INDEX_NAME, ignore: [404] }); + return await this.es.indices.delete({ index: this.index, ignore: [404] }); } async search(source: string, reference: string) { return await this.es.search({ - index: ES_TEST_INDEX_NAME, + index: this.index, + size: 1000, body: { query: { bool: { @@ -86,7 +85,7 @@ export class ESTestIndexTool { async waitForDocs(source: string, reference: string, numDocs: number = 1) { return await this.retry.try(async () => { const searchResult = await this.search(source, reference); - if (searchResult.hits.total.value !== numDocs) { + if (searchResult.hits.total.value < numDocs) { throw new Error(`Expected ${numDocs} but received ${searchResult.hits.total.value}.`); } return searchResult.hits.hits; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts new file mode 100644 index 0000000000000..13f3a4971183c --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/alert.ts @@ -0,0 +1,398 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { Spaces } from '../../../../scenarios'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { + ESTestIndexTool, + ES_TEST_INDEX_NAME, + getUrlPrefix, + ObjectRemover, +} from '../../../../../common/lib'; +import { createEsDocuments } from './create_test_data'; + +const ALERT_TYPE_ID = '.index-threshold'; +const ACTION_TYPE_ID = '.index'; +const ES_TEST_INDEX_SOURCE = 'builtin-alert:index-threshold'; +const ES_TEST_INDEX_REFERENCE = '-na-'; +const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-output`; + +const ALERT_INTERVALS_TO_WRITE = 5; +const ALERT_INTERVAL_SECONDS = 3; +const ALERT_INTERVAL_MILLIS = ALERT_INTERVAL_SECONDS * 1000; + +// eslint-disable-next-line import/no-default-export +export default function alertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + const es = getService('legacyEs'); + const esTestIndexTool = new ESTestIndexTool(es, retry); + const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); + + describe('alert', async () => { + let endDate: string; + let actionId: string; + const objectRemover = new ObjectRemover(supertest); + + beforeEach(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + await esTestIndexToolOutput.destroy(); + await esTestIndexToolOutput.setup(); + + actionId = await createAction(supertest, objectRemover); + + // write documents in the future, figure out the end date + const endDateMillis = Date.now() + (ALERT_INTERVALS_TO_WRITE - 1) * ALERT_INTERVAL_MILLIS; + endDate = new Date(endDateMillis).toISOString(); + + // write documents from now to the future end date in 3 groups + createEsDocumentsInGroups(3); + }); + + afterEach(async () => { + await objectRemover.removeAll(); + await esTestIndexTool.destroy(); + await esTestIndexToolOutput.destroy(); + }); + + // The tests below create two alerts, one that will fire, one that will + // never fire; the tests ensure the ones that should fire, do fire, and + // those that shouldn't fire, do not fire. + it('runs correctly: count all < >', async () => { + await createAlert({ + name: 'never fire', + aggType: 'count', + groupBy: 'all', + thresholdComparator: '<', + threshold: [0], + }); + + await createAlert({ + name: 'always fire', + aggType: 'count', + groupBy: 'all', + thresholdComparator: '>', + threshold: [-1], + }); + + const docs = await waitForDocs(2); + for (const doc of docs) { + const { group } = doc._source; + const { name, value, title, message } = doc._source.params; + + expect(name).to.be('always fire'); + expect(group).to.be('all documents'); + + // we'll check title and message in this test, but not subsequent ones + expect(title).to.be('alert always fire group all documents exceeded threshold'); + + const expectedPrefix = `alert always fire group all documents value ${value} exceeded threshold count > -1 over`; + const messagePrefix = message.substr(0, expectedPrefix.length); + expect(messagePrefix).to.be(expectedPrefix); + } + }); + + it('runs correctly: count grouped <= =>', async () => { + // create some more documents in the first group + createEsDocumentsInGroups(1); + + await createAlert({ + name: 'never fire', + aggType: 'count', + groupBy: 'top', + termField: 'group', + termSize: 2, + thresholdComparator: '<=', + threshold: [-1], + }); + + await createAlert({ + name: 'always fire', + aggType: 'count', + groupBy: 'top', + termField: 'group', + termSize: 2, // two actions will fire each interval + thresholdComparator: '>=', + threshold: [0], + }); + + const docs = await waitForDocs(4); + let inGroup0 = 0; + + for (const doc of docs) { + const { group } = doc._source; + const { name } = doc._source.params; + + expect(name).to.be('always fire'); + if (group === 'group-0') inGroup0++; + } + + // there should be 2 docs in group-0, rando split between others + expect(inGroup0).to.be(2); + }); + + it('runs correctly: sum all between', async () => { + // create some more documents in the first group + createEsDocumentsInGroups(1); + + await createAlert({ + name: 'never fire', + aggType: 'sum', + aggField: 'testedValue', + groupBy: 'all', + thresholdComparator: 'between', + threshold: [-2, -1], + }); + + await createAlert({ + name: 'always fire', + aggType: 'sum', + aggField: 'testedValue', + groupBy: 'all', + thresholdComparator: 'between', + threshold: [0, 1000000], + }); + + const docs = await waitForDocs(2); + for (const doc of docs) { + const { name } = doc._source.params; + + expect(name).to.be('always fire'); + } + }); + + it('runs correctly: avg all', async () => { + // create some more documents in the first group + createEsDocumentsInGroups(1); + + await createAlert({ + name: 'never fire', + aggType: 'avg', + aggField: 'testedValue', + groupBy: 'all', + thresholdComparator: '<', + threshold: [0], + }); + + await createAlert({ + name: 'always fire', + aggType: 'avg', + aggField: 'testedValue', + groupBy: 'all', + thresholdComparator: '>=', + threshold: [0], + }); + + const docs = await waitForDocs(4); + for (const doc of docs) { + const { name } = doc._source.params; + + expect(name).to.be('always fire'); + } + }); + + it('runs correctly: max grouped', async () => { + // create some more documents in the first group + createEsDocumentsInGroups(1); + + await createAlert({ + name: 'never fire', + aggType: 'max', + aggField: 'testedValue', + groupBy: 'top', + termField: 'group', + termSize: 2, + thresholdComparator: '<', + threshold: [0], + }); + + await createAlert({ + name: 'always fire', + aggType: 'max', + aggField: 'testedValue', + groupBy: 'top', + termField: 'group', + termSize: 2, // two actions will fire each interval + thresholdComparator: '>=', + threshold: [0], + }); + + const docs = await waitForDocs(4); + let inGroup2 = 0; + + for (const doc of docs) { + const { group } = doc._source; + const { name } = doc._source.params; + + expect(name).to.be('always fire'); + if (group === 'group-2') inGroup2++; + } + + // there should be 2 docs in group-2, rando split between others + expect(inGroup2).to.be(2); + }); + + it('runs correctly: min grouped', async () => { + // create some more documents in the first group + createEsDocumentsInGroups(1); + + await createAlert({ + name: 'never fire', + aggType: 'min', + aggField: 'testedValue', + groupBy: 'top', + termField: 'group', + termSize: 2, + thresholdComparator: '<', + threshold: [0], + }); + + await createAlert({ + name: 'always fire', + aggType: 'min', + aggField: 'testedValue', + groupBy: 'top', + termField: 'group', + termSize: 2, // two actions will fire each interval + thresholdComparator: '>=', + threshold: [0], + }); + + const docs = await waitForDocs(4); + let inGroup0 = 0; + + for (const doc of docs) { + const { group } = doc._source; + const { name } = doc._source.params; + + expect(name).to.be('always fire'); + if (group === 'group-0') inGroup0++; + } + + // there should be 2 docs in group-0, rando split between others + expect(inGroup0).to.be(2); + }); + + async function createEsDocumentsInGroups(groups: number) { + await createEsDocuments( + es, + esTestIndexTool, + endDate, + ALERT_INTERVALS_TO_WRITE, + ALERT_INTERVAL_MILLIS, + groups + ); + } + + async function waitForDocs(count: number): Promise { + return await esTestIndexToolOutput.waitForDocs( + ES_TEST_INDEX_SOURCE, + ES_TEST_INDEX_REFERENCE, + count + ); + } + + interface CreateAlertParams { + name: string; + aggType: string; + aggField?: string; + groupBy: 'all' | 'top'; + termField?: string; + termSize?: number; + thresholdComparator: string; + threshold: number[]; + } + + async function createAlert(params: CreateAlertParams): Promise { + const action = { + id: actionId, + group: 'threshold met', + params: { + documents: [ + { + source: ES_TEST_INDEX_SOURCE, + reference: ES_TEST_INDEX_REFERENCE, + params: { + name: '{{{alertName}}}', + value: '{{{context.value}}}', + title: '{{{context.title}}}', + message: '{{{context.message}}}', + }, + date: '{{{context.date}}}', + // TODO: I wanted to write the alert value here, but how? + // We only mustache interpolate string values ... + // testedValue: '{{{context.value}}}', + group: '{{{context.group}}}', + }, + ], + }, + }; + + const { statusCode, body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`) + .set('kbn-xsrf', 'foo') + .send({ + name: params.name, + consumer: 'function test', + enabled: true, + alertTypeId: ALERT_TYPE_ID, + schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` }, + actions: [action], + params: { + index: ES_TEST_INDEX_NAME, + timeField: 'date', + aggType: params.aggType, + aggField: params.aggField, + groupBy: params.groupBy, + termField: params.termField, + termSize: params.termSize, + timeWindowSize: ALERT_INTERVAL_SECONDS * 5, + timeWindowUnit: 's', + thresholdComparator: params.thresholdComparator, + threshold: params.threshold, + }, + }); + + // will print the error body, if an error occurred + // if (statusCode !== 200) console.log(createdAlert); + + expect(statusCode).to.be(200); + + const alertId = createdAlert.id; + objectRemover.add(Spaces.space1.id, alertId, 'alert'); + + return alertId; + } + }); +} + +async function createAction(supertest: any, objectRemover: ObjectRemover): Promise { + const { statusCode, body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/action`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'index action for index threshold FT', + actionTypeId: ACTION_TYPE_ID, + config: { + index: ES_TEST_OUTPUT_INDEX_NAME, + }, + secrets: {}, + }); + + // will print the error body, if an error occurred + // if (statusCode !== 200) console.log(createdAction); + + expect(statusCode).to.be(200); + + const actionId = createdAction.id; + objectRemover.add(Spaces.space1.id, actionId, 'action'); + + return actionId; +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts index 523c348e26049..21f73ac9b9833 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/create_test_data.ts @@ -8,53 +8,50 @@ import { times } from 'lodash'; import { v4 as uuid } from 'uuid'; import { ESTestIndexTool, ES_TEST_INDEX_NAME } from '../../../../../common/lib'; -// date to start writing data -export const START_DATE = '2020-01-01T00:00:00Z'; +// default end date +export const END_DATE = '2020-01-01T00:00:00Z'; -const DOCUMENT_SOURCE = 'queryDataEndpointTests'; +export const DOCUMENT_SOURCE = 'queryDataEndpointTests'; +export const DOCUMENT_REFERENCE = '-na-'; // Create a set of es documents to run the queries against. -// Will create 2 documents for each interval. +// Will create `groups` documents for each interval. // The difference between the dates of the docs will be intervalMillis. // The date of the last documents will be startDate - intervalMillis / 2. -// So there will be 2 documents written in the middle of each interval range. -// The data value written to each doc is a power of 2, with 2^0 as the value -// of the last documents, the values increasing for older documents. The -// second document for each time value will be power of 2 + 1 +// So the documents will be written in the middle of each interval range. +// The data value written to each doc is a power of 2 + the group index, with +// 2^0 as the value of the last documents, the values increasing for older +// documents. export async function createEsDocuments( es: any, esTestIndexTool: ESTestIndexTool, - startDate: string = START_DATE, + endDate: string = END_DATE, intervals: number = 1, - intervalMillis: number = 1000 + intervalMillis: number = 1000, + groups: number = 2 ) { - const totalDocuments = intervals * 2; - const startDateMillis = Date.parse(startDate) - intervalMillis / 2; + const endDateMillis = Date.parse(endDate) - intervalMillis / 2; times(intervals, interval => { - const date = startDateMillis - interval * intervalMillis; + const date = endDateMillis - interval * intervalMillis; - // base value for each window is 2^window + // base value for each window is 2^interval const testedValue = 2 ** interval; // don't need await on these, wait at the end of the function - createEsDocument(es, '-na-', date, testedValue, 'groupA'); - createEsDocument(es, '-na-', date, testedValue + 1, 'groupB'); + times(groups, group => { + createEsDocument(es, date, testedValue + group, `group-${group}`); + }); }); - await esTestIndexTool.waitForDocs(DOCUMENT_SOURCE, '-na-', totalDocuments); + const totalDocuments = intervals * groups; + await esTestIndexTool.waitForDocs(DOCUMENT_SOURCE, DOCUMENT_REFERENCE, totalDocuments); } -async function createEsDocument( - es: any, - reference: string, - epochMillis: number, - testedValue: number, - group: string -) { +async function createEsDocument(es: any, epochMillis: number, testedValue: number, group: string) { const document = { source: DOCUMENT_SOURCE, - reference, + reference: DOCUMENT_REFERENCE, date: new Date(epochMillis).toISOString(), testedValue, group, @@ -65,6 +62,7 @@ async function createEsDocument( index: ES_TEST_INDEX_NAME, body: document, }); + // console.log(`writing document to ${ES_TEST_INDEX_NAME}:`, JSON.stringify(document, null, 4)); if (response.result !== 'created') { throw new Error(`document not created: ${JSON.stringify(response)}`); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/index.ts index 9158954f23364..507548f94aaf3 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/index.ts @@ -12,5 +12,6 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./time_series_query_endpoint')); loadTestFile(require.resolve('./fields_endpoint')); loadTestFile(require.resolve('./indices_endpoint')); + loadTestFile(require.resolve('./alert')); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts index 1aa1d3d21f00d..c9b488da5dec5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/index_threshold/time_series_query_endpoint.ts @@ -39,12 +39,12 @@ const START_DATE_MINUS_2INTERVALS = getStartDate(-2 * INTERVAL_MILLIS); are offset from the top of the minute by 30 seconds, the queries always run from the top of the hour. - { "date":"2019-12-31T23:59:30.000Z", "testedValue":1, "group":"groupA" } - { "date":"2019-12-31T23:59:30.000Z", "testedValue":2, "group":"groupB" } - { "date":"2019-12-31T23:58:30.000Z", "testedValue":2, "group":"groupA" } - { "date":"2019-12-31T23:58:30.000Z", "testedValue":3, "group":"groupB" } - { "date":"2019-12-31T23:57:30.000Z", "testedValue":4, "group":"groupA" } - { "date":"2019-12-31T23:57:30.000Z", "testedValue":5, "group":"groupB" } + { "date":"2019-12-31T23:59:30.000Z", "testedValue":1, "group":"group-0" } + { "date":"2019-12-31T23:59:30.000Z", "testedValue":2, "group":"group-1" } + { "date":"2019-12-31T23:58:30.000Z", "testedValue":2, "group":"group-0" } + { "date":"2019-12-31T23:58:30.000Z", "testedValue":3, "group":"group-1" } + { "date":"2019-12-31T23:57:30.000Z", "testedValue":4, "group":"group-0" } + { "date":"2019-12-31T23:57:30.000Z", "testedValue":5, "group":"group-1" } */ // eslint-disable-next-line import/no-default-export @@ -162,7 +162,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider const expected = { results: [ { - group: 'groupA', + group: 'group-0', metrics: [ [START_DATE_MINUS_2INTERVALS, 1], [START_DATE_MINUS_1INTERVALS, 2], @@ -170,7 +170,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider ], }, { - group: 'groupB', + group: 'group-1', metrics: [ [START_DATE_MINUS_2INTERVALS, 1], [START_DATE_MINUS_1INTERVALS, 2], @@ -197,7 +197,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider const expected = { results: [ { - group: 'groupB', + group: 'group-1', metrics: [ [START_DATE_MINUS_2INTERVALS, 5 / 1], [START_DATE_MINUS_1INTERVALS, (5 + 3) / 2], @@ -205,7 +205,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider ], }, { - group: 'groupA', + group: 'group-0', metrics: [ [START_DATE_MINUS_2INTERVALS, 4 / 1], [START_DATE_MINUS_1INTERVALS, (4 + 2) / 2], @@ -230,7 +230,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider }); const result = await runQueryExpect(query, 200); expect(result.results.length).to.be(1); - expect(result.results[0].group).to.be('groupB'); + expect(result.results[0].group).to.be('group-1'); }); it('should return correct sorted group for min', async () => { @@ -245,7 +245,7 @@ export default function timeSeriesQueryEndpointTests({ getService }: FtrProvider }); const result = await runQueryExpect(query, 200); expect(result.results.length).to.be(1); - expect(result.results[0].group).to.be('groupA'); + expect(result.results[0].group).to.be('group-0'); }); it('should return an error when passed invalid input', async () => { From d22828d67a61274582d101060a78fd99175c0d2f Mon Sep 17 00:00:00 2001 From: nnamdifrankie <56440728+nnamdifrankie@users.noreply.github.com> Date: Thu, 19 Mar 2020 21:34:49 -0400 Subject: [PATCH 14/22] [Ingest]EMT-248: add post action request handler and resources (#60581) (#60705) [Ingest]EMT-248: add resource to allow to post new agent action. --- .../ingest_manager/common/constants/routes.ts | 1 + .../common/types/models/agent.ts | 9 +- .../common/types/rest_spec/agent.ts | 16 ++- .../routes/agent/actions_handlers.test.ts | 103 ++++++++++++++++++ .../server/routes/agent/actions_handlers.ts | 57 ++++++++++ .../server/routes/agent/index.ts | 15 +++ .../server/services/agents/actions.test.ts | 67 ++++++++++++ .../server/services/agents/actions.ts | 50 +++++++++ .../server/services/agents/index.ts | 1 + .../server/types/models/agent.ts | 11 ++ .../server/types/rest_spec/agent.ts | 11 +- .../apis/fleet/agents/actions.ts | 86 +++++++++++++++ .../test/api_integration/apis/fleet/index.js | 1 + 13 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts create mode 100644 x-pack/plugins/ingest_manager/server/services/agents/actions.ts create mode 100644 x-pack/test/api_integration/apis/fleet/agents/actions.ts diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 1dc98f9bc8947..5bf7c910168c0 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -50,6 +50,7 @@ export const AGENT_API_ROUTES = { EVENTS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/events`, CHECKIN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/checkin`, ACKS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/acks`, + ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`, ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`, UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/unenroll`, STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`, diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index 179cc3fc9eb55..aa5729a101e11 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -14,14 +14,17 @@ export type AgentType = export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning'; -export interface AgentAction extends SavedObjectAttributes { +export interface NewAgentAction { type: 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE'; - id: string; - created_at: string; data?: string; sent_at?: string; } +export type AgentAction = NewAgentAction & { + id: string; + created_at: string; +} & SavedObjectAttributes; + export interface AgentEvent { type: 'STATE' | 'ERROR' | 'ACTION_RESULT' | 'ACTION'; subtype: // State diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 7bbaf42422bb2..21ab41740ce3e 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Agent, AgentAction, AgentEvent, AgentStatus, AgentType } from '../models'; +import { Agent, AgentAction, AgentEvent, AgentStatus, AgentType, NewAgentAction } from '../models'; export interface GetAgentsRequest { query: { @@ -81,6 +81,20 @@ export interface PostAgentAcksResponse { success: boolean; } +export interface PostNewAgentActionRequest { + body: { + action: NewAgentAction; + }; + params: { + agentId: string; + }; +} + +export interface PostNewAgentActionResponse { + success: boolean; + item: AgentAction; +} + export interface PostAgentUnenrollRequest { body: { kuery: string } | { ids: string[] }; } diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts new file mode 100644 index 0000000000000..a20ba4a880537 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.test.ts @@ -0,0 +1,103 @@ +/* + * 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 { NewAgentActionSchema } from '../../types/models'; +import { + KibanaResponseFactory, + RequestHandlerContext, + SavedObjectsClientContract, +} from 'kibana/server'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks'; +import { ActionsService } from '../../services/agents'; +import { AgentAction } from '../../../common/types/models'; +import { postNewAgentActionHandlerBuilder } from './actions_handlers'; +import { + PostNewAgentActionRequest, + PostNewAgentActionResponse, +} from '../../../common/types/rest_spec'; + +describe('test actions handlers schema', () => { + it('validate that new agent actions schema is valid', async () => { + expect( + NewAgentActionSchema.validate({ + type: 'CONFIG_CHANGE', + data: 'data', + sent_at: '2020-03-14T19:45:02.620Z', + }) + ).toBeTruthy(); + }); + + it('validate that new agent actions schema is invalid when required properties are not provided', async () => { + expect(() => { + NewAgentActionSchema.validate({ + data: 'data', + sent_at: '2020-03-14T19:45:02.620Z', + }); + }).toThrowError(); + }); +}); + +describe('test actions handlers', () => { + let mockResponse: jest.Mocked; + let mockSavedObjectsClient: jest.Mocked; + + beforeEach(() => { + mockSavedObjectsClient = savedObjectsClientMock.create(); + mockResponse = httpServerMock.createResponseFactory(); + }); + + it('should succeed on valid new agent action', async () => { + const postNewAgentActionRequest: PostNewAgentActionRequest = { + body: { + action: { + type: 'CONFIG_CHANGE', + data: 'data', + sent_at: '2020-03-14T19:45:02.620Z', + }, + }, + params: { + agentId: 'id', + }, + }; + + const mockRequest = httpServerMock.createKibanaRequest(postNewAgentActionRequest); + + const agentAction = ({ + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + } as unknown) as AgentAction; + + const actionsService: ActionsService = { + getAgent: jest.fn().mockReturnValueOnce({ + id: 'agent', + }), + updateAgentActions: jest.fn().mockReturnValueOnce(agentAction), + } as jest.Mocked; + + const postNewAgentActionHandler = postNewAgentActionHandlerBuilder(actionsService); + await postNewAgentActionHandler( + ({ + core: { + savedObjects: { + client: mockSavedObjectsClient, + }, + }, + } as unknown) as RequestHandlerContext, + mockRequest, + mockResponse + ); + + const expectedAgentActionResponse = (mockResponse.ok.mock.calls[0][0] + ?.body as unknown) as PostNewAgentActionResponse; + + expect(expectedAgentActionResponse.item).toEqual(agentAction); + expect(expectedAgentActionResponse.success).toEqual(true); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts new file mode 100644 index 0000000000000..2b9c230803593 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts @@ -0,0 +1,57 @@ +/* + * 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. + */ + +// handlers that handle agent actions request + +import { RequestHandler } from 'kibana/server'; +import { TypeOf } from '@kbn/config-schema'; +import { PostNewAgentActionRequestSchema } from '../../types/rest_spec'; +import { ActionsService } from '../../services/agents'; +import { NewAgentAction } from '../../../common/types/models'; +import { PostNewAgentActionResponse } from '../../../common/types/rest_spec'; + +export const postNewAgentActionHandlerBuilder = function( + actionsService: ActionsService +): RequestHandler< + TypeOf, + undefined, + TypeOf +> { + return async (context, request, response) => { + try { + const soClient = context.core.savedObjects.client; + + const agent = await actionsService.getAgent(soClient, request.params.agentId); + + const newAgentAction = request.body.action as NewAgentAction; + + const savedAgentAction = await actionsService.updateAgentActions( + soClient, + agent, + newAgentAction + ); + + const body: PostNewAgentActionResponse = { + success: true, + item: savedAgentAction, + }; + + return response.ok({ body }); + } catch (e) { + if (e.isBoom) { + return response.customError({ + statusCode: e.output.statusCode, + body: { message: e.message }, + }); + } + + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } + }; +}; diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index 414d2d79e9067..d461027017842 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -22,6 +22,7 @@ import { PostAgentAcksRequestSchema, PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, + PostNewAgentActionRequestSchema, } from '../../types'; import { getAgentsHandler, @@ -37,6 +38,7 @@ import { } from './handlers'; import { postAgentAcksHandlerBuilder } from './acks_handlers'; import * as AgentService from '../../services/agents'; +import { postNewAgentActionHandlerBuilder } from './actions_handlers'; export const registerRoutes = (router: IRouter) => { // Get one @@ -111,6 +113,19 @@ export const registerRoutes = (router: IRouter) => { }) ); + // Agent actions + router.post( + { + path: AGENT_API_ROUTES.ACTIONS_PATTERN, + validate: PostNewAgentActionRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + postNewAgentActionHandlerBuilder({ + getAgent: AgentService.getAgent, + updateAgentActions: AgentService.updateAgentActions, + }) + ); + router.post( { path: AGENT_API_ROUTES.UNENROLL_PATTERN, diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts new file mode 100644 index 0000000000000..b500aeb825fec --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createAgentAction, updateAgentActions } from './actions'; +import { Agent, AgentAction, NewAgentAction } from '../../../common/types/models'; +import { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; + +interface UpdatedActions { + actions: AgentAction[]; +} + +describe('test agent actions services', () => { + it('should update agent current actions with new action', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + + const newAgentAction: NewAgentAction = { + type: 'CONFIG_CHANGE', + data: 'data', + sent_at: '2020-03-14T19:45:02.620Z', + }; + + await updateAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + actions: [ + { + type: 'CONFIG_CHANGE', + id: 'action1', + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + }, + ], + } as unknown) as Agent, + newAgentAction + ); + + const updatedAgentActions = (mockSavedObjectsClient.update.mock + .calls[0][2] as unknown) as UpdatedActions; + + expect(updatedAgentActions.actions.length).toEqual(2); + const actualAgentAction = updatedAgentActions.actions.find(action => action?.data === 'data'); + expect(actualAgentAction?.type).toEqual(newAgentAction.type); + expect(actualAgentAction?.data).toEqual(newAgentAction.data); + expect(actualAgentAction?.sent_at).toEqual(newAgentAction.sent_at); + }); + + it('should create agent action from new agent action model', async () => { + const newAgentAction: NewAgentAction = { + type: 'CONFIG_CHANGE', + data: 'data', + sent_at: '2020-03-14T19:45:02.620Z', + }; + const now = new Date(); + const agentAction = createAgentAction(now, newAgentAction); + + expect(agentAction.type).toEqual(newAgentAction.type); + expect(agentAction.data).toEqual(newAgentAction.data); + expect(agentAction.sent_at).toEqual(newAgentAction.sent_at); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts new file mode 100644 index 0000000000000..2f8ed9f504453 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import uuid from 'uuid'; +import { + Agent, + AgentAction, + AgentSOAttributes, + NewAgentAction, +} from '../../../common/types/models'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../../common/constants'; + +export async function updateAgentActions( + soClient: SavedObjectsClientContract, + agent: Agent, + newAgentAction: NewAgentAction +): Promise { + const agentAction = createAgentAction(new Date(), newAgentAction); + + agent.actions.push(agentAction); + + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, { + actions: agent.actions, + }); + + return agentAction; +} + +export function createAgentAction(createdAt: Date, newAgentAction: NewAgentAction): AgentAction { + const agentAction = { + id: uuid.v4(), + created_at: createdAt.toISOString(), + }; + + return Object.assign(agentAction, newAgentAction); +} + +export interface ActionsService { + getAgent: (soClient: SavedObjectsClientContract, agentId: string) => Promise; + + updateAgentActions: ( + soClient: SavedObjectsClientContract, + agent: Agent, + newAgentAction: NewAgentAction + ) => Promise; +} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts index 477f081d1900b..c95c9ecc2a1d8 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts @@ -12,3 +12,4 @@ export * from './unenroll'; export * from './status'; export * from './crud'; export * from './update'; +export * from './actions'; diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent.ts b/x-pack/plugins/ingest_manager/server/types/models/agent.ts index e0d252faaaf87..f70b3cf0ed092 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent.ts @@ -52,3 +52,14 @@ export const AckEventSchema = schema.object({ export const AgentEventSchema = schema.object({ ...AgentEventBase, }); + +export const NewAgentActionSchema = schema.object({ + type: schema.oneOf([ + schema.literal('CONFIG_CHANGE'), + schema.literal('DATA_DUMP'), + schema.literal('RESUME'), + schema.literal('PAUSE'), + ]), + data: schema.maybe(schema.string()), + sent_at: schema.maybe(schema.string()), +}); diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index 9fe84c12521ad..f94c02ccee40b 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { AckEventSchema, AgentEventSchema, AgentTypeSchema } from '../models'; +import { AckEventSchema, AgentEventSchema, AgentTypeSchema, NewAgentActionSchema } from '../models'; export const GetAgentsRequestSchema = { query: schema.object({ @@ -52,6 +52,15 @@ export const PostAgentAcksRequestSchema = { }), }; +export const PostNewAgentActionRequestSchema = { + body: schema.object({ + action: NewAgentActionSchema, + }), + params: schema.object({ + agentId: schema.string(), + }), +}; + export const PostAgentUnenrollRequestSchema = { body: schema.oneOf([ schema.object({ diff --git a/x-pack/test/api_integration/apis/fleet/agents/actions.ts b/x-pack/test/api_integration/apis/fleet/agents/actions.ts new file mode 100644 index 0000000000000..f27b932cff5cb --- /dev/null +++ b/x-pack/test/api_integration/apis/fleet/agents/actions.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function(providerContext: FtrProviderContext) { + const { getService } = providerContext; + const esArchiver = getService('esArchiver'); + const supertest = getService('supertest'); + + describe('fleet_agents_actions', () => { + before(async () => { + await esArchiver.loadIfNeeded('fleet/agents'); + }); + after(async () => { + await esArchiver.unload('fleet/agents'); + }); + + it('should return a 200 if this a valid actions request', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/actions`) + .set('kbn-xsrf', 'xx') + .send({ + action: { + type: 'CONFIG_CHANGE', + data: 'action_data', + sent_at: '2020-03-18T19:45:02.620Z', + }, + }) + .expect(200); + + expect(apiResponse.success).to.be(true); + expect(apiResponse.item.data).to.be('action_data'); + expect(apiResponse.item.sent_at).to.be('2020-03-18T19:45:02.620Z'); + + const { body: agentResponse } = await supertest + .get(`/api/ingest_manager/fleet/agents/agent1`) + .set('kbn-xsrf', 'xx') + .expect(200); + + const updatedAction = agentResponse.item.actions.find( + (itemAction: Record) => itemAction?.data === 'action_data' + ); + + expect(updatedAction.type).to.be('CONFIG_CHANGE'); + expect(updatedAction.data).to.be('action_data'); + expect(updatedAction.sent_at).to.be('2020-03-18T19:45:02.620Z'); + }); + + it('should return a 400 when request does not have type information', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent1/actions`) + .set('kbn-xsrf', 'xx') + .send({ + action: { + data: 'action_data', + sent_at: '2020-03-18T19:45:02.620Z', + }, + }) + .expect(400); + expect(apiResponse.message).to.eql( + '[request body.action.type]: expected at least one defined value but got [undefined]' + ); + }); + + it('should return a 404 when agent does not exist', async () => { + const { body: apiResponse } = await supertest + .post(`/api/ingest_manager/fleet/agents/agent100/actions`) + .set('kbn-xsrf', 'xx') + .send({ + action: { + type: 'CONFIG_CHANGE', + data: 'action_data', + sent_at: '2020-03-18T19:45:02.620Z', + }, + }) + .expect(404); + expect(apiResponse.message).to.eql('Saved object [agents/agent100] not found'); + }); + }); +} diff --git a/x-pack/test/api_integration/apis/fleet/index.js b/x-pack/test/api_integration/apis/fleet/index.js index 69d30291f030b..547bbb8c7c6ee 100644 --- a/x-pack/test/api_integration/apis/fleet/index.js +++ b/x-pack/test/api_integration/apis/fleet/index.js @@ -15,5 +15,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./agents/acks')); loadTestFile(require.resolve('./enrollment_api_keys/crud')); loadTestFile(require.resolve('./install')); + loadTestFile(require.resolve('./agents/actions')); }); } From c23a53e823b46abd5a56d9383a3d7e04b7876de2 Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 19 Mar 2020 19:43:51 -0700 Subject: [PATCH 15/22] [canvas/shareable_runtime] sync sass loaders with kbn/optimizer (#60653) (#60716) * [canvas/shareable_runtime] sync sass loaders with kbn/optimizer * limit sass options to those relevant in this context Co-authored-by: spalger Co-authored-by: Elastic Machine Co-authored-by: spalger Co-authored-by: Elastic Machine --- .../shareable_runtime/webpack.config.js | 55 +++++++++++++++++-- x-pack/package.json | 1 + 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js index 0ce722eb90d43..66b0a7bc558cb 100644 --- a/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/legacy/plugins/canvas/shareable_runtime/webpack.config.js @@ -6,6 +6,7 @@ const path = require('path'); const webpack = require('webpack'); +const { stringifyRequest } = require('loader-utils'); // eslint-disable-line const { KIBANA_ROOT, @@ -140,19 +141,63 @@ module.exports = { }, { test: /\.scss$/, - exclude: /\.module.(s(a|c)ss)$/, + exclude: [/node_modules/, /\.module\.s(a|c)ss$/], use: [ - { loader: 'style-loader' }, - { loader: 'css-loader', options: { importLoaders: 2 } }, + { + loader: 'style-loader', + }, + { + loader: 'css-loader', + options: { + sourceMap: !isProd, + }, + }, { loader: 'postcss-loader', options: { + sourceMap: !isProd, config: { - path: require.resolve('./postcss.config.js'), + path: require.resolve('./postcss.config'), + }, + }, + }, + { + loader: 'resolve-url-loader', + options: { + // eslint-disable-next-line no-unused-vars + join: (_, __) => (uri, base) => { + if (!base) { + return null; + } + + // manually force ui/* urls in legacy styles to resolve to ui/legacy/public + if (uri.startsWith('ui/') && base.split(path.sep).includes('legacy')) { + return path.resolve(KIBANA_ROOT, 'src/legacy/ui/public', uri.replace('ui/', '')); + } + + return null; + }, + }, + }, + { + loader: 'sass-loader', + options: { + // must always be enabled as long as we're using the `resolve-url-loader` to + // rewrite `ui/*` urls. They're dropped by subsequent loaders though + sourceMap: true, + prependData(loaderContext) { + return `@import ${stringifyRequest( + loaderContext, + path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss') + )};\n`; + }, + webpackImporter: false, + sassOptions: { + outputStyle: 'nested', + includePaths: [path.resolve(KIBANA_ROOT, 'node_modules')], }, }, }, - { loader: 'sass-loader' }, ], }, { diff --git a/x-pack/package.json b/x-pack/package.json index 76a9a62e7c092..e2fbb263931c8 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -142,6 +142,7 @@ "jest-cli": "^24.9.0", "jest-styled-components": "^7.0.0", "jsdom": "^15.2.1", + "loader-utils": "^1.2.3", "madge": "3.4.4", "marge": "^1.0.1", "mocha": "^6.2.2", From ede40f7a8871ed2e72002987c7edcfd48284271e Mon Sep 17 00:00:00 2001 From: Spencer Date: Thu, 19 Mar 2020 22:42:41 -0700 Subject: [PATCH 16/22] =?UTF-8?q?[7.x]=20[SIEM]=20Cypress=20screenshots=20?= =?UTF-8?q?upload=20to=20google=20cloud=20(#6055=E2=80=A6=20(#60719)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * testing screenshots upload to google cloud * testing another pattern * fixes artifact pattern * uploads only the .png files * only limit uploads from kibana-siem directory Co-authored-by: spalger Co-authored-by: MadameSheema Co-authored-by: spalger --- vars/kibanaPipeline.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 56b6f100c3ed5..d69ef8a97bb58 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -86,6 +86,7 @@ def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ 'target/kibana-*', + 'target/kibana-siem/**/*.png', 'target/junit/**/*', 'test/**/screenshots/**/*.png', 'test/functional/failure_debug/html/*.html', From 6bfb623b335540764a2c1085bf9f703e31c45c9c Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 20 Mar 2020 07:16:44 +0100 Subject: [PATCH 17/22] [7.x] migrate saved objects management edition view to react/typescript/eui (#59490) (#60621) * migrate saved objects management edition view to react/typescript/eui (#59490) * migrate so management edition view to react * fix bundle name + add forgotten data-test-subj * add FTR tests for edition page * EUIfy react components * wrap form with EuiPanel + caps btns labels * Wrapping whole view in page content panel and removing legacy classes * improve delete confirmation modal * update translations * improve delete popin * add unit test on view components * remove kui classes & address comments * extract createFieldList and add tests * disable form submit during submition Co-authored-by: cchaos * update dataset and fix test description Co-authored-by: cchaos --- .../public/overlays/modal/modal_service.tsx | 1 + .../management/saved_object_registry.ts | 8 +- .../management/sections/objects/_objects.js | 1 - .../management/sections/objects/_view.html | 204 +------- .../management/sections/objects/_view.js | 309 ++---------- .../__snapshots__/header.test.tsx.snap | 165 +++++++ .../__snapshots__/intro.test.tsx.snap | 67 +++ .../not_found_errors.test.tsx.snap | 301 ++++++++++++ .../components/object_view/field.test.tsx | 95 ++++ .../objects/components/object_view/field.tsx | 162 +++++++ .../objects/components/object_view/form.tsx | 186 +++++++ .../components/object_view/header.test.tsx | 125 +++++ .../objects/components/object_view/header.tsx | 110 +++++ .../objects/components/object_view/index.ts | 23 + .../components/object_view/intro.test.tsx | 34 ++ .../objects/components/object_view/intro.tsx | 44 ++ .../object_view/not_found_errors.test.tsx | 64 +++ .../object_view/not_found_errors.tsx | 77 +++ .../objects/lib/create_field_list.test.ts | 132 +++++ .../sections/objects/lib/create_field_list.ts | 135 ++++++ .../lib/{in_app_url.js => in_app_url.ts} | 14 +- .../sections/objects/saved_object_view.tsx | 176 +++++++ .../management/sections/objects/types.ts | 38 ++ .../edit_saved_object.ts | 103 ++++ .../apps/saved_objects_management/index.ts | 27 ++ test/functional/config.js | 1 + .../edit_saved_object/data.json | 81 ++++ .../edit_saved_object/mappings.json | 459 ++++++++++++++++++ .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 30 files changed, 2671 insertions(+), 475 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/field.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/form.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/header.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/index.ts create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/intro.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.test.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/not_found_errors.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.test.ts create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/lib/create_field_list.ts rename src/legacy/core_plugins/kibana/public/management/sections/objects/lib/{in_app_url.js => in_app_url.ts} (71%) create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/saved_object_view.tsx create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/objects/types.ts create mode 100644 test/functional/apps/saved_objects_management/edit_saved_object.ts create mode 100644 test/functional/apps/saved_objects_management/index.ts create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/data.json create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/edit_saved_object/mappings.json diff --git a/src/core/public/overlays/modal/modal_service.tsx b/src/core/public/overlays/modal/modal_service.tsx index 3cf1fe745be8e..f3bbd5c94bdb4 100644 --- a/src/core/public/overlays/modal/modal_service.tsx +++ b/src/core/public/overlays/modal/modal_service.tsx @@ -69,6 +69,7 @@ export interface OverlayModalConfirmOptions { closeButtonAriaLabel?: string; 'data-test-subj'?: string; defaultFocusedButton?: EuiConfirmModalProps['defaultFocusedButton']; + buttonColor?: EuiConfirmModalProps['buttonColor']; } /** diff --git a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts index 8e73a09480c41..cb9ac0e01bb7f 100644 --- a/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts +++ b/src/legacy/core_plugins/kibana/public/management/saved_object_registry.ts @@ -35,9 +35,15 @@ interface SavedObjectRegistryEntry { title: string; } +export interface ISavedObjectsManagementRegistry { + register(service: SavedObjectRegistryEntry): void; + all(): SavedObjectRegistryEntry[]; + get(id: string): SavedObjectRegistryEntry | undefined; +} + const registry: SavedObjectRegistryEntry[] = []; -export const savedObjectManagementRegistry = { +export const savedObjectManagementRegistry: ISavedObjectsManagementRegistry = { register: (service: SavedObjectRegistryEntry) => { registry.push(service); }, diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js index e3ab862cd84b7..c5901ca6ee6bf 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_objects.js @@ -28,7 +28,6 @@ import { ObjectsTable } from './components/objects_table'; import { I18nContext } from 'ui/i18n'; import { get } from 'lodash'; import { npStart } from 'ui/new_platform'; - import { getIndexBreadcrumbs } from './breadcrumbs'; const REACT_OBJECTS_TABLE_DOM_ELEMENT_ID = 'reactSavedObjectsTable'; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html index 6efef7b48fa0e..8bce0aabcd64a 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.html @@ -1,203 +1,5 @@ - - - -
- - -
-
-
- - -
- -
-
- -
- -
- -
-
-
-
- - -
-
-
- - -
- -
-
-
-
-
-
- -
- - -
- - - - - - - - -
-
- - - -
- - - -
-
+ + +
diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js index d1a8d6a1b14af..a847055b40015 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/_view.js @@ -17,26 +17,20 @@ * under the License. */ -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import angular from 'angular'; +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import 'angular'; import 'angular-elastic/elastic'; -import rison from 'rison-node'; -import { savedObjectManagementRegistry } from '../../saved_object_registry'; -import objectViewHTML from './_view.html'; import uiRoutes from 'ui/routes'; import { uiModules } from 'ui/modules'; -import { fatalError, toastNotifications } from 'ui/notify'; -import 'ui/accessibility/kbn_ui_ace_keyboard_mode'; -import { isNumeric } from './lib/numeric'; -import { canViewInApp } from './lib/in_app_url'; +import { I18nContext } from 'ui/i18n'; import { npStart } from 'ui/new_platform'; - -import { castEsToKbnFieldTypeName } from '../../../../../../../plugins/data/public'; - +import objectViewHTML from './_view.html'; import { getViewBreadcrumbs } from './breadcrumbs'; +import { savedObjectManagementRegistry } from '../../saved_object_registry'; +import { SavedObjectEdition } from './saved_object_view'; -const location = 'SavedObject view'; +const REACT_OBJECTS_VIEW_DOM_ELEMENT_ID = 'reactSavedObjectsView'; uiRoutes.when('/management/kibana/objects/:service/:id', { template: objectViewHTML, @@ -44,261 +38,48 @@ uiRoutes.when('/management/kibana/objects/:service/:id', { requireUICapability: 'management.kibana.objects', }); +function createReactView($scope, $routeParams) { + const { service: serviceName, id: objectId, notFound } = $routeParams; + + const { savedObjects, overlays, notifications, application } = npStart.core; + + $scope.$$postDigest(() => { + const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID); + if (!node) { + return; + } + + render( + + + , + node + ); + }); +} + +function destroyReactView() { + const node = document.getElementById(REACT_OBJECTS_VIEW_DOM_ELEMENT_ID); + node && unmountComponentAtNode(node); +} + uiModules .get('apps/management', ['monospaced.elastic']) .directive('kbnManagementObjectsView', function() { return { restrict: 'E', - controller: function($scope, $routeParams, $location, $window, $rootScope, uiCapabilities) { - const serviceObj = savedObjectManagementRegistry.get($routeParams.service); - const service = serviceObj.service; - const savedObjectsClient = npStart.core.savedObjects.client; - const { overlays } = npStart.core; - - /** - * Creates a field definition and pushes it to the memo stack. This function - * is designed to be used in conjunction with _.reduce(). If the - * values is plain object it will recurse through all the keys till it hits - * a string, number or an array. - * - * @param {array} memo The stack of fields - * @param {mixed} value The value of the field - * @param {string} key The key of the field - * @param {object} collection This is a reference the collection being reduced - * @param {array} parents The parent keys to the field - * @returns {array} - */ - const createField = function(memo, val, key, collection, parents) { - if (Array.isArray(parents)) { - parents.push(key); - } else { - parents = [key]; - } - - const field = { type: 'text', name: parents.join('.'), value: val }; - - if (_.isString(field.value)) { - try { - field.value = angular.toJson(JSON.parse(field.value), true); - field.type = 'json'; - } catch (err) { - field.value = field.value; - } - } else if (isNumeric(field.value)) { - field.type = 'number'; - } else if (Array.isArray(field.value)) { - field.type = 'array'; - field.value = angular.toJson(field.value, true); - } else if (_.isBoolean(field.value)) { - field.type = 'boolean'; - field.value = field.value; - } else if (_.isPlainObject(field.value)) { - // do something recursive - return _.reduce(field.value, _.partialRight(createField, parents), memo); - } - - memo.push(field); - - // once the field is added to the object you need to pop the parents - // to remove it since we've hit the end of the branch. - parents.pop(); - return memo; - }; - - const readObjectClass = function(fields, Class) { - const fieldMap = _.indexBy(fields, 'name'); - - _.forOwn(Class.mapping, function(esType, name) { - if (fieldMap[name]) return; - - fields.push({ - name: name, - type: (function() { - switch (castEsToKbnFieldTypeName(esType)) { - case 'string': - return 'text'; - case 'number': - return 'number'; - case 'boolean': - return 'boolean'; - default: - return 'json'; - } - })(), - }); - }); - - if (Class.searchSource && !fieldMap['kibanaSavedObjectMeta.searchSourceJSON']) { - fields.push({ - name: 'kibanaSavedObjectMeta.searchSourceJSON', - type: 'json', - value: '{}', - }); - } - - if (!fieldMap.references) { - fields.push({ - name: 'references', - type: 'array', - value: '[]', - }); - } - }; - - const { edit: canEdit, delete: canDelete } = uiCapabilities.savedObjectsManagement; - $scope.canEdit = canEdit; - $scope.canDelete = canDelete; - $scope.canViewInApp = canViewInApp(uiCapabilities, service.type); - - $scope.notFound = $routeParams.notFound; - - $scope.title = service.type; - - savedObjectsClient - .get(service.type, $routeParams.id) - .then(function(obj) { - $scope.obj = obj; - $scope.link = service.urlFor(obj.id); - - const fields = _.reduce(obj.attributes, createField, []); - // Special handling for references which isn't within "attributes" - createField(fields, obj.references, 'references'); - - if (service.Class) readObjectClass(fields, service.Class); - - // sorts twice since we want numerical sort to prioritize over name, - // and sortBy will do string comparison if trying to match against strings - const nameSortedFields = _.sortBy(fields, 'name'); - $scope.$evalAsync(() => { - $scope.fields = _.sortBy(nameSortedFields, field => { - const orderIndex = service.Class.fieldOrder - ? service.Class.fieldOrder.indexOf(field.name) - : -1; - return orderIndex > -1 ? orderIndex : Infinity; - }); - }); - $scope.$digest(); - }) - .catch(error => fatalError(error, location)); - - // This handles the validation of the Ace Editor. Since we don't have any - // other hooks into the editors to tell us if the content is valid or not - // we need to use the annotations to see if they have any errors. If they - // do then we push the field.name to aceInvalidEditor variable. - // Otherwise we remove it. - const loadedEditors = []; - $scope.aceInvalidEditors = []; - - $scope.aceLoaded = function(editor) { - if (_.contains(loadedEditors, editor)) return; - loadedEditors.push(editor); - - editor.$blockScrolling = Infinity; - - const session = editor.getSession(); - const fieldName = editor.container.id; - - session.setTabSize(2); - session.setUseSoftTabs(true); - session.on('changeAnnotation', function() { - const annotations = session.getAnnotations(); - if (_.some(annotations, { type: 'error' })) { - if (!_.contains($scope.aceInvalidEditors, fieldName)) { - $scope.aceInvalidEditors.push(fieldName); - } - } else { - $scope.aceInvalidEditors = _.without($scope.aceInvalidEditors, fieldName); - } - - if (!$rootScope.$$phase) $scope.$apply(); - }); - }; - - $scope.cancel = function() { - $window.history.back(); - return false; - }; - - /** - * Deletes an object and sets the notification - * @param {type} name description - * @returns {type} description - */ - $scope.delete = function() { - function doDelete() { - savedObjectsClient - .delete(service.type, $routeParams.id) - .then(function() { - return redirectHandler('deleted'); - }) - .catch(error => fatalError(error, location)); - } - const confirmModalOptions = { - confirmButtonText: i18n.translate( - 'kbn.management.objects.confirmModalOptions.deleteButtonLabel', - { - defaultMessage: 'Delete', - } - ), - title: i18n.translate('kbn.management.objects.confirmModalOptions.modalTitle', { - defaultMessage: 'Delete saved Kibana object?', - }), - }; - - overlays - .openConfirm( - i18n.translate('kbn.management.objects.confirmModalOptions.modalDescription', { - defaultMessage: "You can't recover deleted objects", - }), - confirmModalOptions - ) - .then(isConfirmed => { - if (isConfirmed) { - doDelete(); - } - }); - }; - - $scope.submit = function() { - const source = _.cloneDeep($scope.obj.attributes); - - _.each($scope.fields, function(field) { - let value = field.value; - - if (field.type === 'number') { - value = Number(field.value); - } - - if (field.type === 'array') { - value = JSON.parse(field.value); - } - - _.set(source, field.name, value); - }); - - const { references, ...attributes } = source; - - savedObjectsClient - .update(service.type, $routeParams.id, attributes, { references }) - .then(function() { - return redirectHandler('updated'); - }) - .catch(error => fatalError(error, location)); - }; - - function redirectHandler(action) { - $location.path('/management/kibana/objects').search({ - _a: rison.encode({ - tab: serviceObj.title, - }), - }); - - toastNotifications.addSuccess( - `${_.capitalize(action)} '${ - $scope.obj.attributes.title - }' ${$scope.title.toLowerCase()} object` - ); - } + controller: function($scope, $routeParams) { + createReactView($scope, $routeParams); + $scope.$on('$destroy', destroyReactView); }, }; }); diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap new file mode 100644 index 0000000000000..7e1f7ea12b014 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/header.test.tsx.snap @@ -0,0 +1,165 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Intro component renders correctly 1`] = ` +
+ +
+ +
+ +

+ + Edit search + +

+
+
+
+ +
+ +
+ +
+ + + + + + +
+ + + +
+
+
+ +
+ +
+ +
+`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap new file mode 100644 index 0000000000000..812031b4b363c --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/intro.test.tsx.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Intro component renders correctly 1`] = ` + + + } + > +
+
+
+ + +`; diff --git a/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap new file mode 100644 index 0000000000000..ac565a000813e --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/management/sections/objects/components/object_view/__snapshots__/not_found_errors.test.tsx.snap @@ -0,0 +1,301 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotFoundErrors component renders correctly for index-pattern type 1`] = ` + + + } + > +
+
+
+ + +`; + +exports[`NotFoundErrors component renders correctly for index-pattern-field type 1`] = ` + + + } + > +
+
+
+ + +`; + +exports[`NotFoundErrors component renders correctly for search type 1`] = ` + + + } + > +
+
+
+ + +`; + +exports[`NotFoundErrors component renders correctly for unknown type 1`] = ` + + + } + > +
+
+