From 1e032df72727742d51e4a345617680500179d30f Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Thu, 8 Sep 2022 13:41:52 -0700 Subject: [PATCH] [Security Solution][Exceptions] - Initial updates to exceptions viewer UX (#138770) ## Summary **API changes** - Adds API for determining the list-rule references. - Updates the exception items find api to include the `search` param which allows for simple search queries - used with the EUI search bar **UI updates** - Moved the exception components into new `rule_exceptions` folder per suggested folder structure updates listed [here](https://github.com/elastic/kibana/issues/138600) - Updates the rule details tabs to split endpoint and rule exceptions into their own tabs - Updates the viewer utilities header now that these different exception types are split - Updates exception item UI to match new designs - Updates the UI for when there are no items - Removes `use_exception_list_items` hook as it is no longer in use - Flyouts (add/edit) remain untouched --- .../src/common/index.ts | 1 + .../src/common/search/index.test.ts | 56 ++ .../src/common/search/index.ts} | 13 +- .../index.mock.ts | 4 + .../index.test.ts | 15 +- .../find_exception_list_item_schema/index.ts | 2 + .../src/typescript_types/index.ts | 5 +- .../src/api/index.ts | 35 +- .../kbn-securitysolution-list-hooks/index.ts | 1 - .../src/index.ts | 20 + .../src/use_api/index.ts | 4 +- .../src/use_exception_list_items/index.ts | 185 ------- .../__tests__/enumerate_patterns.test.js | 7 - .../lists/public/exceptions/api.test.ts | 90 +--- .../public/exceptions/hooks/use_api.test.ts | 4 - .../hooks/use_exception_list_items.test.ts | 498 ------------------ .../routes/find_exception_list_item_route.ts | 2 + .../exception_lists/exception_list_client.ts | 9 + .../exception_list_client_types.ts | 7 + .../find_exception_list_item.ts | 4 + .../find_exception_list_items.ts | 4 + .../security_solution/common/constants.ts | 3 + ...d_exception_list_references_schema.test.ts | 90 ++++ .../find_exception_list_references_schema.ts | 26 + ...d_exception_list_references_schema.test.ts | 169 ++++++ .../find_exception_list_references_schema.ts | 37 ++ .../schemas/response/index.ts | 1 + .../add_edit_data_view_exception.spec.ts | 159 ------ .../exceptions/add_edit_exception.spec.ts | 157 ------ .../alerts_table_flow/add_exception.spec.ts | 102 ++++ .../exceptions/exceptions_flyout.spec.ts | 14 +- .../all_exception_lists_read_only.spec.ts | 20 +- .../rule_details_flow/add_exception.spec.ts | 221 ++++++++ .../add_exception_data_view.spect.ts | 114 ++++ .../rule_details_flow/edit_exception.spec.ts | 154 ++++++ .../edit_exception_data_view.spec.ts | 155 ++++++ .../rule_details_flow/read_only_view.spect.ts | 107 ++++ .../cypress/screens/exceptions.ts | 19 +- .../cypress/screens/rule_details.ts | 2 +- .../cypress/tasks/api_calls/rules.ts | 2 + .../cypress/tasks/rule_details.ts | 55 +- .../components/exceptions/translations.ts | 266 ---------- .../exception_item_card_meta.test.tsx | 51 -- .../exception_item_card_meta.tsx | 111 ---- .../viewer/exceptions_pagination.test.tsx | 142 ----- .../viewer/exceptions_pagination.tsx | 125 ----- .../viewer/exceptions_utility.test.tsx | 161 ------ .../exceptions/viewer/exceptions_utility.tsx | 114 ---- .../exceptions_viewer_header.stories.tsx | 69 --- .../viewer/exceptions_viewer_header.test.tsx | 293 ----------- .../viewer/exceptions_viewer_header.tsx | 209 -------- .../viewer/exceptions_viewer_items.test.tsx | 122 ----- .../viewer/exceptions_viewer_items.tsx | 101 ---- .../exceptions/viewer/index.test.tsx | 152 ------ .../components/exceptions/viewer/index.tsx | 415 --------------- .../components/exceptions/viewer/reducer.ts | 150 ------ ...on_product_no_results_magnifying_glass.svg | 1 + .../add_exception_flyout/index.test.tsx | 20 +- .../add_exception_flyout/index.tsx | 24 +- .../add_exception_flyout/translations.ts | 0 .../all_items.test.tsx | 97 ++++ .../all_exception_items_table/all_items.tsx | 91 ++++ .../empty_viewer_state.test.tsx | 88 ++++ .../empty_viewer_state.tsx | 122 +++++ .../all_exception_items_table/index.test.tsx | 309 +++++++++++ .../all_exception_items_table/index.tsx | 393 ++++++++++++++ .../pagination.test.tsx | 71 +++ .../all_exception_items_table/pagination.tsx | 66 +++ .../all_exception_items_table/reducer.ts | 88 ++++ .../search_bar.test.tsx | 69 +++ .../all_exception_items_table/search_bar.tsx | 116 ++++ .../all_exception_items_table/translations.ts | 114 ++++ .../utility_bar.test.tsx | 54 ++ .../all_exception_items_table/utility_bar.tsx | 97 ++++ .../edit_exception_flyout/index.test.tsx | 16 +- .../edit_exception_flyout/index.tsx | 18 +- .../edit_exception_flyout/translations.ts | 0 .../components/error_callout/index.test.tsx} | 14 +- .../components/error_callout/index.tsx} | 6 +- .../exception_item_card/comments.tsx | 48 ++ .../exception_item_card/conditions.test.tsx} | 4 +- .../exception_item_card/conditions.tsx} | 26 +- .../exception_item_card/header.test.tsx} | 4 +- .../exception_item_card/header.tsx} | 0 .../exception_item_card/index.test.tsx | 178 +++++-- .../components}/exception_item_card/index.tsx | 75 +-- .../exception_item_card/meta.test.tsx | 184 +++++++ .../components/exception_item_card/meta.tsx | 161 ++++++ .../exception_item_card/translations.ts | 24 +- .../components/item_comments/index.tsx} | 16 +- .../value_with_space_warning.test.tsx.snap | 0 .../use_value_with_space_warning.test.ts | 0 .../value_with_space_warning.test.tsx | 0 .../value_with_space_warning/index.ts | 0 .../use_value_with_space_warning.ts | 0 .../value_with_space_warning.tsx | 0 .../logic}/use_add_exception.test.tsx | 6 +- .../logic}/use_add_exception.tsx | 4 +- ...tch_or_create_rule_exception_list.test.tsx | 0 ...se_fetch_or_create_rule_exception_list.tsx | 0 .../logic/use_find_references.tsx | 89 ++++ .../utils}/exceptionable_endpoint_fields.json | 0 .../utils}/exceptionable_linux_fields.json | 0 .../exceptionable_windows_mac_fields.json | 0 .../rule_exceptions/utils}/helpers.test.tsx | 0 .../rule_exceptions/utils}/helpers.tsx | 2 +- .../rule_exceptions/utils/translations.ts | 143 +++++ .../rule_exceptions/utils}/types.ts | 13 +- .../timeline_actions/alert_context_menu.tsx | 8 +- .../use_investigate_in_timeline.tsx | 1 - .../detection_engine/rules/api.test.ts | 35 ++ .../containers/detection_engine/rules/api.ts | 28 + .../detection_engine/rules/types.ts | 12 + .../rules/details/index.test.tsx | 66 +++ .../detection_engine/rules/details/index.tsx | 77 ++- .../rules/details/translations.ts | 7 + .../utils/get_formatted_comments.tsx | 2 +- .../event_filters/view/components/form.tsx | 6 +- .../security_solution/public/rules/routes.tsx | 2 +- .../rules/find_rule_exceptions_route.test.ts | 154 ++++++ .../rules/find_rule_exceptions_route.ts | 95 ++++ .../security_solution/server/routes/index.ts | 2 + .../translations/translations/fr-FR.json | 35 +- .../translations/translations/ja-JP.json | 34 +- .../translations/translations/zh-CN.json | 34 +- .../group1/find_rule_exception_references.ts | 184 +++++++ .../security_and_spaces/group1/index.ts | 1 + .../tests/find_exception_list_items.ts | 83 ++- .../es_archives/exceptions_2/data.json | 31 +- 129 files changed, 4861 insertions(+), 3941 deletions(-) create mode 100644 packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts rename packages/{kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts => kbn-securitysolution-io-ts-list-types/src/common/search/index.ts} (53%) create mode 100644 packages/kbn-securitysolution-list-hooks/src/index.ts delete mode 100644 packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts delete mode 100644 x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.test.ts create mode 100644 x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.ts delete mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_data_view_exception.spec.ts delete mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_exception.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts rename x-pack/plugins/security_solution/cypress/integration/exceptions/{ => exceptions_management_flow}/all_exception_lists_read_only.spec.ts (70%) create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts create mode 100644 x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts create mode 100644 x-pack/plugins/security_solution/public/common/images/illustration_product_no_results_magnifying_glass.svg rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/add_exception_flyout/index.test.tsx (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/add_exception_flyout/index.tsx (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/add_exception_flyout/translations.ts (100%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/edit_exception_flyout/index.test.tsx (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/edit_exception_flyout/index.tsx (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/components}/edit_exception_flyout/translations.ts (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/error_callout.test.tsx => detection_engine/rule_exceptions/components/error_callout/index.test.tsx} (90%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/error_callout.tsx => detection_engine/rule_exceptions/components/error_callout/index.tsx} (94%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/comments.tsx rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx => detection_engine/rule_exceptions/components/exception_item_card/conditions.test.tsx} (98%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx => detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx} (94%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx => detection_engine/rule_exceptions/components/exception_item_card/header.test.tsx} (96%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx => detection_engine/rule_exceptions/components/exception_item_card/header.tsx} (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/exception_item_card/index.test.tsx (52%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/exception_item_card/index.tsx (60%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.test.tsx create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/exception_item_card/translations.ts (84%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/add_exception_comments.tsx => detection_engine/rule_exceptions/components/item_comments/index.tsx} (88%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/value_with_space_warning/__tests__/__snapshots__/value_with_space_warning.test.tsx.snap (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/value_with_space_warning/__tests__/use_value_with_space_warning.test.ts (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/value_with_space_warning/__tests__/value_with_space_warning.test.tsx (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/value_with_space_warning/index.ts (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/value_with_space_warning/use_value_with_space_warning.ts (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions/viewer => detection_engine/rule_exceptions/components}/value_with_space_warning/value_with_space_warning.tsx (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_add_exception.test.tsx (98%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_add_exception.tsx (98%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_fetch_or_create_rule_exception_list.test.tsx (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/logic}/use_fetch_or_create_rule_exception_list.tsx (100%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/exceptionable_endpoint_fields.json (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/exceptionable_linux_fields.json (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/exceptionable_windows_mac_fields.json (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/helpers.test.tsx (100%) rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/helpers.tsx (99%) create mode 100644 x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/translations.ts rename x-pack/plugins/security_solution/public/{common/components/exceptions => detection_engine/rule_exceptions/utils}/types.ts (83%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts index 730c1f2cd491d..f7e533aa7d4cd 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/index.ts @@ -47,6 +47,7 @@ export * from './os_type'; export * from './page'; export * from './per_page'; export * from './pit'; +export * from './search'; export * from './search_after'; export * from './serializer'; export * from './sort_field'; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts new file mode 100644 index 0000000000000..99d73fd5e059a --- /dev/null +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { exactCheck } from '@kbn/securitysolution-io-ts-utils'; +import { searchOrUndefined } from '.'; + +import * as t from 'io-ts'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { left } from 'fp-ts/lib/Either'; + +import { foldLeftRight, getPaths } from '@kbn/securitysolution-io-ts-utils'; + +describe('search', () => { + test('it will validate a correct search', () => { + const payload = 'name:foo'; + const decoded = searchOrUndefined.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will validate with the value of "undefined"', () => { + const obj = t.exact( + t.type({ + search: searchOrUndefined, + }) + ); + const payload: t.TypeOf = { + search: undefined, + }; + const decoded = obj.decode({ + pit_id: undefined, + }); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it will fail to validate an incorrect search', () => { + const payload = ['foo']; + const decoded = searchOrUndefined.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "["foo"]" supplied to "(string | undefined)"', + ]); + expect(message.schema).toEqual({}); + }); +}); diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts similarity index 53% rename from packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts rename to packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts index 4ca0a66e4f602..319c225edf007 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/common/search/index.ts @@ -6,9 +6,10 @@ * Side Public License, v 1. */ -describe('useExceptionListItems', () => { - test('Tests should be ported', () => { - // TODO: Port all the tests from: x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts here once mocks are figured out and kbn package mocks are figured out - expect(true).toBe(true); - }); -}); +import * as t from 'io-ts'; + +export const search = t.string; +export type Search = t.TypeOf; + +export const searchOrUndefined = t.union([search, t.undefined]); +export type SearchOrUndefined = t.TypeOf; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.mock.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.mock.ts index 8f64dccf6d577..4026d878ca278 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.mock.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.mock.ts @@ -16,6 +16,7 @@ export const getFindExceptionListItemSchemaMock = (): FindExceptionListItemSchem namespace_type: NAMESPACE_TYPE, page: '1', per_page: '25', + search: undefined, sort_field: undefined, sort_order: undefined, }); @@ -26,6 +27,7 @@ export const getFindExceptionListItemSchemaMultipleMock = (): FindExceptionListI namespace_type: 'single,single,agnostic', page: '1', per_page: '25', + search: undefined, sort_field: undefined, sort_order: undefined, }); @@ -37,6 +39,7 @@ export const getFindExceptionListItemSchemaDecodedMock = namespace_type: [NAMESPACE_TYPE], page: 1, per_page: 25, + search: undefined, sort_field: undefined, sort_order: undefined, }); @@ -48,6 +51,7 @@ export const getFindExceptionListItemSchemaDecodedMultipleMock = namespace_type: ['single', 'single', 'agnostic'], page: 1, per_page: 25, + search: undefined, sort_field: undefined, sort_order: undefined, }); diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.test.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.test.ts index 04afee30c1ab3..fab9111aa0eb0 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.test.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.test.ts @@ -55,6 +55,7 @@ describe('find_list_item_schema', () => { namespace_type: ['single'], page: undefined, per_page: undefined, + search: undefined, sort_field: undefined, sort_order: undefined, }; @@ -73,7 +74,7 @@ describe('find_list_item_schema', () => { expect(message.schema).toEqual(expected); }); - test('it should validate with pre_page missing', () => { + test('it should validate with per_page missing', () => { const payload = getFindExceptionListItemSchemaMock(); delete payload.per_page; const decoded = findExceptionListItemSchema.decode(payload); @@ -123,6 +124,18 @@ describe('find_list_item_schema', () => { expect(message.schema).toEqual(expected); }); + test('it should validate with search missing', () => { + const payload = getFindExceptionListItemSchemaMock(); + delete payload.search; + const decoded = findExceptionListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual([]); + const expected = getFindExceptionListItemSchemaDecodedMock(); + delete expected.search; + expect(message.schema).toEqual(expected); + }); + test('it should not allow an extra key to be sent in', () => { const payload: FindExceptionListItemSchema & { extraKey: string; diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.ts index 88756ac0eb301..0ca8140d048dc 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/request/find_exception_list_item_schema/index.ts @@ -21,6 +21,7 @@ import { import { RequiredKeepUndefined } from '../../common/required_keep_undefined'; import { sort_field } from '../../common/sort_field'; import { sort_order } from '../../common/sort_order'; +import { search } from '../../common/search'; export const findExceptionListItemSchema = t.intersection([ t.exact( @@ -34,6 +35,7 @@ export const findExceptionListItemSchema = t.intersection([ namespace_type: DefaultNamespaceArray, // defaults to ['single'] if not set during decode page: StringToPositiveNumber, // defaults to undefined if not set during decode per_page: StringToPositiveNumber, // defaults to undefined if not set during decode + search, sort_field, // defaults to undefined if not set during decode sort_order, // defaults to undefined if not set during decode }) diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index a5eb4f976debd..b8f03629135a2 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -99,10 +99,10 @@ export interface ExceptionListIdentifiers { export interface ApiCallFindListsItemsMemoProps { lists: ExceptionListIdentifiers[]; - filterOptions: FilterExceptionsOptions[]; pagination: Partial; showDetectionsListsOnly: boolean; showEndpointListsOnly: boolean; + filter?: string; onError: (arg: string[]) => void; onSuccess: (arg: UseExceptionListItemsSuccess) => void; } @@ -168,8 +168,9 @@ export interface ApiCallByListIdProps { http: HttpStart; listIds: string[]; namespaceTypes: NamespaceType[]; - filterOptions: FilterExceptionsOptions[]; pagination: Partial; + search?: string; + filter?: string; signal: AbortSignal; } diff --git a/packages/kbn-securitysolution-list-api/src/api/index.ts b/packages/kbn-securitysolution-list-api/src/api/index.ts index a0361d044977c..871987ff7d367 100644 --- a/packages/kbn-securitysolution-list-api/src/api/index.ts +++ b/packages/kbn-securitysolution-list-api/src/api/index.ts @@ -34,8 +34,6 @@ import { import { ENDPOINT_LIST_URL, EXCEPTION_LIST_ITEM_URL, - EXCEPTION_LIST_NAMESPACE, - EXCEPTION_LIST_NAMESPACE_AGNOSTIC, EXCEPTION_LIST_URL, } from '@kbn/securitysolution-list-constants'; import { toError, toPromise } from '../fp_utils'; @@ -324,7 +322,8 @@ export { fetchExceptionListByIdWithValidation as fetchExceptionListById }; * @param http Kibana http service * @param listIds ExceptionList list_ids (not ID) * @param namespaceTypes ExceptionList namespace_types - * @param filterOptions optional - filter by field or tags + * @param search optional - simple search string + * @param filter optional * @param pagination optional * @param signal to cancel request * @@ -334,36 +333,20 @@ const fetchExceptionListsItemsByListIds = async ({ http, listIds, namespaceTypes, - filterOptions, + filter, pagination, + search, signal, }: ApiCallByListIdProps): Promise => { - const filters: string = filterOptions - .map((filter, index) => { - const namespace = namespaceTypes[index]; - const filterNamespace = - namespace === 'agnostic' ? EXCEPTION_LIST_NAMESPACE_AGNOSTIC : EXCEPTION_LIST_NAMESPACE; - const formattedFilters = [ - ...(filter.filter.length - ? [`${filterNamespace}.attributes.entries.field:${filter.filter}*`] - : []), - ...(filter.tags.length - ? filter.tags.map((t) => `${filterNamespace}.attributes.tags:${t}`) - : []), - ]; - - return formattedFilters.join(' AND '); - }) - .join(','); - const query = { list_id: listIds.join(','), namespace_type: namespaceTypes.join(','), page: pagination.page ? `${pagination.page}` : '1', per_page: pagination.perPage ? `${pagination.perPage}` : '20', + search, sort_field: 'exception-list.created_at', sort_order: 'desc', - ...(filters.trim() !== '' ? { filter: filters } : {}), + filter, }; return http.fetch(`${EXCEPTION_LIST_ITEM_URL}/_find`, { @@ -374,11 +357,12 @@ const fetchExceptionListsItemsByListIds = async ({ }; const fetchExceptionListsItemsByListIdsWithValidation = async ({ - filterOptions, + filter, http, listIds, namespaceTypes, pagination, + search, signal, }: ApiCallByListIdProps): Promise => flow( @@ -386,11 +370,12 @@ const fetchExceptionListsItemsByListIdsWithValidation = async ({ tryCatch( () => fetchExceptionListsItemsByListIds({ - filterOptions, + filter, http, listIds, namespaceTypes, pagination, + search, signal, }), toError diff --git a/packages/kbn-securitysolution-list-hooks/index.ts b/packages/kbn-securitysolution-list-hooks/index.ts index 4c523fe577211..c2469bc4c4948 100644 --- a/packages/kbn-securitysolution-list-hooks/index.ts +++ b/packages/kbn-securitysolution-list-hooks/index.ts @@ -10,7 +10,6 @@ export * from './src/use_api'; export * from './src/use_create_list_index'; export * from './src/use_cursor'; export * from './src/use_delete_list'; -export * from './src/use_exception_list_items'; export * from './src/use_exception_lists'; export * from './src/use_export_list'; export * from './src/use_find_lists'; diff --git a/packages/kbn-securitysolution-list-hooks/src/index.ts b/packages/kbn-securitysolution-list-hooks/src/index.ts new file mode 100644 index 0000000000000..e458abd0448ad --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/src/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +export * from './transforms'; +export * from './use_api'; +export * from './use_create_list_index'; +export * from './use_cursor'; +export * from './use_delete_list'; +export * from './use_exception_lists'; +export * from './use_export_list'; +export * from './use_find_lists'; +export * from './use_import_list'; +export * from './use_persist_exception_item'; +export * from './use_persist_exception_list'; +export * from './use_read_list_index'; +export * from './use_read_list_privileges'; diff --git a/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts index 3b980f84d82a8..ec76d169390cc 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_api/index.ts @@ -170,7 +170,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => { }, async getExceptionListsItems({ lists, - filterOptions, + filter, pagination, showDetectionsListsOnly, showEndpointListsOnly, @@ -192,7 +192,7 @@ export const useApi = (http: HttpStart): ExceptionsApi => { per_page: perPage, total, } = await Api.fetchExceptionListsItemsByListIds({ - filterOptions, + filter, http, listIds: ids, namespaceTypes: namespaces, diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts deleted file mode 100644 index 623e1e76a7f53..0000000000000 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { useEffect, useRef, useState } from 'react'; -import type { - ExceptionListItemSchema, - Pagination, - UseExceptionListProps, - FilterExceptionsOptions, -} from '@kbn/securitysolution-io-ts-list-types'; -import { fetchExceptionListsItemsByListIds } from '@kbn/securitysolution-list-api'; - -import { getIdsAndNamespaces } from '@kbn/securitysolution-list-utils'; -import { transformInput } from '../transforms'; - -type Func = () => void; -export type ReturnExceptionListAndItems = [ - boolean, - ExceptionListItemSchema[], - Pagination, - Func | null -]; - -/** - * Hook for using to get an ExceptionList and its ExceptionListItems - * - * @param http Kibana http service - * @param lists array of ExceptionListIdentifiers for all lists to fetch - * @param onError error callback - * @param onSuccess callback when all lists fetched successfully - * @param filterOptions optional - filter by fields or tags - * @param showDetectionsListsOnly boolean, if true, only detection lists are searched - * @param showEndpointListsOnly boolean, if true, only endpoint lists are searched - * @param matchFilters boolean, if true, applies first filter in filterOptions to - * all lists - * @param pagination optional - * - */ -export const useExceptionListItems = ({ - http, - lists, - pagination = { - page: 1, - perPage: 20, - total: 0, - }, - filterOptions, - showDetectionsListsOnly, - showEndpointListsOnly, - matchFilters, - onError, - onSuccess, -}: UseExceptionListProps): ReturnExceptionListAndItems => { - const [exceptionItems, setExceptionListItems] = useState([]); - const [paginationInfo, setPagination] = useState(pagination); - const fetchExceptionListsItems = useRef(null); - const [loading, setLoading] = useState(true); - const { ids, namespaces } = getIdsAndNamespaces({ - lists, - showDetection: showDetectionsListsOnly, - showEndpoint: showEndpointListsOnly, - }); - const filters: FilterExceptionsOptions[] = - matchFilters && filterOptions.length > 0 ? ids.map(() => filterOptions[0]) : filterOptions; - const idsAsString: string = ids.join(','); - const namespacesAsString: string = namespaces.join(','); - const filterAsString: string = filterOptions.map(({ filter }) => filter).join(','); - const filterTagsAsString: string = filterOptions.map(({ tags }) => tags.join(',')).join(','); - - useEffect( - () => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchData = async (): Promise => { - try { - setLoading(true); - - if (ids.length === 0 && isSubscribed) { - setPagination({ - page: 0, - perPage: pagination.perPage, - total: 0, - }); - setExceptionListItems([]); - - if (onSuccess != null) { - onSuccess({ - exceptions: [], - pagination: { - page: 0, - perPage: pagination.perPage, - total: 0, - }, - }); - } - setLoading(false); - } else { - const { - page, - per_page: perPage, - total, - data, - } = await fetchExceptionListsItemsByListIds({ - filterOptions: filters, - http, - listIds: ids, - namespaceTypes: namespaces, - pagination: { - page: pagination.page, - perPage: pagination.perPage, - }, - signal: abortCtrl.signal, - }); - - // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes - // for context around the temporary `id` - const transformedData = data.map((item) => transformInput(item)); - - if (isSubscribed) { - setPagination({ - page, - perPage, - total, - }); - setExceptionListItems(transformedData); - - if (onSuccess != null) { - onSuccess({ - exceptions: transformedData, - pagination: { - page, - perPage, - total, - }, - }); - } - } - } - } catch (error) { - if (isSubscribed) { - setExceptionListItems([]); - setPagination({ - page: 1, - perPage: 20, - total: 0, - }); - if (onError != null) { - onError(error); - } - } - } - - if (isSubscribed) { - setLoading(false); - } - }; - - fetchData(); - - fetchExceptionListsItems.current = fetchData; - return (): void => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, // eslint-disable-next-line react-hooks/exhaustive-deps - [ - http, - idsAsString, - namespacesAsString, - setExceptionListItems, - pagination.page, - pagination.perPage, - filterAsString, - filterTagsAsString, - ] - ); - - return [loading, exceptionItems, paginationInfo, fetchExceptionListsItems.current]; -}; diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index 6d5962f7f51e8..14f7cefc78cae 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -34,11 +34,4 @@ describe(`enumeratePatterns`, () => { 'src/plugins/charts/common/static/color_maps/color_maps.ts kibana-app' ); }); - it(`should resolve x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/translations.ts to kibana-security`, () => { - const short = - 'x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout'; - const actual = enumeratePatterns(REPO_ROOT)(log)(new Map([[short, ['kibana-security']]])); - - expect(actual.flat()).toContain(`${short}/translations.ts kibana-security`); - }); }); diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index a7d55139b7f5a..c6f423184f926 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -380,7 +380,6 @@ describe('Exceptions Lists API', () => { test('it invokes "fetchExceptionListsItemsByListIds" with expected url and body values', async () => { await fetchExceptionListsItemsByListIds({ - filterOptions: [], http: httpMock, listIds: ['myList', 'myOtherListId'], namespaceTypes: ['single', 'single'], @@ -405,14 +404,9 @@ describe('Exceptions Lists API', () => { }); }); - test('it invokes with expected url and body values when a filter exists and "namespaceType" of "single"', async () => { + test('it invokes with expected url and body values when a filter exists', async () => { await fetchExceptionListsItemsByListIds({ - filterOptions: [ - { - filter: 'hello world', - tags: [], - }, - ], + filter: 'exception-list.attributes.entries.field:hello world*', http: httpMock, listIds: ['myList'], namespaceTypes: ['single'], @@ -438,80 +432,8 @@ describe('Exceptions Lists API', () => { }); }); - test('it invokes with expected url and body values when a filter exists and "namespaceType" of "agnostic"', async () => { - await fetchExceptionListsItemsByListIds({ - filterOptions: [ - { - filter: 'hello world', - tags: [], - }, - ], - http: httpMock, - listIds: ['myList'], - namespaceTypes: ['agnostic'], - pagination: { - page: 1, - perPage: 20, - }, - signal: abortCtrl.signal, - }); - - expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { - method: 'GET', - query: { - filter: 'exception-list-agnostic.attributes.entries.field:hello world*', - list_id: 'myList', - namespace_type: 'agnostic', - page: '1', - per_page: '20', - sort_field: 'exception-list.created_at', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('it invokes with expected url and body values when tags exists', async () => { + test('it invokes with expected url and body values when search exists', async () => { await fetchExceptionListsItemsByListIds({ - filterOptions: [ - { - filter: '', - tags: ['malware'], - }, - ], - http: httpMock, - listIds: ['myList'], - namespaceTypes: ['agnostic'], - pagination: { - page: 1, - perPage: 20, - }, - signal: abortCtrl.signal, - }); - - expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { - method: 'GET', - query: { - filter: 'exception-list-agnostic.attributes.tags:malware', - list_id: 'myList', - namespace_type: 'agnostic', - page: '1', - per_page: '20', - sort_field: 'exception-list.created_at', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('it invokes with expected url and body values when filter and tags exists', async () => { - await fetchExceptionListsItemsByListIds({ - filterOptions: [ - { - filter: 'host.name', - tags: ['malware'], - }, - ], http: httpMock, listIds: ['myList'], namespaceTypes: ['agnostic'], @@ -519,18 +441,18 @@ describe('Exceptions Lists API', () => { page: 1, perPage: 20, }, + search: '-@timestamp', signal: abortCtrl.signal, }); expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/items/_find', { method: 'GET', query: { - filter: - 'exception-list-agnostic.attributes.entries.field:host.name* AND exception-list-agnostic.attributes.tags:malware', list_id: 'myList', namespace_type: 'agnostic', page: '1', per_page: '20', + search: '-@timestamp', sort_field: 'exception-list.created_at', sort_order: 'desc', }, @@ -540,7 +462,6 @@ describe('Exceptions Lists API', () => { test('it returns expected format when call succeeds', async () => { const exceptionResponse = await fetchExceptionListsItemsByListIds({ - filterOptions: [], http: httpMock, listIds: ['endpoint_list_id'], namespaceTypes: ['single'], @@ -561,7 +482,6 @@ describe('Exceptions Lists API', () => { await expect( fetchExceptionListsItemsByListIds({ - filterOptions: [], http: httpMock, listIds: ['myList'], namespaceTypes: ['single'], diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts index 0bd97dffb34f8..bf10fb57f1a5c 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts @@ -297,7 +297,6 @@ describe('useApi', () => { await waitForNextUpdate(); await result.current.getExceptionListsItems({ - filterOptions: [], lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], @@ -313,7 +312,6 @@ describe('useApi', () => { }); const expected: ApiCallByListIdProps = { - filterOptions: [], http: mockKibanaHttpService, listIds: ['list_id'], namespaceTypes: ['single'], @@ -351,7 +349,6 @@ describe('useApi', () => { await waitForNextUpdate(); await result.current.getExceptionListsItems({ - filterOptions: [], lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], @@ -389,7 +386,6 @@ describe('useApi', () => { await waitForNextUpdate(); await result.current.getExceptionListsItems({ - filterOptions: [], lists: [ { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, ], diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts deleted file mode 100644 index d8ae72d4d6205..0000000000000 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.test.ts +++ /dev/null @@ -1,498 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act, renderHook } from '@testing-library/react-hooks'; -import type { - ExceptionListItemSchema, - UseExceptionListItemsSuccess, - UseExceptionListProps, -} from '@kbn/securitysolution-io-ts-list-types'; -import * as api from '@kbn/securitysolution-list-api'; -import { - ReturnExceptionListAndItems, - transformInput, - useExceptionListItems, -} from '@kbn/securitysolution-list-hooks'; -import { coreMock } from '@kbn/core/public/mocks'; - -import { getFoundExceptionListItemSchemaMock } from '../../../common/schemas/response/found_exception_list_item_schema.mock'; - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('123'), -})); -jest.mock('@kbn/securitysolution-list-api'); - -const mockKibanaHttpService = coreMock.createStart().http; - -// TODO: Port all of this test code over to the package of: packages/kbn-securitysolution-list-hooks/src/use_exception_list_items/index.test.ts once the mocks and kibana core mocks are figured out - -describe('useExceptionListItems', () => { - const onErrorMock = jest.fn(); - - beforeEach(() => { - jest - .spyOn(api, 'fetchExceptionListsItemsByListIds') - .mockResolvedValue(getFoundExceptionListItemSchemaMock()); - }); - - afterEach(() => { - onErrorMock.mockClear(); - jest.clearAllMocks(); - }); - - test('initializes hook', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >(() => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - await waitForNextUpdate(); - - expect(result.current).toEqual([ - true, - [], - { - page: 1, - perPage: 20, - total: 0, - }, - null, - ]); - }); - }); - - test('fetches exception items', async () => { - await act(async () => { - const onSuccessMock = jest.fn(); - const { result, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >(() => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - const expectedListItemsResult: ExceptionListItemSchema[] = - getFoundExceptionListItemSchemaMock().data.map((item) => transformInput(item)); - const expectedResult: UseExceptionListItemsSuccess = { - exceptions: expectedListItemsResult, - pagination: { page: 1, perPage: 1, total: 1 }, - }; - - expect(result.current).toEqual([ - false, - expectedListItemsResult, - { - page: 1, - perPage: 1, - total: 1, - }, - result.current[3], - ]); - expect(onSuccessMock).toHaveBeenCalledWith(expectedResult); - }); - }); - - test('fetches only detection list items if "showDetectionsListsOnly" is true', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - - await act(async () => { - const onSuccessMock = jest.fn(); - const { waitForNextUpdate } = renderHook( - () => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: true, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ - filterOptions: [], - http: mockKibanaHttpService, - listIds: ['list_id'], - namespaceTypes: ['single'], - pagination: { page: 1, perPage: 20 }, - signal: new AbortController().signal, - }); - }); - }); - - test('fetches only detection list items if "showEndpointListsOnly" is true', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - - await act(async () => { - const onSuccessMock = jest.fn(); - const { waitForNextUpdate } = renderHook( - () => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ - filterOptions: [], - http: mockKibanaHttpService, - listIds: ['list_id_endpoint'], - namespaceTypes: ['agnostic'], - pagination: { page: 1, perPage: 20 }, - signal: new AbortController().signal, - }); - }); - }); - - test('does not fetch items if no lists to fetch', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - - await act(async () => { - const onSuccessMock = jest.fn(); - const { result, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >(() => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).not.toHaveBeenCalled(); - expect(result.current).toEqual([ - false, - [], - { - page: 0, - perPage: 20, - total: 0, - }, - result.current[3], - ]); - }); - }); - - test('applies first filterOptions filter to all lists if "matchFilters" is true', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - - await act(async () => { - const onSuccessMock = jest.fn(); - const { waitForNextUpdate } = renderHook( - () => - useExceptionListItems({ - filterOptions: [{ filter: 'host.name', tags: [] }], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - { - id: 'myListIdEndpoint', - listId: 'list_id_endpoint', - namespaceType: 'agnostic', - type: 'endpoint', - }, - ], - matchFilters: true, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledWith({ - filterOptions: [ - { filter: 'host.name', tags: [] }, - { filter: 'host.name', tags: [] }, - ], - http: mockKibanaHttpService, - listIds: ['list_id', 'list_id_endpoint'], - namespaceTypes: ['single', 'agnostic'], - pagination: { page: 1, perPage: 20 }, - signal: new AbortController().signal, - }); - }); - }); - - test('fetches a new exception list and its items', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - const onSuccessMock = jest.fn(); - await act(async () => { - const { rerender, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >( - ({ - filterOptions, - http, - lists, - matchFilters, - pagination, - onError, - onSuccess, - showDetectionsListsOnly, - showEndpointListsOnly, - }) => - useExceptionListItems({ - filterOptions, - http, - lists, - matchFilters, - onError, - onSuccess, - pagination, - showDetectionsListsOnly, - showEndpointListsOnly, - }), - { - initialProps: { - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }, - } - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - rerender({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'newListId', listId: 'new_list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - onSuccess: onSuccessMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }); - // NOTE: Only need one call here because hook already initilaized - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(2); - }); - }); - - test('fetches list and items when refreshExceptionList callback invoked', async () => { - const spyOnfetchExceptionListsItemsByListIds = jest.spyOn( - api, - 'fetchExceptionListsItemsByListIds' - ); - await act(async () => { - const { result, waitForNextUpdate } = renderHook< - UseExceptionListProps, - ReturnExceptionListAndItems - >(() => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(typeof result.current[3]).toEqual('function'); - - if (result.current[3] != null) { - result.current[3](); - } - // NOTE: Only need one call here because hook already initilaized - await waitForNextUpdate(); - - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(2); - }); - }); - - test('invokes "onError" callback if "fetchExceptionListsItemsByListIds" fails', async () => { - const mockError = new Error('failed to fetches list items'); - const spyOnfetchExceptionListsItemsByListIds = jest - .spyOn(api, 'fetchExceptionListsItemsByListIds') - .mockRejectedValue(mockError); - await act(async () => { - const { waitForNextUpdate } = renderHook( - () => - useExceptionListItems({ - filterOptions: [], - http: mockKibanaHttpService, - lists: [ - { id: 'myListId', listId: 'list_id', namespaceType: 'single', type: 'detection' }, - ], - matchFilters: false, - onError: onErrorMock, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: false, - }) - ); - // NOTE: First `waitForNextUpdate` is initialization - // Second call applies the params - await waitForNextUpdate(); - await waitForNextUpdate(); - - expect(onErrorMock).toHaveBeenCalledWith(mockError); - expect(spyOnfetchExceptionListsItemsByListIds).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts index b28ebf26b012c..f77a3a7327d69 100644 --- a/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts +++ b/x-pack/plugins/lists/server/routes/find_exception_list_item_route.ts @@ -42,6 +42,7 @@ export const findExceptionListItemRoute = (router: ListsPluginRouter): void => { namespace_type: namespaceType, page, per_page: perPage, + search, sort_field: sortField, sort_order: sortOrder, } = request.query; @@ -59,6 +60,7 @@ export const findExceptionListItemRoute = (router: ListsPluginRouter): void => { page, perPage, pit: undefined, + search, searchAfter: undefined, sortField, sortOrder, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index c586a9d764147..baa9d943127f7 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -717,6 +717,7 @@ export class ExceptionListClient { perPage, pit, page, + search, searchAfter, sortField, sortOrder, @@ -750,6 +751,7 @@ export class ExceptionListClient { perPage, pit, savedObjectsClient, + search, searchAfter, sortField, sortOrder, @@ -764,6 +766,7 @@ export class ExceptionListClient { * @param options.perPage How many per page to return * @param options.pit The Point in Time (pit) id if there is one, otherwise "undefined" can be sent in * @param options.page The page number or "undefined" if there is no page number to continue from + * @param options.search The simple query search parameter if there is one, otherwise "undefined" can be sent in * @param options.searchAfter The search_after parameter if there is one, otherwise "undefined" can be sent in * @param options.sortField The sort field string if there is one, otherwise "undefined" can be sent in * @param options.sortOder The sort order string of "asc", "desc", otherwise "undefined" if there is no preference @@ -776,6 +779,7 @@ export class ExceptionListClient { perPage, pit, page, + search, searchAfter, sortField, sortOrder, @@ -793,6 +797,7 @@ export class ExceptionListClient { page, perPage, pit, + search, searchAfter, sortField, sortOrder, @@ -809,6 +814,7 @@ export class ExceptionListClient { perPage, pit, savedObjectsClient, + search, searchAfter, sortField, sortOrder, @@ -898,6 +904,7 @@ export class ExceptionListClient { * @param options.perPage How many per page to return * @param options.page The page number or "undefined" if there is no page number to continue from * @param options.pit The Point in Time (pit) id if there is one, otherwise "undefined" can be sent in + * @param options.search The simple query search parameter if there is one, otherwise "undefined" can be sent in * @param options.searchAfter The search_after parameter if there is one, otherwise "undefined" can be sent in * @param options.sortField The sort field string if there is one, otherwise "undefined" can be sent in * @param options.sortOrder The sort order of "asc" or "desc", otherwise "undefined" can be sent in @@ -908,6 +915,7 @@ export class ExceptionListClient { perPage, page, pit, + search, searchAfter, sortField, sortOrder, @@ -922,6 +930,7 @@ export class ExceptionListClient { perPage, pit, savedObjectsClient, + search, searchAfter, sortField, sortOrder, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index 2b3a800ac5a5a..048930e51b93d 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -46,6 +46,7 @@ import type { PitId, PitOrUndefined, SearchAfterOrUndefined, + SearchOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, Tags, @@ -361,6 +362,8 @@ export interface FindExceptionListItemOptions { perPage: PerPageOrUndefined; /** The Point in Time (pit) id if there is one, otherwise "undefined" can be send in */ pit?: PitOrUndefined; + /** The simple search parameter if there is one, otherwise "undefined" can be sent in */ + search?: SearchOrUndefined; /** The search_after parameter if there is one, otherwise "undefined" can be sent in */ searchAfter?: SearchAfterOrUndefined; /** The page number or "undefined" if there is no page number to continue from */ @@ -382,6 +385,8 @@ export interface FindEndpointListItemOptions { perPage: PerPageOrUndefined; /** The Point in Time (pit) id if there is one, otherwise "undefined" can be sent in */ pit?: PitOrUndefined; + /** The simple search parameter if there is one, otherwise "undefined" can be sent in */ + search?: SearchOrUndefined; /** The search_after parameter if there is one, otherwise "undefined" can be sent in */ searchAfter?: SearchAfterOrUndefined; /** The page number or "undefined" if there is no page number to continue from */ @@ -407,6 +412,8 @@ export interface FindExceptionListsItemOptions { perPage: PerPageOrUndefined; /** The Point in Time (pit) id if there is one, otherwise "undefined" can be sent in */ pit?: PitOrUndefined; + /** The simple search parameter if there is one, otherwise "undefined" can be sent in */ + search?: SearchOrUndefined; /** The search_after parameter if there is one, otherwise "undefined" can be sent in */ searchAfter?: SearchAfterOrUndefined; /** The page number or "undefined" if there is no page number to continue from */ diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts index c08057d77ebbe..fc41afd7563c7 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_item.ts @@ -15,6 +15,7 @@ import type { PerPageOrUndefined, PitOrUndefined, SearchAfterOrUndefined, + SearchOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; @@ -29,6 +30,7 @@ interface FindExceptionListItemOptions { page: PageOrUndefined; perPage: PerPageOrUndefined; pit: PitOrUndefined; + search: SearchOrUndefined; sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; searchAfter: SearchAfterOrUndefined; @@ -42,6 +44,7 @@ export const findExceptionListItem = async ({ page, perPage, pit, + search, searchAfter, sortField, sortOrder, @@ -54,6 +57,7 @@ export const findExceptionListItem = async ({ perPage, pit, savedObjectsClient, + search, searchAfter, sortField, sortOrder, diff --git a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts index f0e1fa07749f8..f3fd291ecd067 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/find_exception_list_items.ts @@ -13,6 +13,7 @@ import type { PerPageOrUndefined, PitOrUndefined, SearchAfterOrUndefined, + SearchOrUndefined, SortFieldOrUndefined, SortOrderOrUndefined, } from '@kbn/securitysolution-io-ts-list-types'; @@ -39,6 +40,7 @@ interface FindExceptionListItemsOptions { sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; searchAfter: SearchAfterOrUndefined; + search: SearchOrUndefined; } export const findExceptionListsItem = async ({ @@ -49,6 +51,7 @@ export const findExceptionListsItem = async ({ page, pit, perPage, + search, searchAfter, sortField, sortOrder, @@ -74,6 +77,7 @@ export const findExceptionListsItem = async ({ page, perPage, pit, + search, searchAfter, sortField, sortOrder, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e86e57e00ca34..6f3958cbb54e1 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -290,10 +290,13 @@ export const prebuiltSavedObjectsBulkCreateUrl = (templateName: string) => * Internal detection engine routes */ export const INTERNAL_DETECTION_ENGINE_URL = '/internal/detection_engine' as const; +export const INTERNAL_DETECTION_ENGINE_RULES_URL = '/internal/detection_engine/rules' as const; export const DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL = `${INTERNAL_DETECTION_ENGINE_URL}/fleet/integrations/installed` as const; export const DETECTION_ENGINE_ALERTS_INDEX_URL = `${INTERNAL_DETECTION_ENGINE_URL}/signal/index` as const; +export const DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL = + `${INTERNAL_DETECTION_ENGINE_RULES_URL}/exceptions/_find_references` as const; /** * Telemetry detection endpoint for any previews requested of what data we are * providing through UI/UX and for e2e tests. diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.test.ts new file mode 100644 index 0000000000000..425928d8fa208 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils'; +import { findExceptionReferencesOnRuleSchema } from './find_exception_list_references_schema'; +import type { FindExceptionReferencesOnRuleSchema } from './find_exception_list_references_schema'; + +describe('find_exception_list_references_schema', () => { + test('validates all fields', () => { + const payload: FindExceptionReferencesOnRuleSchema = { + ids: 'abc,def', + list_ids: '123,456', + namespace_types: 'single,agnostic', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + ids: ['abc', 'def'], + list_ids: ['123', '456'], + namespace_types: ['single', 'agnostic'], + }); + }); + + test('"ids" cannot be undefined', () => { + const payload: Omit = { + list_ids: '123,456', + namespace_types: 'single,agnostic', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['Invalid value "undefined" supplied to "ids"']); + expect(output.schema).toEqual({}); + }); + + test('"list_ids" cannot be undefined', () => { + const payload: Omit = { + ids: 'abc', + namespace_types: 'single', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([ + 'Invalid value "undefined" supplied to "list_ids"', + ]); + expect(output.schema).toEqual({}); + }); + + test('defaults "namespacetypes" to ["single"] if none set', () => { + const payload: Omit = { + ids: 'abc', + list_ids: '123', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + ids: ['abc'], + list_ids: ['123'], + namespace_types: ['single'], + }); + }); + + test('cannot add extra values', () => { + const payload: FindExceptionReferencesOnRuleSchema & { extra_value?: string } = { + ids: 'abc,def', + list_ids: '123,456', + namespace_types: 'single,agnostic', + extra_value: 'aaa', + }; + + const decoded = findExceptionReferencesOnRuleSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "extra_value"']); + expect(output.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.ts new file mode 100644 index 0000000000000..8cd21df8ab08b --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/find_exception_list_references_schema.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; +import { NonEmptyStringArray } from '@kbn/securitysolution-io-ts-types'; +import { DefaultNamespaceArray } from '@kbn/securitysolution-io-ts-list-types'; + +export const findExceptionReferencesOnRuleSchema = t.exact( + t.type({ + ids: NonEmptyStringArray, + list_ids: NonEmptyStringArray, + namespace_types: DefaultNamespaceArray, + }) +); + +export type FindExceptionReferencesOnRuleSchema = t.OutputOf< + typeof findExceptionReferencesOnRuleSchema +>; + +export type FindExceptionReferencesOnRuleSchemaDecoded = t.TypeOf< + typeof findExceptionReferencesOnRuleSchema +>; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.test.ts new file mode 100644 index 0000000000000..743645ba0f818 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { exactCheck, formatErrors, foldLeftRight } from '@kbn/securitysolution-io-ts-utils'; +import { + ruleReferenceSchema, + rulesReferencedByExceptionListsSchema, +} from './find_exception_list_references_schema'; +import type { + RuleReferenceSchema, + RulesReferencedByExceptionListsSchema, +} from './find_exception_list_references_schema'; + +describe('find_exception_list_references_schema', () => { + describe('ruleReferenceSchema', () => { + test('validates all fields', () => { + const payload: RuleReferenceSchema = { + name: 'My rule', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + rule_id: 'my-rule-id', + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }; + + const decoded = ruleReferenceSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + exception_lists: [ + { id: 'myListId', list_id: 'my-list-id', namespace_type: 'single', type: 'detection' }, + ], + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + name: 'My rule', + rule_id: 'my-rule-id', + }); + }); + + test('cannot add extra values', () => { + const payload: RuleReferenceSchema & { extra_value?: string } = { + name: 'My rule', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + rule_id: 'my-rule-id', + extra_value: 'foo', + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }; + + const decoded = ruleReferenceSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "extra_value"']); + expect(output.schema).toEqual({}); + }); + }); + + describe('rulesReferencedByExceptionListsSchema', () => { + test('validates all fields', () => { + const payload: RulesReferencedByExceptionListsSchema = { + references: [ + { + 'my-list-id': [ + { + name: 'My rule', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + rule_id: 'my-rule-id', + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }, + ], + }, + ], + }; + + const decoded = rulesReferencedByExceptionListsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + references: [ + { + 'my-list-id': [ + { + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + name: 'My rule', + rule_id: 'my-rule-id', + }, + ], + }, + ], + }); + }); + + test('validates "references" with empty array', () => { + const payload: RulesReferencedByExceptionListsSchema = { + references: [], + }; + + const decoded = rulesReferencedByExceptionListsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual([]); + expect(output.schema).toEqual({ + references: [], + }); + }); + + test('cannot add extra values', () => { + const payload: RulesReferencedByExceptionListsSchema & { extra_value?: string } = { + extra_value: 'foo', + references: [ + { + 'my-list-id': [ + { + name: 'My rule', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + rule_id: 'my-rule-id', + exception_lists: [ + { + id: 'myListId', + list_id: 'my-list-id', + namespace_type: 'single', + type: 'detection', + }, + ], + }, + ], + }, + ], + }; + + const decoded = rulesReferencedByExceptionListsSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const output = foldLeftRight(checked); + expect(formatErrors(output.errors)).toEqual(['invalid keys "extra_value"']); + expect(output.schema).toEqual({}); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.ts new file mode 100644 index 0000000000000..a7f2527edc096 --- /dev/null +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/find_exception_list_references_schema.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as t from 'io-ts'; + +import { listArray, list_id } from '@kbn/securitysolution-io-ts-list-types'; + +import { rule_id, id, name } from '../common/schemas'; + +export const ruleReferenceSchema = t.exact( + t.type({ + name, + id, + rule_id, + exception_lists: listArray, + }) +); + +export type RuleReferenceSchema = t.OutputOf; + +export const rulesReferencedByExceptionListSchema = t.record(list_id, t.array(ruleReferenceSchema)); + +export type RuleReferencesSchema = t.OutputOf; + +export const rulesReferencedByExceptionListsSchema = t.exact( + t.type({ + references: t.array(rulesReferencedByExceptionListSchema), + }) +); + +export type RulesReferencedByExceptionListsSchema = t.OutputOf< + typeof rulesReferencedByExceptionListsSchema +>; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts index 1b688ce641a7a..e12fbf2918302 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/index.ts @@ -7,6 +7,7 @@ export * from './error_schema'; export * from './get_installed_integrations_response_schema'; +export * from './find_exception_list_references_schema'; export * from './import_rules_schema'; export * from './prepackaged_rules_schema'; export * from './prepackaged_rules_status_schema'; diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_data_view_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_data_view_exception.spec.ts deleted file mode 100644 index c818f4e51060f..0000000000000 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_data_view_exception.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getException } from '../../objects/exception'; -import { getNewRule } from '../../objects/rule'; - -import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../screens/alerts'; - -import { addExceptionFromFirstAlert, goToClosedAlerts, goToOpenedAlerts } from '../../tasks/alerts'; -import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; -import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; -import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; -import { esArchiverLoad, esArchiverUnload, esArchiverResetKibana } from '../../tasks/es_archiver'; -import { login, visitWithoutDateRange } from '../../tasks/login'; -import { - addsException, - addsExceptionFromRuleSettings, - editException, - goToAlertsTab, - goToExceptionsTab, - removeException, - waitForTheRuleToBeExecuted, -} from '../../tasks/rule_details'; - -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -import { deleteAlertsAndRules, postDataView } from '../../tasks/common'; -import { - EXCEPTION_EDIT_FLYOUT_SAVE_BTN, - EXCEPTION_ITEM_CONTAINER, - FIELD_INPUT, -} from '../../screens/exceptions'; -import { - addExceptionEntryFieldValueOfItemX, - addExceptionEntryFieldValueValue, -} from '../../tasks/exceptions'; - -describe('Adds rule exception using data views', () => { - const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; - - before(() => { - esArchiverResetKibana(); - esArchiverLoad('exceptions'); - login(); - - postDataView('exceptions-*'); - }); - - beforeEach(() => { - deleteAlertsAndRules(); - createCustomRuleEnabled( - { - ...getNewRule(), - customQuery: 'agent.name:*', - dataSource: { dataView: 'exceptions-*', type: 'dataView' }, - }, - 'rule_testing', - '1s' - ); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - }); - - afterEach(() => { - esArchiverUnload('exceptions_2'); - }); - - after(() => { - esArchiverUnload('exceptions'); - }); - - it('Creates an exception from an alert and deletes it', () => { - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS); - // Create an exception from the alerts actions menu that matches - // the existing alert - addExceptionFromFirstAlert(); - addsException(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - }); - - it('Creates an exception from a rule and deletes it', () => { - // Create an exception from the exception tab that matches - // the existing alert - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - goToAlertsTab(); - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - }); - - it('Edits an exception', () => { - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - editException(); - - // check that the existing item's field is being populated - cy.get(EXCEPTION_ITEM_CONTAINER) - .eq(0) - .find(FIELD_INPUT) - .eq(0) - .should('have.text', 'agent.name'); - - // check that you can select a different field - addExceptionEntryFieldValueOfItemX('user.name{downarrow}{enter}', 0, 0); - addExceptionEntryFieldValueValue('test', 0); - - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click(); - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled'); - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist'); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_exception.spec.ts deleted file mode 100644 index 886f772bc25e5..0000000000000 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/add_edit_exception.spec.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getException } from '../../objects/exception'; -import { getNewRule } from '../../objects/rule'; - -import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../screens/alerts'; - -import { addExceptionFromFirstAlert, goToClosedAlerts, goToOpenedAlerts } from '../../tasks/alerts'; -import { createCustomRuleEnabled } from '../../tasks/api_calls/rules'; -import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; -import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; -import { esArchiverLoad, esArchiverUnload, esArchiverResetKibana } from '../../tasks/es_archiver'; -import { login, visitWithoutDateRange } from '../../tasks/login'; -import { - addsException, - addsExceptionFromRuleSettings, - editException, - goToAlertsTab, - goToExceptionsTab, - removeException, - waitForTheRuleToBeExecuted, -} from '../../tasks/rule_details'; - -import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; -import { deleteAlertsAndRules } from '../../tasks/common'; -import { - EXCEPTION_EDIT_FLYOUT_SAVE_BTN, - EXCEPTION_ITEM_CONTAINER, - FIELD_INPUT, -} from '../../screens/exceptions'; -import { - addExceptionEntryFieldValueOfItemX, - addExceptionEntryFieldValueValue, -} from '../../tasks/exceptions'; - -describe('Adds rule exception', () => { - const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; - - before(() => { - esArchiverResetKibana(); - esArchiverLoad('exceptions'); - login(); - }); - - beforeEach(() => { - deleteAlertsAndRules(); - createCustomRuleEnabled( - { - ...getNewRule(), - customQuery: 'agent.name:*', - dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, - }, - 'rule_testing', - '1s' - ); - visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); - goToRuleDetails(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - }); - - afterEach(() => { - esArchiverUnload('exceptions_2'); - }); - - after(() => { - esArchiverUnload('exceptions'); - }); - - it('Creates an exception from an alert and deletes it', () => { - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS); - // Create an exception from the alerts actions menu that matches - // the existing alert - addExceptionFromFirstAlert(); - addsException(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - }); - - it('Creates an exception from a rule and deletes it', () => { - // Create an exception from the exception tab that matches - // the existing alert - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - // Alerts table should now be empty from having added exception and closed - // matching alert - goToAlertsTab(); - cy.get(EMPTY_ALERT_TABLE).should('exist'); - - // Closed alert should appear in table - goToClosedAlerts(); - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - - // Remove the exception and load an event that would have matched that exception - // to show that said exception now starts to show up again - goToExceptionsTab(); - removeException(); - esArchiverLoad('exceptions_2'); - goToAlertsTab(); - goToOpenedAlerts(); - waitForTheRuleToBeExecuted(); - waitForAlertsToPopulate(); - - cy.get(ALERTS_COUNT).should('exist'); - cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); - }); - - it('Edits an exception', () => { - goToExceptionsTab(); - addsExceptionFromRuleSettings(getException()); - - editException(); - - // check that the existing item's field is being populated - cy.get(EXCEPTION_ITEM_CONTAINER) - .eq(0) - .find(FIELD_INPUT) - .eq(0) - .should('have.text', 'agent.name'); - - // check that you can select a different field - addExceptionEntryFieldValueOfItemX('user.name{downarrow}{enter}', 0, 0); - addExceptionEntryFieldValueValue('test', 0); - - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click(); - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled'); - cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist'); - }); -}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts new file mode 100644 index 0000000000000..f1d6d2f1cc063 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/alerts_table_flow/add_exception.spec.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getException } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; + +import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts'; + +import { + addExceptionFromFirstAlert, + goToClosedAlerts, + goToOpenedAlerts, +} from '../../../tasks/alerts'; +import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + addsException, + goToAlertsTab, + goToExceptionsTab, + removeException, + waitForTheRuleToBeExecuted, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { deleteAlertsAndRules } from '../../../tasks/common'; + +describe('Adds rule exception from alerts flow', () => { + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + }); + + beforeEach(() => { + deleteAlertsAndRules(); + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + }, + 'rule_testing', + '1s' + ); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + it('Creates an exception from an alert and deletes it', () => { + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS); + // Create an exception from the alerts actions menu that matches + // the existing alert + addExceptionFromFirstAlert(); + addsException(getException()); + + // Alerts table should now be empty from having added exception and closed + // matching alert + cy.get(EMPTY_ALERT_TABLE).should('exist'); + + // Closed alert should appear in table + goToClosedAlerts(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); + removeException(); + esArchiverLoad('exceptions_2'); + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts index 8c2e2af4b8bad..dfb018b4bfb5a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_flyout.spec.ts @@ -14,7 +14,7 @@ import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; import { esArchiverLoad, esArchiverResetKibana, esArchiverUnload } from '../../tasks/es_archiver'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { - openExceptionFlyoutFromRuleSettings, + openExceptionFlyoutFromEmptyViewerPrompt, goToExceptionsTab, editException, } from '../../tasks/rule_details'; @@ -34,7 +34,7 @@ import { FIELD_INPUT, LOADING_SPINNER, EXCEPTION_ITEM_CONTAINER, - ADD_EXCEPTIONS_BTN, + ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN, EXCEPTION_FIELD_LIST, EXCEPTION_EDIT_FLYOUT_SAVE_BTN, EXCEPTION_FLYOUT_VERSION_CONFLICT, @@ -94,7 +94,7 @@ describe('Exceptions flyout', () => { it('Validates empty entry values correctly', () => { cy.root() .pipe(($el) => { - $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); return $el.find(ADD_AND_BTN); }) .should('be.visible'); @@ -123,7 +123,7 @@ describe('Exceptions flyout', () => { it('Does not overwrite values and-ed together', () => { cy.root() .pipe(($el) => { - $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); return $el.find(ADD_AND_BTN); }) .should('be.visible'); @@ -146,7 +146,7 @@ describe('Exceptions flyout', () => { it('Does not overwrite values or-ed together', () => { cy.root() .pipe(($el) => { - $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); return $el.find(ADD_AND_BTN); }) .should('be.visible'); @@ -201,7 +201,7 @@ describe('Exceptions flyout', () => { }); it('Does not overwrite values of nested entry items', () => { - openExceptionFlyoutFromRuleSettings(); + openExceptionFlyoutFromEmptyViewerPrompt(); cy.get(LOADING_SPINNER).should('not.exist'); // exception item 1 @@ -267,7 +267,7 @@ describe('Exceptions flyout', () => { it('Contains custom index fields', () => { cy.root() .pipe(($el) => { - $el.find(ADD_EXCEPTIONS_BTN).trigger('click'); + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); return $el.find(ADD_AND_BTN); }) .should('be.visible'); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_management_flow/all_exception_lists_read_only.spec.ts similarity index 70% rename from x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts rename to x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_management_flow/all_exception_lists_read_only.spec.ts index e17bc694ab48a..c1c4fc18960e6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/exceptions/all_exception_lists_read_only.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/exceptions_management_flow/all_exception_lists_read_only.spec.ts @@ -5,14 +5,18 @@ * 2.0. */ -import { ROLES } from '../../../common/test'; -import { getExceptionList } from '../../objects/exception'; -import { EXCEPTIONS_TABLE_SHOWING_LISTS } from '../../screens/exceptions'; -import { createExceptionList } from '../../tasks/api_calls/exceptions'; -import { dismissCallOut, getCallOut, waitForCallOutToBeShown } from '../../tasks/common/callouts'; -import { esArchiverResetKibana } from '../../tasks/es_archiver'; -import { login, visitWithoutDateRange } from '../../tasks/login'; -import { EXCEPTIONS_URL } from '../../urls/navigation'; +import { ROLES } from '../../../../common/test'; +import { getExceptionList } from '../../../objects/exception'; +import { EXCEPTIONS_TABLE_SHOWING_LISTS } from '../../../screens/exceptions'; +import { createExceptionList } from '../../../tasks/api_calls/exceptions'; +import { + dismissCallOut, + getCallOut, + waitForCallOutToBeShown, +} from '../../../tasks/common/callouts'; +import { esArchiverResetKibana } from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { EXCEPTIONS_URL } from '../../../urls/navigation'; const MISSING_PRIVILEGES_CALLOUT = 'missing-user-privileges'; diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts new file mode 100644 index 0000000000000..add3f01798129 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception.spec.ts @@ -0,0 +1,221 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getException, getExceptionList } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; + +import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts'; +import { createCustomRule, createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { goToClosedAlerts, goToOpenedAlerts } from '../../../tasks/alerts'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + addExceptionFromRuleDetails, + addFirstExceptionFromRuleDetails, + goToAlertsTab, + goToExceptionsTab, + removeException, + searchForExceptionItem, + waitForTheRuleToBeExecuted, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { deleteAlertsAndRules } from '../../../tasks/common'; +import { + NO_EXCEPTIONS_EXIST_PROMPT, + EXCEPTION_ITEM_VIEWER_CONTAINER, + NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT, +} from '../../../screens/exceptions'; +import { + createExceptionList, + createExceptionListItem, + deleteExceptionList, +} from '../../../tasks/api_calls/exceptions'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; + +describe('Add exception from rule details', () => { + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + describe('rule with existing exceptions', () => { + const exceptionList = getExceptionList(); + beforeEach(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createCustomRule( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + exceptionLists: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + }, + '2' + ); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'user.name', + operator: 'included', + type: 'match_any', + value: ['bar'], + }, + ], + }); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item_2', + tags: [], + type: 'simple', + description: 'Test exception item 2', + name: 'Sample Exception List Item 2', + namespace_type: 'single', + entries: [ + { + field: 'unique_value.test', + operator: 'included', + type: 'match_any', + value: ['foo'], + }, + ], + }); + }); + + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); + + it('Creates an exception item', () => { + // displays existing exception items + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); + + // clicks prompt button to add a new exception item + addExceptionFromRuleDetails(getException()); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 3); + }); + + // Trying to figure out with EUI why the search won't trigger + it.skip('Can search for items', () => { + // displays existing exception items + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 2); + + // can search for an exception value + searchForExceptionItem('foo'); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // displays empty search result view if no matches found + searchForExceptionItem('abc'); + + // new exception item displays + cy.get(NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT).should('exist'); + }); + }); + + describe('rule without existing exceptions', () => { + beforeEach(() => { + deleteAlertsAndRules(); + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + }, + 'rule_testing', + '1s' + ); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + it('Creates an exception item when none exist', () => { + // when no exceptions exist, empty component shows with action to add exception + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // clicks prompt button to add first exception that will also select to close + // all matching alerts + addFirstExceptionFromRuleDetails({ + field: 'agent.name', + operator: 'is', + values: ['foo'], + }); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // Alerts table should now be empty from having added exception and closed + // matching alert + goToAlertsTab(); + cy.get(EMPTY_ALERT_TABLE).should('exist'); + + // Closed alert should appear in table + goToClosedAlerts(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); + + // when removing exception and again, no more exist, empty screen shows again + removeException(); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that there are no more exceptions, the docs should match and populate alerts + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts new file mode 100644 index 0000000000000..05b21abe52565 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/add_exception_data_view.spect.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getNewRule } from '../../../objects/rule'; +import { ALERTS_COUNT, EMPTY_ALERT_TABLE, NUMBER_OF_ALERTS } from '../../../screens/alerts'; +import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { goToClosedAlerts, goToOpenedAlerts } from '../../../tasks/alerts'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + addFirstExceptionFromRuleDetails, + goToAlertsTab, + goToExceptionsTab, + removeException, + waitForTheRuleToBeExecuted, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { postDataView, deleteAlertsAndRules } from '../../../tasks/common'; +import { + NO_EXCEPTIONS_EXIST_PROMPT, + EXCEPTION_ITEM_VIEWER_CONTAINER, +} from '../../../screens/exceptions'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; + +describe('Add exception using data views from rule details', () => { + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + postDataView('exceptions-*'); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + beforeEach(() => { + deleteAlertsAndRules(); + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { dataView: 'exceptions-*', type: 'dataView' }, + }, + 'rule_testing', + '1s' + ); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + goToExceptionsTab(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + it('Creates an exception item when none exist', () => { + // when no exceptions exist, empty component shows with action to add exception + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // clicks prompt button to add first exception that will also select to close + // all matching alerts + addFirstExceptionFromRuleDetails({ + field: 'agent.name', + operator: 'is', + values: ['foo'], + }); + + // new exception item displays + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // Alerts table should now be empty from having added exception and closed + // matching alert + goToAlertsTab(); + cy.get(EMPTY_ALERT_TABLE).should('exist'); + + // Closed alert should appear in table + goToClosedAlerts(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // Remove the exception and load an event that would have matched that exception + // to show that said exception now starts to show up again + goToExceptionsTab(); + + // when removing exception and again, no more exist, empty screen shows again + removeException(); + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that there are no more exceptions, the docs should match and populate alerts + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts new file mode 100644 index 0000000000000..26763c9efeebc --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception.spec.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getExceptionList } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; + +import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { goToOpenedAlerts } from '../../../tasks/alerts'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + goToExceptionsTab, + waitForTheRuleToBeExecuted, + editException, + goToAlertsTab, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { deleteAlertsAndRules } from '../../../tasks/common'; +import { + EXCEPTION_EDIT_FLYOUT_SAVE_BTN, + EXCEPTION_ITEM_VIEWER_CONTAINER, + EXCEPTION_ITEM_CONTAINER, + FIELD_INPUT, +} from '../../../screens/exceptions'; +import { + createExceptionList, + createExceptionListItem, + deleteExceptionList, +} from '../../../tasks/api_calls/exceptions'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { + addExceptionEntryFieldValueOfItemX, + addExceptionEntryFieldValueValue, +} from '../../../tasks/exceptions'; +import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../../../screens/alerts'; + +describe('Edit exception from rule details', () => { + const exceptionList = getExceptionList(); + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + beforeEach(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + exceptionLists: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + }, + '2', + '2s' + ); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'unique_value.test', + operator: 'included', + type: 'match_any', + value: ['bar'], + }, + ], + }); + }); + + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + goToExceptionsTab(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + it('Edits an exception item', () => { + // displays existing exception item + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + editException(); + + // check that the existing item's field is being populated + cy.get(EXCEPTION_ITEM_CONTAINER) + .eq(0) + .find(FIELD_INPUT) + .eq(0) + .should('have.text', 'unique_value.test'); + + // check that you can select a different field + addExceptionEntryFieldValueOfItemX('agent.name{downarrow}{enter}', 0, 0); + addExceptionEntryFieldValueValue('foo', 0); + + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click(); + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled'); + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // Alerts table should still show single alert + goToAlertsTab(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that 2 more docs have been added, one should match the edited exception + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(2); + + // there should be 2 alerts, one is the original alert and the second is for the newly + // matching doc + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts new file mode 100644 index 0000000000000..b5178615aa581 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/edit_exception_data_view.spec.ts @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getExceptionList } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; + +import { createCustomRuleEnabled } from '../../../tasks/api_calls/rules'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { goToOpenedAlerts } from '../../../tasks/alerts'; +import { + esArchiverLoad, + esArchiverUnload, + esArchiverResetKibana, +} from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { + goToExceptionsTab, + waitForTheRuleToBeExecuted, + editException, + goToAlertsTab, +} from '../../../tasks/rule_details'; + +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { postDataView, deleteAlertsAndRules } from '../../../tasks/common'; +import { + EXCEPTION_EDIT_FLYOUT_SAVE_BTN, + EXCEPTION_ITEM_VIEWER_CONTAINER, + EXCEPTION_ITEM_CONTAINER, + FIELD_INPUT, +} from '../../../screens/exceptions'; +import { + createExceptionList, + createExceptionListItem, + deleteExceptionList, +} from '../../../tasks/api_calls/exceptions'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { + addExceptionEntryFieldValueOfItemX, + addExceptionEntryFieldValueValue, +} from '../../../tasks/exceptions'; +import { ALERTS_COUNT, NUMBER_OF_ALERTS } from '../../../screens/alerts'; + +describe('Edit exception using data views from rule details', () => { + const exceptionList = getExceptionList(); + const NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS = '1 alert'; + + before(() => { + esArchiverResetKibana(); + esArchiverLoad('exceptions'); + login(); + postDataView('exceptions-*'); + }); + + after(() => { + esArchiverUnload('exceptions'); + }); + + beforeEach(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createCustomRuleEnabled( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { dataView: 'exceptions-*', type: 'dataView' }, + exceptionLists: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + }, + '2', + '2s' + ); + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'unique_value.test', + operator: 'included', + type: 'match_any', + value: ['bar'], + }, + ], + }); + }); + + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(); + goToExceptionsTab(); + }); + + afterEach(() => { + esArchiverUnload('exceptions_2'); + }); + + it('Edits an exception item', () => { + // displays existing exception item + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + editException(); + + // check that the existing item's field is being populated + cy.get(EXCEPTION_ITEM_CONTAINER) + .eq(0) + .find(FIELD_INPUT) + .eq(0) + .should('have.text', 'unique_value.test'); + + // check that you can select a different field + addExceptionEntryFieldValueOfItemX('agent.name{downarrow}{enter}', 0, 0); + addExceptionEntryFieldValueValue('foo', 0); + + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).click(); + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('have.attr', 'disabled'); + cy.get(EXCEPTION_EDIT_FLYOUT_SAVE_BTN).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // Alerts table should still show single alert + goToAlertsTab(); + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', `${NUMBER_OF_AUDITBEAT_EXCEPTIONS_ALERTS}`); + + // load more docs + esArchiverLoad('exceptions_2'); + + // now that 2 more docs have been added, one should match the edited exception + goToAlertsTab(); + goToOpenedAlerts(); + waitForTheRuleToBeExecuted(); + waitForAlertsToPopulate(2); + + // there should be 2 alerts, one is the original alert and the second is for the newly + // matching doc + cy.get(ALERTS_COUNT).should('exist'); + cy.get(NUMBER_OF_ALERTS).should('have.text', '2 alerts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts new file mode 100644 index 0000000000000..b11c688520b1a --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/exceptions/rule_details_flow/read_only_view.spect.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getExceptionList } from '../../../objects/exception'; +import { getNewRule } from '../../../objects/rule'; +import { ROLES } from '../../../../common/test'; +import { createCustomRule } from '../../../tasks/api_calls/rules'; +import { esArchiverResetKibana } from '../../../tasks/es_archiver'; +import { login, visitWithoutDateRange } from '../../../tasks/login'; +import { goToExceptionsTab, goToAlertsTab } from '../../../tasks/rule_details'; +import { goToRuleDetails } from '../../../tasks/alerts_detection_rules'; +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../../urls/navigation'; +import { deleteAlertsAndRules } from '../../../tasks/common'; +import { + NO_EXCEPTIONS_EXIST_PROMPT, + EXCEPTION_ITEM_VIEWER_CONTAINER, + ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER, + ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN, +} from '../../../screens/exceptions'; +import { EXCEPTION_ITEM_ACTIONS_BUTTON } from '../../../screens/rule_details'; +import { + createExceptionList, + createExceptionListItem, + deleteExceptionList, +} from '../../../tasks/api_calls/exceptions'; + +describe('Exceptions viewer read only', () => { + const exceptionList = getExceptionList(); + + before(() => { + esArchiverResetKibana(); + // create rule with exceptions + createExceptionList(exceptionList, exceptionList.list_id).then((response) => { + createCustomRule( + { + ...getNewRule(), + customQuery: 'agent.name:*', + dataSource: { index: ['exceptions*'], type: 'indexPatterns' }, + exceptionLists: [ + { + id: response.body.id, + list_id: exceptionList.list_id, + type: exceptionList.type, + namespace_type: exceptionList.namespace_type, + }, + ], + }, + '2' + ); + }); + + login(ROLES.reader); + visitWithoutDateRange(DETECTIONS_RULE_MANAGEMENT_URL, ROLES.reader); + goToRuleDetails(); + goToExceptionsTab(); + }); + + after(() => { + deleteAlertsAndRules(); + deleteExceptionList(exceptionList.list_id, exceptionList.namespace_type); + }); + + it('Cannot add an exception from empty viewer screen', () => { + // when no exceptions exist, empty component shows with action to add exception + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('exist'); + + // cannot add an exception from empty view + cy.get(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).should('have.attr', 'disabled'); + }); + + it('Cannot take actions on exception', () => { + createExceptionListItem(exceptionList.list_id, { + list_id: exceptionList.list_id, + item_id: 'simple_list_item', + tags: [], + type: 'simple', + description: 'Test exception item', + name: 'Sample Exception List Item', + namespace_type: 'single', + entries: [ + { + field: 'unique_value.test', + operator: 'included', + type: 'match_any', + value: ['bar'], + }, + ], + }); + + goToAlertsTab(); + goToExceptionsTab(); + + // can view exceptions + cy.get(NO_EXCEPTIONS_EXIST_PROMPT).should('not.exist'); + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', 1); + + // cannot access edit/delete actions of item + cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).should('have.attr', 'disabled'); + + // does not display add exception button + cy.get(ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER).should('not.exist'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index a02a318bfb307..1ca8ded946300 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -5,8 +5,6 @@ * 2.0. */ -export const ADD_EXCEPTIONS_BTN = '[data-test-subj="exceptionsHeaderAddExceptionBtn"]'; - export const CLOSE_ALERTS_CHECKBOX = '[data-test-subj="bulk-close-alert-on-add-add-exception-checkbox"]'; @@ -67,3 +65,20 @@ export const EXCEPTION_FLYOUT_VERSION_CONFLICT = '[data-test-subj="exceptionsFlyoutVersionConflict"]'; export const EXCEPTION_FLYOUT_LIST_DELETED_ERROR = '[data-test-subj="errorCalloutContainer"]'; + +// Exceptions all items view +export const NO_EXCEPTIONS_EXIST_PROMPT = + '[data-test-subj="exceptionItemViewerEmptyPrompts-empty-detection"]'; + +export const ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN = + '[data-test-subj="exceptionsEmptyPromptButton"]'; + +export const EXCEPTION_ITEM_VIEWER_CONTAINER = '[data-test-subj="exceptionItemContainer"]'; + +export const ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER = + '[data-test-subj="exceptionsHeaderAddExceptionBtn"]'; + +export const NO_EXCEPTIONS_SEARCH_RESULTS_PROMPT = + '[data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch"]'; + +export const EXCEPTION_ITEM_VIEWER_SEARCH = 'input[data-test-subj="exceptionsViewerSearchBar"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 989353cf7a253..80883d825c18f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -31,7 +31,7 @@ export const DETAILS_DESCRIPTION = '.euiDescriptionList__description'; export const DETAILS_TITLE = '.euiDescriptionList__title'; -export const EXCEPTIONS_TAB = '[data-test-subj="navigation-rule_exceptions"]'; +export const EXCEPTIONS_TAB = 'a[data-test-subj="navigation-rule_exceptions"]'; export const FALSE_POSITIVES_DETAILS = 'False positive examples'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index cb2fc257f7dc7..afee74e1a943b 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -218,6 +218,7 @@ export const createCustomRuleEnabled = ( query: rule.customQuery, language: 'kuery', enabled: true, + exceptions_list: rule.exceptionLists ?? [], tags: ['rule1'], max_signals: maxSignals, building_block_type: rule.buildingBlockType, @@ -243,6 +244,7 @@ export const createCustomRuleEnabled = ( query: rule.customQuery, language: 'kuery', enabled: true, + exceptions_list: rule.exceptionLists ?? [], tags: ['rule1'], max_signals: maxSignals, building_block_type: rule.buildingBlockType, diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index dcc4163fa9bf5..1cec924eae55a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -8,9 +8,11 @@ import type { Exception } from '../objects/exception'; import { RULE_STATUS } from '../screens/create_new_rule'; import { - ADD_EXCEPTIONS_BTN, + ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN, + ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER, CLOSE_ALERTS_CHECKBOX, CONFIRM_BTN, + EXCEPTION_ITEM_VIEWER_SEARCH, FIELD_INPUT, LOADING_SPINNER, OPERATOR_INPUT, @@ -65,14 +67,48 @@ export const addsFieldsToTimeline = (search: string, fields: string[]) => { closeFieldsBrowser(); }; -export const openExceptionFlyoutFromRuleSettings = () => { - cy.get(ADD_EXCEPTIONS_BTN).click(); - cy.get(LOADING_SPINNER).should('not.exist'); - cy.get(FIELD_INPUT).should('be.visible'); +export const openExceptionFlyoutFromEmptyViewerPrompt = () => { + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN_FROM_EMPTY_PROMPT_BTN).trigger('click'); + return $el.find(FIELD_INPUT); + }) + .should('be.visible'); +}; + +export const searchForExceptionItem = (query: string) => { + cy.get(EXCEPTION_ITEM_VIEWER_SEARCH).type(`${query}`).trigger('keydown', { + key: 'Enter', + keyCode: 13, + code: 'Enter', + type: 'keydown', + }); +}; + +export const addExceptionFlyoutFromViewerHeader = () => { + cy.root() + .pipe(($el) => { + $el.find(ADD_EXCEPTIONS_BTN_FROM_VIEWER_HEADER).trigger('click'); + return $el.find(FIELD_INPUT); + }) + .should('be.visible'); +}; + +export const addExceptionFromRuleDetails = (exception: Exception) => { + addExceptionFlyoutFromViewerHeader(); + cy.get(FIELD_INPUT).type(`${exception.field}{downArrow}{enter}`); + cy.get(OPERATOR_INPUT).type(`${exception.operator}{enter}`); + exception.values.forEach((value) => { + cy.get(VALUES_INPUT).type(`${value}{enter}`); + }); + cy.get(CLOSE_ALERTS_CHECKBOX).click({ force: true }); + cy.get(CONFIRM_BTN).click(); + cy.get(CONFIRM_BTN).should('have.attr', 'disabled'); + cy.get(CONFIRM_BTN).should('not.exist'); }; -export const addsExceptionFromRuleSettings = (exception: Exception) => { - openExceptionFlyoutFromRuleSettings(); +export const addFirstExceptionFromRuleDetails = (exception: Exception) => { + openExceptionFlyoutFromEmptyViewerPrompt(); cy.get(FIELD_INPUT).type(`${exception.field}{downArrow}{enter}`); cy.get(OPERATOR_INPUT).type(`${exception.operator}{enter}`); exception.values.forEach((value) => { @@ -89,13 +125,14 @@ export const goToAlertsTab = () => { }; export const goToExceptionsTab = () => { + cy.get(EXCEPTIONS_TAB).should('exist'); cy.get(EXCEPTIONS_TAB).click(); }; export const editException = () => { - cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).click({ force: true }); + cy.get(EXCEPTION_ITEM_ACTIONS_BUTTON).eq(0).click({ force: true }); - cy.get(EDIT_EXCEPTION_BTN).click({ force: true }); + cy.get(EDIT_EXCEPTION_BTN).eq(0).click({ force: true }); }; export const removeException = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts deleted file mode 100644 index 2372e063b48cf..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/translations.ts +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const DETECTION_LIST = i18n.translate( - 'xpack.securitySolution.exceptions.detectionListLabel', - { - defaultMessage: 'Detection list', - } -); - -export const ENDPOINT_LIST = i18n.translate('xpack.securitySolution.exceptions.endpointListLabel', { - defaultMessage: 'Endpoint list', -}); - -export const EDIT = i18n.translate('xpack.securitySolution.exceptions.editButtonLabel', { - defaultMessage: 'Edit', -}); - -export const REMOVE = i18n.translate('xpack.securitySolution.exceptions.removeButtonLabel', { - defaultMessage: 'Remove', -}); - -export const COMMENTS_SHOW = (comments: number) => - i18n.translate('xpack.securitySolution.exceptions.showCommentsLabel', { - values: { comments }, - defaultMessage: 'Show ({comments}) {comments, plural, =1 {Comment} other {Comments}}', - }); - -export const COMMENTS_HIDE = (comments: number) => - i18n.translate('xpack.securitySolution.exceptions.hideCommentsLabel', { - values: { comments }, - defaultMessage: 'Hide ({comments}) {comments, plural, =1 {Comment} other {Comments}}', - }); - -export const NAME = i18n.translate('xpack.securitySolution.exceptions.nameLabel', { - defaultMessage: 'Name', -}); - -export const COMMENT = i18n.translate('xpack.securitySolution.exceptions.commentLabel', { - defaultMessage: 'Comment', -}); - -export const COMMENT_EVENT = i18n.translate('xpack.securitySolution.exceptions.commentEventLabel', { - defaultMessage: 'added a comment', -}); - -export const OPERATING_SYSTEM_LABEL = i18n.translate( - 'xpack.securitySolution.exceptions.operatingSystemFullLabel', - { - defaultMessage: 'Operating System', - } -); - -export const SEARCH_DEFAULT = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder', - { - defaultMessage: 'Search field (ex: host.name)', - } -); - -export const ADD_EXCEPTION_LABEL = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.addExceptionLabel', - { - defaultMessage: 'Add new exception', - } -); - -export const ADD_TO_ENDPOINT_LIST = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.addToEndpointListLabel', - { - defaultMessage: 'Add Endpoint exception', - } -); - -export const ADD_TO_DETECTIONS_LIST = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel', - { - defaultMessage: 'Add rule exception', - } -); - -export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.emptyPromptTitle', - { - defaultMessage: 'This rule has no exceptions', - } -); - -export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody', - { - defaultMessage: 'No search results found.', - } -); - -export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.emptyPromptBody', - { - defaultMessage: - 'You can add exceptions to fine tune the rule so that detection alerts are not created when exception conditions are met. Exceptions improve detection accuracy, which can help reduce the number of false positives.', - } -); - -export const FETCH_LIST_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.fetchingListError', - { - defaultMessage: 'Error fetching exceptions', - } -); - -export const DELETE_EXCEPTION_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.deleteExceptionError', - { - defaultMessage: 'Error deleting exception', - } -); - -export const ITEMS_PER_PAGE = (items: number) => - i18n.translate('xpack.securitySolution.exceptions.exceptionsPaginationLabel', { - values: { items }, - defaultMessage: 'Items per page: {items}', - }); - -export const NUMBER_OF_ITEMS = (items: number) => - i18n.translate('xpack.securitySolution.exceptions.paginationNumberOfItemsLabel', { - values: { items }, - defaultMessage: '{items} items', - }); - -export const REFRESH = i18n.translate('xpack.securitySolution.exceptions.utilityRefreshLabel', { - defaultMessage: 'Refresh', -}); - -export const SHOWING_EXCEPTIONS = (items: number) => - i18n.translate('xpack.securitySolution.exceptions.utilityNumberExceptionsLabel', { - values: { items }, - defaultMessage: 'Showing {items} {items, plural, =1 {exception} other {exceptions}}', - }); - -export const FIELD = i18n.translate('xpack.securitySolution.exceptions.fieldDescription', { - defaultMessage: 'Field', -}); - -export const OPERATOR = i18n.translate('xpack.securitySolution.exceptions.operatorDescription', { - defaultMessage: 'Operator', -}); - -export const VALUE = i18n.translate('xpack.securitySolution.exceptions.valueDescription', { - defaultMessage: 'Value', -}); - -export const AND = i18n.translate('xpack.securitySolution.exceptions.andDescription', { - defaultMessage: 'AND', -}); - -export const OR = i18n.translate('xpack.securitySolution.exceptions.orDescription', { - defaultMessage: 'OR', -}); - -export const ADD_COMMENT_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.addCommentPlaceholder', - { - defaultMessage: 'Add a new comment...', - } -); - -export const ADD_TO_CLIPBOARD = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.addToClipboard', - { - defaultMessage: 'Comment', - } -); - -export const DESCRIPTION = i18n.translate('xpack.securitySolution.exceptions.descriptionLabel', { - defaultMessage: 'Description', -}); - -export const TOTAL_ITEMS_FETCH_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.viewer.fetchTotalsError', - { - defaultMessage: 'Error getting exception item totals', - } -); - -export const CLEAR_EXCEPTIONS_LABEL = i18n.translate( - 'xpack.securitySolution.exceptions.clearExceptionsLabel', - { - defaultMessage: 'Remove Exception List', - } -); - -export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) => - i18n.translate('xpack.securitySolution.exceptions.fetch404Error', { - values: { listId }, - defaultMessage: - 'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.', - }); - -export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.fetchError', - { - defaultMessage: 'Error fetching exception list', - } -); - -export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', { - defaultMessage: 'Error', -}); - -export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', { - defaultMessage: 'Cancel', -}); - -export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate( - 'xpack.securitySolution.exceptions.modalErrorAccordionText', - { - defaultMessage: 'Show rule reference information:', - } -); - -export const DISSASOCIATE_LIST_SUCCESS = (id: string) => - i18n.translate('xpack.securitySolution.exceptions.dissasociateListSuccessText', { - values: { id }, - defaultMessage: 'Exception list ({id}) has successfully been removed', - }); - -export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate( - 'xpack.securitySolution.exceptions.dissasociateExceptionListError', - { - defaultMessage: 'Failed to remove exception list', - } -); - -export const OPERATING_SYSTEM_WINDOWS = i18n.translate( - 'xpack.securitySolution.exceptions.operatingSystemWindows', - { - defaultMessage: 'Windows', - } -); - -export const OPERATING_SYSTEM_MAC = i18n.translate( - 'xpack.securitySolution.exceptions.operatingSystemMac', - { - defaultMessage: 'macOS', - } -); - -export const OPERATING_SYSTEM_WINDOWS_AND_MAC = i18n.translate( - 'xpack.securitySolution.exceptions.operatingSystemWindowsAndMac', - { - defaultMessage: 'Windows and macOS', - } -); - -export const OPERATING_SYSTEM_LINUX = i18n.translate( - 'xpack.securitySolution.exceptions.operatingSystemLinux', - { - defaultMessage: 'Linux', - } -); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx deleted file mode 100644 index b5a24ef3e472d..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; - -import { TestProviders } from '../../../../mock'; -import { ExceptionItemCardMetaInfo } from './exception_item_card_meta'; - -describe('ExceptionItemCardMetaInfo', () => { - it('it renders item creation info', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="exceptionItemMeta-createdBy-value1"]').at(0).text() - ).toEqual('Apr 20, 2020 @ 15:25:31.830'); - expect( - wrapper.find('[data-test-subj="exceptionItemMeta-createdBy-value2"]').at(0).text() - ).toEqual('some user'); - }); - - it('it renders item update info', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value1"]').at(0).text() - ).toEqual('Apr 20, 2020 @ 15:25:31.830'); - expect( - wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value2"]').at(0).text() - ).toEqual('some user'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx deleted file mode 100644 index 3e9cf5e68d95e..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_meta.tsx +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { memo } from 'react'; -import { EuiAvatar, EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import styled from 'styled-components'; - -import * as i18n from './translations'; -import { FormattedDate, FormattedRelativePreferenceDate } from '../../../formatted_date'; - -const StyledCondition = styled('div')` - padding-top: 4px !important; -`; -export interface ExceptionItemCardMetaInfoProps { - item: ExceptionListItemSchema; - dataTestSubj: string; -} - -export const ExceptionItemCardMetaInfo = memo( - ({ item, dataTestSubj }) => { - return ( - - - } - value2={item.created_by} - dataTestSubj={`${dataTestSubj}-createdBy`} - /> - - - - - - } - value2={item.updated_by} - dataTestSubj={`${dataTestSubj}-updatedBy`} - /> - - - ); - } -); -ExceptionItemCardMetaInfo.displayName = 'ExceptionItemCardMetaInfo'; - -interface MetaInfoDetailsProps { - fieldName: string; - label: string; - value1: JSX.Element | string; - value2: string; - dataTestSubj: string; -} - -const MetaInfoDetails = memo(({ label, value1, value2, dataTestSubj }) => { - return ( - - - - {label} - - - - - {value1} - - - - - {i18n.EXCEPTION_ITEM_META_BY} - - - - - - - - - - {value2} - - - - - - ); -}); - -MetaInfoDetails.displayName = 'MetaInfoDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx deleted file mode 100644 index 4d38fac340727..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import { ExceptionsViewerPagination } from './exceptions_pagination'; - -describe('ExceptionsViewerPagination', () => { - it('it renders passed in "pageSize" as selected option', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsPerPageBtn"]').at(0).text()).toEqual( - 'Items per page: 50' - ); - }); - - it('it renders all passed in page size options when per page button clicked', () => { - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); - - expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(0).text()).toEqual( - '20 items' - ); - expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(1).text()).toEqual( - '50 items' - ); - expect(wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(2).text()).toEqual( - '100 items' - ); - }); - - it('it invokes "onPaginationChange" when per page item is clicked', () => { - const mockOnPaginationChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsPerPageBtn"] button').simulate('click'); - wrapper.find('button[data-test-subj="exceptionsPerPageItem"]').at(0).simulate('click'); - - expect(mockOnPaginationChange).toHaveBeenCalledWith({ - pagination: { pageIndex: 0, pageSize: 20, totalItemCount: 1 }, - }); - }); - - it('it renders correct total page count', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('pageCount')).toEqual( - 4 - ); - expect( - wrapper.find('[data-test-subj="exceptionsPagination"]').at(0).prop('activePage') - ).toEqual(0); - }); - - it('it invokes "onPaginationChange" when next clicked', () => { - const mockOnPaginationChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="pagination-button-next"]').at(1).simulate('click'); - - expect(mockOnPaginationChange).toHaveBeenCalledWith({ - pagination: { pageIndex: 1, pageSize: 50, totalItemCount: 160 }, - }); - }); - - it('it invokes "onPaginationChange" when page clicked', () => { - const mockOnPaginationChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('button[data-test-subj="pagination-button-3"]').simulate('click'); - - expect(mockOnPaginationChange).toHaveBeenCalledWith({ - pagination: { pageIndex: 3, pageSize: 50, totalItemCount: 160 }, - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx deleted file mode 100644 index c64130e7eb56d..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_pagination.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ReactElement } from 'react'; -import React, { useCallback, useState, useMemo } from 'react'; -import { - EuiContextMenuItem, - EuiButtonEmpty, - EuiPagination, - EuiFlexItem, - EuiFlexGroup, - EuiPopover, - EuiContextMenuPanel, -} from '@elastic/eui'; - -import * as i18n from '../translations'; -import type { ExceptionsPagination, Filter } from '../types'; - -interface ExceptionsViewerPaginationProps { - pagination: ExceptionsPagination; - onPaginationChange: (arg: Partial) => void; -} - -const ExceptionsViewerPaginationComponent = ({ - pagination, - onPaginationChange, -}: ExceptionsViewerPaginationProps): JSX.Element => { - const [isOpen, setIsOpen] = useState(false); - - const handleClosePerPageMenu = useCallback((): void => setIsOpen(false), [setIsOpen]); - - const handlePerPageMenuClick = useCallback( - (): void => setIsOpen((isPopoverOpen) => !isPopoverOpen), - [setIsOpen] - ); - - const handlePageClick = useCallback( - (pageIndex: number): void => { - onPaginationChange({ - pagination: { - pageIndex, - pageSize: pagination.pageSize, - totalItemCount: pagination.totalItemCount, - }, - }); - }, - [pagination, onPaginationChange] - ); - - const items = useMemo((): ReactElement[] => { - return pagination.pageSizeOptions.map((rows) => ( - { - onPaginationChange({ - pagination: { - pageIndex: 0, - pageSize: rows, - totalItemCount: pagination.totalItemCount, - }, - }); - handleClosePerPageMenu(); - }} - data-test-subj="exceptionsPerPageItem" - > - {i18n.NUMBER_OF_ITEMS(rows)} - - )); - }, [pagination, onPaginationChange, handleClosePerPageMenu]); - - const totalPages = useMemo((): number => { - if (pagination.totalItemCount > 0) { - return Math.ceil(pagination.totalItemCount / pagination.pageSize); - } else { - return 1; - } - }, [pagination]); - - return ( - - - - {i18n.ITEMS_PER_PAGE(pagination.pageSize)} - - } - isOpen={isOpen} - closePopover={handleClosePerPageMenu} - panelPaddingSize="none" - repositionOnScroll - > - - - - - - - - - ); -}; - -ExceptionsViewerPaginationComponent.displayName = 'ExceptionsViewerPaginationComponent'; - -export const ExceptionsViewerPagination = React.memo(ExceptionsViewerPaginationComponent); - -ExceptionsViewerPagination.displayName = 'ExceptionsViewerPagination'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx deleted file mode 100644 index f600fe28f3ecb..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.test.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { mountWithIntl } from '@kbn/test-jest-helpers'; - -import { ExceptionsViewerUtility } from './exceptions_utility'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ - eui: { - euiBreakpoints: { - l: '1200px', - }, - euiSizeM: '10px', - euiBorderThin: '1px solid #ece', - }, -}); - -describe('ExceptionsViewerUtility', () => { - it('it renders correct pluralized text when more than one exception exists', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsShowing"]').at(0).text()).toEqual( - 'Showing 2 exceptions' - ); - }); - - it('it renders correct singular text when less than two exceptions exists', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsShowing"]').at(0).text()).toEqual( - 'Showing 1 exception' - ); - }); - - it('it invokes "onRefreshClick" when refresh button clicked', () => { - const mockOnRefreshClick = jest.fn(); - const wrapper = mountWithIntl( - - - - ); - - wrapper.find('[data-test-subj="exceptionsRefresh"] button').simulate('click'); - - expect(mockOnRefreshClick).toHaveBeenCalledTimes(1); - }); - - it('it does not render any messages when "showEndpointList" and "showDetectionsList" are "false"', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeFalsy(); - }); - - it('it does render detections messages when "showDetectionsList" is "true"', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeTruthy(); - }); - - it('it does render endpoint messages when "showEndpointList" is "true"', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsEndpointMessage"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="exceptionsDetectionsMessage"]').exists()).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx deleted file mode 100644 index fd720377a1b1e..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_utility.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiText, EuiLink, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; -import styled from 'styled-components'; - -import * as i18n from '../translations'; -import type { ExceptionsPagination } from '../types'; -import { - UtilityBar, - UtilityBarSection, - UtilityBarGroup, - UtilityBarText, - UtilityBarAction, -} from '../../utility_bar'; - -const StyledText = styled(EuiText)` - font-style: italic; -`; - -const MyUtilities = styled(EuiFlexGroup)` - height: 50px; -`; - -interface ExceptionsViewerUtilityProps { - pagination: ExceptionsPagination; - showEndpointListsOnly: boolean; - showDetectionsListsOnly: boolean; - ruleSettingsUrl: string; - onRefreshClick: () => void; -} - -const ExceptionsViewerUtilityComponent: React.FC = ({ - pagination, - showEndpointListsOnly, - showDetectionsListsOnly, - ruleSettingsUrl, - onRefreshClick, -}): JSX.Element => ( - - - - - - - {i18n.SHOWING_EXCEPTIONS(pagination.totalItemCount ?? 0)} - - - - - - {i18n.REFRESH} - - - - - - - - {showEndpointListsOnly && ( - - - - ), - }} - /> - )} - {showDetectionsListsOnly && ( - - - - ), - }} - /> - )} - - - -); - -ExceptionsViewerUtilityComponent.displayName = 'ExceptionsViewerUtilityComponent'; - -export const ExceptionsViewerUtility = React.memo(ExceptionsViewerUtilityComponent); - -ExceptionsViewerUtility.displayName = 'ExceptionsViewerUtility'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx deleted file mode 100644 index 05cbe352fa72e..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { storiesOf, addDecorator } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { euiLightVars } from '@kbn/ui-theme'; - -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { ExceptionsViewerHeader } from './exceptions_viewer_header'; - -addDecorator((storyFn) => ( - ({ eui: euiLightVars, darkMode: false })}>{storyFn()} -)); - -storiesOf('Components/ExceptionsViewerHeader', module) - .add('loading', () => { - return ( - - ); - }) - .add('all lists', () => { - return ( - - ); - }) - .add('endpoint only', () => { - return ( - - ); - }) - .add('detections only', () => { - return ( - - ); - }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx deleted file mode 100644 index d19a81e222423..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.test.tsx +++ /dev/null @@ -1,293 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import { ExceptionsViewerHeader } from './exceptions_viewer_header'; - -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; - -describe('ExceptionsViewerHeader', () => { - it('it renders all disabled if "isInitLoading" is true', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find('input[data-test-subj="exceptionsHeaderSearch"]').at(0).prop('disabled') - ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').at(0).prop('disabled') - ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').at(0).prop('disabled') - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') - .at(0) - .prop('disabled') - ).toBeTruthy(); - }); - - // This occurs if user does not have sufficient privileges - it('it does not display add exception button if no list types available', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').exists()).toBeFalsy(); - }); - - it('it displays toggles and add exception popover when more than one list type available', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]').exists()).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"]').exists() - ).toBeTruthy(); - }); - - it('it does not display toggles and add exception popover if only one list type is available', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="exceptionsFilterGroupBtns"]')).toHaveLength(0); - expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"]')).toHaveLength( - 0 - ); - }); - - it('it displays add exception button without popover if only one list type is available', () => { - const wrapper = mount( - - ); - - expect( - wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').exists() - ).toBeTruthy(); - }); - - it('it renders detections filter toggle selected when clicked', () => { - const mockOnFilterChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsDetectionFilterBtn"] button').simulate('click'); - - expect( - wrapper - .find('EuiFilterButton[data-test-subj="exceptionsDetectionFilterBtn"]') - .at(0) - .prop('hasActiveFilters') - ).toBeTruthy(); - expect( - wrapper - .find('EuiFilterButton[data-test-subj="exceptionsEndpointFilterBtn"]') - .at(0) - .prop('hasActiveFilters') - ).toBeFalsy(); - expect(mockOnFilterChange).toHaveBeenCalledWith({ - filter: { - filter: '', - tags: [], - }, - pagination: { - pageIndex: 0, - }, - showDetectionsListsOnly: true, - showEndpointListsOnly: false, - }); - }); - - it('it renders endpoint filter toggle selected and invokes "onFilterChange" when clicked', () => { - const mockOnFilterChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsEndpointFilterBtn"] button').simulate('click'); - - expect( - wrapper - .find('EuiFilterButton[data-test-subj="exceptionsEndpointFilterBtn"]') - .at(0) - .prop('hasActiveFilters') - ).toBeTruthy(); - expect( - wrapper - .find('EuiFilterButton[data-test-subj="exceptionsDetectionFilterBtn"]') - .at(0) - .prop('hasActiveFilters') - ).toBeFalsy(); - expect(mockOnFilterChange).toHaveBeenCalledWith({ - filter: { - filter: '', - tags: [], - }, - pagination: { - pageIndex: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - }); - }); - - it('it invokes "onAddExceptionClick" when user selects to add an exception item and only endpoint exception lists are available', () => { - const mockOnAddExceptionClick = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); - - expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); - }); - - it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item and only endpoint detections lists are available', () => { - const mockOnAddExceptionClick = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); - - expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); - }); - - it('it invokes "onAddEndpointExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { - const mockOnAddExceptionClick = jest.fn(); - const wrapper = mount( - - ); - - wrapper - .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') - .simulate('click'); - wrapper.find('[data-test-subj="addEndpointExceptionBtn"] button').simulate('click'); - - expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); - }); - - it('it invokes "onAddDetectionsExceptionClick" when user selects to add an exception item to endpoint list from popover', () => { - const mockOnAddExceptionClick = jest.fn(); - const wrapper = mount( - - ); - - wrapper - .find('[data-test-subj="exceptionsHeaderAddExceptionPopoverBtn"] button') - .simulate('click'); - wrapper.find('[data-test-subj="addDetectionsExceptionBtn"] button').simulate('click'); - - expect(mockOnAddExceptionClick).toHaveBeenCalledTimes(1); - }); - - it('it invokes "onFilterChange" when search used and "Enter" pressed', () => { - const mockOnFilterChange = jest.fn(); - const wrapper = mount( - - ); - - wrapper.find('EuiFieldSearch').at(0).simulate('keyup', { - charCode: 13, - code: 'Enter', - key: 'Enter', - }); - - expect(mockOnFilterChange).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx deleted file mode 100644 index dc234dc0a7ef4..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.tsx +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; -import { - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiContextMenu, - EuiButton, - EuiFilterGroup, - EuiFilterButton, -} from '@elastic/eui'; -import React, { useEffect, useState, useCallback, useMemo } from 'react'; - -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import * as i18n from '../translations'; -import type { Filter } from '../types'; - -interface ExceptionsViewerHeaderProps { - isInitLoading: boolean; - supportedListTypes: ExceptionListTypeEnum[]; - detectionsListItems: number; - endpointListItems: number; - onFilterChange: (arg: Partial) => void; - onAddExceptionClick: (type: ExceptionListTypeEnum) => void; -} - -/** - * Collection of filters and toggles for filtering exception items. - */ -const ExceptionsViewerHeaderComponent = ({ - isInitLoading, - supportedListTypes, - detectionsListItems, - endpointListItems, - onFilterChange, - onAddExceptionClick, -}: ExceptionsViewerHeaderProps): JSX.Element => { - const [filter, setFilter] = useState(''); - const [tags, setTags] = useState([]); - const [showDetectionsListsOnly, setShowDetectionsList] = useState(false); - const [showEndpointListsOnly, setShowEndpointList] = useState(false); - const [isAddExceptionMenuOpen, setAddExceptionMenuOpen] = useState(false); - - useEffect((): void => { - onFilterChange({ - filter: { filter, tags }, - pagination: { - pageIndex: 0, - }, - showDetectionsListsOnly, - showEndpointListsOnly, - }); - }, [filter, tags, showDetectionsListsOnly, showEndpointListsOnly, onFilterChange]); - - const onAddExceptionDropdownClick = useCallback( - (): void => setAddExceptionMenuOpen(!isAddExceptionMenuOpen), - [setAddExceptionMenuOpen, isAddExceptionMenuOpen] - ); - - const handleDetectionsListClick = useCallback((): void => { - setShowDetectionsList(!showDetectionsListsOnly); - setShowEndpointList(false); - }, [showDetectionsListsOnly, setShowDetectionsList, setShowEndpointList]); - - const handleEndpointListClick = useCallback((): void => { - setShowEndpointList(!showEndpointListsOnly); - setShowDetectionsList(false); - }, [showEndpointListsOnly, setShowEndpointList, setShowDetectionsList]); - - const handleOnSearch = useCallback( - (searchValue: string): void => { - const tagsRegex = /(tags:[^\s]*)/i; - const tagsMatch = searchValue.match(tagsRegex); - const foundTags: string = tagsMatch != null ? tagsMatch[0].split(':')[1] : ''; - const filterString = tagsMatch != null ? searchValue.replace(tagsRegex, '') : searchValue; - - if (foundTags.length > 0) { - setTags(foundTags.split(',')); - } - - setFilter(filterString.trim()); - }, - [setTags, setFilter] - ); - - const onAddException = useCallback( - (type: ExceptionListTypeEnum): void => { - onAddExceptionClick(type); - setAddExceptionMenuOpen(false); - }, - [onAddExceptionClick, setAddExceptionMenuOpen] - ); - - const addExceptionButtonOptions = useMemo( - (): EuiContextMenuPanelDescriptor[] => [ - { - id: 0, - items: [ - { - name: i18n.ADD_TO_ENDPOINT_LIST, - onClick: () => onAddException(ExceptionListTypeEnum.ENDPOINT), - 'data-test-subj': 'addEndpointExceptionBtn', - }, - { - name: i18n.ADD_TO_DETECTIONS_LIST, - onClick: () => onAddException(ExceptionListTypeEnum.DETECTION), - 'data-test-subj': 'addDetectionsExceptionBtn', - }, - ], - }, - ], - [onAddException] - ); - - return ( - - - - - - {supportedListTypes.length === 1 && ( - - onAddException(supportedListTypes[0])} - isDisabled={isInitLoading} - fill - > - {i18n.ADD_EXCEPTION_LABEL} - - - )} - - {supportedListTypes.length > 1 && ( - - - - - - {i18n.DETECTION_LIST} - {detectionsListItems != null ? ` (${detectionsListItems})` : ''} - - - {i18n.ENDPOINT_LIST} - {endpointListItems != null ? ` (${endpointListItems})` : ''} - - - - - - - {i18n.ADD_EXCEPTION_LABEL} - - } - isOpen={isAddExceptionMenuOpen} - closePopover={onAddExceptionDropdownClick} - anchorPosition="downCenter" - panelPaddingSize="none" - repositionOnScroll - > - - - - - - )} - - ); -}; - -ExceptionsViewerHeaderComponent.displayName = 'ExceptionsViewerHeaderComponent'; - -export const ExceptionsViewerHeader = React.memo(ExceptionsViewerHeaderComponent); - -ExceptionsViewerHeader.displayName = 'ExceptionsViewerHeader'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx deleted file mode 100644 index 22c6e7dbf8ecf..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.test.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { mount } from 'enzyme'; - -import * as i18n from '../translations'; -import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { ExceptionsViewerItems } from './exceptions_viewer_items'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; -import { TestProviders } from '../../../mock'; - -const mockTheme = getMockTheme({ - eui: { - euiSize: '10px', - euiColorPrimary: '#ece', - euiColorDanger: '#ece', - }, -}); - -describe('ExceptionsViewerItems', () => { - it('it renders empty prompt if "showEmpty" is "true"', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptTitle"]').last().text()).toEqual( - i18n.EXCEPTION_EMPTY_PROMPT_TITLE - ); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').text()).toEqual( - i18n.EXCEPTION_EMPTY_PROMPT_BODY - ); - }); - - it('it renders no search results found prompt if "showNoResults" is "true"', () => { - const wrapper = mount( - - - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptTitle"]').last().text()).toEqual(''); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').text()).toEqual( - i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY - ); - }); - - it('it renders exceptions if "showEmpty" and "isInitLoading" is "false", and exceptions exist', () => { - const wrapper = mount( - - - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeFalsy(); - }); - - it('it does not render exceptions if "isInitLoading" is "true"', () => { - const wrapper = mount( - - - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx deleted file mode 100644 index e1d91ed0a0580..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_items.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import styled from 'styled-components'; - -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import * as i18n from '../translations'; -import { ExceptionItemCard } from './exception_item_card'; -import type { ExceptionListItemIdentifiers } from '../types'; - -const MyFlexItem = styled(EuiFlexItem)` - margin: ${({ theme }) => `${theme.eui.euiSize} 0`}; - - &:first-child { - margin: ${({ theme }) => `${theme.eui.euiSizeXS} 0 ${theme.eui.euiSize}`}; - } -`; - -const MyExceptionItemContainer = styled(EuiFlexGroup)` - margin: ${({ theme }) => `0 ${theme.eui.euiSize} ${theme.eui.euiSize} 0`}; -`; - -interface ExceptionsViewerItemsProps { - showEmpty: boolean; - showNoResults: boolean; - isInitLoading: boolean; - disableActions: boolean; - exceptions: ExceptionListItemSchema[]; - loadingItemIds: ExceptionListItemIdentifiers[]; - onDeleteException: (arg: ExceptionListItemIdentifiers) => void; - onEditExceptionItem: (item: ExceptionListItemSchema) => void; -} - -const ExceptionsViewerItemsComponent: React.FC = ({ - showEmpty, - showNoResults, - isInitLoading, - exceptions, - loadingItemIds, - onDeleteException, - onEditExceptionItem, - disableActions, -}): JSX.Element => ( - - {showEmpty || showNoResults || isInitLoading ? ( - - - {showNoResults ? '' : i18n.EXCEPTION_EMPTY_PROMPT_TITLE} - - } - body={ -

- {showNoResults - ? i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY - : i18n.EXCEPTION_EMPTY_PROMPT_BODY} -

- } - data-test-subj="exceptionsEmptyPrompt" - /> -
- ) : ( - - - {!isInitLoading && - exceptions.length > 0 && - exceptions.map((exception) => ( - - - - ))} - - - )} -
-); - -ExceptionsViewerItemsComponent.displayName = 'ExceptionsViewerItemsComponent'; - -export const ExceptionsViewerItems = React.memo(ExceptionsViewerItemsComponent); - -ExceptionsViewerItems.displayName = 'ExceptionsViewerItems'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx deleted file mode 100644 index eeab8c7e36b70..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { mount } from 'enzyme'; -import { ThemeProvider } from 'styled-components'; - -import { ExceptionsViewer } from '.'; -import { useKibana } from '../../../lib/kibana'; -import { useExceptionListItems, useApi } from '@kbn/securitysolution-list-hooks'; - -import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; -import { getFoundExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/found_exception_list_item_schema.mock'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; - -const mockTheme = getMockTheme({ - eui: { - euiColorEmptyShade: '#ece', - euiBreakpoints: { - l: '1200px', - }, - euiSizeM: '10px', - }, -}); - -jest.mock('../../../lib/kibana'); -jest.mock('@kbn/securitysolution-list-hooks'); - -describe('ExceptionsViewer', () => { - const ruleName = 'test rule'; - - beforeEach(() => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - http: {}, - application: { - getUrlForApp: () => 'some/url', - }, - }, - }); - - (useApi as jest.Mock).mockReturnValue({ - deleteExceptionItem: jest.fn().mockResolvedValue(true), - getExceptionListsItems: jest.fn().mockResolvedValue(getFoundExceptionListItemSchemaMock()), - }); - - (useExceptionListItems as jest.Mock).mockReturnValue([ - false, - [], - [], - { - page: 1, - perPage: 20, - total: 0, - }, - jest.fn(), - ]); - }); - - it('it renders loader if "loadingList" is true', () => { - (useExceptionListItems as jest.Mock).mockReturnValue([ - true, - [], - [], - { - page: 1, - perPage: 20, - total: 0, - }, - jest.fn(), - ]); - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="loadingPanelAllRulesTable"]').exists()).toBeTruthy(); - }); - - it('it renders empty prompt if no "exceptionListMeta" passed in', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); - }); - - it('it renders empty prompt if no exception items exist', () => { - (useExceptionListItems as jest.Mock).mockReturnValue([ - false, - [getExceptionListSchemaMock()], - [], - { - page: 1, - perPage: 20, - total: 0, - }, - jest.fn(), - ]); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="exceptionsEmptyPrompt"]').exists()).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx deleted file mode 100644 index e724a546f8054..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.tsx +++ /dev/null @@ -1,415 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useCallback, useEffect, useReducer, useState } from 'react'; -import { EuiSpacer } from '@elastic/eui'; -import uuid from 'uuid'; - -import type { - ExceptionListTypeEnum, - ExceptionListItemSchema, - ExceptionListIdentifiers, - UseExceptionListItemsSuccess, -} from '@kbn/securitysolution-io-ts-list-types'; -import { useApi, useExceptionListItems } from '@kbn/securitysolution-list-hooks'; -import * as i18n from '../translations'; -import { useStateToaster } from '../../toasters'; -import { useUserData } from '../../../../detections/components/user_info'; -import { useKibana } from '../../../lib/kibana'; -import { Panel } from '../../panel'; -import { Loader } from '../../loader'; -import { ExceptionsViewerHeader } from './exceptions_viewer_header'; -import type { ExceptionListItemIdentifiers, Filter } from '../types'; -import type { State, ViewerFlyoutName } from './reducer'; -import { allExceptionItemsReducer } from './reducer'; - -import { ExceptionsViewerPagination } from './exceptions_pagination'; -import { ExceptionsViewerUtility } from './exceptions_utility'; -import { ExceptionsViewerItems } from './exceptions_viewer_items'; -import { EditExceptionFlyout } from '../edit_exception_flyout'; -import { AddExceptionFlyout } from '../add_exception_flyout'; - -const initialState: State = { - filterOptions: { filter: '', tags: [] }, - pagination: { - pageIndex: 0, - pageSize: 20, - totalItemCount: 0, - pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], - }, - exceptions: [], - exceptionToEdit: null, - loadingItemIds: [], - isInitLoading: true, - currentModal: null, - exceptionListTypeToEdit: null, - totalEndpointItems: 0, - totalDetectionsItems: 0, - showEndpointListsOnly: false, - showDetectionsListsOnly: false, -}; - -interface ExceptionsViewerProps { - ruleId: string; - ruleName: string; - ruleIndices: string[]; - dataViewId?: string; - exceptionListsMeta: ExceptionListIdentifiers[]; - availableListTypes: ExceptionListTypeEnum[]; - commentsAccordionId: string; - onRuleChange?: () => void; -} - -const ExceptionsViewerComponent = ({ - ruleId, - ruleName, - ruleIndices, - dataViewId, - exceptionListsMeta, - availableListTypes, - commentsAccordionId, - onRuleChange, -}: ExceptionsViewerProps): JSX.Element => { - const { services } = useKibana(); - const [, dispatchToaster] = useStateToaster(); - const onDispatchToaster = useCallback( - ({ title, color, iconType }) => - (): void => { - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title, - color, - iconType, - }, - }); - }, - [dispatchToaster] - ); - const [ - { - exceptions, - filterOptions, - pagination, - loadingItemIds, - isInitLoading, - currentModal, - exceptionToEdit, - exceptionListTypeToEdit, - totalEndpointItems, - totalDetectionsItems, - showDetectionsListsOnly, - showEndpointListsOnly, - }, - dispatch, - ] = useReducer(allExceptionItemsReducer(), { ...initialState }); - const { deleteExceptionItem, getExceptionListsItems } = useApi(services.http); - const [supportedListTypes, setSupportedListTypes] = useState([]); - - const [{ canUserCRUD, hasIndexWrite }] = useUserData(); - - useEffect((): void => { - if (!canUserCRUD || !hasIndexWrite) { - setSupportedListTypes([]); - } else { - setSupportedListTypes(availableListTypes); - } - }, [availableListTypes, canUserCRUD, hasIndexWrite]); - - const setExceptions = useCallback( - ({ - exceptions: newExceptions, - pagination: newPagination, - }: UseExceptionListItemsSuccess): void => { - dispatch({ - type: 'setExceptions', - lists: exceptionListsMeta, - exceptions: newExceptions, - pagination: newPagination, - }); - }, - [dispatch, exceptionListsMeta] - ); - const [loadingList, , , fetchListItems] = useExceptionListItems({ - http: services.http, - lists: exceptionListsMeta, - filterOptions: - filterOptions.filter !== '' || filterOptions.tags.length > 0 ? [filterOptions] : [], - pagination: { - page: pagination.pageIndex + 1, - perPage: pagination.pageSize, - total: pagination.totalItemCount, - }, - showDetectionsListsOnly, - showEndpointListsOnly, - matchFilters: true, - onSuccess: setExceptions, - onError: onDispatchToaster({ - color: 'danger', - title: i18n.FETCH_LIST_ERROR, - iconType: 'alert', - }), - }); - - const setCurrentModal = useCallback( - (modalName: ViewerFlyoutName): void => { - dispatch({ - type: 'updateModalOpen', - modalName, - }); - }, - [dispatch] - ); - - const setExceptionItemTotals = useCallback( - (endpointItemTotals: number | null, detectionItemTotals: number | null): void => { - dispatch({ - type: 'setExceptionItemTotals', - totalEndpointItems: endpointItemTotals, - totalDetectionsItems: detectionItemTotals, - }); - }, - [dispatch] - ); - - const handleGetTotals = useCallback(async (): Promise => { - await getExceptionListsItems({ - lists: exceptionListsMeta, - filterOptions: [], - pagination: { - page: 0, - perPage: 1, - total: 0, - }, - showDetectionsListsOnly: true, - showEndpointListsOnly: false, - onSuccess: ({ pagination: detectionPagination }) => { - setExceptionItemTotals(null, detectionPagination.total ?? 0); - }, - onError: () => { - const dispatchToasterError = onDispatchToaster({ - color: 'danger', - title: i18n.TOTAL_ITEMS_FETCH_ERROR, - iconType: 'alert', - }); - - dispatchToasterError(); - }, - }); - await getExceptionListsItems({ - lists: exceptionListsMeta, - filterOptions: [], - pagination: { - page: 0, - perPage: 1, - total: 0, - }, - showDetectionsListsOnly: false, - showEndpointListsOnly: true, - onSuccess: ({ pagination: endpointPagination }) => { - setExceptionItemTotals(endpointPagination.total ?? 0, null); - }, - onError: () => { - const dispatchToasterError = onDispatchToaster({ - color: 'danger', - title: i18n.TOTAL_ITEMS_FETCH_ERROR, - iconType: 'alert', - }); - - dispatchToasterError(); - }, - }); - }, [setExceptionItemTotals, exceptionListsMeta, getExceptionListsItems, onDispatchToaster]); - - const handleFetchList = useCallback((): void => { - if (fetchListItems != null) { - fetchListItems(); - handleGetTotals(); - } - }, [fetchListItems, handleGetTotals]); - - const handleFilterChange = useCallback( - (filters: Partial): void => { - dispatch({ - type: 'updateFilterOptions', - filters, - }); - }, - [dispatch] - ); - - const handleAddException = useCallback( - (type: ExceptionListTypeEnum): void => { - dispatch({ - type: 'updateExceptionListTypeToEdit', - exceptionListType: type, - }); - setCurrentModal('addException'); - }, - [setCurrentModal] - ); - - const handleEditException = useCallback( - (exception: ExceptionListItemSchema): void => { - dispatch({ - type: 'updateExceptionToEdit', - lists: exceptionListsMeta, - exception, - }); - setCurrentModal('editException'); - }, - [setCurrentModal, exceptionListsMeta] - ); - - const handleOnCancelExceptionModal = useCallback((): void => { - setCurrentModal(null); - handleFetchList(); - }, [setCurrentModal, handleFetchList]); - - const handleOnConfirmExceptionModal = useCallback((): void => { - setCurrentModal(null); - handleFetchList(); - }, [setCurrentModal, handleFetchList]); - - const setLoadingItemIds = useCallback( - (items: ExceptionListItemIdentifiers[]): void => { - dispatch({ - type: 'updateLoadingItemIds', - items, - }); - }, - [dispatch] - ); - - const handleDeleteException = useCallback( - ({ id: itemId, namespaceType }: ExceptionListItemIdentifiers) => { - setLoadingItemIds([{ id: itemId, namespaceType }]); - - deleteExceptionItem({ - id: itemId, - namespaceType, - onSuccess: () => { - setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId)); - handleFetchList(); - }, - onError: () => { - const dispatchToasterError = onDispatchToaster({ - color: 'danger', - title: i18n.DELETE_EXCEPTION_ERROR, - iconType: 'alert', - }); - - dispatchToasterError(); - setLoadingItemIds(loadingItemIds.filter(({ id }) => id !== itemId)); - }, - }); - }, - [setLoadingItemIds, deleteExceptionItem, loadingItemIds, handleFetchList, onDispatchToaster] - ); - - // Logic for initial render - useEffect((): void => { - if (isInitLoading && !loadingList && (exceptions.length === 0 || exceptions != null)) { - handleGetTotals(); - dispatch({ - type: 'updateIsInitLoading', - loading: false, - }); - } - }, [handleGetTotals, isInitLoading, exceptions, loadingList, dispatch]); - - // Used in utility bar info text - const ruleSettingsUrl = services.application.getUrlForApp( - `security/detections/rules/id/${encodeURI(ruleId)}/edit` - ); - - const showEmpty: boolean = !isInitLoading && !loadingList && exceptions.length === 0; - - const showNoResults: boolean = - exceptions.length === 0 && (totalEndpointItems > 0 || totalDetectionsItems > 0); - - return ( - <> - {currentModal === 'editException' && - exceptionToEdit != null && - exceptionListTypeToEdit != null && ( - - )} - - {currentModal === 'addException' && exceptionListTypeToEdit != null && ( - - )} - - - {(isInitLoading || loadingList) && ( - - )} - - - - - - - - - - - - - ); -}; - -ExceptionsViewerComponent.displayName = 'ExceptionsViewerComponent'; - -export const ExceptionsViewer = React.memo(ExceptionsViewerComponent); - -ExceptionsViewer.displayName = 'ExceptionsViewer'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts deleted file mode 100644 index 1d91f45fd0c9f..0000000000000 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/reducer.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { - ExceptionListType, - ExceptionListItemSchema, - ExceptionListIdentifiers, - Pagination, -} from '@kbn/securitysolution-io-ts-list-types'; -import type { - FilterOptions, - ExceptionsPagination, - ExceptionListItemIdentifiers, - Filter, -} from '../types'; - -export type ViewerFlyoutName = 'addException' | 'editException' | null; - -export interface State { - filterOptions: FilterOptions; - pagination: ExceptionsPagination; - exceptions: ExceptionListItemSchema[]; - exceptionToEdit: ExceptionListItemSchema | null; - loadingItemIds: ExceptionListItemIdentifiers[]; - isInitLoading: boolean; - currentModal: ViewerFlyoutName; - exceptionListTypeToEdit: ExceptionListType | null; - totalEndpointItems: number; - totalDetectionsItems: number; - showEndpointListsOnly: boolean; - showDetectionsListsOnly: boolean; -} - -export type Action = - | { - type: 'setExceptions'; - lists: ExceptionListIdentifiers[]; - exceptions: ExceptionListItemSchema[]; - pagination: Pagination; - } - | { - type: 'updateFilterOptions'; - filters: Partial; - } - | { type: 'updateIsInitLoading'; loading: boolean } - | { type: 'updateModalOpen'; modalName: ViewerFlyoutName } - | { - type: 'updateExceptionToEdit'; - lists: ExceptionListIdentifiers[]; - exception: ExceptionListItemSchema; - } - | { type: 'updateLoadingItemIds'; items: ExceptionListItemIdentifiers[] } - | { type: 'updateExceptionListTypeToEdit'; exceptionListType: ExceptionListType | null } - | { - type: 'setExceptionItemTotals'; - totalEndpointItems: number | null; - totalDetectionsItems: number | null; - }; - -export const allExceptionItemsReducer = - () => - (state: State, action: Action): State => { - switch (action.type) { - case 'setExceptions': { - const { exceptions, pagination } = action; - - return { - ...state, - pagination: { - ...state.pagination, - pageIndex: pagination.page - 1, - pageSize: pagination.perPage, - totalItemCount: pagination.total ?? 0, - }, - exceptions, - }; - } - case 'updateFilterOptions': { - const { filter, pagination, showEndpointListsOnly, showDetectionsListsOnly } = - action.filters; - return { - ...state, - filterOptions: { - ...state.filterOptions, - ...filter, - }, - pagination: { - ...state.pagination, - ...pagination, - }, - showEndpointListsOnly: showEndpointListsOnly ?? state.showEndpointListsOnly, - showDetectionsListsOnly: showDetectionsListsOnly ?? state.showDetectionsListsOnly, - }; - } - case 'setExceptionItemTotals': { - return { - ...state, - totalEndpointItems: - action.totalEndpointItems == null - ? state.totalEndpointItems - : action.totalEndpointItems, - totalDetectionsItems: - action.totalDetectionsItems == null - ? state.totalDetectionsItems - : action.totalDetectionsItems, - }; - } - case 'updateIsInitLoading': { - return { - ...state, - isInitLoading: action.loading, - }; - } - case 'updateLoadingItemIds': { - return { - ...state, - loadingItemIds: [...state.loadingItemIds, ...action.items], - }; - } - case 'updateExceptionToEdit': { - const { exception, lists } = action; - const exceptionListToEdit = lists.find((list) => { - return list !== null && exception.list_id === list.listId; - }); - return { - ...state, - exceptionToEdit: exception, - exceptionListTypeToEdit: exceptionListToEdit ? exceptionListToEdit.type : null, - }; - } - case 'updateModalOpen': { - return { - ...state, - currentModal: action.modalName, - }; - } - case 'updateExceptionListTypeToEdit': { - return { - ...state, - exceptionListTypeToEdit: action.exceptionListType, - }; - } - default: - return state; - } - }; diff --git a/x-pack/plugins/security_solution/public/common/images/illustration_product_no_results_magnifying_glass.svg b/x-pack/plugins/security_solution/public/common/images/illustration_product_no_results_magnifying_glass.svg new file mode 100644 index 0000000000000..b9a0df1630b20 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/images/illustration_product_no_results_magnifying_glass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx index 5945b1a0111c0..de5eca78aaffb 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.test.tsx @@ -14,30 +14,30 @@ import { AddExceptionFlyout } from '.'; import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public'; import { useAsync } from '@kbn/securitysolution-hook-utils'; import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; -import { useFetchIndex } from '../../../containers/source'; +import { useFetchIndex } from '../../../../common/containers/source'; import { createStubIndexPattern, stubIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { useAddOrUpdateException } from '../use_add_exception'; -import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; +import { useAddOrUpdateException } from '../../logic/use_add_exception'; +import { useFetchOrCreateRuleExceptionList } from '../../logic/use_fetch_or_create_rule_exception_list'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; -import * as helpers from '../helpers'; +import * as helpers from '../../utils/helpers'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; -import { TestProviders } from '../../../mock'; +import { TestProviders } from '../../../../common/mock'; import { getRulesEqlSchemaMock, getRulesSchemaMock, } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; -import type { AlertData } from '../types'; +import type { AlertData } from '../../utils/types'; jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); -jest.mock('../../../lib/kibana'); -jest.mock('../../../containers/source'); +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/containers/source'); jest.mock('../../../../detections/containers/detection_engine/rules'); -jest.mock('../use_add_exception'); -jest.mock('../use_fetch_or_create_rule_exception_list'); +jest.mock('../../logic/use_add_exception'); +jest.mock('../../logic/use_fetch_or_create_rule_exception_list'); jest.mock('@kbn/securitysolution-hook-utils', () => ({ ...jest.requireActual('@kbn/securitysolution-hook-utils'), useAsync: jest.fn(), diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx index e5ae187d79d34..1a547b6e62d60 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/index.tsx @@ -46,17 +46,17 @@ import { isThresholdRule, } from '../../../../../common/detection_engine/utils'; import type { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; -import * as i18nCommon from '../../../translations'; +import * as i18nCommon from '../../../../common/translations'; import * as i18n from './translations'; -import * as sharedI18n from '../translations'; -import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { useKibana } from '../../../lib/kibana'; -import { Loader } from '../../loader'; -import { useAddOrUpdateException } from '../use_add_exception'; +import * as sharedI18n from '../../utils/translations'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useKibana } from '../../../../common/lib/kibana'; +import { Loader } from '../../../../common/components/loader'; +import { useAddOrUpdateException } from '../../logic/use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; -import { useFetchOrCreateRuleExceptionList } from '../use_fetch_or_create_rule_exception_list'; -import { AddExceptionComments } from '../add_exception_comments'; +import { useFetchOrCreateRuleExceptionList } from '../../logic/use_fetch_or_create_rule_exception_list'; +import { ExceptionItemComments } from '../item_comments'; import { enrichNewExceptionItemsWithComments, enrichExceptionItemsWithOS, @@ -66,11 +66,11 @@ import { entryHasNonEcsType, retrieveAlertOsTypes, filterIndexPatterns, -} from '../helpers'; +} from '../../utils/helpers'; import type { ErrorInfo } from '../error_callout'; import { ErrorCallout } from '../error_callout'; -import type { AlertData } from '../types'; -import { useFetchIndex } from '../../../containers/source'; +import type { AlertData } from '../../utils/types'; +import { useFetchIndex } from '../../../../common/containers/source'; export interface AddExceptionFlyoutProps { ruleName: string; @@ -549,7 +549,7 @@ export const AddExceptionFlyout = memo(function AddExceptionFlyout({ - diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_flyout/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/add_exception_flyout/translations.ts diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx new file mode 100644 index 0000000000000..0df9fad55a14d --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { mount } from 'enzyme'; + +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionsViewerItems } from './all_items'; +import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; +import { TestProviders } from '../../../../common/mock'; + +const mockTheme = getMockTheme({ + eui: { + euiSize: '10px', + euiColorPrimary: '#ece', + euiColorDanger: '#ece', + }, +}); + +describe('ExceptionsViewerItems', () => { + it('it renders empty prompt if "viewerState" is "empty"', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-detection"]').exists() + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); + }); + + it('it renders no search results found prompt if "viewerState" is "empty_search"', () => { + const wrapper = mount( + + + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch"]').exists() + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeFalsy(); + }); + + it('it renders exceptions if "viewerState" and "null"', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsContainer"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx new file mode 100644 index 0000000000000..fdffa134dd96f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/all_items.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import type { + ExceptionListItemSchema, + ExceptionListTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionItemCard } from '../exception_item_card'; +import type { ExceptionListItemIdentifiers } from '../../utils/types'; +import type { RuleReferences } from '../../logic/use_find_references'; +import type { ViewerState } from './reducer'; +import { ExeptionItemsViewerEmptyPrompts } from './empty_viewer_state'; + +const MyFlexItem = styled(EuiFlexItem)` + margin: ${({ theme }) => `${theme.eui.euiSize} 0`}; + &:first-child { + margin: ${({ theme }) => `${theme.eui.euiSizeXS} 0 ${theme.eui.euiSize}`}; + } +`; + +interface ExceptionItemsViewerProps { + isReadOnly: boolean; + disableActions: boolean; + exceptions: ExceptionListItemSchema[]; + listType: ExceptionListTypeEnum; + ruleReferences: RuleReferences | null; + viewerState: ViewerState; + onCreateExceptionListItem: () => void; + onDeleteException: (arg: ExceptionListItemIdentifiers) => void; + onEditExceptionItem: (item: ExceptionListItemSchema) => void; +} + +const ExceptionItemsViewerComponent: React.FC = ({ + isReadOnly, + exceptions, + listType, + disableActions, + ruleReferences, + viewerState, + onCreateExceptionListItem, + onDeleteException, + onEditExceptionItem, +}): JSX.Element => { + return ( + <> + {viewerState != null && viewerState !== 'deleting' ? ( + + ) : ( + + + + {exceptions.map((exception) => ( + + + + ))} + + + + )} + + ); +}; + +ExceptionItemsViewerComponent.displayName = 'ExceptionItemsViewerComponent'; + +export const ExceptionsViewerItems = React.memo(ExceptionItemsViewerComponent); + +ExceptionsViewerItems.displayName = 'ExceptionsViewerItems'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.test.tsx new file mode 100644 index 0000000000000..66892e31031be --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.test.tsx @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExeptionItemsViewerEmptyPrompts } from './empty_viewer_state'; +import * as i18n from './translations'; + +describe('ExeptionItemsViewerEmptyPrompts', () => { + it('it renders loading screen when "currentState" is "loading"', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-loading"]').exists() + ).toBeTruthy(); + }); + + it('it renders empty search screen when "currentState" is "empty_search"', () => { + const wrapper = mount( + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch"]').exists() + ).toBeTruthy(); + }); + + it('it renders no endpoint items screen when "currentState" is "empty" and "listType" is "endpoint"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptButton"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON + ); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-endpoint"]').exists() + ).toBeTruthy(); + }); + + it('it renders no exception items screen when "currentState" is "empty" and "listType" is "detection"', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BODY + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptButton"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BUTTON + ); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-detection"]').exists() + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx new file mode 100644 index 0000000000000..2be1860f138d3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/empty_viewer_state.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { + EuiLoadingContent, + EuiImage, + EuiEmptyPrompt, + EuiButton, + useEuiTheme, + EuiPanel, +} from '@elastic/eui'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import * as i18n from './translations'; +import type { ViewerState } from './reducer'; +import illustration from '../../../../common/images/illustration_product_no_results_magnifying_glass.svg'; + +interface ExeptionItemsViewerEmptyPromptsComponentProps { + isReadOnly: boolean; + listType: ExceptionListTypeEnum; + currentState: ViewerState; + onCreateExceptionListItem: () => void; +} + +const ExeptionItemsViewerEmptyPromptsComponent = ({ + isReadOnly, + listType, + currentState, + onCreateExceptionListItem, +}: ExeptionItemsViewerEmptyPromptsComponentProps): JSX.Element => { + const { euiTheme } = useEuiTheme(); + + const content = useMemo(() => { + switch (currentState) { + case 'error': + return ( + {i18n.EXCEPTION_ERROR_TITLE}} + body={

{i18n.EXCEPTION_ERROR_DESCRIPTION}

} + data-test-subj={'exceptionItemViewerEmptyPrompts-error'} + /> + ); + case 'empty': + return ( + + {i18n.EXCEPTION_EMPTY_PROMPT_TITLE} + + } + body={ +

+ {listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY + : i18n.EXCEPTION_EMPTY_PROMPT_BODY} +

+ } + actions={[ + + {listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON + : i18n.EXCEPTION_EMPTY_PROMPT_BUTTON} + , + ]} + data-test-subj={`exceptionItemViewerEmptyPrompts-empty-${listType}`} + /> + ); + case 'empty_search': + return ( + } + title={

{i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_TITLE}

} + body={

{i18n.EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY}

} + data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch" + /> + ); + default: + return ( + + ); + } + }, [currentState, euiTheme.colors.darkestShade, isReadOnly, listType, onCreateExceptionListItem]); + + return ( + + {content} + + ); +}; + +export const ExeptionItemsViewerEmptyPrompts = React.memo(ExeptionItemsViewerEmptyPromptsComponent); + +ExeptionItemsViewerEmptyPrompts.displayName = 'ExeptionItemsViewerEmptyPrompts'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx new file mode 100644 index 0000000000000..d34cf07aa9146 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.test.tsx @@ -0,0 +1,309 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useReducer } from 'react'; +import { mount, shallow } from 'enzyme'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionsViewer } from '.'; +import { useKibana } from '../../../../common/lib/kibana'; +import { TestProviders } from '../../../../common/mock'; +import type { Rule } from '../../../../detections/containers/detection_engine/rules/types'; +import { mockRule } from '../../../../detections/pages/detection_engine/rules/all/__mocks__/mock'; +import { useFindExceptionListReferences } from '../../logic/use_find_references'; +import * as i18n from './translations'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('@kbn/securitysolution-list-hooks'); +jest.mock('../../logic/use_find_references'); +jest.mock('react', () => { + const r = jest.requireActual('react'); + return { ...r, useReducer: jest.fn() }; +}); + +const sampleExceptionItem = { + _version: 'WzEwMjM4MSwxXQ==', + comments: [], + created_at: '2022-08-18T17:38:09.018Z', + created_by: 'elastic', + description: 'Index - exception list item', + entries: [ + { + field: 'Endpoint.policy.applied.artifacts.global.identifiers.name', + operator: 'included', + type: 'match', + value: 'sdf', + id: '6a62a5fb-a7d7-44bf-942c-a44b69baba63', + }, + ], + id: '863f3cb0-1f1c-11ed-8a48-9982ed15e50b', + item_id: '74eacd42-7617-4d32-9363-3c074a8892fe', + list_id: '9633e7f2-b92c-4a51-ad56-3e69e5e5f517', + name: 'Index - exception list item', + namespace_type: 'single', + os_types: [], + tags: [], + tie_breaker_id: '5ed24b1f-e717-4798-92ac-9eefd33bb9c0', + type: 'simple', + updated_at: '2022-08-18T17:38:09.020Z', + updated_by: 'elastic', + meta: undefined, +}; + +const getMockRule = (): Rule => ({ + ...mockRule('123'), + exceptions_list: [ + { + id: '5b543420', + list_id: 'list_id', + type: 'endpoint', + namespace_type: 'single', + }, + ], +}); + +describe('ExceptionsViewer', () => { + beforeEach(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + http: {}, + application: { + getUrlForApp: () => 'some/url', + }, + }, + }); + + (useFindExceptionListReferences as jest.Mock).mockReturnValue([false, null]); + }); + + it('it renders loading screen when "currentState" is "loading"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: null, + exceptionToEdit: null, + viewerState: 'loading', + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-loading"]').exists() + ).toBeTruthy(); + }); + + it('it renders empty search screen when "currentState" is "empty_search"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: null, + exceptionToEdit: null, + viewerState: 'empty_search', + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-emptySearch"]').exists() + ).toBeTruthy(); + }); + + it('it renders no endpoint items screen when "currentState" is "empty" and "listType" is "endpoint"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: null, + exceptionToEdit: null, + viewerState: 'empty', + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptButton"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON + ); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-endpoint"]').exists() + ).toBeTruthy(); + }); + + it('it renders no exception items screen when "currentState" is "empty" and "listType" is "detection"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: null, + exceptionToEdit: null, + viewerState: 'empty', + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptBody"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BODY + ); + expect(wrapper.find('[data-test-subj="exceptionsEmptyPromptButton"]').at(0).text()).toEqual( + i18n.EXCEPTION_EMPTY_PROMPT_BUTTON + ); + expect( + wrapper.find('[data-test-subj="exceptionItemViewerEmptyPrompts-empty-detection"]').exists() + ).toBeTruthy(); + }); + + it('it renders add exception flyout if "currentFlyout" is "addException"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: 'addException', + exceptionToEdit: null, + viewerState: null, + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="addExceptionItemFlyout"]').exists()).toBeTruthy(); + }); + + it('it renders edit exception flyout if "currentFlyout" is "editException"', () => { + (useReducer as jest.Mock).mockReturnValue([ + { + exceptions: [sampleExceptionItem], + pagination: { pageIndex: 0, pageSize: 25, totalItemCount: 0, pageSizeOptions: [25, 50] }, + currenFlyout: 'editException', + exceptionToEdit: sampleExceptionItem, + viewerState: null, + exceptionLists: [], + }, + jest.fn(), + ]); + + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="editExceptionItemFlyout"]').exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx new file mode 100644 index 0000000000000..e6168bb39edbd --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/index.tsx @@ -0,0 +1,393 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo, useEffect, useReducer, useState } from 'react'; +import { EuiPanel, EuiSpacer } from '@elastic/eui'; + +import type { + ExceptionListItemSchema, + UseExceptionListItemsSuccess, + Pagination, + ExceptionListTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; +import { transformInput } from '@kbn/securitysolution-list-hooks'; + +import { + deleteExceptionListItemById, + fetchExceptionListsItemsByListIds, +} from '@kbn/securitysolution-list-api'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; +import { useUserData } from '../../../../detections/components/user_info'; +import { useKibana, useToasts } from '../../../../common/lib/kibana'; +import { ExceptionsViewerSearchBar } from './search_bar'; +import type { ExceptionListItemIdentifiers } from '../../utils/types'; +import type { State, ViewerFlyoutName, ViewerState } from './reducer'; +import { allExceptionItemsReducer } from './reducer'; + +import { ExceptionsViewerPagination } from './pagination'; +import { ExceptionsViewerUtility } from './utility_bar'; +import { ExceptionsViewerItems } from './all_items'; +import { EditExceptionFlyout } from '../edit_exception_flyout'; +import { AddExceptionFlyout } from '../add_exception_flyout'; +import * as i18n from './translations'; +import { useFindExceptionListReferences } from '../../logic/use_find_references'; +import type { Rule } from '../../../../detections/containers/detection_engine/rules/types'; + +const STATES_SEARCH_HIDDEN: ViewerState[] = ['error', 'empty']; +const STATES_PAGINATION_UTILITY_HIDDEN: ViewerState[] = [ + 'loading', + 'empty_search', + 'empty', + 'error', + 'searching', +]; + +const initialState: State = { + pagination: { + pageIndex: 0, + pageSize: 25, + totalItemCount: 0, + pageSizeOptions: [1, 5, 10, 25, 50, 100, 200, 300], + }, + exceptions: [], + exceptionToEdit: null, + currenFlyout: null, + viewerState: 'loading', +}; + +export interface GetExceptionItemProps { + pagination?: Partial; + search?: string; + filters?: string; +} + +interface ExceptionsViewerProps { + rule: Rule | null; + listType: ExceptionListTypeEnum; + onRuleChange?: () => void; +} + +const ExceptionsViewerComponent = ({ + rule, + listType, + onRuleChange, +}: ExceptionsViewerProps): JSX.Element => { + const { services } = useKibana(); + const toasts = useToasts(); + const [{ canUserCRUD, hasIndexWrite }] = useUserData(); + const [isReadOnly, setReadOnly] = useState(true); + const [lastUpdated, setLastUpdated] = useState(null); + const exceptionListsToQuery = useMemo( + () => + rule != null && rule.exceptions_list != null + ? rule.exceptions_list.filter((list) => list.type === listType) + : [], + [listType, rule] + ); + + // Reducer state + const [{ exceptions, pagination, currenFlyout, exceptionToEdit, viewerState }, dispatch] = + useReducer(allExceptionItemsReducer(), { + ...initialState, + }); + + // Reducer actions + const setExceptions = useCallback( + ({ + exceptions: newExceptions, + pagination: newPagination, + }: UseExceptionListItemsSuccess): void => { + setLastUpdated(Date.now()); + + dispatch({ + type: 'setExceptions', + exceptions: newExceptions, + pagination: newPagination, + }); + }, + [dispatch] + ); + + const setViewerState = useCallback( + (state: ViewerState): void => { + dispatch({ + type: 'setViewerState', + state, + }); + }, + [dispatch] + ); + + const setFlyoutType = useCallback( + (flyoutType: ViewerFlyoutName): void => { + dispatch({ + type: 'updateFlyoutOpen', + flyoutType, + }); + }, + [dispatch] + ); + + const [_, allReferences] = useFindExceptionListReferences(exceptionListsToQuery); + + const handleFetchItems = useCallback( + async (options?: GetExceptionItemProps) => { + const abortCtrl = new AbortController(); + + const newPagination = + options?.pagination != null + ? { + page: (options.pagination.page ?? 0) + 1, + perPage: options.pagination.perPage, + } + : { + page: pagination.pageIndex + 1, + perPage: pagination.pageSize, + }; + + if (exceptionListsToQuery.length === 0) { + return { + data: [], + pageIndex: pagination.pageIndex, + itemsPerPage: pagination.pageSize, + total: 0, + }; + } + + const { + page: pageIndex, + per_page: itemsPerPage, + total, + data, + } = await fetchExceptionListsItemsByListIds({ + filter: undefined, + http: services.http, + listIds: exceptionListsToQuery.map((list) => list.list_id), + namespaceTypes: exceptionListsToQuery.map((list) => list.namespace_type), + search: options?.search, + pagination: newPagination, + signal: abortCtrl.signal, + }); + + // Please see `x-pack/plugins/lists/public/exceptions/transforms.ts` doc notes + // for context around the temporary `id` + const transformedData = data.map((item) => transformInput(item)); + + return { + data: transformedData, + pageIndex, + itemsPerPage, + total, + }; + }, + [pagination.pageIndex, pagination.pageSize, exceptionListsToQuery, services.http] + ); + + const handleGetExceptionListItems = useCallback( + async (options?: GetExceptionItemProps) => { + try { + setViewerState('loading'); + + const { pageIndex, itemsPerPage, total, data } = await handleFetchItems(options); + + setViewerState(total > 0 ? null : 'empty'); + + setExceptions({ + exceptions: data, + pagination: { + page: pageIndex, + perPage: itemsPerPage, + total, + }, + }); + } catch (e) { + setViewerState('error'); + + toasts.addError(e, { + title: i18n.EXCEPTION_ERROR_TITLE, + toastMessage: i18n.EXCEPTION_ERROR_DESCRIPTION, + }); + } + }, + [handleFetchItems, setExceptions, setViewerState, toasts] + ); + + const handleSearch = useCallback( + async (options?: GetExceptionItemProps) => { + try { + setViewerState('searching'); + + const { pageIndex, itemsPerPage, total, data } = await handleFetchItems(options); + + setViewerState(total > 0 ? null : 'empty_search'); + + setExceptions({ + exceptions: data, + pagination: { + page: pageIndex, + perPage: itemsPerPage, + total, + }, + }); + } catch (e) { + toasts.addError(e, { + title: i18n.EXCEPTION_SEARCH_ERROR_TITLE, + toastMessage: i18n.EXCEPTION_SEARCH_ERROR_BODY, + }); + } + }, + [handleFetchItems, setExceptions, setViewerState, toasts] + ); + + const handleAddException = useCallback((): void => { + setFlyoutType('addException'); + }, [setFlyoutType]); + + const handleEditException = useCallback( + (exception: ExceptionListItemSchema): void => { + dispatch({ + type: 'updateExceptionToEdit', + exception, + }); + setFlyoutType('editException'); + }, + [setFlyoutType] + ); + + const handleCancelExceptionItemFlyout = useCallback((): void => { + setFlyoutType(null); + handleGetExceptionListItems(); + }, [setFlyoutType, handleGetExceptionListItems]); + + const handleConfirmExceptionFlyout = useCallback((): void => { + setFlyoutType(null); + handleGetExceptionListItems(); + }, [setFlyoutType, handleGetExceptionListItems]); + + const handleDeleteException = useCallback( + async ({ id: itemId, name, namespaceType }: ExceptionListItemIdentifiers) => { + const abortCtrl = new AbortController(); + + try { + setViewerState('deleting'); + + await deleteExceptionListItemById({ + http: services.http, + id: itemId, + namespaceType, + signal: abortCtrl.signal, + }); + + toasts.addSuccess({ + title: i18n.EXCEPTION_ITEM_DELETE_TITLE, + text: i18n.EXCEPTION_ITEM_DELETE_TEXT(name), + }); + + await handleGetExceptionListItems(); + } catch (e) { + setViewerState('error'); + + toasts.addError(e, { + title: i18n.EXCEPTION_DELETE_ERROR_TITLE, + }); + } + }, + [handleGetExceptionListItems, services.http, setViewerState, toasts] + ); + + // User privileges checks + useEffect((): void => { + setReadOnly(!canUserCRUD || !hasIndexWrite); + }, [setReadOnly, canUserCRUD, hasIndexWrite]); + + useEffect(() => { + if (exceptionListsToQuery.length > 0) { + handleGetExceptionListItems(); + } else { + setViewerState('empty'); + } + }, [exceptionListsToQuery.length, handleGetExceptionListItems, setViewerState]); + + return ( + <> + {currenFlyout === 'editException' && exceptionToEdit != null && rule != null && ( + + )} + + {currenFlyout === 'addException' && rule != null && ( + + )} + + + <> + {!STATES_SEARCH_HIDDEN.includes(viewerState) && ( + + )} + {!STATES_PAGINATION_UTILITY_HIDDEN.includes(viewerState) && ( + <> + + + + + )} + + + + {!STATES_PAGINATION_UTILITY_HIDDEN.includes(viewerState) && ( + + )} + + + + ); +}; + +ExceptionsViewerComponent.displayName = 'ExceptionsViewerComponent'; + +export const ExceptionsViewer = React.memo(ExceptionsViewerComponent); + +ExceptionsViewer.displayName = 'ExceptionsViewer'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.test.tsx new file mode 100644 index 0000000000000..ccca3542b2520 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { ExceptionsViewerPagination } from './pagination'; + +describe('ExceptionsViewerPagination', () => { + it('it invokes "onPaginationChange" when per page item is clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="tablePaginationPopoverButton"]').at(0).simulate('click'); + wrapper.find('button[data-test-subj="tablePagination-50-rows"]').at(0).simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ pagination: { page: 0, perPage: 50 } }); + }); + + it('it invokes "onPaginationChange" when next clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="pagination-button-next"]').at(0).simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ pagination: { page: 1, perPage: 5 } }); + }); + + it('it invokes "onPaginationChange" when page clicked', () => { + const mockOnPaginationChange = jest.fn(); + const wrapper = mount( + + ); + + wrapper.find('button[data-test-subj="pagination-button-2"]').simulate('click'); + + expect(mockOnPaginationChange).toHaveBeenCalledWith({ pagination: { page: 2, perPage: 50 } }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.tsx new file mode 100644 index 0000000000000..4310462164950 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/pagination.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback } from 'react'; +import { EuiTablePagination } from '@elastic/eui'; + +import type { ExceptionsPagination } from '../../utils/types'; +import * as i18n from './translations'; +import type { GetExceptionItemProps } from '.'; + +interface ExceptionsViewerPaginationProps { + pagination: ExceptionsPagination; + onPaginationChange: (arg: GetExceptionItemProps) => void; +} + +const ExceptionsViewerPaginationComponent = ({ + pagination, + onPaginationChange, +}: ExceptionsViewerPaginationProps): JSX.Element => { + const handleItemsPerPageChange = useCallback( + (pageSize: number) => { + onPaginationChange({ + pagination: { + page: pagination.pageIndex, + perPage: pageSize, + }, + }); + }, + [onPaginationChange, pagination.pageIndex] + ); + + const handlePageIndexChange = useCallback( + (pageIndex: number) => { + onPaginationChange({ + pagination: { + page: pageIndex, + perPage: pagination.pageSize, + }, + }); + }, + [onPaginationChange, pagination.pageSize] + ); + + return ( + + ); +}; + +ExceptionsViewerPaginationComponent.displayName = 'ExceptionsViewerPaginationComponent'; + +export const ExceptionsViewerPagination = React.memo(ExceptionsViewerPaginationComponent); + +ExceptionsViewerPagination.displayName = 'ExceptionsViewerPagination'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts new file mode 100644 index 0000000000000..dbf617e55f66f --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/reducer.ts @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExceptionListItemSchema, Pagination } from '@kbn/securitysolution-io-ts-list-types'; +import type { ExceptionsPagination } from '../../utils/types'; + +export type ViewerFlyoutName = 'addException' | 'editException' | null; +export type ViewerState = + | 'error' + | 'empty' + | 'empty_search' + | 'loading' + | 'searching' + | 'deleting' + | null; + +export interface State { + pagination: ExceptionsPagination; + // Individual exception items + exceptions: ExceptionListItemSchema[]; + // Exception item selected to update + exceptionToEdit: ExceptionListItemSchema | null; + // Flyout to be opened (edit vs add vs none) + currenFlyout: ViewerFlyoutName; + viewerState: ViewerState; +} + +export type Action = + | { + type: 'setExceptions'; + exceptions: ExceptionListItemSchema[]; + pagination: Pagination; + } + | { type: 'updateFlyoutOpen'; flyoutType: ViewerFlyoutName } + | { + type: 'updateExceptionToEdit'; + exception: ExceptionListItemSchema; + } + | { + type: 'setViewerState'; + state: ViewerState; + }; + +export const allExceptionItemsReducer = + () => + (state: State, action: Action): State => { + switch (action.type) { + case 'setExceptions': { + const { exceptions, pagination } = action; + + return { + ...state, + pagination: { + ...state.pagination, + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total ?? 0, + }, + exceptions, + }; + } + case 'updateExceptionToEdit': { + const { exception } = action; + return { + ...state, + exceptionToEdit: exception, + }; + } + case 'updateFlyoutOpen': { + return { + ...state, + currenFlyout: action.flyoutType, + }; + } + case 'setViewerState': { + return { + ...state, + viewerState: action.state, + }; + } + default: + return state; + } + }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.test.tsx new file mode 100644 index 0000000000000..454036b3f3036 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; + +import { ExceptionsViewerSearchBar } from './search_bar'; + +describe('ExceptionsViewerSearchBar', () => { + it('it does not display add exception button if user is read only', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').exists()).toBeFalsy(); + }); + + it('it invokes "onAddExceptionClick" when user selects to add an exception item', () => { + const mockOnAddExceptionClick = jest.fn(); + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); + + expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').at(0).text()).toEqual( + 'Add rule exception' + ); + expect(mockOnAddExceptionClick).toHaveBeenCalledWith('detection'); + }); + + it('it invokes "onAddExceptionClick" when user selects to add an endpoint exception item', () => { + const mockOnAddExceptionClick = jest.fn(); + const wrapper = mount( + + ); + + wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"] button').simulate('click'); + + expect(wrapper.find('[data-test-subj="exceptionsHeaderAddExceptionBtn"]').at(0).text()).toEqual( + 'Add endpoint exception' + ); + expect(mockOnAddExceptionClick).toHaveBeenCalledWith('endpoint'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx new file mode 100644 index 0000000000000..1cc371c440dba --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/search_bar.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiSearchBar } from '@elastic/eui'; + +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import * as i18n from '../../utils/translations'; +import type { GetExceptionItemProps } from '.'; + +const ITEMS_SCHEMA = { + strict: true, + fields: { + created_by: { + type: 'string', + }, + description: { + type: 'string', + }, + id: { + type: 'string', + }, + item_id: { + type: 'string', + }, + list_id: { + type: 'string', + }, + name: { + type: 'string', + }, + os_types: { + type: 'string', + }, + tags: { + type: 'string', + }, + }, +}; + +interface ExceptionsViewerSearchBarProps { + canAddException: boolean; + // Exception list type used to determine what type of item is + // being created when "onAddExceptionClick" is invoked + listType: ExceptionListTypeEnum; + isSearching: boolean; + onSearch: (arg: GetExceptionItemProps) => void; + onAddExceptionClick: (type: ExceptionListTypeEnum) => void; +} + +/** + * Search exception items and take actions (to creat an item) + */ +const ExceptionsViewerSearchBarComponent = ({ + canAddException, + listType, + isSearching, + onSearch, + onAddExceptionClick, +}: ExceptionsViewerSearchBarProps): JSX.Element => { + const handleOnSearch = useCallback( + ({ queryText }): void => { + onSearch({ search: queryText }); + }, + [onSearch] + ); + + const handleAddException = useCallback(() => { + onAddExceptionClick(listType); + }, [onAddExceptionClick, listType]); + + const addExceptionButtonText = useMemo(() => { + return listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.ADD_TO_ENDPOINT_LIST + : i18n.ADD_TO_DETECTIONS_LIST; + }, [listType]); + + return ( + + + + + {!canAddException && ( + + + {addExceptionButtonText} + + + )} + + ); +}; + +ExceptionsViewerSearchBarComponent.displayName = 'ExceptionsViewerSearchBarComponent'; + +export const ExceptionsViewerSearchBar = React.memo(ExceptionsViewerSearchBarComponent); + +ExceptionsViewerSearchBar.displayName = 'ExceptionsViewerSearchBar'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts new file mode 100644 index 0000000000000..727fb78bede2a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/translations.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.noSearchResultsPromptTitle', + { + defaultMessage: 'No results match your search criteria', + } +); + +export const EXCEPTION_NO_SEARCH_RESULTS_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.noSearchResultsPromptBody', + { + defaultMessage: 'Try modifying your search.', + } +); + +export const EXCEPTION_EMPTY_PROMPT_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.addExceptionsEmptyPromptTitle', + { + defaultMessage: 'Add exceptions to this rule', + } +); + +export const EXCEPTION_EMPTY_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.emptyPromptBody', + { + defaultMessage: 'There are no exceptions for this rule. Create your first rule exception.', + } +); + +export const EXCEPTION_EMPTY_ENDPOINT_PROMPT_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.endpoint.emptyPromptBody', + { + defaultMessage: + 'There are no endpoint exceptions. Endpoint exceptions are applied to the endpoint and the detection rule. Create your first endpoint exception.', + } +); + +export const EXCEPTION_EMPTY_PROMPT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.emptyPromptButtonLabel', + { + defaultMessage: 'Add rule exception', + } +); + +export const EXCEPTION_EMPTY_PROMPT_ENDPOINT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.endpoint.emptyPromptButtonLabel', + { + defaultMessage: 'Add endpoint exception', + } +); + +export const EXCEPTION_ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.exceptionItemsFetchError', + { + defaultMessage: 'Unable to load exception items', + } +); + +export const EXCEPTION_ERROR_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.exceptionItemsFetchErrorDescription', + { + defaultMessage: + 'There was an error loading the exception items. Contact your administrator for help.', + } +); + +export const EXCEPTION_SEARCH_ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.exceptionItemSearchErrorTitle', + { + defaultMessage: 'Error searching', + } +); + +export const EXCEPTION_SEARCH_ERROR_BODY = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.exceptionItemSearchErrorBody', + { + defaultMessage: 'An error occurred searching for exception items. Please try again.', + } +); + +export const EXCEPTION_DELETE_ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.exceptionDeleteErrorTitle', + { + defaultMessage: 'Error deleting exception item', + } +); + +export const EXCEPTION_ITEMS_PAGINATION_ARIA_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.paginationAriaLabel', + { + defaultMessage: 'Exception item table pagination', + } +); + +export const EXCEPTION_ITEM_DELETE_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.allItems.exceptionItemDeleteSuccessTitle', + { + defaultMessage: 'Exception deleted', + } +); + +export const EXCEPTION_ITEM_DELETE_TEXT = (itemName: string) => + i18n.translate('xpack.securitySolution.exceptions.allItems.exceptionItemDeleteSuccessText', { + values: { itemName }, + defaultMessage: '"{itemName}" deleted successfully.', + }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx new file mode 100644 index 0000000000000..aa604dbfbf001 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test-jest-helpers'; + +import { ExceptionsViewerUtility } from './utility_bar'; +import { TestProviders } from '../../../../common/mock'; + +describe('ExceptionsViewerUtility', () => { + it('it renders correct item counts', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsShowing"]').at(0).text()).toEqual( + 'Showing 1-50 of 105' + ); + }); + + it('it renders last updated message', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="exceptionsViewerLastUpdated"]').at(0).text()).toEqual( + 'Updated now' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx new file mode 100644 index 0000000000000..a68930a9a40b8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/all_exception_items_table/utility_bar.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; + +import { FormattedMessage } from '@kbn/i18n-react'; +import type { ExceptionsPagination } from '../../utils/types'; +import { + UtilityBar, + UtilityBarSection, + UtilityBarGroup, + UtilityBarText, +} from '../../../../common/components/utility_bar'; +import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; + +const StyledText = styled.span` + font-weight: bold; +`; + +const MyUtilities = styled(EuiFlexGroup)` + height: 50px; +`; + +const StyledCondition = styled.span` + display: inline-block !important; + vertical-align: middle !important; +`; + +interface ExceptionsViewerUtilityProps { + pagination: ExceptionsPagination; + // Corresponds to last time exception items were fetched + lastUpdated: string | number | null; +} + +/** + * Utilities include exception item counts and group by options + */ +const ExceptionsViewerUtilityComponent: React.FC = ({ + pagination, + lastUpdated, +}): JSX.Element => ( + + + + + + + {`1-${Math.min( + pagination.pageSize, + pagination.totalItemCount + )}`} + ), + partTwo: {`${pagination.totalItemCount}`}, + }} + /> + + + + + + + + + + + ), + }} + /> + + + +); + +ExceptionsViewerUtilityComponent.displayName = 'ExceptionsViewerUtilityComponent'; + +export const ExceptionsViewerUtility = React.memo(ExceptionsViewerUtilityComponent); + +ExceptionsViewerUtility.displayName = 'ExceptionsViewerUtility'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx index 2885920222b5d..d5d5c3fc37316 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.test.tsx @@ -12,10 +12,10 @@ import type { ReactWrapper } from 'enzyme'; import { mount } from 'enzyme'; import { EditExceptionFlyout } from '.'; -import { useCurrentUser } from '../../../lib/kibana'; -import { useFetchIndex } from '../../../containers/source'; +import { useCurrentUser } from '../../../../common/lib/kibana'; +import { useFetchIndex } from '../../../../common/containers/source'; import { stubIndexPattern, createStubIndexPattern } from '@kbn/data-plugin/common/stubs'; -import { useAddOrUpdateException } from '../use_add_exception'; +import { useAddOrUpdateException } from '../../logic/use_add_exception'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import type { EntriesArray } from '@kbn/securitysolution-io-ts-list-types'; @@ -24,7 +24,7 @@ import { getRulesSchemaMock, } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; -import { getMockTheme } from '../../../lib/kibana/kibana_react.mock'; +import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public'; const mockTheme = getMockTheme({ @@ -36,11 +36,11 @@ const mockTheme = getMockTheme({ }, }); -jest.mock('../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); jest.mock('../../../../detections/containers/detection_engine/rules'); -jest.mock('../use_add_exception'); -jest.mock('../../../containers/source'); -jest.mock('../use_fetch_or_create_rule_exception_list'); +jest.mock('../../logic/use_add_exception'); +jest.mock('../../../../common/containers/source'); +jest.mock('../../logic/use_fetch_or_create_rule_exception_list'); jest.mock('../../../../detections/containers/detection_engine/alerts/use_signal_index'); jest.mock('../../../../detections/containers/detection_engine/rules/use_rule_async'); jest.mock('@kbn/lists-plugin/public'); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx index 73095d11decaf..78e86dd4f1fa0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/edit_exception_flyout/index.tsx @@ -45,16 +45,16 @@ import { isNewTermsRule, isThresholdRule, } from '../../../../../common/detection_engine/utils'; -import { useFetchIndex } from '../../../containers/source'; +import { useFetchIndex } from '../../../../common/containers/source'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { useRuleAsync } from '../../../../detections/containers/detection_engine/rules/use_rule_async'; import * as i18n from './translations'; -import * as sharedI18n from '../translations'; -import { useKibana } from '../../../lib/kibana'; -import { useAppToasts } from '../../../hooks/use_app_toasts'; -import { useAddOrUpdateException } from '../use_add_exception'; -import { AddExceptionComments } from '../add_exception_comments'; +import * as sharedI18n from '../../utils/translations'; +import { useKibana } from '../../../../common/lib/kibana'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { useAddOrUpdateException } from '../../logic/use_add_exception'; +import { ExceptionItemComments } from '../item_comments'; import { enrichExistingExceptionItemWithComments, enrichExceptionItemsWithOS, @@ -62,8 +62,8 @@ import { entryHasNonEcsType, lowercaseHashValues, filterIndexPatterns, -} from '../helpers'; -import { Loader } from '../../loader'; +} from '../../utils/helpers'; +import { Loader } from '../../../../common/components/loader'; import type { ErrorInfo } from '../error_callout'; import { ErrorCallout } from '../error_callout'; @@ -410,7 +410,7 @@ export const EditExceptionFlyout = memo(function EditExceptionFlyout({ - (({ comments }) => { + const { euiTheme } = useEuiTheme(); + + return ( + + + {i18n.exceptionItemCommentsAccordion(comments.length)} + + } + arrowDisplay="none" + data-test-subj="exceptionsViewerCommentAccordion" + > + + + + + + ); +}); + +ExceptionItemCardComments.displayName = 'ExceptionItemCardComments'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.test.tsx index e31f049e55e13..33fbb2150323c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { mount } from 'enzyme'; -import { TestProviders } from '../../../../mock'; -import { ExceptionItemCardConditions } from './exception_item_card_conditions'; +import { TestProviders } from '../../../../common/mock'; +import { ExceptionItemCardConditions } from './conditions'; describe('ExceptionItemCardConditions', () => { it('it includes os condition if one exists', () => { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx similarity index 94% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx index 451cb3a4ecf31..95e3f7c211888 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_conditions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/conditions.tsx @@ -6,7 +6,14 @@ */ import React, { memo, useMemo, useCallback } from 'react'; -import { EuiExpression, EuiToken, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { + EuiExpression, + EuiToken, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiPanel, +} from '@elastic/eui'; import styled from 'styled-components'; import type { EntryExists, @@ -21,7 +28,7 @@ import type { import { ListOperatorTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import * as i18n from './translations'; -import { ValueWithSpaceWarning } from '../value_with_space_warning/value_with_space_warning'; +import { ValueWithSpaceWarning } from '../value_with_space_warning'; const OS_LABELS = Object.freeze({ linux: i18n.OS_LINUX, @@ -60,6 +67,12 @@ const StyledCondition = styled('span')` margin-right: 6px; `; +const StyledConditionContent = styled(EuiPanel)` + border: 1px; + border-color: #d3dae6; + border-style: solid; +`; + export interface CriteriaConditionsProps { entries: ExceptionListItemSchema['entries']; dataTestSubj: string; @@ -148,7 +161,12 @@ export const ExceptionItemCardConditions = memo( ); return ( -
+ {osLabel != null && (
@@ -183,7 +201,7 @@ export const ExceptionItemCardConditions = memo(
); })} -
+ ); } ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.test.tsx similarity index 96% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.test.tsx index fe8811152e2e1..2a60e81375745 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.test.tsx @@ -11,8 +11,8 @@ import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas import { ThemeProvider } from 'styled-components'; import * as i18n from './translations'; -import { ExceptionItemCardHeader } from './exception_item_card_header'; -import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock'; +import { ExceptionItemCardHeader } from './header'; +import { getMockTheme } from '../../../../common/lib/kibana/kibana_react.mock'; const mockTheme = getMockTheme({ eui: { diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/exception_item_card_header.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/header.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx similarity index 52% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx index 46a0f74642c08..5219f5d72d847 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.test.tsx @@ -6,40 +6,52 @@ */ import React from 'react'; -import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import { ExceptionItemCard } from '.'; import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; import { getCommentsArrayMock } from '@kbn/lists-plugin/common/schemas/types/comment.mock'; -import { getMockTheme } from '../../../../lib/kibana/kibana_react.mock'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { TestProviders } from '../../../../common/mock'; -jest.mock('../../../../lib/kibana'); - -const mockTheme = getMockTheme({ - eui: { - euiColorDanger: '#ece', - euiColorLightestShade: '#ece', - euiColorPrimary: '#ece', - euiFontWeightSemiBold: 1, - }, -}); +jest.mock('../../../../common/lib/kibana'); describe('ExceptionItemCard', () => { it('it renders header, item meta information and conditions', () => { const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: [] }; const wrapper = mount( - + - + ); expect(wrapper.find('ExceptionItemCardHeader')).toHaveLength(1); @@ -54,16 +66,37 @@ describe('ExceptionItemCard', () => { const exceptionItem = { ...getExceptionListItemSchemaMock(), comments: getCommentsArrayMock() }; const wrapper = mount( - + - + ); expect(wrapper.find('ExceptionItemCardHeader')).toHaveLength(1); @@ -78,16 +111,37 @@ describe('ExceptionItemCard', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - + - + ); expect(wrapper.find('button[data-test-subj="item-actionButton"]').exists()).toBeFalsy(); @@ -98,16 +152,37 @@ describe('ExceptionItemCard', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - + - + ); // click on popover @@ -127,16 +202,37 @@ describe('ExceptionItemCard', () => { const exceptionItem = getExceptionListItemSchemaMock(); const wrapper = mount( - + - + ); // click on popover @@ -150,6 +246,7 @@ describe('ExceptionItemCard', () => { expect(mockOnDeleteException).toHaveBeenCalledWith({ id: '1', + name: 'some name', namespaceType: 'single', }); }); @@ -158,16 +255,37 @@ describe('ExceptionItemCard', () => { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.comments = getCommentsArrayMock(); const wrapper = mount( - + - + ); expect(wrapper.find('.euiAccordion-isOpen')).toHaveLength(0); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx similarity index 60% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx index 322050e27e4b8..cae3136dc6ad4 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/index.tsx @@ -6,50 +6,46 @@ */ import type { EuiCommentProps } from '@elastic/eui'; -import { - EuiPanel, - EuiFlexGroup, - EuiCommentList, - EuiAccordion, - EuiFlexItem, - EuiText, - useEuiTheme, -} from '@elastic/eui'; +import { EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { useMemo, useCallback } from 'react'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; -import { getFormattedComments } from '../../helpers'; -import type { ExceptionListItemIdentifiers } from '../../types'; +import { getFormattedComments } from '../../utils/helpers'; +import type { ExceptionListItemIdentifiers } from '../../utils/types'; import * as i18n from './translations'; -import { ExceptionItemCardHeader } from './exception_item_card_header'; -import { ExceptionItemCardConditions } from './exception_item_card_conditions'; -import { ExceptionItemCardMetaInfo } from './exception_item_card_meta'; +import { ExceptionItemCardHeader } from './header'; +import { ExceptionItemCardConditions } from './conditions'; +import { ExceptionItemCardMetaInfo } from './meta'; +import type { RuleReferenceSchema } from '../../../../../common/detection_engine/schemas/response'; +import { ExceptionItemCardComments } from './comments'; export interface ExceptionItemProps { - loadingItemIds: ExceptionListItemIdentifiers[]; exceptionItem: ExceptionListItemSchema; + listType: ExceptionListTypeEnum; + disableActions: boolean; + ruleReferences: RuleReferenceSchema[]; onDeleteException: (arg: ExceptionListItemIdentifiers) => void; onEditException: (item: ExceptionListItemSchema) => void; - disableActions: boolean; dataTestSubj: string; } const ExceptionItemCardComponent = ({ disableActions, - loadingItemIds, exceptionItem, + listType, + ruleReferences, onDeleteException, onEditException, dataTestSubj, }: ExceptionItemProps): JSX.Element => { - const { euiTheme } = useEuiTheme(); - const handleDelete = useCallback((): void => { onDeleteException({ id: exceptionItem.id, + name: exceptionItem.name, namespaceType: exceptionItem.namespace_type, }); - }, [onDeleteException, exceptionItem.id, exceptionItem.namespace_type]); + }, [onDeleteException, exceptionItem.id, exceptionItem.name, exceptionItem.namespace_type]); const handleEdit = useCallback((): void => { onEditException(exceptionItem); @@ -59,11 +55,6 @@ const ExceptionItemCardComponent = ({ return getFormattedComments(exceptionItem.comments); }, [exceptionItem.comments]); - const disableItemActions = useMemo((): boolean => { - const foundItems = loadingItemIds.some(({ id }) => id === exceptionItem.id); - return disableActions || foundItems; - }, [loadingItemIds, exceptionItem.id, disableActions]); - return ( @@ -73,24 +64,31 @@ const ExceptionItemCardComponent = ({ actions={[ { key: 'edit', - icon: 'pencil', - label: i18n.EXCEPTION_ITEM_EDIT_BUTTON, + icon: 'controlsHorizontal', + label: + listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.ENDPOINT_EXCEPTION_ITEM_EDIT_BUTTON + : i18n.EXCEPTION_ITEM_EDIT_BUTTON, onClick: handleEdit, }, { key: 'delete', icon: 'trash', - label: i18n.EXCEPTION_ITEM_DELETE_BUTTON, + label: + listType === ExceptionListTypeEnum.ENDPOINT + ? i18n.ENDPOINT_EXCEPTION_ITEM_DELETE_BUTTON + : i18n.EXCEPTION_ITEM_DELETE_BUTTON, onClick: handleDelete, }, ]} - disableActions={disableItemActions} + disableActions={disableActions} dataTestSubj="exceptionItemCardHeader" /> @@ -101,24 +99,7 @@ const ExceptionItemCardComponent = ({ dataTestSubj="exceptionItemCardConditions" /> - {formattedComments.length > 0 && ( - - - {i18n.exceptionItemCommentsAccordion(formattedComments.length)} - - } - arrowDisplay="none" - data-test-subj="exceptionsViewerCommentAccordion" - > - - - - - - )} + {formattedComments.length > 0 && } ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.test.tsx new file mode 100644 index 0000000000000..c755321f5f4ea --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.test.tsx @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; + +import { ExceptionItemCardMetaInfo } from './meta'; +import { TestProviders } from '../../../../common/mock'; + +describe('ExceptionItemCardMetaInfo', () => { + it('it renders item creation info', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-createdBy-value1"]').at(0).text() + ).toEqual('Apr 20, 2020 @ 15:25:31.830'); + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-createdBy-value2"]').at(0).text() + ).toEqual('some user'); + }); + + it('it renders item update info', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value1"]').at(0).text() + ).toEqual('Apr 20, 2020 @ 15:25:31.830'); + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-updatedBy-value2"]').at(0).text() + ).toEqual('some user'); + }); + + it('it renders references info', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-affectedRulesButton"]').at(0).text() + ).toEqual('Affects 1 rule'); + }); + + it('it renders references info when multiple references exist', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper.find('[data-test-subj="exceptionItemMeta-affectedRulesButton"]').at(0).text() + ).toEqual('Affects 2 rules'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx new file mode 100644 index 0000000000000..a24526dd04e3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/meta.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo, useState } from 'react'; +import type { EuiContextMenuPanelProps } from '@elastic/eui'; +import { + EuiBadge, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiToolTip, + EuiText, + EuiButtonEmpty, + EuiPopover, +} from '@elastic/eui'; +import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import styled from 'styled-components'; + +import * as i18n from './translations'; +import { FormattedDate } from '../../../../common/components/formatted_date'; +import { SecurityPageName } from '../../../../../common/constants'; +import type { RuleReferenceSchema } from '../../../../../common/detection_engine/schemas/response'; +import { SecuritySolutionLinkAnchor } from '../../../../common/components/links'; +import { RuleDetailTabs } from '../../../../detections/pages/detection_engine/rules/details'; +import { getRuleDetailsTabUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; + +const StyledFlexItem = styled(EuiFlexItem)` + border-right: 1px solid #d3dae6; + padding: 4px 12px 4px 0; +`; + +export interface ExceptionItemCardMetaInfoProps { + item: ExceptionListItemSchema; + references: RuleReferenceSchema[]; + dataTestSubj: string; +} + +export const ExceptionItemCardMetaInfo = memo( + ({ item, references, dataTestSubj }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onAffectedRulesClick = () => setIsPopoverOpen((isOpen) => !isOpen); + const onClosePopover = () => setIsPopoverOpen(false); + + const itemActions = useMemo((): EuiContextMenuPanelProps['items'] => { + if (references == null) { + return []; + } + return references.map((reference) => ( + + + + {reference.name} + + + + )); + }, [references, dataTestSubj]); + + return ( + + + } + value2={item.created_by} + dataTestSubj={`${dataTestSubj}-createdBy`} + /> + + + } + value2={item.updated_by} + dataTestSubj={`${dataTestSubj}-updatedBy`} + /> + + + + {i18n.AFFECTED_RULES(references?.length ?? 0)} + + } + panelPaddingSize="none" + isOpen={isPopoverOpen} + closePopover={onClosePopover} + data-test-subj={`${dataTestSubj}-items`} + > + + + + + ); + } +); +ExceptionItemCardMetaInfo.displayName = 'ExceptionItemCardMetaInfo'; + +interface MetaInfoDetailsProps { + fieldName: string; + label: string; + value1: JSX.Element | string; + value2: string; + dataTestSubj: string; +} + +const MetaInfoDetails = memo(({ label, value1, value2, dataTestSubj }) => { + return ( + + + + {label} + + + + + {value1} + + + + + {i18n.EXCEPTION_ITEM_META_BY} + + + + + + + {value2} + + + + + + ); +}); + +MetaInfoDetails.displayName = 'MetaInfoDetails'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/translations.ts similarity index 84% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/translations.ts index 5d46243697355..ccdd5eebf3b8c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item_card/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/exception_item_card/translations.ts @@ -10,14 +10,28 @@ import { i18n } from '@kbn/i18n'; export const EXCEPTION_ITEM_EDIT_BUTTON = i18n.translate( 'xpack.securitySolution.exceptions.exceptionItem.editItemButton', { - defaultMessage: 'Edit item', + defaultMessage: 'Edit rule exception', } ); export const EXCEPTION_ITEM_DELETE_BUTTON = i18n.translate( 'xpack.securitySolution.exceptions.exceptionItem.deleteItemButton', { - defaultMessage: 'Delete item', + defaultMessage: 'Delete rule exception', + } +); + +export const ENDPOINT_EXCEPTION_ITEM_EDIT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.endpoint.editItemButton', + { + defaultMessage: 'Edit endpoint exception', + } +); + +export const ENDPOINT_EXCEPTION_ITEM_DELETE_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.exceptionItem.endpoint.deleteItemButton', + { + defaultMessage: 'Delete endpoint exception', } ); @@ -159,3 +173,9 @@ export const OS_MAC = i18n.translate( defaultMessage: 'Mac', } ); + +export const AFFECTED_RULES = (numRules: number) => + i18n.translate('xpack.securitySolution.exceptions.exceptionItem.affectedRules', { + values: { numRules }, + defaultMessage: 'Affects {numRules} {numRules, plural, =1 {rule} other {rules}}', + }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx index aa927eef34d76..c83e729669813 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_comments.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/item_comments/index.tsx @@ -18,11 +18,11 @@ import { EuiText, } from '@elastic/eui'; import type { Comment } from '@kbn/securitysolution-io-ts-list-types'; -import * as i18n from './translations'; -import { useCurrentUser } from '../../lib/kibana'; -import { getFormattedComments } from './helpers'; +import * as i18n from '../../utils/translations'; +import { useCurrentUser } from '../../../../common/lib/kibana'; +import { getFormattedComments } from '../../utils/helpers'; -interface AddExceptionCommentsProps { +interface ExceptionItemCommentsProps { exceptionItemComments?: Comment[]; newCommentValue: string; newCommentOnChange: (value: string) => void; @@ -45,11 +45,11 @@ const CommentAccordion = styled(EuiAccordion)` `} `; -export const AddExceptionComments = memo(function AddExceptionComments({ +export const ExceptionItemComments = memo(function ExceptionItemComments({ exceptionItemComments, newCommentValue, newCommentOnChange, -}: AddExceptionCommentsProps) { +}: ExceptionItemCommentsProps) { const [shouldShowComments, setShouldShowComments] = useState(false); const currentUser = useCurrentUser(); @@ -71,7 +71,7 @@ export const AddExceptionComments = memo(function AddExceptionComments({ const commentsAccordionTitle = useMemo(() => { if (exceptionItemComments && exceptionItemComments.length > 0) { return ( - + {!shouldShowComments ? i18n.COMMENTS_SHOW(exceptionItemComments.length) : i18n.COMMENTS_HIDE(exceptionItemComments.length)} @@ -97,7 +97,7 @@ export const AddExceptionComments = memo(function AddExceptionComments({ id={'add-exception-comments-accordion'} buttonClassName={COMMENT_ACCORDION_BUTTON_CLASS_NAME} buttonContent={commentsAccordionTitle} - data-test-subj="addExceptionCommentsAccordion" + data-test-subj="ExceptionItemCommentsAccordion" onToggle={(isOpen) => handleTriggerOnClick(isOpen)} > diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/__tests__/__snapshots__/value_with_space_warning.test.tsx.snap b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/__tests__/__snapshots__/value_with_space_warning.test.tsx.snap similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/__tests__/__snapshots__/value_with_space_warning.test.tsx.snap rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/__tests__/__snapshots__/value_with_space_warning.test.tsx.snap diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/__tests__/use_value_with_space_warning.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/__tests__/use_value_with_space_warning.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/__tests__/use_value_with_space_warning.test.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/__tests__/use_value_with_space_warning.test.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/__tests__/value_with_space_warning.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/__tests__/value_with_space_warning.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/__tests__/value_with_space_warning.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/__tests__/value_with_space_warning.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/index.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/index.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/index.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/index.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/use_value_with_space_warning.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/use_value_with_space_warning.ts similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/use_value_with_space_warning.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/use_value_with_space_warning.ts diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/value_with_space_warning.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/value_with_space_warning.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/viewer/value_with_space_warning/value_with_space_warning.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/components/value_with_space_warning/value_with_space_warning.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.test.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.test.tsx index a451ee9e4963f..36446f9ca2a4b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.test.tsx @@ -9,7 +9,7 @@ import type { RenderHookResult } from '@testing-library/react-hooks'; import { act, renderHook } from '@testing-library/react-hooks'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { coreMock } from '@kbn/core/public/mocks'; -import { KibanaServices } from '../../lib/kibana'; +import { KibanaServices } from '../../../common/lib/kibana'; import * as alertsApi from '../../../detections/containers/detection_engine/alerts/api'; import * as listsApi from '@kbn/securitysolution-list-api'; @@ -23,7 +23,7 @@ import type { CreateExceptionListItemSchema, UpdateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; -import { TestProviders } from '../../mock'; +import { TestProviders } from '../../../common/mock'; import type { UseAddOrUpdateExceptionProps, ReturnUseAddOrUpdateException, @@ -33,7 +33,7 @@ import { useAddOrUpdateException } from './use_add_exception'; const mockKibanaHttpService = coreMock.createStart().http; const mockKibanaServices = KibanaServices.get as jest.Mock; -jest.mock('../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); jest.mock('@kbn/securitysolution-list-api'); const fetchMock = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.tsx similarity index 98% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.tsx index c06f4928ad894..88556d305bb03 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/use_add_exception.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_add_exception.tsx @@ -22,8 +22,8 @@ import { } from '../../../detections/components/alerts_table/default_config'; import { getQueryFilter } from '../../../../common/detection_engine/get_query_filter'; import type { Index } from '../../../../common/detection_engine/schemas/common/schemas'; -import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from './helpers'; -import { useKibana } from '../../lib/kibana'; +import { formatExceptionItemForUpdate, prepareExceptionItemsForBulkClose } from '../utils/helpers'; +import { useKibana } from '../../../common/lib/kibana'; /** * Adds exception items to the list. Also optionally closes alerts. diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_fetch_or_create_rule_exception_list.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_fetch_or_create_rule_exception_list.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_fetch_or_create_rule_exception_list.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/use_fetch_or_create_rule_exception_list.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_fetch_or_create_rule_exception_list.tsx diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx new file mode 100644 index 0000000000000..f30d5aeb598a0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/logic/use_find_references.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useMemo, useState } from 'react'; +import type { ListArray } from '@kbn/securitysolution-io-ts-list-types'; + +import type { RuleReferenceSchema } from '../../../../common/detection_engine/schemas/response'; +import { findRuleExceptionReferences } from '../../../detections/containers/detection_engine/rules/api'; +import { useToasts } from '../../../common/lib/kibana'; +import type { FindRulesReferencedByExceptionsListProp } from '../../../detections/containers/detection_engine/rules/types'; +import * as i18n from '../utils/translations'; + +export type ReturnUseFindExceptionListReferences = [boolean, RuleReferences | null]; + +export interface RuleReferences { + [key: string]: RuleReferenceSchema[]; +} +/** + * Hook for finding what rules are referenced by a set of exception lists + * @param ruleExceptionLists array of exception list info stored on a rule + */ +export const useFindExceptionListReferences = ( + ruleExceptionLists: ListArray +): ReturnUseFindExceptionListReferences => { + const toasts = useToasts(); + const [isLoading, setIsLoading] = useState(false); + const [references, setReferences] = useState(null); + const listRefs = useMemo((): FindRulesReferencedByExceptionsListProp[] => { + return ruleExceptionLists.map((list) => { + return { + id: list.id, + listId: list.list_id, + namespaceType: list.namespace_type, + }; + }); + }, [ruleExceptionLists]); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const findReferences = async () => { + try { + setIsLoading(true); + + const { references: referencesResults } = await findRuleExceptionReferences({ + lists: listRefs, + signal: abortCtrl.signal, + }); + + const results = referencesResults.reduce((acc, result) => { + const [[key, value]] = Object.entries(result); + + acc[key] = value; + + return acc; + }, {}); + + if (isSubscribed) { + setIsLoading(false); + setReferences(results); + } + } catch (error) { + if (isSubscribed) { + setIsLoading(false); + toasts.addError(error, { title: i18n.ERROR_FETCHING_REFERENCES_TITLE }); + } + } + }; + + if (listRefs.length === 0 && isSubscribed) { + setIsLoading(false); + setReferences(null); + } else { + findReferences(); + } + + return (): void => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [ruleExceptionLists, listRefs, toasts]); + + return [isLoading, references]; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_endpoint_fields.json similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_endpoint_fields.json diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_linux_fields.json b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_linux_fields.json similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_linux_fields.json rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_linux_fields.json diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_windows_mac_fields.json b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_windows_mac_fields.json similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_windows_mac_fields.json rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/exceptionable_windows_mac_fields.json diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.test.tsx diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx index c7890f99dc44b..e4087567de39c 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/helpers.tsx @@ -42,7 +42,7 @@ import type { AlertData, Flattened } from './types'; import type { Ecs } from '../../../../common/ecs'; import type { CodeSignature } from '../../../../common/ecs/file'; -import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; +import { WithCopyToClipboard } from '../../../common/lib/clipboard/with_copy_to_clipboard'; import exceptionableLinuxFields from './exceptionable_linux_fields.json'; import exceptionableWindowsMacFields from './exceptionable_windows_mac_fields.json'; import exceptionableEndpointFields from './exceptionable_endpoint_fields.json'; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/translations.ts new file mode 100644 index 0000000000000..8f189c7aaf7db --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/translations.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const COMMENTS_SHOW = (comments: number) => + i18n.translate('xpack.securitySolution.exceptions.showCommentsLabel', { + values: { comments }, + defaultMessage: 'Show ({comments}) {comments, plural, =1 {Comment} other {Comments}}', + }); + +export const COMMENTS_HIDE = (comments: number) => + i18n.translate('xpack.securitySolution.exceptions.hideCommentsLabel', { + values: { comments }, + defaultMessage: 'Hide ({comments}) {comments, plural, =1 {Comment} other {Comments}}', + }); + +export const COMMENT_EVENT = i18n.translate('xpack.securitySolution.exceptions.commentEventLabel', { + defaultMessage: 'added a comment', +}); + +export const OPERATING_SYSTEM_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.operatingSystemFullLabel', + { + defaultMessage: 'Operating System', + } +); + +export const ADD_TO_ENDPOINT_LIST = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.addToEndpointListLabel', + { + defaultMessage: 'Add endpoint exception', + } +); + +export const ADD_TO_DETECTIONS_LIST = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel', + { + defaultMessage: 'Add rule exception', + } +); + +export const ADD_COMMENT_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.addCommentPlaceholder', + { + defaultMessage: 'Add a new comment...', + } +); + +export const ADD_TO_CLIPBOARD = i18n.translate( + 'xpack.securitySolution.exceptions.viewer.addToClipboard', + { + defaultMessage: 'Comment', + } +); + +export const CLEAR_EXCEPTIONS_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.clearExceptionsLabel', + { + defaultMessage: 'Remove Exception List', + } +); + +export const ADD_EXCEPTION_FETCH_404_ERROR = (listId: string) => + i18n.translate('xpack.securitySolution.exceptions.fetch404Error', { + values: { listId }, + defaultMessage: + 'The associated exception list ({listId}) no longer exists. Please remove the missing exception list to add additional exceptions to the detection rule.', + }); + +export const ADD_EXCEPTION_FETCH_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.fetchError', + { + defaultMessage: 'Error fetching exception list', + } +); + +export const ERROR = i18n.translate('xpack.securitySolution.exceptions.errorLabel', { + defaultMessage: 'Error', +}); + +export const CANCEL = i18n.translate('xpack.securitySolution.exceptions.cancelLabel', { + defaultMessage: 'Cancel', +}); + +export const MODAL_ERROR_ACCORDION_TEXT = i18n.translate( + 'xpack.securitySolution.exceptions.modalErrorAccordionText', + { + defaultMessage: 'Show rule reference information:', + } +); + +export const DISSASOCIATE_LIST_SUCCESS = (id: string) => + i18n.translate('xpack.securitySolution.exceptions.dissasociateListSuccessText', { + values: { id }, + defaultMessage: 'Exception list ({id}) has successfully been removed', + }); + +export const DISSASOCIATE_EXCEPTION_LIST_ERROR = i18n.translate( + 'xpack.securitySolution.exceptions.dissasociateExceptionListError', + { + defaultMessage: 'Failed to remove exception list', + } +); + +export const OPERATING_SYSTEM_WINDOWS = i18n.translate( + 'xpack.securitySolution.exceptions.operatingSystemWindows', + { + defaultMessage: 'Windows', + } +); + +export const OPERATING_SYSTEM_MAC = i18n.translate( + 'xpack.securitySolution.exceptions.operatingSystemMac', + { + defaultMessage: 'macOS', + } +); + +export const OPERATING_SYSTEM_WINDOWS_AND_MAC = i18n.translate( + 'xpack.securitySolution.exceptions.operatingSystemWindowsAndMac', + { + defaultMessage: 'Windows and macOS', + } +); + +export const OPERATING_SYSTEM_LINUX = i18n.translate( + 'xpack.securitySolution.exceptions.operatingSystemLinux', + { + defaultMessage: 'Linux', + } +); + +export const ERROR_FETCHING_REFERENCES_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.fetchingReferencesErrorToastTitle', + { + defaultMessage: 'Error fetching exception references', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts similarity index 83% rename from x-pack/plugins/security_solution/public/common/components/exceptions/types.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts index fe0f137800d26..56db2ad8257f8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_exceptions/utils/types.ts @@ -11,21 +11,10 @@ import type { CodeSignature } from '../../../../common/ecs/file'; export interface ExceptionListItemIdentifiers { id: string; + name: string; namespaceType: NamespaceType; } -export interface FilterOptions { - filter: string; - tags: string[]; -} - -export interface Filter { - filter: Partial; - pagination: Partial; - showDetectionsListsOnly: boolean; - showEndpointListsOnly: boolean; -} - export interface ExceptionsPagination { pageIndex: number; pageSize: number; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 436c4f63ac77c..0127fe63efea7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -17,17 +17,17 @@ import { DEFAULT_ACTION_BUTTON_WIDTH } from '@kbn/timelines-plugin/public'; import { useOsqueryContextActionItem } from '../../osquery/use_osquery_context_action_item'; import { OsqueryFlyout } from '../../osquery/osquery_flyout'; import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; -import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; +import { buildGetAlertByIdQuery } from '../../../../detection_engine/rule_exceptions/utils/helpers'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; import type { Ecs } from '../../../../../common/ecs'; -import type { AddExceptionFlyoutProps } from '../../../../common/components/exceptions/add_exception_flyout'; -import { AddExceptionFlyout } from '../../../../common/components/exceptions/add_exception_flyout'; +import type { AddExceptionFlyoutProps } from '../../../../detection_engine/rule_exceptions/components/add_exception_flyout'; +import { AddExceptionFlyout } from '../../../../detection_engine/rule_exceptions/components/add_exception_flyout'; import * as i18n from '../translations'; import type { inputsModel, State } from '../../../../common/store'; import { inputsSelectors } from '../../../../common/store'; import { TimelineId } from '../../../../../common/types'; -import type { AlertData, EcsHit } from '../../../../common/components/exceptions/types'; +import type { AlertData, EcsHit } from '../../../../detection_engine/rule_exceptions/utils/types'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { ALERTS_QUERY_NAMES } from '../../../containers/detection_engine/alerts/constants'; import { useSignalIndex } from '../../../containers/detection_engine/alerts/use_signal_index'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index 577ecca52c59e..ce5b7ee9c5de5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -73,7 +73,6 @@ export const useInvestigateInTimeline = ({ if (exceptionsLists.length > 0) { await getExceptionListsItems({ lists: exceptionsLists, - filterOptions: [], pagination: { page: 0, perPage: 10000, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index 48fa12f03565f..2511e8da834f0 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -20,6 +20,7 @@ import { fetchTags, getPrePackagedRulesStatus, previewRule, + findRuleExceptionReferences, } from './api'; import { getRulesSchemaMock } from '../../../../../common/detection_engine/schemas/response/rules_schema.mocks'; import { @@ -28,6 +29,8 @@ import { } from '../../../../../common/detection_engine/schemas/request/rule_schemas.mock'; import { getPatchRulesSchemaMock } from '../../../../../common/detection_engine/schemas/request/patch_rules_schema.mock'; import { rulesMock } from './mock'; +import type { FindRulesReferencedByExceptionsListProp } from './types'; +import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../../common/constants'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -664,4 +667,36 @@ describe('Detections Rules API', () => { expect(resp).toEqual(prePackagedRulesStatus); }); }); + + describe('findRuleExceptionReferences', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(getRulesSchemaMock()); + }); + + test('GETs exception references', async () => { + const payload: FindRulesReferencedByExceptionsListProp[] = [ + { + id: '123', + listId: 'list_id_1', + namespaceType: 'single', + }, + { + id: '456', + listId: 'list_id_2', + namespaceType: 'single', + }, + ]; + await findRuleExceptionReferences({ lists: payload, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, { + query: { + ids: '123,456', + list_ids: 'list_id_1,list_id_2', + namespace_types: 'single,single', + }, + method: 'GET', + signal: abortCtrl.signal, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index 876a3a0a469a8..f2e78eeee99ef 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -17,6 +17,7 @@ import { DETECTION_ENGINE_RULES_PREVIEW, DETECTION_ENGINE_INSTALLED_INTEGRATIONS_URL, DETECTION_ENGINE_RULES_URL_FIND, + DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, } from '../../../../../common/constants'; import type { BulkAction } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; import type { @@ -26,6 +27,7 @@ import type { import type { RulesSchema, GetInstalledIntegrationsResponse, + RulesReferencedByExceptionListsSchema, } from '../../../../../common/detection_engine/schemas/response'; import type { @@ -43,6 +45,7 @@ import type { BulkActionProps, BulkActionResponseMap, PreviewRulesProps, + FindRulesReferencedByExceptionsProps, } from './types'; import { KibanaServices } from '../../../../common/lib/kibana'; import * as i18n from '../../../pages/detection_engine/rules/translations'; @@ -369,3 +372,28 @@ export const fetchInstalledIntegrations = async ({ signal, } ); + +/** + * Fetch info on what exceptions lists are referenced by what rules + * + * @param lists exception list information needed for making request + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const findRuleExceptionReferences = async ({ + lists, + signal, +}: FindRulesReferencedByExceptionsProps): Promise => + KibanaServices.get().http.fetch( + DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, + { + method: 'GET', + query: { + ids: lists.map(({ id }) => id).join(','), + list_ids: lists.map(({ listId }) => listId).join(','), + namespace_types: lists.map(({ namespaceType }) => namespaceType).join(','), + }, + signal, + } + ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 45c89c307de4e..77811b3d8aa2d 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -7,6 +7,7 @@ import * as t from 'io-ts'; +import type { NamespaceType } from '@kbn/securitysolution-io-ts-list-types'; import { listArray } from '@kbn/securitysolution-io-ts-list-types'; import type { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { @@ -341,3 +342,14 @@ export interface PrePackagedRulesStatusResponse { timelines_not_installed: number; timelines_not_updated: number; } + +export interface FindRulesReferencedByExceptionsListProp { + id: string; + listId: string; + namespaceType: NamespaceType; +} + +export interface FindRulesReferencedByExceptionsProps { + lists: FindRulesReferencedByExceptionsListProp[]; + signal?: AbortSignal; +} diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 66892235a0f91..6c5ff0ab2851e 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -294,4 +294,70 @@ describe('RuleDetailsPageComponent', () => { }); }); }); + + it('renders exceptions tab', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { + ...mockRule, + outcome: 'conflict', + alias_target_id: 'aliased_rule_id', + alias_purpose: 'savedObjectConversion', + }, + }); + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="navigation-rule_exceptions"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="navigation-endpoint_exceptions"]').exists() + ).toBeFalsy(); + }); + }); + + it('renders endpoint exeptions tab when rule includes endpoint list', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { + ...mockRule, + outcome: 'conflict', + alias_target_id: 'aliased_rule_id', + alias_purpose: 'savedObjectConversion', + exceptions_list: [ + { + id: 'endpoint_list', + list_id: 'endpoint_list', + type: 'endpoint', + namespace_type: 'agnostic', + }, + ], + }, + }); + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="navigation-rule_exceptions"]').exists()).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="navigation-endpoint_exceptions"]').exists() + ).toBeTruthy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 0660df924f56a..cc36e6f75da4b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -26,7 +26,6 @@ import { Switch, useParams } from 'react-router-dom'; import type { ConnectedProps } from 'react-redux'; import { connect, useDispatch } from 'react-redux'; import styled from 'styled-components'; -import type { ExceptionListIdentifiers } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { Dispatch } from 'redux'; @@ -79,8 +78,7 @@ import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_l import { SecurityPageName } from '../../../../../app/types'; import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; -import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; -import { APP_UI_ID, DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; +import { APP_UI_ID } from '../../../../../../common/constants'; import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; @@ -127,6 +125,7 @@ import { } from '../../../../components/alerts_table/alerts_filter_group'; import { useSignalHelpers } from '../../../../../common/containers/sourcerer/use_signal_helpers'; import { HeaderPage } from '../../../../../common/components/header_page'; +import { ExceptionsViewer } from '../../../../../detection_engine/rule_exceptions/components/all_exception_items_table'; import type { NavTab } from '../../../../../common/components/navigation/types'; /** @@ -148,6 +147,7 @@ const StyledMinHeightTabContainer = styled.div` export enum RuleDetailTabs { alerts = 'alerts', exceptions = 'rule_exceptions', + endpointExceptions = 'endpoint_exceptions', executionResults = 'execution_results', executionEvents = 'execution_events', } @@ -155,6 +155,7 @@ export enum RuleDetailTabs { export const RULE_DETAILS_TAB_NAME: Record = { [RuleDetailTabs.alerts]: detectionI18n.ALERT, [RuleDetailTabs.exceptions]: i18n.EXCEPTIONS_TAB, + [RuleDetailTabs.endpointExceptions]: i18n.ENDPOINT_EXCEPTIONS_TAB, [RuleDetailTabs.executionResults]: i18n.EXECUTION_RESULTS_TAB, [RuleDetailTabs.executionEvents]: i18n.EXECUTION_EVENTS_TAB, }; @@ -220,7 +221,6 @@ const RuleDetailsPageComponent: React.FC = ({ runtimeMappings, loading: isLoadingIndexPattern, } = useSourcererDataView(SourcererScopeName.detections); - const loading = userInfoLoading || listsConfigLoading; const { detailName: ruleId } = useParams<{ detailName: string; @@ -247,9 +247,15 @@ const RuleDetailsPageComponent: React.FC = ({ [RuleDetailTabs.exceptions]: { id: RuleDetailTabs.exceptions, name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.exceptions], - disabled: false, + disabled: rule == null, href: `/rules/id/${ruleId}/${RuleDetailTabs.exceptions}`, }, + [RuleDetailTabs.endpointExceptions]: { + id: RuleDetailTabs.endpointExceptions, + name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.endpointExceptions], + disabled: rule == null, + href: `/rules/id/${ruleId}/${RuleDetailTabs.endpointExceptions}`, + }, [RuleDetailTabs.executionResults]: { id: RuleDetailTabs.executionResults, name: RULE_DETAILS_TAB_NAME[RuleDetailTabs.executionResults], @@ -263,10 +269,11 @@ const RuleDetailsPageComponent: React.FC = ({ href: `/rules/id/${ruleId}/${RuleDetailTabs.executionEvents}`, }, }), - [isExistingRule, ruleId] + [isExistingRule, rule, ruleId] ); const [pageTabs, setTabs] = useState>>(ruleDetailTabs); + const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = rule != null ? getStepsData({ rule, detailsView: true }) @@ -389,11 +396,19 @@ const RuleDetailsPageComponent: React.FC = ({ if (!ruleExecutionSettings.extendedLogging.isEnabled) { hiddenTabs.push(RuleDetailTabs.executionEvents); } + if (rule != null) { + const hasEndpointList = (rule.exceptions_list ?? []).some( + (list) => list.type === ExceptionListTypeEnum.ENDPOINT + ); + if (!hasEndpointList) { + hiddenTabs.push(RuleDetailTabs.endpointExceptions); + } + } const tabs = omit>(hiddenTabs, ruleDetailTabs); setTabs(tabs); - }, [hasIndexRead, ruleDetailTabs, ruleExecutionSettings]); + }, [hasIndexRead, rule, ruleDetailTabs, ruleExecutionSettings]); const showUpdating = useMemo( () => isLoadingIndexPattern || isAlertsLoading || loading, @@ -611,34 +626,6 @@ const RuleDetailsPageComponent: React.FC = ({ [setShowOnlyThreatIndicatorAlerts] ); - const exceptionLists = useMemo((): { - lists: ExceptionListIdentifiers[]; - allowedExceptionListTypes: ExceptionListTypeEnum[]; - } => { - if (rule != null && rule.exceptions_list != null) { - return rule.exceptions_list.reduce<{ - lists: ExceptionListIdentifiers[]; - allowedExceptionListTypes: ExceptionListTypeEnum[]; - }>( - (acc, { id, list_id: listId, namespace_type: namespaceType, type }) => { - const { allowedExceptionListTypes, lists } = acc; - const shouldAddEndpoint = - type === ExceptionListTypeEnum.ENDPOINT && - !allowedExceptionListTypes.includes(ExceptionListTypeEnum.ENDPOINT); - return { - lists: [...lists, { id, listId, namespaceType, type }], - allowedExceptionListTypes: shouldAddEndpoint - ? [...allowedExceptionListTypes, ExceptionListTypeEnum.ENDPOINT] - : allowedExceptionListTypes, - }; - }, - { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] } - ); - } else { - return { lists: [], allowedExceptionListTypes: [ExceptionListTypeEnum.DETECTION] }; - } - }, [rule]); - const onSkipFocusBeforeEventsTable = useCallback(() => { focusUtilityBarAction(containerElement.current); }, [containerElement]); @@ -858,14 +845,20 @@ const RuleDetailsPageComponent: React.FC = ({ + + + diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts index 02e8e01a281e9..1a50d570ea179 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/translations.ts @@ -42,6 +42,13 @@ export const EXCEPTIONS_TAB = i18n.translate( } ); +export const ENDPOINT_EXCEPTIONS_TAB = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleDetails.endpointExceptionsTab', + { + defaultMessage: 'Endpoint exceptions', + } +); + export const EXECUTION_RESULTS_TAB = i18n.translate( 'xpack.securitySolution.detectionEngine.ruleDetails.ruleExecutionResultsTab', { diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/get_formatted_comments.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/get_formatted_comments.tsx index a75d2a76ae704..0d4c8caf892b2 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/get_formatted_comments.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/utils/get_formatted_comments.tsx @@ -10,7 +10,7 @@ import type { EuiCommentProps } from '@elastic/eui'; import { EuiAvatar, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import type { CommentsArray } from '@kbn/securitysolution-io-ts-list-types'; -import { COMMENT_EVENT } from '../../../../common/components/exceptions/translations'; +import { COMMENT_EVENT } from '../../../../detection_engine/rule_exceptions/utils/translations'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; const CustomEuiAvatar = styled(EuiAvatar)` diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx index f157a7e4d804e..f3431de72d7ce 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/form.tsx @@ -29,13 +29,11 @@ import { getExceptionBuilderComponentLazy } from '@kbn/lists-plugin/public'; import type { OnChangeProps } from '@kbn/lists-plugin/public'; import { useTestIdGenerator } from '../../../../hooks/use_test_id_generator'; import type { PolicyData } from '../../../../../../common/endpoint/types'; -import { AddExceptionComments } from '../../../../../common/components/exceptions/add_exception_comments'; import { useFetchIndex } from '../../../../../common/containers/source'; import { Loader } from '../../../../../common/components/loader'; import { useLicense } from '../../../../../common/hooks/use_license'; import { useKibana } from '../../../../../common/lib/kibana'; import type { ArtifactFormComponentProps } from '../../../../components/artifact_list_page'; -import { filterIndexPatterns } from '../../../../../common/components/exceptions/helpers'; import { isArtifactGlobal, getPolicyIdsFromArtifact, @@ -57,6 +55,8 @@ import { ENDPOINT_EVENT_FILTERS_LIST_ID, EVENT_FILTER_LIST_TYPE } from '../../co import type { EffectedPolicySelection } from '../../../../components/effected_policy_select'; import { EffectedPolicySelect } from '../../../../components/effected_policy_select'; import { isGlobalPolicyEffected } from '../../../../components/effected_policy_select/utils'; +import { ExceptionItemComments } from '../../../../../detection_engine/rule_exceptions/components/item_comments'; +import { filterIndexPatterns } from '../../../../../detection_engine/rule_exceptions/utils/helpers'; const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ OperatingSystem.MAC, @@ -316,7 +316,7 @@ export const EventFiltersForm: React.FC ( - { + let server: ReturnType; + let { clients, context } = requestContextMock.createTools(); + + beforeEach(() => { + server = serverMock.create(); + ({ clients, context } = requestContextMock.createTools()); + + clients.rulesClient.find.mockResolvedValue({ + ...getFindResultWithSingleHit(), + data: [ + { + ...getRuleMock({ + ...getQueryRuleParams(), + exceptionsList: [ + { + type: 'detection', + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + list_id: 'my_default_list', + namespace_type: 'single', + }, + ], + }), + }, + ], + }); + + findRuleExceptionReferencesRoute(server.router); + }); + + describe('happy paths', () => { + test('returns 200 when adding an exception item and rule_default exception list already exists', async () => { + const request = requestMock.create({ + method: 'get', + path: `${DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL}?exception_list`, + query: { + ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, + list_ids: `my_default_list`, + namespace_types: `single`, + }, + }); + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + references: [ + { + my_default_list: [ + { + exception_lists: [ + { + id: '4656dc92-5832-11ea-8e2d-0242ac130003', + list_id: 'my_default_list', + namespace_type: 'single', + type: 'detection', + }, + ], + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + name: 'Detect Root/Admin Users', + rule_id: 'rule-1', + }, + ], + }, + ], + }); + }); + + test('returns 200 when no references found', async () => { + const request = requestMock.create({ + method: 'get', + path: DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, + query: { + ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, + list_ids: `my_default_list`, + namespace_types: `single`, + }, + }); + + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + references: [ + { + my_default_list: [], + }, + ], + }); + }); + }); + + describe('error codes', () => { + test('returns 400 if query param lengths do not match', async () => { + const request = requestMock.create({ + method: 'get', + path: DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, + query: { + ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, + list_ids: `my_default_list`, + namespace_types: `single,agnostic`, + }, + }); + + clients.rulesClient.find.mockResolvedValue(getEmptyFindResult()); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(400); + expect(response.body).toEqual({ + message: + '"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: 1 to equal "namespace_types" length: 2 and "list_ids" length: 1.', + status_code: 400, + }); + }); + + test('returns 500 if rules client fails', async () => { + const request = requestMock.create({ + method: 'get', + path: DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, + query: { + ids: `4656dc92-5832-11ea-8e2d-0242ac130003`, + list_ids: `my_default_list`, + namespace_types: `single`, + }, + }); + + clients.rulesClient.find.mockRejectedValue(new Error('find request failed')); + + const response = await server.inject(request, requestContextMock.convertContext(context)); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ message: 'find request failed', status_code: 500 }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts new file mode 100644 index 0000000000000..8ac258322e260 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rule_exceptions_route.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformError } from '@kbn/securitysolution-es-utils'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import type { FindResult } from '@kbn/alerting-plugin/server'; + +import type { SecuritySolutionPluginRouter } from '../../../../types'; +import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '../../../../../common/constants'; +import { buildSiemResponse } from '../utils'; +import { enrichFilterWithRuleTypeMapping } from '../../rules/enrich_filter_with_rule_type_mappings'; +import type { FindExceptionReferencesOnRuleSchemaDecoded } from '../../../../../common/detection_engine/schemas/request/find_exception_list_references_schema'; +import { findExceptionReferencesOnRuleSchema } from '../../../../../common/detection_engine/schemas/request/find_exception_list_references_schema'; +import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; +import type { RuleReferencesSchema } from '../../../../../common/detection_engine/schemas/response/find_exception_list_references_schema'; +import { rulesReferencedByExceptionListsSchema } from '../../../../../common/detection_engine/schemas/response/find_exception_list_references_schema'; +import type { RuleParams } from '../../schemas/rule_schemas'; + +export const findRuleExceptionReferencesRoute = (router: SecuritySolutionPluginRouter) => { + router.get( + { + path: DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL, + validate: { + query: buildRouteValidation< + typeof findExceptionReferencesOnRuleSchema, + FindExceptionReferencesOnRuleSchemaDecoded + >(findExceptionReferencesOnRuleSchema), + }, + options: { + tags: ['access:securitySolution'], + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const { ids, namespace_types: namespaceTypes, list_ids: listIds } = request.query; + + const ctx = await context.resolve(['core', 'securitySolution', 'alerting']); + const rulesClient = ctx.alerting.getRulesClient(); + + if (ids.length !== namespaceTypes.length || ids.length !== listIds.length) { + return siemResponse.error({ + body: `"ids", "list_ids" and "namespace_types" need to have the same comma separated number of values. Expected "ids" length: ${ids.length} to equal "namespace_types" length: ${namespaceTypes.length} and "list_ids" length: ${listIds.length}.`, + statusCode: 400, + }); + } + + const foundRules: Array> = await Promise.all( + ids.map(async (id, index) => { + return rulesClient.find({ + options: { + perPage: 10000, + filter: enrichFilterWithRuleTypeMapping(null), + hasReference: { + id, + type: getSavedObjectType({ namespaceType: namespaceTypes[index] }), + }, + }, + }); + }) + ); + + const references = foundRules.map(({ data }, index) => { + const wantedData = data.map(({ name, id, params }) => ({ + name, + id, + rule_id: params.ruleId, + exception_lists: params.exceptionsList, + })); + return { [listIds[index]]: wantedData }; + }); + + const [validated, errors] = validate({ references }, rulesReferencedByExceptionListsSchema); + + if (errors != null) { + return siemResponse.error({ statusCode: 500, body: errors }); + } else { + return response.ok({ body: validated ?? { references: [] } }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index 5e8e700fcc1e0..af14b3c01226d 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -74,6 +74,7 @@ import { createPrebuiltSavedObjectsRoute } from '../lib/prebuilt_saved_objects/r import { readAlertsIndexExistsRoute } from '../lib/detection_engine/routes/index/read_alerts_index_exists_route'; import { getInstalledIntegrationsRoute } from '../lib/detection_engine/routes/fleet/get_installed_integrations/get_installed_integrations_route'; import { registerResolverRoutes } from '../endpoint/routes/resolver'; +import { findRuleExceptionReferencesRoute } from '../lib/detection_engine/routes/rules/find_rule_exceptions_route'; import { createRuleExceptionsRoute } from '../lib/detection_engine/routes/rules/create_rule_exceptions_route'; export const initRoutes = ( @@ -132,6 +133,7 @@ export const initRoutes = ( patchTimelinesRoute(router, config, security); importRulesRoute(router, config, ml); exportRulesRoute(router, config, logger); + findRuleExceptionReferencesRoute(router); importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security); diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 77e4c5bba571b..43b3cbacf2a8d 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -25449,17 +25449,12 @@ "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, =1 {événement} other {événements}}", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "La liste d'exceptions ({id}) a été retirée avec succès", "xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel": "Afficher {comments, plural, =1 {commentaire} other {commentaires}} ({comments})", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "Éléments par page : {items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "Une erreur s'est produite lors du chargement des politiques : \"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "La liste d'exceptions associée ({listId}) n'existe plus. Veuillez retirer la liste d'exceptions manquante pour ajouter des exceptions supplémentaires à la règle de détection.", "xpack.securitySolution.exceptions.hideCommentsLabel": "Masquer ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} éléments", "xpack.securitySolution.exceptions.referenceModalDescription": "Cette liste d'exceptions est associée à ({referenceCount}) {referenceCount, plural, =1 {règle} other {règles}}. Le retrait de cette liste d'exceptions supprimera également sa référence des règles associées.", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "Liste d'exceptions - {listId} - supprimée avec succès.", "xpack.securitySolution.exceptions.showCommentsLabel": "Afficher ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "Affichage de {items} {items, plural, =1 {exception} other {exceptions}}", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "Toutes les exceptions à cette règle sont appliquées à la règle de détection, et non au point de terminaison. Afficher les {ruleSettings} pour plus de détails.", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "Toutes les exceptions à cette règle sont appliquées au point de terminaison et à la règle de détection. Afficher les {ruleSettings} pour plus de détails.", "xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "Description pour le champ {field} :", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "Lorsque l'actualisation automatique est activée, la chronologie vous montrera les {numberOfItems} derniers événements correspondant à votre recherche.", "xpack.securitySolution.formattedNumber.countsLabel": "{mantissa}{scale}{hasRemainder}", @@ -28026,16 +28021,12 @@ "xpack.securitySolution.exceptions.addException.operatingSystemPlaceHolder": "Sélectionner un système d'exploitation", "xpack.securitySolution.exceptions.addException.sequenceWarning": "La requête de cette règle contient une instruction de séquence EQL. L'exception créée s'appliquera à tous les événements de la séquence.", "xpack.securitySolution.exceptions.addException.success": "Exception ajoutée avec succès", - "xpack.securitySolution.exceptions.andDescription": "AND", "xpack.securitySolution.exceptions.badge.readOnly.tooltip": "Impossible de créer, de modifier ou de supprimer des exceptions", "xpack.securitySolution.exceptions.cancelLabel": "Annuler", "xpack.securitySolution.exceptions.clearExceptionsLabel": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.commentEventLabel": "a ajouté un commentaire", - "xpack.securitySolution.exceptions.commentLabel": "Commentaire", - "xpack.securitySolution.exceptions.descriptionLabel": "Description", - "xpack.securitySolution.exceptions.detectionListLabel": "Liste de détection", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "Impossible de retirer la liste d'exceptions", - "xpack.securitySolution.exceptions.editButtonLabel": "Modifier", + "xpack.securitySolution.exceptions.dissasociateListSuccessText": "La liste d'exceptions ({id}) a été retirée avec succès", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "Fermer toutes les alertes qui correspondent à cette exception et ont été générées par cette règle", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "Fermer toutes les alertes qui correspondent à cette exception et ont été générées par cette règle (les listes et les champs non ECS ne sont pas pris en charge)", "xpack.securitySolution.exceptions.editException.cancel": "Annuler", @@ -28048,8 +28039,12 @@ "xpack.securitySolution.exceptions.editException.success": "L'exception a été mise à jour avec succès", "xpack.securitySolution.exceptions.editException.versionConflictDescription": "Cette exception semble avoir été mise à jour depuis que vous l'avez sélectionnée pour la modifier. Essayez de cliquer sur \"Annuler\" et de modifier à nouveau l'exception.", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "Désolé, une erreur est survenue", - "xpack.securitySolution.exceptions.endpointListLabel": "Liste de points de terminaison", "xpack.securitySolution.exceptions.errorLabel": "Erreur", + "xpack.securitySolution.exceptions.failedLoadPolicies": "Une erreur s'est produite lors du chargement des politiques : \"{error}\"", + "xpack.securitySolution.exceptions.fetch404Error": "La liste d'exceptions associée ({listId}) n'existe plus. Veuillez retirer la liste d'exceptions manquante pour ajouter des exceptions supplémentaires à la règle de détection.", + "xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions", + "xpack.securitySolution.exceptions.hideCommentsLabel": "Masquer ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", + "xpack.securitySolution.exceptions.modalErrorAccordionText": "Afficher les informations de référence de la règle :", "xpack.securitySolution.exceptions.exceptionItem.conditions.and": "AND", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator": "existe", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator.not": "n'existe pas", @@ -28072,37 +28067,21 @@ "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "par", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "Mis à jour", "xpack.securitySolution.exceptions.fetchError": "Erreur lors de la récupération de la liste d'exceptions", - "xpack.securitySolution.exceptions.fieldDescription": "Champ", "xpack.securitySolution.exceptions.modalErrorAccordionText": "Afficher les informations de référence de la règle :", - "xpack.securitySolution.exceptions.nameLabel": "Nom", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "Système d'exploitation", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "Windows et macOS", - "xpack.securitySolution.exceptions.operatorDescription": "Opérateur", - "xpack.securitySolution.exceptions.orDescription": "OR", "xpack.securitySolution.exceptions.referenceModalCancelButton": "Annuler", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "Retirer la liste d'exceptions", "xpack.securitySolution.exceptions.referenceModalTitle": "Retirer la liste d'exceptions", - "xpack.securitySolution.exceptions.removeButtonLabel": "Retirer", "xpack.securitySolution.exceptions.searchPlaceholder": "par ex. Exemple de liste de noms", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "Actualiser", - "xpack.securitySolution.exceptions.valueDescription": "Valeur", + "xpack.securitySolution.exceptions.showCommentsLabel": "Afficher ({comments}) {comments, plural, =1 {commentaire} other {commentaires}}", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "Ajouter un nouveau commentaire...", - "xpack.securitySolution.exceptions.viewer.addExceptionLabel": "Ajouter une nouvelle exception", "xpack.securitySolution.exceptions.viewer.addToClipboard": "Commentaire", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "Ajouter une exception à une règle", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "Ajouter une exception de point de terminaison", - "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "Erreur lors de la suppression de l'exception", - "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "Vous pouvez ajouter des exceptions pour affiner la règle de façon à ce que les alertes de détection ne soient pas créées lorsque les conditions d'exception sont remplies. Les exceptions améliorent la précision de la détection, ce qui peut permettre de réduire le nombre de faux positifs.", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "Cette règle ne possède aucune exception", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "paramètres de règles", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "paramètres de règles", - "xpack.securitySolution.exceptions.viewer.fetchingListError": "Erreur lors de la récupération des exceptions", - "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "Erreur lors de l'obtention des totaux d'éléments de l'exception", - "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "Aucun résultat n'a été trouvé pour la recherche.", - "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "Champ de recherche (ex : host.name)", "xpack.securitySolution.exitFullScreenButton": "Quitter le plein écran", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "Masquer les valeurs les plus élevées", "xpack.securitySolution.expandedValue.links.expandIpDetails": "Développer les détails d'IP", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index bd85b88aef4c8..1d8efe71c1530 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25426,17 +25426,12 @@ "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, other {イベント}}", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外リスト({id})が正常に削除されました", "xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel": "{comments, plural, other {件のコメント}}を表示({comments})", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "ページごとの項目数:{items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "ポリシーの読み込みエラーが発生しました:\"{error}\"", "xpack.securitySolution.exceptions.fetch404Error": "関連付けられた例外リスト({listId})は存在しません。その他の例外を検出ルールに追加するには、見つからない例外リストを削除してください。", "xpack.securitySolution.exceptions.hideCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を非表示", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items}個の項目", "xpack.securitySolution.exceptions.referenceModalDescription": "この例外リストは、({referenceCount}) {referenceCount, plural, other {個のルール}}に関連付けられています。この例外リストを削除すると、関連付けられたルールからの参照も削除されます。", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "例外リスト{listId}が正常に削除されました。", "xpack.securitySolution.exceptions.showCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を表示", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "{items} {items, plural, other {件の例外}}を表示しています", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "このルールのすべての例外は、エンドポイントではなく、検出ルールに適用されます。詳細については、{ruleSettings}を確認してください。", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "このルールのすべての例外は、エンドポイントと検出ルールに適用されます。詳細については、{ruleSettings}を確認してください。", "xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "フィールド {field} の説明:", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "自動更新が有効な間、タイムラインはクエリに一致する最新の {numberOfItems} 件のイベントを表示します。", "xpack.securitySolution.formattedNumber.countsLabel": "{mantissa}{scale}{hasRemainder}", @@ -28003,16 +27998,12 @@ "xpack.securitySolution.exceptions.addException.operatingSystemPlaceHolder": "オペレーティングシステムを選択", "xpack.securitySolution.exceptions.addException.sequenceWarning": "このルールのクエリにはEQLシーケンス文があります。作成された例外は、シーケンスのすべてのイベントに適用されます。", "xpack.securitySolution.exceptions.addException.success": "正常に例外を追加しました", - "xpack.securitySolution.exceptions.andDescription": "AND", "xpack.securitySolution.exceptions.badge.readOnly.tooltip": "例外を作成、編集、削除できません", "xpack.securitySolution.exceptions.cancelLabel": "キャンセル", "xpack.securitySolution.exceptions.clearExceptionsLabel": "例外リストを削除", "xpack.securitySolution.exceptions.commentEventLabel": "コメントを追加しました", - "xpack.securitySolution.exceptions.commentLabel": "コメント", - "xpack.securitySolution.exceptions.descriptionLabel": "説明", - "xpack.securitySolution.exceptions.detectionListLabel": "検出リスト", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "例外リストを削除できませんでした", - "xpack.securitySolution.exceptions.editButtonLabel": "編集", + "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外リスト({id})が正常に削除されました", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "この例外一致し、このルールによって生成された、すべてのアラートを閉じる", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "この例外と一致し、このルールによって生成された、すべてのアラートを閉じる(リストと非ECSフィールドはサポートされません)", "xpack.securitySolution.exceptions.editException.cancel": "キャンセル", @@ -28025,8 +28016,11 @@ "xpack.securitySolution.exceptions.editException.success": "正常に例外を更新しました", "xpack.securitySolution.exceptions.editException.versionConflictDescription": "最初に編集することを選択したときからこの例外が更新されている可能性があります。[キャンセル]をクリックし、もう一度例外を編集してください。", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "申し訳ございません、エラーが発生しました", - "xpack.securitySolution.exceptions.endpointListLabel": "エンドポイントリスト", "xpack.securitySolution.exceptions.errorLabel": "エラー", + "xpack.securitySolution.exceptions.failedLoadPolicies": "ポリシーの読み込みエラーが発生しました:\"{error}\"", + "xpack.securitySolution.exceptions.fetch404Error": "関連付けられた例外リスト({listId})は存在しません。その他の例外を検出ルールに追加するには、見つからない例外リストを削除してください。", + "xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー", + "xpack.securitySolution.exceptions.hideCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を非表示", "xpack.securitySolution.exceptions.exceptionItem.conditions.and": "AND", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator": "存在する", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator.not": "存在しない", @@ -28049,37 +28043,21 @@ "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "グループ基準", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "更新しました", "xpack.securitySolution.exceptions.fetchError": "例外リストの取得エラー", - "xpack.securitySolution.exceptions.fieldDescription": "フィールド", "xpack.securitySolution.exceptions.modalErrorAccordionText": "ルール参照情報を表示:", - "xpack.securitySolution.exceptions.nameLabel": "名前", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "オペレーティングシステム", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "WindowsおよびmacOS", - "xpack.securitySolution.exceptions.operatorDescription": "演算子", - "xpack.securitySolution.exceptions.orDescription": "OR", "xpack.securitySolution.exceptions.referenceModalCancelButton": "キャンセル", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "例外リストを削除", "xpack.securitySolution.exceptions.referenceModalTitle": "例外リストを削除", - "xpack.securitySolution.exceptions.removeButtonLabel": "削除", "xpack.securitySolution.exceptions.searchPlaceholder": "例:例外リスト名", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "更新", - "xpack.securitySolution.exceptions.valueDescription": "値", + "xpack.securitySolution.exceptions.showCommentsLabel": "({comments}){comments, plural, other {件のコメント}}を表示", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "新しいコメントを追加...", - "xpack.securitySolution.exceptions.viewer.addExceptionLabel": "新しい例外を追加", "xpack.securitySolution.exceptions.viewer.addToClipboard": "コメント", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "ルール例外の追加", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "エンドポイント例外の追加", - "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "例外の削除エラー", - "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "例外を追加してルールを微調整し、例外条件が満たされたときに検出アラートが作成されないようにすることができます。例外により検出の精度が改善します。これにより、誤検出数が減ります。", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "このルールには例外がありません", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "ルール設定", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "ルール設定", - "xpack.securitySolution.exceptions.viewer.fetchingListError": "例外の取得エラー", - "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "例外項目合計数の取得エラー", - "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "検索結果が見つかりません。", - "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "検索フィールド(例:host.name)", "xpack.securitySolution.exitFullScreenButton": "全画面を終了", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "上位の値を非表示", "xpack.securitySolution.expandedValue.links.expandIpDetails": "IP詳細を展開", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 722114403f28e..120a031c7c912 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25457,17 +25457,12 @@ "xpack.securitySolution.eventsViewer.unit": "{totalCount, plural, other {个事件}}", "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外列表 ({id}) 已成功移除", "xpack.securitySolution.exceptions.exceptionItem.showCommentsLabel": "显示{comments, plural, other {注释}} ({comments})", - "xpack.securitySolution.exceptions.exceptionsPaginationLabel": "每页项数:{items}", "xpack.securitySolution.exceptions.failedLoadPolicies": "加载策略时出错:“{error}”", "xpack.securitySolution.exceptions.fetch404Error": "关联的例外列表 ({listId}) 已不存在。请移除缺少的例外列表,以将其他例外添加到检测规则。", "xpack.securitySolution.exceptions.hideCommentsLabel": "隐藏 ({comments}) 个{comments, plural, other {注释}}", - "xpack.securitySolution.exceptions.paginationNumberOfItemsLabel": "{items} 项", "xpack.securitySolution.exceptions.referenceModalDescription": "此例外列表与 ({referenceCount}) 个{referenceCount, plural, other {规则}}关联。移除此例外列表还将会删除其对关联规则的引用。", "xpack.securitySolution.exceptions.referenceModalSuccessDescription": "例外列表 - {listId} - 已成功删除。", "xpack.securitySolution.exceptions.showCommentsLabel": "显示 ({comments} 个) {comments, plural, other {注释}}", - "xpack.securitySolution.exceptions.utilityNumberExceptionsLabel": "正在显示 {items} 个{items, plural, other {例外}}", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription": "此规则的所有例外将应用到检测规则,而非终端。查看{ruleSettings}以了解详情。", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription": "此规则的所有例外将应用到终端和检测规则。查看{ruleSettings}以了解详情。", "xpack.securitySolution.fieldBrowser.descriptionForScreenReaderOnly": "{field} 字段的描述:", "xpack.securitySolution.footer.autoRefreshActiveTooltip": "自动刷新已启用时,时间线将显示匹配查询的最近 {numberOfItems} 个事件。", "xpack.securitySolution.formattedNumber.countsLabel": "{mantissa}{scale}{hasRemainder}", @@ -28034,16 +28029,12 @@ "xpack.securitySolution.exceptions.addException.operatingSystemPlaceHolder": "选择操作系统", "xpack.securitySolution.exceptions.addException.sequenceWarning": "此规则的查询包含 EQL 序列语句。创建的例外将应用于序列中的所有事件。", "xpack.securitySolution.exceptions.addException.success": "已成功添加例外", - "xpack.securitySolution.exceptions.andDescription": "且", "xpack.securitySolution.exceptions.badge.readOnly.tooltip": "无法创建、编辑或删除例外", "xpack.securitySolution.exceptions.cancelLabel": "取消", "xpack.securitySolution.exceptions.clearExceptionsLabel": "移除例外列表", "xpack.securitySolution.exceptions.commentEventLabel": "已添加注释", - "xpack.securitySolution.exceptions.commentLabel": "注释", - "xpack.securitySolution.exceptions.descriptionLabel": "描述", - "xpack.securitySolution.exceptions.detectionListLabel": "检测列表", "xpack.securitySolution.exceptions.dissasociateExceptionListError": "无法移除例外列表", - "xpack.securitySolution.exceptions.editButtonLabel": "编辑", + "xpack.securitySolution.exceptions.dissasociateListSuccessText": "例外列表 ({id}) 已成功移除", "xpack.securitySolution.exceptions.editException.bulkCloseLabel": "关闭所有与此例外匹配且根据此规则生成的告警", "xpack.securitySolution.exceptions.editException.bulkCloseLabel.disabled": "关闭所有与此例外匹配且根据此规则生成的告警(不支持列表和非 ECS 字段)", "xpack.securitySolution.exceptions.editException.cancel": "取消", @@ -28056,8 +28047,11 @@ "xpack.securitySolution.exceptions.editException.success": "已成功更新例外", "xpack.securitySolution.exceptions.editException.versionConflictDescription": "此例外可能自您首次选择编辑后已更新。尝试单击“取消”,重新编辑该例外。", "xpack.securitySolution.exceptions.editException.versionConflictTitle": "抱歉,有错误", - "xpack.securitySolution.exceptions.endpointListLabel": "终端列表", "xpack.securitySolution.exceptions.errorLabel": "错误", + "xpack.securitySolution.exceptions.failedLoadPolicies": "加载策略时出错:“{error}”", + "xpack.securitySolution.exceptions.fetch404Error": "关联的例外列表 ({listId}) 已不存在。请移除缺少的例外列表,以将其他例外添加到检测规则。", + "xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错", + "xpack.securitySolution.exceptions.hideCommentsLabel": "隐藏 ({comments}) 个{comments, plural, other {注释}}", "xpack.securitySolution.exceptions.exceptionItem.conditions.and": "且", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator": "存在", "xpack.securitySolution.exceptions.exceptionItem.conditions.existsOperator.not": "不存在", @@ -28080,37 +28074,21 @@ "xpack.securitySolution.exceptions.exceptionItem.metaDetailsBy": "依据", "xpack.securitySolution.exceptions.exceptionItem.updatedLabel": "已更新", "xpack.securitySolution.exceptions.fetchError": "提取例外列表时出错", - "xpack.securitySolution.exceptions.fieldDescription": "字段", "xpack.securitySolution.exceptions.modalErrorAccordionText": "显示规则引用信息:", - "xpack.securitySolution.exceptions.nameLabel": "名称", "xpack.securitySolution.exceptions.operatingSystemFullLabel": "操作系统", "xpack.securitySolution.exceptions.operatingSystemLinux": "Linux", "xpack.securitySolution.exceptions.operatingSystemMac": "macOS", "xpack.securitySolution.exceptions.operatingSystemWindows": "Windows", "xpack.securitySolution.exceptions.operatingSystemWindowsAndMac": "Windows 和 macOS", - "xpack.securitySolution.exceptions.operatorDescription": "运算符", - "xpack.securitySolution.exceptions.orDescription": "OR", "xpack.securitySolution.exceptions.referenceModalCancelButton": "取消", "xpack.securitySolution.exceptions.referenceModalDeleteButton": "移除例外列表", "xpack.securitySolution.exceptions.referenceModalTitle": "移除例外列表", - "xpack.securitySolution.exceptions.removeButtonLabel": "移除", "xpack.securitySolution.exceptions.searchPlaceholder": "例如,示例列表名称", - "xpack.securitySolution.exceptions.utilityRefreshLabel": "刷新", - "xpack.securitySolution.exceptions.valueDescription": "值", + "xpack.securitySolution.exceptions.showCommentsLabel": "显示 ({comments} 个) {comments, plural, other {注释}}", "xpack.securitySolution.exceptions.viewer.addCommentPlaceholder": "添加新注释......", - "xpack.securitySolution.exceptions.viewer.addExceptionLabel": "添加新例外", "xpack.securitySolution.exceptions.viewer.addToClipboard": "注释", "xpack.securitySolution.exceptions.viewer.addToDetectionsListLabel": "添加规则例外", "xpack.securitySolution.exceptions.viewer.addToEndpointListLabel": "添加终端例外", - "xpack.securitySolution.exceptions.viewer.deleteExceptionError": "删除例外时出错", - "xpack.securitySolution.exceptions.viewer.emptyPromptBody": "可以添加例外以微调规则,以便在满足例外条件时不创建检测告警。例外提升检测精确性,从而可以减少误报数。", - "xpack.securitySolution.exceptions.viewer.emptyPromptTitle": "此规则没有例外", - "xpack.securitySolution.exceptions.viewer.exceptionDetectionDetailsDescription.ruleSettingsLink": "规则设置", - "xpack.securitySolution.exceptions.viewer.exceptionEndpointDetailsDescription.ruleSettingsLink": "规则设置", - "xpack.securitySolution.exceptions.viewer.fetchingListError": "提取例外时出错", - "xpack.securitySolution.exceptions.viewer.fetchTotalsError": "获取例外项总数时出错", - "xpack.securitySolution.exceptions.viewer.noSearchResultsPromptBody": "找不到搜索结果。", - "xpack.securitySolution.exceptions.viewer.searchDefaultPlaceholder": "搜索字段(例如:host.name)", "xpack.securitySolution.exitFullScreenButton": "退出全屏", "xpack.securitySolution.expandedValue.hideTopValues.HideTopValues": "隐藏排名最前值", "xpack.securitySolution.expandedValue.links.expandIpDetails": "展开 IP 详情", diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts new file mode 100644 index 0000000000000..e75a35d88acc3 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rule_exception_references.ts @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import { DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + CreateExceptionListSchema, + ExceptionListTypeEnum, +} from '@kbn/securitysolution-io-ts-list-types'; +import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { + createRule, + getSimpleRule, + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + createExceptionList, +} from '../../utils'; +import { deleteAllExceptions } from '../../../lists_api_integration/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const log = getService('log'); + + describe('find_rule_exception_references', () => { + before(async () => { + await createSignalsIndex(supertest, log); + }); + + after(async () => { + await deleteSignalsIndex(supertest, log); + await deleteAllAlerts(supertest, log); + }); + + afterEach(async () => { + await deleteAllExceptions(supertest, log); + }); + + it('returns empty array per list_id if no references are found', async () => { + // create exception list + const newExceptionList: CreateExceptionListSchema = { + ...getCreateExceptionListMinimalSchemaMock(), + list_id: 'i_exist', + namespace_type: 'single', + type: ExceptionListTypeEnum.DETECTION, + }; + const exceptionList = await createExceptionList(supertest, log, newExceptionList); + + // create rule + await createRule(supertest, log, getSimpleRule('rule-1')); + + const { body: references } = await supertest + .get(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL) + .set('kbn-xsrf', 'true') + .query({ + ids: `${exceptionList.id}`, + list_ids: `${exceptionList.list_id}`, + namespace_types: `${exceptionList.namespace_type}`, + }) + .expect(200); + + expect(references).to.eql({ references: [{ i_exist: [] }] }); + }); + + it('returns empty array per list_id if list does not exist', async () => { + // create rule + await createRule(supertest, log, getSimpleRule('rule-1')); + + const { body: references } = await supertest + .get(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL) + .set('kbn-xsrf', 'true') + .query({ + ids: `1234`, + list_ids: `i_dont_exist`, + namespace_types: `single`, + }) + .expect(200); + + expect(references).to.eql({ references: [{ i_dont_exist: [] }] }); + }); + + it('returns found references', async () => { + // create exception list + const newExceptionList: CreateExceptionListSchema = { + ...getCreateExceptionListMinimalSchemaMock(), + list_id: 'i_exist', + namespace_type: 'single', + type: ExceptionListTypeEnum.DETECTION, + }; + const exceptionList = await createExceptionList(supertest, log, newExceptionList); + const exceptionList2 = await createExceptionList(supertest, log, { + ...newExceptionList, + list_id: 'i_exist_2', + }); + + // create rule + const rule = await createRule(supertest, log, { + ...getSimpleRule('rule-2'), + exceptions_list: [ + { + id: `${exceptionList.id}`, + list_id: `${exceptionList.list_id}`, + namespace_type: `${exceptionList.namespace_type}`, + type: `${exceptionList.type}`, + }, + { + id: `${exceptionList2.id}`, + list_id: `${exceptionList2.list_id}`, + namespace_type: `${exceptionList2.namespace_type}`, + type: `${exceptionList2.type}`, + }, + ], + }); + + const { body: references } = await supertest + .get(DETECTION_ENGINE_RULES_EXCEPTIONS_REFERENCE_URL) + .set('kbn-xsrf', 'true') + .query({ + ids: `${exceptionList.id},${exceptionList2.id}`, + list_ids: `${exceptionList.list_id},${exceptionList2.list_id}`, + namespace_types: `${exceptionList.namespace_type},${exceptionList2.namespace_type}`, + }) + .expect(200); + + expect(references).to.eql({ + references: [ + { + i_exist: [ + { + exception_lists: [ + { + id: references.references[0].i_exist[0].exception_lists[0].id, + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: references.references[0].i_exist[0].exception_lists[1].id, + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: rule.id, + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ], + }, + { + i_exist_2: [ + { + exception_lists: [ + { + id: references.references[1].i_exist_2[0].exception_lists[0].id, + list_id: 'i_exist', + namespace_type: 'single', + type: 'detection', + }, + { + id: references.references[1].i_exist_2[0].exception_lists[1].id, + list_id: 'i_exist_2', + namespace_type: 'single', + type: 'detection', + }, + ], + id: rule.id, + name: 'Simple Rule Query', + rule_id: 'rule-2', + }, + ], + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts index 283456df6b1f0..a857757f2d864 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/index.ts @@ -31,6 +31,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./find_rules')); + loadTestFile(require.resolve('./find_rule_exception_references')); loadTestFile(require.resolve('./generating_signals')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); loadTestFile(require.resolve('./get_rule_execution_results')); diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts index bb65b1c9c4933..909930d713473 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/find_exception_list_items.ts @@ -9,8 +9,14 @@ import expect from '@kbn/expect'; import { EXCEPTION_LIST_URL, EXCEPTION_LIST_ITEM_URL } from '@kbn/securitysolution-list-constants'; import { getExceptionListItemResponseMockWithoutAutoGeneratedValues } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; -import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; -import { getCreateExceptionListMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { + getCreateExceptionListItemMinimalSchemaMock, + getCreateExceptionListItemMinimalSchemaMockWithoutId, +} from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; +import { + getCreateExceptionListMinimalSchemaMock, + getCreateExceptionListDetectionSchemaMock, +} from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { deleteAllExceptions, removeExceptionListItemServerGeneratedProperties } from '../../utils'; @@ -51,6 +57,79 @@ export default ({ getService }: FtrProviderContext): void => { }); }); + it('should return matching items when search is passed in', async () => { + // create exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + // create exception list items + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMockWithoutId(), + list_id: getCreateExceptionListDetectionSchemaMock().list_id, + item_id: '1', + entries: [ + { field: 'host.name', value: 'some host', operator: 'included', type: 'match' }, + ], + }) + .expect(200); + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMockWithoutId(), + item_id: '2', + list_id: getCreateExceptionListDetectionSchemaMock().list_id, + entries: [{ field: 'foo', operator: 'included', type: 'exists' }], + }) + .expect(200); + + const { body } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${ + getCreateExceptionListMinimalSchemaMock().list_id + }&namespace_type=single&page=1&per_page=25&search=host&sort_field=exception-list.created_at&sort_order=desc` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + body.data = [removeExceptionListItemServerGeneratedProperties(body.data[0])]; + expect(body).to.eql({ + data: [ + { + comments: [], + created_by: 'elastic', + description: 'some description', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match', + value: 'some host', + }, + ], + item_id: '1', + list_id: 'some-list-id', + name: 'some name', + namespace_type: 'single', + os_types: ['windows'], + tags: [], + type: 'simple', + updated_by: 'elastic', + }, + ], + page: 1, + per_page: 25, + total: 1, + }); + }); + it('should return 404 if given a list_id that does not exist', async () => { const { body } = await supertest .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=non_exist`) diff --git a/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json b/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json index 3ad636b20a9cc..72dc01b9bac54 100644 --- a/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/exceptions_2/data.json @@ -4,7 +4,7 @@ "id": "_aZE5nwBOpWiDweSth_E", "index": "exceptions-0002", "source": { - "@timestamp": "2019-09-02T00:41:06.527Z", + "@timestamp": "2019-09-02T00:45:06.527Z", "agent": { "name": "foo" }, @@ -23,4 +23,31 @@ ] } } -} \ No newline at end of file +} + +{ + "type": "doc", + "value": { + "id": "_aZE5nwBOpWiDweSth_F", + "index": "exceptions-0002", + "source": { + "@timestamp": "2019-09-02T00:46:06.527Z", + "agent": { + "name": "bar" + }, + "unique_value": { + "test": "test field 2" + }, + "user" : [ + { + "name" : "foo", + "id" : "123" + }, + { + "name" : "bar", + "id" : "456" + } + ] + } + } +}