From 434d29795da8180a5e0734a2c1a4eecf84b7fcb3 Mon Sep 17 00:00:00 2001 From: John Dorlus Date: Thu, 10 Dec 2020 19:05:49 -0500 Subject: [PATCH 01/37] Skipped verifying license management tab on cloud in upgrade assistant and license management. (#85650) * Added a check for the assertion that checks the number of tabs in the side nav. The License Management tab should not be on cloud since the app is disabled. * add semicolon. * We opted to skip the test on cloud. * Removed unused var declaration. --- .../license_management_security.ts | 17 ++++++++++------- .../upgrade_assistant_security.ts | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts b/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts index 59fc287c6cf2e..810c7c60f3836 100644 --- a/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts +++ b/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts @@ -55,13 +55,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(links.map((link) => link.text)).to.contain('Stack Management'); }); - it('should render the "Stack" section with License Management', async () => { - await PageObjects.common.navigateToApp('management'); - const sections = await managementMenu.getSections(); - expect(sections).to.have.length(3); - expect(sections[2]).to.eql({ - sectionId: 'stack', - sectionLinks: ['license_management', 'upgrade_assistant'], + describe('[SkipCloud] global dashboard with license management user: skip cloud', function () { + this.tags('skipCloud'); + it('should render the "Stack" section with License Management', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[2]).to.eql({ + sectionId: 'stack', + sectionLinks: ['license_management', 'upgrade_assistant'], + }); }); }); }); diff --git a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts index 1f541dbe03537..327e38bc66f05 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts @@ -13,7 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('security', () => { + describe('security', function () { before(async () => { await esArchiver.load('empty_kibana'); await PageObjects.common.navigateToApp('home'); @@ -58,13 +58,16 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(links.map((link) => link.text)).to.contain('Stack Management'); }); - it('should render the "Stack" section with Upgrde Assistant', async () => { - await PageObjects.common.navigateToApp('management'); - const sections = await managementMenu.getSections(); - expect(sections).to.have.length(3); - expect(sections[2]).to.eql({ - sectionId: 'stack', - sectionLinks: ['license_management', 'upgrade_assistant'], + describe('[SkipCloud] global dashboard all with global_upgrade_assistant_role', function () { + this.tags('skipCloud'); + it('should render the "Stack" section with Upgrde Assistant', async function () { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[2]).to.eql({ + sectionId: 'stack', + sectionLinks: ['license_management', 'upgrade_assistant'], + }); }); }); }); From 2ea0816e57f12d11381d6f4ff355584c6ebfa093 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Thu, 10 Dec 2020 16:41:48 -0800 Subject: [PATCH 02/37] [APM] add sanitize_field_names & transaction_ignore_urls vars to Node.js agent remote config (#85655) --- .../setting_definitions/general_settings.ts | 4 ++-- .../agent_configuration/setting_definitions/index.test.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index d4c36a3542bed..f243bcc0c694e 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -235,7 +235,7 @@ export const generalSettings: RawSettingDefinition[] = [ 'Sometimes it is necessary to sanitize, i.e., remove, sensitive data sent to Elastic APM. This config accepts a list of wildcard patterns of field names which should be sanitized. These apply to HTTP headers (including cookies) and `application/x-www-form-urlencoded` data (POST form fields). The query string and the captured request body (such as `application/json` data) will not get sanitized.', } ), - includeAgents: ['java', 'python', 'go', 'dotnet'], + includeAgents: ['java', 'python', 'go', 'dotnet', 'nodejs'], }, // Ignore transactions based on URLs @@ -254,6 +254,6 @@ export const generalSettings: RawSettingDefinition[] = [ 'Used to restrict requests to certain URLs from being instrumented. This config accepts a comma-separated list of wildcard patterns of URL paths that should be ignored. When an incoming HTTP request is detected, its request path will be tested against each element in this list. For example, adding `/home/index` to this list would match and remove instrumentation from `http://localhost/home/index` as well as `http://whatever.com/home/index?value1=123`', } ), - includeAgents: ['java'], + includeAgents: ['java', 'nodejs'], }, ]; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index d098608e76d55..ac0820309e77c 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -102,6 +102,8 @@ describe('filterByAgent', () => { expect(getSettingKeysForAgent('nodejs')).toEqual([ 'capture_body', 'log_level', + 'sanitize_field_names', + 'transaction_ignore_urls', 'transaction_max_spans', 'transaction_sample_rate', ]); From 28738e6b4b7962abc00aac326ebf5027df226b04 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Thu, 10 Dec 2020 18:07:47 -0700 Subject: [PATCH 03/37] [Security Solution] Fixes CIDR, float, long, integer, array, and text based issues when using value lists in exceptions (#85191) ## Summary Fixes different bugs/issues when using exceptions with value based lists for both the UI, the backend, and the large value based lists. See https://github.com/elastic/kibana/issues/79516, but this also fixes several other bugs found mentioned below. For the front end UI: * Adds the ability to specify value based lists that are IP Ranges when the source event is an IP. Before you could only match IP to IP and the IP Ranges lists could not be used. * Breaks down a few functions into smaller functions for unit test writing abilities. You can now add ip ranges as list values for the UI when before it would not show up: Screen Shot 2020-12-07 at 2 15 39 PM For value based lists: * Fixes text data type to use "and" between matching using `operator: 'and'` and changes it from a `terms query to a `match` query * Adds new API for searching against types called `searchListItemByValues ` so that numeric, text, array based, and other non-stringable types can be sent and then the value based lists will push that to ElasticSearch. This shifts as many corner cases and string/numeric coercions to ElasticSearch rather than Kibana client side code. * Adds ability to handle arrays within arrays through a `flatten` call. * Utilizes the `named queries` from ElasticSearch for the new API so that clients can get which parts matched and then use that for their exception list logic rather than in-memory string to string checks. This fixes CIDR and ranges as well as works with arrays. For the backend exception lists that used value based lists: * Broke down the `filterEventsAgainstList` function into a folder called `filters` and the functions into other files for better unit based testing. * Changed the calls from `getListItemByValues` to `searchListItemByValues` which can return exactly what it matched against and this will not break anyone using the existing REST API for `getListItemByValues` since that REST API and client side API stays the same. * Cleaned up extra promises being used in a few spots that async/await automatically will create. * Removed the stringabilities and stringify in favor of just a simpler exact check using `JSON.stringify()` For the tests: * Adds unit tests to broken down functions * Adds ip_array, keyword_array, text_array, FTR tests for the backend. * Adds more CIDR and range based FTR tests for the backend. * Unskips and fixes all the numeric tests and range tests that could not operate previously from bugs. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) --- .../search_es_list_item_schema.mock.ts | 1 + .../lists/common/schemas/response/index.ts | 1 + .../response/search_list_item_schema.mock.ts | 15 + .../response/search_list_item_schema.test.ts | 48 + .../response/search_list_item_schema.ts | 26 + .../items/delete_list_item_by_value.test.ts | 10 +- .../lists/server/services/items/index.ts | 2 + .../items/search_list_item_by_values.mock.ts | 17 + .../items/search_list_item_by_values.test.ts | 96 ++ .../items/search_list_item_by_values.ts | 40 + .../server/services/lists/list_client.ts | 19 + .../services/lists/list_client_types.ts | 6 + .../get_query_filter_from_type_value.test.ts | 870 ++++++++++++++++-- .../utils/get_query_filter_from_type_value.ts | 159 +++- .../lists/server/services/utils/index.ts | 1 + ..._elastic_named_search_to_list_item.test.ts | 150 +++ ...sform_elastic_named_search_to_list_item.ts | 50 + .../transform_elastic_to_list_item.test.ts | 76 +- .../utils/transform_elastic_to_list_item.ts | 14 +- .../autocomplete/field_value_lists.tsx | 17 +- .../components/autocomplete/helpers.test.ts | 125 ++- .../common/components/autocomplete/helpers.ts | 34 + .../signals/filter_events_with_list.ts | 216 ----- .../create_field_and_set_tuples.test.ts | 287 ++++++ .../filters/create_field_and_set_tuples.ts | 39 + .../create_set_to_filter_against.test.ts | 106 +++ .../filters/create_set_to_filter_against.ts | 63 ++ .../signals/filters/filter_events.test.ts | 105 +++ .../signals/filters/filter_events.ts | 39 + .../filter_events_against_list.test.ts} | 147 +-- .../filters/filter_events_against_list.ts | 93 ++ .../detection_engine/signals/filters/types.ts | 49 + .../signals/rule_messages.mock.ts | 14 + .../signals/search_after_bulk_create.test.ts | 37 +- .../signals/search_after_bulk_create.ts | 2 +- .../signals/signal_rule_alert_type.ts | 2 +- .../exception_operators_data_types/double.ts | 38 +- .../exception_operators_data_types/float.ts | 32 +- .../exception_operators_data_types/index.ts | 3 + .../exception_operators_data_types/integer.ts | 32 +- .../exception_operators_data_types/ip.ts | 109 ++- .../ip_array.ts | 735 +++++++++++++++ .../exception_operators_data_types/keyword.ts | 33 + .../keyword_array.ts | 624 +++++++++++++ .../exception_operators_data_types/long.ts | 32 +- .../exception_operators_data_types/text.ts | 74 +- .../text_array.ts | 619 +++++++++++++ .../rule_exceptions/ip_as_array/data.json | 51 + .../rule_exceptions/ip_as_array/mappings.json | 20 + .../keyword_as_array/data.json | 51 + .../keyword_as_array/mappings.json | 20 + .../rule_exceptions/text_as_array/data.json | 51 + .../text_as_array/mappings.json | 20 + x-pack/test/lists_api_integration/utils.ts | 7 +- 54 files changed, 5022 insertions(+), 505 deletions(-) create mode 100644 x-pack/plugins/lists/common/schemas/response/search_list_item_schema.mock.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/search_list_item_schema.test.ts create mode 100644 x-pack/plugins/lists/common/schemas/response/search_list_item_schema.ts create mode 100644 x-pack/plugins/lists/server/services/items/search_list_item_by_values.mock.ts create mode 100644 x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts create mode 100644 x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts create mode 100644 x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.test.ts create mode 100644 x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts rename x-pack/plugins/security_solution/server/lib/detection_engine/signals/{filter_events_with_list.test.ts => filters/filter_events_against_list.test.ts} (77%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.mock.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts create mode 100644 x-pack/test/functional/es_archives/rule_exceptions/ip_as_array/data.json create mode 100644 x-pack/test/functional/es_archives/rule_exceptions/ip_as_array/mappings.json create mode 100644 x-pack/test/functional/es_archives/rule_exceptions/keyword_as_array/data.json create mode 100644 x-pack/test/functional/es_archives/rule_exceptions/keyword_as_array/mappings.json create mode 100644 x-pack/test/functional/es_archives/rule_exceptions/text_as_array/data.json create mode 100644 x-pack/test/functional/es_archives/rule_exceptions/text_as_array/mappings.json diff --git a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts index c8017c9c1279a..c3097d0d9e37c 100644 --- a/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts +++ b/x-pack/plugins/lists/common/schemas/elastic_response/search_es_list_item_schema.mock.ts @@ -70,6 +70,7 @@ export const getSearchListItemMock = (): SearchResponse _score: 0, _source: getSearchEsListItemMock(), _type: '', + matched_queries: ['0.0'], }, ], max_score: 0, diff --git a/x-pack/plugins/lists/common/schemas/response/index.ts b/x-pack/plugins/lists/common/schemas/response/index.ts index deca06ad99fea..5e739ccf3a0a0 100644 --- a/x-pack/plugins/lists/common/schemas/response/index.ts +++ b/x-pack/plugins/lists/common/schemas/response/index.ts @@ -15,3 +15,4 @@ export * from './found_list_schema'; export * from './list_item_schema'; export * from './list_schema'; export * from './list_item_index_exist_schema'; +export * from './search_list_item_schema'; diff --git a/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.mock.ts b/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.mock.ts new file mode 100644 index 0000000000000..1ad241ffca077 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchListItemSchema } from '../../../common/schemas'; +import { VALUE } from '../../../common/constants.mock'; + +import { getListItemResponseMock } from './list_item_schema.mock'; + +export const getSearchListItemResponseMock = (): SearchListItemSchema => ({ + items: [getListItemResponseMock()], + value: VALUE, +}); diff --git a/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.test.ts b/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.test.ts new file mode 100644 index 0000000000000..132c3f16688f0 --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { left } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { exactCheck, foldLeftRight, getPaths } from '../../shared_imports'; + +import { getSearchListItemResponseMock } from './search_list_item_schema.mock'; +import { SearchListItemSchema, searchListItemSchema } from './search_list_item_schema'; + +describe('search_list_item_schema', () => { + test('it should validate a typical search list item response', () => { + const payload = getSearchListItemResponseMock(); + const decoded = searchListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([]); + expect(message.schema).toEqual(payload); + }); + + test('it should NOT validate with an "undefined" for "items"', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { items, ...noItems } = getSearchListItemResponseMock(); + const decoded = searchListItemSchema.decode(noItems); + const checked = exactCheck(noItems, decoded); + const message = pipe(checked, foldLeftRight); + + expect(getPaths(left(message.errors))).toEqual([ + 'Invalid value "undefined" supplied to "items"', + ]); + expect(message.schema).toEqual({}); + }); + + test('it should not allow an extra key to be sent in', () => { + const payload: SearchListItemSchema & { extraKey?: string } = getSearchListItemResponseMock(); + payload.extraKey = 'some new value'; + const decoded = searchListItemSchema.decode(payload); + const checked = exactCheck(payload, decoded); + const message = pipe(checked, foldLeftRight); + expect(getPaths(left(message.errors))).toEqual(['invalid keys "extraKey"']); + expect(message.schema).toEqual({}); + }); +}); diff --git a/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.ts b/x-pack/plugins/lists/common/schemas/response/search_list_item_schema.ts new file mode 100644 index 0000000000000..5177098a6f67f --- /dev/null +++ b/x-pack/plugins/lists/common/schemas/response/search_list_item_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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { listItemArraySchema } from './list_item_schema'; + +/** + * NOTE: Although this is defined within "response" this does not expose a REST API + * endpoint right now for this particular response. Instead this is only used internally + * for the plugins at this moment. If this changes, please remove this message. + */ +export const searchListItemSchema = t.exact( + t.type({ + items: listItemArraySchema, + value: t.unknown, + }) +); + +export type SearchListItemSchema = t.TypeOf; + +export const searchListItemArraySchema = t.array(searchListItemSchema); +export type SearchListItemArraySchema = t.TypeOf; diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts index f658a51730d97..1120f99bf917a 100644 --- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts +++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts @@ -47,7 +47,15 @@ describe('delete_list_item_by_value', () => { body: { query: { bool: { - filter: [{ term: { list_id: 'some-list-id' } }, { terms: { ip: ['127.0.0.1'] } }], + filter: [ + { term: { list_id: 'some-list-id' } }, + { + bool: { + minimum_should_match: 1, + should: [{ term: { ip: { _name: '0.0', value: '127.0.0.1' } } }], + }, + }, + ], }, }, }, diff --git a/x-pack/plugins/lists/server/services/items/index.ts b/x-pack/plugins/lists/server/services/items/index.ts index bc04ba88b943e..31003771679a9 100644 --- a/x-pack/plugins/lists/server/services/items/index.ts +++ b/x-pack/plugins/lists/server/services/items/index.ts @@ -11,6 +11,7 @@ export * from './delete_list_item_by_value'; export * from './delete_list_item'; export * from './find_list_item'; export * from './get_list_item_by_value'; +export * from './get_list_item_by_values'; export * from './get_list_item'; export * from './get_list_item_by_values'; export * from './get_list_item_template'; @@ -18,3 +19,4 @@ export * from './get_list_item_index'; export * from './update_list_item'; export * from './write_lines_to_bulk_list_items'; export * from './write_list_items_to_stream'; +export * from './search_list_item_by_values'; diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.mock.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.mock.ts new file mode 100644 index 0000000000000..40b5fbb3ab8fa --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.mock.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { SearchListItemByValuesOptions } from '../items'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from '../../../common/constants.mock'; + +export const searchListItemByValuesOptionsMocks = (): SearchListItemByValuesOptions => ({ + callCluster: getCallClusterMock(), + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [VALUE, VALUE_2], +}); diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts new file mode 100644 index 0000000000000..b2a89dfe321ad --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.test.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchListItemArraySchema } from '../../../common/schemas'; +import { getSearchListItemMock } from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; +import { getCallClusterMock } from '../../../common/get_call_cluster.mock'; +import { LIST_ID, LIST_ITEM_INDEX, TYPE, VALUE, VALUE_2 } from '../../../common/constants.mock'; + +import { searchListItemByValues } from './search_list_item_by_values'; + +describe('search_list_item_by_values', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('Returns a an empty array of items if the value is empty', async () => { + const data = getSearchListItemMock(); + data.hits.hits = []; + const callCluster = getCallClusterMock(data); + const listItem = await searchListItemByValues({ + callCluster, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [], + }); + + expect(listItem).toEqual([]); + }); + + test('Returns a an empty array of items if the ES query is also empty', async () => { + const data = getSearchListItemMock(); + data.hits.hits = []; + const callCluster = getCallClusterMock(data); + const listItem = await searchListItemByValues({ + callCluster, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [VALUE, VALUE_2], + }); + + const expected: SearchListItemArraySchema = [ + { items: [], value: VALUE }, + { items: [], value: VALUE_2 }, + ]; + expect(listItem).toEqual(expected); + }); + + test('Returns transformed list item if the data exists within ES', async () => { + const data = getSearchListItemMock(); + const callCluster = getCallClusterMock(data); + const listItem = await searchListItemByValues({ + callCluster, + listId: LIST_ID, + listItemIndex: LIST_ITEM_INDEX, + type: TYPE, + value: [VALUE, VALUE_2], + }); + + const expected: SearchListItemArraySchema = [ + { + items: [ + { + _version: undefined, + created_at: '2020-04-20T15:25:31.830Z', + created_by: 'some user', + deserializer: undefined, + id: 'some-list-item-id', + list_id: 'some-list-id', + meta: {}, + serializer: undefined, + tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', + type: 'ip', + updated_at: '2020-04-20T15:25:31.830Z', + updated_by: 'some user', + value: '127.0.0.1', + }, + ], + value: '127.0.0.1', + }, + { + items: [], + value: VALUE_2, + }, + ]; + expect(listItem).toEqual(expected); + }); +}); diff --git a/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts new file mode 100644 index 0000000000000..33025a6a177ff --- /dev/null +++ b/x-pack/plugins/lists/server/services/items/search_list_item_by_values.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { LegacyAPICaller } from 'kibana/server'; + +import { SearchEsListItemSchema, SearchListItemArraySchema, Type } from '../../../common/schemas'; +import { getQueryFilterFromTypeValue, transformElasticNamedSearchToListItem } from '../utils'; + +export interface SearchListItemByValuesOptions { + listId: string; + callCluster: LegacyAPICaller; + listItemIndex: string; + type: Type; + value: unknown[]; +} + +export const searchListItemByValues = async ({ + listId, + callCluster, + listItemIndex, + type, + value, +}: SearchListItemByValuesOptions): Promise => { + const response = await callCluster('search', { + body: { + query: { + bool: { + filter: getQueryFilterFromTypeValue({ listId, type, value }), + }, + }, + }, + ignoreUnavailable: true, + index: listItemIndex, + size: 10000, // TODO: This has a limit on the number which is 10,000 the default of Elastic but we might want to provide a way to increase that number + }); + return transformElasticNamedSearchToListItem({ response, type, value }); +}; diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts index 590bfef6625f5..b0640ac8d6ba9 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client.ts @@ -12,6 +12,7 @@ import { ListItemArraySchema, ListItemSchema, ListSchema, + SearchListItemArraySchema, } from '../../../common/schemas'; import { ConfigType } from '../../config'; import { @@ -35,6 +36,7 @@ import { getListItemIndex, getListItemTemplate, importListItemsToStream, + searchListItemByValues, updateListItem, } from '../../services/items'; import { @@ -67,6 +69,7 @@ import { GetListItemsByValueOptions, GetListOptions, ImportListItemsToStreamOptions, + SearchListItemByValuesOptions, UpdateListItemOptions, UpdateListOptions, } from './list_client_types'; @@ -472,6 +475,22 @@ export class ListClient { }); }; + public searchListItemByValues = async ({ + type, + listId, + value, + }: SearchListItemByValuesOptions): Promise => { + const { callCluster } = this; + const listItemIndex = this.getListItemIndex(); + return searchListItemByValues({ + callCluster, + listId, + listItemIndex, + type, + value, + }); + }; + public findList = async ({ filter, currentIndexPosition, diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts index ea983b38c7e5d..fd9066cfe2409 100644 --- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts +++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts @@ -160,3 +160,9 @@ export interface FindListItemOptions { sortField: SortFieldOrUndefined; sortOrder: SortOrderOrUndefined; } + +export interface SearchListItemByValuesOptions { + type: Type; + listId: string; + value: unknown[]; +} diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts index 3d48e44e26eaa..aec9ef629788c 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.test.ts @@ -4,7 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { QueryFilterType, getQueryFilterFromTypeValue } from './get_query_filter_from_type_value'; +import { + QueryFilterType, + getEmptyQuery, + getQueryFilterFromTypeValue, + getShouldQuery, + getTermsQuery, + getTextQuery, +} from './get_query_filter_from_type_value'; describe('get_query_filter_from_type_value', () => { beforeEach(() => { @@ -15,78 +22,813 @@ describe('get_query_filter_from_type_value', () => { jest.clearAllMocks(); }); - test('it returns an ip if given an ip', () => { - const queryFilter = getQueryFilterFromTypeValue({ - listId: 'list-123', - type: 'ip', - value: ['127.0.0.1'], - }); - const expected: QueryFilterType = [ - { term: { list_id: 'list-123' } }, - { terms: { ip: ['127.0.0.1'] } }, - ]; - expect(queryFilter).toEqual(expected); - }); + describe('getQueryFilterFromTypeValue', () => { + test('it returns an ip if given an ip', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ term: { ip: { _name: '0.0', value: '127.0.0.1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two ip if given two ip', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1', '127.0.0.2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { term: { ip: { _name: '0.0', value: '127.0.0.1' } } }, + { term: { ip: { _name: '1.0', value: '127.0.0.2' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); - test('it returns two ip if given two ip', () => { - const queryFilter = getQueryFilterFromTypeValue({ - listId: 'list-123', - type: 'ip', - value: ['127.0.0.1', '127.0.0.2'], - }); - const expected: QueryFilterType = [ - { term: { list_id: 'list-123' } }, - { terms: { ip: ['127.0.0.1', '127.0.0.2'] } }, - ]; - expect(queryFilter).toEqual(expected); + test('it returns a keyword if given a keyword', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: ['host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ term: { keyword: { _name: '0.0', value: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two keywords if given two values', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: ['host-name-1', 'host-name-2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { term: { keyword: { _name: '0.0', value: 'host-name-1' } } }, + { term: { keyword: { _name: '1.0', value: 'host-name-2' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns an empty query given an empty value', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'keyword', + value: [], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match_none: { + _name: 'empty', + }, + }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns an empty query object given an empty array', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: [], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match_none: { + _name: 'empty', + }, + }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns an empty query object given an array with only null values', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: [null, null], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match_none: { + _name: 'empty', + }, + }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out a null value if mixed with a string value for non-text based query', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: [null, 'host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ term: { ip: { _name: '1.0', value: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out an object value if mixed with a string value for non-text based query', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'ip', + value: [{}, 'host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ term: { ip: { _name: '1.0', value: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out a null value if mixed with a string value for text based query', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'text', + value: [null, 'host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ match: { text: { _name: '1.0', operator: 'and', query: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out object values if mixed with a string value for text based query', () => { + const query = getQueryFilterFromTypeValue({ + listId: 'list-123', + type: 'text', + value: [{}, 'host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ match: { text: { _name: '1.0', operator: 'and', query: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); }); - test('it returns a keyword if given a keyword', () => { - const queryFilter = getQueryFilterFromTypeValue({ - listId: 'list-123', - type: 'keyword', - value: ['host-name-1'], - }); - const expected: QueryFilterType = [ - { term: { list_id: 'list-123' } }, - { terms: { keyword: ['host-name-1'] } }, - ]; - expect(queryFilter).toEqual(expected); + describe('getEmptyQuery', () => { + test('it returns an empty query given a list_id', () => { + const emptyQuery = getEmptyQuery({ listId: 'list-123' }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { bool: { minimum_should_match: 1, should: [{ match_none: { _name: 'empty' } }] } }, + ]; + expect(emptyQuery).toEqual(expected); + }); }); - test('it returns two keywords if given two values', () => { - const queryFilter = getQueryFilterFromTypeValue({ - listId: 'list-123', - type: 'keyword', - value: ['host-name-1', 'host-name-2'], - }); - const expected: QueryFilterType = [ - { term: { list_id: 'list-123' } }, - { terms: { keyword: ['host-name-1', 'host-name-2'] } }, - ]; - expect(queryFilter).toEqual(expected); + describe('getTermsQuery', () => { + describe('scalar values', () => { + test('it returns a expected terms query give a single string value, listId, and type', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ term: { ip: { _name: '0.0', value: '127.0.0.1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two expected terms query given two string values, listId, and type', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1', '127.0.0.2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { term: { ip: { _name: '0.0', value: '127.0.0.1' } } }, + { term: { ip: { _name: '1.0', value: '127.0.0.2' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two expected numeric terms without converting them into strings', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [5, 3], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { term: { ip: { _name: '0.0', value: 5 } } }, + { term: { ip: { _name: '1.0', value: 3 } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns a string and a numeric without converting them into a homogenous type', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [5, '3'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { term: { ip: { _name: '0.0', value: 5 } } }, + { term: { ip: { _name: '1.0', value: '3' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out a null value if mixed with a string value', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [null, 'host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ term: { ip: { _name: '1.0', value: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out an object value if mixed with a string value', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [{}, 'host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ term: { ip: { _name: '1.0', value: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + }); + + describe('array values', () => { + test('it returns a expected terms query give a single string value, listId, and type', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [['127.0.0.1']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ terms: { _name: '0.0', ip: ['127.0.0.1'] } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two expected terms query given two string values, listId, and type', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [['127.0.0.1'], ['127.0.0.2']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { terms: { _name: '0.0', ip: ['127.0.0.1'] } }, + { terms: { _name: '1.0', ip: ['127.0.0.2'] } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two expected numeric terms without converting them into strings', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [[5], [3]], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ terms: { _name: '0.0', ip: [5] } }, { terms: { _name: '1.0', ip: [3] } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns a string and a numeric without converting them into a homogenous type', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [[5], ['3']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { terms: { _name: '0.0', ip: [5] } }, + { terms: { _name: '1.0', ip: ['3'] } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out a null value if mixed with a string value', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [[null], ['host-name-1']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ terms: { _name: '1.0', ip: ['host-name-1'] } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out an object value if mixed with a string value', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [[{}], ['host-name-1']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ terms: { _name: '1.0', ip: ['host-name-1'] } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it flattens and removes null values correctly in a deeply nested set of arrays', () => { + const query = getTermsQuery({ + listId: 'list-123', + type: 'ip', + value: [ + [null], + [ + 'host-name-1', + ['host-name-2', [null], ['host-name-3'], ['host-name-4', null, 'host-name-5']], + ], + ['host-name-6'], + ], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { + terms: { + _name: '1.0', + ip: ['host-name-1', 'host-name-2', 'host-name-3', 'host-name-4', 'host-name-5'], + }, + }, + { terms: { _name: '2.0', ip: ['host-name-6'] } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + }); }); - test('it returns an empty keyword given an empty value', () => { - const queryFilter = getQueryFilterFromTypeValue({ - listId: 'list-123', - type: 'keyword', - value: [], - }); - const expected: QueryFilterType = [ - { term: { list_id: 'list-123' } }, - { terms: { keyword: [] } }, - ]; - expect(queryFilter).toEqual(expected); + describe('getTextQuery', () => { + describe('scalar values', () => { + test('it returns a expected terms query give a single string value, listId, and type', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ match: { ip: { _name: '0.0', operator: 'and', query: '127.0.0.1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two expected terms query given two string values, listId, and type', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: ['127.0.0.1', '127.0.0.2'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match: { ip: { _name: '0.0', operator: 'and', query: '127.0.0.1' } } }, + { match: { ip: { _name: '1.0', operator: 'and', query: '127.0.0.2' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two expected numeric terms without converting them into strings', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [5, 3], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match: { ip: { _name: '0.0', operator: 'and', query: 5 } } }, + { match: { ip: { _name: '1.0', operator: 'and', query: 3 } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns a string and a numeric without converting them into a homogenous type', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [5, '3'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match: { ip: { _name: '0.0', operator: 'and', query: 5 } } }, + { match: { ip: { _name: '1.0', operator: 'and', query: '3' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out a null value if mixed with a string value', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [null, 'host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ match: { ip: { _name: '1.0', operator: 'and', query: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out an object value if mixed with a string value', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [{}, 'host-name-1'], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ match: { ip: { _name: '1.0', operator: 'and', query: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + }); + + describe('array values', () => { + test('it returns a expected terms query give a single string value, listId, and type', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [['127.0.0.1']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ match: { ip: { _name: '0.0', operator: 'and', query: '127.0.0.1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two expected terms query given two string values, listId, and type', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [['127.0.0.1'], ['127.0.0.2']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match: { ip: { _name: '0.0', operator: 'and', query: '127.0.0.1' } } }, + { match: { ip: { _name: '1.0', operator: 'and', query: '127.0.0.2' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns two expected numeric terms without converting them into strings', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [[5], [3]], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match: { ip: { _name: '0.0', operator: 'and', query: 5 } } }, + { match: { ip: { _name: '1.0', operator: 'and', query: 3 } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it returns a string and a numeric without converting them into a homogenous type', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [[5], ['3']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match: { ip: { _name: '0.0', operator: 'and', query: 5 } } }, + { match: { ip: { _name: '1.0', operator: 'and', query: '3' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out a null value if mixed with a string value', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [[null], ['host-name-1']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ match: { ip: { _name: '1.0', operator: 'and', query: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it filters out a object value if mixed with a string value', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [[{}], ['host-name-1']], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [{ match: { ip: { _name: '1.0', operator: 'and', query: 'host-name-1' } } }], + }, + }, + ]; + expect(query).toEqual(expected); + }); + + test('it flattens and removes null values correctly in a deeply nested set of arrays', () => { + const query = getTextQuery({ + listId: 'list-123', + type: 'ip', + value: [ + [null], + [ + 'host-name-1', + ['host-name-2', [null], ['host-name-3'], ['host-name-4', null, 'host-name-5']], + ], + ['host-name-6'], + ], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { match: { ip: { _name: '1.0', operator: 'and', query: 'host-name-1' } } }, + { match: { ip: { _name: '1.1', operator: 'and', query: 'host-name-2' } } }, + { match: { ip: { _name: '1.2', operator: 'and', query: 'host-name-3' } } }, + { match: { ip: { _name: '1.3', operator: 'and', query: 'host-name-4' } } }, + { match: { ip: { _name: '1.4', operator: 'and', query: 'host-name-5' } } }, + { match: { ip: { _name: '2.0', operator: 'and', query: 'host-name-6' } } }, + ], + }, + }, + ]; + expect(query).toEqual(expected); + }); + }); }); - test('it returns an empty ip given an empty value', () => { - const queryFilter = getQueryFilterFromTypeValue({ - listId: 'list-123', - type: 'ip', - value: [], + describe('getShouldQuery', () => { + test('it returns a should as-is when passed one', () => { + const query = getShouldQuery({ + listId: 'list-123', + should: [ + { + bool: { + minimum_should_match: 1, + should: [{ terms: { _name: '0.0', ip: ['127.0.0.1'] } }], + }, + }, + ], + }); + const expected: QueryFilterType = [ + { term: { list_id: 'list-123' } }, + { + bool: { + minimum_should_match: 1, + should: [ + { + bool: { + minimum_should_match: 1, + should: [{ terms: { _name: '0.0', ip: ['127.0.0.1'] } }], + }, + }, + ], + }, + }, + ]; + expect(query).toEqual(expected); }); - const expected: QueryFilterType = [{ term: { list_id: 'list-123' } }, { terms: { ip: [] } }]; - expect(queryFilter).toEqual(expected); }); }); diff --git a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts index 3baba07aa9885..cf332cd6dd957 100644 --- a/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts +++ b/x-pack/plugins/lists/server/services/utils/get_query_filter_from_type_value.ts @@ -4,19 +4,170 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty, isObject } from 'lodash/fp'; + import { Type } from '../../../common/schemas'; export type QueryFilterType = [ - { term: Record }, - { terms: Record } + { term: Record }, + { terms: Record } | { bool: {} } ]; +/** + * Given a type, value, and listId, this will return a valid query. If the type is + * "text" it will return a "text" match, otherwise it returns a terms query. If an + * array or array of arrays is passed, this will flatten, remove any "null" values, + * and then the result. + * @param type The type of list + * @param value The unknown value + * @param listId The list id + */ export const getQueryFilterFromTypeValue = ({ type, value, listId, }: { type: Type; - value: string[]; + value: unknown[]; + listId: string; +}): QueryFilterType => { + const valueFlattened = value + .flat(Infinity) + .filter((singleValue) => singleValue != null && !isObject(singleValue)); + if (isEmpty(valueFlattened)) { + return getEmptyQuery({ listId }); + } else if (type === 'text') { + return getTextQuery({ listId, type, value }); + } else { + return getTermsQuery({ listId, type, value }); + } +}; + +/** + * Returns an empty named query that should not match anything + * @param listId The list id to associate with the empty query + */ +export const getEmptyQuery = ({ listId }: { listId: string }): QueryFilterType => [ + { term: { list_id: listId } }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match_none: { + _name: 'empty', + }, + }, + ], + }, + }, +]; + +/** + * Returns a terms query against a large value based list. If it detects that an array or item has a "null" + * value it will filter that value out. If it has arrays within arrays it will flatten those out as well. + * @param value The value which can be unknown + * @param type The list type type + * @param listId The list id + */ +export const getTermsQuery = ({ + value, + type, + listId, +}: { + value: unknown[]; + type: Type; + listId: string; +}): QueryFilterType => { + const should = value.reduce((accum, item, index) => { + if (Array.isArray(item)) { + const itemFlattened = item + .flat(Infinity) + .filter((singleValue) => singleValue != null && !isObject(singleValue)); + if (itemFlattened.length === 0) { + return accum; + } else { + return [...accum, { terms: { _name: `${index}.0`, [type]: itemFlattened } }]; + } + } else { + if (item == null || isObject(item)) { + return accum; + } else { + return [...accum, { term: { [type]: { _name: `${index}.0`, value: item } } }]; + } + } + }, []); + return getShouldQuery({ listId, should }); +}; + +/** + * Returns a text query against a large value based list. If it detects that an array or item has a "null" + * value it will filter that value out. If it has arrays within arrays it will flatten those out as well. + * @param value The value which can be unknown + * @param type The list type type + * @param listId The list id + */ +export const getTextQuery = ({ + value, + type, + listId, +}: { + value: unknown[]; + type: Type; + listId: string; +}): QueryFilterType => { + const should = value.reduce((accum, item, index) => { + if (Array.isArray(item)) { + const itemFlattened = item + .flat(Infinity) + .filter((singleValue) => singleValue != null && !isObject(singleValue)); + if (itemFlattened.length === 0) { + return accum; + } else { + return [ + ...accum, + ...itemFlattened.map((flatItem, secondIndex) => ({ + match: { + [type]: { _name: `${index}.${secondIndex}`, operator: 'and', query: flatItem }, + }, + })), + ]; + } + } else { + if (item == null || isObject(item)) { + return accum; + } else { + return [ + ...accum, + { match: { [type]: { _name: `${index}.0`, operator: 'and', query: item } } }, + ]; + } + } + }, []); + + return getShouldQuery({ listId, should }); +}; + +/** + * Given an unknown should this constructs a simple bool and terms with the should + * clause/query. + * @param listId The list id to query against + * @param should The unknown should to construct the query against + */ +export const getShouldQuery = ({ + listId, + should, +}: { listId: string; -}): QueryFilterType => [{ term: { list_id: listId } }, { terms: { [type]: value } }]; + should: unknown; +}): QueryFilterType => { + return [ + { term: { list_id: listId } }, + { + bool: { + minimum_should_match: 1, + should, + }, + }, + ]; +}; diff --git a/x-pack/plugins/lists/server/services/utils/index.ts b/x-pack/plugins/lists/server/services/utils/index.ts index f7ed118ea5857..57f37a1d6bfca 100644 --- a/x-pack/plugins/lists/server/services/utils/index.ts +++ b/x-pack/plugins/lists/server/services/utils/index.ts @@ -15,6 +15,7 @@ export * from './get_search_after_with_tie_breaker'; export * from './get_sort_with_tie_breaker'; export * from './get_source_with_tie_breaker'; export * from './scroll_to_start_page'; +export * from './transform_elastic_named_search_to_list_item'; export * from './transform_elastic_to_list'; export * from './transform_elastic_to_list_item'; export * from './transform_list_item_to_elastic_query'; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.test.ts new file mode 100644 index 0000000000000..83a486b5d1544 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getSearchListItemResponseMock } from '../../../common/schemas/response/search_list_item_schema.mock'; +import { LIST_INDEX, LIST_ITEM_ID, TYPE, VALUE } from '../../../common/constants.mock'; +import { + getSearchEsListItemMock, + getSearchListItemMock, +} from '../../../common/schemas/elastic_response/search_es_list_item_schema.mock'; +import { SearchListItemArraySchema } from '../../../common/schemas'; + +import { transformElasticNamedSearchToListItem } from './transform_elastic_named_search_to_list_item'; + +describe('transform_elastic_named_search_to_list_item', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('if given an empty array for values, it returns an empty array', () => { + const response = getSearchListItemMock(); + const queryFilter = transformElasticNamedSearchToListItem({ + response, + type: TYPE, + value: [], + }); + const expected: SearchListItemArraySchema = []; + expect(queryFilter).toEqual(expected); + }); + + test('if given an empty array for hits, it returns an empty match', () => { + const response = getSearchListItemMock(); + response.hits.hits = []; + const queryFilter = transformElasticNamedSearchToListItem({ + response, + type: TYPE, + value: [VALUE], + }); + const expected: SearchListItemArraySchema = [{ items: [], value: VALUE }]; + expect(queryFilter).toEqual(expected); + }); + + test('it transforms a single elastic type to a search list item type', () => { + const response = getSearchListItemMock(); + const queryFilter = transformElasticNamedSearchToListItem({ + response, + type: TYPE, + value: [VALUE], + }); + const expected: SearchListItemArraySchema = [getSearchListItemResponseMock()]; + expect(queryFilter).toEqual(expected); + }); + + test('it transforms two elastic types to a search list item type', () => { + const response = getSearchListItemMock(); + response.hits.hits = [ + ...response.hits.hits, + { + _id: LIST_ITEM_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListItemMock(), + _type: '', + matched_queries: ['1.0'], + }, + ]; + const queryFilter = transformElasticNamedSearchToListItem({ + response, + type: TYPE, + value: [VALUE, VALUE], + }); + const expected: SearchListItemArraySchema = [ + getSearchListItemResponseMock(), + getSearchListItemResponseMock(), + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it transforms only 1 elastic type to a search list item type if only 1 is found as a value', () => { + const response = getSearchListItemMock(); + const queryFilter = transformElasticNamedSearchToListItem({ + response, + type: TYPE, + value: [VALUE, '127.0.0.2'], + }); + const expected: SearchListItemArraySchema = [ + getSearchListItemResponseMock(), + { items: [], value: '127.0.0.2' }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it attaches two found results if the value is found in two hits from Elastic Search', () => { + const response = getSearchListItemMock(); + response.hits.hits = [ + ...response.hits.hits, + { + _id: LIST_ITEM_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListItemMock(), + _type: '', + matched_queries: ['0.0'], + }, + ]; + const queryFilter = transformElasticNamedSearchToListItem({ + response, + type: TYPE, + value: [VALUE], + }); + const { + items: [firstItem], + value, + } = getSearchListItemResponseMock(); + const expected: SearchListItemArraySchema = [ + { + items: [firstItem, firstItem], + value, + }, + ]; + expect(queryFilter).toEqual(expected); + }); + + test('it will return an empty array if no values are passed in', () => { + const response = getSearchListItemMock(); + response.hits.hits = [ + ...response.hits.hits, + { + _id: LIST_ITEM_ID, + _index: LIST_INDEX, + _score: 0, + _source: getSearchEsListItemMock(), + _type: '', + matched_queries: ['1.0'], + }, + ]; + const queryFilter = transformElasticNamedSearchToListItem({ + response, + type: TYPE, + value: [], + }); + expect(queryFilter).toEqual([]); + }); +}); diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts new file mode 100644 index 0000000000000..0326d22aa8436 --- /dev/null +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_named_search_to_list_item.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse } from 'elasticsearch'; + +import { SearchEsListItemSchema, SearchListItemArraySchema, Type } from '../../../common/schemas'; + +import { transformElasticHitsToListItem } from './transform_elastic_to_list_item'; + +export interface TransformElasticMSearchToListItemOptions { + response: SearchResponse; + type: Type; + value: unknown[]; +} + +/** + * Given an Elasticsearch response this will look to see if the named query matches the + * index found. The named query will have to be in the format of, "1.0", "1.1", "2.0" where the + * major number "1,2,n" will match with the index. + * Ref: https://www.elastic.co/guide/en/elasticsearch//reference/7.9/query-dsl-bool-query.html#named-queries + * @param response The elastic response + * @param type The list type + * @param value The values to check against the named queries. + */ +export const transformElasticNamedSearchToListItem = ({ + response, + type, + value, +}: TransformElasticMSearchToListItemOptions): SearchListItemArraySchema => { + return value.map((singleValue, index) => { + const matchingHits = response.hits.hits.filter((hit) => { + if (hit.matched_queries != null) { + return hit.matched_queries.some((matchedQuery) => { + const [matchedQueryIndex] = matchedQuery.split('.'); + return matchedQueryIndex === `${index}`; + }); + } else { + return false; + } + }); + const items = transformElasticHitsToListItem({ hits: matchingHits, type }); + return { + items, + value: singleValue, + }; + }); +}; diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts index 8a5554c3865c5..09e5ecd74b0de 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.test.ts @@ -8,7 +8,10 @@ import { getSearchListItemMock } from '../../../common/schemas/elastic_response/ import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock'; import { ListItemArraySchema } from '../../../common/schemas'; -import { transformElasticToListItem } from './transform_elastic_to_list_item'; +import { + transformElasticHitsToListItem, + transformElasticToListItem, +} from './transform_elastic_to_list_item'; describe('transform_elastic_to_list_item', () => { beforeEach(() => { @@ -19,28 +22,61 @@ describe('transform_elastic_to_list_item', () => { jest.clearAllMocks(); }); - test('it transforms an elastic type to a list item type', () => { - const response = getSearchListItemMock(); - const queryFilter = transformElasticToListItem({ - response, - type: 'ip', + describe('transformElasticToListItem', () => { + test('it transforms an elastic type to a list item type', () => { + const response = getSearchListItemMock(); + const queryFilter = transformElasticToListItem({ + response, + type: 'ip', + }); + const expected: ListItemArraySchema = [getListItemResponseMock()]; + expect(queryFilter).toEqual(expected); + }); + + test('it transforms an elastic keyword type to a list item type', () => { + const response = getSearchListItemMock(); + response.hits.hits[0]._source.ip = undefined; + response.hits.hits[0]._source.keyword = 'host-name-example'; + const queryFilter = transformElasticToListItem({ + response, + type: 'keyword', + }); + const listItemResponse = getListItemResponseMock(); + listItemResponse.type = 'keyword'; + listItemResponse.value = 'host-name-example'; + const expected: ListItemArraySchema = [listItemResponse]; + expect(queryFilter).toEqual(expected); }); - const expected: ListItemArraySchema = [getListItemResponseMock()]; - expect(queryFilter).toEqual(expected); }); - test('it transforms an elastic keyword type to a list item type', () => { - const response = getSearchListItemMock(); - response.hits.hits[0]._source.ip = undefined; - response.hits.hits[0]._source.keyword = 'host-name-example'; - const queryFilter = transformElasticToListItem({ - response, - type: 'keyword', + describe('transformElasticHitsToListItem', () => { + test('it transforms an elastic type to a list item type', () => { + const { + hits: { hits }, + } = getSearchListItemMock(); + const queryFilter = transformElasticHitsToListItem({ + hits, + type: 'ip', + }); + const expected: ListItemArraySchema = [getListItemResponseMock()]; + expect(queryFilter).toEqual(expected); + }); + + test('it transforms an elastic keyword type to a list item type', () => { + const { + hits: { hits }, + } = getSearchListItemMock(); + hits[0]._source.ip = undefined; + hits[0]._source.keyword = 'host-name-example'; + const queryFilter = transformElasticHitsToListItem({ + hits, + type: 'keyword', + }); + const listItemResponse = getListItemResponseMock(); + listItemResponse.type = 'keyword'; + listItemResponse.value = 'host-name-example'; + const expected: ListItemArraySchema = [listItemResponse]; + expect(queryFilter).toEqual(expected); }); - const listItemResponse = getListItemResponseMock(); - listItemResponse.type = 'keyword'; - listItemResponse.value = 'host-name-example'; - const expected: ListItemArraySchema = [listItemResponse]; - expect(queryFilter).toEqual(expected); }); }); diff --git a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts index 14794870bf67a..db16f213adec8 100644 --- a/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts +++ b/x-pack/plugins/lists/server/services/utils/transform_elastic_to_list_item.ts @@ -17,11 +17,23 @@ export interface TransformElasticToListItemOptions { type: Type; } +export interface TransformElasticHitToListItemOptions { + hits: SearchResponse['hits']['hits']; + type: Type; +} + export const transformElasticToListItem = ({ response, type, }: TransformElasticToListItemOptions): ListItemArraySchema => { - return response.hits.hits.map((hit) => { + return transformElasticHitsToListItem({ hits: response.hits.hits, type }); +}; + +export const transformElasticHitsToListItem = ({ + hits, + type, +}: TransformElasticHitToListItemOptions): ListItemArraySchema => { + return hits.map((hit) => { const { _id, _source: { diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx index 28a73801c8c0f..c82ef392ce3d8 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx @@ -9,7 +9,7 @@ import { EuiFormRow, EuiComboBoxOptionOption, EuiComboBox } from '@elastic/eui'; import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { useFindLists, ListSchema } from '../../../lists_plugin_deps'; import { useKibana } from '../../../common/lib/kibana'; -import { getGenericComboBoxProps } from './helpers'; +import { filterFieldToList, getGenericComboBoxProps } from './helpers'; import * as i18n from './translations'; interface AutocompleteFieldListsProps { @@ -41,17 +41,10 @@ export const AutocompleteFieldListsComponent: React.FC name, []); - const optionsMemo = useMemo(() => { - if ( - selectedField != null && - selectedField.esTypes != null && - selectedField.esTypes.length > 0 - ) { - return lists.filter(({ type }) => selectedField.esTypes?.includes(type)); - } else { - return []; - } - }, [lists, selectedField]); + const optionsMemo = useMemo(() => filterFieldToList(lists, selectedField), [ + lists, + selectedField, + ]); const selectedOptionsMemo = useMemo(() => { if (selectedValue != null) { const list = lists.filter(({ id }) => id === selectedValue); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts index f78740f764202..abbeec2b64d72 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts @@ -6,6 +6,7 @@ import moment from 'moment'; import '../../../common/mock/match_media'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; import * as i18n from './translations'; import { @@ -15,7 +16,16 @@ import { existsOperator, doesNotExistOperator, } from './operators'; -import { getOperators, checkEmptyValue, paramIsValid, getGenericComboBoxProps } from './helpers'; +import { + getOperators, + checkEmptyValue, + paramIsValid, + getGenericComboBoxProps, + typeMatch, + filterFieldToList, +} from './helpers'; +import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; +import { ListSchema } from '../../../../../lists/common'; describe('helpers', () => { // @ts-ignore @@ -260,4 +270,117 @@ describe('helpers', () => { }); }); }); + + describe('#typeMatch', () => { + test('ip -> ip is true', () => { + expect(typeMatch('ip', 'ip')).toEqual(true); + }); + + test('keyword -> keyword is true', () => { + expect(typeMatch('keyword', 'keyword')).toEqual(true); + }); + + test('text -> text is true', () => { + expect(typeMatch('text', 'text')).toEqual(true); + }); + + test('ip_range -> ip is true', () => { + expect(typeMatch('ip_range', 'ip')).toEqual(true); + }); + + test('date_range -> date is true', () => { + expect(typeMatch('date_range', 'date')).toEqual(true); + }); + + test('double_range -> double is true', () => { + expect(typeMatch('double_range', 'double')).toEqual(true); + }); + + test('float_range -> float is true', () => { + expect(typeMatch('float_range', 'float')).toEqual(true); + }); + + test('integer_range -> integer is true', () => { + expect(typeMatch('integer_range', 'integer')).toEqual(true); + }); + + test('long_range -> long is true', () => { + expect(typeMatch('long_range', 'long')).toEqual(true); + }); + + test('ip -> date is false', () => { + expect(typeMatch('ip', 'date')).toEqual(false); + }); + + test('long -> float is false', () => { + expect(typeMatch('long', 'float')).toEqual(false); + }); + + test('integer -> long is false', () => { + expect(typeMatch('integer', 'long')).toEqual(false); + }); + }); + + describe('#filterFieldToList', () => { + test('it returns empty array if given a undefined for field', () => { + const filter = filterFieldToList([], undefined); + expect(filter).toEqual([]); + }); + + test('it returns empty array if filed does not contain esTypes', () => { + const field: IFieldType = { name: 'some-name', type: 'some-type' }; + const filter = filterFieldToList([], field); + expect(filter).toEqual([]); + }); + + test('it returns single filtered list of ip_range -> ip', () => { + const field: IFieldType = { name: 'some-name', type: 'ip', esTypes: ['ip'] }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of ip -> ip', () => { + const field: IFieldType = { name: 'some-name', type: 'ip', esTypes: ['ip'] }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'ip' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of keyword -> keyword', () => { + const field: IFieldType = { name: 'some-name', type: 'keyword', esTypes: ['keyword'] }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'keyword' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns single filtered list of text -> text', () => { + const field: IFieldType = { name: 'some-name', type: 'text', esTypes: ['text'] }; + const listItem: ListSchema = { ...getListResponseMock(), type: 'text' }; + const filter = filterFieldToList([listItem], field); + const expected: ListSchema[] = [listItem]; + expect(filter).toEqual(expected); + }); + + test('it returns 2 filtered lists of ip_range -> ip', () => { + const field: IFieldType = { name: 'some-name', type: 'ip', esTypes: ['ip'] }; + const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const listItem2: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const filter = filterFieldToList([listItem1, listItem2], field); + const expected: ListSchema[] = [listItem1, listItem2]; + expect(filter).toEqual(expected); + }); + + test('it returns 1 filtered lists of ip_range -> ip if the 2nd is not compatible type', () => { + const field: IFieldType = { name: 'some-name', type: 'ip', esTypes: ['ip'] }; + const listItem1: ListSchema = { ...getListResponseMock(), type: 'ip_range' }; + const listItem2: ListSchema = { ...getListResponseMock(), type: 'text' }; + const filter = filterFieldToList([listItem1, listItem2], field); + const expected: ListSchema[] = [listItem1]; + expect(filter).toEqual(expected); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index 1ad296e0299b1..44e5adde65650 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -18,6 +18,7 @@ import { } from './operators'; import { GetGenericComboBoxPropsReturn, OperatorOption } from './types'; import * as i18n from './translations'; +import { ListSchema, Type } from '../../../lists_plugin_deps'; /** * Returns the appropriate operators given a field type @@ -138,3 +139,36 @@ export function getGenericComboBoxProps({ selectedComboOptions: newSelectedComboOptions, }; } + +/** + * Given an array of lists and optionally a field this will return all + * the lists that match against the field based on the types from the field + * @param lists The lists to match against the field + * @param field The field to check against the list to see if they are compatible + */ +export const filterFieldToList = (lists: ListSchema[], field?: IFieldType): ListSchema[] => { + if (field != null) { + const { esTypes = [] } = field; + return lists.filter(({ type }) => esTypes.some((esType) => typeMatch(type, esType))); + } else { + return []; + } +}; + +/** + * Given an input list type and a string based ES type this will match + * if they're exact or if they are compatible with a range + * @param type The type to match against the esType + * @param esType The ES type to match with + */ +export const typeMatch = (type: Type, esType: string): boolean => { + return ( + type === esType || + (type === 'ip_range' && esType === 'ip') || + (type === 'date_range' && esType === 'date') || + (type === 'double_range' && esType === 'double') || + (type === 'float_range' && esType === 'float') || + (type === 'integer_range' && esType === 'integer') || + (type === 'long_range' && esType === 'long') + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts deleted file mode 100644 index 1c13de16d9b1e..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.ts +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { get } from 'lodash/fp'; -import { Logger } from 'src/core/server'; - -import { ListClient } from '../../../../../lists/server'; -import { BuildRuleMessage } from './rule_messages'; -import { - EntryList, - ExceptionListItemSchema, - entriesList, - Type, -} from '../../../../../lists/common/schemas'; -import { hasLargeValueList } from '../../../../common/detection_engine/utils'; -import { SearchTypes } from '../../../../common/detection_engine/types'; -import { SearchResponse } from '../../types'; - -// narrow unioned type to be single -const isStringableType = (val: SearchTypes): val is string | number | boolean => - ['string', 'number', 'boolean'].includes(typeof val); - -const isStringableArray = (val: SearchTypes): val is Array => { - if (!Array.isArray(val)) { - return false; - } - // TS does not allow .every to be called on val as-is, even though every type in the union - // is an array. https://github.com/microsoft/TypeScript/issues/36390 - // @ts-expect-error - return val.every((subVal) => isStringableType(subVal)); -}; - -export const createSetToFilterAgainst = async ({ - events, - field, - listId, - listType, - listClient, - logger, - buildRuleMessage, -}: { - events: SearchResponse['hits']['hits']; - field: string; - listId: string; - listType: Type; - listClient: ListClient; - logger: Logger; - buildRuleMessage: BuildRuleMessage; -}): Promise> => { - const valuesFromSearchResultField = events.reduce((acc, searchResultItem) => { - const valueField = get(field, searchResultItem._source); - if (valueField != null) { - if (isStringableType(valueField)) { - acc.add(valueField.toString()); - } else if (isStringableArray(valueField)) { - valueField.forEach((subVal) => acc.add(subVal.toString())); - } - } - return acc; - }, new Set()); - logger.debug( - `number of distinct values from ${field}: ${[...valuesFromSearchResultField].length}` - ); - - // matched will contain any list items that matched with the - // values passed in from the Set. - const matchedListItems = await listClient.getListItemByValues({ - listId, - type: listType, - value: [...valuesFromSearchResultField], - }); - - logger.debug(`number of matched items from list with id ${listId}: ${matchedListItems.length}`); - // create a set of list values that were a hit - easier to work with - const matchedListItemsSet = new Set(matchedListItems.map((item) => item.value)); - return matchedListItemsSet; -}; - -export const filterEventsAgainstList = async ({ - listClient, - exceptionsList, - logger, - eventSearchResult, - buildRuleMessage, -}: { - listClient: ListClient; - exceptionsList: ExceptionListItemSchema[]; - logger: Logger; - eventSearchResult: SearchResponse; - buildRuleMessage: BuildRuleMessage; -}): Promise> => { - try { - if (exceptionsList == null || exceptionsList.length === 0) { - logger.debug(buildRuleMessage('about to return original search result')); - return eventSearchResult; - } - - const exceptionItemsWithLargeValueLists = exceptionsList.reduce( - (acc, exception) => { - const { entries } = exception; - if (hasLargeValueList(entries)) { - return [...acc, exception]; - } - - return acc; - }, - [] - ); - - if (exceptionItemsWithLargeValueLists.length === 0) { - logger.debug( - buildRuleMessage('no exception items of type list found - returning original search result') - ); - return eventSearchResult; - } - - const valueListExceptionItems = exceptionsList.filter((listItem: ExceptionListItemSchema) => { - return listItem.entries.every((entry) => entriesList.is(entry)); - }); - - // now that we have all the exception items which are value lists (whether single entry or have multiple entries) - const res = await valueListExceptionItems.reduce['hits']['hits']>>( - async ( - filteredAccum: Promise['hits']['hits']>, - exceptionItem: ExceptionListItemSchema - ) => { - // 1. acquire the values from the specified fields to check - // e.g. if the value list is checking against source.ip, gather - // all the values for source.ip from the search response events. - - // 2. search against the value list with the values found in the search result - // and see if there are any matches. For every match, add that value to a set - // that represents the "matched" values - - // 3. filter the search result against the set from step 2 using the - // given operator (included vs excluded). - // acquire the list values we are checking for in the field. - const filtered = await filteredAccum; - const typedEntries = exceptionItem.entries.filter((entry): entry is EntryList => - entriesList.is(entry) - ); - const fieldAndSetTuples = await Promise.all( - typedEntries.map(async (entry) => { - const { list, field, operator } = entry; - const { id, type } = list; - const matchedSet = await createSetToFilterAgainst({ - events: filtered, - field, - listId: id, - listType: type, - listClient, - logger, - buildRuleMessage, - }); - - return Promise.resolve({ field, operator, matchedSet }); - }) - ); - - // check if for each tuple, the entry is not in both for when two value list entries exist. - // need to re-write this as a reduce. - const filteredEvents = filtered.filter((item) => { - const vals = fieldAndSetTuples.map((tuple) => { - const eventItem = get(tuple.field, item._source); - if (tuple.operator === 'included') { - // only create a signal if the field value is not in the value list - if (eventItem != null) { - if (isStringableType(eventItem)) { - return !tuple.matchedSet.has(eventItem); - } else if (isStringableArray(eventItem)) { - return !eventItem.some((val) => tuple.matchedSet.has(val)); - } - } - return true; - } else if (tuple.operator === 'excluded') { - // only create a signal if the field value is in the value list - if (eventItem != null) { - if (isStringableType(eventItem)) { - return tuple.matchedSet.has(eventItem); - } else if (isStringableArray(eventItem)) { - return eventItem.some((val) => tuple.matchedSet.has(val)); - } - } - return true; - } - return false; - }); - return vals.some((value) => value); - }); - const diff = eventSearchResult.hits.hits.length - filteredEvents.length; - logger.debug( - buildRuleMessage(`Exception with id ${exceptionItem.id} filtered out ${diff} events`) - ); - const toReturn = filteredEvents; - return toReturn; - }, - Promise.resolve['hits']['hits']>(eventSearchResult.hits.hits) - ); - - const toReturn: SearchResponse = { - took: eventSearchResult.took, - timed_out: eventSearchResult.timed_out, - _shards: eventSearchResult._shards, - hits: { - total: res.length, - max_score: eventSearchResult.hits.max_score, - hits: res, - }, - }; - return toReturn; - } catch (exc) { - throw new Error(`Failed to query lists index. Reason: ${exc.message}`); - } -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts new file mode 100644 index 0000000000000..9192eeb35d0e8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.test.ts @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createFieldAndSetTuples } from './create_field_and_set_tuples'; +import { mockLogger, sampleDocWithSortId } from '../__mocks__/es_results'; + +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { listMock } from '../../../../../../lists/server/mocks'; +import { getSearchListItemResponseMock } from '../../../../../../lists/common/schemas/response/search_list_item_schema.mock'; +import { EntryList } from '../../../../../../lists/common'; +import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; + +describe('filterEventsAgainstList', () => { + let listClient = listMock.getListClient(); + let exceptionItem = getExceptionListItemSchemaMock(); + let events = [sampleDocWithSortId('123', '1.1.1.1')]; + + beforeEach(() => { + jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.searchListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.map((item) => ({ + ...getSearchListItemResponseMock(), + value: item, + })) + ) + ); + exceptionItem = { + ...getExceptionListItemSchemaMock(), + entries: [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ], + }; + events = [sampleDocWithSortId('123', '1.1.1.1')]; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns an empty array if exceptionItem entries are empty', async () => { + exceptionItem.entries = []; + const field = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect(field).toEqual([]); + }); + + test('it returns a single field and set tuple if entries has a single item', async () => { + const field = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect(field.length).toEqual(1); + }); + + test('it returns "included" if the operator is "included"', async () => { + (exceptionItem.entries[0] as EntryList).operator = 'included'; + const [{ operator }] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect(operator).toEqual('included'); + }); + + test('it returns "excluded" if the operator is "excluded"', async () => { + (exceptionItem.entries[0] as EntryList).operator = 'excluded'; + const [{ operator }] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect(operator).toEqual('excluded'); + }); + + test('it returns "field" if the "field is "source.ip"', async () => { + (exceptionItem.entries[0] as EntryList).field = 'source.ip'; + const [{ field }] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect(field).toEqual('source.ip'); + }); + + test('it returns a single matched set as a JSON.stringify() set from the "events"', async () => { + events = [sampleDocWithSortId('123', '1.1.1.1')]; + (exceptionItem.entries[0] as EntryList).field = 'source.ip'; + const [{ matchedSet }] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect([...matchedSet]).toEqual([JSON.stringify('1.1.1.1')]); + }); + + test('it returns two matched sets as a JSON.stringify() set from the "events"', async () => { + events = [sampleDocWithSortId('123', '1.1.1.1'), sampleDocWithSortId('456', '2.2.2.2')]; + (exceptionItem.entries[0] as EntryList).field = 'source.ip'; + const [{ matchedSet }] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect([...matchedSet]).toEqual([JSON.stringify('1.1.1.1'), JSON.stringify('2.2.2.2')]); + }); + + test('it returns an array as a set as a JSON.stringify() array from the "events"', async () => { + events = [sampleDocWithSortId('123', ['1.1.1.1', '2.2.2.2'])]; + (exceptionItem.entries[0] as EntryList).field = 'source.ip'; + const [{ matchedSet }] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect([...matchedSet]).toEqual([JSON.stringify(['1.1.1.1', '2.2.2.2'])]); + }); + + test('it returns 2 fields when given two exception list items', async () => { + events = [sampleDocWithSortId('123', '1.1.1.1'), sampleDocWithSortId('456', '2.2.2.2')]; + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + { + field: 'destination.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const fields = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect(fields.length).toEqual(2); + }); + + test('it returns two matched sets from two different events, one excluded, and one included', async () => { + events = [sampleDocWithSortId('123', '1.1.1.1'), sampleDocWithSortId('456', '2.2.2.2')]; + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + { + field: 'destination.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const [{ operator: operator1 }, { operator: operator2 }] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect(operator1).toEqual('included'); + expect(operator2).toEqual('excluded'); + }); + + test('it returns two fields from two different events', async () => { + events = [sampleDocWithSortId('123', '1.1.1.1'), sampleDocWithSortId('456', '2.2.2.2')]; + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + { + field: 'destination.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const [{ field: field1 }, { field: field2 }] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect(field1).toEqual('source.ip'); + expect(field2).toEqual('destination.ip'); + }); + + test('it returns two matches from two different events', async () => { + events = [ + sampleDocWithSortId('123', '1.1.1.1', '3.3.3.3'), + sampleDocWithSortId('456', '2.2.2.2', '5.5.5.5'), + ]; + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + { + field: 'destination.ip', + operator: 'excluded', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const [ + { matchedSet: matchedSet1 }, + { matchedSet: matchedSet2 }, + ] = await createFieldAndSetTuples({ + listClient, + logger: mockLogger, + events, + exceptionItem, + buildRuleMessage, + }); + expect([...matchedSet1]).toEqual([JSON.stringify('1.1.1.1'), JSON.stringify('2.2.2.2')]); + expect([...matchedSet2]).toEqual([JSON.stringify('3.3.3.3'), JSON.stringify('5.5.5.5')]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts new file mode 100644 index 0000000000000..d31b7a0eb613e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_field_and_set_tuples.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EntryList, entriesList } from '../../../../../../lists/common'; +import { createSetToFilterAgainst } from './create_set_to_filter_against'; +import { CreateFieldAndSetTuplesOptions, FieldSet } from './types'; + +export const createFieldAndSetTuples = async ({ + events, + exceptionItem, + listClient, + logger, + buildRuleMessage, +}: CreateFieldAndSetTuplesOptions): Promise => { + const typedEntries = exceptionItem.entries.filter((entry): entry is EntryList => + entriesList.is(entry) + ); + const fieldAndSetTuples = await Promise.all( + typedEntries.map(async (entry) => { + const { list, field, operator } = entry; + const { id, type } = list; + const matchedSet = await createSetToFilterAgainst({ + events, + field, + listId: id, + listType: type, + listClient, + logger, + buildRuleMessage, + }); + + return { field, operator, matchedSet }; + }) + ); + return fieldAndSetTuples; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts new file mode 100644 index 0000000000000..0ac09713cc8a6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.test.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockLogger, sampleDocWithSortId } from '../__mocks__/es_results'; + +import { listMock } from '../../../../../../lists/server/mocks'; +import { getSearchListItemResponseMock } from '../../../../../../lists/common/schemas/response/search_list_item_schema.mock'; +import { createSetToFilterAgainst } from './create_set_to_filter_against'; +import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; + +describe('createSetToFilterAgainst', () => { + let listClient = listMock.getListClient(); + let events = [sampleDocWithSortId('123', '1.1.1.1')]; + + beforeEach(() => { + jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.searchListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.map((item) => ({ + ...getSearchListItemResponseMock(), + value: item, + })) + ) + ); + events = [sampleDocWithSortId('123', '1.1.1.1')]; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it returns an empty array if list return is empty', async () => { + listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); + const field = await createSetToFilterAgainst({ + events, + field: 'source.ip', + listId: 'list-123', + listType: 'ip', + listClient, + logger: mockLogger, + buildRuleMessage, + }); + expect([...field]).toEqual([]); + }); + + test('it returns 1 field if the list returns a single item', async () => { + events = [sampleDocWithSortId('123', '1.1.1.1')]; + const field = await createSetToFilterAgainst({ + events, + field: 'source.ip', + listId: 'list-123', + listType: 'ip', + listClient, + logger: mockLogger, + buildRuleMessage, + }); + expect(listClient.searchListItemByValues).toHaveBeenCalledWith({ + listId: 'list-123', + type: 'ip', + value: ['1.1.1.1'], + }); + expect([...field]).toEqual([JSON.stringify('1.1.1.1')]); + }); + + test('it returns 2 fields if the list returns 2 items', async () => { + events = [sampleDocWithSortId('123', '1.1.1.1'), sampleDocWithSortId('123', '2.2.2.2')]; + const field = await createSetToFilterAgainst({ + events, + field: 'source.ip', + listId: 'list-123', + listType: 'ip', + listClient, + logger: mockLogger, + buildRuleMessage, + }); + expect(listClient.searchListItemByValues).toHaveBeenCalledWith({ + listId: 'list-123', + type: 'ip', + value: ['1.1.1.1', '2.2.2.2'], + }); + expect([...field]).toEqual([JSON.stringify('1.1.1.1'), JSON.stringify('2.2.2.2')]); + }); + + test('it returns 0 fields if the field does not match up to a valid field within the event', async () => { + events = [sampleDocWithSortId('123', '1.1.1.1'), sampleDocWithSortId('123', '2.2.2.2')]; + const field = await createSetToFilterAgainst({ + events, + field: 'nonexistent.field', // field does not exist + listId: 'list-123', + listType: 'ip', + listClient, + logger: mockLogger, + buildRuleMessage, + }); + expect(listClient.searchListItemByValues).toHaveBeenCalledWith({ + listId: 'list-123', + type: 'ip', + value: [], + }); + expect([...field]).toEqual([]); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts new file mode 100644 index 0000000000000..c546654676c83 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/create_set_to_filter_against.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash/fp'; +import { CreateSetToFilterAgainstOptions } from './types'; + +/** + * Creates a field set to filter against using the stringed version of the + * data type for compare. Creates a set of list values that are stringified that + * are easier to work with as well as ensures that deep values can work since it turns + * things into a string using JSON.stringify(). + * + * @param events The events to filter against + * @param field The field checking against the list + * @param listId The list id for the list function call + * @param listType The type of list for the list function call + * @param listClient The list client API + * @param logger logger for errors, debug, etc... + */ +export const createSetToFilterAgainst = async ({ + events, + field, + listId, + listType, + listClient, + logger, + buildRuleMessage, +}: CreateSetToFilterAgainstOptions): Promise> => { + const valuesFromSearchResultField = events.reduce((acc, searchResultItem) => { + const valueField = get(field, searchResultItem._source); + if (valueField != null) { + acc.add(valueField); + } + return acc; + }, new Set()); + + logger.debug( + buildRuleMessage( + `number of distinct values from ${field}: ${[...valuesFromSearchResultField].length}` + ) + ); + + const matchedListItems = await listClient.searchListItemByValues({ + listId, + type: listType, + value: [...valuesFromSearchResultField], + }); + + logger.debug( + buildRuleMessage( + `number of matched items from list with id ${listId}: ${matchedListItems.length}` + ) + ); + + return new Set( + matchedListItems + .filter((item) => item.items.length !== 0) + .map((item) => JSON.stringify(item.value)) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts new file mode 100644 index 0000000000000..6a045f6694da1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { sampleDocWithSortId } from '../__mocks__/es_results'; + +import { listMock } from '../../../../../../lists/server/mocks'; +import { getSearchListItemResponseMock } from '../../../../../../lists/common/schemas/response/search_list_item_schema.mock'; +import { filterEvents } from './filter_events'; +import { FieldSet } from './types'; + +describe('filterEvents', () => { + let listClient = listMock.getListClient(); + let events = [sampleDocWithSortId('123', '1.1.1.1')]; + + beforeEach(() => { + jest.clearAllMocks(); + listClient = listMock.getListClient(); + listClient.searchListItemByValues = jest.fn(({ value }) => + Promise.resolve( + value.map((item) => ({ + ...getSearchListItemResponseMock(), + value: item, + })) + ) + ); + events = [sampleDocWithSortId('123', '1.1.1.1')]; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it filters out the event if it is "included"', () => { + events = [sampleDocWithSortId('123', '1.1.1.1')]; + const fieldAndSetTuples: FieldSet[] = [ + { + field: 'source.ip', + operator: 'included', + matchedSet: new Set([JSON.stringify('1.1.1.1')]), + }, + ]; + const field = filterEvents({ + events, + fieldAndSetTuples, + }); + expect([...field]).toEqual([]); + }); + + test('it does not filter out the event if it is "excluded"', () => { + events = [sampleDocWithSortId('123', '1.1.1.1')]; + const fieldAndSetTuples: FieldSet[] = [ + { + field: 'source.ip', + operator: 'excluded', + matchedSet: new Set([JSON.stringify('1.1.1.1')]), + }, + ]; + const field = filterEvents({ + events, + fieldAndSetTuples, + }); + expect([...field]).toEqual(events); + }); + + test('it does NOT filter out the event if the field is not found', () => { + events = [sampleDocWithSortId('123', '1.1.1.1')]; + const fieldAndSetTuples: FieldSet[] = [ + { + field: 'madeup.nonexistent', // field does not exist + operator: 'included', + matchedSet: new Set([JSON.stringify('1.1.1.1')]), + }, + ]; + const field = filterEvents({ + events, + fieldAndSetTuples, + }); + expect([...field]).toEqual(events); + }); + + test('it does NOT filter out the event if it is in both an inclusion and exclusion list', () => { + events = [sampleDocWithSortId('123', '1.1.1.1'), sampleDocWithSortId('123', '2.2.2.2')]; + const fieldAndSetTuples: FieldSet[] = [ + { + field: 'source.ip', + operator: 'included', + matchedSet: new Set([JSON.stringify('1.1.1.1')]), + }, + { + field: 'source.ip', + operator: 'excluded', + matchedSet: new Set([JSON.stringify('1.1.1.1')]), + }, + ]; + + const field = filterEvents({ + events, + fieldAndSetTuples, + }); + expect([...field]).toEqual(events); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts new file mode 100644 index 0000000000000..e8667510da686 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash/fp'; +import { SearchResponse } from '../../../types'; +import { FilterEventsOptions } from './types'; + +/** + * Check if for each tuple, the entry is not in both for when two or more value list entries exist. + * If the entry is in both an inclusion and exclusion list it will not be filtered out. + * @param events The events to check against + * @param fieldAndSetTuples The field and set tuples + */ +export const filterEvents = ({ + events, + fieldAndSetTuples, +}: FilterEventsOptions): SearchResponse['hits']['hits'] => { + return events.filter((item) => { + return fieldAndSetTuples + .map((tuple) => { + const eventItem = get(tuple.field, item._source); + if (eventItem == null) { + return true; + } else if (tuple.operator === 'included') { + // only create a signal if the event is not in the value list + return !tuple.matchedSet.has(JSON.stringify(eventItem)); + } else if (tuple.operator === 'excluded') { + // only create a signal if the event is in the value list + return tuple.matchedSet.has(JSON.stringify(eventItem)); + } else { + return false; + } + }) + .some((value) => value); + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts similarity index 77% rename from x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts index 01e7e7160e1ae..eb6e905c03038 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filter_events_with_list.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.test.ts @@ -5,27 +5,27 @@ */ import uuid from 'uuid'; -import { filterEventsAgainstList } from './filter_events_with_list'; -import { buildRuleMessageFactory } from './rule_messages'; -import { mockLogger, repeatedSearchResultsWithSortId } from './__mocks__/es_results'; +import { filterEventsAgainstList } from './filter_events_against_list'; +import { buildRuleMessageMock as buildRuleMessage } from '../rule_messages.mock'; +import { mockLogger, repeatedSearchResultsWithSortId } from '../__mocks__/es_results'; -import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; -import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; -import { listMock } from '../../../../../lists/server/mocks'; +import { getExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { listMock } from '../../../../../../lists/server/mocks'; +import { getSearchListItemResponseMock } from '../../../../../../lists/common/schemas/response/search_list_item_schema.mock'; const someGuids = Array.from({ length: 13 }).map((x) => uuid.v4()); -const buildRuleMessage = buildRuleMessageFactory({ - id: 'fake id', - ruleId: 'fake rule id', - index: 'fakeindex', - name: 'fake name', -}); + describe('filterEventsAgainstList', () => { let listClient = listMock.getListClient(); + beforeEach(() => { jest.clearAllMocks(); listClient = listMock.getListClient(); - listClient.getListItemByValues = jest.fn().mockResolvedValue([]); + listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it('should respond with eventSearchResult if exceptionList is empty array', async () => { @@ -87,6 +87,7 @@ describe('filterEventsAgainstList', () => { }); expect(res.hits.hits.length).toEqual(4); }); + it('should respond with less items in the list if some values match', async () => { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ @@ -100,10 +101,10 @@ describe('filterEventsAgainstList', () => { }, }, ]; - listClient.getListItemByValues = jest.fn(({ value }) => + listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ - ...getListItemResponseMock(), + ...getSearchListItemResponseMock(), value: item, })) ) @@ -120,8 +121,8 @@ describe('filterEventsAgainstList', () => { ]), buildRuleMessage, }); - expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); - expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( + expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); + expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( 'ci-badguys.txt' ); expect(res.hits.hits.length).toEqual(2); @@ -159,13 +160,13 @@ describe('filterEventsAgainstList', () => { ]; // this call represents an exception list with a value list containing ['2.2.2.2', '4.4.4.4'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '2.2.2.2' }, - { ...getListItemResponseMock(), value: '4.4.4.4' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: '2.2.2.2' }, + { ...getSearchListItemResponseMock(), value: '4.4.4.4' }, ]); // this call represents an exception list with a value list containing ['6.6.6.6'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '6.6.6.6' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: '6.6.6.6' }, ]); const res = await filterEventsAgainstList({ @@ -185,7 +186,7 @@ describe('filterEventsAgainstList', () => { ]), buildRuleMessage, }); - expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(6); // @ts-expect-error @@ -221,12 +222,12 @@ describe('filterEventsAgainstList', () => { ]; // this call represents an exception list with a value list containing ['2.2.2.2', '4.4.4.4'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '2.2.2.2' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: '2.2.2.2' }, ]); // this call represents an exception list with a value list containing ['6.6.6.6'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '6.6.6.6' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: '6.6.6.6' }, ]); const res = await filterEventsAgainstList({ @@ -246,7 +247,7 @@ describe('filterEventsAgainstList', () => { ]), buildRuleMessage, }); - expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); // @ts-expect-error const ipVals = res.hits.hits.map((item) => item._source.source.ip); expect(res.hits.hits.length).toEqual(7); @@ -280,12 +281,12 @@ describe('filterEventsAgainstList', () => { ]; // this call represents an exception list with a value list containing ['2.2.2.2'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '2.2.2.2' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: '2.2.2.2' }, ]); // this call represents an exception list with a value list containing ['4.4.4.4'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '4.4.4.4' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: '4.4.4.4' }, ]); const res = await filterEventsAgainstList({ @@ -321,7 +322,7 @@ describe('filterEventsAgainstList', () => { ), buildRuleMessage, }); - expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(8); // @ts-expect-error @@ -362,8 +363,8 @@ describe('filterEventsAgainstList', () => { ]; // this call represents an exception list with a value list containing ['2.2.2.2', '4.4.4.4'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValue([ - { ...getListItemResponseMock(), value: '2.2.2.2' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValue([ + { ...getSearchListItemResponseMock(), value: '2.2.2.2' }, ]); const res = await filterEventsAgainstList({ @@ -383,7 +384,7 @@ describe('filterEventsAgainstList', () => { ]), buildRuleMessage, }); - expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); expect(res.hits.hits.length).toEqual(9); // @ts-expect-error @@ -401,7 +402,7 @@ describe('filterEventsAgainstList', () => { ]).toEqual(ipVals); }); - it('should respond with less items in the list given one exception item with two entries of type list and array of values in document', async () => { + it('should respond with same items in the list given one exception item with two entries of type list and array of values in document', async () => { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ { @@ -425,12 +426,12 @@ describe('filterEventsAgainstList', () => { ]; // this call represents an exception list with a value list containing ['2.2.2.2'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '2.2.2.2' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: ['2.2.2.2', '3.3.3.3'] }, ]); // this call represents an exception list with a value list containing ['4.4.4.4'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '4.4.4.4' }, + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: ['3.3.3.3', '4.4.4.4'] }, ]); const res = await filterEventsAgainstList({ @@ -454,17 +455,16 @@ describe('filterEventsAgainstList', () => { ), buildRuleMessage, }); - expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); - expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ - '1.1.1.1', - '2.2.2.2', - '3.3.3.3', + expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ + ['1.1.1.1', '1.1.1.1'], + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], ]); - expect((listClient.getListItemByValues as jest.Mock).mock.calls[1][0].value).toEqual([ - '1.1.1.1', - '2.2.2.2', - '3.3.3.3', - '4.4.4.4', + expect((listClient.searchListItemByValues as jest.Mock).mock.calls[1][0].value).toEqual([ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ['3.3.3.3', '4.4.4.4'], ]); expect(res.hits.hits.length).toEqual(2); @@ -505,6 +505,7 @@ describe('filterEventsAgainstList', () => { }); expect(res.hits.hits.length).toEqual(0); }); + it('should respond with less items in the list if some values match', async () => { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ @@ -518,10 +519,10 @@ describe('filterEventsAgainstList', () => { }, }, ]; - listClient.getListItemByValues = jest.fn(({ value }) => + listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ - ...getListItemResponseMock(), + ...getSearchListItemResponseMock(), value: item, })) ) @@ -538,14 +539,14 @@ describe('filterEventsAgainstList', () => { ]), buildRuleMessage, }); - expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); - expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( + expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].type).toEqual('ip'); + expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].listId).toEqual( 'ci-badguys.txt' ); expect(res.hits.hits.length).toEqual(2); }); - it('should respond with less items in the list given one exception item with two entries of type list and array of values in document', async () => { + it('should respond with the same items in the list given one exception item with two entries of type list and array of values in document', async () => { const exceptionItem = getExceptionListItemSchemaMock(); exceptionItem.entries = [ { @@ -568,13 +569,16 @@ describe('filterEventsAgainstList', () => { }, ]; - // this call represents an exception list with a value list containing ['2.2.2.2'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '2.2.2.2' }, + // this call represents an exception list with a value list containing ['2.2.2.2', '3.3.3.3'] + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { + ...getSearchListItemResponseMock(), + value: ['1.1.1.1', '2.2.2.2'], + }, ]); - // this call represents an exception list with a value list containing ['4.4.4.4'] - (listClient.getListItemByValues as jest.Mock).mockResolvedValueOnce([ - { ...getListItemResponseMock(), value: '4.4.4.4' }, + // this call represents an exception list with a value list containing ['3.3.3.3', '4.4.4.4'] + (listClient.searchListItemByValues as jest.Mock).mockResolvedValueOnce([ + { ...getSearchListItemResponseMock(), value: ['3.3.3.3', '4.4.4.4'] }, ]); const res = await filterEventsAgainstList({ @@ -598,17 +602,16 @@ describe('filterEventsAgainstList', () => { ), buildRuleMessage, }); - expect(listClient.getListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); - expect((listClient.getListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ - '1.1.1.1', - '2.2.2.2', - '3.3.3.3', + expect(listClient.searchListItemByValues as jest.Mock).toHaveBeenCalledTimes(2); + expect((listClient.searchListItemByValues as jest.Mock).mock.calls[0][0].value).toEqual([ + ['1.1.1.1', '1.1.1.1'], + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], ]); - expect((listClient.getListItemByValues as jest.Mock).mock.calls[1][0].value).toEqual([ - '1.1.1.1', - '2.2.2.2', - '3.3.3.3', - '4.4.4.4', + expect((listClient.searchListItemByValues as jest.Mock).mock.calls[1][0].value).toEqual([ + ['1.1.1.1', '2.2.2.2'], + ['2.2.2.2', '3.3.3.3'], + ['3.3.3.3', '4.4.4.4'], ]); expect(res.hits.hits.length).toEqual(2); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts new file mode 100644 index 0000000000000..e6c20713afd97 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/filter_events_against_list.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ExceptionListItemSchema, entriesList } from '../../../../../../lists/common/schemas'; +import { hasLargeValueList } from '../../../../../common/detection_engine/utils'; +import { FilterEventsAgainstListOptions } from './types'; +import { filterEvents } from './filter_events'; +import { createFieldAndSetTuples } from './create_field_and_set_tuples'; +import { SearchResponse } from '../../../types'; + +/** + * Filters events against a large value based list. It does this through these + * steps below. + * + * 1. acquire the values from the specified fields to check + * e.g. if the value list is checking against source.ip, gather + * all the values for source.ip from the search response events. + * + * 2. search against the value list with the values found in the search result + * and see if there are any matches. For every match, add that value to a set + * that represents the "matched" values + * + * 3. filter the search result against the set from step 2 using the + * given operator (included vs excluded). + * acquire the list values we are checking for in the field. + * + * @param listClient The list client to use for queries + * @param exceptionsList The exception list + * @param logger Logger for messages + * @param eventSearchResult The current events from the search + */ +export const filterEventsAgainstList = async ({ + listClient, + exceptionsList, + logger, + eventSearchResult, + buildRuleMessage, +}: FilterEventsAgainstListOptions): Promise> => { + try { + const atLeastOneLargeValueList = exceptionsList.some(({ entries }) => + hasLargeValueList(entries) + ); + + if (!atLeastOneLargeValueList) { + logger.debug( + buildRuleMessage('no exception items of type list found - returning original search result') + ); + return eventSearchResult; + } + + const valueListExceptionItems = exceptionsList.filter((listItem: ExceptionListItemSchema) => { + return listItem.entries.every((entry) => entriesList.is(entry)); + }); + + const res = await valueListExceptionItems.reduce['hits']['hits']>>( + async ( + filteredAccum: Promise['hits']['hits']>, + exceptionItem: ExceptionListItemSchema + ) => { + const events = await filteredAccum; + const fieldAndSetTuples = await createFieldAndSetTuples({ + events, + exceptionItem, + listClient, + logger, + buildRuleMessage, + }); + const filteredEvents = filterEvents({ events, fieldAndSetTuples }); + const diff = eventSearchResult.hits.hits.length - filteredEvents.length; + logger.debug( + buildRuleMessage(`Exception with id ${exceptionItem.id} filtered out ${diff} events`) + ); + return filteredEvents; + }, + Promise.resolve['hits']['hits']>(eventSearchResult.hits.hits) + ); + + return { + took: eventSearchResult.took, + timed_out: eventSearchResult.timed_out, + _shards: eventSearchResult._shards, + hits: { + total: res.length, + max_score: eventSearchResult.hits.max_score, + hits: res, + }, + }; + } catch (exc) { + throw new Error(`Failed to query large value based lists index. Reason: ${exc.message}`); + } +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts new file mode 100644 index 0000000000000..673719d87dcd0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/filters/types.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Logger } from 'src/core/server'; + +import { ListClient } from '../../../../../../lists/server'; +import { BuildRuleMessage } from '../rule_messages'; +import { ExceptionListItemSchema, Type } from '../../../../../../lists/common/schemas'; +import { SearchResponse } from '../../../types'; + +export interface FilterEventsAgainstListOptions { + listClient: ListClient; + exceptionsList: ExceptionListItemSchema[]; + logger: Logger; + eventSearchResult: SearchResponse; + buildRuleMessage: BuildRuleMessage; +} + +export interface CreateSetToFilterAgainstOptions { + events: SearchResponse['hits']['hits']; + field: string; + listId: string; + listType: Type; + listClient: ListClient; + logger: Logger; + buildRuleMessage: BuildRuleMessage; +} + +export interface FilterEventsOptions { + events: SearchResponse['hits']['hits']; + fieldAndSetTuples: FieldSet[]; +} + +export interface CreateFieldAndSetTuplesOptions { + events: SearchResponse['hits']['hits']; + exceptionItem: ExceptionListItemSchema; + listClient: ListClient; + logger: Logger; + buildRuleMessage: BuildRuleMessage; +} + +export interface FieldSet { + field: string; + operator: 'excluded' | 'included'; + matchedSet: Set; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.mock.ts new file mode 100644 index 0000000000000..9478ed18d472b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/rule_messages.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { buildRuleMessageFactory } from './rule_messages'; + +export const buildRuleMessageMock = buildRuleMessageFactory({ + id: 'fake id', + ruleId: 'fake rule id', + index: 'fakeindex', + name: 'fake name', +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index c82c1fe969ee3..46722c69e53e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -19,10 +19,11 @@ import { buildRuleMessageFactory } from './rule_messages'; import { DEFAULT_SIGNALS_INDEX } from '../../../../common/constants'; import { alertsMock, AlertServicesMock } from '../../../../../alerts/server/mocks'; import uuid from 'uuid'; -import { getListItemResponseMock } from '../../../../../lists/common/schemas/response/list_item_schema.mock'; import { listMock } from '../../../../../lists/server/mocks'; import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { BulkResponse } from './types'; +import { SearchListItemArraySchema } from '../../../../../lists/common/schemas'; +import { getSearchListItemResponseMock } from '../../../../../lists/common/schemas/response/search_list_item_schema.mock'; const buildRuleMessage = buildRuleMessageFactory({ id: 'fake id', @@ -39,7 +40,7 @@ describe('searchAfterAndBulkCreate', () => { beforeEach(() => { jest.clearAllMocks(); listClient = listMock.getListClient(); - listClient.getListItemByValues = jest.fn().mockResolvedValue([]); + listClient.searchListItemByValues = jest.fn().mockResolvedValue([]); inputIndexPattern = ['auditbeat-*']; mockService = alertsMock.createAlertServices(); }); @@ -362,9 +363,12 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success when all search results are in the allowlist and with sortId present', async () => { - listClient.getListItemByValues = jest - .fn() - .mockResolvedValue([{ value: '1.1.1.1' }, { value: '2.2.2.2' }, { value: '3.3.3.3' }]); + const searchListItems: SearchListItemArraySchema = [ + { ...getSearchListItemResponseMock(), value: '1.1.1.1' }, + { ...getSearchListItemResponseMock(), value: '2.2.2.2' }, + { ...getSearchListItemResponseMock(), value: '3.3.3.3' }, + ]; + listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems); const sampleParams = sampleRuleAlertParams(30); mockService.callCluster .mockResolvedValueOnce( @@ -423,9 +427,14 @@ describe('searchAfterAndBulkCreate', () => { }); test('should return success when all search results are in the allowlist and no sortId present', async () => { - listClient.getListItemByValues = jest - .fn() - .mockResolvedValue([{ value: '1.1.1.1' }, { value: '2.2.2.2' }, { value: '3.3.3.3' }]); + const searchListItems: SearchListItemArraySchema = [ + { ...getSearchListItemResponseMock(), value: '1.1.1.1' }, + { ...getSearchListItemResponseMock(), value: '2.2.2.2' }, + { ...getSearchListItemResponseMock(), value: '2.2.2.2' }, + { ...getSearchListItemResponseMock(), value: '2.2.2.2' }, + ]; + + listClient.searchListItemByValues = jest.fn().mockResolvedValue(searchListItems); const sampleParams = sampleRuleAlertParams(30); mockService.callCluster.mockResolvedValueOnce( repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3), [ @@ -605,10 +614,10 @@ describe('searchAfterAndBulkCreate', () => { }) .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); - listClient.getListItemByValues = jest.fn(({ value }) => + listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ - ...getListItemResponseMock(), + ...getSearchListItemResponseMock(), value: item, })) ) @@ -711,10 +720,10 @@ describe('searchAfterAndBulkCreate', () => { ]; const sampleParams = sampleRuleAlertParams(30); mockService.callCluster.mockResolvedValueOnce(sampleEmptyDocSearchResults()); - listClient.getListItemByValues = jest.fn(({ value }) => + listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ - ...getListItemResponseMock(), + ...getSearchListItemResponseMock(), value: item, })) ) @@ -771,10 +780,10 @@ describe('searchAfterAndBulkCreate', () => { .mockImplementation(() => { throw Error('Fake Error'); // throws the exception we are testing }); - listClient.getListItemByValues = jest.fn(({ value }) => + listClient.searchListItemByValues = jest.fn(({ value }) => Promise.resolve( value.slice(0, 2).map((item) => ({ - ...getListItemResponseMock(), + ...getSearchListItemResponseMock(), value: item, })) ) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 0e6ddbc766faa..32865e117cba9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -6,7 +6,7 @@ import { singleSearchAfter } from './single_search_after'; import { singleBulkCreate } from './single_bulk_create'; -import { filterEventsAgainstList } from './filter_events_with_list'; +import { filterEventsAgainstList } from './filters/filter_events_against_list'; import { sendAlertTelemetryEvents } from './send_telemetry_events'; import { createSearchAfterReturnType, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 7965a09efefa9..6be4a83d237a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -66,7 +66,7 @@ import { buildSignalFromEvent, buildSignalGroupFromSequence } from './build_bulk import { createThreatSignals } from './threat_mapping/create_threat_signals'; import { getIndexVersion } from '../routes/index/get_index_version'; import { MIN_EQL_RULE_INDEX_VERSION } from '../routes/index/get_signals_template'; -import { filterEventsAgainstList } from './filter_events_with_list'; +import { filterEventsAgainstList } from './filters/filter_events_against_list'; import { isOutdated } from '../migrations/helpers'; export const signalRulesAlertType = ({ diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts index e29487880de6b..a5793489cd8d0 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/double.ts @@ -404,9 +404,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"is in list" operator', () => { - // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent - // a double against an index that has the doubles stored as real doubles. - describe.skip('working against double values in the data set', () => { + describe('working against double values in the data set', () => { it('will return 3 results if we have a list that includes 1 double', async () => { await importFile(supertest, 'double', ['1.0'], 'list_items.txt'); const rule = getRuleForSignalTesting(['double']); @@ -545,17 +543,19 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([]); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 1 result if we have a list which contains the double range of 1.0-1.2', async () => { - await importFile(supertest, 'double_range', ['1.0-1.2'], 'list_items.txt'); + it('will return 1 result if we have a list which contains the double range of 1.0-1.2', async () => { + await importFile(supertest, 'double_range', ['1.0-1.2'], 'list_items.txt', [ + '1.0', + '1.2', + ]); const rule = getRuleForSignalTesting(['double_as_string']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ { - field: 'ip', + field: 'double', list: { id: 'list_items.txt', - type: 'ip', + type: 'double_range', }, operator: 'included', type: 'list', @@ -565,16 +565,14 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccess(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql(['1.3']); }); }); }); describe('"is not in list" operator', () => { - // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent - // a double against an index that has the doubles stored as real doubles. - describe.skip('working against double values in the data set', () => { + describe('working against double values in the data set', () => { it('will return 1 result if we have a list that excludes 1 double', async () => { await importFile(supertest, 'double', ['1.0'], 'list_items.txt'); const rule = getRuleForSignalTesting(['double']); @@ -715,17 +713,19 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 3 results if we have a list which contains the double range of 1.0-1.2', async () => { - await importFile(supertest, 'double_range', ['1.0-1.2'], 'list_items.txt'); + it('will return 3 results if we have a list which contains the double range of 1.0-1.2', async () => { + await importFile(supertest, 'double_range', ['1.0-1.2'], 'list_items.txt', [ + '1.0', + '1.2', + ]); const rule = getRuleForSignalTesting(['double_as_string']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ { - field: 'ip', + field: 'double', list: { id: 'list_items.txt', - type: 'ip', + type: 'double_range', }, operator: 'excluded', type: 'list', @@ -733,9 +733,9 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccess(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.double).sort(); expect(hits).to.eql(['1.0', '1.1', '1.2']); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts index d68f0f6a69277..955d27c086466 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/float.ts @@ -404,9 +404,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"is in list" operator', () => { - // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent - // a float against an index that has the floats stored as real floats. - describe.skip('working against float values in the data set', () => { + describe('working against float values in the data set', () => { it('will return 3 results if we have a list that includes 1 float', async () => { await importFile(supertest, 'float', ['1.0'], 'list_items.txt'); const rule = getRuleForSignalTesting(['float']); @@ -545,17 +543,16 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([]); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 1 result if we have a list which contains the float range of 1.0-1.2', async () => { - await importFile(supertest, 'float_range', ['1.0-1.2'], 'list_items.txt'); + it('will return 1 result if we have a list which contains the float range of 1.0-1.2', async () => { + await importFile(supertest, 'float_range', ['1.0-1.2'], 'list_items.txt', ['1.0', '1.2']); const rule = getRuleForSignalTesting(['float_as_string']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ { - field: 'ip', + field: 'float', list: { id: 'list_items.txt', - type: 'ip', + type: 'float_range', }, operator: 'included', type: 'list', @@ -565,16 +562,14 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccess(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql(['1.3']); }); }); }); describe('"is not in list" operator', () => { - // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent - // a float against an index that has the floats stored as real floats. - describe.skip('working against float values in the data set', () => { + describe('working against float values in the data set', () => { it('will return 1 result if we have a list that excludes 1 float', async () => { await importFile(supertest, 'float', ['1.0'], 'list_items.txt'); const rule = getRuleForSignalTesting(['float']); @@ -715,17 +710,16 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql(['1.0', '1.1', '1.2', '1.3']); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 3 results if we have a list which contains the float range of 1.0-1.2', async () => { - await importFile(supertest, 'float_range', ['1.0-1.2'], 'list_items.txt'); + it('will return 3 results if we have a list which contains the float range of 1.0-1.2', async () => { + await importFile(supertest, 'float_range', ['1.0-1.2'], 'list_items.txt', ['1.0', '1.2']); const rule = getRuleForSignalTesting(['float_as_string']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ { - field: 'ip', + field: 'float', list: { id: 'list_items.txt', - type: 'ip', + type: 'float_range', }, operator: 'excluded', type: 'list', @@ -733,9 +727,9 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccess(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.float).sort(); expect(hits).to.eql(['1.0', '1.1', '1.2']); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts index 0fbb97d284429..6b32eb19c83d9 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts @@ -16,8 +16,11 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./float')); loadTestFile(require.resolve('./integer')); loadTestFile(require.resolve('./ip')); + loadTestFile(require.resolve('./ip_array')); loadTestFile(require.resolve('./keyword')); + loadTestFile(require.resolve('./keyword_array')); loadTestFile(require.resolve('./long')); loadTestFile(require.resolve('./text')); + loadTestFile(require.resolve('./text_array')); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts index 9b38f0f7cbb42..a1275afe288bf 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/integer.ts @@ -404,9 +404,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"is in list" operator', () => { - // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent - // a integer against an index that has the integers stored as real integers. - describe.skip('working against integer values in the data set', () => { + describe('working against integer values in the data set', () => { it('will return 3 results if we have a list that includes 1 integer', async () => { await importFile(supertest, 'integer', ['1'], 'list_items.txt'); const rule = getRuleForSignalTesting(['integer']); @@ -545,17 +543,16 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([]); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 1 result if we have a list which contains the integer range of 1-3', async () => { - await importFile(supertest, 'integer_range', ['1-3'], 'list_items.txt'); + it('will return 1 result if we have a list which contains the integer range of 1-3', async () => { + await importFile(supertest, 'integer_range', ['1-3'], 'list_items.txt', ['1', '2']); const rule = getRuleForSignalTesting(['integer_as_string']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ { - field: 'ip', + field: 'integer', list: { id: 'list_items.txt', - type: 'ip', + type: 'integer_range', }, operator: 'included', type: 'list', @@ -565,16 +562,14 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccess(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql(['4']); }); }); }); describe('"is not in list" operator', () => { - // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent - // a integer against an index that has the integers stored as real integers. - describe.skip('working against integer values in the data set', () => { + describe('working against integer values in the data set', () => { it('will return 1 result if we have a list that excludes 1 integer', async () => { await importFile(supertest, 'integer', ['1'], 'list_items.txt'); const rule = getRuleForSignalTesting(['integer']); @@ -715,17 +710,16 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql(['1', '2', '3', '4']); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 3 results if we have a list which contains the integer range of 1-3', async () => { - await importFile(supertest, 'integer_range', ['1-3'], 'list_items.txt'); + it('will return 3 results if we have a list which contains the integer range of 1-3', async () => { + await importFile(supertest, 'integer_range', ['1-3'], 'list_items.txt', ['1', '2', '3']); const rule = getRuleForSignalTesting(['integer_as_string']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ { - field: 'ip', + field: 'integer', list: { id: 'list_items.txt', - type: 'ip', + type: 'integer_range', }, operator: 'excluded', type: 'list', @@ -733,9 +727,9 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccess(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.integer).sort(); expect(hits).to.eql(['1', '2', '3']); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts index c3537efc12de7..311354c63ca4a 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip.ts @@ -180,7 +180,7 @@ export default ({ getService }: FtrProviderContext) => { expect(ips).to.eql([]); }); - it('should filter a CIDR range of 127.0.0.1/30', async () => { + it('should filter a CIDR range of "127.0.0.1/30"', async () => { const rule = getRuleForSignalTesting(['ip']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -494,9 +494,12 @@ export default ({ getService }: FtrProviderContext) => { expect(ips).to.eql([]); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 1 result if we have a list which contains the CIDR range of 127.0.0.1/30', async () => { - await importFile(supertest, 'ip_range', ['127.0.0.1/30'], 'list_items.txt'); + it('will return 1 result if we have a list which contains the CIDR range of "127.0.0.1/30"', async () => { + await importFile(supertest, 'ip_range', ['127.0.0.1/30'], 'list_items.txt', [ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + ]); const rule = getRuleForSignalTesting(['ip']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -504,7 +507,63 @@ export default ({ getService }: FtrProviderContext) => { field: 'ip', list: { id: 'list_items.txt', - type: 'ip', + type: 'ip_range', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.4']); + }); + + it('will return 1 result if we have a list which contains the range syntax of "127.0.0.1-127.0.0.3"', async () => { + await importFile(supertest, 'ip_range', ['127.0.0.1-127.0.0.3'], 'list_items.txt', [ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + ]); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip_range', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.4']); + }); + + it('will return 1 result if we have a list which contains the range mixed syntax of "127.0.0.1/32,127.0.0.2-127.0.0.3"', async () => { + await importFile( + supertest, + 'ip_range', + ['127.0.0.1/32', '127.0.0.2-127.0.0.3'], + 'list_items.txt', + ['127.0.0.1', '127.0.0.2', '127.0.0.3'] + ); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip_range', }, operator: 'included', type: 'list', @@ -594,9 +653,12 @@ export default ({ getService }: FtrProviderContext) => { expect(ips).to.eql(['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4']); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 3 results if we have a list which contains the CIDR range of 127.0.0.1/30', async () => { - await importFile(supertest, 'ip_range', ['127.0.0.1/30'], 'list_items.txt'); + it('will return 3 results if we have a list which contains the CIDR range of "127.0.0.1/30"', async () => { + await importFile(supertest, 'ip_range', ['127.0.0.1/30'], 'list_items.txt', [ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + ]); const rule = getRuleForSignalTesting(['ip']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -604,9 +666,36 @@ export default ({ getService }: FtrProviderContext) => { field: 'ip', list: { id: 'list_items.txt', - type: 'ip', + type: 'ip_range', }, - operator: 'included', + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql(['127.0.0.1', '127.0.0.2', '127.0.0.3']); + }); + + it('will return 3 results if we have a list which contains the range syntax of "127.0.0.1-127.0.0.3"', async () => { + await importFile(supertest, 'ip_range', ['127.0.0.1-127.0.0.3'], 'list_items.txt', [ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + ]); + const rule = getRuleForSignalTesting(['ip']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip_range', + }, + operator: 'excluded', type: 'list', }, ], diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts new file mode 100644 index 0000000000000..8f4827ec6e71c --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts @@ -0,0 +1,735 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type ip', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/ip_as_array'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/ip_as_array'); + }); + + describe('"is" operator', () => { + it('should find all the ips from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + [], + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ['127.0.0.8', '127.0.0.9', '127.0.0.10'], + ]); + }); + + it('should filter 1 single ip if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + [], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ['127.0.0.8', '127.0.0.9', '127.0.0.10'], + ]); + }); + + it('should filter 2 ips if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.5', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); + }); + + it('should filter 3 ips if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.5', + }, + ], + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.8', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[]]); + }); + + it('should filter a CIDR range of "127.0.0.1/30"', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.1/30', // CIDR IP Range is 127.0.0.0 - 127.0.0.3 + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + [], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ['127.0.0.8', '127.0.0.9', '127.0.0.10'], + ]); + }); + + it('should filter a CIDR range of "127.0.0.4/31"', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match', + value: '127.0.0.4/31', // CIDR IP Range is 127.0.0.4 - 127.0.0.5 + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '192.168.0.1', // this value does not exist + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '127.0.0.1', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4']]); + }); + + it('will return just 1 result we excluded 2 from the same array elements', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '127.0.0.1', + }, + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '127.0.0.2', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4']]); + }); + + it('will return 0 results if we exclude two ips', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '127.0.0.1', + }, + ], + [ + { + field: 'ip', + operator: 'excluded', + type: 'match', + value: '127.0.0.5', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single ip if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match_any', + value: ['127.0.0.1'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + [], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ['127.0.0.8', '127.0.0.9', '127.0.0.10'], + ]); + }); + + it('should filter 2 ips if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match_any', + value: ['127.0.0.1', '127.0.0.5'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); + }); + + it('should filter 3 ips if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'match_any', + value: ['127.0.0.1', '127.0.0.5', '127.0.0.8'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[]]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match_any', + value: ['192.168.0.1', '192.168.0.2'], // These values do not exist + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'match_any', + value: ['127.0.0.1', '127.0.0.5'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ]); + }); + }); + + describe('"exists" operator', () => { + it('will return 1 empty result if matching against ip', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[]]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 3 results if matching against ip', async () => { + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ['127.0.0.8', '127.0.0.9', '127.0.0.10'], + ]); + }); + }); + + describe('"is in list" operator', () => { + it('will return 3 results if we have a list that includes 1 ip', async () => { + await importFile(supertest, 'ip', ['127.0.0.1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + [], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ['127.0.0.8', '127.0.0.9', '127.0.0.10'], + ]); + }); + + it('will return 2 results if we have a list that includes 2 ips', async () => { + await importFile(supertest, 'ip', ['127.0.0.1', '127.0.0.5'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); + }); + + it('will return 1 result if we have a list that includes all ips', async () => { + await importFile( + supertest, + 'ip', + ['127.0.0.1', '127.0.0.5', '127.0.0.8'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[]]); + }); + + it('will return 2 results if we have a list which contains the CIDR ranges of "127.0.0.1/32, 127.0.0.2/31, 127.0.0.4/30"', async () => { + await importFile( + supertest, + 'ip_range', + ['127.0.0.1/32', '127.0.0.2/31', '127.0.0.4/30'], + 'list_items.txt', + [ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + '127.0.0.4', + '127.0.0.5', + '127.0.0.6', + '127.0.0.7', + ] + ); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip_range', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); + }); + + it('will return 2 results if we have a list which contains the range syntax of "127.0.0.1-127.0.0.7', async () => { + await importFile(supertest, 'ip_range', ['127.0.0.1-127.0.0.7'], 'list_items.txt', [ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + '127.0.0.4', + '127.0.0.5', + '127.0.0.6', + '127.0.0.7', + ]); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip_range', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); + }); + }); + + describe('"is not in list" operator', () => { + it('will return 1 result if we have a list that excludes 1 ip', async () => { + await importFile(supertest, 'ip', ['127.0.0.1'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4']]); + }); + + it('will return 2 results if we have a list that excludes 2 ips', async () => { + await importFile(supertest, 'ip', ['127.0.0.1', '127.0.0.5'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ]); + }); + + it('will return 3 results if we have a list that excludes all ips', async () => { + await importFile( + supertest, + 'ip', + ['127.0.0.1', '127.0.0.5', '127.0.0.8'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ['127.0.0.8', '127.0.0.9', '127.0.0.10'], + ]); + }); + + it('will return 3 results if we have a list which contains the CIDR ranges of "127.0.0.1/32, 127.0.0.2/31, 127.0.0.4/30"', async () => { + await importFile( + supertest, + 'ip_range', + ['127.0.0.1/32', '127.0.0.2/31', '127.0.0.4/30'], + 'list_items.txt', + [ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + '127.0.0.4', + '127.0.0.5', + '127.0.0.6', + '127.0.0.7', + ] + ); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip_range', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ]); + }); + + it('will return 3 results if we have a list which contains the range syntax of "127.0.0.1-127.0.0.7"', async () => { + await importFile(supertest, 'ip_range', ['127.0.0.1-127.0.0.7'], 'list_items.txt', [ + '127.0.0.1', + '127.0.0.2', + '127.0.0.3', + '127.0.0.4', + '127.0.0.5', + '127.0.0.6', + '127.0.0.7', + ]); + const rule = getRuleForSignalTesting(['ip_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'ip', + list: { + id: 'list_items.txt', + type: 'ip_range', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const ips = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + expect(ips).to.eql([ + ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'], + ['127.0.0.5', null, '127.0.0.6', '127.0.0.7'], + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts index 0c227c9acc38c..e4e80cb1b65ea 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword.ts @@ -402,6 +402,39 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"is in list" operator', () => { + it('will return 4 results if we have two lists with an AND contradiction keyword === "word one" AND keyword === "word two"', async () => { + await importFile(supertest, 'keyword', ['word one'], 'list_items_1.txt'); + await importFile(supertest, 'keyword', ['word two'], 'list_items_2.txt'); + const rule = getRuleForSignalTesting(['keyword']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items_1.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + { + field: 'keyword', + list: { + id: 'list_items_2.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 4, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql(['word four', 'word one', 'word three', 'word two']); + }); + it('will return 3 results if we have a list that includes 1 keyword', async () => { await importFile(supertest, 'keyword', ['word one'], 'list_items.txt'); const rule = getRuleForSignalTesting(['keyword']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts new file mode 100644 index 0000000000000..01e301c350851 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts @@ -0,0 +1,624 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type keyword', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/keyword_as_array'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/keyword_as_array'); + }); + + describe('"is" operator', () => { + it('should find all the keyword from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + + it('should filter 1 single keyword if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ]); + }); + + it('should filter 2 keyword if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word seven', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); + }); + + it('should filter 3 keyword if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word six', + }, + ], + [ + { + field: 'keyword', + operator: 'included', + type: 'match', + value: 'word nine', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([[]]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match', + value: '500.0', // this value is not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match', + value: 'word one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([['word one', 'word two', 'word three', 'word four']]); + }); + + it('will return 0 results if we exclude two keyword', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match', + value: 'word five', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single keyword if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match_any', + value: ['word one'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ]); + }); + + it('should filter 2 keyword if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match_any', + value: ['word one', 'word six'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); + }); + + it('should filter 3 keyword if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'match_any', + value: ['word one', 'word five', 'word eight'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([[]]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match_any', + value: ['500', '600'], // both these values are not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'match_any', + value: ['word one', 'word six'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + }); + + describe('"exists" operator', () => { + it('will return 1 results if matching against keyword for the empty array', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([[]]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 3 results if matching against keyword', async () => { + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + }); + + describe('"is in list" operator', () => { + it('will return 4 results if we have two lists with an AND contradiction keyword === "word one" AND keyword === "word five"', async () => { + await importFile(supertest, 'keyword', ['word one'], 'list_items_1.txt'); + await importFile(supertest, 'keyword', ['word five'], 'list_items_2.txt'); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items_1.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + { + field: 'keyword', + list: { + id: 'list_items_2.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + + it('will return 3 results if we have two lists with an AND keyword === "word one" AND keyword === "word two" since we have an array', async () => { + await importFile(supertest, 'keyword', ['word one'], 'list_items_1.txt'); + await importFile(supertest, 'keyword', ['word two'], 'list_items_2.txt'); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items_1.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + { + field: 'keyword', + list: { + id: 'list_items_2.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ]); + }); + + it('will return 3 results if we have a list that includes 1 keyword', async () => { + await importFile(supertest, 'keyword', ['word one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ]); + }); + + it('will return 2 results if we have a list that includes 2 keyword', async () => { + await importFile(supertest, 'keyword', ['word one', 'word six'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); + }); + + it('will return only the empty array for results if we have a list that includes all keyword', async () => { + await importFile( + supertest, + 'keyword', + ['word one', 'word five', 'word eight'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([[]]); + }); + }); + + describe('"is not in list" operator', () => { + it('will return 1 result if we have a list that excludes 1 keyword', async () => { + await importFile(supertest, 'keyword', ['word one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([['word one', 'word two', 'word three', 'word four']]); + }); + + it('will return 1 result if we have a list that excludes 1 keyword but repeat 2 elements from the array in the list', async () => { + await importFile(supertest, 'keyword', ['word one', 'word two'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([['word one', 'word two', 'word three', 'word four']]); + }); + + it('will return 2 results if we have a list that excludes 2 keyword', async () => { + await importFile(supertest, 'keyword', ['word one', 'word five'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + + it('will return 3 results if we have a list that excludes 3 items', async () => { + await importFile( + supertest, + 'keyword', + ['word one', 'word six', 'word ten'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['keyword_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'keyword', + list: { + id: 'list_items.txt', + type: 'keyword', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.keyword).sort(); + expect(hits).to.eql([ + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts index 5c110996c2198..ee52c41bc78e8 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/long.ts @@ -404,9 +404,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"is in list" operator', () => { - // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent - // a long against an index that has the longs stored as real longs. - describe.skip('working against long values in the data set', () => { + describe('working against long values in the data set', () => { it('will return 3 results if we have a list that includes 1 long', async () => { await importFile(supertest, 'long', ['1'], 'list_items.txt'); const rule = getRuleForSignalTesting(['long']); @@ -545,17 +543,16 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([]); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 1 result if we have a list which contains the long range of 1-3', async () => { - await importFile(supertest, 'long_range', ['1-3'], 'list_items.txt'); + it('will return 1 result if we have a list which contains the long range of 1-3', async () => { + await importFile(supertest, 'long_range', ['1-3'], 'list_items.txt', ['1', '2', '3']); const rule = getRuleForSignalTesting(['long_as_string']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ { - field: 'ip', + field: 'long', list: { id: 'list_items.txt', - type: 'ip', + type: 'long_range', }, operator: 'included', type: 'list', @@ -565,16 +562,14 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccess(supertest, id); await waitForSignalsToBePresent(supertest, 1, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql(['4']); }); }); }); describe('"is not in list" operator', () => { - // TODO: Enable this test once the bugs are fixed, we cannot use a list of strings that represent - // a long against an index that has the longs stored as real longs. - describe.skip('working against long values in the data set', () => { + describe('working against long values in the data set', () => { it('will return 1 result if we have a list that excludes 1 long', async () => { await importFile(supertest, 'long', ['1'], 'list_items.txt'); const rule = getRuleForSignalTesting(['long']); @@ -715,17 +710,16 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql(['1', '2', '3', '4']); }); - // TODO: Fix this bug and then unskip this test - it.skip('will return 3 results if we have a list which contains the long range of 1-3', async () => { - await importFile(supertest, 'long_range', ['1-3'], 'list_items.txt'); + it('will return 3 results if we have a list which contains the long range of 1-3', async () => { + await importFile(supertest, 'long_range', ['1-3'], 'list_items.txt', ['1', '2', '3']); const rule = getRuleForSignalTesting(['long_as_string']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ { - field: 'ip', + field: 'long', list: { id: 'list_items.txt', - type: 'ip', + type: 'long_range', }, operator: 'excluded', type: 'list', @@ -733,9 +727,9 @@ export default ({ getService }: FtrProviderContext) => { ], ]); await waitForRuleSuccess(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); + await waitForSignalsToBePresent(supertest, 3, [id]); const signalsOpen = await getSignalsById(supertest, id); - const hits = signalsOpen.hits.hits.map((hit) => hit._source.ip).sort(); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.long).sort(); expect(hits).to.eql(['1', '2', '3']); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts index d2066b1023d3c..095d885149389 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text.ts @@ -592,10 +592,37 @@ export default ({ getService }: FtrProviderContext) => { }); }); - // TODO: Unskip these once this is fixed - describe.skip('working against text values with spaces', () => { + describe('working against text values with spaces', () => { it('will return 3 results if we have a list that includes 1 text', async () => { - await importFile(supertest, 'text', ['one'], 'list_items.txt'); + await importTextFile(supertest, 'text', ['word one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word four', 'word three', 'word two']); + }); + + it('will return 3 results if we have a list that includes 1 text with additional wording', async () => { + await importTextFile( + supertest, + 'text', + ['word one additional wording'], + 'list_items.txt' + ); const rule = getRuleForSignalTesting(['text']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -618,7 +645,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('will return 2 results if we have a list that includes 2 text', async () => { - await importFile(supertest, 'text', ['one', 'three'], 'list_items.txt'); + await importFile(supertest, 'text', ['word one', 'word three'], 'list_items.txt'); const rule = getRuleForSignalTesting(['text']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -644,7 +671,7 @@ export default ({ getService }: FtrProviderContext) => { await importTextFile( supertest, 'text', - ['one', 'two', 'three', 'four'], + ['word one', 'word two', 'word three', 'word four'], 'list_items.txt' ); const rule = getRuleForSignalTesting(['text']); @@ -746,10 +773,37 @@ export default ({ getService }: FtrProviderContext) => { }); }); - // TODO: Unskip these once this is fixed - describe.skip('working against text values with spaces', () => { + describe('working against text values with spaces', () => { it('will return 1 result if we have a list that excludes 1 text', async () => { - await importTextFile(supertest, 'text', ['one'], 'list_items.txt'); + await importTextFile(supertest, 'text', ['word one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql(['word one']); + }); + + it('will return 1 result if we have a list that excludes 1 text with additional wording', async () => { + await importTextFile( + supertest, + 'text', + ['word one additional wording'], + 'list_items.txt' + ); const rule = getRuleForSignalTesting(['text']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -772,7 +826,7 @@ export default ({ getService }: FtrProviderContext) => { }); it('will return 2 results if we have a list that excludes 2 text', async () => { - await importTextFile(supertest, 'text', ['one', 'three'], 'list_items.txt'); + await importTextFile(supertest, 'text', ['word one', 'word three'], 'list_items.txt'); const rule = getRuleForSignalTesting(['text']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -798,7 +852,7 @@ export default ({ getService }: FtrProviderContext) => { await importTextFile( supertest, 'text', - ['one', 'two', 'three', 'four'], + ['word one', 'word two', 'word three', 'word four'], 'list_items.txt' ); const rule = getRuleForSignalTesting(['text']); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts new file mode 100644 index 0000000000000..ed63f1a0db25f --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts @@ -0,0 +1,619 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { + createListsIndex, + deleteAllExceptions, + deleteListsIndex, + importFile, +} from '../../../../lists_api_integration/utils'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + createRule, + createRuleWithExceptionEntries, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsById, + waitForRuleSuccess, + waitForSignalsToBePresent, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('Rule exception operators for data type text', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await createListsIndex(supertest); + await esArchiver.load('rule_exceptions/text_as_array'); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await deleteAllExceptions(es); + await deleteListsIndex(supertest); + await esArchiver.unload('rule_exceptions/text_as_array'); + }); + + describe('"is" operator', () => { + it('should find all the text from the data set when no exceptions are set on the rule', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + + it('should filter 1 single text if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ]); + }); + + it('should filter 2 text if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word seven', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); + }); + + it('should filter 3 text if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word six', + }, + ], + [ + { + field: 'text', + operator: 'included', + type: 'match', + value: 'word nine', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([[]]); + }); + }); + + describe('"is not" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: '500.0', // this value is not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + + it('will return just 1 result we excluded', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'word one', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([['word one', 'word two', 'word three', 'word four']]); + }); + + it('will return 0 results if we exclude two text', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'word one', + }, + ], + [ + { + field: 'text', + operator: 'excluded', + type: 'match', + value: 'word five', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + }); + + describe('"is one of" operator', () => { + it('should filter 1 single text if it is set as an exception', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match_any', + value: ['word one'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ]); + }); + + it('should filter 2 text if both are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match_any', + value: ['word one', 'word six'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); + }); + + it('should filter 3 text if all 3 are set as exceptions', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'match_any', + value: ['word one', 'word five', 'word eight'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([[]]); + }); + }); + + describe('"is not one of" operator', () => { + it('will return 0 results if it cannot find what it is excluding', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match_any', + value: ['500', '600'], // both these values are not in the data set + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([]); + }); + + it('will return just the result we excluded', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'match_any', + value: ['word one', 'word six'], + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + }); + + describe('"exists" operator', () => { + it('will return 1 results if matching against text for the empty array', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'included', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([[]]); + }); + }); + + describe('"does not exist" operator', () => { + it('will return 3 results if matching against text', async () => { + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + operator: 'excluded', + type: 'exists', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + }); + + describe('"is in list" operator', () => { + it('will return 4 results if we have two lists with an AND contradiction text === "word one" AND text === "word five"', async () => { + await importFile(supertest, 'text', ['word one'], 'list_items_1.txt'); + await importFile(supertest, 'text', ['word five'], 'list_items_2.txt'); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items_1.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + { + field: 'text', + list: { + id: 'list_items_2.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + + it('will return 3 results if we have two lists with an AND text === "word one" AND text === "word two" since we have an array', async () => { + await importFile(supertest, 'text', ['word one'], 'list_items_1.txt'); + await importFile(supertest, 'text', ['word two'], 'list_items_2.txt'); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items_1.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + { + field: 'text', + list: { + id: 'list_items_2.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ]); + }); + + it('will return 3 results if we have a list that includes 1 text', async () => { + await importFile(supertest, 'text', ['word one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + [], + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ]); + }); + + it('will return 2 results if we have a list that includes 2 text', async () => { + await importFile(supertest, 'text', ['word one', 'word six'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); + }); + + it('will return only the empty array for results if we have a list that includes all text', async () => { + await importFile( + supertest, + 'text', + ['word one', 'word five', 'word eight'], + 'list_items.txt' + ); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'included', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([[]]); + }); + }); + + describe('"is not in list" operator', () => { + it('will return 1 result if we have a list that excludes 1 text', async () => { + await importFile(supertest, 'text', ['word one'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([['word one', 'word two', 'word three', 'word four']]); + }); + + it('will return 1 result if we have a list that excludes 1 text but repeat 2 elements from the array in the list', async () => { + await importFile(supertest, 'text', ['word one', 'word two'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([['word one', 'word two', 'word three', 'word four']]); + }); + + it('will return 2 results if we have a list that excludes 2 text', async () => { + await importFile(supertest, 'text', ['word one', 'word five'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 2, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + + it('will return 3 results if we have a list that excludes 3 items', async () => { + await importFile(supertest, 'text', ['word one', 'word six', 'word ten'], 'list_items.txt'); + const rule = getRuleForSignalTesting(['text_as_array']); + const { id } = await createRuleWithExceptionEntries(supertest, rule, [ + [ + { + field: 'text', + list: { + id: 'list_items.txt', + type: 'text', + }, + operator: 'excluded', + type: 'list', + }, + ], + ]); + await waitForRuleSuccess(supertest, id); + await waitForSignalsToBePresent(supertest, 3, [id]); + const signalsOpen = await getSignalsById(supertest, id); + const hits = signalsOpen.hits.hits.map((hit) => hit._source.text).sort(); + expect(hits).to.eql([ + ['word eight', 'word nine', 'word ten'], + ['word five', null, 'word six', 'word seven'], + ['word one', 'word two', 'word three', 'word four'], + ]); + }); + }); + }); +}; diff --git a/x-pack/test/functional/es_archives/rule_exceptions/ip_as_array/data.json b/x-pack/test/functional/es_archives/rule_exceptions/ip_as_array/data.json new file mode 100644 index 0000000000000..4a4316a05b2d8 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/ip_as_array/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "ip_as_array", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "ip": [] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "ip_as_array", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "ip": ["127.0.0.1", "127.0.0.2", "127.0.0.3", "127.0.0.4"] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "ip_as_array", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "ip": ["127.0.0.5", null, "127.0.0.6", "127.0.0.7"] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "ip_as_array", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "ip": ["127.0.0.8", "127.0.0.9", "127.0.0.10"] + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/ip_as_array/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/ip_as_array/mappings.json new file mode 100644 index 0000000000000..c46b79ce11381 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/ip_as_array/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "ip_as_array", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "ip": { "type": "ip" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/keyword_as_array/data.json b/x-pack/test/functional/es_archives/rule_exceptions/keyword_as_array/data.json new file mode 100644 index 0000000000000..2c51d4cbc63c3 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/keyword_as_array/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "keyword_as_array", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "keyword": [] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "keyword_as_array", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "keyword": ["word one", "word two", "word three", "word four"] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "keyword_as_array", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "keyword": ["word five", null, "word six", "word seven"] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "keyword_as_array", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "keyword": ["word eight", "word nine", "word ten"] + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/keyword_as_array/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/keyword_as_array/mappings.json new file mode 100644 index 0000000000000..df62e96aecfc9 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/keyword_as_array/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "keyword_as_array", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "keyword": { "type": "keyword" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/text_as_array/data.json b/x-pack/test/functional/es_archives/rule_exceptions/text_as_array/data.json new file mode 100644 index 0000000000000..228132cda90d2 --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/text_as_array/data.json @@ -0,0 +1,51 @@ +{ + "type": "doc", + "value": { + "id": "1", + "index": "text_as_array", + "source": { + "@timestamp": "2020-10-28T05:00:53.000Z", + "text": [] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "2", + "index": "text_as_array", + "source": { + "@timestamp": "2020-10-28T05:01:53.000Z", + "text": ["word one", "word two", "word three", "word four"] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "3", + "index": "text_as_array", + "source": { + "@timestamp": "2020-10-28T05:02:53.000Z", + "text": ["word five", null, "word six", "word seven"] + }, + "type": "_doc" + } +} + +{ + "type": "doc", + "value": { + "id": "4", + "index": "text_as_array", + "source": { + "@timestamp": "2020-10-28T05:03:53.000Z", + "text": ["word eight", "word nine", "word ten"] + }, + "type": "_doc" + } +} diff --git a/x-pack/test/functional/es_archives/rule_exceptions/text_as_array/mappings.json b/x-pack/test/functional/es_archives/rule_exceptions/text_as_array/mappings.json new file mode 100644 index 0000000000000..b0a3885da991f --- /dev/null +++ b/x-pack/test/functional/es_archives/rule_exceptions/text_as_array/mappings.json @@ -0,0 +1,20 @@ +{ + "type": "index", + "value": { + "index": "text_as_array", + "mappings": { + "properties": { + "@timestamp": { + "type": "date" + }, + "keyword": { "type": "text" } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 53472b459b8ac..c008f4e1a4e57 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -175,12 +175,14 @@ export const deleteAllExceptions = async (es: Client): Promise => { * @param type The type to import as * @param contents The contents of the import * @param fileName filename to import as + * @param testValues Optional test values in case you're using CIDR or range based lists */ export const importFile = async ( supertest: SuperTest, type: Type, contents: string[], - fileName: string + fileName: string, + testValues?: string[] ): Promise => { await supertest .post(`${LIST_ITEM_URL}/_import?type=${type}`) @@ -191,7 +193,8 @@ export const importFile = async ( // although we have pushed the list and its items, it is async so we // have to wait for the contents before continuing - await waitForListItems(supertest, contents, fileName); + const testValuesOrContents = testValues ?? contents; + await waitForListItems(supertest, testValuesOrContents, fileName); }; /** From e5c7134925bb9bebdb6294930c5cd486f3de14eb Mon Sep 17 00:00:00 2001 From: Justin Ibarra Date: Fri, 11 Dec 2020 07:45:44 +0100 Subject: [PATCH 04/37] [Detection Rules] Add 7.11 rules (#85506) --- ...tion_added_to_google_workspace_domain.json | 35 ++ ...tempt_to_deactivate_okta_network_zone.json | 36 ++ .../attempt_to_delete_okta_network_zone.json | 36 ++ ...cobalt_strike_default_teamserver_cert.json | 58 +++ ...ommand_and_control_common_webservices.json | 44 ++ ..._control_dns_directly_to_the_internet.json | 4 +- ...nd_and_control_dns_tunneling_nslookup.json | 51 ++ ...control_encrypted_channel_freesslcert.json | 44 ++ ...fer_protocol_activity_to_the_internet.json | 4 +- .../command_and_control_iexplore_via_com.json | 44 ++ ...hat_protocol_activity_to_the_internet.json | 4 +- ...ol_port_8000_activity_to_the_internet.json | 4 +- ...l_proxy_port_activity_to_the_internet.json | 4 +- ...te_desktop_protocol_from_the_internet.json | 4 +- ...ol_remote_file_copy_desktopimgdownldr.json | 4 +- ...and_control_remote_file_copy_mpcmdrun.json | 4 +- ...d_control_remote_file_copy_powershell.json | 59 +++ ..._and_control_remote_file_copy_scripts.json | 44 ++ ...mand_and_control_smtp_to_the_internet.json | 4 +- ..._server_port_activity_to_the_internet.json | 4 +- ...ol_ssh_secure_shell_from_the_internet.json | 4 +- ...trol_ssh_secure_shell_to_the_internet.json | 4 +- ..._control_tor_activity_to_the_internet.json | 29 +- ...l_network_computing_from_the_internet.json | 4 +- ...ual_network_computing_to_the_internet.json | 4 +- ...l_access_attempted_bypass_of_okta_mfa.json | 6 +- ...mpts_to_brute_force_okta_user_account.json | 4 +- .../credential_access_cmdline_dump_tool.json | 47 ++ ...ess_copy_ntds_sam_volshadowcp_cmdline.json | 48 ++ ...ial_access_credential_dumping_msbuild.json | 4 +- ...credential_access_dump_registry_hives.json | 47 ++ ...ntial_access_iis_apppoolsa_pwd_appcmd.json | 8 +- ..._access_iis_connectionstrings_dumping.json | 8 +- ..._access_kerberoasting_unusual_process.json | 47 ++ ...ial_access_lsass_memdump_file_created.json | 48 ++ ..._365_brute_force_user_account_attempt.json | 52 ++ ...65_potential_password_spraying_attack.json | 52 ++ ...okta_brute_force_or_password_spraying.json | 4 +- ...ntial_access_potential_ssh_bruteforce.json | 51 ++ ...al_access_promt_for_pwd_via_osascript.json | 48 ++ ...evasion_attempt_del_quarantine_attrib.json | 47 ++ ..._base32_encoding_or_decoding_activity.json | 14 +- ..._base64_encoding_or_decoding_activity.json | 14 +- ...e_evasion_clearing_windows_event_logs.json | 6 +- ...e_evasion_deleting_websvr_access_logs.json | 47 ++ ...vasion_dotnet_compiler_parent_process.json | 8 +- ...evasion_enable_inbound_rdp_with_netsh.json | 44 ++ ...ense_evasion_execution_lolbas_wuauclt.json | 47 ++ ...ion_execution_msbuild_started_renamed.json | 4 +- ...execution_suspicious_explorer_winword.json | 4 +- ...ion_hex_encoding_or_decoding_activity.json | 14 +- ...sion_hide_encoded_executable_registry.json | 44 ++ ...ense_evasion_iis_httplogging_disabled.json | 4 +- .../defense_evasion_log_files_deleted.json | 47 ++ ...querading_as_elastic_endpoint_process.json | 8 +- ...e_evasion_masquerading_renamed_autoit.json | 10 +- ...vasion_masquerading_trusted_directory.json | 44 ++ ...defense_evasion_masquerading_werfault.json | 15 +- ...osoft_365_exchange_dlp_policy_removed.json | 52 ++ ...change_malware_filter_policy_deletion.json | 51 ++ ..._365_exchange_malware_filter_rule_mod.json | 52 ++ ...65_exchange_safe_attach_rule_disabled.json | 51 ++ ...vasion_port_forwarding_added_registry.json | 47 ++ ...evasion_potential_processherpaderping.json | 47 ++ ...cess_termination_followed_by_deletion.json | 43 ++ ...defense_evasion_rundll32_no_arguments.json | 4 +- ...ion_scheduledjobs_at_protocol_enabled.json | 47 ++ ..._evasion_sdelete_like_filename_rename.json | 8 +- ...vasion_stop_process_service_threshold.json | 48 ++ ...ser_password_reset_or_unlock_attempts.json | 4 +- ...evasion_suspicious_powershell_imgload.json | 44 ++ ...evasion_suspicious_zoom_child_process.json | 23 +- .../defense_evasion_timestomp_touch.json | 46 ++ .../defense_evasion_unusual_dir_ads.json | 43 ++ .../discovery_adfind_command_activity.json | 77 +++ .../discovery_admin_recon.json | 48 ++ .../discovery_file_dir_discovery.json | 43 ++ .../prepackaged_rules/discovery_net_view.json | 48 ++ .../discovery_peripheral_device.json | 43 ++ .../discovery_query_registry_via_reg.json | 43 ++ ...ote_system_discovery_commands_windows.json | 43 ++ .../discovery_security_software_wmic.json | 44 ++ ...d_to_google_workspace_trusted_domains.json | 35 ++ ...n_command_shell_started_by_powershell.json | 14 +- .../execution_command_shell_via_rundll32.json | 49 ++ .../execution_from_unusual_directory.json | 27 + .../execution_from_unusual_path_cmdline.json | 28 + ...ing_osascript_exec_followed_by_netcon.json | 63 +++ ...ution_scripts_process_started_via_wmi.json | 49 ++ ...xecution_shared_modules_local_sxs_dll.json | 48 ++ ...n_shell_execution_via_apple_scripting.json | 48 ++ ...n_suspicious_image_load_wmi_ms_office.json | 46 ++ ...on_suspicious_ms_office_child_process.json | 12 +- .../execution_suspicious_psexesvc.json | 8 +- ...ecution_suspicious_short_program_name.json | 27 + ...ution_unusual_dns_service_file_writes.json | 4 +- ...usual_network_connection_via_rundll32.json | 11 +- ...explorer_suspicious_child_parent_args.json | 54 ++ .../execution_via_system_manager.json | 14 +- ..._365_exchange_transport_rule_creation.json | 52 ++ ...osoft_365_exchange_transport_rule_mod.json | 53 ++ .../exfiltration_winrar_encryption.json | 46 ++ .../google_workspace_admin_role_deletion.json | 35 ++ ...le_workspace_mfa_enforcement_disabled.json | 35 ++ .../google_workspace_policy_modified.json | 32 ++ ...pact_attempt_to_revoke_okta_api_token.json | 4 +- .../impact_possible_okta_dos_attack.json | 6 +- .../rules/prepackaged_rules/index.ts | 480 ++++++++++++++---- ...tack_via_azure_registered_application.json | 6 +- ...5_exchange_anti_phish_policy_deletion.json | 52 ++ ...soft_365_exchange_anti_phish_rule_mod.json | 52 ++ ...osoft_365_exchange_safelinks_disabled.json | 52 ++ ...mote_desktop_protocol_to_the_internet.json | 4 +- ...mote_procedure_call_from_the_internet.json | 4 +- ...remote_procedure_call_to_the_internet.json | 4 +- ...file_sharing_activity_to_the_internet.json | 4 +- ...icious_activity_reported_by_okta_user.json | 6 +- .../lateral_movement_dcom_hta.json | 47 ++ .../lateral_movement_dcom_mmc20.json | 47 ++ ...t_dcom_shellwindow_shellbrowserwindow.json | 47 ++ ...movement_executable_tool_transfer_smb.json | 44 ++ ..._movement_execution_from_tsclient_mup.json | 47 ++ ...nt_execution_via_file_shares_sequence.json | 47 ++ ...vement_incoming_winrm_shell_execution.json | 47 ++ .../lateral_movement_incoming_wmi.json | 44 ++ ...ment_mount_hidden_or_webdav_share_net.json | 44 ++ ...l_movement_powershell_remoting_target.json | 50 ++ ...lateral_movement_rdp_enabled_registry.json | 44 ++ .../lateral_movement_rdp_sharprdp_target.json | 48 ++ .../lateral_movement_rdp_tunnel_plink.json | 47 ++ ...ovement_remote_file_copy_hidden_share.json | 44 ++ .../lateral_movement_remote_services.json | 45 ++ ...ateral_movement_scheduled_task_target.json | 60 +++ .../lateral_movement_suspicious_cmd_wmi.json | 44 ++ ...ement_suspicious_rdp_client_imageload.json | 47 ++ ...l_movement_via_startup_folder_rdp_smb.json | 62 +++ ...led_for_google_workspace_organization.json | 32 ++ ...exchange_dkim_signing_config_disabled.json | 34 ++ ..._teams_custom_app_interaction_allowed.json | 34 ++ ...ttempt_to_deactivate_okta_application.json | 36 ++ ...kta_attempt_to_deactivate_okta_policy.json | 36 ++ ...tempt_to_deactivate_okta_policy_rule.json} | 13 +- ...ta_attempt_to_delete_okta_application.json | 35 ++ .../okta_attempt_to_delete_okta_policy.json | 13 +- ...ta_attempt_to_delete_okta_policy_rule.json | 36 ++ ...ta_attempt_to_modify_okta_application.json | 36 ++ ...a_attempt_to_modify_okta_network_zone.json | 11 +- .../okta_attempt_to_modify_okta_policy.json | 8 +- ...a_attempt_to_modify_okta_policy_rule.json} | 11 +- ..._or_delete_application_sign_on_policy.json | 7 +- ...threat_detected_by_okta_threatinsight.json | 6 +- ...tor_privileges_assigned_to_okta_group.json | 15 +- ...inistrator_role_assigned_to_okta_user.json | 52 ++ .../persistence_appcertdlls_registry.json | 43 ++ .../persistence_appinitdlls_registry.json | 43 ++ ...ence_attempt_to_create_okta_api_token.json | 10 +- ..._deactivate_mfa_for_okta_user_account.json | 8 +- ...nce_attempt_to_deactivate_okta_policy.json | 52 -- ...set_mfa_factors_for_okta_user_account.json | 8 +- ...ce_creation_change_launch_agents_file.json | 50 ++ ...creation_modif_launch_deamon_sequence.json | 50 ++ ...tence_evasion_registry_ifeo_injection.json | 46 ++ ...istence_folder_action_scripts_runtime.json | 63 +++ ...workspace_admin_role_assigned_to_user.json | 52 ++ ...a_domain_wide_delegation_of_authority.json | 52 ++ ...e_workspace_custom_admin_role_created.json | 52 ++ ...stence_google_workspace_role_modified.json | 52 ++ ...stence_local_scheduled_task_scripting.json | 48 ++ ...rsistence_login_logout_hooks_defaults.json | 48 ++ ...5_exchange_management_role_assignment.json | 52 ++ ...oft_365_teams_external_access_enabled.json | 51 ++ ...rosoft_365_teams_guest_access_enabled.json | 51 ++ .../persistence_ms_office_addins_file.json | 47 ++ .../persistence_ms_outlook_vba_template.json | 51 ++ ...escalation_via_accessibility_features.json | 15 +- .../persistence_registry_uncommon.json | 46 ++ ...persistence_run_key_and_startup_broad.json | 43 ++ ...ce_runtime_run_key_startup_susp_procs.json | 43 ++ .../persistence_services_registry.json | 43 ++ ...er_file_written_by_suspicious_process.json | 43 ++ ...lder_file_written_by_unsigned_process.json | 42 ++ .../persistence_startup_folder_scripts.json | 43 ++ ...stence_suspicious_com_hijack_registry.json | 46 ++ ...s_image_load_scheduled_task_ms_office.json | 47 ++ ...nce_suspicious_scheduled_task_runtime.json | 47 ++ ...e_suspicious_service_created_registry.json | 43 ++ ...sistence_via_hidden_run_key_valuename.json | 48 ++ ...sa_security_support_provider_registry.json | 43 ++ ...emetrycontroller_scheduledtask_hijack.json | 4 +- ...nt_instrumentation_event_subscription.json | 43 ++ ...on_explicit_creds_via_apple_scripting.json | 64 +++ ...e_escalation_named_pipe_impersonation.json | 46 ++ ...ation_printspooler_registry_copyfiles.json | 49 ++ ...calation_rogue_windir_environment_var.json | 46 ++ ...lege_escalation_uac_bypass_com_clipup.json | 47 ++ ...ge_escalation_uac_bypass_com_ieinstal.json | 47 ++ ...n_uac_bypass_com_interface_icmluautil.json | 44 ++ ...alation_uac_bypass_diskcleanup_hijack.json | 8 +- ...escalation_uac_bypass_dll_sideloading.json | 47 ++ ...ege_escalation_uac_bypass_mock_windir.json | 47 ++ ...scalation_uac_bypass_winfw_mmc_hijack.json | 47 ++ ...tion_unusual_parentchild_relationship.json | 12 +- ...n_unusual_svchost_childproc_childless.json | 62 +++ 203 files changed, 6911 insertions(+), 437 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/application_added_to_google_workspace_domain.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_deactivate_okta_network_zone.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_delete_okta_network_zone.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_common_webservices.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_encrypted_channel_freesslcert.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_iexplore_via_com.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_powershell.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_scripts.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cmdline_dump_tool.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_copy_ntds_sam_volshadowcp_cmdline.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dump_registry_hives.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberoasting_unusual_process.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_lsass_memdump_file_created.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_potential_ssh_bruteforce.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_promt_for_pwd_via_osascript.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_del_quarantine_attrib.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_websvr_access_logs.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_enable_inbound_rdp_with_netsh.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_lolbas_wuauclt.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hide_encoded_executable_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_log_files_deleted.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_trusted_directory.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_dlp_policy_removed.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_port_forwarding_added_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_potential_processherpaderping.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_process_termination_followed_by_deletion.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_scheduledjobs_at_protocol_enabled.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_powershell_imgload.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_timestomp_touch.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_dir_ads.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_adfind_command_activity.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_admin_recon.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_file_dir_discovery.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_view.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_peripheral_device.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_query_registry_via_reg.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_remote_system_discovery_commands_windows.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_wmic.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/domain_added_to_google_workspace_trusted_domains.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_via_rundll32.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_directory.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_path_cmdline.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripting_osascript_exec_followed_by_netcon.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripts_process_started_via_wmi.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shared_modules_local_sxs_dll.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shell_execution_via_apple_scripting.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_image_load_wmi_ms_office.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_short_program_name.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_explorer_suspicious_child_parent_args.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_creation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_mod.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_winrar_encryption.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_admin_role_deletion.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_mfa_enforcement_disabled.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_policy_modified.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_rule_mod.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_safelinks_disabled.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_hta.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_mmc20.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_shellwindow_shellbrowserwindow.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_executable_tool_transfer_smb.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_from_tsclient_mup.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_via_file_shares_sequence.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_winrm_shell_execution.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_wmi.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mount_hidden_or_webdav_share_net.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_powershell_remoting_target.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_enabled_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_sharprdp_target.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_tunnel_plink.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_file_copy_hidden_share.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_services.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_scheduled_task_target.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_cmd_wmi.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_rdp_client_imageload.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_via_startup_folder_rdp_smb.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/mfa_disabled_for_google_workspace_organization.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_exchange_dkim_signing_config_disabled.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_teams_custom_app_interaction_allowed.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_application.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy.json rename x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/{okta_attempt_to_deactivate_okta_mfa_rule.json => okta_attempt_to_deactivate_okta_policy_rule.json} (59%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_application.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy_rule.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_application.json rename x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/{okta_attempt_to_modify_okta_mfa_rule.json => okta_attempt_to_modify_okta_policy_rule.json} (58%) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_role_assigned_to_okta_user.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appcertdlls_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appinitdlls_registry.json delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_change_launch_agents_file.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_modif_launch_deamon_sequence.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_registry_ifeo_injection.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_folder_action_scripts_runtime.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_admin_role_assigned_to_user.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_custom_admin_role_created.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_role_modified.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_scripting.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_login_logout_hooks_defaults.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_exchange_management_role_assignment.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_external_access_enabled.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_guest_access_enabled.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_office_addins_file.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_outlook_vba_template.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_registry_uncommon.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_run_key_and_startup_broad.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_runtime_run_key_startup_susp_procs.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_services_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_suspicious_process.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_unsigned_process.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_scripts.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_image_load_scheduled_task_ms_office.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_scheduled_task_runtime.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_service_created_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_hidden_run_key_valuename.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_lsa_security_support_provider_registry.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_windows_management_instrumentation_event_subscription.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_apple_scripting.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_named_pipe_impersonation.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_registry_copyfiles.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_rogue_windir_environment_var.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_clipup.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_ieinstal.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_interface_icmluautil.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_dll_sideloading.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_mock_windir.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_winfw_mmc_hijack.json create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_svchost_childproc_childless.json diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/application_added_to_google_workspace_domain.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/application_added_to_google_workspace_domain.json new file mode 100644 index 0000000000000..b5b2b1994a511 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/application_added_to_google_workspace_domain.json @@ -0,0 +1,35 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when a Google marketplace application is added to the Google Workspace domain. An adversary may add a malicious application to an organization\u2019s Google Workspace domain in order to maintain a presence in their target\u2019s organization and steal data.", + "false_positives": [ + "Applications can be added to a Google Workspace domain by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Application Added to Google Workspace Domain", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:ADD_APPLICATION", + "references": [ + "https://support.google.com/a/answer/6328701?hl=en#" + ], + "risk_score": 47, + "rule_id": "785a404b-75aa-4ffd-8be5-3334a5a544dd", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_deactivate_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_deactivate_okta_network_zone.json new file mode 100644 index 0000000000000..9ccd80d65d542 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_deactivate_okta_network_zone.json @@ -0,0 +1,36 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to deactivate an Okta network zone. Okta network zones can be configured to limit or restrict access to a network based on IP addresses or geolocations. An adversary may attempt to modify, delete, or deactivate an Okta network zone in order to remove or weaken an organization's security controls.", + "false_positives": [ + "Consider adding exceptions to this rule to filter false positives if your organization's Okta network zones are regularly modified." + ], + "index": [ + "filebeat-*", + "logs-okta*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Deactivate an Okta Network Zone", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:zone.deactivate", + "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/network/network-zones.htm", + "https://developer.okta.com/docs/reference/api/system-log/", + "https://developer.okta.com/docs/reference/api/event-types/" + ], + "risk_score": 47, + "rule_id": "8a5c1e5f-ad63-481e-b53a-ef959230f7f1", + "severity": "medium", + "tags": [ + "Elastic", + "Identity", + "Okta", + "Continuous Monitoring", + "SecOps", + "Network Security" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_delete_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_delete_okta_network_zone.json new file mode 100644 index 0000000000000..541aaea36f691 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/attempt_to_delete_okta_network_zone.json @@ -0,0 +1,36 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to delete an Okta network zone. Okta network zones can be configured to limit or restrict access to a network based on IP addresses or geolocations. An adversary may attempt to modify, delete, or deactivate an Okta network zone in order to remove or weaken an organization's security controls.", + "false_positives": [ + "Consider adding exceptions to this rule to filter false positives if Oyour organization's Okta network zones are regularly deleted." + ], + "index": [ + "filebeat-*", + "logs-okta*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Delete an Okta Network Zone", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:zone.delete", + "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/network/network-zones.htm", + "https://developer.okta.com/docs/reference/api/system-log/", + "https://developer.okta.com/docs/reference/api/event-types/" + ], + "risk_score": 47, + "rule_id": "c749e367-a069-4a73-b1f2-43a3798153ad", + "severity": "medium", + "tags": [ + "Elastic", + "Identity", + "Okta", + "Continuous Monitoring", + "SecOps", + "Network Security" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json new file mode 100644 index 0000000000000..b7a36ff30bffb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_cobalt_strike_default_teamserver_cert.json @@ -0,0 +1,58 @@ +{ + "author": [ + "Elastic" + ], + "description": "This rule detects the use of the default Cobalt Strike Team Server TLS certificate. Cobalt Strike is software for Adversary Simulations and Red Team Operations which are security assessments that replicate the tactics and techniques of an advanced adversary in a network. If using Filebeat, this rule requires the Suricata or Zeek modules. Modifications to the Packetbeat configuration can be made to include MD5 and SHA256 hashing algorithms (the default is SHA1) - see the Reference section for additional information on module configuration.", + "index": [ + "filebeat-*", + "packetbeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Default Cobalt Strike Team Server Certificate", + "note": "While Cobalt Strike is intended to be used for penetration tests and IR training, it is frequently used by actual threat actors (TA) such as APT19, APT29, APT32, APT41, FIN6, DarkHydrus, CopyKittens, Cobalt Group, Leviathan, and many other unnamed criminal TAs. This rule uses high-confidence atomic indicators, alerts should be investigated rapidly.", + "query": "event.category:(network or network_traffic) and (tls.server.hash.md5:950098276A495286EB2A2556FBAB6D83 or tls.server.hash.sha1:6ECE5ECE4192683D2D84E25B0BA7E04F9CB7EB7C or tls.server.hash.sha256:87F2085C32B6A2CC709B365F55873E207A9CAA10BFFECF2FD16D3CF9D94D390C)", + "references": [ + "https://attack.mitre.org/software/S0154/", + "https://www.cobaltstrike.com/help-setup-collaboration", + "https://www.elastic.co/guide/en/beats/packetbeat/current/configuration-tls.html", + "https://www.elastic.co/guide/en/beats/filebeat/7.9/filebeat-module-suricata.html", + "https://www.elastic.co/guide/en/beats/filebeat/7.9/filebeat-module-zeek.html" + ], + "risk_score": 100, + "rule_id": "e7075e8d-a966-458e-a183-85cd331af255", + "severity": "critical", + "tags": [ + "Command and Control", + "Post-Execution", + "Threat Detection, Prevention and Hunting", + "Elastic", + "Network" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0011", + "name": "Command and Control", + "reference": "https://attack.mitre.org/tactics/TA0011/" + }, + "technique": [ + { + "id": "T1071", + "name": "Application Layer Protocol", + "reference": "https://attack.mitre.org/techniques/T1071/", + "subtechnique": [ + { + "id": "T1071.001", + "name": "Web Protocols", + "reference": "https://attack.mitre.org/techniques/T1071/001/" + } + ] + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_common_webservices.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_common_webservices.json new file mode 100644 index 0000000000000..30bdaba20b0d0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_common_webservices.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries may implement command and control communications that use common web services in order to hide their activity. This attack technique is typically targeted to an organization and uses web services common to the victim network which allows the adversary to blend into legitimate traffic. activity. These popular services are typically targeted since they have most likely been used before a compromise and allow adversaries to blend in the network.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Connection to Commonly Abused Web Services", + "query": "network where network.protocol == \"dns\" and\n /* Add new WebSvc domains here */\n wildcard(dns.question.name, \"*.githubusercontent.*\",\n \"*.pastebin.*\",\n \"*drive.google.*\",\n \"*docs.live.*\",\n \"*api.dropboxapi.*\",\n \"*dropboxusercontent.*\",\n \"*onedrive.*\",\n \"*4shared.*\",\n \"*.file.io\",\n \"*filebin.net\",\n \"*slack-files.com\",\n \"*ghostbin.*\",\n \"*ngrok.*\",\n \"*portmap.*\",\n \"*serveo.net\",\n \"*localtunnel.me\",\n \"*pagekite.me\",\n \"*localxpose.io\",\n \"*notabug.org\"\n ) and\n /* Insert noisy false positives here */\n not process.name in (\"MicrosoftEdgeCP.exe\",\n \"MicrosoftEdge.exe\",\n \"iexplore.exe\",\n \"chrome.exe\",\n \"msedge.exe\",\n \"opera.exe\",\n \"firefox.exe\",\n \"Dropbox.exe\",\n \"slack.exe\",\n \"svchost.exe\",\n \"thunderbird.exe\",\n \"outlook.exe\",\n \"OneDrive.exe\")\n", + "risk_score": 21, + "rule_id": "66883649-f908-4a5b-a1e0-54090a1d3a32", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Command and Control" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0011", + "name": "Command and Control", + "reference": "https://attack.mitre.org/tactics/TA0011/" + }, + "technique": [ + { + "id": "T1102", + "name": "Web Service", + "reference": "https://attack.mitre.org/techniques/T1102/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json index 3df567b09055a..ca9ef1b26f5b1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_directly_to_the_internet.json @@ -13,7 +13,7 @@ "language": "kuery", "license": "Elastic License", "name": "DNS Activity to the Internet", - "query": "event.category:(network or network_traffic) and (event.type:connection or type:dns) and (destination.port:53 or event.dataset:zeek.dns) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 169.254.169.254/32 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.251 or 224.0.0.252 or 255.255.255.255 or \"::1\" or \"ff02::fb\")", + "query": "event.category:(network or network_traffic) and (event.type:connection or type:dns) and (destination.port:53 or event.dataset:zeek.dns) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or 255.255.255.255 or \"::1\" or \"FE80::/10\" or \"FF00::/8\")", "references": [ "https://www.us-cert.gov/ncas/alerts/TA15-240A", "https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-81-2.pdf" @@ -45,5 +45,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json new file mode 100644 index 0000000000000..8e9822cc610a7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_dns_tunneling_nslookup.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "This rule identifies a large number (15) of nslookup.exe executions with an explicit query type from the same host. This may indicate command and control activity utilizing the DNS protocol.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential DNS Tunneling via NsLookup", + "query": "event.category:process and event.type:start and process.name:nslookup.exe and process.args:(-querytype=* or -qt=* or -q=* or -type=*)", + "references": [ + "https://unit42.paloaltonetworks.com/dns-tunneling-in-the-wild-overview-of-oilrigs-dns-tunneling/" + ], + "risk_score": 47, + "rule_id": "3a59fc81-99d3-47ea-8cd6-d48d561fca20", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Command and Control" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0011", + "name": "Command and Control", + "reference": "https://attack.mitre.org/tactics/TA0011/" + }, + "technique": [ + { + "id": "T1071", + "name": "Application Layer Protocol", + "reference": "https://attack.mitre.org/techniques/T1071/" + } + ] + } + ], + "threshold": { + "field": "host.id", + "value": 15 + }, + "type": "threshold", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_encrypted_channel_freesslcert.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_encrypted_channel_freesslcert.json new file mode 100644 index 0000000000000..63d8f155a194f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_encrypted_channel_freesslcert.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies unusual processes connecting to domains using known free SSL certificates. Adversaries may employ a known encryption algorithm to conceal command and control traffic.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Connection to Commonly Abused Free SSL Certificate Providers", + "query": "network where network.protocol == \"dns\" and\n /* Add new free SSL certificate provider domains here */\n dns.question.name : (\"*letsencrypt.org\", \"*.sslforfree.com\", \"*.zerossl.com\", \"*.freessl.org\") and\n \n /* Native Windows process paths that are unlikely to have network connections to domains secured using free SSL certificates */\n process.executable : (\"C:\\\\Windows\\\\System32\\\\*.exe\",\n \"C:\\\\Windows\\\\System\\\\*.exe\",\n\t \"C:\\\\Windows\\\\SysWOW64\\\\*.exe\",\n\t\t \"C:\\\\Windows\\\\Microsoft.NET\\\\Framework*\\\\*.exe\",\n\t\t \"C:\\\\Windows\\\\explorer.exe\",\n\t\t \"C:\\\\Windows\\\\notepad.exe\") and\n \n /* Insert noisy false positives here */\n not process.name : (\"svchost.exe\", \"MicrosoftEdge*.exe\", \"msedge.exe\")\n", + "risk_score": 21, + "rule_id": "e3cf38fa-d5b8-46cc-87f9-4a7513e4281d", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Command and Control" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0011", + "name": "Command and Control", + "reference": "https://attack.mitre.org/tactics/TA0011/" + }, + "technique": [ + { + "id": "T1573", + "name": "Encrypted Channel", + "reference": "https://attack.mitre.org/techniques/T1573/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json index c73fdf1bded9d..37b95be7a0c41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ftp_file_transfer_protocol_activity_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "FTP (File Transfer Protocol) Activity to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(20 or 21) or event.dataset:zeek.ftp) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(20 or 21) or event.dataset:zeek.ftp) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 21, "rule_id": "87ec6396-9ac4-4706-bcf0-2ebb22002f43", "severity": "low", @@ -58,5 +58,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_iexplore_via_com.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_iexplore_via_com.json new file mode 100644 index 0000000000000..b0718fc2418be --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_iexplore_via_com.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies instances of Internet Explorer (iexplore.exe) being started via the Component Object Model (COM) making unusual network connections. Adversaries could abuse Internet Explorer via COM to avoid suspicious processes making network connections and bypass host-based firewall restrictions.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Command and Control via Internet Explorer", + "query": "sequence by host.id, process.entity_id with maxspan = 1s\n [process where event.type:\"start\" and process.parent.name:\"iexplore.exe\" and process.parent.args:\"-Embedding\"]\n /* IE started via COM in normal conditions makes few connections, mainly to Microsoft and OCSP related domains, add FPs here */\n [network where network.protocol : \"dns\" and process.name:\"iexplore.exe\" and\n not wildcard(dns.question.name, \"*.microsoft.com\", \n \"*.digicert.com\", \n \"*.msocsp.com\", \n \"*.windowsupdate.com\", \n \"*.bing.com\",\n \"*.identrust.com\")\n ]\n", + "risk_score": 43, + "rule_id": "acd611f3-2b93-47b3-a0a3-7723bcc46f6d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Command and Control" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0011", + "name": "Command and Control", + "reference": "https://attack.mitre.org/tactics/TA0011/" + }, + "technique": [ + { + "id": "T1071", + "name": "Application Layer Protocol", + "reference": "https://attack.mitre.org/techniques/T1071/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json index f1901fa70def2..c29ec8c70f78f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_irc_internet_relay_chat_protocol_activity_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "IRC (Internet Relay Chat) Protocol Activity to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(6667 or 6697) or event.dataset:zeek.irc) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(6667 or 6697) or event.dataset:zeek.irc) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 47, "rule_id": "c6474c34-4953-447a-903e-9fcb7b6661aa", "severity": "medium", @@ -58,5 +58,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json index 0c35bd5e23ed5..fba51f8c0f3c0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_port_8000_activity_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "TCP Port 8000 Activity to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port:8000 and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port:8000 and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 21, "rule_id": "08d5d7e2-740f-44d8-aeda-e41f4263efaf", "severity": "low", @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json index 8535a9591b88f..3a7bf829b5364 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_proxy_port_activity_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "Proxy Port Activity to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(1080 or 3128 or 8080) or event.dataset:zeek.socks) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(1080 or 3128 or 8080) or event.dataset:zeek.socks) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 47, "rule_id": "ad0e5e75-dd89-4875-8d0a-dfdc1828b5f3", "severity": "medium", @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json index 4a3fd026f54a7..2e94a13f71795 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_rdp_remote_desktop_protocol_from_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "RDP (Remote Desktop Protocol) from the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:3389 or event.dataset:zeek.rdp) and not source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:3389 or event.dataset:zeek.rdp) and not source.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" ) and destination.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 )", "risk_score": 47, "rule_id": "8c1bdde8-4204-45c0-9e0c-c85ca3902488", "severity": "medium", @@ -73,5 +73,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json index 596c4bbac57ba..d55e2b7cc4471 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_desktopimgdownldr.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "Remote File Download via Desktopimgdownldr Utility", - "query": "event.category:process and event.type:(start or process_started) and (process.name:desktopimgdownldr.exe or process.pe.original_file_name:desktopimgdownldr.exe or winlog.event_data.OriginalFileName:desktopimgdownldr.exe) and process.args:/lockscreenurl\\:http*", + "query": "event.category:process and event.type:(start or process_started) and (process.name:desktopimgdownldr.exe or process.pe.original_file_name:desktopimgdownldr.exe) and process.args:/lockscreenurl\\:http*", "references": [ "https://labs.sentinelone.com/living-off-windows-land-a-new-native-file-downldr/" ], @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json index 9eef2fbbc62a6..8c27bbf85d567 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_mpcmdrun.json @@ -12,7 +12,7 @@ "license": "Elastic License", "name": "Remote File Download via MpCmdRun", "note": "### Investigating Remote File Download via MpCmdRun\nVerify details such as the parent process, URL reputation, and downloaded file details. Additionally, `MpCmdRun` logs this information in the Appdata Temp folder in `MpCmdRun.log`.", - "query": "event.category:process and event.type:(start or process_started) and (process.name:MpCmdRun.exe or process.pe.original_file_name:MpCmdRun.exe or winlog.event_data.OriginalFileName:MpCmdRun.exe) and process.args:((\"-DownloadFile\" or \"-downloadfile\") and \"-url\" and \"-path\")", + "query": "event.category:process and event.type:(start or process_started) and (process.name:MpCmdRun.exe or process.pe.original_file_name:MpCmdRun.exe) and process.args:((\"-DownloadFile\" or \"-downloadfile\") and \"-url\" and \"-path\")", "references": [ "https://twitter.com/mohammadaskar2/status/1301263551638761477", "https://www.bleepingcomputer.com/news/microsoft/microsoft-defender-can-ironically-be-used-to-download-malware/" @@ -45,5 +45,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_powershell.json new file mode 100644 index 0000000000000..cb9d215acfda3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_powershell.json @@ -0,0 +1,59 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies powershell.exe being used to download an executable file from an untrusted remote destination.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Remote File Download via PowerShell", + "query": "sequence by host.id, process.entity_id with maxspan=30s\n [network where process.name : \"powershell.exe\" and network.protocol == \"dns\" and\n not dns.question.name : (\"localhost\", \"*.microsoft.com\", \"*.azureedge.net\", \"*.powershellgallery.com\", \"*.windowsupdate.com\", \"metadata.google.internal\") and \n not user.domain : \"NT AUTHORITY\"]\n [file where process.name : \"powershell.exe\" and event.type == \"creation\" and file.extension : (\"exe\", \"dll\", \"ps1\", \"bat\") and \n not file.name : \"__PSScriptPolicy*.ps1\"]\n", + "risk_score": 47, + "rule_id": "33f306e8-417c-411b-965c-c2812d6d3f4d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Command and Control" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0011", + "name": "Command and Control", + "reference": "https://attack.mitre.org/tactics/TA0011/" + }, + "technique": [ + { + "id": "T1105", + "name": "Ingress Tool Transfer", + "reference": "https://attack.mitre.org/techniques/T1105/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1086", + "name": "PowerShell", + "reference": "https://attack.mitre.org/techniques/T1086/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_scripts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_scripts.json new file mode 100644 index 0000000000000..37f1364c5f61f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_remote_file_copy_scripts.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies built-in Windows script interpreters (cscript.exe or wscript.exe) being used to download an executable file from a remote destination.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Remote File Download via Script Interpreter", + "query": "sequence by host.id, process.entity_id\n [network where process.name : (\"wscript.exe\", \"cscript.exe\") and network.protocol != \"dns\" and\n network.direction == \"outgoing\" and network.type == \"ipv4\" and destination.ip != \"127.0.0.1\"\n ]\n [file where event.type == \"creation\" and file.extension : (\"exe\", \"dll\")]\n", + "risk_score": 43, + "rule_id": "1d276579-3380-4095-ad38-e596a01bc64f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Command and Control" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0011", + "name": "Command and Control", + "reference": "https://attack.mitre.org/tactics/TA0011/" + }, + "technique": [ + { + "id": "T1105", + "name": "Ingress Tool Transfer", + "reference": "https://attack.mitre.org/techniques/T1105/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json index f041255374f12..21c4d22e2af8c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_smtp_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "SMTP to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(25 or 465 or 587) or event.dataset:zeek.smtp) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(25 or 465 or 587) or event.dataset:zeek.smtp) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 21, "rule_id": "67a9beba-830d-4035-bfe8-40b7e28f8ac4", "severity": "low", @@ -58,5 +58,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json index 7e4f3907fc31e..45cfc2bc5fc3b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_sql_server_port_activity_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "SQL Traffic to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(1433 or 1521 or 3306 or 5432) or event.dataset:zeek.mysql) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(1433 or 1521 or 3306 or 5432) or event.dataset:zeek.mysql) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 47, "rule_id": "139c7458-566a-410c-a5cd-f80238d6a5cd", "severity": "medium", @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json index 08ab14aeb5c7c..95e564ff7af2c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_from_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "SSH (Secure Shell) from the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:22 or event.dataset:zeek.ssh) and not source.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\") and destination.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16)", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:22 or event.dataset:zeek.ssh) and not source.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" ) and destination.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 )", "risk_score": 47, "rule_id": "ea0784f0-a4d7-4fea-ae86-4baaf27a6f17", "severity": "medium", @@ -73,5 +73,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json index 4bc48ebe0c316..d5e0a29dd2a01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_ssh_secure_shell_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "SSH (Secure Shell) to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:22 or event.dataset:zeek.ssh) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:22 or event.dataset:zeek.ssh) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 21, "rule_id": "6f1500bc-62d7-4eb9-8601-7485e87da2f4", "severity": "low", @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json index e82106a87bc2e..014c46a09e448 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_tor_activity_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "Tor Activity to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port:(9001 or 9030) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port:(9001 or 9030) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\")", "risk_score": 47, "rule_id": "7d2c38d7-ede7-4bdf-b140-445906e6c540", "severity": "medium", @@ -38,25 +38,22 @@ "id": "T1043", "name": "Commonly Used Port", "reference": "https://attack.mitre.org/techniques/T1043/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0011", - "name": "Command and Control", - "reference": "https://attack.mitre.org/tactics/TA0011/" - }, - "technique": [ + }, { - "id": "T1188", - "name": "Multi-hop Proxy", - "reference": "https://attack.mitre.org/techniques/T1188/" + "id": "T1090", + "name": "Proxy", + "reference": "https://attack.mitre.org/techniques/T1090/", + "subtechnique": [ + { + "id": "T1090.003", + "name": "Multi-hop Proxy", + "reference": "https://attack.mitre.org/techniques/T1090/003/" + } + ] } ] } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json index 9321d2a2103de..0eba8b5a09153 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_from_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "VNC (Virtual Network Computing) from the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port >= 5800 and destination.port <= 5810 and not source.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\") and destination.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16)", + "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port >= 5800 and destination.port <= 5810 and not source.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" ) and destination.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 )", "risk_score": 73, "rule_id": "5700cb81-df44-46aa-a5d7-337798f53eb8", "severity": "high", @@ -58,5 +58,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json index 38f38e9762645..7152d91518dd8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/command_and_control_vnc_virtual_network_computing_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "VNC (Virtual Network Computing) to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port >= 5800 and destination.port <= 5810 and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and destination.port >= 5800 and destination.port <= 5810 and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 47, "rule_id": "3ad49c61-7adc-42c1-b788-732eda2f5abf", "severity": "medium", @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json index fb8256bf2509c..621a70d11b065 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempted_bypass_of_okta_mfa.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to bypass the Okta multi-factor authentication (MFA) policies configured for an organization in order to obtain unauthorized access to an application. This rule detects when an Okta MFA bypass attempt occurs.", + "description": "Detects attempts to bypass Okta multi-factor authentication (MFA). An adversary may attempt to bypass the Okta MFA policies configured for an organization in order to obtain unauthorized access to an application.", "index": [ "filebeat-*", "logs-okta*" @@ -10,7 +10,7 @@ "language": "kuery", "license": "Elastic License", "name": "Attempted Bypass of Okta MFA", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:user.mfa.attempt_bypass", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -45,5 +45,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json index d8d5b5305aaaa..335a393e8c226 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_attempts_to_brute_force_okta_user_account.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "Attempts to Brute Force an Okta User Account", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:user.account.lock", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -50,5 +50,5 @@ "value": 3 }, "type": "threshold", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cmdline_dump_tool.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cmdline_dump_tool.json new file mode 100644 index 0000000000000..a557809877b53 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_cmdline_dump_tool.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of known Windows utilities often abused to dump LSASS memory or the Active Directory database (NTDS.dit) in preparation for credential access.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Credential Access via Windows Utilities", + "query": "process where event.type in (\"start\", \"process_started\") and\n/* update here with any new lolbas with dump capability */\n(process.pe.original_file_name == \"procdump\" and process.args : \"-ma\") or\n(process.name : \"ProcessDump.exe\" and not process.parent.executable : \"C:\\\\Program Files*\\\\Cisco Systems\\\\*.exe\") or\n(process.pe.original_file_name == \"WriteMiniDump.exe\" and not process.parent.executable : \"C:\\\\Program Files*\\\\Steam\\\\*.exe\") or\n(process.pe.original_file_name == \"RUNDLL32.EXE\" and (process.args : \"MiniDump*\" or process.command_line : \"*comsvcs.dll*#24*\")) or\n(process.pe.original_file_name == \"RdrLeakDiag.exe\" and process.args : \"/fullmemdmp\") or\n(process.pe.original_file_name == \"SqlDumper.exe\" and process.args : \"0x01100*\") or\n(process.pe.original_file_name == \"TTTracer.exe\" and process.args : \"-dumpFull\" and process.args : \"-attach\") or\n(process.pe.original_file_name == \"ntdsutil.exe\" and process.args : \"create*full*\") or\n(process.pe.original_file_name == \"diskshadow.exe\" and process.args : \"/s\")\n", + "references": [ + "https://lolbas-project.github.io/" + ], + "risk_score": 73, + "rule_id": "00140285-b827-4aee-aa09-8113f58a08f3", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1003", + "name": "OS Credential Dumping", + "reference": "https://attack.mitre.org/techniques/T1003/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_copy_ntds_sam_volshadowcp_cmdline.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_copy_ntds_sam_volshadowcp_cmdline.json new file mode 100644 index 0000000000000..ccd4079f22289 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_copy_ntds_sam_volshadowcp_cmdline.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a copy operation of the Active Directory Domain Database (ntds.dit) or Security Account Manager (SAM) files. Those files contain sensitive information including hashed domain and/or local credentials.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "max_signals": 33, + "name": "NTDS or SAM Database File Copied", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.pe.original_file_name in (\"Cmd.Exe\", \"PowerShell.EXE\", \"XCOPY.EXE\") and\n process.args : (\"copy\", \"xcopy\", \"Copy-Item\", \"move\", \"cp\", \"mv\") and\n process.args : (\"*\\\\ntds.dit\", \"*\\\\config\\\\SAM\", \"\\\\*\\\\GLOBALROOT\\\\Device\\\\HarddiskVolumeShadowCopy*\\\\*\")\n", + "references": [ + "https://thedfirreport.com/2020/11/23/pysa-mespinoza-ransomware/" + ], + "risk_score": 73, + "rule_id": "3bc6deaa-fbd4-433a-ae21-3e892f95624f", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1003", + "name": "OS Credential Dumping", + "reference": "https://attack.mitre.org/techniques/T1003/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json index 0761ba515d9b1..9aba46f783dda 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_credential_dumping_msbuild.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "Microsoft Build Engine Loading Windows Credential Libraries", - "query": "event.category:process and event.type:change and (winlog.event_data.OriginalFileName:(vaultcli.dll or SAMLib.DLL) or dll.name:(vaultcli.dll or SAMLib.DLL)) and process.name: MSBuild.exe", + "query": "event.category:process and event.type:change and (process.pe.original_file_name:(vaultcli.dll or SAMLib.DLL) or dll.name:(vaultcli.dll or SAMLib.DLL)) and process.name: MSBuild.exe", "risk_score": 73, "rule_id": "9d110cb3-5f4b-4c9a-b9f5-53f0a1707ae5", "severity": "high", @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dump_registry_hives.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dump_registry_hives.json new file mode 100644 index 0000000000000..a98fc46d405f5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_dump_registry_hives.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to export a registry hive which may contain credentials using the Windows reg.exe tool.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Credential Acquisition via Registry Hive Dumping", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.pe.original_file_name == \"reg.exe\" and\n process.args : (\"save\", \"export\") and\n process.args : (\"hklm\\\\sam\", \"hklm\\\\security\") and\n not process.parent.executable : \"C:\\\\Program Files*\\\\Rapid7\\\\Insight Agent\\\\components\\\\insight_agent\\\\*\\\\ir_agent.exe\"\n \n", + "references": [ + "https://medium.com/threatpunter/detecting-attempts-to-steal-passwords-from-the-registry-7512674487f8" + ], + "risk_score": 73, + "rule_id": "a7e7bfa3-088e-4f13-b29e-3986e0e756b8", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1003", + "name": "OS Credential Dumping", + "reference": "https://attack.mitre.org/techniques/T1003/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json index 6a182617945f1..cde3713732ede 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_apppoolsa_pwd_appcmd.json @@ -8,11 +8,11 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "lucene", + "language": "eql", "license": "Elastic License", "max_signals": 33, "name": "Microsoft IIS Service Account Password Dumped", - "query": "event.category:process AND event.type:(start OR process_started) AND (process.name:appcmd.exe OR process.pe.original_file_name:appcmd.exe or winlog.event_data.OriginalFileName:appcmd.exe) AND process.args:(/[lL][iI][sS][tT]/ AND /\\/[tT][eE][xX][tT]\\:[pP][aA][sS][sS][wW][oO][rR][dD]/)", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"appcmd.exe\" or process.pe.original_file_name == \"appcmd.exe\") and \n process.args : \"/list\" and process.args : \"/text*password\"\n", "references": [ "https://blog.netspi.com/decrypting-iis-passwords-to-break-out-of-the-dmz-part-1/" ], @@ -43,6 +43,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json index f750a0f5594b4..e7c1154f5296e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_iis_connectionstrings_dumping.json @@ -8,11 +8,11 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "max_signals": 33, "name": "Microsoft IIS Connection Strings Decryption", - "query": "event.category:process and event.type:(start or process_started) and (process.name:aspnet_regiis.exe or process.pe.original_file_name:aspnet_regiis.exe or winlog.event_data.OriginalFileName:aspnet_regiis.exe) and process.args:(connectionStrings and \"-pdf\")", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"aspnet_regiis.exe\" or process.pe.original_file_name == \"aspnet_regiis.exe\") and\n process.args : \"connectionStrings\" and process.args : \"-pdf\"\n", "references": [ "https://blog.netspi.com/decrypting-iis-passwords-to-break-out-of-the-dmz-part-1/", "https://symantec-enterprise-blogs.security.com/blogs/threat-intelligence/greenbug-espionage-telco-south-asia" @@ -44,6 +44,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberoasting_unusual_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberoasting_unusual_process.json new file mode 100644 index 0000000000000..bc518ad202d8a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_kerberoasting_unusual_process.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies network connections to the standard Kerberos port from an unusual process. On Windows, the only process that normally performs Kerberos traffic from a domain joined host is lsass.exe.", + "false_positives": [ + "HTTP traffic on a non standard port. Verify that the destination IP address is not related to a Domain Controller." + ], + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Kerberos Traffic from Unusual Process", + "query": "network where event.type == \"start\" and network.direction == \"outgoing\" and\n destination.port == 88 and source.port >= 49152 and\n process.executable != \"C:\\\\Windows\\\\System32\\\\lsass.exe\" and destination.address !=\"127.0.0.1\" and destination.address !=\"::1\" and\n /* insert False Positives here */\n not process.name in (\"swi_fc.exe\", \"fsIPcam.exe\", \"IPCamera.exe\", \"MicrosoftEdgeCP.exe\", \"MicrosoftEdge.exe\", \"iexplore.exe\", \"chrome.exe\", \"msedge.exe\", \"opera.exe\", \"firefox.exe\")\n", + "risk_score": 43, + "rule_id": "897dc6b5-b39f-432a-8d75-d3730d50c782", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1558", + "name": "Steal or Forge Kerberos Tickets", + "reference": "https://attack.mitre.org/techniques/T1558/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_lsass_memdump_file_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_lsass_memdump_file_created.json new file mode 100644 index 0000000000000..759146a5d73a2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_lsass_memdump_file_created.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation of a Local Security Authority Subsystem Service (lsass.exe) default memory dump. This may indicate a credential access attempt via trusted system utilities such as Task Manager (taskmgr.exe) and SQL Dumper (sqldumper.exe) or known pentesting tools such as Dumpert and AndrewSpecial.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "LSASS Memory Dump Creation", + "query": "event.category:file and file.name:(lsass.DMP or lsass*.dmp or dumpert.dmp or Andrew.dmp or SQLDmpr*.mdmp or Coredump.dmp)", + "references": [ + "https://github.com/outflanknl/Dumpert", + "https://github.com/hoangprod/AndrewSpecial" + ], + "risk_score": 73, + "rule_id": "f2f46686-6f3c-4724-bd7d-24e31c70f98f", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1003", + "name": "OS Credential Dumping", + "reference": "https://attack.mitre.org/techniques/T1003/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json new file mode 100644 index 0000000000000..8bd1d60f6dcaa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_brute_force_user_account_attempt.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to brute force a Microsoft 365 user account. An adversary may attempt a brute force attack to obtain unauthorized access to user accounts.", + "false_positives": [ + "Automated processes that attempt to authenticate using expired credentials and unbounded retries may lead to false positives." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempts to Brute Force a Microsoft 365 User Account", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:AzureActiveDirectory and event.category:authentication and event.action:UserLoginFailed and event.outcome:failure", + "risk_score": 73, + "rule_id": "26f68dba-ce29-497b-8e13-b4fde1db5a2d", + "severity": "high", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1110", + "name": "Brute Force", + "reference": "https://attack.mitre.org/techniques/T1110/" + } + ] + } + ], + "threshold": { + "field": "user.id", + "value": 10 + }, + "type": "threshold", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json new file mode 100644 index 0000000000000..348c5506d55f6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_microsoft_365_potential_password_spraying_attack.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a high number (25) of failed Microsoft 365 user authentication attempts from a single IP address within 30 minutes, which could be indicative of a password spraying attack. An adversary may attempt a password spraying attack to obtain unauthorized access to user accounts.", + "false_positives": [ + "Automated processes that attempt to authenticate using expired credentials and unbounded retries may lead to false positives." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential Password Spraying of Microsoft 365 User Accounts", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:AzureActiveDirectory and event.category:authentication and event.action:UserLoginFailed and event.outcome:failure", + "risk_score": 73, + "rule_id": "3efee4f0-182a-40a8-a835-102c68a4175d", + "severity": "high", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1110", + "name": "Brute Force", + "reference": "https://attack.mitre.org/techniques/T1110/" + } + ] + } + ], + "threshold": { + "field": "source.ip", + "value": 25 + }, + "type": "threshold", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json index 9e10dd6dae522..188c5bfbff8d1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_okta_brute_force_or_password_spraying.json @@ -13,7 +13,7 @@ "language": "kuery", "license": "Elastic License", "name": "Okta Brute Force or Password Spraying Attack", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.category:authentication and event.outcome:failure", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -52,5 +52,5 @@ "value": 25 }, "type": "threshold", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_potential_ssh_bruteforce.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_potential_ssh_bruteforce.json new file mode 100644 index 0000000000000..3a43651662246 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_potential_ssh_bruteforce.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a high number (20) of macOS SSH KeyGen process executions from the same host. An adversary may attempt a brute force attack to obtain unauthorized access to user accounts.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Potential SSH Brute Force Detected", + "query": "event.category:process and event.type:start and process.name:\"sshd-keygen-wrapper\" and process.parent.name:launchd", + "references": [ + "https://themittenmac.com/detecting-ssh-activity-via-process-monitoring/" + ], + "risk_score": 47, + "rule_id": "ace1e989-a541-44df-93a8-a8b0591b63c0", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1110", + "name": "Brute Force", + "reference": "https://attack.mitre.org/techniques/T1110/" + } + ] + } + ], + "threshold": { + "field": "host.id", + "value": 20 + }, + "type": "threshold", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_promt_for_pwd_via_osascript.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_promt_for_pwd_via_osascript.json new file mode 100644 index 0000000000000..8de7a0293fac7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/credential_access_promt_for_pwd_via_osascript.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of osascript to execute scripts via standard input that may prompt a user with a rogue dialog for credentials.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Prompt for Credentials with OSASCRIPT", + "query": "process where event.type in (\"start\", \"process_started\") and process.name:\"osascript\" and process.args:\"-e\" and process.args:\"password\"\n", + "references": [ + "https://github.com/EmpireProject/EmPyre/blob/master/lib/modules/collection/osx/prompt.py", + "https://ss64.com/osx/osascript.html" + ], + "risk_score": 73, + "rule_id": "38948d29-3d5d-42e3-8aec-be832aaaf8eb", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Credential Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0006", + "name": "Credential Access", + "reference": "https://attack.mitre.org/tactics/TA0006/" + }, + "technique": [ + { + "id": "T1056", + "name": "Input Capture", + "reference": "https://attack.mitre.org/techniques/T1056/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_del_quarantine_attrib.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_del_quarantine_attrib.json new file mode 100644 index 0000000000000..0c662efe2b310 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_attempt_del_quarantine_attrib.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a potential Gatekeeper bypass. In macOS, when applications or programs are downloaded from the internet, there is a quarantine flag set on the file. This attribute is read by Apple's Gatekeeper defense program at execution time. An adversary may disable this attribute to evade defenses.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Attempt to Remove File Quarantine Attribute", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name == \"xattr\" and process.args == \"com.apple.quarantine\" and process.args == \"-d\"\n", + "references": [ + "https://www.trendmicro.com/en_us/research/20/k/new-macos-backdoor-connected-to-oceanlotus-surfaces.html" + ], + "risk_score": 43, + "rule_id": "f0b48bbc-549e-4bcf-8ee0-a7a72586c6a7", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1089", + "name": "Disabling Security Tools", + "reference": "https://attack.mitre.org/techniques/T1089/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json index 140e1ccd8e890..446029f8cbbcb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base16_or_base32_encoding_or_decoding_activity.json @@ -38,17 +38,7 @@ "id": "T1140", "name": "Deobfuscate/Decode Files or Information", "reference": "https://attack.mitre.org/techniques/T1140/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ + }, { "id": "T1027", "name": "Obfuscated Files or Information", @@ -58,5 +48,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json index fa322fca5db8a..d65483a69ec1d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_base64_encoding_or_decoding_activity.json @@ -38,17 +38,7 @@ "id": "T1140", "name": "Deobfuscate/Decode Files or Information", "reference": "https://attack.mitre.org/techniques/T1140/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ + }, { "id": "T1027", "name": "Obfuscated Files or Information", @@ -58,5 +48,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json index 11d57b855f974..ba4d1ba6b8830 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_clearing_windows_event_logs.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "Identifies attempts to clear Windows event log stores. This is often done by attackers in an attempt to evade detection or destroy forensic evidence on a system.", + "description": "Identifies attempts to clear or disable Windows event log stores using Windows wevetutil command. This is often done by attackers in an attempt to evade detection or destroy forensic evidence on a system.", "from": "now-9m", "index": [ "winlogbeat-*", @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "Clearing Windows Event Logs", - "query": "event.category:process and event.type:(start or process_started) and process.name:wevtutil.exe and process.args:cl or process.name:powershell.exe and process.args:Clear-EventLog", + "query": "event.category:process and event.type:(process_started or start) and (process.name:\"wevtutil.exe\" or process.pe.original_file_name:\"wevtutil.exe\") and process.args:(\"/e:false\" or cl or \"clear-log\") or process.name:\"powershell.exe\" and process.args:\"Clear-EventLog\"", "risk_score": 21, "rule_id": "d331bbe2-6db4-4941-80a5-8270db72eb61", "severity": "low", @@ -40,5 +40,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_websvr_access_logs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_websvr_access_logs.json new file mode 100644 index 0000000000000..4b8ca550087c2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_deleting_websvr_access_logs.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the deletion of WebServer access logs. This may indicate an attempt to evade detection or destroy forensic evidence on a system.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "WebServer Access Logs Deleted", + "query": "file where event.type == \"deletion\" and\n file.path : (\"C:\\\\inetpub\\\\logs\\\\LogFiles\\\\*.log\", \n \"/var/log/apache*/access.log\",\n \"/etc/httpd/logs/access_log\", \n \"/var/log/httpd/access_log\", \n \"/var/www/*/logs/access.log\")\n", + "risk_score": 47, + "rule_id": "665e7a4f-c58e-4fc6-bc83-87a7572670ac", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Windows", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1070", + "name": "Indicator Removal on Host", + "reference": "https://attack.mitre.org/techniques/T1070/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json index 08cbb33710b26..6b601f9845815 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_dotnet_compiler_parent_process.json @@ -8,10 +8,10 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "Suspicious .NET Code Compilation", - "query": "event.category:process and event.type:(start or process_started) and process.name:(csc.exe or vbc.exe) and process.parent.name:(wscript.exe or mshta.exe or wscript.exe or wmic.exe or svchost.exe or rundll32.exe or cmstp.exe or regsvr32.exe)", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name : (\"csc.exe\", \"vbc.exe\") and\n process.parent.name : (\"wscript.exe\", \"mshta.exe\", \"cscript.exe\", \"wmic.exe\", \"svchost.exe\", \"rundll32.exe\", \"cmstp.exe\", \"regsvr32.exe\")\n", "risk_score": 47, "rule_id": "201200f1-a99b-43fb-88ed-f65a45c4972c", "severity": "medium", @@ -39,6 +39,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_enable_inbound_rdp_with_netsh.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_enable_inbound_rdp_with_netsh.json new file mode 100644 index 0000000000000..1785d826ce89c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_enable_inbound_rdp_with_netsh.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies use of the network shell utility (netsh.exe) to enable inbound Remote Desktop Protocol (RDP) connections in the Windows Firewall.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Remote Desktop Enabled in Windows Firewall", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"netsh.exe\" or process.pe.original_file_name == \"netsh.exe\") and\n process.args : (\"localport=3389\", \"RemoteDesktop\", \"group=\\\"remote desktop\\\"\") and\n process.args : (\"action=allow\", \"enable=Yes\", \"enable\")\n", + "risk_score": 47, + "rule_id": "074464f9-f30d-4029-8c03-0ed237fffec7", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1089", + "name": "Disabling Security Tools", + "reference": "https://attack.mitre.org/techniques/T1089/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_lolbas_wuauclt.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_lolbas_wuauclt.json new file mode 100644 index 0000000000000..741f575fd3186 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_lolbas_wuauclt.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies abuse of the Windows Update Auto Update Client (wuauclt.exe) to load an arbitrary DLL. This behavior is used as a defense evasion technique to blend-in malicious activity with legitimate Windows software.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "ImageLoad via Windows Update Auto Update Client", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.pe.original_file_name == \"wuauclt.exe\" or process.name : \"wuauclt.exe\") and\n /* necessary windows update client args to load a dll */\n process.args : \"/RunHandlerComServer\" and process.args : \"/UpdateDeploymentProvider\" and\n /* common paths writeable by a standard user where the target DLL can be placed */\n process.args : (\"C:\\\\Users\\\\*.dll\", \"C:\\\\ProgramData\\\\*.dll\", \"C:\\\\Windows\\\\Temp\\\\*.dll\", \"C:\\\\Windows\\\\Tasks\\\\*.dll\")\n", + "references": [ + "https://dtm.uk/wuauclt/" + ], + "risk_score": 47, + "rule_id": "edf8ee23-5ea7-4123-ba19-56b41e424ae3", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1218", + "name": "Signed Binary Proxy Execution", + "reference": "https://attack.mitre.org/techniques/T1218/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json index 5daab573db5bd..63fa323292092 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_msbuild_started_renamed.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "Microsoft Build Engine Using an Alternate Name", - "query": "event.category:process and event.type:(start or process_started) and (process.pe.original_file_name:MSBuild.exe or winlog.event_data.OriginalFileName:MSBuild.exe) and not process.name: MSBuild.exe", + "query": "event.category:process and event.type:(start or process_started) and process.pe.original_file_name:MSBuild.exe and not process.name: MSBuild.exe", "risk_score": 21, "rule_id": "9d110cb3-5f4b-4c9a-b9f5-53f0a1707ae4", "severity": "low", @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json index 7d9f190ba7be2..19ff6f2e92148 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_execution_suspicious_explorer_winword.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "Potential DLL SideLoading via Trusted Microsoft Programs", - "query": "event.category:process and event.type:(start or process_started) and (process.pe.original_file_name:(WinWord.exe or EXPLORER.EXE or w3wp.exe or DISM.EXE) or winlog.event_data.OriginalFileName:(WinWord.exe or EXPLORER.EXE or w3wp.exe or DISM.EXE)) and not (process.name:(winword.exe or WINWORD.EXE or explorer.exe or w3wp.exe or Dism.exe) or process.executable:(\"C:\\Windows\\explorer.exe\" or C\\:\\\\Program?Files\\\\Microsoft?Office\\\\root\\\\Office*\\\\WINWORD.EXE or C\\:\\\\Program?Files?\\(x86\\)\\\\Microsoft?Office\\\\root\\\\Office*\\\\WINWORD.EXE or \"C:\\Windows\\System32\\Dism.exe\" or \"C:\\Windows\\SysWOW64\\Dism.exe\" or \"C:\\Windows\\System32\\inetsrv\\w3wp.exe\"))", + "query": "event.category:process and event.type:(start or process_started) and process.pe.original_file_name:(WinWord.exe or EXPLORER.EXE or w3wp.exe or DISM.EXE) and not (process.name:(winword.exe or WINWORD.EXE or explorer.exe or w3wp.exe or Dism.exe) or process.executable:(\"C:\\Windows\\explorer.exe\" or C\\:\\\\Program?Files\\\\Microsoft?Office\\\\root\\\\Office*\\\\WINWORD.EXE or C\\:\\\\Program?Files?\\(x86\\)\\\\Microsoft?Office\\\\root\\\\Office*\\\\WINWORD.EXE or \"C:\\Windows\\System32\\Dism.exe\" or \"C:\\Windows\\SysWOW64\\Dism.exe\" or \"C:\\Windows\\System32\\inetsrv\\w3wp.exe\"))", "risk_score": 73, "rule_id": "1160dcdb-0a0a-4a79-91d8-9b84616edebd", "severity": "high", @@ -40,5 +40,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json index 6d3d6f456da4c..d7c89b4e0c471 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hex_encoding_or_decoding_activity.json @@ -38,17 +38,7 @@ "id": "T1140", "name": "Deobfuscate/Decode Files or Information", "reference": "https://attack.mitre.org/techniques/T1140/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ + }, { "id": "T1027", "name": "Obfuscated Files or Information", @@ -58,5 +48,5 @@ } ], "type": "query", - "version": 4 + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hide_encoded_executable_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hide_encoded_executable_registry.json new file mode 100644 index 0000000000000..825918605bd02 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_hide_encoded_executable_registry.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies registry write modifications to hide an encoded portable executable. This could be indicative of adversary defense evasion by avoiding the storing of malicious content directly on disk.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Encoded Executable Stored in the Registry", + "query": "registry where\n/* update here with encoding combinations */\n registry.data.strings : \"TVqQAAMAAAAEAAAA*\"\n", + "risk_score": 47, + "rule_id": "93c1ce76-494c-4f01-8167-35edfb52f7b1", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1140", + "name": "Deobfuscate/Decode Files or Information", + "reference": "https://attack.mitre.org/techniques/T1140/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json index 7d75f50856125..cd0e88826adc9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_iis_httplogging_disabled.json @@ -12,7 +12,7 @@ "license": "Elastic License", "max_signals": 33, "name": "IIS HTTP Logging Disabled", - "query": "event.category:process and event.type:(start or process_started) and (process.name:appcmd.exe or process.pe.original_file_name:appcmd.exe or winlog.event_data.OriginalFileName:appcmd.exe) and process.args:/dontLog\\:\\\"True\\\" and not process.parent.name:iissetup.exe", + "query": "event.category:process and event.type:(start or process_started) and (process.name:appcmd.exe or process.pe.original_file_name:appcmd.exe) and process.args:/dontLog\\:\\\"True\\\" and not process.parent.name:iissetup.exe", "risk_score": 73, "rule_id": "ebf1adea-ccf2-4943-8b96-7ab11ca173a5", "severity": "high", @@ -41,5 +41,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_log_files_deleted.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_log_files_deleted.json new file mode 100644 index 0000000000000..af9ca14e6d01e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_log_files_deleted.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the deletion of sensitive Linux system logs. This may indicate an attempt to evade detection or destroy forensic evidence on a system.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "System Log File Deletion", + "query": "file where event.type == \"deletion\" and \n file.path : \n (\n \"/var/run/utmp\", \n \"/var/log/wtmp\", \n \"/var/log/btmp\", \n \"/var/log/lastlog\", \n \"/var/log/faillog\",\n \"/var/log/syslog\", \n \"/var/log/messages\", \n \"/var/log/secure\", \n \"/var/log/auth.log\"\n )\n", + "references": [ + "https://www.fireeye.com/blog/threat-research/2020/11/live-off-the-land-an-overview-of-unc1945.html" + ], + "risk_score": 47, + "rule_id": "aa895aea-b69c-4411-b110-8d7599634b30", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1070", + "name": "Indicator Removal on Host", + "reference": "https://attack.mitre.org/techniques/T1070/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json index 163c7e834ba34..bc8609f0a180a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_as_elastic_endpoint_process.json @@ -8,10 +8,10 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "Suspicious Endpoint Security Parent Process", - "query": "event.category:process and event.type:(start or process_started) and process.name:(esensor.exe or \"elastic-endpoint.exe\" or \"elastic-agent.exe\") and not process.parent.executable:\"C:\\Windows\\System32\\services.exe\"", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.name : (\"esensor.exe\", \"elastic-endpoint.exe\") and\n process.parent.executable != null and\n /* add FPs here */\n not process.parent.executable : (\"C:\\\\Program Files\\\\Elastic\\\\*\", \n \"C:\\\\Windows\\\\System32\\\\services.exe\", \n \"C:\\\\Windows\\\\System32\\\\WerFault*.exe\", \n \"C:\\\\Windows\\\\System32\\\\wermgr.exe\")\n", "risk_score": 47, "rule_id": "b41a13c6-ba45-4bab-a534-df53d0cfed6a", "severity": "medium", @@ -39,6 +39,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json index be83f8c41a2ea..ed326a798ad31 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_renamed_autoit.json @@ -2,16 +2,16 @@ "author": [ "Elastic" ], - "description": "Identifies a suspicious AutoIt process execution. Malware written as AutoIt scripts tend to rename the AutoIt executable to avoid detection.", + "description": "Identifies a suspicious AutoIt process execution. Malware written as an AutoIt script tends to rename the AutoIt executable to avoid detection.", "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "lucene", + "language": "eql", "license": "Elastic License", "name": "Renamed AutoIt Scripts Interpreter", - "query": "event.category:process AND event.type:(start OR process_started) AND (process.pe.original_file_name:/[aA][uU][tT][oO][iI][tT]\\d\\.[eE][xX][eE]/ OR winlog.event_data.OriginalFileName:/[aA][uU][tT][oO][iI][tT]\\d\\.[eE][xX][eE]/) AND NOT process.name:/[aA][uU][tT][oO][iI][tT]\\d{1,3}\\.[eE][xX][eE]/", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.pe.original_file_name : \"AutoIt*.exe\" and not process.name : \"AutoIt*.exe\"\n", "risk_score": 47, "rule_id": "2e1e835d-01e5-48ca-b9fc-7a61f7f11902", "severity": "medium", @@ -39,6 +39,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_trusted_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_trusted_directory.json new file mode 100644 index 0000000000000..693a9a77326f8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_trusted_directory.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies execution from a directory masquerading as the Windows Program Files directories. These paths are trusted and usually host trusted third party programs. An adversary may leverage masquerading, along with low privileges to bypass detections whitelisting those folders.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Program Files Directory Masquerading", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n /* capture both fake program files directory in process executable as well as if passed in process args as a dll*/\n process.args : (\"C:\\\\*Program*Files*\\\\*\", \"C:\\\\*Program*Files*\\\\*\") and\n not process.args : (\"C:\\\\Program Files\\\\*\", \"C:\\\\Program Files (x86)\\\\*\")\n", + "risk_score": 43, + "rule_id": "32c5cf9c-2ef8-4e87-819e-5ccb7cd18b14", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1036", + "name": "Masquerading", + "reference": "https://attack.mitre.org/techniques/T1036/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json index 9f5615d466374..39cf9860dadcd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_masquerading_werfault.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "Identifies a suspicious WerFault command line parameter, which may indicate an attempt to run unnoticed.", + "description": "Identifies suspicious instances of the Windows Error Reporting process (WerFault.exe or Wermgr.exe) with matching command-line and process executable values performing outgoing network connections. This may be indicative of a masquerading attempt to evade suspicious child process behavior detections.", "false_positives": [ "Legit Application Crash with rare Werfault commandline value" ], @@ -11,13 +11,14 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", - "name": "Process Potentially Masquerading as WerFault", - "query": "event.category:process and event.type:(start or process_started) and process.name:WerFault.exe and not process.args:(((\"-u\" or \"-pss\") and \"-p\" and \"-s\") or (\"/h\" and \"/shared\") or (\"-k\" and \"-lcq\"))", + "name": "Potential Windows Error Manager Masquerading", + "query": "sequence by host.id, process.entity_id with maxspan = 5s\n [process where event.type:\"start\" and process.name : (\"wermgr.exe\", \"WerFault.exe\") and process.args_count == 1]\n [network where process.name : (\"wermgr.exe\", \"WerFault.exe\") and network.protocol != \"dns\" and\n network.direction == \"outgoing\" and destination.ip !=\"::1\" and destination.ip !=\"127.0.0.1\"\n ]\n", "references": [ "https://twitter.com/SBousseaden/status/1235533224337641473", - "https://www.hexacorn.com/blog/2019/09/20/werfault-command-line-switches-v0-1/" + "https://www.hexacorn.com/blog/2019/09/20/werfault-command-line-switches-v0-1/", + "https://app.any.run/tasks/26051d84-b68e-4afb-8a9a-76921a271b81/" ], "risk_score": 47, "rule_id": "6ea41894-66c3-4df7-ad6b-2c5074eb3df8", @@ -46,6 +47,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_dlp_policy_removed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_dlp_policy_removed.json new file mode 100644 index 0000000000000..0166512e7361b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_dlp_policy_removed.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a Data Loss Prevention (DLP) policy is removed in Microsoft 365. An adversary may remove a DLP policy to evade existing DLP monitoring.", + "false_positives": [ + "A DLP policy may be removed by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange DLP Policy Removed", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-DlpPolicy\" and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-dlppolicy?view=exchange-ps", + "https://docs.microsoft.com/en-us/microsoft-365/compliance/data-loss-prevention-policies?view=o365-worldwide" + ], + "risk_score": 47, + "rule_id": "60f3adec-1df9-4104-9c75-b97d9f078b25", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json new file mode 100644 index 0000000000000..7f087c1db21c8 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a malware filter policy has been deleted in Microsoft 365. A malware filter policy is used to alert administrators that an internal user sent a message that contained malware. This may indicate an account or machine compromise that would need to be investigated. Deletion of a malware filter policy may be done to evade detection.", + "false_positives": [ + "A malware filter policy may be deleted by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Malware Filter Policy Deletion", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-MalwareFilterPolicy\" and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-malwarefilterpolicy?view=exchange-ps" + ], + "risk_score": 47, + "rule_id": "d743ff2a-203e-4a46-a3e3-40512cfe8fbb", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json new file mode 100644 index 0000000000000..5475d0ee04d36 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a malware filter rule has been deleted or disabled in Microsoft 365. An adversary or insider threat may want to modify a malware filter rule to evade detection.", + "false_positives": [ + "A malware filter rule may be deleted by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Malware Filter Rule Modification", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-MalwareFilterRule\" or \"Disable-MalwareFilterRule\") and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-malwarefilterrule?view=exchange-ps", + "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-malwarefilterrule?view=exchange-ps" + ], + "risk_score": 47, + "rule_id": "ca79768e-40e1-4e45-a097-0e5fbc876ac2", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json new file mode 100644 index 0000000000000..7b5af375da4fe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a safe attachment rule is disabled in Microsoft 365. Safe attachment rules can extend malware protections to include routing all messages and attachments without a known malware signature to a special hypervisor environment. An adversary or insider threat may disable a safe attachment rule to exfiltrate data or evade defenses.", + "false_positives": [ + "A safe attachment rule may be disabled by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Safe Attachment Rule Disabled", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Disable-SafeAttachmentRule\" and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-safeattachmentrule?view=exchange-ps" + ], + "risk_score": 21, + "rule_id": "03024bd9-d23f-4ec1-8674-3cf1a21e130b", + "severity": "low", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1562", + "name": "Impair Defenses", + "reference": "https://attack.mitre.org/techniques/T1562/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_port_forwarding_added_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_port_forwarding_added_registry.json new file mode 100644 index 0000000000000..b694298622967 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_port_forwarding_added_registry.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation of a new port forwarding rule. An adversary may abuse this technique to bypass network segmentation restrictions.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Port Forwarding Rule Addition", + "query": "registry where registry.path : \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Services\\\\PortProxy\\\\v4tov4\\\\*\"\n", + "references": [ + "https://www.fireeye.com/blog/threat-research/2019/01/bypassing-network-restrictions-through-rdp-tunneling.html" + ], + "risk_score": 47, + "rule_id": "3535c8bb-3bd5-40f4-ae32-b7cd589d5372", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1089", + "name": "Disabling Security Tools", + "reference": "https://attack.mitre.org/techniques/T1089/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_potential_processherpaderping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_potential_processherpaderping.json new file mode 100644 index 0000000000000..5f8975c41cf18 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_potential_processherpaderping.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies process execution followed by a file overwrite of an executable by the same parent process. This may indicate an evasion attempt to execute malicious code in a stealthy way.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Process Herpaderping Attempt", + "query": "sequence with maxspan=5s\n [process where event.type == \"start\" and not process.parent.executable : \"C:\\\\Windows\\\\SoftwareDistribution\\\\*.exe\"] by host.id, process.executable, process.parent.entity_id\n [file where event.type == \"change\" and event.action == \"overwrite\" and file.extension == \"exe\"] by host.id, file.path, process.entity_id\n", + "references": [ + "https://github.com/jxy-s/herpaderping" + ], + "risk_score": 73, + "rule_id": "ccc55af4-9882-4c67-87b4-449a7ae8079c", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1036", + "name": "Masquerading", + "reference": "https://attack.mitre.org/techniques/T1036/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_process_termination_followed_by_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_process_termination_followed_by_deletion.json new file mode 100644 index 0000000000000..69819be8c538c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_process_termination_followed_by_deletion.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a process termination event quickly followed by the deletion of its executable file. Malware tools and other non-native files dropped or created on a system by an adversary may leave traces to indicate to what occurred. Removal of these files can occur during an intrusion, or as part of a post-intrusion process to minimize the adversary's footprint.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Process Termination followed by Deletion", + "query": "sequence by host.id with maxspan=5s\n [process where event.type == \"end\" and \n process.code_signature.trusted == false and\n not process.executable : (\"C:\\\\Windows\\\\SoftwareDistribution\\\\*.exe\", \"C:\\\\Windows\\\\WinSxS\\\\*.exe\")\n ] by process.executable\n [file where event.type == \"deletion\" and file.extension : (\"exe\", \"scr\", \"com\")] by file.path\n", + "risk_score": 47, + "rule_id": "09443c92-46b3-45a4-8f25-383b028b258d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1070", + "name": "Indicator Removal on Host", + "reference": "https://attack.mitre.org/techniques/T1070/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json index c2712f1574a7c..b518ad072e694 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_rundll32_no_arguments.json @@ -11,7 +11,7 @@ "language": "eql", "license": "Elastic License", "name": "Unusual Child Processes of RunDLL32", - "query": "sequence with maxspan=1h\n [process where event.type in (\"start\", \"process_started\") and\n /* uncomment once in winlogbeat */\n (process.name : \"rundll32.exe\" /* or process.pe.original_file_name == \"RUNDLL32.EXE\" */ ) and\n process.args_count < 2\n ] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.parent.name : \"rundll32.exe\"\n ] by process.parent.entity_id\n", + "query": "sequence with maxspan=1h\n [process where event.type in (\"start\", \"process_started\") and\n (process.name : \"rundll32.exe\" or process.pe.original_file_name == \"RUNDLL32.EXE\") and\n process.args_count == 1\n ] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.parent.name : \"rundll32.exe\"\n ] by process.parent.entity_id\n", "risk_score": 21, "rule_id": "f036953a-4615-4707-a1ca-dc53bf69dcd5", "severity": "high", @@ -40,5 +40,5 @@ } ], "type": "eql", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_scheduledjobs_at_protocol_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_scheduledjobs_at_protocol_enabled.json new file mode 100644 index 0000000000000..6fa2c70520f68 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_scheduledjobs_at_protocol_enabled.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to enable the Windows scheduled tasks AT command via the registry. Attackers may use this method to move laterally or persist locally. The AT command has been deprecated since Windows 8 and Windows Server 2012, but still exists for backwards compatibility.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Scheduled Tasks AT Command Enabled", + "query": "registry where \n registry.path : \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Schedule\\\\Configuration\\\\EnableAt\" and registry.data.strings == \"1\"\n", + "references": [ + "https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-scheduledjob" + ], + "risk_score": 47, + "rule_id": "9aa0e1f6-52ce-42e1-abb3-09657cee2698", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1089", + "name": "Disabling Security Tools", + "reference": "https://attack.mitre.org/techniques/T1089/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json index 6fea9a3c78945..47df5a750c829 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_sdelete_like_filename_rename.json @@ -8,11 +8,11 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "lucene", + "language": "eql", "license": "Elastic License", "name": "Potential Secure File Deletion via SDelete Utility", "note": "Verify process details such as command line and hash to confirm this activity legitimacy.", - "query": "event.category:file AND event.type:change AND file.name:/.+A+\\.AAA/", + "query": "file where event.type == \"change\" and wildcard(file.name,\"*AAA.AAA\")\n", "risk_score": 21, "rule_id": "5aee924b-6ceb-4633-980e-1bde8cdb40c5", "severity": "low", @@ -40,6 +40,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json new file mode 100644 index 0000000000000..b638f05a732af --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_stop_process_service_threshold.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "This rule identifies a high number (10) of process terminations (stop, delete, or suspend) from the same host within a short time period. This may indicate a defense evasion attempt.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "High Number of Process and/or Service Terminations", + "query": "event.category:process and event.type:start and process.name:(net.exe or sc.exe or taskkill.exe) and process.args:(stop or pause or delete or \"/PID\" or \"/IM\" or \"/T\" or \"/F\" or \"/t\" or \"/f\" or \"/im\" or \"/pid\")", + "risk_score": 47, + "rule_id": "035889c4-2686-4583-a7df-67f89c292f2c", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1089", + "name": "Disabling Security Tools", + "reference": "https://attack.mitre.org/techniques/T1089/" + } + ] + } + ], + "threshold": { + "field": "host.id", + "value": 10 + }, + "type": "threshold", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json index fedeaca68ab64..728f2cee89434 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "High Number of Okta User Password Reset or Unlock Attempts", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:(system.email.account_unlock.sent_message or system.email.password_reset.sent_message or system.sms.send_account_unlock_message or system.sms.send_password_reset_message or system.voice.send_account_unlock_call or system.voice.send_password_reset_call or user.account.unlock_token)", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -83,5 +83,5 @@ "value": 5 }, "type": "threshold", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_powershell_imgload.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_powershell_imgload.json new file mode 100644 index 0000000000000..9d0d327bc9a77 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_powershell_imgload.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the PowerShell engine being invoked by unexpected processes. Rather than executing PowerShell functionality with powershell.exe, some attackers do this to operate more stealthily.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious PowerShell Engine ImageLoad", + "query": "library where file.name : (\"System.Management.Automation.ni.dll\", \"System.Management.Automation.dll\") and\n/* add false positives relevant to your environment here */\nnot process.executable : (\"C:\\\\Windows\\\\System32\\\\RemoteFXvGPUDisablement.exe\", \"C:\\\\Windows\\\\System32\\\\sdiagnhost.exe\", \"C:\\\\Program Files*\\\\*.exe\") and\n not process.name : (\n \"Altaro.SubAgent.exe\",\n \"AppV_Manage.exe\",\n \"azureadconnect.exe\",\n \"CcmExec.exe\",\n \"configsyncrun.exe\",\n \"choco.exe\",\n \"ctxappvservice.exe\",\n \"DVLS.Console.exe\",\n \"edgetransport.exe\",\n \"exsetup.exe\",\n \"forefrontactivedirectoryconnector.exe\",\n \"InstallUtil.exe\",\n \"JenkinsOnDesktop.exe\",\n \"Microsoft.EnterpriseManagement.ServiceManager.UI.Console.exe\",\n \"mmc.exe\",\n \"mscorsvw.exe\",\n \"msexchangedelivery.exe\",\n \"msexchangefrontendtransport.exe\",\n \"msexchangehmworker.exe\",\n \"msexchangesubmission.exe\",\n \"msiexec.exe\",\n \"MsiExec.exe\",\n \"noderunner.exe\",\n \"NServiceBus.Host.exe\",\n \"NServiceBus.Host32.exe\",\n \"NServiceBus.Hosting.Azure.HostProcess.exe\",\n \"OuiGui.WPF.exe\",\n \"powershell.exe\",\n \"powershell_ise.exe\",\n \"pwsh.exe\",\n \"SCCMCliCtrWPF.exe\",\n \"ScriptEditor.exe\",\n \"ScriptRunner.exe\",\n \"sdiagnhost.exe\",\n \"servermanager.exe\",\n \"setup100.exe\",\n \"ServiceHub.VSDetouredHost.exe\",\n \"SPCAF.Client.exe\",\n \"SPCAF.SettingsEditor.exe\",\n \"SQLPS.exe\",\n \"telemetryservice.exe\",\n \"UMWorkerProcess.exe\",\n \"w3wp.exe\",\n \"wsmprovhost.exe\"\n )\n", + "risk_score": 47, + "rule_id": "852c1f19-68e8-43a6-9dce-340771fe1be3", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1086", + "name": "PowerShell", + "reference": "https://attack.mitre.org/techniques/T1086/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json index f3c20e5251184..5ce48d1526466 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_suspicious_zoom_child_process.json @@ -3,18 +3,15 @@ "Elastic" ], "description": "A suspicious Zoom child process was detected, which may indicate an attempt to run unnoticed. Verify process details such as command line, network connections, file writes and associated file signature details as well.", - "false_positives": [ - "New Zoom Executable" - ], "from": "now-9m", "index": [ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "Suspicious Zoom Child Process", - "query": "event.category:process and event.type:(start or process_started) and process.parent.name:Zoom.exe and not process.name:(Zoom.exe or WerFault.exe or airhost.exe or CptControl.exe or CptHost.exe or cpthost.exe or CptInstall.exe or CptService.exe or Installer.exe or zCrashReport.exe or Zoom_launcher.exe or zTscoder.exe or plugin_Launcher.exe or mDNSResponder.exe or zDevHelper.exe or APcptControl.exe or CrashSender*.exe or aomhost64.exe or Magnify.exe or m_plugin_launcher.exe or com.zoom.us.zTranscode.exe or RoomConnector.exe or tabtip.exe or Explorer.exe or chrome.exe or firefox.exe or iexplore.exe or outlook.exe or lync.exe or ApplicationFrameHost.exe or ZoomAirhostInstaller.exe or narrator.exe or NVDA.exe or Magnify.exe or Outlook.exe or m_plugin_launcher.exe or mphost.exe or APcptControl.exe or winword.exe or excel.exe or powerpnt.exe or ONENOTE.EXE or wpp.exe or debug_message.exe or zAssistant.exe or msiexec.exe or msedge.exe or dwm.exe or vcredist_x86.exe or Controller.exe or Installer.exe or CptInstall.exe or Zoom_launcher.exe or ShellExperienceHost.exe or wps.exe)", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.parent.name : \"Zoom.exe\" and process.name : (\"cmd.exe\", \"powershell.exe\", \"pwsh.exe\")\n", "risk_score": 47, "rule_id": "97aba1ef-6034-4bd3-8c1a-1e0996b27afa", "severity": "medium", @@ -38,17 +35,7 @@ "id": "T1036", "name": "Masquerading", "reference": "https://attack.mitre.org/techniques/T1036/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0005", - "name": "Defense Evasion", - "reference": "https://attack.mitre.org/tactics/TA0005/" - }, - "technique": [ + }, { "id": "T1055", "name": "Process Injection", @@ -57,6 +44,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_timestomp_touch.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_timestomp_touch.json new file mode 100644 index 0000000000000..d14103889a35e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_timestomp_touch.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Timestomping is an anti-forensics technique which is used to modify the timestamps of a file, often to mimic files that are in the same folder.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "max_signals": 33, + "name": "Timestomping using Touch Command", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name == \"touch\" and wildcard(process.args, \"-r\", \"-t\", \"-a*\",\"-m*\")\n", + "risk_score": 47, + "rule_id": "b0046934-486e-462f-9487-0d4cf9e429c6", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Linux", + "macOS", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1099", + "name": "Timestomp", + "reference": "https://attack.mitre.org/techniques/T1099/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_dir_ads.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_dir_ads.json new file mode 100644 index 0000000000000..659a9333e694e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/defense_evasion_unusual_dir_ads.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies processes running from an Alternate Data Stream. This is uncommon for legitimate processes and sometimes done by adversaries to hide malware.", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Unusual Process Execution Path - Alternate Data Stream", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.args : \"C:\\\\*:*\"\n", + "risk_score": 47, + "rule_id": "4bd1c1af-79d4-4d37-9efa-6e0240640242", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1564", + "name": "Hide Artifacts", + "reference": "https://attack.mitre.org/techniques/T1564/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_adfind_command_activity.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_adfind_command_activity.json new file mode 100644 index 0000000000000..23eae7093f0fa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_adfind_command_activity.json @@ -0,0 +1,77 @@ +{ + "author": [ + "Elastic" + ], + "description": "This rule detects the Active Directory query tool, AdFind.exe. AdFind has legitimate purposes, but it is frequently leveraged by threat actors to perform post-exploitation Active Directory reconnaissance. The AdFind tool has been observed in Trickbot, Ryuk, Maze, and FIN6 campaigns. For Winlogbeat, this rule requires Sysmon.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "AdFind Command Activity", + "note": "`AdFind.exe` is a legitimate domain query tool. Rule alerts should be investigated to identify if the user has a role that would explain using this tool and that it is being run from an expected directory and endpoint. Leverage the exception workflow in the Kibana Security App or Elasticsearch API to tune this rule to your environment.", + "query": "process where event.type in (\"start\", \"process_started\") and \n (process.name : \"AdFind.exe\" or process.pe.original_file_name == \"AdFind.exe\") and \n process.args : (\"objectcategory=computer\", \"(objectcategory=computer)\", \n \"objectcategory=person\", \"(objectcategory=person)\",\n \"objectcategory=subnet\", \"(objectcategory=subnet)\",\n \"objectcategory=group\", \"(objectcategory=group)\", \n \"objectcategory=organizationalunit\", \"(objectcategory=organizationalunit)\",\n \"objectcategory=attributeschema\", \"(objectcategory=attributeschema)\",\n \"domainlist\", \"dcmodes\", \"adinfo\", \"dclist\", \"computers_pwnotreqd\", \"trustdmp\")\n", + "references": [ + "http://www.joeware.net/freetools/tools/adfind/", + "https://thedfirreport.com/2020/05/08/adfind-recon/", + "https://www.fireeye.com/blog/threat-research/2020/05/tactics-techniques-procedures-associated-with-maze-ransomware-incidents.html", + "https://www.cybereason.com/blog/dropping-anchor-from-a-trickbot-infection-to-the-discovery-of-the-anchor-malware", + "https://www.fireeye.com/blog/threat-research/2019/04/pick-six-intercepting-a-fin6-intrusion.html", + "https://usa.visa.com/dam/VCOM/global/support-legal/documents/fin6-cybercrime-group-expands-threat-To-ecommerce-merchants.pdf" + ], + "risk_score": 21, + "rule_id": "eda499b8-a073-4e35-9733-22ec71f57f3a", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1069", + "name": "Permission Groups Discovery", + "reference": "https://attack.mitre.org/techniques/T1069/", + "subtechnique": [ + { + "id": "T1069.002", + "name": "Domain Groups", + "reference": "https://attack.mitre.org/techniques/T1069/002/" + } + ] + }, + { + "id": "T1087", + "name": "Account Discovery", + "reference": "https://attack.mitre.org/techniques/T1087/", + "subtechnique": [ + { + "id": "T1087.002", + "name": "Domain Account", + "reference": "https://attack.mitre.org/techniques/T1087/002/" + } + ] + }, + { + "id": "T1482", + "name": "Domain Trust Discovery", + "reference": "https://attack.mitre.org/techniques/T1482/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_admin_recon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_admin_recon.json new file mode 100644 index 0000000000000..803cd6704d424 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_admin_recon.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies instances of lower privilege accounts enumerating Administrator accounts or groups using built-in Windows tools.", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Enumeration of Administrator Accounts", + "query": "process where event.type in (\"start\", \"process_started\") and\n (((process.name : \"net.exe\" or process.pe.original_file_name == \"net.exe\") or\n ((process.name : \"net1.exe\" or process.pe.original_file_name == \"net1.exe\") and\n not process.parent.name : \"net.exe\")) and\n process.args : (\"group\", \"user\", \"localgroup\") and\n process.args : (\"admin\", \"Domain Admins\", \"Remote Desktop Users\", \"Enterprise Admins\", \"Organization Management\") and\n not process.args : \"/add\")\n\n or\n\n ((process.name : \"wmic.exe\" or process.pe.original_file_name == \"wmic.exe\") and\n process.args : (\"group\", \"useraccount\"))\n", + "risk_score": 21, + "rule_id": "871ea072-1b71-4def-b016-6278b505138d", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1069", + "name": "Permission Groups Discovery", + "reference": "https://attack.mitre.org/techniques/T1069/" + }, + { + "id": "T1087", + "name": "Account Discovery", + "reference": "https://attack.mitre.org/techniques/T1087/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_file_dir_discovery.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_file_dir_discovery.json new file mode 100644 index 0000000000000..0881e0b084394 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_file_dir_discovery.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Enumeration of files and directories using built-in tools. Adversaries may use the information discovered to plan follow-on activity.", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "File and Directory Discovery", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"cmd.exe\" or process.pe.original_file_name == \"Cmd.Exe\") and\n process.args : (\"dir\", \"tree\")\n\n", + "risk_score": 21, + "rule_id": "7b08314d-47a0-4b71-ae4e-16544176924f", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1083", + "name": "File and Directory Discovery", + "reference": "https://attack.mitre.org/techniques/T1083/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_view.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_view.json new file mode 100644 index 0000000000000..072472e948422 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_net_view.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to enumerate hosts in a network using the built-in Windows net.exe tool.", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Windows Network Enumeration", + "query": "process where event.type in (\"start\", \"process_started\") and\n ((process.name : \"net.exe\" or process.pe.original_file_name == \"net.exe\") or\n ((process.name : \"net1.exe\" or process.pe.original_file_name == \"net1.exe\") and\n not process.parent.name : \"net.exe\")) and\n (process.args : \"view\" or (process.args : \"time\" and process.args : \"\\\\\\\\*\"))\n\n\n /* expand when ancestory is available\n and not descendant of [process where event.type == (\"start\", \"process_started\") and process.name : \"cmd.exe\" and\n ((process.parent.name : \"userinit.exe\") or\n (process.parent.name : \"gpscript.exe\") or\n (process.parent.name : \"explorer.exe\" and\n process.args : \"C:\\\\*\\\\Start Menu\\\\Programs\\\\Startup\\\\*.bat*\"))]\n */\n", + "risk_score": 47, + "rule_id": "7b8bfc26-81d2-435e-965c-d722ee397ef1", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1018", + "name": "Remote System Discovery", + "reference": "https://attack.mitre.org/techniques/T1018/" + }, + { + "id": "T1135", + "name": "Network Share Discovery", + "reference": "https://attack.mitre.org/techniques/T1135/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_peripheral_device.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_peripheral_device.json new file mode 100644 index 0000000000000..8b0dc1d1c1ead --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_peripheral_device.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies use of the Windows file system utility (fsutil.exe ) to gather information about attached peripheral devices and components connected to a computer system.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Peripheral Device Discovery", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"fsutil.exe\" or process.pe.original_file_name == \"fsutil.exe\") and \n process.args : \"fsinfo\" and process.args : \"drives\"\n", + "risk_score": 21, + "rule_id": "0c7ca5c2-728d-4ad9-b1c5-bbba83ecb1f4", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1120", + "name": "Peripheral Device Discovery", + "reference": "https://attack.mitre.org/techniques/T1120/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_query_registry_via_reg.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_query_registry_via_reg.json new file mode 100644 index 0000000000000..23e0cf236ffd4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_query_registry_via_reg.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Enumeration or discovery of the Windows registry using reg.exe. This information can be used to perform follow-on activities.", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Query Registry via reg.exe", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"reg.exe\" or process.pe.original_file_name == \"reg.exe\") and\n process.args == \"query\"\n", + "risk_score": 21, + "rule_id": "68113fdc-3105-4cdd-85bb-e643c416ef0b", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1012", + "name": "Query Registry", + "reference": "https://attack.mitre.org/techniques/T1012/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_remote_system_discovery_commands_windows.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_remote_system_discovery_commands_windows.json new file mode 100644 index 0000000000000..fa879cc9301dc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_remote_system_discovery_commands_windows.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Discovery of remote system information using built-in commands, which may be used to mover laterally.", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Remote System Discovery Commands", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"nbtstat.exe\" and process.args : (\"-n\", \"-s\")) or\n (process.name : \"arp.exe\" and process.args : \"-a\")\n", + "risk_score": 21, + "rule_id": "0635c542-1b96-4335-9b47-126582d2c19a", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1018", + "name": "Remote System Discovery", + "reference": "https://attack.mitre.org/techniques/T1018/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_wmic.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_wmic.json new file mode 100644 index 0000000000000..32880a5342ffe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/discovery_security_software_wmic.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of Windows Management Instrumentation Command (WMIC) to discover certain System Security Settings such as AntiVirus or Host Firewall details.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Security Software Discovery using WMIC", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name:\"wmic.exe\" or process.pe.original_file_name:\"wmic.exe\") and\n process.args:\"/namespace:\\\\\\\\root\\\\SecurityCenter2\" and process.args:\"Get\"\n", + "risk_score": 47, + "rule_id": "6ea55c81-e2ba-42f2-a134-bccf857ba922", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Discovery" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0007", + "name": "Discovery", + "reference": "https://attack.mitre.org/tactics/TA0007/" + }, + "technique": [ + { + "id": "T1518", + "name": "Software Discovery", + "reference": "https://attack.mitre.org/techniques/T1518/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/domain_added_to_google_workspace_trusted_domains.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/domain_added_to_google_workspace_trusted_domains.json new file mode 100644 index 0000000000000..e3c06cae1c229 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/domain_added_to_google_workspace_trusted_domains.json @@ -0,0 +1,35 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when a domain is added to the list of trusted Google Workspace domains. An adversary may add a trusted domain in order to collect and exfiltrate data from their target\u2019s organization with less restrictive security controls.", + "false_positives": [ + "Trusted domains may be added by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Domain Added to Google Workspace Trusted Domains", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:ADD_TRUSTED_DOMAINS", + "references": [ + "https://support.google.com/a/answer/6160020?hl=en" + ], + "risk_score": 73, + "rule_id": "cf549724-c577-4fd6-8f9b-d1b8ec519ec0", + "severity": "high", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json index 220a7f94dce9a..df561f4c0ee1c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_started_by_powershell.json @@ -35,17 +35,7 @@ "id": "T1059", "name": "Command and Scripting Interpreter", "reference": "https://attack.mitre.org/techniques/T1059/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0002", - "name": "Execution", - "reference": "https://attack.mitre.org/tactics/TA0002/" - }, - "technique": [ + }, { "id": "T1086", "name": "PowerShell", @@ -55,5 +45,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_via_rundll32.json new file mode 100644 index 0000000000000..f4808c7e12670 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_command_shell_via_rundll32.json @@ -0,0 +1,49 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies command shell activity started via RunDLL32, which is commonly abused by attackers to host malicious code.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Command Shell Activity Started via RunDLL32", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name : (\"cmd.exe\", \"powershell.exe\") and\n process.parent.name : \"rundll32.exe\" and \n /* common FPs can be added here */\n not process.parent.args : \"C:\\\\Windows\\\\System32\\\\SHELL32.dll,RunAsNewUser_RunDLL\"\n", + "risk_score": 21, + "rule_id": "9ccf3ce0-0057-440a-91f5-870c6ad39093", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + }, + { + "id": "T1086", + "name": "PowerShell", + "reference": "https://attack.mitre.org/techniques/T1086/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_directory.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_directory.json new file mode 100644 index 0000000000000..5df91110a8dfe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_directory.json @@ -0,0 +1,27 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies process execution from suspicious default Windows directories. This is sometimes done by adversaries to hide malware in trusted paths.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Process Execution from an Unusual Directory", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n /* add suspicious execution paths here */\nprocess.executable : (\"C:\\\\PerfLogs\\\\*.exe\",\"C:\\\\Users\\\\Public\\\\*.exe\",\"C:\\\\Users\\\\Default\\\\*.exe\",\"C:\\\\Windows\\\\Tasks\\\\*.exe\",\"C:\\\\Intel\\\\*.exe\",\"C:\\\\AMD\\\\Temp\\\\*.exe\",\"C:\\\\Windows\\\\AppReadiness\\\\*.exe\",\n\"C:\\\\Windows\\\\ServiceState\\\\*.exe\",\"C:\\\\Windows\\\\security\\\\*.exe\",\"C:\\\\Windows\\\\IdentityCRL\\\\*.exe\",\"C:\\\\Windows\\\\Branding\\\\*.exe\",\"C:\\\\Windows\\\\csc\\\\*.exe\",\n \"C:\\\\Windows\\\\DigitalLocker\\\\*.exe\",\"C:\\\\Windows\\\\en-US\\\\*.exe\",\"C:\\\\Windows\\\\wlansvc\\\\*.exe\",\"C:\\\\Windows\\\\Prefetch\\\\*.exe\",\"C:\\\\Windows\\\\Fonts\\\\*.exe\",\n \"C:\\\\Windows\\\\diagnostics\\\\*.exe\",\"C:\\\\Windows\\\\TAPI\\\\*.exe\",\"C:\\\\Windows\\\\INF\\\\*.exe\",\"C:\\\\Windows\\\\System32\\\\Speech\\\\*.exe\",\"C:\\\\windows\\\\tracing\\\\*.exe\",\n \"c:\\\\windows\\\\IME\\\\*.exe\",\"c:\\\\Windows\\\\Performance\\\\*.exe\",\"c:\\\\windows\\\\intel\\\\*.exe\",\"c:\\\\windows\\\\ms\\\\*.exe\",\"C:\\\\Windows\\\\dot3svc\\\\*.exe\",\"C:\\\\Windows\\\\ServiceProfiles\\\\*.exe\",\n \"C:\\\\Windows\\\\panther\\\\*.exe\",\"C:\\\\Windows\\\\RemotePackages\\\\*.exe\",\"C:\\\\Windows\\\\OCR\\\\*.exe\",\"C:\\\\Windows\\\\appcompat\\\\*.exe\",\"C:\\\\Windows\\\\apppatch\\\\*.exe\",\"C:\\\\Windows\\\\addins\\\\*.exe\",\n \"C:\\\\Windows\\\\Setup\\\\*.exe\",\"C:\\\\Windows\\\\Help\\\\*.exe\",\"C:\\\\Windows\\\\SKB\\\\*.exe\",\"C:\\\\Windows\\\\Vss\\\\*.exe\",\"C:\\\\Windows\\\\Web\\\\*.exe\",\"C:\\\\Windows\\\\servicing\\\\*.exe\",\"C:\\\\Windows\\\\CbsTemp\\\\*.exe\",\n \"C:\\\\Windows\\\\Logs\\\\*.exe\",\"C:\\\\Windows\\\\WaaS\\\\*.exe\",\"C:\\\\Windows\\\\twain_32\\\\*.exe\",\"C:\\\\Windows\\\\ShellExperiences\\\\*.exe\",\"C:\\\\Windows\\\\ShellComponents\\\\*.exe\",\"C:\\\\Windows\\\\PLA\\\\*.exe\",\n \"C:\\\\Windows\\\\Migration\\\\*.exe\",\"C:\\\\Windows\\\\debug\\\\*.exe\",\"C:\\\\Windows\\\\Cursors\\\\*.exe\",\"C:\\\\Windows\\\\Containers\\\\*.exe\",\"C:\\\\Windows\\\\Boot\\\\*.exe\",\"C:\\\\Windows\\\\bcastdvr\\\\*.exe\",\n \"C:\\\\Windows\\\\assembly\\\\*.exe\",\"C:\\\\Windows\\\\TextInput\\\\*.exe\",\"C:\\\\Windows\\\\security\\\\*.exe\",\"C:\\\\Windows\\\\schemas\\\\*.exe\",\"C:\\\\Windows\\\\SchCache\\\\*.exe\",\"C:\\\\Windows\\\\Resources\\\\*.exe\",\n \"C:\\\\Windows\\\\rescache\\\\*.exe\",\"C:\\\\Windows\\\\Provisioning\\\\*.exe\",\"C:\\\\Windows\\\\PrintDialog\\\\*.exe\",\"C:\\\\Windows\\\\PolicyDefinitions\\\\*.exe\",\"C:\\\\Windows\\\\media\\\\*.exe\",\n \"C:\\\\Windows\\\\Globalization\\\\*.exe\",\"C:\\\\Windows\\\\L2Schemas\\\\*.exe\",\"C:\\\\Windows\\\\LiveKernelReports\\\\*.exe\",\"C:\\\\Windows\\\\ModemLogs\\\\*.exe\",\"C:\\\\Windows\\\\ImmersiveControlPanel\\\\*.exe\") and\n not process.name : (\"SpeechUXWiz.exe\",\"SystemSettings.exe\",\"TrustedInstaller.exe\",\"PrintDialog.exe\",\"MpSigStub.exe\",\"LMS.exe\",\"mpam-*.exe\")\n /* uncomment once in winlogbeat */\n /* and not (process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true) */\n", + "risk_score": 47, + "rule_id": "ebfe1448-7fac-4d59-acea-181bd89b1f7f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_path_cmdline.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_path_cmdline.json new file mode 100644 index 0000000000000..6c3ffac1d4605 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_from_unusual_path_cmdline.json @@ -0,0 +1,28 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies process execution from suspicious default Windows directories. This may be abused by adversaries to hide malware in trusted paths.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Execution from Unusual Directory - Command Line", + "note": "This is related to the Process Execution from an Unusual Directory rule", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.name : (\"wscript.exe\",\"cscript.exe\",\"rundll32.exe\",\"regsvr32.exe\",\"cmstp.exe\",\"RegAsm.exe\",\"installutil.exe\",\"mshta.exe\",\"RegSvcs.exe\") and\n /* add suspicious execution paths here */\nprocess.args : (\"C:\\\\PerfLogs\\\\*\",\"C:\\\\Users\\\\Public\\\\*\",\"C:\\\\Users\\\\Default\\\\*\",\"C:\\\\Windows\\\\Tasks\\\\*\",\"C:\\\\Intel\\\\*\", \"C:\\\\AMD\\\\Temp\\\\*\", \n \"C:\\\\Windows\\\\AppReadiness\\\\*\", \"C:\\\\Windows\\\\ServiceState\\\\*\",\"C:\\\\Windows\\\\security\\\\*\",\"C:\\\\Windows\\\\IdentityCRL\\\\*\",\"C:\\\\Windows\\\\Branding\\\\*\",\"C:\\\\Windows\\\\csc\\\\*\",\n \"C:\\\\Windows\\\\DigitalLocker\\\\*\",\"C:\\\\Windows\\\\en-US\\\\*\",\"C:\\\\Windows\\\\wlansvc\\\\*\",\"C:\\\\Windows\\\\Prefetch\\\\*\",\"C:\\\\Windows\\\\Fonts\\\\*\",\n \"C:\\\\Windows\\\\diagnostics\\\\*\",\"C:\\\\Windows\\\\TAPI\\\\*\",\"C:\\\\Windows\\\\INF\\\\*\",\"C:\\\\Windows\\\\System32\\\\Speech\\\\*\",\"C:\\\\windows\\\\tracing\\\\*\",\n \"c:\\\\windows\\\\IME\\\\*\",\"c:\\\\Windows\\\\Performance\\\\*\",\"c:\\\\windows\\\\intel\\\\*\",\"c:\\\\windows\\\\ms\\\\*\",\"C:\\\\Windows\\\\dot3svc\\\\*\",\"C:\\\\Windows\\\\ServiceProfiles\\\\*\",\n \"C:\\\\Windows\\\\panther\\\\*\",\"C:\\\\Windows\\\\RemotePackages\\\\*\",\"C:\\\\Windows\\\\OCR\\\\*\",\"C:\\\\Windows\\\\appcompat\\\\*\",\"C:\\\\Windows\\\\apppatch\\\\*\",\"C:\\\\Windows\\\\addins\\\\*\",\n \"C:\\\\Windows\\\\Setup\\\\*\",\"C:\\\\Windows\\\\Help\\\\*\",\"C:\\\\Windows\\\\SKB\\\\*\",\"C:\\\\Windows\\\\Vss\\\\*\",\"C:\\\\Windows\\\\Web\\\\*\",\"C:\\\\Windows\\\\servicing\\\\*\",\"C:\\\\Windows\\\\CbsTemp\\\\*\",\n \"C:\\\\Windows\\\\Logs\\\\*\",\"C:\\\\Windows\\\\WaaS\\\\*\",\"C:\\\\Windows\\\\twain_32\\\\*\",\"C:\\\\Windows\\\\ShellExperiences\\\\*\",\"C:\\\\Windows\\\\ShellComponents\\\\*\",\"C:\\\\Windows\\\\PLA\\\\*\",\n \"C:\\\\Windows\\\\Migration\\\\*\",\"C:\\\\Windows\\\\debug\\\\*\",\"C:\\\\Windows\\\\Cursors\\\\*\",\"C:\\\\Windows\\\\Containers\\\\*\",\"C:\\\\Windows\\\\Boot\\\\*\",\"C:\\\\Windows\\\\bcastdvr\\\\*\",\n \"C:\\\\Windows\\\\assembly\\\\*\",\"C:\\\\Windows\\\\TextInput\\\\*\",\"C:\\\\Windows\\\\security\\\\*\",\"C:\\\\Windows\\\\schemas\\\\*\",\"C:\\\\Windows\\\\SchCache\\\\*\",\"C:\\\\Windows\\\\Resources\\\\*\",\n \"C:\\\\Windows\\\\rescache\\\\*\",\"C:\\\\Windows\\\\Provisioning\\\\*\",\"C:\\\\Windows\\\\PrintDialog\\\\*\",\"C:\\\\Windows\\\\PolicyDefinitions\\\\*\",\"C:\\\\Windows\\\\media\\\\*\",\n \"C:\\\\Windows\\\\Globalization\\\\*\",\"C:\\\\Windows\\\\L2Schemas\\\\*\",\"C:\\\\Windows\\\\LiveKernelReports\\\\*\",\"C:\\\\Windows\\\\ModemLogs\\\\*\",\"C:\\\\Windows\\\\ImmersiveControlPanel\\\\*\")\n", + "risk_score": 47, + "rule_id": "cff92c41-2225-4763-b4ce-6f71e5bda5e6", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripting_osascript_exec_followed_by_netcon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripting_osascript_exec_followed_by_netcon.json new file mode 100644 index 0000000000000..c4d58dd594c2a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripting_osascript_exec_followed_by_netcon.json @@ -0,0 +1,63 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects execution via the Apple script interpreter (osascript) followed by a network connection from the same process within a short time period. Adversaries may use malicious scripts for execution and command and control.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Apple Script Execution followed by Network Connection", + "query": "sequence by host.id, process.entity_id with maxspan=30s\n [process where event.type == \"start\" and process.name == \"osascript\"]\n [network where event.type != \"end\" and process.name == \"osascript\" and destination.ip != \"::1\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \n \"172.16.0.0/12\", \n \"192.168.0.0/16\", \n \"127.0.0.0/8\", \n \"169.254.0.0/16\", \n \"224.0.0.0/4\", \n \"FE80::/10\", \n \"FF00::/8\")\n ]\n", + "references": [ + "https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/index.html" + ], + "risk_score": 47, + "rule_id": "47f76567-d58a-4fed-b32b-21f571e28910", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Command and Control", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0011", + "name": "Command and Control", + "reference": "https://attack.mitre.org/tactics/TA0011/" + }, + "technique": [ + { + "id": "T1105", + "name": "Ingress Tool Transfer", + "reference": "https://attack.mitre.org/techniques/T1105/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripts_process_started_via_wmi.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripts_process_started_via_wmi.json new file mode 100644 index 0000000000000..a6bf38f6880ae --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_scripts_process_started_via_wmi.json @@ -0,0 +1,49 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies use of the built-in Windows script interpreters (cscript.exe or wscript.exe) being used to execute a process via Windows Management Instrumentation (WMI). This may be indicative of malicious activity.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Windows Script Interpreter Executing Process via WMI", + "query": "sequence by host.id with maxspan=5s\n [library where file.name : \"wmiutils.dll\" and process.name : (\"wscript.exe\", \"cscript.exe\")]\n [process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"wmiprvse.exe\" and\n user.domain != \"NT AUTHORITY\" and\n (process.pe.original_file_name in\n (\n \"cscript.exe\",\n \"wscript.exe\",\n \"PowerShell.EXE\",\n \"Cmd.Exe\",\n \"MSHTA.EXE\",\n \"RUNDLL32.EXE\",\n \"REGSVR32.EXE\",\n \"MSBuild.exe\",\n \"InstallUtil.exe\",\n \"RegAsm.exe\",\n \"RegSvcs.exe\",\n \"msxsl.exe\",\n \"CONTROL.EXE\",\n \"EXPLORER.EXE\",\n \"Microsoft.Workflow.Compiler.exe\",\n \"msiexec.exe\"\n ) or\n process.executable : (\"C:\\\\Users\\\\*.exe\", \"C:\\\\ProgramData\\\\*.exe\")\n )\n ]\n", + "risk_score": 47, + "rule_id": "b64b183e-1a76-422d-9179-7b389513e74d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1193", + "name": "Spearphishing Attachment", + "reference": "https://attack.mitre.org/techniques/T1193/" + }, + { + "id": "T1047", + "name": "Windows Management Instrumentation", + "reference": "https://attack.mitre.org/techniques/T1047/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shared_modules_local_sxs_dll.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shared_modules_local_sxs_dll.json new file mode 100644 index 0000000000000..9a8774efffb98 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shared_modules_local_sxs_dll.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation, change, or deletion of a DLL module within a Windows SxS local folder. Adversaries may abuse shared modules to execute malicious payloads by instructing the Windows module loader to load DLLs from arbitrary local paths.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Execution via local SxS Shared Module", + "note": "The SxS DotLocal folder is a legitimate feature that can be abused to hijack standard modules loading order by forcing an executable on the same application.exe.local folder to load a malicious DLL module from the same directory.", + "query": "file where file.extension : \"dll\" and file.path : \"C:\\\\*\\\\*.exe.local\\\\*.dll\"\n", + "references": [ + "https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-redirection" + ], + "risk_score": 43, + "rule_id": "a3ea12f3-0d4e-4667-8b44-4230c63f3c75", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1129", + "name": "Shared Modules", + "reference": "https://attack.mitre.org/techniques/T1129/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shell_execution_via_apple_scripting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shell_execution_via_apple_scripting.json new file mode 100644 index 0000000000000..e5c41d27cb4e6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_shell_execution_via_apple_scripting.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of the shell process (sh) via scripting (JXA or AppleScript). Adversaries may use the doShellScript functionality in JXA or do shell script in AppleScript to execute system commands.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Shell Execution via Apple Scripting", + "query": "sequence by host.id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"osascript\"] by process.pid\n [process where event.type in (\"start\", \"process_started\") and process.name == \"sh\" and process.args == \"-c\"] by process.ppid\n", + "references": [ + "https://developer.apple.com/library/archive/technotes/tn2065/_index.html", + "https://objectivebythesea.com/v2/talks/OBTS_v2_Thomas.pdf" + ], + "risk_score": 47, + "rule_id": "d461fac0-43e8-49e2-85ea-3a58fe120b4f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_image_load_wmi_ms_office.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_image_load_wmi_ms_office.json new file mode 100644 index 0000000000000..fb292015f4ae3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_image_load_wmi_ms_office.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a suspicious image load (wmiutils.dll) from Microsoft Office processes. This behavior may indicate adversarial activity where child processes are spawned via Windows Management Instrumentation (WMI). This technique can be used to execute code and evade traditional parent/child processes spawned from MS Office products.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious WMI Image Load from MS Office", + "query": "library where process.name in (\"WINWORD.EXE\", \"EXCEL.EXE\", \"POWERPNT.EXE\", \"MSPUB.EXE\", \"MSACCESS.EXE\") and\n event.action == \"load\" and\n event.category == \"library\" and\n file.name == \"wmiutils.dll\"\n", + "references": [ + "https://medium.com/threatpunter/detecting-adversary-tradecraft-with-image-load-event-logging-and-eql-8de93338c16" + ], + "risk_score": 21, + "rule_id": "891cb88e-441a-4c3e-be2d-120d99fe7b0d", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1047", + "name": "Windows Management Instrumentation", + "reference": "https://attack.mitre.org/techniques/T1047/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json index 90c60ceea37ab..03e759e0529ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_ms_office_child_process.json @@ -8,13 +8,13 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "Suspicious MS Office Child Process", - "query": "event.category:process and event.type:(start or process_started) and process.parent.name:(eqnedt32.exe or excel.exe or fltldr.exe or msaccess.exe or mspub.exe or powerpnt.exe or winword.exe) and process.name:(Microsoft.Workflow.Compiler.exe or arp.exe or atbroker.exe or bginfo.exe or bitsadmin.exe or cdb.exe or certutil.exe or cmd.exe or cmstp.exe or cscript.exe or csi.exe or dnx.exe or dsget.exe or dsquery.exe or forfiles.exe or fsi.exe or ftp.exe or gpresult.exe or hostname.exe or ieexec.exe or iexpress.exe or installutil.exe or ipconfig.exe or mshta.exe or msxsl.exe or nbtstat.exe or net.exe or net1.exe or netsh.exe or netstat.exe or nltest.exe or odbcconf.exe or ping.exe or powershell.exe or pwsh.exe or qprocess.exe or quser.exe or qwinsta.exe or rcsi.exe or reg.exe or regasm.exe or regsvcs.exe or regsvr32.exe or sc.exe or schtasks.exe or systeminfo.exe or tasklist.exe or tracert.exe or whoami.exe or wmic.exe or wscript.exe or xwizard.exe)", - "risk_score": 21, + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name : (\"eqnedt32.exe\", \"excel.exe\", \"fltldr.exe\", \"msaccess.exe\", \"mspub.exe\", \"powerpnt.exe\", \"winword.exe\") and\n process.name : (\"Microsoft.Workflow.Compiler.exe\", \"arp.exe\", \"atbroker.exe\", \"bginfo.exe\", \"bitsadmin.exe\", \"cdb.exe\", \"certutil.exe\",\n \"cmd.exe\", \"cmstp.exe\", \"cscript.exe\", \"csi.exe\", \"dnx.exe\", \"dsget.exe\", \"dsquery.exe\", \"forfiles.exe\", \"fsi.exe\",\n \"ftp.exe\", \"gpresult.exe\", \"hostname.exe\", \"ieexec.exe\", \"iexpress.exe\", \"installutil.exe\", \"ipconfig.exe\", \"mshta.exe\",\n \"msxsl.exe\", \"nbtstat.exe\", \"net.exe\", \"net1.exe\", \"netsh.exe\", \"netstat.exe\", \"nltest.exe\", \"odbcconf.exe\", \"ping.exe\",\n \"powershell.exe\", \"pwsh.exe\", \"qprocess.exe\", \"quser.exe\", \"qwinsta.exe\", \"rcsi.exe\", \"reg.exe\", \"regasm.exe\", \"regsvcs.exe\",\n \"regsvr32.exe\", \"sc.exe\", \"schtasks.exe\", \"systeminfo.exe\", \"tasklist.exe\", \"tracert.exe\", \"whoami.exe\",\n \"wmic.exe\", \"wscript.exe\", \"xwizard.exe\", \"explorer.exe\", \"rundll32.exe\", \"hh.exe\")\n", + "risk_score": 47, "rule_id": "a624863f-a70d-417f-a7d2-7a404638d47f", - "severity": "low", + "severity": "medium", "tags": [ "Elastic", "Host", @@ -39,6 +39,6 @@ ] } ], - "type": "query", - "version": 5 + "type": "eql", + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json index 205b5148f2fb4..039953a9fccd8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_psexesvc.json @@ -8,10 +8,10 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "Suspicious Process Execution via Renamed PsExec Executable", - "query": "event.category:process and event.type:(start or process_started) and (process.pe.original_file_name:(psexesvc.exe or PSEXESVC.exe) or winlog.event_data.OriginalFileName:(psexesvc.exe or PSEXESVC.exe)) and process.parent.name:services.exe and not process.name:(psexesvc.exe or PSEXESVC.exe)", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.pe.original_file_name : \"psexesvc.exe\" and not process.name : \"PSEXESVC.exe\"\n", "risk_score": 47, "rule_id": "e2f9fdf5-8076-45ad-9427-41e0e03dc9c2", "severity": "medium", @@ -39,6 +39,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_short_program_name.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_short_program_name.json new file mode 100644 index 0000000000000..a8d91c127826e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_suspicious_short_program_name.json @@ -0,0 +1,27 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies process execution with a single character process name. This is often done by adversaries while staging or executing temporary utilities.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Execution - Short Program Name", + "query": "process where event.type in (\"start\", \"process_started\") and length(process.name) > 0 and\n length(process.name) == 5 and host.os.name == \"Windows\" and length(process.pe.original_file_name) > 5\n", + "risk_score": 47, + "rule_id": "17c7f6a5-5bc9-4e1f-92bf-13632d24384d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_file_writes.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_file_writes.json index fb77c4c78240c..38454b3de3c69 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_file_writes.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_dns_service_file_writes.json @@ -11,7 +11,7 @@ "license": "Elastic License", "name": "Unusual File Modification by dns.exe", "note": "### Investigating Unusual File Write\nDetection alerts from this rule indicate potential unusual/abnormal file writes from the DNS Server service process (`dns.exe`) after exploitation from CVE-2020-1350 (SigRed) has occurred. Here are some possible avenues of investigation:\n- Post-exploitation, adversaries may write additional files or payloads to the system as additional discovery/exploitation/persistence mechanisms.\n- Any suspicious or abnormal files written from `dns.exe` should be reviewed and investigated with care.", - "query": "event.category:file and process.name:dns.exe and not file.name:dns.log", + "query": "event.category:file and process.name:dns.exe and event.type:(creation or deletion or change) and not file.name:dns.log", "references": [ "https://research.checkpoint.com/2020/resolving-your-way-into-domain-admin-exploiting-a-17-year-old-bug-in-windows-dns-servers/", "https://msrc-blog.microsoft.com/2020/07/14/july-2020-security-update-cve-2020-1350-vulnerability-in-windows-domain-name-system-dns-server/" @@ -44,5 +44,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json index 7a7aec00cc887..573b36ed2decf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_unusual_network_connection_via_rundll32.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "Identifies unusual instances of rundll32.exe making outbound network connections. This may indicate adversarial activity and may identify malicious DLLs.", + "description": "Identifies unusual instances of rundll32.exe making outbound network connections. This may indicate adversarial Command and Control activity.", "from": "now-9m", "index": [ "winlogbeat-*", @@ -11,10 +11,11 @@ "language": "eql", "license": "Elastic License", "name": "Unusual Network Connection via RunDLL32", - "query": "sequence by process.entity_id\n [process where process.name : \"rundll32.exe\" and event.type == \"start\"]\n [network where process.name : \"rundll32.exe\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\", \"127.0.0.0/8\")]\n", - "risk_score": 21, + "query": "sequence by host.id, process.entity_id with maxspan=1m\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name : \"rundll32.exe\" and process.args_count == 1]\n [network where process.name : \"rundll32.exe\" and network.protocol != \"dns\" and network.direction == \"outgoing\" and\n not cidrmatch(destination.ip, \"10.0.0.0/8\", \"172.16.0.0/12\", \"192.168.0.0/16\", \"127.0.0.0/8\")]\n", + "risk_score": 47, + "rule_id": "52aaab7b-b51c-441a-89ce-4387b3aea886", - "severity": "low", + "severity": "medium", "tags": [ "Elastic", "Host", @@ -40,5 +41,5 @@ } ], "type": "eql", - "version": 6 + "version": 7 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_explorer_suspicious_child_parent_args.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_explorer_suspicious_child_parent_args.json new file mode 100644 index 0000000000000..001b2d4043b4d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_explorer_suspicious_child_parent_args.json @@ -0,0 +1,54 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a suspicious Windows explorer child process. Explorer.exe can be abused to launch malicious scripts or executables from a trusted parent process.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Explorer Child Process", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name : (\"cscript.exe\", \"wscript.exe\", \"powershell.exe\", \"rundll32.exe\", \"cmd.exe\", \"mshta.exe\", \"regsvr32.exe\") and\n /* Explorer started via DCOM */\n process.parent.name : \"explorer.exe\" and process.parent.args : \"-Embedding\"\n", + "risk_score": 43, + "rule_id": "9a5b4e31-6cde-4295-9ff7-6be1b8567e1b", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1064", + "name": "Scripting", + "reference": "https://attack.mitre.org/techniques/T1064/" + }, + { + "id": "T1192", + "name": "Spearphishing Link", + "reference": "https://attack.mitre.org/techniques/T1192/" + }, + { + "id": "T1193", + "name": "Spearphishing Attachment", + "reference": "https://attack.mitre.org/techniques/T1193/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json index 13493a90e3e50..1e734bbc247ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/execution_via_system_manager.json @@ -44,17 +44,7 @@ "id": "T1064", "name": "Scripting", "reference": "https://attack.mitre.org/techniques/T1064/" - } - ] - }, - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0002", - "name": "Execution", - "reference": "https://attack.mitre.org/tactics/TA0002/" - }, - "technique": [ + }, { "id": "T1086", "name": "PowerShell", @@ -64,5 +54,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_creation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_creation.json new file mode 100644 index 0000000000000..e56773fc9a004 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_creation.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a transport rule creation in Microsoft 365. Exchange Online mail transport rules should be set to not forward email to domains outside of your organization as a best practice. An adversary may create transport rules to exfiltrate data.", + "false_positives": [ + "A new transport rule may be created by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Transport Rule Creation", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"New-TransportRule\" and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/new-transportrule?view=exchange-ps", + "https://docs.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules" + ], + "risk_score": 47, + "rule_id": "ff4dd44a-0ac6-44c4-8609-3f81bc820f02", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0010", + "name": "Exfiltration", + "reference": "https://attack.mitre.org/tactics/TA0010/" + }, + "technique": [ + { + "id": "T1537", + "name": "Transfer Data to Cloud Account", + "reference": "https://attack.mitre.org/techniques/T1537/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_mod.json new file mode 100644 index 0000000000000..1b8243c67d1a2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_microsoft_365_exchange_transport_rule_mod.json @@ -0,0 +1,53 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a transport rule has been disabled or deleted in Microsoft 365. Mail flow rules (also known as transport rules) are used to identify and take action on messages that flow through your organization. An adversary or insider threat may modify a transport rule to exfiltrate data or evade defenses.", + "false_positives": [ + "A transport rule may be modified by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Transport Rule Modification", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-TransportRule\" or \"Disable-TransportRule\") and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-transportrule?view=exchange-ps", + "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-transportrule?view=exchange-ps", + "https://docs.microsoft.com/en-us/exchange/security-and-compliance/mail-flow-rules/mail-flow-rules" + ], + "risk_score": 47, + "rule_id": "272a6484-2663-46db-a532-ef734bf9a796", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0010", + "name": "Exfiltration", + "reference": "https://attack.mitre.org/tactics/TA0010/" + }, + "technique": [ + { + "id": "T1537", + "name": "Transfer Data to Cloud Account", + "reference": "https://attack.mitre.org/techniques/T1537/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_winrar_encryption.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_winrar_encryption.json new file mode 100644 index 0000000000000..8be2a1f77f0a7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/exfiltration_winrar_encryption.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies use of WinRar or 7z to create an encrypted files. Adversaries will often compress and encrypt data in preparation for exfiltration.", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Encrypting Files with WinRar or 7z", + "query": "process where event.type in (\"start\", \"process_started\") and\n ((process.name:\"rar.exe\" or process.code_signature.subject_name == \"win.rar GmbH\" or\n process.pe.original_file_name == \"Command line RAR\") and\n process.args == \"a\" and process.args : (\"-hp*\", \"-p*\", \"-dw\", \"-tb\", \"-ta\", \"/hp*\", \"/p*\", \"/dw\", \"/tb\", \"/ta\"))\n\n or\n (process.pe.original_file_name in (\"7z.exe\", \"7za.exe\") and\n process.args == \"a\" and process.args : (\"-p*\", \"-sdel\"))\n\n /* uncomment if noisy for backup software related FPs */\n /* not process.parent.executable : (\"C:\\\\Program Files\\\\*.exe\", \"C:\\\\Program Files (x86)\\\\*.exe\") */\n", + "references": [ + "https://www.welivesecurity.com/2020/12/02/turla-crutch-keeping-back-door-open/" + ], + "risk_score": 47, + "rule_id": "45d273fb-1dca-457d-9855-bcb302180c21", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Exfiltration" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0010", + "name": "Exfiltration", + "reference": "https://attack.mitre.org/tactics/TA0010/" + }, + "technique": [ + { + "id": "T1560", + "name": "Archive Collected Data", + "reference": "https://attack.mitre.org/techniques/T1560/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_admin_role_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_admin_role_deletion.json new file mode 100644 index 0000000000000..3703faa62ddf3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_admin_role_deletion.json @@ -0,0 +1,35 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when a custom admin role is deleted. An adversary may delete a custom admin role in order to impact the permissions or capabilities of system administrators.", + "false_positives": [ + "Google Workspace admin roles may be deleted by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Google Workspace Admin Role Deletion", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:DELETE_ROLE", + "references": [ + "https://support.google.com/a/answer/2406043?hl=en" + ], + "risk_score": 47, + "rule_id": "93e63c3e-4154-4fc6-9f86-b411e0987bbf", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_mfa_enforcement_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_mfa_enforcement_disabled.json new file mode 100644 index 0000000000000..9fcf76532bdd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_mfa_enforcement_disabled.json @@ -0,0 +1,35 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when multi-factor authentication (MFA) enforcement is disabled for Google Workspace users. An adversary may disable MFA enforcement in order to weaken an organization\u2019s security controls.", + "false_positives": [ + "MFA policies may be modified by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Google Workspace MFA Enforcement Disabled", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:ENFORCE_STRONG_AUTHENTICATION and gsuite.admin.new_value:false", + "references": [ + "https://support.google.com/a/answer/9176657?hl=en#" + ], + "risk_score": 47, + "rule_id": "cad4500a-abd7-4ef3-b5d3-95524de7cfe1", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_policy_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_policy_modified.json new file mode 100644 index 0000000000000..2db5d9260730e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/google_workspace_policy_modified.json @@ -0,0 +1,32 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when a Google Workspace password policy is modified. An adversary may attempt to modify a password policy in order to weaken an organization\u2019s security controls.", + "false_positives": [ + "Password policies may be modified by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Google Workspace Password Policy Modified", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:(CHANGE_APPLICATION_SETTING or CREATE_APPLICATION_SETTING) and gsuite.admin.setting.name:( \"Password Management - Enforce strong password\" or \"Password Management - Password reset frequency\" or \"Password Management - Enable password reuse\" or \"Password Management - Enforce password policy at next login\" or \"Password Management - Minimum password length\" or \"Password Management - Maximum password length\" )", + "risk_score": 47, + "rule_id": "a99f82f5-8e77-4f8b-b3ce-10c0f6afbc73", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json index f2ad30fa26020..e0bf31d9596bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_attempt_to_revoke_okta_api_token.json @@ -13,7 +13,7 @@ "language": "kuery", "license": "Elastic License", "name": "Attempt to Revoke Okta API Token", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:system.api_token.revoke", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -48,5 +48,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json index d1852478c666f..7d4acfc42db9c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/impact_possible_okta_dos_attack.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to disrupt an organization's business operations by performing a denial of service (DoS) attack against its Okta infrastructure.", + "description": "Detects possible Denial of Service (DoS) attacks against an Okta organization. An adversary may attempt to disrupt an organization's business operations by performing a DoS attack against its Okta service.", "index": [ "filebeat-*", "logs-okta*" @@ -10,7 +10,7 @@ "language": "kuery", "license": "Elastic License", "name": "Possible Okta DoS Attack", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:(application.integration.rate_limit_exceeded or system.org.rate_limit.warning or system.org.rate_limit.violation or core.concurrency.org.limit.violation)", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -50,5 +50,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts index 935cf52d986ff..1fa1bfe57b483 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/index.ts @@ -168,14 +168,14 @@ import rule156 from './impact_iam_group_deletion.json'; import rule157 from './impact_possible_okta_dos_attack.json'; import rule158 from './impact_rds_cluster_deletion.json'; import rule159 from './initial_access_suspicious_activity_reported_by_okta_user.json'; -import rule160 from './okta_attempt_to_deactivate_okta_mfa_rule.json'; -import rule161 from './okta_attempt_to_modify_okta_mfa_rule.json'; +import rule160 from './okta_attempt_to_deactivate_okta_policy.json'; +import rule161 from './okta_attempt_to_deactivate_okta_policy_rule.json'; import rule162 from './okta_attempt_to_modify_okta_network_zone.json'; import rule163 from './okta_attempt_to_modify_okta_policy.json'; -import rule164 from './okta_threat_detected_by_okta_threatinsight.json'; -import rule165 from './persistence_administrator_privileges_assigned_to_okta_group.json'; -import rule166 from './persistence_attempt_to_create_okta_api_token.json'; -import rule167 from './persistence_attempt_to_deactivate_okta_policy.json'; +import rule164 from './okta_attempt_to_modify_okta_policy_rule.json'; +import rule165 from './okta_threat_detected_by_okta_threatinsight.json'; +import rule166 from './persistence_administrator_privileges_assigned_to_okta_group.json'; +import rule167 from './persistence_attempt_to_create_okta_api_token.json'; import rule168 from './persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json'; import rule169 from './defense_evasion_cloudtrail_logging_deleted.json'; import rule170 from './defense_evasion_ec2_network_acl_deletion.json'; @@ -226,105 +226,240 @@ import rule214 from './credential_access_domain_backup_dpapi_private_keys.json'; import rule215 from './persistence_gpo_schtask_service_creation.json'; import rule216 from './credential_access_compress_credentials_keychains.json'; import rule217 from './credential_access_kerberosdump_kcc.json'; -import rule218 from './execution_suspicious_psexesvc.json'; -import rule219 from './execution_via_xp_cmdshell_mssql_stored_procedure.json'; -import rule220 from './privilege_escalation_printspooler_service_suspicious_file.json'; -import rule221 from './privilege_escalation_printspooler_suspicious_spl_file.json'; -import rule222 from './defense_evasion_azure_diagnostic_settings_deletion.json'; -import rule223 from './execution_command_virtual_machine.json'; -import rule224 from './execution_via_hidden_shell_conhost.json'; -import rule225 from './impact_resource_group_deletion.json'; -import rule226 from './persistence_via_telemetrycontroller_scheduledtask_hijack.json'; -import rule227 from './persistence_via_update_orchestrator_service_hijack.json'; -import rule228 from './collection_update_event_hub_auth_rule.json'; -import rule229 from './credential_access_iis_apppoolsa_pwd_appcmd.json'; -import rule230 from './credential_access_iis_connectionstrings_dumping.json'; -import rule231 from './defense_evasion_event_hub_deletion.json'; -import rule232 from './defense_evasion_firewall_policy_deletion.json'; -import rule233 from './defense_evasion_sdelete_like_filename_rename.json'; -import rule234 from './lateral_movement_remote_ssh_login_enabled.json'; -import rule235 from './persistence_azure_automation_account_created.json'; -import rule236 from './persistence_azure_automation_runbook_created_or_modified.json'; -import rule237 from './persistence_azure_automation_webhook_created.json'; -import rule238 from './privilege_escalation_uac_bypass_diskcleanup_hijack.json'; -import rule239 from './credential_access_attempts_to_brute_force_okta_user_account.json'; -import rule240 from './credential_access_storage_account_key_regenerated.json'; -import rule241 from './defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json'; -import rule242 from './defense_evasion_system_critical_proc_abnormal_file_activity.json'; -import rule243 from './defense_evasion_unusual_system_vp_child_program.json'; -import rule244 from './discovery_blob_container_access_mod.json'; -import rule245 from './persistence_mfa_disabled_for_azure_user.json'; -import rule246 from './persistence_user_added_as_owner_for_azure_application.json'; -import rule247 from './persistence_user_added_as_owner_for_azure_service_principal.json'; -import rule248 from './defense_evasion_dotnet_compiler_parent_process.json'; -import rule249 from './defense_evasion_suspicious_managedcode_host_process.json'; -import rule250 from './execution_command_shell_started_by_unusual_process.json'; -import rule251 from './defense_evasion_masquerading_as_elastic_endpoint_process.json'; -import rule252 from './defense_evasion_masquerading_suspicious_werfault_childproc.json'; -import rule253 from './defense_evasion_masquerading_werfault.json'; -import rule254 from './credential_access_key_vault_modified.json'; -import rule255 from './credential_access_mimikatz_memssp_default_logs.json'; -import rule256 from './defense_evasion_code_injection_conhost.json'; -import rule257 from './defense_evasion_network_watcher_deletion.json'; -import rule258 from './initial_access_external_guest_user_invite.json'; -import rule259 from './defense_evasion_masquerading_renamed_autoit.json'; -import rule260 from './impact_azure_automation_runbook_deleted.json'; -import rule261 from './initial_access_consent_grant_attack_via_azure_registered_application.json'; -import rule262 from './persistence_azure_conditional_access_policy_modified.json'; -import rule263 from './persistence_azure_privileged_identity_management_role_modified.json'; -import rule264 from './command_and_control_teamviewer_remote_file_copy.json'; -import rule265 from './defense_evasion_installutil_beacon.json'; -import rule266 from './defense_evasion_mshta_beacon.json'; -import rule267 from './defense_evasion_network_connection_from_windows_binary.json'; -import rule268 from './defense_evasion_rundll32_no_arguments.json'; -import rule269 from './defense_evasion_suspicious_scrobj_load.json'; -import rule270 from './defense_evasion_suspicious_wmi_script.json'; -import rule271 from './execution_ms_office_written_file.json'; -import rule272 from './execution_pdf_written_file.json'; -import rule273 from './lateral_movement_cmd_service.json'; -import rule274 from './persistence_app_compat_shim.json'; -import rule275 from './command_and_control_remote_file_copy_desktopimgdownldr.json'; -import rule276 from './command_and_control_remote_file_copy_mpcmdrun.json'; -import rule277 from './defense_evasion_execution_suspicious_explorer_winword.json'; -import rule278 from './defense_evasion_suspicious_zoom_child_process.json'; -import rule279 from './ml_linux_anomalous_compiler_activity.json'; -import rule280 from './ml_linux_anomalous_kernel_module_arguments.json'; -import rule281 from './ml_linux_anomalous_sudo_activity.json'; -import rule282 from './ml_linux_system_information_discovery.json'; -import rule283 from './ml_linux_system_network_configuration_discovery.json'; -import rule284 from './ml_linux_system_network_connection_discovery.json'; -import rule285 from './ml_linux_system_process_discovery.json'; -import rule286 from './ml_linux_system_user_discovery.json'; -import rule287 from './discovery_post_exploitation_public_ip_reconnaissance.json'; -import rule288 from './initial_access_zoom_meeting_with_no_passcode.json'; -import rule289 from './defense_evasion_gcp_logging_sink_deletion.json'; -import rule290 from './defense_evasion_gcp_pub_sub_topic_deletion.json'; -import rule291 from './credential_access_gcp_iam_service_account_key_deletion.json'; -import rule292 from './credential_access_gcp_key_created_for_service_account.json'; -import rule293 from './defense_evasion_gcp_firewall_rule_created.json'; -import rule294 from './defense_evasion_gcp_firewall_rule_deleted.json'; -import rule295 from './defense_evasion_gcp_firewall_rule_modified.json'; -import rule296 from './defense_evasion_gcp_logging_bucket_deletion.json'; -import rule297 from './defense_evasion_gcp_storage_bucket_permissions_modified.json'; -import rule298 from './impact_gcp_storage_bucket_deleted.json'; -import rule299 from './initial_access_gcp_iam_custom_role_creation.json'; -import rule300 from './defense_evasion_gcp_storage_bucket_configuration_modified.json'; -import rule301 from './exfiltration_gcp_logging_sink_modification.json'; -import rule302 from './impact_gcp_iam_role_deletion.json'; -import rule303 from './impact_gcp_service_account_deleted.json'; -import rule304 from './impact_gcp_service_account_disabled.json'; -import rule305 from './impact_gcp_virtual_private_cloud_network_deleted.json'; -import rule306 from './impact_gcp_virtual_private_cloud_route_created.json'; -import rule307 from './impact_gcp_virtual_private_cloud_route_deleted.json'; -import rule308 from './ml_linux_anomalous_metadata_process.json'; -import rule309 from './ml_linux_anomalous_metadata_user.json'; -import rule310 from './ml_windows_anomalous_metadata_process.json'; -import rule311 from './ml_windows_anomalous_metadata_user.json'; -import rule312 from './persistence_gcp_service_account_created.json'; -import rule313 from './collection_gcp_pub_sub_subscription_creation.json'; -import rule314 from './collection_gcp_pub_sub_topic_creation.json'; -import rule315 from './defense_evasion_gcp_pub_sub_subscription_deletion.json'; -import rule316 from './persistence_azure_pim_user_added_global_admin.json'; +import rule218 from './defense_evasion_attempt_del_quarantine_attrib.json'; +import rule219 from './execution_suspicious_psexesvc.json'; +import rule220 from './execution_via_xp_cmdshell_mssql_stored_procedure.json'; +import rule221 from './privilege_escalation_printspooler_service_suspicious_file.json'; +import rule222 from './privilege_escalation_printspooler_suspicious_spl_file.json'; +import rule223 from './defense_evasion_azure_diagnostic_settings_deletion.json'; +import rule224 from './execution_command_virtual_machine.json'; +import rule225 from './execution_via_hidden_shell_conhost.json'; +import rule226 from './impact_resource_group_deletion.json'; +import rule227 from './persistence_via_telemetrycontroller_scheduledtask_hijack.json'; +import rule228 from './persistence_via_update_orchestrator_service_hijack.json'; +import rule229 from './collection_update_event_hub_auth_rule.json'; +import rule230 from './credential_access_iis_apppoolsa_pwd_appcmd.json'; +import rule231 from './credential_access_iis_connectionstrings_dumping.json'; +import rule232 from './defense_evasion_event_hub_deletion.json'; +import rule233 from './defense_evasion_firewall_policy_deletion.json'; +import rule234 from './defense_evasion_sdelete_like_filename_rename.json'; +import rule235 from './lateral_movement_remote_ssh_login_enabled.json'; +import rule236 from './persistence_azure_automation_account_created.json'; +import rule237 from './persistence_azure_automation_runbook_created_or_modified.json'; +import rule238 from './persistence_azure_automation_webhook_created.json'; +import rule239 from './privilege_escalation_uac_bypass_diskcleanup_hijack.json'; +import rule240 from './credential_access_attempts_to_brute_force_okta_user_account.json'; +import rule241 from './credential_access_storage_account_key_regenerated.json'; +import rule242 from './defense_evasion_suspicious_okta_user_password_reset_or_unlock_attempts.json'; +import rule243 from './defense_evasion_system_critical_proc_abnormal_file_activity.json'; +import rule244 from './defense_evasion_unusual_system_vp_child_program.json'; +import rule245 from './discovery_blob_container_access_mod.json'; +import rule246 from './persistence_mfa_disabled_for_azure_user.json'; +import rule247 from './persistence_user_added_as_owner_for_azure_application.json'; +import rule248 from './persistence_user_added_as_owner_for_azure_service_principal.json'; +import rule249 from './defense_evasion_dotnet_compiler_parent_process.json'; +import rule250 from './defense_evasion_suspicious_managedcode_host_process.json'; +import rule251 from './execution_command_shell_started_by_unusual_process.json'; +import rule252 from './defense_evasion_masquerading_as_elastic_endpoint_process.json'; +import rule253 from './defense_evasion_masquerading_suspicious_werfault_childproc.json'; +import rule254 from './defense_evasion_masquerading_werfault.json'; +import rule255 from './credential_access_key_vault_modified.json'; +import rule256 from './credential_access_mimikatz_memssp_default_logs.json'; +import rule257 from './defense_evasion_code_injection_conhost.json'; +import rule258 from './defense_evasion_network_watcher_deletion.json'; +import rule259 from './initial_access_external_guest_user_invite.json'; +import rule260 from './defense_evasion_masquerading_renamed_autoit.json'; +import rule261 from './impact_azure_automation_runbook_deleted.json'; +import rule262 from './initial_access_consent_grant_attack_via_azure_registered_application.json'; +import rule263 from './persistence_azure_conditional_access_policy_modified.json'; +import rule264 from './persistence_azure_privileged_identity_management_role_modified.json'; +import rule265 from './command_and_control_teamviewer_remote_file_copy.json'; +import rule266 from './defense_evasion_installutil_beacon.json'; +import rule267 from './defense_evasion_mshta_beacon.json'; +import rule268 from './defense_evasion_network_connection_from_windows_binary.json'; +import rule269 from './defense_evasion_rundll32_no_arguments.json'; +import rule270 from './defense_evasion_suspicious_scrobj_load.json'; +import rule271 from './defense_evasion_suspicious_wmi_script.json'; +import rule272 from './execution_ms_office_written_file.json'; +import rule273 from './execution_pdf_written_file.json'; +import rule274 from './lateral_movement_cmd_service.json'; +import rule275 from './persistence_app_compat_shim.json'; +import rule276 from './command_and_control_remote_file_copy_desktopimgdownldr.json'; +import rule277 from './command_and_control_remote_file_copy_mpcmdrun.json'; +import rule278 from './defense_evasion_execution_suspicious_explorer_winword.json'; +import rule279 from './defense_evasion_suspicious_zoom_child_process.json'; +import rule280 from './ml_linux_anomalous_compiler_activity.json'; +import rule281 from './ml_linux_anomalous_kernel_module_arguments.json'; +import rule282 from './ml_linux_anomalous_sudo_activity.json'; +import rule283 from './ml_linux_system_information_discovery.json'; +import rule284 from './ml_linux_system_network_configuration_discovery.json'; +import rule285 from './ml_linux_system_network_connection_discovery.json'; +import rule286 from './ml_linux_system_process_discovery.json'; +import rule287 from './ml_linux_system_user_discovery.json'; +import rule288 from './discovery_post_exploitation_public_ip_reconnaissance.json'; +import rule289 from './initial_access_zoom_meeting_with_no_passcode.json'; +import rule290 from './defense_evasion_gcp_logging_sink_deletion.json'; +import rule291 from './defense_evasion_gcp_pub_sub_topic_deletion.json'; +import rule292 from './credential_access_gcp_iam_service_account_key_deletion.json'; +import rule293 from './credential_access_gcp_key_created_for_service_account.json'; +import rule294 from './defense_evasion_gcp_firewall_rule_created.json'; +import rule295 from './defense_evasion_gcp_firewall_rule_deleted.json'; +import rule296 from './defense_evasion_gcp_firewall_rule_modified.json'; +import rule297 from './defense_evasion_gcp_logging_bucket_deletion.json'; +import rule298 from './defense_evasion_gcp_storage_bucket_permissions_modified.json'; +import rule299 from './impact_gcp_storage_bucket_deleted.json'; +import rule300 from './initial_access_gcp_iam_custom_role_creation.json'; +import rule301 from './defense_evasion_gcp_storage_bucket_configuration_modified.json'; +import rule302 from './exfiltration_gcp_logging_sink_modification.json'; +import rule303 from './impact_gcp_iam_role_deletion.json'; +import rule304 from './impact_gcp_service_account_deleted.json'; +import rule305 from './impact_gcp_service_account_disabled.json'; +import rule306 from './impact_gcp_virtual_private_cloud_network_deleted.json'; +import rule307 from './impact_gcp_virtual_private_cloud_route_created.json'; +import rule308 from './impact_gcp_virtual_private_cloud_route_deleted.json'; +import rule309 from './ml_linux_anomalous_metadata_process.json'; +import rule310 from './ml_linux_anomalous_metadata_user.json'; +import rule311 from './ml_windows_anomalous_metadata_process.json'; +import rule312 from './ml_windows_anomalous_metadata_user.json'; +import rule313 from './persistence_gcp_service_account_created.json'; +import rule314 from './collection_gcp_pub_sub_subscription_creation.json'; +import rule315 from './collection_gcp_pub_sub_topic_creation.json'; +import rule316 from './defense_evasion_gcp_pub_sub_subscription_deletion.json'; +import rule317 from './persistence_azure_pim_user_added_global_admin.json'; +import rule318 from './command_and_control_cobalt_strike_default_teamserver_cert.json'; +import rule319 from './defense_evasion_enable_inbound_rdp_with_netsh.json'; +import rule320 from './defense_evasion_execution_lolbas_wuauclt.json'; +import rule321 from './privilege_escalation_unusual_svchost_childproc_childless.json'; +import rule322 from './lateral_movement_rdp_tunnel_plink.json'; +import rule323 from './privilege_escalation_uac_bypass_winfw_mmc_hijack.json'; +import rule324 from './persistence_ms_office_addins_file.json'; +import rule325 from './discovery_adfind_command_activity.json'; +import rule326 from './discovery_security_software_wmic.json'; +import rule327 from './execution_command_shell_via_rundll32.json'; +import rule328 from './lateral_movement_suspicious_cmd_wmi.json'; +import rule329 from './lateral_movement_via_startup_folder_rdp_smb.json'; +import rule330 from './privilege_escalation_uac_bypass_com_interface_icmluautil.json'; +import rule331 from './privilege_escalation_uac_bypass_mock_windir.json'; +import rule332 from './defense_evasion_potential_processherpaderping.json'; +import rule333 from './privilege_escalation_uac_bypass_dll_sideloading.json'; +import rule334 from './execution_shared_modules_local_sxs_dll.json'; +import rule335 from './privilege_escalation_uac_bypass_com_clipup.json'; +import rule336 from './execution_via_explorer_suspicious_child_parent_args.json'; +import rule337 from './execution_from_unusual_directory.json'; +import rule338 from './execution_from_unusual_path_cmdline.json'; +import rule339 from './credential_access_kerberoasting_unusual_process.json'; +import rule340 from './discovery_peripheral_device.json'; +import rule341 from './lateral_movement_mount_hidden_or_webdav_share_net.json'; +import rule342 from './defense_evasion_deleting_websvr_access_logs.json'; +import rule343 from './defense_evasion_log_files_deleted.json'; +import rule344 from './defense_evasion_timestomp_touch.json'; +import rule345 from './lateral_movement_dcom_hta.json'; +import rule346 from './lateral_movement_execution_via_file_shares_sequence.json'; +import rule347 from './privilege_escalation_uac_bypass_com_ieinstal.json'; +import rule348 from './command_and_control_common_webservices.json'; +import rule349 from './command_and_control_encrypted_channel_freesslcert.json'; +import rule350 from './defense_evasion_process_termination_followed_by_deletion.json'; +import rule351 from './lateral_movement_remote_file_copy_hidden_share.json'; +import rule352 from './attempt_to_deactivate_okta_network_zone.json'; +import rule353 from './attempt_to_delete_okta_network_zone.json'; +import rule354 from './lateral_movement_dcom_mmc20.json'; +import rule355 from './lateral_movement_dcom_shellwindow_shellbrowserwindow.json'; +import rule356 from './okta_attempt_to_deactivate_okta_application.json'; +import rule357 from './okta_attempt_to_delete_okta_application.json'; +import rule358 from './okta_attempt_to_delete_okta_policy_rule.json'; +import rule359 from './okta_attempt_to_modify_okta_application.json'; +import rule360 from './persistence_administrator_role_assigned_to_okta_user.json'; +import rule361 from './lateral_movement_executable_tool_transfer_smb.json'; +import rule362 from './command_and_control_dns_tunneling_nslookup.json'; +import rule363 from './lateral_movement_execution_from_tsclient_mup.json'; +import rule364 from './lateral_movement_rdp_sharprdp_target.json'; +import rule365 from './persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json'; +import rule366 from './execution_suspicious_short_program_name.json'; +import rule367 from './lateral_movement_incoming_wmi.json'; +import rule368 from './persistence_via_hidden_run_key_valuename.json'; +import rule369 from './credential_access_potential_ssh_bruteforce.json'; +import rule370 from './credential_access_promt_for_pwd_via_osascript.json'; +import rule371 from './lateral_movement_remote_services.json'; +import rule372 from './application_added_to_google_workspace_domain.json'; +import rule373 from './defense_evasion_suspicious_powershell_imgload.json'; +import rule374 from './domain_added_to_google_workspace_trusted_domains.json'; +import rule375 from './execution_suspicious_image_load_wmi_ms_office.json'; +import rule376 from './google_workspace_admin_role_deletion.json'; +import rule377 from './google_workspace_mfa_enforcement_disabled.json'; +import rule378 from './google_workspace_policy_modified.json'; +import rule379 from './mfa_disabled_for_google_workspace_organization.json'; +import rule380 from './persistence_evasion_registry_ifeo_injection.json'; +import rule381 from './persistence_google_workspace_admin_role_assigned_to_user.json'; +import rule382 from './persistence_google_workspace_custom_admin_role_created.json'; +import rule383 from './persistence_google_workspace_role_modified.json'; +import rule384 from './persistence_suspicious_image_load_scheduled_task_ms_office.json'; +import rule385 from './defense_evasion_masquerading_trusted_directory.json'; +import rule386 from './exfiltration_microsoft_365_exchange_transport_rule_creation.json'; +import rule387 from './initial_access_microsoft_365_exchange_safelinks_disabled.json'; +import rule388 from './microsoft_365_exchange_dkim_signing_config_disabled.json'; +import rule389 from './persistence_appcertdlls_registry.json'; +import rule390 from './persistence_appinitdlls_registry.json'; +import rule391 from './persistence_registry_uncommon.json'; +import rule392 from './persistence_run_key_and_startup_broad.json'; +import rule393 from './persistence_services_registry.json'; +import rule394 from './persistence_startup_folder_file_written_by_suspicious_process.json'; +import rule395 from './persistence_startup_folder_scripts.json'; +import rule396 from './persistence_suspicious_com_hijack_registry.json'; +import rule397 from './persistence_via_lsa_security_support_provider_registry.json'; +import rule398 from './defense_evasion_microsoft_365_exchange_malware_filter_policy_deletion.json'; +import rule399 from './defense_evasion_microsoft_365_exchange_malware_filter_rule_mod.json'; +import rule400 from './defense_evasion_microsoft_365_exchange_safe_attach_rule_disabled.json'; +import rule401 from './exfiltration_microsoft_365_exchange_transport_rule_mod.json'; +import rule402 from './initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json'; +import rule403 from './initial_access_microsoft_365_exchange_anti_phish_rule_mod.json'; +import rule404 from './lateral_movement_suspicious_rdp_client_imageload.json'; +import rule405 from './persistence_runtime_run_key_startup_susp_procs.json'; +import rule406 from './persistence_suspicious_scheduled_task_runtime.json'; +import rule407 from './defense_evasion_microsoft_365_exchange_dlp_policy_removed.json'; +import rule408 from './lateral_movement_scheduled_task_target.json'; +import rule409 from './persistence_microsoft_365_exchange_management_role_assignment.json'; +import rule410 from './persistence_microsoft_365_teams_guest_access_enabled.json'; +import rule411 from './credential_access_dump_registry_hives.json'; +import rule412 from './defense_evasion_scheduledjobs_at_protocol_enabled.json'; +import rule413 from './persistence_ms_outlook_vba_template.json'; +import rule414 from './persistence_suspicious_service_created_registry.json'; +import rule415 from './privilege_escalation_named_pipe_impersonation.json'; +import rule416 from './credential_access_cmdline_dump_tool.json'; +import rule417 from './credential_access_copy_ntds_sam_volshadowcp_cmdline.json'; +import rule418 from './credential_access_lsass_memdump_file_created.json'; +import rule419 from './lateral_movement_incoming_winrm_shell_execution.json'; +import rule420 from './lateral_movement_powershell_remoting_target.json'; +import rule421 from './defense_evasion_hide_encoded_executable_registry.json'; +import rule422 from './defense_evasion_port_forwarding_added_registry.json'; +import rule423 from './lateral_movement_rdp_enabled_registry.json'; +import rule424 from './privilege_escalation_printspooler_registry_copyfiles.json'; +import rule425 from './privilege_escalation_rogue_windir_environment_var.json'; +import rule426 from './execution_scripts_process_started_via_wmi.json'; +import rule427 from './command_and_control_iexplore_via_com.json'; +import rule428 from './command_and_control_remote_file_copy_scripts.json'; +import rule429 from './persistence_local_scheduled_task_scripting.json'; +import rule430 from './persistence_startup_folder_file_written_by_unsigned_process.json'; +import rule431 from './command_and_control_remote_file_copy_powershell.json'; +import rule432 from './credential_access_microsoft_365_brute_force_user_account_attempt.json'; +import rule433 from './microsoft_365_teams_custom_app_interaction_allowed.json'; +import rule434 from './persistence_microsoft_365_teams_external_access_enabled.json'; +import rule435 from './credential_access_microsoft_365_potential_password_spraying_attack.json'; +import rule436 from './defense_evasion_stop_process_service_threshold.json'; +import rule437 from './defense_evasion_unusual_dir_ads.json'; +import rule438 from './discovery_admin_recon.json'; +import rule439 from './discovery_file_dir_discovery.json'; +import rule440 from './discovery_net_view.json'; +import rule441 from './discovery_query_registry_via_reg.json'; +import rule442 from './discovery_remote_system_discovery_commands_windows.json'; +import rule443 from './exfiltration_winrar_encryption.json'; +import rule444 from './persistence_via_windows_management_instrumentation_event_subscription.json'; +import rule445 from './execution_scripting_osascript_exec_followed_by_netcon.json'; +import rule446 from './execution_shell_execution_via_apple_scripting.json'; +import rule447 from './persistence_creation_change_launch_agents_file.json'; +import rule448 from './persistence_creation_modif_launch_deamon_sequence.json'; +import rule449 from './persistence_folder_action_scripts_runtime.json'; +import rule450 from './persistence_login_logout_hooks_defaults.json'; +import rule451 from './privilege_escalation_explicit_creds_via_apple_scripting.json'; export const rawRules = [ rule1, @@ -643,4 +778,139 @@ export const rawRules = [ rule314, rule315, rule316, + rule317, + rule318, + rule319, + rule320, + rule321, + rule322, + rule323, + rule324, + rule325, + rule326, + rule327, + rule328, + rule329, + rule330, + rule331, + rule332, + rule333, + rule334, + rule335, + rule336, + rule337, + rule338, + rule339, + rule340, + rule341, + rule342, + rule343, + rule344, + rule345, + rule346, + rule347, + rule348, + rule349, + rule350, + rule351, + rule352, + rule353, + rule354, + rule355, + rule356, + rule357, + rule358, + rule359, + rule360, + rule361, + rule362, + rule363, + rule364, + rule365, + rule366, + rule367, + rule368, + rule369, + rule370, + rule371, + rule372, + rule373, + rule374, + rule375, + rule376, + rule377, + rule378, + rule379, + rule380, + rule381, + rule382, + rule383, + rule384, + rule385, + rule386, + rule387, + rule388, + rule389, + rule390, + rule391, + rule392, + rule393, + rule394, + rule395, + rule396, + rule397, + rule398, + rule399, + rule400, + rule401, + rule402, + rule403, + rule404, + rule405, + rule406, + rule407, + rule408, + rule409, + rule410, + rule411, + rule412, + rule413, + rule414, + rule415, + rule416, + rule417, + rule418, + rule419, + rule420, + rule421, + rule422, + rule423, + rule424, + rule425, + rule426, + rule427, + rule428, + rule429, + rule430, + rule431, + rule432, + rule433, + rule434, + rule435, + rule436, + rule437, + rule438, + rule439, + rule440, + rule441, + rule442, + rule443, + rule444, + rule445, + rule446, + rule447, + rule448, + rule449, + rule450, + rule451, ]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json index 1dab4e8df71b4..a2b89f6e82d23 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_consent_grant_attack_via_azure_registered_application.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "Identifies when a user grants permissions to an Azure-registered application or when an administrator grants tenant-wide permissions to an application. An adversary may create an Azure-registered application that requests access to data such as contact information, email, or documents.", + "description": "Detects when a user grants permissions to an Azure-registered application or when an administrator grants tenant-wide permissions to an application. An adversary may create an Azure-registered application that requests access to data such as contact information, email, or documents.", "from": "now-25m", "index": [ "filebeat-*" @@ -11,7 +11,7 @@ "license": "Elastic License", "name": "Possible Consent Grant Attack via Azure-Registered Application", "note": "- The Azure Filebeat module must be enabled to use this rule.\n- In a consent grant attack, an attacker tricks an end user into granting a malicious application consent to access their data, usually via a phishing attack. After the malicious application has been granted consent, it has account-level access to data without the need for an organizational account.\n- Normal remediation steps, like resetting passwords for breached accounts or requiring Multi-Factor Authentication (MFA) on accounts, are not effective against this type of attack, since these are third-party applications and are external to the organization.\n- Security analysts should review the list of trusted applications for any suspicious items.\n", - "query": "event.dataset:(azure.activitylogs or azure.auditlogs) and ( azure.activitylogs.operation_name:\"Consent to application\" or azure.auditlogs.operation_name:\"Consent to application\" ) and event.outcome:success", + "query": "event.dataset:(azure.activitylogs or azure.auditlogs or o365.audit) and ( azure.activitylogs.operation_name:\"Consent to application\" or azure.auditlogs.operation_name:\"Consent to application\" or o365.audit.Operation:\"Consent to application.\" ) and event.outcome:success", "references": [ "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/detect-and-remediate-illicit-consent-grants?view=o365-worldwide" ], @@ -59,5 +59,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json new file mode 100644 index 0000000000000..44c88c3818e74 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_policy_deletion.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the deletion of an anti-phishing policy in Microsoft 365. By default, Microsoft 365 includes built-in features that help protect users from phishing attacks. Anti-phishing polices increase this protection by refining settings to better detect and prevent attacks.", + "false_positives": [ + "An anti-phishing policy may be deleted by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Anti-Phish Policy Deletion", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Remove-AntiPhishPolicy\" and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-antiphishpolicy?view=exchange-ps", + "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/set-up-anti-phishing-policies?view=o365-worldwide" + ], + "risk_score": 47, + "rule_id": "d68eb1b5-5f1c-4b6d-9e63-5b6b145cd4aa", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1566", + "name": "Phishing", + "reference": "https://attack.mitre.org/techniques/T1566/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_rule_mod.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_rule_mod.json new file mode 100644 index 0000000000000..d3d78127c63fe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_anti_phish_rule_mod.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the modification of an anti-phishing rule in Microsoft 365. By default, Microsoft 365 includes built-in features that help protect users from phishing attacks. Anti-phishing rules increase this protection by refining settings to better detect and prevent attacks.", + "false_positives": [ + "An anti-phishing rule may be deleted by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Anti-Phish Rule Modification", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:(\"Remove-AntiPhishRule\" or \"Disable-AntiPhishRule\") and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/remove-antiphishrule?view=exchange-ps", + "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-antiphishrule?view=exchange-ps" + ], + "risk_score": 47, + "rule_id": "97314185-2568-4561-ae81-f3e480e5e695", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1566", + "name": "Phishing", + "reference": "https://attack.mitre.org/techniques/T1566/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_safelinks_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_safelinks_disabled.json new file mode 100644 index 0000000000000..2f8fe344887fb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_microsoft_365_exchange_safelinks_disabled.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a Safe Link policy is disabled in Microsoft 365. Safe Link policies for Office applications extend phishing protection to documents that contain hyperlinks, even after they have been delivered to a user.", + "false_positives": [ + "Disabling safe links may be done by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Safe Link Policy Disabled", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Disable-SafeLinksRule\" and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/disable-safelinksrule?view=exchange-ps", + "https://docs.microsoft.com/en-us/microsoft-365/security/office-365-security/atp-safe-links?view=o365-worldwide" + ], + "risk_score": 47, + "rule_id": "a989fa1b-9a11-4dd8-a3e9-f0de9c6eb5f2", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monioring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0001", + "name": "Initial Access", + "reference": "https://attack.mitre.org/tactics/TA0001/" + }, + "technique": [ + { + "id": "T1566", + "name": "Phishing", + "reference": "https://attack.mitre.org/techniques/T1566/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json index ce0f44713523f..00bdbec354a74 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rdp_remote_desktop_protocol_to_the_internet.json @@ -14,7 +14,7 @@ "language": "kuery", "license": "Elastic License", "name": "RDP (Remote Desktop Protocol) to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:3389 or event.dataset:zeek.rdp) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:3389 or event.dataset:zeek.rdp) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 21, "rule_id": "e56993d2-759c-4120-984c-9ec9bb940fd5", "severity": "low", @@ -58,5 +58,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json index b8f3e01823312..ceacc5a300faf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_from_the_internet.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "RPC (Remote Procedure Call) from the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:135 or event.dataset:zeek.dce_rpc) and not source.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\") and destination.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16)", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:135 or event.dataset:zeek.dce_rpc) and not source.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" ) and destination.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 )", "risk_score": 73, "rule_id": "143cb236-0956-4f42-a706-814bcaa0cf5a", "severity": "high", @@ -40,5 +40,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json index e8e4ea4eb3746..99309b9a4a309 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_rpc_remote_procedure_call_to_the_internet.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "RPC (Remote Procedure Call) to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:135 or event.dataset:zeek.dce_rpc) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:135 or event.dataset:zeek.dce_rpc) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 73, "rule_id": "32923416-763a-4531-bb35-f33b9232ecdb", "severity": "high", @@ -40,5 +40,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json index fec0f308a8d27..7741051b3fa29 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_smb_windows_file_sharing_activity_to_the_internet.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "SMB (Windows File Sharing) Activity to the Internet", - "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(139 or 445) or event.dataset:zeek.smb) and source.ip:(10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16) and not destination.ip:(10.0.0.0/8 or 127.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 or \"::1\")", + "query": "event.category:(network or network_traffic) and network.transport:tcp and (destination.port:(139 or 445) or event.dataset:zeek.smb) and source.ip:( 10.0.0.0/8 or 172.16.0.0/12 or 192.168.0.0/16 ) and not destination.ip:( 10.0.0.0/8 or 127.0.0.0/8 or 169.254.0.0/16 or 172.16.0.0/12 or 192.168.0.0/16 or 224.0.0.0/4 or \"::1\" or \"FE80::/10\" or \"FF00::/8\" )", "risk_score": 73, "rule_id": "c82b2bd8-d701-420c-ba43-f11a155b681a", "severity": "high", @@ -55,5 +55,5 @@ } ], "type": "query", - "version": 5 + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json index 5b1946dc7c07d..4d0cc3033a2e6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/initial_access_suspicious_activity_reported_by_okta_user.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "This rule detects when a user reports suspicious activity for their Okta account. These events should be investigated, as they can help security teams identify when an adversary is attempting to gain access to their network.", + "description": "Detects when a user reports suspicious activity for their Okta account. These events should be investigated, as they can help security teams identify when an adversary is attempting to gain access to their network.", "false_positives": [ "A user may report suspicious activity on their Okta account in error." ], @@ -13,7 +13,7 @@ "language": "kuery", "license": "Elastic License", "name": "Suspicious Activity Reported by Okta User", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:user.account.report_suspicious_activity_by_enduser", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -93,5 +93,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_hta.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_hta.json new file mode 100644 index 0000000000000..590f82e31b36b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_hta.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of Distributed Component Object Model (DCOM) to execute commands from a remote host, which are launched via the HTA Application COM Object. This behavior may indicate an attacker abusing a DCOM application to move laterally while attempting to evading detection.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Incoming DCOM Lateral Movement via MSHTA", + "query": "sequence with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and\n process.name : \"mshta.exe\" and process.args : \"-Embedding\"\n ] by host.id, process.entity_id\n [network where event.type == \"start\" and process.name : \"mshta.exe\" and \n network.direction == \"incoming\" and network.transport == \"tcp\" and\n source.port > 49151 and destination.port > 49151 and not source.address in (\"127.0.0.1\", \"::1\")\n ] by host.id, process.entity_id\n", + "references": [ + "https://codewhitesec.blogspot.com/2018/07/lethalhta.html" + ], + "risk_score": 73, + "rule_id": "622ecb68-fa81-4601-90b5-f8cd661e4520", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_mmc20.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_mmc20.json new file mode 100644 index 0000000000000..5dee8493d57ad --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_mmc20.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of Distributed Component Object Model (DCOM) to run commands from a remote host, which are launched via the MMC20 Application COM Object. This behavior may indicate an attacker abusing a DCOM application to move laterally.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Incoming DCOM Lateral Movement with MMC", + "query": "sequence by host.id with maxspan=1m\n [network where event.type == \"start\" and process.name : \"mmc.exe\" and\n source.port >= 49152 and destination.port >= 49152 and source.address not in (\"127.0.0.1\", \"::1\") and\n network.direction == \"incoming\" and network.transport == \"tcp\"\n ] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and process.parent.name : \"mmc.exe\"\n ] by process.parent.entity_id\n", + "references": [ + "https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/" + ], + "risk_score": 73, + "rule_id": "51ce96fb-9e52-4dad-b0ba-99b54440fc9a", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_shellwindow_shellbrowserwindow.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_shellwindow_shellbrowserwindow.json new file mode 100644 index 0000000000000..4e04dc1d458cb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_dcom_shellwindow_shellbrowserwindow.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies use of Distributed Component Object Model (DCOM) to run commands from a remote host, which are launched via the ShellBrowserWindow or ShellWindows Application COM Object. This behavior may indicate an attacker abusing a DCOM application to stealthily move laterally.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Incoming DCOM Lateral Movement with ShellBrowserWindow or ShellWindows", + "query": "sequence by host.id with maxspan=5s\n [network where event.type == \"start\" and process.name : \"explorer.exe\" and\n network.direction == \"incoming\" and network.transport == \"tcp\" and\n source.port > 49151 and destination.port > 49151 and not source.address in (\"127.0.0.1\", \"::1\")\n ] by process.entity_id\n [process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"explorer.exe\"\n ] by process.parent.entity_id\n", + "references": [ + "https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/" + ], + "risk_score": 47, + "rule_id": "8f919d4b-a5af-47ca-a594-6be59cd924a4", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_executable_tool_transfer_smb.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_executable_tool_transfer_smb.json new file mode 100644 index 0000000000000..bc3f48904c43f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_executable_tool_transfer_smb.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation or change of a Windows executable file over network shares. Adversaries may transfer tools or other files between systems in a compromised environment.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Lateral Tool Transfer", + "query": "sequence by host.id with maxspan=30s\n [network where event.type == \"start\" and process.pid == 4 and destination.port == 445 and\n network.direction == \"incoming\" and network.transport == \"tcp\" and\n source.address != \"127.0.0.1\" and source.address != \"::1\"\n ] by process.entity_id\n /* add more executable extensions here if they are not noisy in your environment */\n [file where event.type in (\"creation\", \"change\") and process.pid == 4 and file.extension : (\"exe\", \"dll\", \"bat\", \"cmd\")] by process.entity_id\n", + "risk_score": 47, + "rule_id": "58bc134c-e8d2-4291-a552-b4b3e537c60b", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1570", + "name": "Lateral Tool Transfer", + "reference": "https://attack.mitre.org/techniques/T1570/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_from_tsclient_mup.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_from_tsclient_mup.json new file mode 100644 index 0000000000000..35ca9326e3668 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_from_tsclient_mup.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies execution from the Remote Desktop Protocol (RDP) shared mountpoint tsclient on the target host. This may indicate a lateral movement attempt.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Execution via TSClient Mountpoint", + "query": "process where event.type in (\"start\", \"process_started\") and process.executable : \"\\\\Device\\\\Mup\\\\tsclient\\\\*.exe\"\n", + "references": [ + "https://posts.specterops.io/revisiting-remote-desktop-lateral-movement-8fb905cb46c3" + ], + "risk_score": 73, + "rule_id": "4fe9d835-40e1-452d-8230-17c147cafad8", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_via_file_shares_sequence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_via_file_shares_sequence.json new file mode 100644 index 0000000000000..d0f301249017e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_execution_via_file_shares_sequence.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the execution of a file that was created by the virtual system process. This may indicate lateral movement via network file shares.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Remote Execution via File Shares", + "query": "sequence with maxspan=1m\n [file where event.type in (\"creation\", \"change\") and process.pid == 4 and file.extension : \"exe\"] by host.id, file.path\n [process where event.type in (\"start\", \"process_started\")] by host.id, process.executable\n", + "references": [ + "https://blog.menasec.net/2020/08/new-trick-to-detect-lateral-movement.html" + ], + "risk_score": 47, + "rule_id": "ab75c24b-2502-43a0-bf7c-e60e662c811e", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1077", + "name": "Windows Admin Shares", + "reference": "https://attack.mitre.org/techniques/T1077/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_winrm_shell_execution.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_winrm_shell_execution.json new file mode 100644 index 0000000000000..0c9453940b3bc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_winrm_shell_execution.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies remote execution via Windows Remote Management (WinRM) remote shell on a target host. This could be an indication of lateral movement.", + "false_positives": [ + "WinRM is a dual-use protocol that can be used for benign or malicious activity. It's important to baseline your environment to determine the amount of noise to expect from this tool." + ], + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Incoming Execution via WinRM Remote Shell", + "query": "sequence by host.id with maxspan=30s\n [network where process.pid == 4 and network.direction == \"incoming\" and\n destination.port in (5985, 5986) and network.protocol == \"http\" and not source.address in (\"::1\", \"127.0.0.1\")\n ]\n [process where event.type == \"start\" and process.parent.name : \"winrshost.exe\" and not process.name : \"conhost.exe\"]\n", + "risk_score": 47, + "rule_id": "1cd01db9-be24-4bef-8e7c-e923f0ff78ab", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_wmi.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_wmi.json new file mode 100644 index 0000000000000..130c8d37ed853 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_incoming_wmi.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies processes executed via Windows Management Instrumentation (WMI) on a remote host. This could be indicative of adversary lateral movement, but could be noisy if administrators use WMI to remotely manage hosts.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "WMI Incoming Lateral Movement", + "query": "sequence by host.id with maxspan = 2s\n\n /* Accepted Incoming RPC connection by Winmgmt service */\n\n [network where process.name : \"svchost.exe\" and network.direction == \"incoming\" and\n source.address != \"127.0.0.1\" and source.address != \"::1\" and \n source.port >= 49152 and destination.port >= 49152\n ]\n\n /* Excluding Common FPs Nessus and SCCM */\n\n [process where event.type in (\"start\", \"process_started\") and process.parent.name : \"WmiPrvSE.exe\" and\n not process.args : (\"C:\\\\windows\\\\temp\\\\nessus_*.txt\", \n \"C:\\\\windows\\\\TEMP\\\\nessus_*.TMP\", \n \"C:\\\\Windows\\\\CCM\\\\SystemTemp\\\\*\", \n \"C:\\\\Windows\\\\CCMCache\\\\*\", \n \"C:\\\\CCM\\\\Cache\\\\*\")\n ]\n", + "risk_score": 47, + "rule_id": "f3475224-b179-4f78-8877-c2bd64c26b88", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1047", + "name": "Windows Management Instrumentation", + "reference": "https://attack.mitre.org/techniques/T1047/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mount_hidden_or_webdav_share_net.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mount_hidden_or_webdav_share_net.json new file mode 100644 index 0000000000000..575b715239ad7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_mount_hidden_or_webdav_share_net.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the use of net.exe to mount a WebDav or hidden remote share. This may indicate lateral movement or preparation for data exfiltration.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Mounting Hidden or WebDav Remote Shares", + "query": "process where event.type in (\"start\", \"process_started\") and\n ((process.name : \"net.exe\" or process.pe.original_file_name == \"net.exe\") or ((process.name : \"net1.exe\" or process.pe.original_file_name == \"net1.exe\") and\n not process.parent.name : \"net.exe\")) and\n process.args : \"use\" and\n /* including hidden and webdav based online shares such as onedrive */\n process.args : (\"\\\\\\\\*\\\\*$*\", \"\\\\\\\\*@SSL\\\\*\", \"http*\") and\n /* excluding shares deletion operation */\n not process.args : \"/d*\"\n", + "risk_score": 21, + "rule_id": "c4210e1c-64f2-4f48-b67e-b5a8ffe3aa14", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1077", + "name": "Windows Admin Shares", + "reference": "https://attack.mitre.org/techniques/T1077/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_powershell_remoting_target.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_powershell_remoting_target.json new file mode 100644 index 0000000000000..aae62e66ad255 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_powershell_remoting_target.json @@ -0,0 +1,50 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies remote execution via Windows PowerShell remoting. Windows PowerShell remoting allows for running any Windows PowerShell command on one or more remote computers. This could be an indication of lateral movement.", + "false_positives": [ + "PowerShell remoting is a dual-use protocol that can be used for benign or malicious activity. It's important to baseline your environment to determine the amount of noise to expect from this tool." + ], + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Incoming Execution via PowerShell Remoting", + "query": "sequence by host.id with maxspan = 30s\n [network where network.direction == \"incoming\" and destination.port in (5985, 5986) and\n network.protocol == \"http\" and source.address != \"127.0.0.1\" and source.address != \"::1\"\n ]\n [process where event.type == \"start\" and process.parent.name : \"wsmprovhost.exe\" and not process.name : \"conhost.exe\"]\n", + "references": [ + "https://docs.microsoft.com/en-us/powershell/scripting/learn/remoting/running-remote-commands?view=powershell-7.1" + ], + "risk_score": 43, + "rule_id": "2772264c-6fb9-4d9d-9014-b416eed21254", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_enabled_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_enabled_registry.json new file mode 100644 index 0000000000000..00205b99c1cd9 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_enabled_registry.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies registry write modifications to enable Remote Desktop Protocol (RDP) access. This could be indicative of adversary lateral movement preparation.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "RDP Enabled via Registry", + "query": "registry where\nregistry.path : \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Terminal Server\\\\fDenyTSConnections\" and\nregistry.data.strings == \"0\" and not (process.name : \"svchost.exe\" and user.domain == \"NT AUTHORITY\") and\nnot process.executable : \"C:\\\\Windows\\\\System32\\\\SystemPropertiesRemote.exe\"\n", + "risk_score": 43, + "rule_id": "58aa72ca-d968-4f34-b9f7-bea51d75eb50", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_sharprdp_target.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_sharprdp_target.json new file mode 100644 index 0000000000000..9a9a9d0e7a202 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_sharprdp_target.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies potential behavior of SharpRDP, which is a tool that can be used to perform authenticated command execution against a remote target via Remote Desktop Protocol (RDP) for the purposes of lateral movement.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential SharpRDP Behavior", + "query": "/* Incoming RDP followed by a new RunMRU string value set to cmd, powershell, taskmgr or tsclient, followed by process execution within 1m */\n\nsequence by host.id with maxspan=1m\n [network where event.type == \"start\" and process.name : \"svchost.exe\" and destination.port == 3389 and \n network.direction == \"incoming\" and network.transport == \"tcp\" and\n source.address != \"127.0.0.1\" and source.address != \"::1\"\n ]\n\n [registry where process.name : \"explorer.exe\" and \n registry.path : (\"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Explorer\\\\RunMRU\\\\*\") and\n registry.data.strings : (\"cmd.exe*\", \"powershell.exe*\", \"taskmgr*\", \"\\\\\\\\tsclient\\\\*.exe\\\\*\")\n ]\n \n [process where event.type in (\"start\", \"process_started\") and\n (process.parent.name : (\"cmd.exe\", \"powershell.exe\", \"taskmgr.exe\") or process.args : (\"\\\\\\\\tsclient\\\\*.exe\")) and \n not process.name : \"conhost.exe\"\n ]\n", + "references": [ + "https://posts.specterops.io/revisiting-remote-desktop-lateral-movement-8fb905cb46c3", + "https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES/blob/master/Lateral%20Movement/LM_sysmon_3_12_13_1_SharpRDP.evtx" + ], + "risk_score": 73, + "rule_id": "8c81e506-6e82-4884-9b9a-75d3d252f967", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_tunnel_plink.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_tunnel_plink.json new file mode 100644 index 0000000000000..40e9a171909c0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_rdp_tunnel_plink.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies potential use of an SSH utility to establish RDP over a reverse SSH Tunnel. This could be indicative of adversary lateral movement to interactively access restricted networks.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Potential Remote Desktop Tunneling Detected", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and \n/* RDP port and usual SSH tunneling related switches in commandline */\nwildcard(process.args, \"*:3389\") and wildcard(process.args,\"-L\", \"-P\", \"-R\", \"-pw\", \"-ssh\") \n", + "references": [ + "https://blog.netspi.com/how-to-access-rdp-over-a-reverse-ssh-tunnel/" + ], + "risk_score": 73, + "rule_id": "76fd43b7-3480-4dd9-8ad7-8bd36bfad92f", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_file_copy_hidden_share.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_file_copy_hidden_share.json new file mode 100644 index 0000000000000..06d07e92abe6c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_file_copy_hidden_share.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a remote file copy attempt to a hidden network share. This may indicate lateral movement or data staging activity.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Remote File Copy to a Hidden Share", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name : (\"cmd.exe\", \"powershell.exe\", \"robocopy.exe\", \"xcopy.exe\") and \n process.args : (\"copy*\", \"move*\", \"cp\", \"mv\") and process.args : \"*$*\"\n", + "risk_score": 47, + "rule_id": "fa01341d-6662-426b-9d0c-6d81e33c8a9d", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1077", + "name": "Windows Admin Shares", + "reference": "https://attack.mitre.org/techniques/T1077/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_services.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_services.json new file mode 100644 index 0000000000000..fd9eeb9be8eb6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_remote_services.json @@ -0,0 +1,45 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies remote execution of Windows services over remote procedure call (RPC). This could be indicative of lateral movement, but will be noisy if commonly done by administrators.\"", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Remotely Started Services via RPC", + "query": "sequence with maxspan=1s\n [network where process.name : \"services.exe\" and\n network.direction == \"incoming\" and network.transport == \"tcp\" and \n source.port >= 49152 and destination.port >= 49152 and source.address not in (\"127.0.0.1\", \"::1\")\n ] by host.id, process.entity_id\n\n [process where event.type in (\"start\", \"process_started\") and process.parent.name : \"services.exe\" and \n not (process.name : \"svchost.exe\" and process.args : \"tiledatamodelsvc\") and \n not (process.name : \"msiexec.exe\" and process.args : \"/V\")\n \n /* uncomment if psexec is noisy in your environment */\n /* and not process.name : \"PSEXESVC.exe\" */\n ] by host.id, process.parent.entity_id\n", + "risk_score": 47, + "rule_id": "aa9a274d-6b53-424d-ac5e-cb8ca4251650", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1035", + "name": "Service Execution", + "reference": "https://attack.mitre.org/techniques/T1035/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_scheduled_task_target.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_scheduled_task_target.json new file mode 100644 index 0000000000000..ca29829849a0e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_scheduled_task_target.json @@ -0,0 +1,60 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies remote scheduled task creations on a target host. This could be indicative of adversary lateral movement.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Remote Scheduled Task Creation", + "note": "Decode the base64 encoded tasks actions registry value to investigate the task configured action.", + "query": "/* Task Scheduler service incoming connection followed by TaskCache registry modification */\n\nsequence by host.id, process.entity_id with maxspan = 1m\n [network where process.name : \"svchost.exe\" and\n network.direction == \"incoming\" and source.port >= 49152 and destination.port >= 49152 and\n source.address != \"127.0.0.1\" and source.address != \"::1\"\n ]\n [registry where registry.path : \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Schedule\\\\TaskCache\\\\Tasks\\\\*\\\\Actions\"]\n", + "risk_score": 47, + "rule_id": "954ee7c8-5437-49ae-b2d6-2960883898e9", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1053", + "name": "Scheduled Task/Job", + "reference": "https://attack.mitre.org/techniques/T1053/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_cmd_wmi.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_cmd_wmi.json new file mode 100644 index 0000000000000..f19b940e758f7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_cmd_wmi.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies suspicious command execution (cmd) via Windows Management Instrumentation (WMI) on a remote host. This could be indicative of adversary lateral movement.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Cmd Execution via WMI", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name == \"WmiPrvSE.exe\" and process.name == \"cmd.exe\" and\n wildcard(process.args, \"\\\\\\\\127.0.0.1\\\\*\") and process.args in (\"2>&1\", \"1>\")\n", + "risk_score": 47, + "rule_id": "12f07955-1674-44f7-86b5-c35da0a6f41a", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1047", + "name": "Windows Management Instrumentation", + "reference": "https://attack.mitre.org/techniques/T1047/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_rdp_client_imageload.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_rdp_client_imageload.json new file mode 100644 index 0000000000000..65eedcd8348ba --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_suspicious_rdp_client_imageload.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies suspicious Image Loading of the Remote Desktop Services ActiveX Client (mstscax), this may indicate the presence of RDP lateral movement capability.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious RDP ActiveX Client Loaded", + "query": "library where file.name == \"mstscax.dll\" and\n /* depending on noise in your env add here extra paths */\n wildcard(process.executable, \"C:\\\\Windows\\\\*\",\n \"C:\\\\Users\\\\Public\\\\*\",\n \"C:\\\\Users\\\\Default\\\\*\",\n \"C:\\\\Intel\\\\*\",\n \"C:\\\\PerfLogs\\\\*\",\n \"C:\\\\ProgramData\\\\*\",\n \"\\\\Device\\\\Mup\\\\*\",\n \"\\\\\\\\*\") and\n /* add here FPs */\n not process.executable in (\"C:\\\\Windows\\\\System32\\\\mstsc.exe\", \"C:\\\\Windows\\\\SysWOW64\\\\mstsc.exe\")\n", + "references": [ + "https://posts.specterops.io/revisiting-remote-desktop-lateral-movement-8fb905cb46c3" + ], + "risk_score": 47, + "rule_id": "71c5cb27-eca5-4151-bb47-64bc3f883270", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_via_startup_folder_rdp_smb.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_via_startup_folder_rdp_smb.json new file mode 100644 index 0000000000000..02f49c816d786 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/lateral_movement_via_startup_folder_rdp_smb.json @@ -0,0 +1,62 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies suspicious file creations in the startup folder of a remote system. An adversary could abuse this to move laterally by dropping a malicious script or executable that will be executed after a reboot or user logon.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Lateral Movement via Startup Folder", + "query": "file where event.type in (\"creation\", \"change\") and\n /* via RDP TSClient mounted share or SMB */\n (process.name : \"mstsc.exe\" or process.pid == 4) and\n file.path : \"C:\\\\*\\\\Microsoft\\\\Windows\\\\Start Menu\\\\Programs\\\\Startup\\\\*\"\n", + "references": [ + "https://www.mdsec.co.uk/2017/06/rdpinception/" + ], + "risk_score": 73, + "rule_id": "25224a80-5a4a-4b8a-991e-6ab390465c4f", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Lateral Movement" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0008", + "name": "Lateral Movement", + "reference": "https://attack.mitre.org/tactics/TA0008/" + }, + "technique": [ + { + "id": "T1021", + "name": "Remote Services", + "reference": "https://attack.mitre.org/techniques/T1021/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1060", + "name": "Registry Run Keys / Startup Folder", + "reference": "https://attack.mitre.org/techniques/T1060/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/mfa_disabled_for_google_workspace_organization.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/mfa_disabled_for_google_workspace_organization.json new file mode 100644 index 0000000000000..68bb88edbed3e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/mfa_disabled_for_google_workspace_organization.json @@ -0,0 +1,32 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when multi-factor authentication (MFA) is disabled for a Google Workspace organization. An adversary may attempt to modify a password policy in order to weaken an organization\u2019s security controls.", + "false_positives": [ + "MFA settings may be modified by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "MFA Disabled for Google Workspace Organization", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:(ENFORCE_STRONG_AUTHENTICATION or ALLOW_STRONG_AUTHENTICATION) and gsuite.admin.new_value:false", + "risk_score": 47, + "rule_id": "e555105c-ba6d-481f-82bb-9b633e7b4827", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_exchange_dkim_signing_config_disabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_exchange_dkim_signing_config_disabled.json new file mode 100644 index 0000000000000..227bbe1189fef --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_exchange_dkim_signing_config_disabled.json @@ -0,0 +1,34 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a DomainKeys Identified Mail (DKIM) signing configuration is disabled in Microsoft 365. With DKIM in Microsoft 365, messages that are sent from Exchange Online will be cryptographically signed. This will allow the receiving email system to validate that the messages were generated by a server that the organization authorized and not being spoofed.", + "false_positives": [ + "Disabling a DKIM configuration may be done by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange DKIM Signing Configuration Disabled", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"Set-DkimSigningConfig\" and o365.audit.Parameters.Enabled:False and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/set-dkimsigningconfig?view=exchange-ps" + ], + "risk_score": 47, + "rule_id": "514121ce-c7b6-474a-8237-68ff71672379", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Data Protection" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_teams_custom_app_interaction_allowed.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_teams_custom_app_interaction_allowed.json new file mode 100644 index 0000000000000..33f4bc886720c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/microsoft_365_teams_custom_app_interaction_allowed.json @@ -0,0 +1,34 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when custom applications are allowed in Microsoft Teams. If an organization requires applications other than those available in the Teams app store, custom applications can be developed as packages and uploaded. An adversary may abuse this behavior to establish persistence in an environment.", + "false_positives": [ + "Custom applications may be allowed by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Teams Custom Application Interaction Allowed", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:MicrosoftTeams and event.category:web and event.action:TeamsTenantSettingChanged and o365.audit.Name:\"Allow sideloading and interaction of custom apps\" and o365.audit.NewValue:True and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/deploy-and-publish/apps-upload" + ], + "risk_score": 47, + "rule_id": "bbd1a775-8267-41fa-9232-20e5582596ac", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_application.json new file mode 100644 index 0000000000000..9d620536155e5 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_application.json @@ -0,0 +1,36 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to deactivate an Okta application. An adversary may attempt to modify, deactivate, or delete an Okta application in order to weaken an organization's security controls or disrupt their business operations.", + "false_positives": [ + "Consider adding exceptions to this rule to filter false positives if your organization's Okta applications are regularly deactivated and the behavior is expected." + ], + "index": [ + "filebeat-*", + "logs-okta*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Deactivate an Okta Application", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:application.lifecycle.deactivate", + "references": [ + "https://help.okta.com/en/prod/Content/Topics/Apps/Apps_Apps.htm", + "https://developer.okta.com/docs/reference/api/system-log/", + "https://developer.okta.com/docs/reference/api/event-types/" + ], + "risk_score": 21, + "rule_id": "edb91186-1c7e-4db8-b53e-bfa33a1a0a8a", + "severity": "low", + "tags": [ + "Elastic", + "Identity", + "Okta", + "Continuous Monitoring", + "SecOps", + "Monitoring" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy.json new file mode 100644 index 0000000000000..87ccf8d5412c0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy.json @@ -0,0 +1,36 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to deactivate an Okta policy. An adversary may attempt to deactivate an Okta policy in order to weaken an organization's security controls. For example, an adversary may attempt to deactivate an Okta multi-factor authentication (MFA) policy in order to weaken the authentication requirements for user accounts.", + "false_positives": [ + "If the behavior of deactivating Okta policies is expected, consider adding exceptions to this rule to filter false positives." + ], + "index": [ + "filebeat-*", + "logs-okta*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Deactivate an Okta Policy", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:policy.lifecycle.deactivate", + "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", + "https://developer.okta.com/docs/reference/api/system-log/", + "https://developer.okta.com/docs/reference/api/event-types/" + ], + "risk_score": 21, + "rule_id": "b719a170-3bdb-4141-b0e3-13e3cf627bfe", + "severity": "low", + "tags": [ + "Elastic", + "Identity", + "Okta", + "Continuous Monitoring", + "SecOps", + "Monitoring" + ], + "type": "query", + "version": 3 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy_rule.json similarity index 59% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy_rule.json index 0ee0bbd6d6226..d180a26181e4f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_deactivate_okta_policy_rule.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to deactivate an Okta multi-factor authentication (MFA) rule in order to remove or weaken an organization's security controls.", + "description": "Detects attempts to deactivate a rule within an Okta policy. An adversary may attempt to deactivate a rule within an Okta policy in order to remove or weaken an organization's security controls.", "false_positives": [ "Consider adding exceptions to this rule to filter false positives if Okta MFA rules are regularly deactivated in your organization." ], @@ -12,16 +12,17 @@ ], "language": "kuery", "license": "Elastic License", - "name": "Attempt to Deactivate Okta MFA Rule", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "name": "Attempt to Deactivate an Okta Policy Rule", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:policy.rule.deactivate", "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" ], - "risk_score": 21, + "risk_score": 47, "rule_id": "cc92c835-da92-45c9-9f29-b4992ad621a0", - "severity": "low", + "severity": "medium", "tags": [ "Elastic", "Identity", @@ -31,5 +32,5 @@ "Identity and Access" ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_application.json new file mode 100644 index 0000000000000..ef4b940b87b60 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_application.json @@ -0,0 +1,35 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to delete an Okta application. An adversary may attempt to modify, deactivate, or delete an Okta application in order to weaken an organization's security controls or disrupt their business operations.", + "false_positives": [ + "Consider adding exceptions to this rule to filter false positives if your organization's Okta applications are regularly deleted and the behavior is expected." + ], + "index": [ + "filebeat-*", + "logs-okta*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Delete an Okta Application", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:application.lifecycle.delete", + "references": [ + "https://developer.okta.com/docs/reference/api/system-log/", + "https://developer.okta.com/docs/reference/api/event-types/" + ], + "risk_score": 21, + "rule_id": "d48e1c13-4aca-4d1f-a7b1-a9161c0ad86f", + "severity": "low", + "tags": [ + "Elastic", + "Identity", + "Okta", + "Continuous Monitoring", + "SecOps", + "Monitoring" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json index 211fdb1ae3474..295b9523eefdd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to delete an Okta policy in order to weaken an organization's security controls. For example, an adversary may attempt to delete an Okta multi-factor authentication (MFA) policy in order to weaken the authentication requirements for user accounts.", + "description": "Detects attempts to delete an Okta policy. An adversary may attempt to delete an Okta policy in order to weaken an organization's security controls. For example, an adversary may attempt to delete an Okta multi-factor authentication (MFA) policy in order to weaken the authentication requirements for user accounts.", "false_positives": [ "Consider adding exceptions to this rule to filter false positives if Okta policies are regularly deleted in your organization." ], @@ -12,16 +12,17 @@ ], "language": "kuery", "license": "Elastic License", - "name": "Attempt to Delete Okta Policy", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "name": "Attempt to Delete an Okta Policy", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:policy.lifecycle.delete", "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" ], - "risk_score": 21, + "risk_score": 47, "rule_id": "b4bb1440-0fcb-4ed1-87e5-b06d58efc5e9", - "severity": "low", + "severity": "medium", "tags": [ "Elastic", "Identity", @@ -31,5 +32,5 @@ "Monitoring" ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy_rule.json new file mode 100644 index 0000000000000..d752e7dcb21ad --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_delete_okta_policy_rule.json @@ -0,0 +1,36 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to delete a rule within an Okta policy. An adversary may attempt to delete an Okta policy rule in order to weaken an organization's security controls.", + "false_positives": [ + "Consider adding exceptions to this rule to filter false positives if Okta MFA rules are regularly modified in your organization." + ], + "index": [ + "filebeat-*", + "logs-okta*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Delete an Okta Policy Rule", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:policy.rule.delete", + "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", + "https://developer.okta.com/docs/reference/api/system-log/", + "https://developer.okta.com/docs/reference/api/event-types/" + ], + "risk_score": 21, + "rule_id": "d5d86bf5-cf0c-4c06-b688-53fdc072fdfd", + "severity": "low", + "tags": [ + "Elastic", + "Identity", + "Okta", + "Continuous Monitoring", + "SecOps", + "Monitoring" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_application.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_application.json new file mode 100644 index 0000000000000..5a05ea9277237 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_application.json @@ -0,0 +1,36 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to modify an Okta application. An adversary may attempt to modify, deactivate, or delete an Okta application in order to weaken an organization's security controls or disrupt their business operations.", + "false_positives": [ + "Consider adding exceptions to this rule to filter false positives if your organization's Okta applications are regularly modified and the behavior is expected." + ], + "index": [ + "filebeat-*", + "logs-okta*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Attempt to Modify an Okta Application", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:application.lifecycle.update", + "references": [ + "https://help.okta.com/en/prod/Content/Topics/Apps/Apps_Apps.htm", + "https://developer.okta.com/docs/reference/api/system-log/", + "https://developer.okta.com/docs/reference/api/event-types/" + ], + "risk_score": 21, + "rule_id": "c74fd275-ab2c-4d49-8890-e2943fa65c09", + "severity": "low", + "tags": [ + "Elastic", + "Identity", + "Okta", + "Continuous Monitoring", + "SecOps", + "Monitoring" + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json index 682dc17f0ed49..86ea5c81025c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_network_zone.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "Okta network zones can be configured to limit or restrict access to a network based on IP addresses or geolocations. An adversary may attempt to modify, delete, or deactivate an Okta network zone in order to remove or weaken an organization's security controls.", + "description": "Detects attempts to modify an Okta network zone. Okta network zones can be configured to limit or restrict access to a network based on IP addresses or geolocations. An adversary may attempt to modify, delete, or deactivate an Okta network zone in order to remove or weaken an organization's security controls.", "false_positives": [ "Consider adding exceptions to this rule to filter false positives if Oyour organization's Okta network zones are regularly modified." ], @@ -12,10 +12,11 @@ ], "language": "kuery", "license": "Elastic License", - "name": "Attempt to Modify Okta Network Zone", - "note": "The Okta Filebeat module must be enabled to use this rule.", - "query": "event.dataset:okta.system and event.action:(zone.update or zone.deactivate or zone.delete or network_zone.rule.disabled or zone.remove_blacklist)", + "name": "Attempt to Modify an Okta Network Zone", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:(zone.update or network_zone.rule.disabled or zone.remove_blacklist)", "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/network/network-zones.htm", "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" ], @@ -31,5 +32,5 @@ "Network Security" ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json index 88e556d37a27c..c43f5fae05aa8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to modify an Okta policy in order to weaken an organization's security controls. For example, an adversary may attempt to modify an Okta multi-factor authentication (MFA) policy in order to weaken the authentication requirements for user accounts.", + "description": "Detects attempts to modify an Okta policy. An adversary may attempt to modify an Okta policy in order to weaken an organization's security controls. For example, an adversary may attempt to modify an Okta multi-factor authentication (MFA) policy in order to weaken the authentication requirements for user accounts.", "false_positives": [ "Consider adding exceptions to this rule to filter false positives if Okta policies are regularly modified in your organization." ], @@ -12,8 +12,8 @@ ], "language": "kuery", "license": "Elastic License", - "name": "Attempt to Modify Okta Policy", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "name": "Attempt to Modify an Okta Policy", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:policy.lifecycle.update", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -31,5 +31,5 @@ "Monitoring" ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy_rule.json similarity index 58% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json rename to x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy_rule.json index eb726e24c89da..8590fb3110c4f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_mfa_rule.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_okta_policy_rule.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to modify an Okta multi-factor authentication (MFA) rule in order to remove or weaken an organization's security controls.", + "description": "Detects attempts to modify a rule within an Okta policy. An adversary may attempt to modify an Okta policy rule in order to weaken an organization's security controls.", "false_positives": [ "Consider adding exceptions to this rule to filter false positives if Okta MFA rules are regularly modified in your organization." ], @@ -12,10 +12,11 @@ ], "language": "kuery", "license": "Elastic License", - "name": "Attempt to Modify Okta MFA Rule", - "note": "The Okta Filebeat module must be enabled to use this rule.", - "query": "event.dataset:okta.system and event.action:(policy.rule.update or policy.rule.delete)", + "name": "Attempt to Modify an Okta Policy Rule", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:policy.rule.update", "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/Security_Policies.htm", "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" ], @@ -31,5 +32,5 @@ "Identity and Access" ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json index 262a91f8e25c9..d1459abb679b2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_attempt_to_modify_or_delete_application_sign_on_policy.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to modify or delete the sign on policy for an Okta application in order to remove or weaken an organization's security controls.", + "description": "Detects attempts to modify or delete a sign on policy for an Okta application. An adversary may attempt to modify or delete the sign on policy for an Okta application in order to remove or weaken an organization's security controls.", "false_positives": [ "Consider adding exceptions to this rule to filter false positives if sign on policies for Okta applications are regularly modified or deleted in your organization." ], @@ -13,9 +13,10 @@ "language": "kuery", "license": "Elastic License", "name": "Modification or Removal of an Okta Application Sign-On Policy", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:(application.policy.sign_on.update or application.policy.sign_on.rule.delete)", "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/App_Based_Signon.htm", "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" ], @@ -31,5 +32,5 @@ "Identity and Access" ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json index 0101ae0459454..2b3d8e88b0a49 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/okta_threat_detected_by_okta_threatinsight.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "This rule detects when Okta ThreatInsight identifies a request from a malicious IP address. Investigating requests from IP addresses identified as malicious by Okta ThreatInsight can help security teams monitor for and respond to credential based attacks against their organization, such as brute force and password spraying attacks.", + "description": "Detects when Okta ThreatInsight identifies a request from a malicious IP address. Investigating requests from IP addresses identified as malicious by Okta ThreatInsight can help security teams monitor for and respond to credential based attacks against their organization, such as brute force and password spraying attacks.", "index": [ "filebeat-*", "logs-okta*" @@ -10,7 +10,7 @@ "language": "kuery", "license": "Elastic License", "name": "Threat Detected by Okta ThreatInsight", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:security.threat.detected", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -28,5 +28,5 @@ "Monitoring" ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json index fad3e3c922478..e9f3729e6287a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_privileges_assigned_to_okta_group.json @@ -2,9 +2,9 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to assign administrator privileges to an Okta group in order to assign additional permissions to compromised user accounts.", + "description": "Detects when an administrator role is assigned to an Okta group. An adversary may attempt to assign administrator privileges to an Okta group in order to assign additional permissions to compromised user accounts and maintain access to their target organization.", "false_positives": [ - "Consider adding exceptions to this rule to filter false positives if administrator privileges are regularly assigned to Okta groups in your organization." + "Administrator roles may be assigned to Okta users by a Super Admin user. Verify that the behavior was expected. Exceptions can be added to this rule to filter expected behavior." ], "index": [ "filebeat-*", @@ -12,16 +12,17 @@ ], "language": "kuery", "license": "Elastic License", - "name": "Administrator Privileges Assigned to Okta Group", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "name": "Administrator Privileges Assigned to an Okta Group", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:group.privilege.grant", "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/administrators-admin-comparison.htm", "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" ], - "risk_score": 21, + "risk_score": 47, "rule_id": "b8075894-0b62-46e5-977c-31275da34419", - "severity": "low", + "severity": "medium", "tags": [ "Elastic", "Identity", @@ -48,5 +49,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_role_assigned_to_okta_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_role_assigned_to_okta_user.json new file mode 100644 index 0000000000000..b614511449aea --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_administrator_role_assigned_to_okta_user.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when an administrator role is assigned to an Okta user. An adversary may attempt to assign an administrator role to an Okta user in order to assign additional permissions to a user account and maintain access to their target's environment.", + "false_positives": [ + "Administrator roles may be assigned to Okta users by a Super Admin user. Verify that the behavior was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "index": [ + "filebeat-*", + "logs-okta*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Administrator Role Assigned to an Okta User", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:okta.system and event.action:user.account.privilege.grant", + "references": [ + "https://help.okta.com/en/prod/Content/Topics/Security/administrators-admin-comparison.htm", + "https://developer.okta.com/docs/reference/api/system-log/", + "https://developer.okta.com/docs/reference/api/event-types/" + ], + "risk_score": 47, + "rule_id": "f06414a6-f2a4-466d-8eba-10f85e8abf71", + "severity": "medium", + "tags": [ + "Elastic", + "Okta", + "SecOps", + "Monitoring", + "Continuous Monitoring" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appcertdlls_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appcertdlls_registry.json new file mode 100644 index 0000000000000..8f2c14ed5018c --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appcertdlls_registry.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to maintain persistence by creating registry keys using AppCert DLLs. AppCert DLLs are loaded by every process using the common API functions to create processes.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Registry Persistence via AppCert DLL", + "query": "registry where\n/* uncomment once stable length(bytes_written_string) > 0 and */\n registry.path : \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Session Manager\\\\AppCertDLLs\\\\*\"\n", + "risk_score": 47, + "rule_id": "513f0ffd-b317-4b9c-9494-92ce861f22c7", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1182", + "name": "AppCert DLLs", + "reference": "https://attack.mitre.org/techniques/T1182/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appinitdlls_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appinitdlls_registry.json new file mode 100644 index 0000000000000..174961449c6fc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_appinitdlls_registry.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Attackers may maintain persistence by creating registry keys using AppInit DLLs. AppInit DLLs are loaded by every process using the common library, user32.dll.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Registry Persistence via AppInit DLL", + "query": "registry where\n registry.path : (\"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Windows\\\\AppInit_Dlls\", \n \"HKLM\\\\SOFTWARE\\\\Wow6432Node\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Windows\\\\AppInit_Dlls\") and\n not process.executable : (\"C:\\\\Windows\\\\System32\\\\msiexec.exe\", \n \"C:\\\\Windows\\\\SysWOW64\\\\msiexec.exe\", \n \"C:\\\\Program Files\\\\Commvault\\\\ContentStore*\\\\Base\\\\cvd.exe\",\n \"C:\\\\Program Files (x86)\\\\Commvault\\\\ContentStore*\\\\Base\\\\cvd.exe\")\n", + "risk_score": 47, + "rule_id": "d0e159cf-73e9-40d1-a9ed-077e3158a855", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1103", + "name": "AppInit DLLs", + "reference": "https://attack.mitre.org/techniques/T1103/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json index 9d1a7c7aef464..7f69ce8c9e31f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_create_okta_api_token.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may create an Okta API token to maintain access to an organization's network while they work to achieve their objectives. An attacker may abuse an API token to execute techniques such as creating user accounts or disabling security rules or policies.", + "description": "Detects attempts to create an Okta API token. An adversary may create an Okta API token to maintain access to an organization's network while they work to achieve their objectives. An attacker may abuse an API token to execute techniques such as creating user accounts or disabling security rules or policies.", "false_positives": [ "If the behavior of creating Okta API tokens is expected, consider adding exceptions to this rule to filter false positives." ], @@ -13,15 +13,15 @@ "language": "kuery", "license": "Elastic License", "name": "Attempt to Create Okta API Token", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:system.api_token.create", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", "https://developer.okta.com/docs/reference/api/event-types/" ], - "risk_score": 21, + "risk_score": 47, "rule_id": "96b9f4ea-0e8c-435b-8d53-2096e75fcac5", - "severity": "low", + "severity": "medium", "tags": [ "Elastic", "Identity", @@ -48,5 +48,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json index 764c60b829498..10789088601c5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_mfa_for_okta_user_account.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may deactivate multi-factor authentication (MFA) for an Okta user account in order to weaken the authentication requirements for the account.", + "description": "Detects attempts to deactivate multi-factor authentication (MFA) for an Okta user. An adversary may deactivate MFA for an Okta user account in order to weaken the authentication requirements for the account.", "false_positives": [ "If the behavior of deactivating MFA for Okta user accounts is expected, consider adding exceptions to this rule to filter false positives." ], @@ -12,8 +12,8 @@ ], "language": "kuery", "license": "Elastic License", - "name": "Attempt to Deactivate MFA for Okta User Account", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "name": "Attempt to Deactivate MFA for an Okta User Account", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:user.mfa.factor.deactivate", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -48,5 +48,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json deleted file mode 100644 index 9003f6877341f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_deactivate_okta_policy.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "author": [ - "Elastic" - ], - "description": "An adversary may attempt to deactivate an Okta policy in order to weaken an organization's security controls. For example, an adversary may attempt to deactivate an Okta multi-factor authentication (MFA) policy in order to weaken the authentication requirements for user accounts.", - "false_positives": [ - "If the behavior of deactivating Okta policies is expected, consider adding exceptions to this rule to filter false positives." - ], - "index": [ - "filebeat-*", - "logs-okta*" - ], - "language": "kuery", - "license": "Elastic License", - "name": "Attempt to Deactivate Okta Policy", - "note": "The Okta Filebeat module must be enabled to use this rule.", - "query": "event.dataset:okta.system and event.action:policy.lifecycle.deactivate", - "references": [ - "https://developer.okta.com/docs/reference/api/system-log/", - "https://developer.okta.com/docs/reference/api/event-types/" - ], - "risk_score": 21, - "rule_id": "b719a170-3bdb-4141-b0e3-13e3cf627bfe", - "severity": "low", - "tags": [ - "Elastic", - "Identity", - "Okta", - "Continuous Monitoring", - "SecOps", - "Monitoring" - ], - "threat": [ - { - "framework": "MITRE ATT&CK", - "tactic": { - "id": "TA0003", - "name": "Persistence", - "reference": "https://attack.mitre.org/tactics/TA0003/" - }, - "technique": [ - { - "id": "T1098", - "name": "Account Manipulation", - "reference": "https://attack.mitre.org/techniques/T1098/" - } - ] - } - ], - "type": "query", - "version": 2 -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json index 4fef3e833a7b6..85ca7e5b17229 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_attempt_to_reset_mfa_factors_for_okta_user_account.json @@ -2,7 +2,7 @@ "author": [ "Elastic" ], - "description": "An adversary may attempt to remove the multi-factor authentication (MFA) factors registered on an Okta user's account in order to register new MFA factors and abuse the account to blend in with normal activity in the victim's environment.", + "description": "Detects attempts to reset an Okta user's enrolled multi-factor authentication (MFA) factors. An adversary may attempt to reset the MFA factors for an Okta user's account in order to register new MFA factors and abuse the account to blend in with normal activity in the victim's environment.", "false_positives": [ "Consider adding exceptions to this rule to filter false positives if the MFA factors for Okta user accounts are regularly reset in your organization." ], @@ -12,8 +12,8 @@ ], "language": "kuery", "license": "Elastic License", - "name": "Attempt to Reset MFA Factors for Okta User Account", - "note": "The Okta Filebeat module must be enabled to use this rule.", + "name": "Attempt to Reset MFA Factors for an Okta User Account", + "note": "The Okta Fleet integration or Filebeat module must be enabled to use this rule.", "query": "event.dataset:okta.system and event.action:user.mfa.factor.reset_all", "references": [ "https://developer.okta.com/docs/reference/api/system-log/", @@ -48,5 +48,5 @@ } ], "type": "query", - "version": 2 + "version": 3 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_change_launch_agents_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_change_launch_agents_file.json new file mode 100644 index 0000000000000..c54600fdf5f81 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_change_launch_agents_file.json @@ -0,0 +1,50 @@ +{ + "author": [ + "Elastic" + ], + "description": "An adversary can establish persistence by installing a new launch agent that executes at login by using launchd or launchctl to load a plist into the appropriate directories.", + "false_positives": [ + "Trusted applications persisting via LaunchAgent" + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Launch Agent Creation or Modification and Immediate Loading", + "query": "sequence by host.id with maxspan=1m\n [file where event.type != \"deletion\" and \n file.path : (\"/System/Library/LaunchAgents/*\", \"/Library/LaunchAgents/*\", \"/Users/*/Library/LaunchAgents/*\")\n ]\n [process where event.type in (\"start\", \"process_started\") and process.name == \"launchctl\" and process.args == \"load\"]\n", + "references": [ + "https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html" + ], + "risk_score": 21, + "rule_id": "082e3f8c-6f80-485c-91eb-5b112cb79b28", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1159", + "name": "Launch Agent", + "reference": "https://attack.mitre.org/techniques/T1159/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_modif_launch_deamon_sequence.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_modif_launch_deamon_sequence.json new file mode 100644 index 0000000000000..786fdc0ef8dc6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_creation_modif_launch_deamon_sequence.json @@ -0,0 +1,50 @@ +{ + "author": [ + "Elastic" + ], + "description": "Adversaries may create or modify launch daemons to repeatedly execute malicious payloads as part of persistence.", + "false_positives": [ + "Trusted applications persisting via LaunchDaemons" + ], + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "LaunchDaemon Creation or Modification and Immediate Loading", + "query": "sequence by host.id with maxspan=1m\n [file where event.type != \"deletion\" and file.path in (\"/System/Library/LaunchDaemons/*\", \" /Library/LaunchDaemons/*\")]\n [process where event.type in (\"start\", \"process_started\") and process.name == \"launchctl\" and process.args == \"load\"]\n", + "references": [ + "https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html" + ], + "risk_score": 21, + "rule_id": "9d19ece6-c20e-481a-90c5-ccca596537de", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1543", + "name": "Create or Modify System Process", + "reference": "https://attack.mitre.org/techniques/T1543/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_registry_ifeo_injection.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_registry_ifeo_injection.json new file mode 100644 index 0000000000000..5fb49313154c4 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_evasion_registry_ifeo_injection.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "The Debugger and SilentProcessExit registry keys can allow an adversary to intercept the execution of files, causing a different process to be executed. This functionality can be abused by an adversary to establish persistence.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Image File Execution Options Injection", + "query": "registry where length(registry.data.strings) > 0 and\n registry.path : (\"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Image File Execution Options\\\\*.exe\\\\Debugger\", \n \"HKLM\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Image File Execution Options\\\\*\\\\Debugger\", \n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\SilentProcessExit\\\\*\\\\MonitorProcess\", \n \"HKLM\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\SilentProcessExit\\\\*\\\\MonitorProcess\") and\n /* add FPs here */\n not registry.data.strings : (\"C:\\\\Program Files*\\\\ThinKiosk\\\\thinkiosk.exe\", \"*\\\\PSAppDeployToolkit\\\\*\")\n", + "references": [ + "https://oddvar.moe/2018/04/10/persistence-using-globalflags-in-image-file-execution-options-hidden-from-autoruns-exe/" + ], + "risk_score": 41, + "rule_id": "6839c821-011d-43bd-bd5b-acff00257226", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1183", + "name": "Image File Execution Options Injection", + "reference": "https://attack.mitre.org/techniques/T1183/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_folder_action_scripts_runtime.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_folder_action_scripts_runtime.json new file mode 100644 index 0000000000000..b114d44735086 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_folder_action_scripts_runtime.json @@ -0,0 +1,63 @@ +{ + "author": [ + "Elastic" + ], + "description": "A Folder Action script is executed when the folder to which it is attached has items added or removed, or when its window is opened, closed, moved, or resized. Adversaries may abuse this feature to establish persistence by utilizing a malicious script.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Persistence via Folder Action Script", + "query": "sequence by host.id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"com.apple.foundation.UserScriptService\"] by process.pid\n [process where event.type in (\"start\", \"process_started\") and process.name in (\"osascript\", \"sh\")] by process.ppid\n", + "references": [ + "https://posts.specterops.io/folder-actions-for-persistence-on-macos-8923f222343d" + ], + "risk_score": 47, + "rule_id": "c292fa52-4115-408a-b897-e14f684b3cb7", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Execution", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1037", + "name": "Boot or Logon Initialization Scripts", + "reference": "https://attack.mitre.org/techniques/T1037/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_admin_role_assigned_to_user.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_admin_role_assigned_to_user.json new file mode 100644 index 0000000000000..16f20b731dadb --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_admin_role_assigned_to_user.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when an admin role is assigned to a Google Workspace user. An adversary may assign an admin role to a user in order to elevate the permissions of another user account and persist in their target\u2019s environment.", + "false_positives": [ + "Google Workspace admin role assignments may be modified by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Google Workspace Admin Role Assigned to a User", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:ASSIGN_ROLE", + "references": [ + "https://support.google.com/a/answer/172176?hl=en" + ], + "risk_score": 47, + "rule_id": "68994a6c-c7ba-4e82-b476-26a26877adf6", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json new file mode 100644 index 0000000000000..8ca413dc898d0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_api_access_granted_via_domain_wide_delegation_of_authority.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when a domain-wide delegation of authority is granted to a service account. Domain-wide delegation can be configured to grant third-party and internal applications to access the data of Google Workspace users. An adversary may configure domain-wide delegation to maintain access to their target\u2019s data.", + "false_positives": [ + "Domain-wide delegation of authority may be granted to service accounts by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Google Workspace API Access Granted via Domain-Wide Delegation of Authority", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:AUTHORIZE_API_CLIENT_ACCESS", + "references": [ + "https://developers.google.com/admin-sdk/directory/v1/guides/delegation" + ], + "risk_score": 47, + "rule_id": "acbc8bb9-2486-49a8-8779-45fb5f9a93ee", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_custom_admin_role_created.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_custom_admin_role_created.json new file mode 100644 index 0000000000000..0b98ba7de8063 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_custom_admin_role_created.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when a custom admin role is created in Google Workspace. An adversary may create a custom admin role in order to elevate the permissions of other user accounts and persist in their target\u2019s environment.", + "false_positives": [ + "Custom Google Workspace admin roles may be created by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Google Workspace Custom Admin Role Created", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:CREATE_ROLE", + "references": [ + "https://support.google.com/a/answer/2406043?hl=en" + ], + "risk_score": 47, + "rule_id": "ad3f2807-2b3e-47d7-b282-f84acbbe14be", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_role_modified.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_role_modified.json new file mode 100644 index 0000000000000..d8c344cc0e0ba --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_google_workspace_role_modified.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects when a custom admin role or its permissions are modified. An adversary may modify a custom admin role in order to elevate the permissions of other user accounts and persist in their target\u2019s environment.", + "false_positives": [ + "Google Workspace admin roles may be modified by system administrators. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-130m", + "index": [ + "filebeat-*" + ], + "interval": "10m", + "language": "kuery", + "license": "Elastic License", + "name": "Google Workspace Role Modified", + "note": "### Important Information Regarding Google Workspace Event Lag Times\n- As per Google's documentation, Google Workspace administrators may observe lag times ranging from minutes up to 3 days between the time of an event's occurrence and the event being visible in the Google Workspace admin/audit logs.\n- This rule is configured to run every 10 minutes with a lookback time of 130 minutes.\n- To reduce the risk of false negatives, consider reducing the interval that the Google Workspace (formerly G Suite) Filebeat module polls Google's reporting API for new events.\n- By default, `var.interval` is set to 2 hours (2h). Consider changing this interval to a lower value, such as 10 minutes (10m).\n- See the following references for further information.\n - https://support.google.com/a/answer/7061566\n - https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-module-gsuite.html", + "query": "event.dataset:gsuite.admin and event.provider:admin and event.category:iam and event.action:(ADD_PRIVILEGE or UPDATE_ROLE)", + "references": [ + "https://support.google.com/a/answer/2406043?hl=en" + ], + "risk_score": 47, + "rule_id": "6f435062-b7fc-4af9-acea-5b1ead65c5a5", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Google Workspace", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_scripting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_scripting.json new file mode 100644 index 0000000000000..ad28885de0740 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_local_scheduled_task_scripting.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "A scheduled task was created by a Windows script via cscript.exe, wscript.exe or powershell.exe. This can be abused by an adversary to establish persistence.", + "false_positives": [ + "Legitimate scheduled tasks may be created during installation of new software." + ], + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Scheduled Task Created by a Windows Script", + "note": "Decode the base64 encoded Tasks Actions registry value to investigate the task's configured action.", + "query": "sequence by host.id with maxspan = 30s\n [library where file.name : \"taskschd.dll\" and process.name : (\"cscript.exe\", \"wscript.exe\", \"powershell.exe\")]\n [registry where registry.path : \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Schedule\\\\TaskCache\\\\Tasks\\\\*\\\\Actions\"]\n", + "risk_score": 43, + "rule_id": "689b9d57-e4d5-4357-ad17-9c334609d79a", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1053", + "name": "Scheduled Task/Job", + "reference": "https://attack.mitre.org/techniques/T1053/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_login_logout_hooks_defaults.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_login_logout_hooks_defaults.json new file mode 100644 index 0000000000000..579c51c1bd6fc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_login_logout_hooks_defaults.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies use of the Defaults command to install a login or logoff hook in MacOS. An adversary may abuse this capability to establish persistence in an environment by inserting code to be executed at login or logout.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Persistence via Login or Logout Hook", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.name == \"defaults\" and process.args == \"write\" and process.args in (\"LoginHook\", \"LogoutHook\")\n", + "references": [ + "https://www.virusbulletin.com/uploads/pdf/conference_slides/2014/Wardle-VB2014.pdf", + "https://www.manpagez.com/man/1/defaults/" + ], + "risk_score": 47, + "rule_id": "5d0265bf-dea9-41a9-92ad-48a8dcd05080", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1037", + "name": "Boot or Logon Initialization Scripts", + "reference": "https://attack.mitre.org/techniques/T1037/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_exchange_management_role_assignment.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_exchange_management_role_assignment.json new file mode 100644 index 0000000000000..851cfeb502e24 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_exchange_management_role_assignment.json @@ -0,0 +1,52 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when a new role is assigned to a management group in Microsoft 365. An adversary may attempt to add a role in order to maintain persistence in an environment.", + "false_positives": [ + "A new role may be assigned to a management group by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Exchange Management Group Role Assignment", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:Exchange and event.category:web and event.action:\"New-ManagementRoleAssignment\" and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/exchange/new-managementroleassignment?view=exchange-ps", + "https://docs.microsoft.com/en-us/microsoft-365/admin/add-users/about-admin-roles?view=o365-worldwide" + ], + "risk_score": 47, + "rule_id": "98995807-5b09-4e37-8a54-5cae5dc932d7", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Identity and Access" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_external_access_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_external_access_enabled.json new file mode 100644 index 0000000000000..350f775e48a58 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_external_access_enabled.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when external access is enabled in Microsoft Teams. External access lets Teams and Skype for Business users communicate with other users that are outside their organization. An adversary may enable external access or add an allowed domain to exfiltrate data or maintain persistence in an environment.", + "false_positives": [ + "Teams external access may be enabled by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Teams External Access Enabled", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:(SkypeForBusiness or MicrosoftTeams) and event.category:web and event.action:\"Set-CsTenantFederationConfiguration\" and o365.audit.Parameters.AllowFederatedUsers:True and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/microsoftteams/manage-external-access" + ], + "risk_score": 47, + "rule_id": "27f7c15a-91f8-4c3d-8b9e-1f99cc030a51", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_guest_access_enabled.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_guest_access_enabled.json new file mode 100644 index 0000000000000..69de0fce7dfc6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_microsoft_365_teams_guest_access_enabled.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies when guest access is enabled in Microsoft Teams. Guest access in Teams allows people outside the organization to access teams and channels. An adversary may enable guest access to maintain persistence in an environment.", + "false_positives": [ + "Teams guest access may be enabled by a system or network administrator. Verify that the configuration change was expected. Exceptions can be added to this rule to filter expected behavior." + ], + "from": "now-30m", + "index": [ + "filebeat-*" + ], + "language": "kuery", + "license": "Elastic License", + "name": "Microsoft 365 Teams Guest Access Enabled", + "note": "The Microsoft 365 Fleet integration or Filebeat module must be enabled to use this rule.", + "query": "event.dataset:o365.audit and event.provider:(SkypeForBusiness or MicrosoftTeams) and event.category:web and event.action:\"Set-CsTeamsClientConfiguration\" and o365.audit.Parameters.AllowGuestUser:True and event.outcome:success", + "references": [ + "https://docs.microsoft.com/en-us/powershell/module/skype/get-csteamsclientconfiguration?view=skype-ps" + ], + "risk_score": 47, + "rule_id": "5e552599-ddec-4e14-bad1-28aa42404388", + "severity": "medium", + "tags": [ + "Elastic", + "Cloud", + "Microsoft 365", + "Continuous Monitoring", + "SecOps", + "Configuration Audit" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1098", + "name": "Account Manipulation", + "reference": "https://attack.mitre.org/techniques/T1098/" + } + ] + } + ], + "type": "query", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_office_addins_file.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_office_addins_file.json new file mode 100644 index 0000000000000..ab7d3d4827161 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_office_addins_file.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to establish persistence on an endpoint by abusing Microsoft Office add-ins.", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Persistence via Microsoft Office AddIns", + "query": "file where event.type != \"deletion\" and\n wildcard(file.extension,\"wll\",\"xll\",\"ppa\",\"ppam\",\"xla\",\"xlam\") and\n wildcard(file.path, \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Word\\\\Startup\\\\*\",\n \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\AddIns\\\\*\",\n \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Excel\\\\XLSTART\\\\*\")\n", + "references": [ + "https://labs.mwrinfosecurity.com/blog/add-in-opportunities-for-office-persistence/" + ], + "risk_score": 71, + "rule_id": "f44fa4b6-524c-4e87-8d9e-a32599e4fb7c", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1137", + "name": "Office Application Startup", + "reference": "https://attack.mitre.org/techniques/T1137/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_outlook_vba_template.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_outlook_vba_template.json new file mode 100644 index 0000000000000..7818bc61e6752 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_ms_outlook_vba_template.json @@ -0,0 +1,51 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to establish persistence on an endpoint by installing a rogue Microsoft Outlook VBA Template.", + "false_positives": [ + "A legitimate VBA for Outlook is usually configured interactively via OUTLOOK.EXE." + ], + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Persistence via Microsoft Outlook VBA", + "query": "file where event.type != \"deletion\" and\n wildcard(file.path, \"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Outlook\\\\VbaProject.OTM\")\n", + "references": [ + "https://www.mdsec.co.uk/2020/11/a-fresh-outlook-on-mail-based-persistence/", + "https://www.linkedin.com/pulse/outlook-backdoor-using-vba-samir-b-/" + ], + "risk_score": 43, + "rule_id": "397945f3-d39a-4e6f-8bcb-9656c2031438", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1137", + "name": "Office Application Startup", + "reference": "https://attack.mitre.org/techniques/T1137/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json index e7f4598a19f33..c915dc79da65a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_priv_escalation_via_accessibility_features.json @@ -7,13 +7,16 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "Potential Modification of Accessibility Binaries", - "query": "event.category:process and event.type:(start or process_started) and process.parent.name:winlogon.exe and not process.name:(atbroker.exe or displayswitch.exe or magnify.exe or narrator.exe or osk.exe or sethc.exe or utilman.exe)", - "risk_score": 21, + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n process.parent.name : (\"Utilman.exe\", \"winlogon.exe\") and user.name == \"SYSTEM\" and\n process.args :\n (\n \"C:\\\\Windows\\\\System32\\\\osk.exe\",\n \"C:\\\\Windows\\\\System32\\\\Magnify.exe\",\n \"C:\\\\Windows\\\\System32\\\\Narrator.exe\",\n \"C:\\\\Windows\\\\System32\\\\Sethc.exe\",\n \"utilman.exe\",\n \"ATBroker.exe\",\n \"DisplaySwitch.exe\",\n \"sethc.exe\"\n )\n and not process.pe.original_file_name in\n (\n \"osk.exe\",\n \"sethc.exe\",\n \"utilman2.exe\",\n \"DisplaySwitch.exe\",\n \"ATBroker.exe\",\n \"ScreenMagnifier.exe\",\n \"SR.exe\",\n \"Narrator.exe\",\n \"magnify.exe\",\n \"MAGNIFY.EXE\"\n )\n\n/* uncomment once in winlogbeat to avoid bypass with rogue process with matching pe original file name */\n/* and process.code_signature.subject_name == \"Microsoft Windows\" and process.code_signature.status == \"trusted\" */\n", + "references": [ + "https://www.elastic.co/blog/practical-security-engineering-stateful-detection" + ], + "risk_score": 73, "rule_id": "7405ddf1-6c8e-41ce-818f-48bea6bcaed8", - "severity": "low", + "severity": "high", "tags": [ "Elastic", "Host", @@ -53,6 +56,6 @@ ] } ], - "type": "query", - "version": 4 + "type": "eql", + "version": 5 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_registry_uncommon.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_registry_uncommon.json new file mode 100644 index 0000000000000..c539ccfab16ed --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_registry_uncommon.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects changes to registry persistence keys that are uncommonly used or modified by legitimate programs. This could be an indication of an adversary's attempt to persist in a stealthy manner.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Uncommon Registry Persistence Change", + "query": "registry where\n /* uncomment once stable length(registry.data.strings) > 0 and */\n registry.path : (\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Terminal Server\\\\Install\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\\\\*\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Terminal Server\\\\Install\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Runonce\\\\*\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Windows\\\\Load\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Windows\\\\Run\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Windows\\\\IconServiceLib\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\Shell\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\Shell\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\AppSetup\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\Taskman\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\Userinit\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\VmApplet\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\Explorer\\\\Run\\\\*\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System\\\\Shell\",\n \"HKLM\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows\\\\System\\\\Scripts\\\\Logoff\\\\Script\",\n \"HKLM\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows\\\\System\\\\Scripts\\\\Logon\\\\Script\",\n \"HKLM\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows\\\\System\\\\Scripts\\\\Shutdown\\\\Script\",\n \"HKLM\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows\\\\System\\\\Scripts\\\\Startup\\\\Script\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\Explorer\\\\Run\\\\*\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System\\\\Shell\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows\\\\System\\\\Scripts\\\\Logoff\\\\Script\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows\\\\System\\\\Scripts\\\\Logon\\\\Script\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows\\\\System\\\\Scripts\\\\Shutdown\\\\Script\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Policies\\\\Microsoft\\\\Windows\\\\System\\\\Scripts\\\\Startup\\\\Script\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Active Setup\\\\Installed Components\\\\*\\\\ShellComponent\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows CE Services\\\\AutoStartOnConnect\\\\MicrosoftActiveSync\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows CE Services\\\\AutoStartOnDisconnect\\\\MicrosoftActiveSync\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Ctf\\\\LangBarAddin\\\\*\\\\FilePath\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Internet Explorer\\\\Extensions\\\\*\\\\Exec\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Internet Explorer\\\\Extensions\\\\*\\\\Script\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Command Processor\\\\Autorun\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Ctf\\\\LangBarAddin\\\\*\\\\FilePath\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Internet Explorer\\\\Extensions\\\\*\\\\Exec\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Internet Explorer\\\\Extensions\\\\*\\\\Script\",\n \"HKEY_USERS\\\\*\\\\SOFTWARE\\\\Microsoft\\\\Command Processor\\\\Autorun\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Image File Execution Options\\\\*\\\\VerifierDlls\",\n \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\GpExtensions\\\\*\\\\DllName\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\SafeBoot\\\\AlternateShell\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Terminal Server\\\\Wds\\\\rdpwd\\\\StartupPrograms\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Terminal Server\\\\WinStations\\\\RDP-Tcp\\\\InitialProgram\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Session Manager\\\\BootExecute\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Session Manager\\\\SetupExecute\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Session Manager\\\\Execute\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\Session Manager\\\\S0InitialCommand\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\ServiceControlManagerExtension\",\n \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Control\\\\BootVerificationProgram\\\\ImagePath\",\n \"HKLM\\\\SYSTEM\\\\Setup\\\\CmdLine\",\n \"HKEY_USERS\\\\*\\\\Environment\\\\UserInitMprLogonScript\") and\n \n not registry.data.strings : (\"C:\\\\Windows\\\\system32\\\\userinit.exe\", \"cmd.exe\", \"C:\\\\Program Files (x86)\\\\*.exe\",\n \"C:\\\\Program Files\\\\*.exe\") and\n not (process.name : \"rundll32.exe\" and registry.path : \"*\\\\Software\\\\Microsoft\\\\Internet Explorer\\\\Extensions\\\\*\\\\Script\") and\n not process.executable : (\"C:\\\\Windows\\\\System32\\\\msiexec.exe\",\n \"C:\\\\Windows\\\\SysWOW64\\\\msiexec.exe\",\n \"C:\\\\ProgramData\\\\Microsoft\\\\Windows Defender\\\\Platform\\\\*\\\\MsMpEng.exe\",\n \"C:\\\\Program Files\\\\*.exe\",\n \"C:\\\\Program Files (x86)\\\\*.exe\")\n", + "references": [ + "https://www.microsoftpressstore.com/articles/article.aspx?p=2762082&seqNum=2" + ], + "risk_score": 47, + "rule_id": "54902e45-3467-49a4-8abc-529f2c8cfb80", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1112", + "name": "Modify Registry", + "reference": "https://attack.mitre.org/techniques/T1112/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_run_key_and_startup_broad.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_run_key_and_startup_broad.json new file mode 100644 index 0000000000000..19f8566ec0258 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_run_key_and_startup_broad.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies run key or startup key registry modifications. In order to survive reboots and other system interrupts, attackers will modify run keys within the registry or leverage startup folder items as a form of persistence.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Startup or Run Key Registry Modification", + "query": "/* uncomment length once stable */\nregistry where /* length(registry.data.strings) > 0 and */\n registry.path : (\n /* Machine Hive */\n \"HKLM\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\\\\*\", \n \"HKLM\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\RunOnce\\\\*\", \n \"HKLM\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\RunOnceEx\\\\*\", \n \"HKLM\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Explorer\\\\User Shell Folders\\\\*\", \n \"HKLM\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Explorer\\\\Shell Folders\\\\*\", \n \"HKLM\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\Explorer\\\\Run\\\\*\", \n \"HKLM\\\\Software\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\Shell\\\\*\", \n /* Users Hive */\n \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\\\\*\", \n \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\RunOnce\\\\*\", \n \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\RunOnceEx\\\\*\", \n \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Explorer\\\\User Shell Folders\\\\*\", \n \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Explorer\\\\Shell Folders\\\\*\", \n \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\Explorer\\\\Run\\\\*\", \n \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Winlogon\\\\Shell\\\\*\"\n ) and\n /* add here common legit changes without making too restrictive as this is one of the most abused AESPs */\n not registry.data.strings : \"ctfmon.exe /n\" and\n not (registry.value : \"Application Restart #*\" and process.name : \"csrss.exe\") and\n user.domain != \"NT AUTHORITY\" and\n not registry.data.strings : (\"C:\\\\Program Files\\\\*.exe\", \"C:\\\\Program Files (x86)\\\\*.exe\") and\n not process.executable : (\"C:\\\\Windows\\\\System32\\\\msiexec.exe\", \"C:\\\\Windows\\\\SysWOW64\\\\msiexec.exe\")\n", + "risk_score": 21, + "rule_id": "97fc44d3-8dae-4019-ae83-298c3015600f", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1060", + "name": "Registry Run Keys / Startup Folder", + "reference": "https://attack.mitre.org/techniques/T1060/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_runtime_run_key_startup_susp_procs.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_runtime_run_key_startup_susp_procs.json new file mode 100644 index 0000000000000..ea2e3727b3d23 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_runtime_run_key_startup_susp_procs.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies execution of suspicious persistent programs (scripts, rundll32, etc.) by looking at process lineage and command line usage.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Execution of Persistent Suspicious Program", + "query": "/* userinit followed by explorer followed by early child process of explorer (unlikely to be launched interactively) within 1m */\nsequence by host.id, user.name with maxspan=1m\n [process where event.type in (\"start\", \"process_started\") and process.name : \"userinit.exe\" and process.parent.name : \"winlogon.exe\"]\n [process where event.type in (\"start\", \"process_started\") and process.name : \"explorer.exe\"]\n [process where event.type in (\"start\", \"process_started\") and process.parent.name : \"explorer.exe\" and\n /* add suspicious programs here */\n process.pe.original_file_name in (\"cscript.exe\",\n \"wscript.exe\",\n \"PowerShell.EXE\",\n \"MSHTA.EXE\",\n \"RUNDLL32.EXE\",\n \"REGSVR32.EXE\",\n \"RegAsm.exe\",\n \"MSBuild.exe\",\n \"InstallUtil.exe\") and\n /* add potential suspicious paths here */\n process.args : (\"C:\\\\Users\\\\*\", \"C:\\\\ProgramData\\\\*\", \"C:\\\\Windows\\\\Temp\\\\*\", \"C:\\\\Windows\\\\Tasks\\\\*\", \"C:\\\\PerfLogs\\\\*\", \"C:\\\\Intel\\\\*\")\n ]\n", + "risk_score": 47, + "rule_id": "e7125cea-9fe1-42a5-9a05-b0792cf86f5a", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1060", + "name": "Registry Run Keys / Startup Folder", + "reference": "https://attack.mitre.org/techniques/T1060/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_services_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_services_registry.json new file mode 100644 index 0000000000000..d6ca742d89b49 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_services_registry.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies processes modifying the services registry key directly, instead of through the expected Windows APIs. This could be an indication of an adversary attempting to stealthily persist through abnormal service creation or modification of an existing service.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Unusual Persistence via Services Registry", + "query": "registry where registry.path : (\"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Services\\\\*\\\\ServiceDLL\", \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Services\\\\*\\\\ImagePath\") and\n not registry.data.strings : (\"C:\\\\windows\\\\system32\\\\Drivers\\\\*.sys\", \n \"\\\\SystemRoot\\\\System32\\\\drivers\\\\*.sys\", \n \"system32\\\\DRIVERS\\\\USBSTOR\") and\n not (process.name : \"procexp??.exe\" and registry.data.strings : \"C:\\\\*\\\\procexp*.sys\") and\n not process.executable : (\"C:\\\\Program Files*\\\\*.exe\", \n \"C:\\\\Windows\\\\System32\\\\svchost.exe\", \n \"C:\\\\Windows\\\\winsxs\\\\*\\\\TiWorker.exe\", \n \"C:\\\\Windows\\\\System32\\\\drvinst.exe\", \n \"C:\\\\Windows\\\\System32\\\\services.exe\", \n \"C:\\\\Windows\\\\System32\\\\msiexec.exe\", \n \"C:\\\\Windows\\\\System32\\\\regsvr32.exe\")\n", + "risk_score": 21, + "rule_id": "403ef0d3-8259-40c9-a5b6-d48354712e49", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1050", + "name": "New Service", + "reference": "https://attack.mitre.org/techniques/T1050/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_suspicious_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_suspicious_process.json new file mode 100644 index 0000000000000..7a398dad485d2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_suspicious_process.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies files written to or modified in the startup folder by commonly abused processes. Adversaries may use this technique to maintain persistence.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Shortcut File Written or Modified for Persistence", + "query": "file where event.type != \"deletion\" and\n user.domain != \"NT AUTHORITY\" and\n file.path : (\"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Windows\\\\Start Menu\\\\Programs\\\\Startup\\\\*\", \n \"C:\\\\ProgramData\\\\Microsoft\\\\Windows\\\\Start Menu\\\\Programs\\\\StartUp\\\\*\") and\n process.name : (\"cmd.exe\",\n \"powershell.exe\",\n \"wmic.exe\",\n \"mshta.exe\",\n \"pwsh.exe\",\n \"cscript.exe\",\n \"wscript.exe\",\n \"regsvr32.exe\",\n \"RegAsm.exe\",\n \"rundll32.exe\",\n \"EQNEDT32.EXE\",\n \"WINWORD.EXE\",\n \"EXCEL.EXE\",\n \"POWERPNT.EXE\",\n \"MSPUB.EXE\",\n \"MSACCESS.EXE\",\n \"iexplore.exe\",\n \"InstallUtil.exe\")\n", + "risk_score": 47, + "rule_id": "440e2db4-bc7f-4c96-a068-65b78da59bde", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1060", + "name": "Registry Run Keys / Startup Folder", + "reference": "https://attack.mitre.org/techniques/T1060/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_unsigned_process.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_unsigned_process.json new file mode 100644 index 0000000000000..f9410f73ad61a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_file_written_by_unsigned_process.json @@ -0,0 +1,42 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies files written or modified in the startup folder by unsigned processes. Adversaries may abuse this technique to maintain persistence in an environment.", + "index": [ + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Startup Folder Persistence via Unsigned Process", + "query": "sequence by host.id, process.entity_id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\") and process.code_signature.trusted == false and\n /* suspicious paths can be added here */\n process.executable : (\"C:\\\\Users\\\\*.exe\", \n \"C:\\\\ProgramData\\\\*.exe\", \n \"C:\\\\Windows\\\\Temp\\\\*.exe\", \n \"C:\\\\Windows\\\\Tasks\\\\*.exe\", \n \"C:\\\\Intel\\\\*.exe\", \n \"C:\\\\PerfLogs\\\\*.exe\")\n ]\n [file where event.type != \"deletion\" and user.domain != \"NT AUTHORITY\" and\n file.path : (\"C:\\\\Users\\\\*\\\\AppData\\\\Roaming\\\\Microsoft\\\\Windows\\\\Start Menu\\\\Programs\\\\Startup\\\\*\", \n \"C:\\\\ProgramData\\\\Microsoft\\\\Windows\\\\Start Menu\\\\Programs\\\\StartUp\\\\*\")\n ]\n", + "risk_score": 41, + "rule_id": "2fba96c0-ade5-4bce-b92f-a5df2509da3f", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1060", + "name": "Registry Run Keys / Startup Folder", + "reference": "https://attack.mitre.org/techniques/T1060/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_scripts.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_scripts.json new file mode 100644 index 0000000000000..607cc7c8030dc --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_startup_folder_scripts.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies script engines creating files in the startup folder, or the creation of script files in the startup folder.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Persistent Scripts in the Startup Directory", + "query": "file where event.type != \"deletion\" and user.domain != \"NT AUTHORITY\"\n and (\n // detect shortcuts created by wscript.exe or cscript.exe\n file.path : \"C:\\\\*\\\\Programs\\\\Startup\\\\*.lnk\" and\n process.name : (\"wscript.exe\", \"cscript.exe\")\n ) or\n // detect vbs or js files created by any process\n file.path : (\"C:\\\\*\\\\Programs\\\\Startup\\\\*.vbs\", \n \"C:\\\\*\\\\Programs\\\\Startup\\\\*.vbe\", \n \"C:\\\\*\\\\Programs\\\\Startup\\\\*.wsh\", \n \"C:\\\\*\\\\Programs\\\\Startup\\\\*.wsf\", \n \"C:\\\\*\\\\Programs\\\\Startup\\\\*.js\")\n", + "risk_score": 47, + "rule_id": "f7c4dc5a-a58d-491d-9f14-9b66507121c0", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1060", + "name": "Registry Run Keys / Startup Folder", + "reference": "https://attack.mitre.org/techniques/T1060/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json new file mode 100644 index 0000000000000..117a5108d2cab --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_com_hijack_registry.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies Component Object Model (COM) hijacking via registry modification. Adversaries may establish persistence by executing malicious content triggered by hijacked references to COM objects.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Component Object Model Hijacking", + "query": "registry where\n /* uncomment once length is stable length(bytes_written_string) > 0 and */\n (registry.path : \"HK*}\\\\InprocServer32\\\\\" and registry.data.strings: (\"scrobj.dll\", \"C:\\\\*\\\\scrobj.dll\") and\n not registry.path : \"*\\\\{06290BD*-48AA-11D2-8432-006008C3FBFC}\\\\*\") \n or\n /* in general COM Registry changes on Users Hive is less noisy and worth alerting */\n (registry.path : (\"HKEY_USERS\\\\*Classes\\\\*\\\\InprocXServer32\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\*\\\\LocalServer32\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\*\\\\DelegateExecute\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\*\\\\TreatAs\\\\\", \n \"HKEY_USERS\\\\*Classes\\\\CLSID\\\\*\\\\ScriptletURL\\\\\") and\n /* not necessary but good for filtering privileged installations */\n user.domain != \"NT AUTHORITY\")\n", + "references": [ + "https://bohops.com/2018/08/18/abusing-the-com-registry-structure-part-2-loading-techniques-for-evasion-and-persistence/" + ], + "risk_score": 47, + "rule_id": "16a52c14-7883-47af-8745-9357803f0d4c", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1122", + "name": "Component Object Model Hijacking", + "reference": "https://attack.mitre.org/techniques/T1122/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_image_load_scheduled_task_ms_office.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_image_load_scheduled_task_ms_office.json new file mode 100644 index 0000000000000..74fc89eae5914 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_image_load_scheduled_task_ms_office.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a suspicious image load (taskschd.dll) from Microsoft Office processes. This behavior may indicate adversarial activity where a scheduled task is configured via Windows Component Object Model (COM). This technique can be used to configure persistence and evade monitoring by avoiding the usage of the traditional Windows binary (schtasks.exe) used to manage scheduled tasks.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Image Load (taskschd.dll) from MS Office", + "query": "library where process.name in (\"WINWORD.EXE\", \"EXCEL.EXE\", \"POWERPNT.EXE\", \"MSPUB.EXE\", \"MSACCESS.EXE\") and\n event.action == \"load\" and\n event.category == \"library\" and\n file.name == \"taskschd.dll\"\n", + "references": [ + "https://medium.com/threatpunter/detecting-adversary-tradecraft-with-image-load-event-logging-and-eql-8de93338c16", + "https://www.clearskysec.com/wp-content/uploads/2020/10/Operation-Quicksand.pdf" + ], + "risk_score": 21, + "rule_id": "baa5d22c-5e1c-4f33-bfc9-efa73bb53022", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Execution" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1053", + "name": "Scheduled Task/Job", + "reference": "https://attack.mitre.org/techniques/T1053/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_scheduled_task_runtime.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_scheduled_task_runtime.json new file mode 100644 index 0000000000000..101fc0eb0ac81 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_scheduled_task_runtime.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies execution of a suspicious program via scheduled tasks by looking at process lineage and command line usage.", + "false_positives": [ + "Legitimate scheduled tasks running third party software." + ], + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Execution via Scheduled Task", + "query": "process where event.type == \"start\" and\n /* Schedule service cmdline on Win10+ */\n process.parent.name : \"svchost.exe\" and process.parent.args : \"Schedule\" and\n /* add suspicious programs here */\n process.pe.original_file_name in\n (\n \"cscript.exe\",\n \"wscript.exe\",\n \"PowerShell.EXE\",\n \"Cmd.Exe\",\n \"MSHTA.EXE\",\n \"RUNDLL32.EXE\",\n \"REGSVR32.EXE\",\n \"MSBuild.exe\",\n \"InstallUtil.exe\",\n \"RegAsm.exe\",\n \"RegSvcs.exe\",\n \"msxsl.exe\",\n \"CONTROL.EXE\",\n \"EXPLORER.EXE\",\n \"Microsoft.Workflow.Compiler.exe\",\n \"msiexec.exe\"\n ) and\n /* add suspicious paths here */\n process.args : (\n \"C:\\\\Users\\\\*\",\n \"C:\\\\ProgramData\\\\*\", \n \"C:\\\\Windows\\\\Temp\\\\*\", \n \"C:\\\\Windows\\\\Tasks\\\\*\", \n \"C:\\\\PerfLogs\\\\*\", \n \"C:\\\\Intel\\\\*\", \n \"C:\\\\Windows\\\\Debug\\\\*\", \n \"C:\\\\HP\\\\*\")\n", + "risk_score": 43, + "rule_id": "5d1d6907-0747-4d5d-9b24-e4a18853dc0a", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1053", + "name": "Scheduled Task/Job", + "reference": "https://attack.mitre.org/techniques/T1053/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_service_created_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_service_created_registry.json new file mode 100644 index 0000000000000..6fea602025f46 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_suspicious_service_created_registry.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies the creation of a suspicious ImagePath value. This could be an indication of an adversary attempting to stealthily persist or escalate privileges through abnormal service creation.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious ImagePath Service Creation", + "query": "registry where registry.path : \"HKLM\\\\SYSTEM\\\\ControlSet*\\\\Services\\\\*\\\\ImagePath\" and\n /* add suspicious registry ImagePath values here */\n registry.data.strings : (\"%COMSPEC%*\", \"*\\\\.\\\\pipe\\\\*\")\n", + "risk_score": 73, + "rule_id": "36a8e048-d888-4f61-a8b9-0f9e2e40f317", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1050", + "name": "New Service", + "reference": "https://attack.mitre.org/techniques/T1050/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_hidden_run_key_valuename.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_hidden_run_key_valuename.json new file mode 100644 index 0000000000000..97bd9efa161e6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_hidden_run_key_valuename.json @@ -0,0 +1,48 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a persistence mechanism that utilizes the NtSetValueKey native API to create a hidden (null terminated) registry key. An adversary may use this method to hide from system utilities such as the Registry Editor (regedit).", + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Persistence via Hidden Run Key Detected", + "query": "/* Registry Path ends with backslash */\nregistry where /* length(registry.data.strings) > 0 and */\n registry.path : (\"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\\\\\", \n \"HKLM\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\\\\\", \n \"HKLM\\\\Software\\\\WOW6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Run\\\\\", \n \"HKEY_USERS\\\\*\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\Explorer\\\\Run\\\\\", \n \"HKLM\\\\Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\Explorer\\\\Run\\\\\")\n", + "references": [ + "https://github.com/outflanknl/SharpHide", + "https://github.com/ewhitehats/InvisiblePersistence/blob/master/InvisibleRegValues_Whitepaper.pdf" + ], + "risk_score": 73, + "rule_id": "a9b05c3b-b304-4bf9-970d-acdfaef2944c", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1060", + "name": "Registry Run Keys / Startup Folder", + "reference": "https://attack.mitre.org/techniques/T1060/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_lsa_security_support_provider_registry.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_lsa_security_support_provider_registry.json new file mode 100644 index 0000000000000..c1a0beb2e1fde --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_lsa_security_support_provider_registry.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies registry modifications related to the Windows Security Support Provider (SSP) configuration. Adversaries may abuse this to establish persistence in an environment.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Installation of Security Support Provider", + "query": "registry where\n registry.path : (\"HKLM\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Lsa\\\\Security Packages*\", \n \"HKLM\\\\SYSTEM\\\\CurrentControlSet\\\\Control\\\\Lsa\\\\OSConfig\\\\Security Packages*\") and\n not process.executable : (\"C:\\\\Windows\\\\System32\\\\msiexec.exe\", \"C:\\\\Windows\\\\SysWOW64\\\\msiexec.exe\")\n", + "risk_score": 47, + "rule_id": "e86da94d-e54b-4fb5-b96c-cecff87e8787", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1101", + "name": "Security Support Provider", + "reference": "https://attack.mitre.org/techniques/T1101/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json index 0622309387f35..389c4ba4a3d41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_telemetrycontroller_scheduledtask_hijack.json @@ -11,7 +11,7 @@ "language": "kuery", "license": "Elastic License", "name": "Persistence via TelemetryController Scheduled Task Hijack", - "query": "event.category:process and event.type:(start or process_started) and process.parent.name:(CompatTelRunner.exe or compattelrunner.exe) and not process.name:(conhost.exe or DeviceCensus.exe or devicecensus.exe or CompatTelRunner.exe or compattelrunner.exe or DismHost.exe or dismhost.exe or rundll32.exe)", + "query": "event.category:process and event.type:(start or process_started) and process.parent.name:(CompatTelRunner.exe or compattelrunner.exe) and process.args:-cv* and not process.name:(conhost.exe or DeviceCensus.exe or devicecensus.exe or CompatTelRunner.exe or compattelrunner.exe or DismHost.exe or dismhost.exe or rundll32.exe or powershell.exe)", "references": [ "https://www.trustedsec.com/blog/abusing-windows-telemetry-for-persistence/?utm_content=131234033&utm_medium=social&utm_source=twitter&hss_channel=tw-403811306" ], @@ -43,5 +43,5 @@ } ], "type": "query", - "version": 1 + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_windows_management_instrumentation_event_subscription.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_windows_management_instrumentation_event_subscription.json new file mode 100644 index 0000000000000..bb57e19369f62 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/persistence_via_windows_management_instrumentation_event_subscription.json @@ -0,0 +1,43 @@ +{ + "author": [ + "Elastic" + ], + "description": "An adversary can use Windows Management Instrumentation (WMI) to install event filters, providers, consumers, and bindings that execute code when a defined event occurs. Adversaries may use the capabilities of WMI to subscribe to an event and execute arbitrary code when that event occurs, providing persistence on a system.", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Persistence via WMI Event Subscription", + "query": "process where event.type in (\"start\", \"process_started\") and\n (process.name : \"wmic.exe\" or process.pe.original_file_name == \"wmic.exe\") and\n process.args : \"create\" and\n process.args : (\"ActiveScriptEventConsumer\", \"CommandLineEventConsumer\")\n\n", + "risk_score": 21, + "rule_id": "9b6813a1-daf1-457e-b0e6-0bb4e55b8a4c", + "severity": "low", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Persistence" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0003", + "name": "Persistence", + "reference": "https://attack.mitre.org/tactics/TA0003/" + }, + "technique": [ + { + "id": "T1546", + "name": "Event Triggered Execution", + "reference": "https://attack.mitre.org/techniques/T1546/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_apple_scripting.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_apple_scripting.json new file mode 100644 index 0000000000000..1b741cd1a8c97 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_explicit_creds_via_apple_scripting.json @@ -0,0 +1,64 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies execution of the security_authtrampoline process via the Apple script interpreter (osascript). This occurs when programs use AuthorizationExecute-WithPrivileges from the Security.framework to run another program with root privileges. It should not be run by itself, as this is a sign of execution with explicit logon credentials.", + "from": "now-9m", + "index": [ + "auditbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Execution with Explicit Credentials via Apple Scripting", + "query": "sequence by host.id with maxspan=5s\n [process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"osascript\"] by process.pid\n [process where event.type in (\"start\", \"process_started\") and process.name == \"security_authtrampoline\"] by process.ppid\n", + "references": [ + "https://objectivebythesea.com/v2/talks/OBTS_v2_Thomas.pdf", + "https://www.manpagez.com/man/8/security_authtrampoline/" + ], + "risk_score": 47, + "rule_id": "f0eb70e9-71e9-40cd-813f-bf8e8c812cb1", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "macOS", + "Threat Detection", + "Execution", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1078", + "name": "Valid Accounts", + "reference": "https://attack.mitre.org/techniques/T1078/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0002", + "name": "Execution", + "reference": "https://attack.mitre.org/tactics/TA0002/" + }, + "technique": [ + { + "id": "T1059", + "name": "Command and Scripting Interpreter", + "reference": "https://attack.mitre.org/techniques/T1059/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_named_pipe_impersonation.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_named_pipe_impersonation.json new file mode 100644 index 0000000000000..e440baf5c281e --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_named_pipe_impersonation.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a privilege escalation attempt via named pipe impersonation. An adversary may abuse this technique by utilizing a framework such Metasploit's meterpreter getsystem command.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Privilege Escalation via Named Pipe Impersonation", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.pe.original_file_name in (\"Cmd.Exe\", \"PowerShell.EXE\") and \n process.args : \"echo\" and process.args : \">\" and process.args : \"\\\\\\\\.\\\\pipe\\\\*\"\n", + "references": [ + "https://www.ired.team/offensive-security/privilege-escalation/windows-namedpipes-privilege-escalation" + ], + "risk_score": 73, + "rule_id": "3ecbdc9e-e4f2-43fa-8cca-63802125e582", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1134", + "name": "Access Token Manipulation", + "reference": "https://attack.mitre.org/techniques/T1134/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_registry_copyfiles.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_registry_copyfiles.json new file mode 100644 index 0000000000000..76b17b0d89229 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_printspooler_registry_copyfiles.json @@ -0,0 +1,49 @@ +{ + "author": [ + "Elastic" + ], + "description": "Detects attempts to exploit a privilege escalation vulnerability (CVE-2020-1030) related to the print spooler service. Exploitation involves chaining multiple primitives to load an arbitrary DLL into the print spooler process running as SYSTEM.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Suspicious Print Spooler Point and Print DLL", + "query": "sequence by host.id with maxspan=30s\n[registry where\n registry.path : \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Print\\\\Printers\\\\*\\\\SpoolDirectory\" and\n registry.data.strings : \"C:\\\\Windows\\\\System32\\\\spool\\\\drivers\\\\x64\\\\4\"]\n[registry where\n registry.path : \"HKLM\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\Print\\\\Printers\\\\*\\\\CopyFiles\\\\Payload\\\\Module\" and\n registry.data.strings : \"C:\\\\Windows\\\\System32\\\\spool\\\\drivers\\\\x64\\\\4\\\\*\"]\n", + "references": [ + "https://www.accenture.com/us-en/blogs/cyber-defense/discovering-exploiting-shutting-down-dangerous-windows-print-spooler-vulnerability", + "https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES/blob/master/Privilege%20Escalation/privesc_sysmon_cve_20201030_spooler.evtx", + "https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2020-1030" + ], + "risk_score": 74, + "rule_id": "bd7eefee-f671-494e-98df-f01daf9e5f17", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1068", + "name": "Exploitation for Privilege Escalation", + "reference": "https://attack.mitre.org/techniques/T1068/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_rogue_windir_environment_var.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_rogue_windir_environment_var.json new file mode 100644 index 0000000000000..6ad1d8f89fcdd --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_rogue_windir_environment_var.json @@ -0,0 +1,46 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies a privilege escalation attempt via a rogue Windows directory (Windir) environment variable. This is a known primitive that is often combined with other vulnerabilities to elevate privileges.", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Privilege Escalation via Windir Environment Variable", + "query": "registry where registry.path : (\"HKEY_USERS\\\\*\\\\Environment\\\\windir\", \"HKEY_USERS\\\\*\\\\Environment\\\\systemroot\") and \n not registry.data.strings : (\"C:\\\\windows\", \"%SystemRoot%\")\n", + "references": [ + "https://www.tiraniddo.dev/2017/05/exploiting-environment-variables-in.html" + ], + "risk_score": 71, + "rule_id": "d563aaba-2e72-462b-8658-3e5ea22db3a6", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1034", + "name": "Path Interception", + "reference": "https://attack.mitre.org/techniques/T1034/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_clipup.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_clipup.json new file mode 100644 index 0000000000000..2a1749d04fdfe --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_clipup.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to bypass User Account Control (UAC) by abusing an elevated COM Interface to launch a rogue Windows ClipUp program. Attackers may attempt to bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "UAC Bypass Attempt with IEditionUpgradeManager Elevated COM Interface", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and process.name == \"Clipup.exe\" and \nprocess.executable != \"C:\\\\Windows\\\\System32\\\\ClipUp.exe\" and process.parent.name == \"dllhost.exe\" and\n /* CLSID of the Elevated COM Interface IEditionUpgradeManager */\n wildcard(process.parent.args,\"/Processid:{BD54C901-076B-434E-B6C7-17C531F4AB41}\")\n", + "references": [ + "https://github.com/hfiref0x/UACME" + ], + "risk_score": 71, + "rule_id": "b90cdde7-7e0d-4359-8bf0-2c112ce2008a", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1088", + "name": "Bypass User Account Control", + "reference": "https://attack.mitre.org/techniques/T1088/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_ieinstal.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_ieinstal.json new file mode 100644 index 0000000000000..410124fdd699f --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_ieinstal.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies User Account Control (UAC) bypass attempts by abusing an elevated COM Interface to launch a malicious program. Attackers may attempt to bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "UAC Bypass Attempt via Elevated COM Internet Explorer Add-On Installer", + "query": "process where event.type in (\"start\", \"process_started\", \"info\") and\n wildcard(process.executable, \"C:\\\\*\\\\AppData\\\\*\\\\Temp\\\\IDC*.tmp\\\\*.exe\") and\n process.parent.name == \"ieinstal.exe\" and process.parent.args == \"-Embedding\"\n\n /* uncomment once in winlogbeat */\n /* and not (process.code_signature.subject_name == \"Microsoft Corporation\" and process.code_signature.trusted == true) */\n", + "references": [ + "https://swapcontext.blogspot.com/2020/11/uac-bypasses-from-comautoapprovallist.html" + ], + "risk_score": 47, + "rule_id": "fc7c0fa4-8f03-4b3e-8336-c5feab0be022", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1088", + "name": "Bypass User Account Control", + "reference": "https://attack.mitre.org/techniques/T1088/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_interface_icmluautil.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_interface_icmluautil.json new file mode 100644 index 0000000000000..9f5cdfffa57c7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_com_interface_icmluautil.json @@ -0,0 +1,44 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies User Account Control (UAC) bypass attempts via the ICMLuaUtil Elevated COM interface. Attackers may attempt to bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "UAC Bypass via ICMLuaUtil Elevated COM Interface", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name == \"dllhost.exe\" and\n process.parent.args in (\"/Processid:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}\", \"/Processid:{D2E7041B-2927-42FB-8E9F-7CE93B6DC937}\") and\n process.pe.original_file_name != \"WerFault.exe\"\n", + "risk_score": 73, + "rule_id": "68d56fdc-7ffa-4419-8e95-81641bd6f845", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1088", + "name": "Bypass User Account Control", + "reference": "https://attack.mitre.org/techniques/T1088/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json index 80b01f90d3cf4..50774166af698 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_diskcleanup_hijack.json @@ -8,10 +8,10 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "UAC Bypass via DiskCleanup Scheduled Task Hijack", - "query": "event.category:process and event.type:(start or process_started) and process.args:(/autoclean or /AUTOCLEAN) and process.parent.name:svchost.exe and not process.executable:(\"C:\\Windows\\System32\\cleanmgr.exe\" or \"C:\\Windows\\SysWOW64\\cleanmgr.exe\")", + "query": "process where event.type in (\"start\", \"process_started\") and\nprocess.args:\"/autoclean\" and process.args:\"/d\" and\nnot process.executable : (\"C:\\\\Windows\\\\System32\\\\cleanmgr.exe\", \"C:\\\\Windows\\\\SysWOW64\\\\cleanmgr.exe\")\n", "risk_score": 47, "rule_id": "1dcc51f6-ba26-49e7-9ef4-2655abb2361e", "severity": "medium", @@ -39,6 +39,6 @@ ] } ], - "type": "query", - "version": 1 + "type": "eql", + "version": 2 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_dll_sideloading.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_dll_sideloading.json new file mode 100644 index 0000000000000..5ad7ca602a36a --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_dll_sideloading.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to bypass User Account Control (UAC) via DLL side-loading. Attackers may attempt to bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "UAC Bypass Attempt via Privileged IFileOperation COM Interface", + "query": "file where event.type : \"change\" and process.name : \"dllhost.exe\" and\n /* Known modules names side loaded into process running with high or system integrity level for UAC Bypass, update here for new modules */\n file.name : (\"wow64log.dll\", \"comctl32.dll\", \"DismCore.dll\", \"OskSupport.dll\", \"duser.dll\", \"Accessibility.ni.dll\") and\n /* has no impact on rule logic just to avoid OS install related FPs */\n not file.path : (\"C:\\\\Windows\\\\SoftwareDistribution\\\\*\", \"C:\\\\Windows\\\\WinSxS\\\\*\")\n", + "references": [ + "https://github.com/hfiref0x/UACME" + ], + "risk_score": 73, + "rule_id": "5a14d01d-7ac8-4545-914c-b687c2cf66b3", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1088", + "name": "Bypass User Account Control", + "reference": "https://attack.mitre.org/techniques/T1088/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_mock_windir.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_mock_windir.json new file mode 100644 index 0000000000000..069dada4a099b --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_mock_windir.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies an attempt to bypass User Account Control (UAC) by masquerading as a Microsoft trusted Windows directory. Attackers may bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "UAC Bypass Attempt via Windows Directory Masquerading", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.args : (\"C:\\\\Windows \\\\system32\\\\*.exe\", \"C:\\\\Windows \\\\SysWOW64\\\\*.exe\")\n", + "references": [ + "https://medium.com/tenable-techblog/uac-bypass-by-mocking-trusted-directories-24a96675f6e" + ], + "risk_score": 71, + "rule_id": "290aca65-e94d-403b-ba0f-62f320e63f51", + "severity": "high", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1088", + "name": "Bypass User Account Control", + "reference": "https://attack.mitre.org/techniques/T1088/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_winfw_mmc_hijack.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_winfw_mmc_hijack.json new file mode 100644 index 0000000000000..23d18b4ad17d7 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_uac_bypass_winfw_mmc_hijack.json @@ -0,0 +1,47 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies attempts to bypass User Account Control (UAC) by hijacking the Microsoft Management Console (MMC) Windows Firewall snap-in. Attackers bypass UAC to stealthily execute code with elevated permissions.", + "from": "now-9m", + "index": [ + "winlogbeat-*", + "logs-endpoint.events.*" + ], + "language": "eql", + "license": "Elastic License", + "name": "UAC Bypass via Windows Firewall Snap-In Hijack", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name == \"mmc.exe\" and\n /* process.Ext.token.integrity_level_name == \"high\" can be added in future for tuning */\n /* args of the Windows Firewall SnapIn */\n process.parent.args == \"WF.msc\" and process.name != \"WerFault.exe\"\n", + "references": [ + "https://github.com/AzAgarampur/byeintegrity-uac" + ], + "risk_score": 47, + "rule_id": "1178ae09-5aff-460a-9f2f-455cd0ac4d8e", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Privilege Escalation" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1088", + "name": "Bypass User Account Control", + "reference": "https://attack.mitre.org/techniques/T1088/" + } + ] + } + ], + "type": "eql", + "version": 1 +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json index ad871716a67aa..a367f4c89a71c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_parentchild_relationship.json @@ -8,10 +8,14 @@ "winlogbeat-*", "logs-endpoint.events.*" ], - "language": "kuery", + "language": "eql", "license": "Elastic License", "name": "Unusual Parent-Child Relationship", - "query": "event.category:process and event.type:(start or process_started) and process.parent.executable:* and (process.parent.name:autochk.exe and not process.name:(chkdsk.exe or doskey.exe or WerFault.exe) or process.parent.name:smss.exe and not process.name:(autochk.exe or smss.exe or csrss.exe or wininit.exe or winlogon.exe or WerFault.exe) or process.name:autochk.exe and not process.parent.name:smss.exe or process.name:(fontdrvhost.exe or dwm.exe) and not process.parent.name:(wininit.exe or winlogon.exe) or process.name:(consent.exe or RuntimeBroker.exe or TiWorker.exe) and not process.parent.name:svchost.exe or process.name:wermgr.exe and not process.parent.name:(svchost.exe or TiWorker.exe) or process.name:SearchIndexer.exe and not process.parent.name:services.exe or process.name:SearchProtocolHost.exe and not process.parent.name:(SearchIndexer.exe or dllhost.exe) or process.name:dllhost.exe and not process.parent.name:(services.exe or svchost.exe) or process.name:smss.exe and not process.parent.name:(System or smss.exe) or process.name:csrss.exe and not process.parent.name:(smss.exe or svchost.exe) or process.name:wininit.exe and not process.parent.name:smss.exe or process.name:winlogon.exe and not process.parent.name:smss.exe or process.name:(lsass.exe or LsaIso.exe) and not process.parent.name:wininit.exe or process.name:LogonUI.exe and not process.parent.name:(wininit.exe or winlogon.exe) or process.name:services.exe and not process.parent.name:wininit.exe or process.name:svchost.exe and not process.parent.name:(MsMpEng.exe or services.exe) or process.name:spoolsv.exe and not process.parent.name:services.exe or process.name:taskhost.exe and not process.parent.name:(services.exe or svchost.exe) or process.name:taskhostw.exe and not process.parent.name:(services.exe or svchost.exe) or process.name:userinit.exe and not process.parent.name:(dwm.exe or winlogon.exe))", + "query": "process where event.type in (\"start\", \"process_started\") and\nprocess.parent.name != null and\n (\n /* suspicious parent processes */\n (process.name:\"autochk.exe\" and not process.parent.name:\"smss.exe\") or\n (process.name:(\"fontdrvhost.exe\", \"dwm.exe\") and not process.parent.name:(\"wininit.exe\", \"winlogon.exe\")) or\n (process.name:(\"consent.exe\", \"RuntimeBroker.exe\", \"TiWorker.exe\") and not process.parent.name:\"svchost.exe\") or\n (process.name:\"SearchIndexer.exe\" and not process.parent.name:\"services.exe\") or\n (process.name:\"SearchProtocolHost.exe\" and not process.parent.name:(\"SearchIndexer.exe\", \"dllhost.exe\")) or\n (process.name:\"dllhost.exe\" and not process.parent.name:(\"services.exe\", \"svchost.exe\")) or\n (process.name:\"smss.exe\" and not process.parent.name:(\"System\", \"smss.exe\")) or\n (process.name:\"csrss.exe\" and not process.parent.name:(\"smss.exe\", \"svchost.exe\")) or\n (process.name:\"wininit.exe\" and not process.parent.name:\"smss.exe\") or\n (process.name:\"winlogon.exe\" and not process.parent.name:\"smss.exe\") or\n (process.name:(\"lsass.exe\", \"LsaIso.exe\") and not process.parent.name:\"wininit.exe\") or\n (process.name:\"LogonUI.exe\" and not process.parent.name:(\"wininit.exe\", \"winlogon.exe\")) or\n (process.name:\"services.exe\" and not process.parent.name:\"wininit.exe\") or\n (process.name:\"svchost.exe\" and not process.parent.name:(\"MsMpEng.exe\", \"services.exe\")) or\n (process.name:\"spoolsv.exe\" and not process.parent.name:\"services.exe\") or\n (process.name:\"taskhost.exe\" and not process.parent.name:(\"services.exe\", \"svchost.exe\")) or\n (process.name:\"taskhostw.exe\" and not process.parent.name:(\"services.exe\", \"svchost.exe\")) or\n (process.name:\"userinit.exe\" and not process.parent.name:(\"dwm.exe\", \"winlogon.exe\")) or\n (process.name:(\"wmiprvse.exe\", \"wsmprovhost.exe\", \"winrshost.exe\") and not process.parent.name:\"svchost.exe\") or\n /* suspicious child processes */\n (process.parent.name:(\"SearchProtocolHost.exe\", \"taskhost.exe\", \"csrss.exe\") and not process.name:(\"werfault.exe\", \"wermgr.exe\", \"WerFaultSecure.exe\")) or\n (process.parent.name:\"autochk.exe\" and not process.name:(\"chkdsk.exe\", \"doskey.exe\", \"WerFault.exe\")) or\n (process.parent.name:\"smss.exe\" and not process.name:(\"autochk.exe\", \"smss.exe\", \"csrss.exe\", \"wininit.exe\", \"winlogon.exe\", \"setupcl.exe\", \"WerFault.exe\")) or\n (process.parent.name:\"wermgr.exe\" and not process.name:(\"WerFaultSecure.exe\", \"wermgr.exe\", \"WerFault.exe\")) or\n (process.parent.name:\"conhost.exe\" and not process.name:(\"mscorsvw.exe\", \"wermgr.exe\", \"WerFault.exe\", \"WerFaultSecure.exe\"))\n )\n", + "references": [ + "https://github.com/sbousseaden/Slides/blob/master/Hunting MindMaps/PNG/Windows Processes TH.map.png", + "https://www.andreafortuna.org/2017/06/15/standard-windows-processes-a-brief-reference/" + ], "risk_score": 47, "rule_id": "35df0dd8-092d-4a83-88c1-5151a804f31b", "severity": "medium", @@ -39,6 +43,6 @@ ] } ], - "type": "query", - "version": 5 + "type": "eql", + "version": 6 } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_svchost_childproc_childless.json b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_svchost_childproc_childless.json new file mode 100644 index 0000000000000..9ffd9eed711aa --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/prepackaged_rules/privilege_escalation_unusual_svchost_childproc_childless.json @@ -0,0 +1,62 @@ +{ + "author": [ + "Elastic" + ], + "description": "Identifies unusual child processes of Service Host (svchost.exe) that traditionally do not spawn any child processes. This may indicate a code injection or an equivalent form of exploitation.", + "false_positives": [ + "Changes to Windows services or a rarely executed child process." + ], + "from": "now-9m", + "index": [ + "logs-endpoint.events.*", + "winlogbeat-*" + ], + "language": "eql", + "license": "Elastic License", + "name": "Unusual Service Host Child Process - Childless Service", + "query": "process where event.type in (\"start\", \"process_started\") and\n process.parent.name : \"svchost.exe\" and\n\n /* based on svchost service arguments -s svcname where the service is known to be childless */\n\n process.parent.args : (\"WdiSystemHost\",\"LicenseManager\",\n \"StorSvc\",\"CDPSvc\",\"cdbhsvc\",\"BthAvctpSvc\",\"SstpSvc\",\"WdiServiceHost\",\n \"imgsvc\",\"TrkWks\",\"WpnService\",\"IKEEXT\",\"PolicyAgent\",\"CryptSvc\",\n \"netprofm\",\"ProfSvc\",\"StateRepository\",\"camsvc\",\"LanmanWorkstation\",\n \"NlaSvc\",\"EventLog\",\"hidserv\",\"DisplayEnhancementService\",\"ShellHWDetection\",\n \"AppHostSvc\",\"fhsvc\",\"CscService\",\"PushToInstall\") and\n\n /* unknown FPs can be added here */\n\n not process.name : (\"WerFault.exe\",\"WerFaultSecure.exe\",\"wermgr.exe\")\n", + "risk_score": 47, + "rule_id": "6a8ab9cc-4023-4d17-b5df-1a3e16882ce7", + "severity": "medium", + "tags": [ + "Elastic", + "Host", + "Windows", + "Threat Detection", + "Defense Evasion" + ], + "threat": [ + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0004", + "name": "Privilege Escalation", + "reference": "https://attack.mitre.org/tactics/TA0004/" + }, + "technique": [ + { + "id": "T1093", + "name": "Process Hollowing", + "reference": "https://attack.mitre.org/techniques/T1093/" + } + ] + }, + { + "framework": "MITRE ATT&CK", + "tactic": { + "id": "TA0005", + "name": "Defense Evasion", + "reference": "https://attack.mitre.org/tactics/TA0005/" + }, + "technique": [ + { + "id": "T1055", + "name": "Process Injection", + "reference": "https://attack.mitre.org/techniques/T1055/" + } + ] + } + ], + "type": "eql", + "version": 1 +} From efe62acd80245003e31141b7a76602a280aba82f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Fri, 11 Dec 2020 11:25:45 +0100 Subject: [PATCH 05/37] [Logs UI] Add helper hooks with search strategy request cancellation (#83906) --- x-pack/plugins/infra/.storybook/main.js | 7 +- .../common/search_strategies/common/errors.ts | 22 +- .../components/centered_flyout_body.tsx | 25 ++ .../data_search_error_callout.stories.tsx | 86 +++++++ .../components/data_search_error_callout.tsx | 77 +++++++ .../data_search_progress.stories.tsx | 52 +++++ .../components/data_search_progress.tsx | 45 ++++ .../log_entry_fields_table.tsx | 101 +++++++++ .../log_entry_flyout/log_entry_flyout.tsx | 214 +++++++----------- .../scrollable_log_text_stream_view.tsx | 15 +- .../logs/log_entries/api/fetch_log_entry.ts | 31 --- .../infra/public/containers/logs/log_entry.ts | 62 +++++ .../public/containers/logs/log_flyout.tsx | 81 +++---- .../log_entry_rate/page_results_content.tsx | 41 ++-- .../sections/anomalies/log_entry_example.tsx | 17 +- .../pages/logs/stream/page_logs_content.tsx | 28 +-- .../utils/data_search/data_search.stories.mdx | 140 ++++++++++++ .../infra/public/utils/data_search/index.ts | 9 + .../infra/public/utils/data_search/types.ts | 36 +++ .../use_data_search_request.test.tsx | 188 +++++++++++++++ .../data_search/use_data_search_request.ts | 97 ++++++++ ...test_partial_data_search_response.test.tsx | 116 ++++++++++ ...use_latest_partial_data_search_response.ts | 114 ++++++++++ .../infra/public/utils/use_observable.ts | 94 ++++++++ .../log_entry_search_strategy.test.ts | 30 +++ .../services/log_entries/queries/log_entry.ts | 2 +- 26 files changed, 1445 insertions(+), 285 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/centered_flyout_body.tsx create mode 100644 x-pack/plugins/infra/public/components/data_search_error_callout.stories.tsx create mode 100644 x-pack/plugins/infra/public/components/data_search_error_callout.tsx create mode 100644 x-pack/plugins/infra/public/components/data_search_progress.stories.tsx create mode 100644 x-pack/plugins/infra/public/components/data_search_progress.tsx create mode 100644 x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_fields_table.tsx delete mode 100644 x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts create mode 100644 x-pack/plugins/infra/public/containers/logs/log_entry.ts create mode 100644 x-pack/plugins/infra/public/utils/data_search/data_search.stories.mdx create mode 100644 x-pack/plugins/infra/public/utils/data_search/index.ts create mode 100644 x-pack/plugins/infra/public/utils/data_search/types.ts create mode 100644 x-pack/plugins/infra/public/utils/data_search/use_data_search_request.test.tsx create mode 100644 x-pack/plugins/infra/public/utils/data_search/use_data_search_request.ts create mode 100644 x-pack/plugins/infra/public/utils/data_search/use_latest_partial_data_search_response.test.tsx create mode 100644 x-pack/plugins/infra/public/utils/data_search/use_latest_partial_data_search_response.ts create mode 100644 x-pack/plugins/infra/public/utils/use_observable.ts diff --git a/x-pack/plugins/infra/.storybook/main.js b/x-pack/plugins/infra/.storybook/main.js index 1818aa44a9399..95e8ab8535a4f 100644 --- a/x-pack/plugins/infra/.storybook/main.js +++ b/x-pack/plugins/infra/.storybook/main.js @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -module.exports = require('@kbn/storybook').defaultConfig; +const defaultConfig = require('@kbn/storybook').defaultConfig; + +module.exports = { + ...defaultConfig, + stories: ['../**/*.stories.mdx', ...defaultConfig.stories], +}; diff --git a/x-pack/plugins/infra/common/search_strategies/common/errors.ts b/x-pack/plugins/infra/common/search_strategies/common/errors.ts index 4f7954c09c48b..3a08564f34941 100644 --- a/x-pack/plugins/infra/common/search_strategies/common/errors.ts +++ b/x-pack/plugins/infra/common/search_strategies/common/errors.ts @@ -6,12 +6,22 @@ import * as rt from 'io-ts'; -const genericErrorRT = rt.type({ +const abortedRequestSearchStrategyErrorRT = rt.type({ + type: rt.literal('aborted'), +}); + +export type AbortedRequestSearchStrategyError = rt.TypeOf< + typeof abortedRequestSearchStrategyErrorRT +>; + +const genericSearchStrategyErrorRT = rt.type({ type: rt.literal('generic'), message: rt.string, }); -const shardFailureErrorRT = rt.type({ +export type GenericSearchStrategyError = rt.TypeOf; + +const shardFailureSearchStrategyErrorRT = rt.type({ type: rt.literal('shardFailure'), shardInfo: rt.type({ shard: rt.number, @@ -21,6 +31,12 @@ const shardFailureErrorRT = rt.type({ message: rt.string, }); -export const searchStrategyErrorRT = rt.union([genericErrorRT, shardFailureErrorRT]); +export type ShardFailureSearchStrategyError = rt.TypeOf; + +export const searchStrategyErrorRT = rt.union([ + abortedRequestSearchStrategyErrorRT, + genericSearchStrategyErrorRT, + shardFailureSearchStrategyErrorRT, +]); export type SearchStrategyError = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/components/centered_flyout_body.tsx b/x-pack/plugins/infra/public/components/centered_flyout_body.tsx new file mode 100644 index 0000000000000..ec762610f36c4 --- /dev/null +++ b/x-pack/plugins/infra/public/components/centered_flyout_body.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlyoutBody } from '@elastic/eui'; +import { euiStyled } from '../../../observability/public'; + +export const CenteredEuiFlyoutBody = euiStyled(EuiFlyoutBody)` + & .euiFlyoutBody__overflow { + display: flex; + flex-direction: column; + } + + & .euiFlyoutBody__overflowContent { + align-items: center; + align-self: stretch; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: center; + overflow: hidden; + } +`; diff --git a/x-pack/plugins/infra/public/components/data_search_error_callout.stories.tsx b/x-pack/plugins/infra/public/components/data_search_error_callout.stories.tsx new file mode 100644 index 0000000000000..4e46e5fdd3f45 --- /dev/null +++ b/x-pack/plugins/infra/public/components/data_search_error_callout.stories.tsx @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PropsOf } from '@elastic/eui'; +import { Meta, Story } from '@storybook/react/types-6-0'; +import React from 'react'; +import { EuiThemeProvider } from '../../../observability/public'; +import { DataSearchErrorCallout } from './data_search_error_callout'; + +export default { + title: 'infra/dataSearch/DataSearchErrorCallout', + decorators: [ + (wrappedStory) => ( + +
{wrappedStory()}
+
+ ), + ], + parameters: { + layout: 'padded', + }, + argTypes: { + errors: { + control: { + type: 'object', + }, + }, + }, +} as Meta; + +type DataSearchErrorCalloutProps = PropsOf; + +const DataSearchErrorCalloutTemplate: Story = (args) => ( + +); + +const commonArgs = { + title: 'Failed to load data', + errors: [ + { + type: 'generic' as const, + message: 'A generic error message', + }, + { + type: 'shardFailure' as const, + shardInfo: { + index: 'filebeat-7.9.3-2020.12.01-000003', + node: 'a45hJUm3Tba4U8MkvkCU_g', + shard: 0, + }, + message: 'No mapping found for [@timestamp] in order to sort on', + }, + ], +}; + +export const ErrorCallout = DataSearchErrorCalloutTemplate.bind({}); + +ErrorCallout.args = { + ...commonArgs, +}; + +export const ErrorCalloutWithRetry = DataSearchErrorCalloutTemplate.bind({}); + +ErrorCalloutWithRetry.args = { + ...commonArgs, +}; +ErrorCalloutWithRetry.argTypes = { + onRetry: { action: 'retrying' }, +}; + +export const AbortedErrorCallout = DataSearchErrorCalloutTemplate.bind({}); + +AbortedErrorCallout.args = { + ...commonArgs, + errors: [ + { + type: 'aborted', + }, + ], +}; +AbortedErrorCallout.argTypes = { + onRetry: { action: 'retrying' }, +}; diff --git a/x-pack/plugins/infra/public/components/data_search_error_callout.tsx b/x-pack/plugins/infra/public/components/data_search_error_callout.tsx new file mode 100644 index 0000000000000..a0ed3bed95078 --- /dev/null +++ b/x-pack/plugins/infra/public/components/data_search_error_callout.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { + AbortedRequestSearchStrategyError, + GenericSearchStrategyError, + SearchStrategyError, + ShardFailureSearchStrategyError, +} from '../../common/search_strategies/common/errors'; + +export const DataSearchErrorCallout: React.FC<{ + title: React.ReactNode; + errors: SearchStrategyError[]; + onRetry?: () => void; +}> = ({ errors, onRetry, title }) => { + const calloutColor = errors.some((error) => error.type !== 'aborted') ? 'danger' : 'warning'; + + return ( + + {errors?.map((error, errorIndex) => ( + + ))} + {onRetry ? ( + + + + ) : null} + + ); +}; + +const DataSearchErrorMessage: React.FC<{ error: SearchStrategyError }> = ({ error }) => { + if (error.type === 'aborted') { + return ; + } else if (error.type === 'shardFailure') { + return ; + } else { + return ; + } +}; + +const AbortedRequestErrorMessage: React.FC<{ + error?: AbortedRequestSearchStrategyError; +}> = ({}) => ( + +); + +const GenericErrorMessage: React.FC<{ error: GenericSearchStrategyError }> = ({ error }) => ( +

{error.message ?? `${error}`}

+); + +const ShardFailureErrorMessage: React.FC<{ error: ShardFailureSearchStrategyError }> = ({ + error, +}) => ( + +); diff --git a/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx b/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx new file mode 100644 index 0000000000000..d5293a7282305 --- /dev/null +++ b/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { PropsOf } from '@elastic/eui'; +import { Meta, Story } from '@storybook/react/types-6-0'; +import React from 'react'; +import { EuiThemeProvider } from '../../../observability/public'; +import { DataSearchProgress } from './data_search_progress'; + +export default { + title: 'infra/dataSearch/DataSearchProgress', + decorators: [ + (wrappedStory) => ( + +
{wrappedStory()}
+
+ ), + ], + parameters: { + layout: 'padded', + }, +} as Meta; + +type DataSearchProgressProps = PropsOf; + +const DataSearchProgressTemplate: Story = (args) => ( + +); + +export const UndeterminedProgress = DataSearchProgressTemplate.bind({}); + +export const DeterminedProgress = DataSearchProgressTemplate.bind({}); + +DeterminedProgress.args = { + label: 'Searching', + maxValue: 10, + value: 3, +}; + +export const CancelableDeterminedProgress = DataSearchProgressTemplate.bind({}); + +CancelableDeterminedProgress.args = { + label: 'Searching', + maxValue: 10, + value: 3, +}; +CancelableDeterminedProgress.argTypes = { + onCancel: { action: 'canceled' }, +}; diff --git a/x-pack/plugins/infra/public/components/data_search_progress.tsx b/x-pack/plugins/infra/public/components/data_search_progress.tsx new file mode 100644 index 0000000000000..bf699ac976232 --- /dev/null +++ b/x-pack/plugins/infra/public/components/data_search_progress.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; + +export const DataSearchProgress: React.FC<{ + label?: React.ReactNode; + maxValue?: number; + onCancel?: () => void; + value?: number; +}> = ({ label, maxValue, onCancel, value }) => { + const valueText = useMemo( + () => + Number.isFinite(maxValue) && Number.isFinite(value) ? `${value} / ${maxValue}` : undefined, + [value, maxValue] + ); + + return ( + + + + + {onCancel ? ( + + + + ) : null} + + ); +}; + +const cancelButtonLabel = i18n.translate('xpack.infra.dataSearch.cancelButtonLabel', { + defaultMessage: 'Cancel request', +}); diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_fields_table.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_fields_table.tsx new file mode 100644 index 0000000000000..44e9902e0413f --- /dev/null +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_fields_table.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; +import { + LogEntry, + LogEntryField, +} from '../../../../common/search_strategies/log_entries/log_entry'; +import { TimeKey } from '../../../../common/time'; +import { FieldValue } from '../log_text_stream/field_value'; + +export const LogEntryFieldsTable: React.FC<{ + logEntry: LogEntry; + onSetFieldFilter?: (filter: string, logEntryId: string, timeKey?: TimeKey) => void; +}> = ({ logEntry, onSetFieldFilter }) => { + const createSetFilterHandler = useMemo( + () => + onSetFieldFilter + ? (field: LogEntryField) => () => { + onSetFieldFilter?.(`${field.field}:"${field.value}"`, logEntry.id, logEntry.key); + } + : undefined, + [logEntry, onSetFieldFilter] + ); + + const columns = useMemo>>( + () => [ + { + field: 'field', + name: i18n.translate('xpack.infra.logFlyout.fieldColumnLabel', { + defaultMessage: 'Field', + }), + sortable: true, + }, + { + actions: [ + { + type: 'icon', + icon: 'filter', + name: setFilterButtonLabel, + description: setFilterButtonDescription, + available: () => !!createSetFilterHandler, + onClick: (item) => createSetFilterHandler?.(item)(), + }, + ], + }, + { + field: 'value', + name: i18n.translate('xpack.infra.logFlyout.valueColumnLabel', { + defaultMessage: 'Value', + }), + render: (_name: string, item: LogEntryField) => ( + + ), + }, + ], + [createSetFilterHandler] + ); + + return ( + + columns={columns} + items={logEntry.fields} + search={searchOptions} + sorting={initialSortingOptions} + /> + ); +}; + +const emptyHighlightTerms: string[] = []; + +const initialSortingOptions = { + sort: { + field: 'field', + direction: 'asc' as const, + }, +}; + +const searchOptions = { + box: { + incremental: true, + schema: true, + }, +}; + +const setFilterButtonLabel = i18n.translate('xpack.infra.logFlyout.filterAriaLabel', { + defaultMessage: 'Filter', +}); + +const setFilterButtonDescription = i18n.translate('xpack.infra.logFlyout.setFilterTooltip', { + defaultMessage: 'View event with filter', +}); diff --git a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx index bc0f6dc97017a..5684d4068f3be 100644 --- a/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_entry_flyout/log_entry_flyout.tsx @@ -5,132 +5,60 @@ */ import { - EuiBasicTableColumn, - EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, - EuiInMemoryTable, EuiSpacer, EuiTextColor, EuiTitle, - EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import moment from 'moment'; -import React, { useCallback, useMemo } from 'react'; -import { euiStyled } from '../../../../../observability/public'; -import { - LogEntry, - LogEntryField, -} from '../../../../common/search_strategies/log_entries/log_entry'; +import React, { useEffect } from 'react'; import { TimeKey } from '../../../../common/time'; -import { InfraLoadingPanel } from '../../loading'; -import { FieldValue } from '../log_text_stream/field_value'; +import { useLogEntry } from '../../../containers/logs/log_entry'; +import { CenteredEuiFlyoutBody } from '../../centered_flyout_body'; +import { DataSearchErrorCallout } from '../../data_search_error_callout'; +import { DataSearchProgress } from '../../data_search_progress'; import { LogEntryActionsMenu } from './log_entry_actions_menu'; +import { LogEntryFieldsTable } from './log_entry_fields_table'; export interface LogEntryFlyoutProps { - flyoutError: string | null; - flyoutItem: LogEntry | null; - setFlyoutVisibility: (visible: boolean) => void; - setFilter: (filter: string, flyoutItemId: string, timeKey?: TimeKey) => void; - loading: boolean; + logEntryId: string | null | undefined; + onCloseFlyout: () => void; + onSetFieldFilter?: (filter: string, logEntryId: string, timeKey?: TimeKey) => void; + sourceId: string | null | undefined; } -const emptyHighlightTerms: string[] = []; - -const initialSortingOptions = { - sort: { - field: 'field', - direction: 'asc' as const, - }, -}; - -const searchOptions = { - box: { - incremental: true, - schema: true, - }, -}; - export const LogEntryFlyout = ({ - flyoutError, - flyoutItem, - loading, - setFlyoutVisibility, - setFilter, + logEntryId, + onCloseFlyout, + onSetFieldFilter, + sourceId, }: LogEntryFlyoutProps) => { - const createFilterHandler = useCallback( - (field: LogEntryField) => () => { - if (!flyoutItem) { - return; - } - - const filter = `${field.field}:"${field.value}"`; - const timestampMoment = moment(flyoutItem.key.time); - let target; + const { + cancelRequest: cancelLogEntryRequest, + errors: logEntryErrors, + fetchLogEntry, + isRequestRunning, + loaded: logEntryRequestProgress, + logEntry, + total: logEntryRequestTotal, + } = useLogEntry({ + sourceId, + logEntryId, + }); - if (timestampMoment.isValid()) { - target = { - time: timestampMoment.valueOf(), - tiebreaker: flyoutItem.key.tiebreaker, - }; - } - - setFilter(filter, flyoutItem.id, target); - }, - [flyoutItem, setFilter] - ); - - const closeFlyout = useCallback(() => setFlyoutVisibility(false), [setFlyoutVisibility]); - - const columns = useMemo>>( - () => [ - { - field: 'field', - name: i18n.translate('xpack.infra.logFlyout.fieldColumnLabel', { - defaultMessage: 'Field', - }), - sortable: true, - }, - { - field: 'value', - name: i18n.translate('xpack.infra.logFlyout.valueColumnLabel', { - defaultMessage: 'Value', - }), - render: (_name: string, item: LogEntryField) => ( - - - - - - - ), - }, - ], - [createFilterHandler] - ); + useEffect(() => { + if (sourceId && logEntryId) { + fetchLogEntry(); + } + }, [fetchLogEntry, sourceId, logEntryId]); return ( - + @@ -140,12 +68,12 @@ export const LogEntryFlyout = ({ defaultMessage="Details for log entry {logEntryId}" id="xpack.infra.logFlyout.flyoutTitle" values={{ - logEntryId: flyoutItem ? {flyoutItem.id} : '', + logEntryId: logEntryId ? {logEntryId} : '', }} /> - {flyoutItem ? ( + {logEntry ? ( <> @@ -153,7 +81,7 @@ export const LogEntryFlyout = ({ id="xpack.infra.logFlyout.flyoutSubTitle" defaultMessage="From index {indexName}" values={{ - indexName: {flyoutItem.index}, + indexName: {logEntry.index}, }} /> @@ -161,40 +89,54 @@ export const LogEntryFlyout = ({ ) : null} - {flyoutItem !== null ? : null} + {logEntry ? : null} - - {loading ? ( - - +
+ +
+ + ) : logEntry ? ( + 0 ? ( + + ) : undefined + } + > + + + ) : ( + +
+ - - ) : flyoutItem ? ( - - columns={columns} - items={flyoutItem.fields} - search={searchOptions} - sorting={initialSortingOptions} - /> - ) : ( - {flyoutError} - )} - +
+
+ )}
); }; -export const InfraFlyoutLoadingPanel = euiStyled.div` - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -`; +const loadingProgressMessage = i18n.translate('xpack.infra.logFlyout.loadingMessage', { + defaultMessage: 'Searching log entry in shards', +}); + +const loadingErrorCalloutTitle = i18n.translate('xpack.infra.logFlyout.loadingErrorCalloutTitle', { + defaultMessage: 'Error while searching the log entry', +}); diff --git a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx index ab0f0ac78529e..3c86ce3e32526 100644 --- a/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx @@ -51,8 +51,7 @@ interface ScrollableLogTextStreamViewProps { }) => any; loadNewerItems: () => void; reloadItems: () => void; - setFlyoutItem?: (id: string) => void; - setFlyoutVisibility?: (visible: boolean) => void; + onOpenLogEntryFlyout?: (logEntryId?: string) => void; setContextEntry?: (entry: LogEntry) => void; highlightedItem: string | null; currentHighlightKey: UniqueTimeKey | null; @@ -143,15 +142,14 @@ export class ScrollableLogTextStreamView extends React.PureComponent< lastLoadedTime, updateDateRange, startLiveStreaming, - setFlyoutItem, - setFlyoutVisibility, + onOpenLogEntryFlyout, setContextEntry, } = this.props; const hideScrollbar = this.props.hideScrollbar ?? true; const { targetId, items, isScrollLocked } = this.state; const hasItems = items.length > 0; - const hasFlyoutAction = !!(setFlyoutItem && setFlyoutVisibility); + const hasFlyoutAction = !!onOpenLogEntryFlyout; const hasContextAction = !!setContextEntry; return ( @@ -305,12 +303,7 @@ export class ScrollableLogTextStreamView extends React.PureComponent< } private handleOpenFlyout = (id: string) => { - const { setFlyoutItem, setFlyoutVisibility } = this.props; - - if (setFlyoutItem && setFlyoutVisibility) { - setFlyoutItem(id); - setFlyoutVisibility(true); - } + this.props.onOpenLogEntryFlyout?.(id); }; private handleOpenViewLogInContext = (entry: LogEntry) => { diff --git a/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts b/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts deleted file mode 100644 index 764de1d34a3bf..0000000000000 --- a/x-pack/plugins/infra/public/containers/logs/log_entries/api/fetch_log_entry.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ISearchStart } from '../../../../../../../../src/plugins/data/public'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; -import { - LogEntry, - LogEntrySearchRequestParams, - logEntrySearchRequestParamsRT, - logEntrySearchResponsePayloadRT, - LOG_ENTRY_SEARCH_STRATEGY, -} from '../../../../../common/search_strategies/log_entries/log_entry'; - -export { LogEntry }; - -export const fetchLogEntry = async ( - requestArgs: LogEntrySearchRequestParams, - search: ISearchStart -) => { - const response = await search - .search( - { params: logEntrySearchRequestParamsRT.encode(requestArgs) }, - { strategy: LOG_ENTRY_SEARCH_STRATEGY } - ) - .toPromise(); - - return decodeOrThrow(logEntrySearchResponsePayloadRT)(response.rawResponse); -}; diff --git a/x-pack/plugins/infra/public/containers/logs/log_entry.ts b/x-pack/plugins/infra/public/containers/logs/log_entry.ts new file mode 100644 index 0000000000000..af8618b8be565 --- /dev/null +++ b/x-pack/plugins/infra/public/containers/logs/log_entry.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback } from 'react'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import { + logEntrySearchRequestParamsRT, + logEntrySearchResponsePayloadRT, + LOG_ENTRY_SEARCH_STRATEGY, +} from '../../../common/search_strategies/log_entries/log_entry'; +import { useDataSearch, useLatestPartialDataSearchResponse } from '../../utils/data_search'; + +export const useLogEntry = ({ + sourceId, + logEntryId, +}: { + sourceId: string | null | undefined; + logEntryId: string | null | undefined; +}) => { + const { search: fetchLogEntry, requests$: logEntrySearchRequests$ } = useDataSearch({ + getRequest: useCallback(() => { + return !!logEntryId && !!sourceId + ? { + request: { + params: logEntrySearchRequestParamsRT.encode({ sourceId, logEntryId }), + }, + options: { strategy: LOG_ENTRY_SEARCH_STRATEGY }, + } + : null; + }, [sourceId, logEntryId]), + }); + + const { + cancelRequest, + isRequestRunning, + isResponsePartial, + latestResponseData, + latestResponseErrors, + loaded, + total, + } = useLatestPartialDataSearchResponse( + logEntrySearchRequests$, + null, + decodeLogEntrySearchResponse + ); + + return { + cancelRequest, + errors: latestResponseErrors, + fetchLogEntry, + isRequestRunning, + isResponsePartial, + loaded, + logEntry: latestResponseData ?? null, + total, + }; +}; + +const decodeLogEntrySearchResponse = decodeOrThrow(logEntrySearchResponsePayloadRT); diff --git a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx index 121f0e6b651dc..7f35af5800518 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_flyout.tsx @@ -6,12 +6,8 @@ import createContainer from 'constate'; import { isString } from 'lodash'; -import React, { useContext, useEffect, useMemo, useState } from 'react'; -import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; +import React, { useCallback, useState } from 'react'; import { UrlStateContainer } from '../../utils/url_state'; -import { useTrackedPromise } from '../../utils/use_tracked_promise'; -import { fetchLogEntry } from './log_entries/api/fetch_log_entry'; -import { useLogSourceContext } from './log_source'; export enum FlyoutVisibility { hidden = 'hidden', @@ -25,97 +21,78 @@ export interface FlyoutOptionsUrlState { } export const useLogFlyout = () => { - const { services } = useKibanaContextForPlugin(); - const { sourceId } = useLogSourceContext(); - const [flyoutVisible, setFlyoutVisibility] = useState(false); - const [flyoutId, setFlyoutId] = useState(null); + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + const [logEntryId, setLogEntryId] = useState(null); const [surroundingLogsId, setSurroundingLogsId] = useState(null); - const [loadFlyoutItemRequest, loadFlyoutItem] = useTrackedPromise( - { - cancelPreviousOn: 'creation', - createPromise: async () => { - if (!flyoutId) { - throw new Error('Failed to load log entry: Id not specified.'); - } - return await fetchLogEntry({ sourceId, logEntryId: flyoutId }, services.data.search); - }, - }, - [sourceId, flyoutId] - ); - - const isLoading = useMemo(() => { - return loadFlyoutItemRequest.state === 'pending'; - }, [loadFlyoutItemRequest.state]); - - useEffect(() => { - if (flyoutId) { - loadFlyoutItem(); + const closeFlyout = useCallback(() => setIsFlyoutOpen(false), []); + const openFlyout = useCallback((newLogEntryId?: string) => { + if (newLogEntryId) { + setLogEntryId(newLogEntryId); } - }, [loadFlyoutItem, flyoutId]); + setIsFlyoutOpen(true); + }, []); return { - flyoutVisible, - setFlyoutVisibility, - flyoutId, - setFlyoutId, + isFlyoutOpen, + closeFlyout, + openFlyout, + logEntryId, + setLogEntryId, surroundingLogsId, setSurroundingLogsId, - isLoading, - flyoutItem: - loadFlyoutItemRequest.state === 'resolved' ? loadFlyoutItemRequest.value.data : null, - flyoutError: - loadFlyoutItemRequest.state === 'rejected' ? `${loadFlyoutItemRequest.value}` : null, }; }; export const LogFlyout = createContainer(useLogFlyout); +export const [LogEntryFlyoutProvider, useLogEntryFlyoutContext] = LogFlyout; export const WithFlyoutOptionsUrlState = () => { const { - flyoutVisible, - setFlyoutVisibility, - flyoutId, - setFlyoutId, + isFlyoutOpen, + openFlyout, + closeFlyout, + logEntryId, + setLogEntryId, surroundingLogsId, setSurroundingLogsId, - } = useContext(LogFlyout.Context); + } = useLogEntryFlyoutContext(); return ( { if (newUrlState && newUrlState.flyoutId) { - setFlyoutId(newUrlState.flyoutId); + setLogEntryId(newUrlState.flyoutId); } if (newUrlState && newUrlState.surroundingLogsId) { setSurroundingLogsId(newUrlState.surroundingLogsId); } if (newUrlState && newUrlState.flyoutVisibility === FlyoutVisibility.visible) { - setFlyoutVisibility(true); + openFlyout(); } if (newUrlState && newUrlState.flyoutVisibility === FlyoutVisibility.hidden) { - setFlyoutVisibility(false); + closeFlyout(); } }} onInitialize={(initialUrlState) => { if (initialUrlState && initialUrlState.flyoutId) { - setFlyoutId(initialUrlState.flyoutId); + setLogEntryId(initialUrlState.flyoutId); } if (initialUrlState && initialUrlState.surroundingLogsId) { setSurroundingLogsId(initialUrlState.surroundingLogsId); } if (initialUrlState && initialUrlState.flyoutVisibility === FlyoutVisibility.visible) { - setFlyoutVisibility(true); + openFlyout(); } if (initialUrlState && initialUrlState.flyoutVisibility === FlyoutVisibility.hidden) { - setFlyoutVisibility(false); + closeFlyout(); } }} /> diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index bb0c9196fb0cc..c4a464a4cffad 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -7,21 +7,25 @@ import datemath from '@elastic/datemath'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui'; import moment from 'moment'; -import { encode, RisonValue } from 'rison-node'; import { stringify } from 'query-string'; -import React, { useCallback, useEffect, useMemo, useState, useContext } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { encode, RisonValue } from 'rison-node'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { euiStyled, useTrackPageview } from '../../../../../observability/public'; import { TimeRange } from '../../../../common/http_api/shared/time_range'; import { bucketSpan } from '../../../../common/log_analysis'; +import { TimeKey } from '../../../../common/time'; import { CategoryJobNoticesSection, LogAnalysisJobProblemIndicator, } from '../../../components/logging/log_analysis_job_status'; import { DatasetsSelector } from '../../../components/logging/log_analysis_results/datasets_selector'; import { useLogAnalysisSetupFlyoutStateContext } from '../../../components/logging/log_analysis_setup/setup_flyout'; +import { LogEntryFlyout } from '../../../components/logging/log_entry_flyout'; import { useLogAnalysisCapabilitiesContext } from '../../../containers/logs/log_analysis/log_analysis_capabilities'; import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_categories'; import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; +import { useLogEntryFlyoutContext } from '../../../containers/logs/log_flyout'; import { useLogSourceContext } from '../../../containers/logs/log_source'; import { useInterval } from '../../../hooks/use_interval'; import { AnomaliesResults } from './sections/anomalies'; @@ -31,9 +35,6 @@ import { StringTimeRange, useLogAnalysisResultsUrlState, } from './use_log_entry_rate_results_url_state'; -import { LogEntryFlyout, LogEntryFlyoutProps } from '../../../components/logging/log_entry_flyout'; -import { LogFlyout } from '../../../containers/logs/log_flyout'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; export const SORT_DEFAULTS = { direction: 'desc' as const, @@ -77,6 +78,12 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { setAutoRefresh, } = useLogAnalysisResultsUrlState(); + const { + closeFlyout: closeLogEntryFlyout, + isFlyoutOpen: isLogEntryFlyoutOpen, + logEntryId: flyoutLogEntryId, + } = useLogEntryFlyoutContext(); + const [queryTimeRange, setQueryTimeRange] = useState<{ value: TimeRange; lastChangedTime: number; @@ -85,8 +92,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { lastChangedTime: Date.now(), })); - const linkToLogStream = useCallback( - (filter, id, timeKey) => { + const linkToLogStream = useCallback( + (filter: string, id: string, timeKey?: TimeKey) => { const params = { logPosition: encode({ end: moment(queryTimeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), @@ -144,14 +151,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { filteredDatasets: selectedDatasets, }); - const { - flyoutVisible, - setFlyoutVisibility, - flyoutError, - flyoutItem, - isLoading: isFlyoutLoading, - } = useContext(LogFlyout.Context); - const handleQueryTimeRangeChange = useCallback( ({ start: startTime, end: endTime }: { start: string; end: string }) => { setQueryTimeRange({ @@ -305,14 +304,12 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { - - {flyoutVisible ? ( + {isLogEntryFlyoutOpen ? ( ) : null} diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx index a226486666095..b639cecf676ad 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/log_entry_example.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useCallback, useState, useContext } from 'react'; +import React, { useMemo, useCallback, useState } from 'react'; import moment from 'moment'; import { encode } from 'rison-node'; import { i18n } from '@kbn/i18n'; @@ -37,7 +37,7 @@ import { } from '../../../../../utils/source_configuration'; import { localizedDate } from '../../../../../../common/formatters/datetime'; import { LogEntryAnomaly } from '../../../../../../common/http_api'; -import { LogFlyout } from '../../../../../containers/logs/log_flyout'; +import { useLogEntryFlyoutContext } from '../../../../../containers/logs/log_flyout'; export const exampleMessageScale = 'medium' as const; export const exampleTimestampFormat = 'time' as const; @@ -88,7 +88,7 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ const setItemIsHovered = useCallback(() => setIsHovered(true), []); const setItemIsNotHovered = useCallback(() => setIsHovered(false), []); - const { setFlyoutVisibility, setFlyoutId } = useContext(LogFlyout.Context); + const { openFlyout: openLogEntryFlyout } = useLogEntryFlyoutContext(); // handle special cases for the dataset value const humanFriendlyDataset = getFriendlyNameForPartitionId(dataset); @@ -129,8 +129,7 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ { label: VIEW_DETAILS_LABEL, onClick: () => { - setFlyoutId(id); - setFlyoutVisibility(true); + openLogEntryFlyout(id); }, }, { @@ -144,13 +143,7 @@ export const LogEntryExampleMessage: React.FunctionComponent = ({ href: viewAnomalyInMachineLearningLinkProps.href, }, ]; - }, [ - id, - setFlyoutId, - setFlyoutVisibility, - viewInStreamLinkProps, - viewAnomalyInMachineLearningLinkProps, - ]); + }, [id, openLogEntryFlyout, viewInStreamLinkProps, viewAnomalyInMachineLearningLinkProps]); return ( { const { sourceConfiguration, sourceId } = useLogSourceContext(); const { textScale, textWrap } = useContext(LogViewConfiguration.Context); const { - setFlyoutVisibility, - flyoutVisible, - setFlyoutId, surroundingLogsId, setSurroundingLogsId, - flyoutItem, - flyoutError, - isLoading, - } = useContext(LogFlyoutState.Context); + closeFlyout: closeLogEntryFlyout, + openFlyout: openLogEntryFlyout, + isFlyoutOpen, + logEntryId: flyoutLogEntryId, + } = useLogEntryFlyoutContext(); const { logSummaryHighlights } = useContext(LogHighlightsState.Context); const { applyLogFilterQuery } = useContext(LogFilterState.Context); const { @@ -76,13 +74,12 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { - {flyoutVisible ? ( + {isFlyoutOpen ? ( ) : null} @@ -116,8 +113,7 @@ export const LogsPageLogsContent: React.FunctionComponent = () => { scale={textScale} target={targetPosition} wrap={textWrap} - setFlyoutItem={setFlyoutId} - setFlyoutVisibility={setFlyoutVisibility} + onOpenLogEntryFlyout={openLogEntryFlyout} setContextEntry={setContextEntry} highlightedItem={surroundingLogsId ? surroundingLogsId : null} currentHighlightKey={currentHighlightKey} diff --git a/x-pack/plugins/infra/public/utils/data_search/data_search.stories.mdx b/x-pack/plugins/infra/public/utils/data_search/data_search.stories.mdx new file mode 100644 index 0000000000000..a698b806b4cd7 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/data_search/data_search.stories.mdx @@ -0,0 +1,140 @@ +import { Canvas, Meta, Story } from '@storybook/addon-docs/blocks'; + + + +# The `data` plugin and `SearchStrategies` + +The search functionality abstraction provided by the `search` service of the +`data` plugin is pretty powerful: + +- The execution of the request is delegated to a search strategy, which is + executed on the Kibana server side. +- Any plugin can register custom search strategies with custom parameters and + response shapes. +- Search requests can be cancelled via an `AbortSignal`. +- Search requests are decoupled from the transport layer. The service will poll + for new results transparently. +- Partial responses can be returned as they become available if the search + takes longer. + +# Working with `data.search.search()` in the Browser + +The following chapters describe a set of React components and hooks that aim to +make it easy to take advantage of these characteristics from client-side React +code. They implement a producer/consumer pattern that decouples the craeation +of search requests from the consumption of the responses. This keeps each +code-path small and encourages the use of reactive processing, which in turn +reduces the risk of race conditions and incorrect assumptions about the +response timing. + +## Issuing new requests + +The main API to issue new requests is the `data.search.search()` function. It +returns an `Observable` representing the stream of partial and final results +without the consumer having to know the underlying transport mechanisms. +Besides receiving a search-strategy-specific parameter object, it supports +selection of the search strategy as well an `AbortSignal` used for request +cancellation. + +The hook `useDataSearch()` is designed to ease the integration between the +`Observable` world and the React world. It uses the function it is given to +derive the parameters to use for the next search request. The request can then +be issued by calling the returned `search()` function. For each new request the +hook emits an object describing the request and its state in the `requests$` +`Observable`. + +```typescript +const { search, requests$ } = useDataSearch({ + getRequest: useCallback((searchTerm: string) => ({ + request: { + params: { + searchTerm + } + } + }), []); +}); +``` + +## Executing requests and consuming the responses + +The response `Observable`s emitted by `data.search.search()` is "cold", so it +won't be executed unless a subscriber subscribes to it. And in order to cleanly +cancel and garbage collect the subscription it should be integrated with the +React component life-cycle. + +The `useLatestPartialDataSearchResponse()` does that in such a way that the +newest response observable is subscribed to and that any previous response +observables are unsubscribed from for proper cancellation if a new request has +been created. This uses RxJS's `switchMap()` operator under the hood. The hook +also makes sure that all observables are unsubscribed from on unmount. + +Since the specific response shape depends on the data strategy used, the hook +takes a projection function, that is responsible for decoding the response in +an appropriate way. + +A request can fail due to various reasons that include servers-side errors, +Elasticsearch shard failures and network failures. The intention is to map all +of them to a common `SearchStrategyError` interface. While the +`useLatestPartialDataSearchResponse()` hook does that for errors emitted +natively by the response `Observable`, it's the responsibility of the +projection function to handle errors that are encoded in the response body, +which includes most server-side errors. Note that errors and partial results in +a response are not mutually exclusive. + +The request status (running, partial, etc), the response +and the errors are turned in to React component state so they can be used in +the usual rendering cycle: + +```typescript +const { + cancelRequest, + isRequestRunning, + isResponsePartial, + latestResponseData, + latestResponseErrors, + loaded, + total, +} = useLatestPartialDataSearchResponse( + requests$, + 'initialValue', + useMemo(() => decodeOrThrow(mySearchStrategyResponsePayloadRT), []), +); +``` + +## Representing the request state to the user + +After the values have been made available to the React rendering process using +the `useLatestPartialDataSearchResponse()` hook, normal component hierarchies +can be used to make the request state and result available to the user. The +following utility components can make that even easier. + +### Undetermined progress + +If `total` and `loaded` are not (yet) known, we can show an undetermined +progress bar. + + + + + +### Known progress + +If `total` and `loaded` are returned by the search strategy, they can be used +to show a progress bar with the option to cancel the request if it takes too +long. + + + + + +### Failed requests + +Assuming the errors are represented as an array of `SearchStrategyError`s in +the `latestResponseErrors` return value, they can be rendered as appropriate +for the respective part of the UI. For many cases a `EuiCallout` is suitable, +so the `DataSearchErrorCallout` can serve as a starting point: + + + + + diff --git a/x-pack/plugins/infra/public/utils/data_search/index.ts b/x-pack/plugins/infra/public/utils/data_search/index.ts new file mode 100644 index 0000000000000..c08ab0727fd90 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/data_search/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; +export * from './use_data_search_request'; +export * from './use_latest_partial_data_search_response'; diff --git a/x-pack/plugins/infra/public/utils/data_search/types.ts b/x-pack/plugins/infra/public/utils/data_search/types.ts new file mode 100644 index 0000000000000..ba0a4c639dae4 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/data_search/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Observable } from 'rxjs'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, +} from '../../../../../../src/plugins/data/public'; +import { SearchStrategyError } from '../../../common/search_strategies/common/errors'; + +export interface DataSearchRequestDescriptor { + request: Request; + options: ISearchOptions; + response$: Observable>; + abortController: AbortController; +} + +export interface NormalizedKibanaSearchResponse { + total?: number; + loaded?: number; + isRunning: boolean; + isPartial: boolean; + data: ResponseData; + errors: SearchStrategyError[]; +} + +export interface DataSearchResponseDescriptor { + request: Request; + options: ISearchOptions; + response: NormalizedKibanaSearchResponse; + abortController: AbortController; +} diff --git a/x-pack/plugins/infra/public/utils/data_search/use_data_search_request.test.tsx b/x-pack/plugins/infra/public/utils/data_search/use_data_search_request.test.tsx new file mode 100644 index 0000000000000..87c091f12ad90 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/data_search/use_data_search_request.test.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { Observable, of, Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { + DataPublicPluginStart, + IKibanaSearchResponse, + ISearchGeneric, + ISearchStart, +} from '../../../../../../src/plugins/data/public'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; +import { createKibanaReactContext } from '../../../../../../src/plugins/kibana_react/public'; +import { PluginKibanaContextValue } from '../../hooks/use_kibana'; +import { useDataSearch } from './use_data_search_request'; + +describe('useDataSearch hook', () => { + it('forwards the search function arguments to the getRequest function', async () => { + const dataMock = createDataPluginMock(); + const { Provider: KibanaContextProvider } = createKibanaReactContext< + Partial + >({ + data: dataMock, + }); + + const getRequest = jest.fn((_firstArgument: string, _secondArgument: string) => null); + + const { result } = renderHook( + () => + useDataSearch({ + getRequest, + }), + { + wrapper: ({ children }) => {children}, + } + ); + + act(() => { + result.current.search('first', 'second'); + }); + + expect(getRequest).toHaveBeenLastCalledWith('first', 'second'); + expect(dataMock.search.search).not.toHaveBeenCalled(); + }); + + it('creates search requests with the given params and options', async () => { + const dataMock = createDataPluginMock(); + const searchResponseMock$ = of({ + rawResponse: { + firstKey: 'firstValue', + }, + }); + dataMock.search.search.mockReturnValue(searchResponseMock$); + const { Provider: KibanaContextProvider } = createKibanaReactContext< + Partial + >({ + data: dataMock, + }); + + const getRequest = jest.fn((firstArgument: string, secondArgument: string) => ({ + request: { + params: { + firstArgument, + secondArgument, + }, + }, + options: { + strategy: 'test-search-strategy', + }, + })); + + const { result } = renderHook( + () => + useDataSearch({ + getRequest, + }), + { + wrapper: ({ children }) => {children}, + } + ); + + // the request execution is lazy + expect(dataMock.search.search).not.toHaveBeenCalled(); + + // execute requests$ observable + const firstRequestPromise = result.current.requests$.pipe(take(1)).toPromise(); + + act(() => { + result.current.search('first', 'second'); + }); + + const firstRequest = await firstRequestPromise; + + expect(dataMock.search.search).toHaveBeenLastCalledWith( + { + params: { firstArgument: 'first', secondArgument: 'second' }, + }, + { + abortSignal: expect.any(Object), + strategy: 'test-search-strategy', + } + ); + expect(firstRequest).toHaveProperty('abortController', expect.any(Object)); + expect(firstRequest).toHaveProperty('request.params', { + firstArgument: 'first', + secondArgument: 'second', + }); + expect(firstRequest).toHaveProperty('options.strategy', 'test-search-strategy'); + expect(firstRequest).toHaveProperty('response$', expect.any(Observable)); + await expect(firstRequest.response$.toPromise()).resolves.toEqual({ + rawResponse: { + firstKey: 'firstValue', + }, + }); + }); + + it('aborts the request when the response observable looses the last subscriber', async () => { + const dataMock = createDataPluginMock(); + const searchResponseMock$ = new Subject(); + dataMock.search.search.mockReturnValue(searchResponseMock$); + const { Provider: KibanaContextProvider } = createKibanaReactContext< + Partial + >({ + data: dataMock, + }); + + const getRequest = jest.fn((firstArgument: string, secondArgument: string) => ({ + request: { + params: { + firstArgument, + secondArgument, + }, + }, + options: { + strategy: 'test-search-strategy', + }, + })); + + const { result } = renderHook( + () => + useDataSearch({ + getRequest, + }), + { + wrapper: ({ children }) => {children}, + } + ); + + // the request execution is lazy + expect(dataMock.search.search).not.toHaveBeenCalled(); + + // execute requests$ observable + const firstRequestPromise = result.current.requests$.pipe(take(1)).toPromise(); + + act(() => { + result.current.search('first', 'second'); + }); + + const firstRequest = await firstRequestPromise; + + // execute requests$ observable + const firstResponseSubscription = firstRequest.response$.subscribe({ + next: jest.fn(), + }); + + // get the abort signal + const [, firstRequestOptions] = dataMock.search.search.mock.calls[0]; + + expect(firstRequestOptions?.abortSignal?.aborted).toBe(false); + + // unsubscribe + firstResponseSubscription.unsubscribe(); + + expect(firstRequestOptions?.abortSignal?.aborted).toBe(true); + }); +}); + +const createDataPluginMock = () => { + const dataMock = dataPluginMock.createStartContract() as DataPublicPluginStart & { + search: ISearchStart & { search: jest.MockedFunction }; + }; + return dataMock; +}; diff --git a/x-pack/plugins/infra/public/utils/data_search/use_data_search_request.ts b/x-pack/plugins/infra/public/utils/data_search/use_data_search_request.ts new file mode 100644 index 0000000000000..a23f06adc0353 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/data_search/use_data_search_request.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback } from 'react'; +import { Subject } from 'rxjs'; +import { map, share, switchMap, tap } from 'rxjs/operators'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, + ISearchOptions, +} from '../../../../../../src/plugins/data/public'; +import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; +import { tapUnsubscribe, useObservable } from '../use_observable'; + +export type DataSearchRequestFactory = ( + ...args: Args +) => + | { + request: Request; + options: ISearchOptions; + } + | null + | undefined; + +export const useDataSearch = < + RequestFactoryArgs extends any[], + Request extends IKibanaSearchRequest, + RawResponse +>({ + getRequest, +}: { + getRequest: DataSearchRequestFactory; +}) => { + const { services } = useKibanaContextForPlugin(); + const request$ = useObservable( + () => new Subject<{ request: Request; options: ISearchOptions }>(), + [] + ); + const requests$ = useObservable( + (inputs$) => + inputs$.pipe( + switchMap(([currentRequest$]) => currentRequest$), + map(({ request, options }) => { + const abortController = new AbortController(); + let isAbortable = true; + + return { + abortController, + request, + options, + response$: services.data.search + .search>(request, { + abortSignal: abortController.signal, + ...options, + }) + .pipe( + // avoid aborting failed or completed requests + tap({ + error: () => { + isAbortable = false; + }, + complete: () => { + isAbortable = false; + }, + }), + tapUnsubscribe(() => { + if (isAbortable) { + abortController.abort(); + } + }), + share() + ), + }; + }) + ), + [request$] + ); + + const search = useCallback( + (...args: RequestFactoryArgs) => { + const request = getRequest(...args); + + if (request) { + request$.next(request); + } + }, + [getRequest, request$] + ); + + return { + requests$, + search, + }; +}; diff --git a/x-pack/plugins/infra/public/utils/data_search/use_latest_partial_data_search_response.test.tsx b/x-pack/plugins/infra/public/utils/data_search/use_latest_partial_data_search_response.test.tsx new file mode 100644 index 0000000000000..4c336aa1107a2 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/data_search/use_latest_partial_data_search_response.test.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { act, renderHook } from '@testing-library/react-hooks'; +import { Observable, of, Subject } from 'rxjs'; +import { + IKibanaSearchRequest, + IKibanaSearchResponse, +} from '../../../../../../src/plugins/data/public'; +import { DataSearchRequestDescriptor } from './types'; +import { useLatestPartialDataSearchResponse } from './use_latest_partial_data_search_response'; + +describe('useLatestPartialDataSearchResponse hook', () => { + it("subscribes to the latest request's response observable", () => { + const firstRequest = { + abortController: new AbortController(), + options: {}, + request: { params: 'firstRequestParam' }, + response$: new Subject>(), + }; + + const secondRequest = { + abortController: new AbortController(), + options: {}, + request: { params: 'secondRequestParam' }, + response$: new Subject>(), + }; + + const requests$ = new Subject< + DataSearchRequestDescriptor, string> + >(); + + const { result } = renderHook(() => + useLatestPartialDataSearchResponse(requests$, 'initial', (response) => ({ + data: `projection of ${response}`, + })) + ); + + expect(result).toHaveProperty('current.isRequestRunning', false); + expect(result).toHaveProperty('current.latestResponseData', undefined); + + // first request is started + act(() => { + requests$.next(firstRequest); + }); + + expect(result).toHaveProperty('current.isRequestRunning', true); + expect(result).toHaveProperty('current.latestResponseData', 'initial'); + + // first response of the first request arrives + act(() => { + firstRequest.response$.next({ rawResponse: 'request-1-response-1', isRunning: true }); + }); + + expect(result).toHaveProperty('current.isRequestRunning', true); + expect(result).toHaveProperty( + 'current.latestResponseData', + 'projection of request-1-response-1' + ); + + // second request is started before the second response of the first request arrives + act(() => { + requests$.next(secondRequest); + secondRequest.response$.next({ rawResponse: 'request-2-response-1', isRunning: true }); + }); + + expect(result).toHaveProperty('current.isRequestRunning', true); + expect(result).toHaveProperty( + 'current.latestResponseData', + 'projection of request-2-response-1' + ); + + // second response of the second request arrives + act(() => { + secondRequest.response$.next({ rawResponse: 'request-2-response-2', isRunning: false }); + }); + + expect(result).toHaveProperty('current.isRequestRunning', false); + expect(result).toHaveProperty( + 'current.latestResponseData', + 'projection of request-2-response-2' + ); + }); + + it("unsubscribes from the latest request's response observable on unmount", () => { + const onUnsubscribe = jest.fn(); + + const firstRequest = { + abortController: new AbortController(), + options: {}, + request: { params: 'firstRequestParam' }, + response$: new Observable>(() => { + return onUnsubscribe; + }), + }; + + const requests$ = of, string>>( + firstRequest + ); + + const { unmount } = renderHook(() => + useLatestPartialDataSearchResponse(requests$, 'initial', (response) => ({ + data: `projection of ${response}`, + })) + ); + + expect(onUnsubscribe).not.toHaveBeenCalled(); + + unmount(); + + expect(onUnsubscribe).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/infra/public/utils/data_search/use_latest_partial_data_search_response.ts b/x-pack/plugins/infra/public/utils/data_search/use_latest_partial_data_search_response.ts new file mode 100644 index 0000000000000..71fd96283d0ef --- /dev/null +++ b/x-pack/plugins/infra/public/utils/data_search/use_latest_partial_data_search_response.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback } from 'react'; +import { Observable, of } from 'rxjs'; +import { catchError, map, startWith, switchMap } from 'rxjs/operators'; +import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/public'; +import { AbortError } from '../../../../../../src/plugins/kibana_utils/public'; +import { SearchStrategyError } from '../../../common/search_strategies/common/errors'; +import { useLatest, useObservable, useObservableState } from '../use_observable'; +import { DataSearchRequestDescriptor, DataSearchResponseDescriptor } from './types'; + +export const useLatestPartialDataSearchResponse = < + Request extends IKibanaSearchRequest, + RawResponse, + Response, + InitialResponse +>( + requests$: Observable>, + initialResponse: InitialResponse, + projectResponse: (rawResponse: RawResponse) => { data: Response; errors?: SearchStrategyError[] } +) => { + const latestInitialResponse = useLatest(initialResponse); + const latestProjectResponse = useLatest(projectResponse); + + const latestResponse$: Observable< + DataSearchResponseDescriptor + > = useObservable( + (inputs$) => + inputs$.pipe( + switchMap(([currentRequests$]) => + currentRequests$.pipe( + switchMap(({ abortController, options, request, response$ }) => + response$.pipe( + map((response) => { + const { data, errors = [] } = latestProjectResponse.current(response.rawResponse); + return { + abortController, + options, + request, + response: { + data, + errors, + isPartial: response.isPartial ?? false, + isRunning: response.isRunning ?? false, + loaded: response.loaded, + total: response.total, + }, + }; + }), + startWith({ + abortController, + options, + request, + response: { + data: latestInitialResponse.current, + errors: [], + isPartial: true, + isRunning: true, + loaded: 0, + total: undefined, + }, + }), + catchError((error) => + of({ + abortController, + options, + request, + response: { + data: latestInitialResponse.current, + errors: [ + error instanceof AbortError + ? { + type: 'aborted' as const, + } + : { + type: 'generic' as const, + message: `${error.message ?? error}`, + }, + ], + isPartial: true, + isRunning: false, + loaded: 0, + total: undefined, + }, + }) + ) + ) + ) + ) + ) + ), + [requests$] as const + ); + + const { latestValue } = useObservableState(latestResponse$, undefined); + + const cancelRequest = useCallback(() => { + latestValue?.abortController.abort(); + }, [latestValue]); + + return { + cancelRequest, + isRequestRunning: latestValue?.response.isRunning ?? false, + isResponsePartial: latestValue?.response.isPartial ?? false, + latestResponseData: latestValue?.response.data, + latestResponseErrors: latestValue?.response.errors, + loaded: latestValue?.response.loaded, + total: latestValue?.response.total, + }; +}; diff --git a/x-pack/plugins/infra/public/utils/use_observable.ts b/x-pack/plugins/infra/public/utils/use_observable.ts new file mode 100644 index 0000000000000..342aa5aa797b1 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/use_observable.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useRef, useState } from 'react'; +import { BehaviorSubject, Observable, PartialObserver, Subscription } from 'rxjs'; + +export const useLatest = (value: Value) => { + const valueRef = useRef(value); + valueRef.current = value; + return valueRef; +}; + +export const useObservable = < + OutputValue, + OutputObservable extends Observable, + InputValues extends Readonly +>( + createObservableOnce: (inputValues: Observable) => OutputObservable, + inputValues: InputValues +) => { + const [inputValues$] = useState(() => new BehaviorSubject(inputValues)); + const [output$] = useState(() => createObservableOnce(inputValues$)); + + useEffect(() => { + inputValues$.next(inputValues); + // `inputValues` can't be statically analyzed + // eslint-disable-next-line react-hooks/exhaustive-deps + }, inputValues); + + return output$; +}; + +export const useObservableState = ( + state$: Observable, + initialState: InitialState | (() => InitialState) +) => { + const [latestValue, setLatestValue] = useState(initialState); + const [latestError, setLatestError] = useState(); + + useSubscription(state$, { + next: setLatestValue, + error: setLatestError, + }); + + return { latestValue, latestError }; +}; + +export const useSubscription = ( + input$: Observable, + { next, error, complete, unsubscribe }: PartialObserver & { unsubscribe?: () => void } +) => { + const latestSubscription = useRef(); + const latestNext = useLatest(next); + const latestError = useLatest(error); + const latestComplete = useLatest(complete); + const latestUnsubscribe = useLatest(unsubscribe); + + useEffect(() => { + const fixedUnsubscribe = latestUnsubscribe.current; + + const subscription = input$.subscribe({ + next: (value) => latestNext.current?.(value), + error: (value) => latestError.current?.(value), + complete: () => latestComplete.current?.(), + }); + + latestSubscription.current = subscription; + + return () => { + subscription.unsubscribe(); + fixedUnsubscribe?.(); + }; + }, [input$, latestNext, latestError, latestComplete, latestUnsubscribe]); + + return latestSubscription.current; +}; + +export const tapUnsubscribe = (onUnsubscribe: () => void) => (source$: Observable) => { + return new Observable((subscriber) => { + const subscription = source$.subscribe({ + next: (value) => subscriber.next(value), + error: (error) => subscriber.error(error), + complete: () => subscriber.complete(), + }); + + return () => { + onUnsubscribe(); + subscription.unsubscribe(); + }; + }); +}; diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts index 044cea3899baf..38626675f5ae7 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -164,6 +164,35 @@ describe('LogEntry search strategy', () => { await expect(response.toPromise()).rejects.toThrowError(ResponseError); }); + + it('forwards cancellation to the underlying search strategy', async () => { + const esSearchStrategyMock = createEsSearchStrategyMock({ + id: 'ASYNC_REQUEST_ID', + isRunning: false, + rawResponse: { + took: 1, + _shards: { total: 1, failed: 0, skipped: 0, successful: 1 }, + timed_out: false, + hits: { total: 0, max_score: 0, hits: [] }, + }, + }); + const dataMock = createDataPluginMock(esSearchStrategyMock); + const sourcesMock = createInfraSourcesMock(); + sourcesMock.getSourceConfiguration.mockResolvedValue(createSourceConfigurationMock()); + const mockDependencies = createSearchStrategyDependenciesMock(); + + const logEntrySearchStrategy = logEntrySearchStrategyProvider({ + data: dataMock, + sources: sourcesMock, + }); + const requestId = logEntrySearchRequestStateRT.encode({ + esRequestId: 'ASYNC_REQUEST_ID', + }); + + await logEntrySearchStrategy.cancel?.(requestId, {}, mockDependencies); + + expect(esSearchStrategyMock.cancel).toHaveBeenCalled(); + }); }); const createSourceConfigurationMock = () => ({ @@ -208,6 +237,7 @@ const createEsSearchStrategyMock = (esSearchResponse: IEsSearchResponse) => ({ return of(esSearchResponse); } }), + cancel: jest.fn().mockResolvedValue(undefined), }); const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => ({ diff --git a/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts b/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts index 880a48fd5b8f7..dac97479d4b04 100644 --- a/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts +++ b/x-pack/plugins/infra/server/services/log_entries/queries/log_entry.ts @@ -17,7 +17,7 @@ export const createGetLogEntryQuery = ( logEntryId: string, timestampField: string, tiebreakerField: string -): RequestParams.Search> => ({ +): RequestParams.AsyncSearchSubmit> => ({ index: logEntryIndex, terminate_after: 1, track_scores: false, From a4caffae2fc865ca3a1904e0c3c82d43ba0b669d Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Fri, 11 Dec 2020 11:38:25 +0100 Subject: [PATCH 06/37] Improve and cleanup chrome helpMenu links (#82300) * Improve and cleanup chrome helpMenu links * update doc due to merge * remove dev dependencies from test plugin * update generated doc after merge * update generated doc * generated doc * generated doc --- ...core-public.chromehelpextensionlinkbase.md | 12 + ...romehelpextensionmenucustomlink.content.md | 13 + ....chromehelpextensionmenucustomlink.href.md | 13 + ...omehelpextensionmenucustomlink.linktype.md | 13 + ...ublic.chromehelpextensionmenucustomlink.md | 16 +- ...chromehelpextensionmenudiscusslink.href.md | 13 + ...mehelpextensionmenudiscusslink.linktype.md | 13 + ...blic.chromehelpextensionmenudiscusslink.md | 15 +- ...helpextensionmenudocumentationlink.href.md | 13 + ...extensionmenudocumentationlink.linktype.md | 13 + ...hromehelpextensionmenudocumentationlink.md | 15 +- ...hromehelpextensionmenugithublink.labels.md | 13 + ...omehelpextensionmenugithublink.linktype.md | 13 + ...ublic.chromehelpextensionmenugithublink.md | 17 +- ...chromehelpextensionmenugithublink.title.md | 13 + ...core-public.chromehelpextensionmenulink.md | 2 +- ...gin-core-public.iexternalurlpolicy.host.md | 2 +- ...a-plugin-core-public.iexternalurlpolicy.md | 4 +- ...core-public.iexternalurlpolicy.protocol.md | 2 +- .../core/public/kibana-plugin-core-public.md | 9 +- ...mbeddable-public.embeddable.getupdated_.md | 2 +- ...in-plugins-embeddable-public.embeddable.md | 2 +- src/core/public/chrome/index.ts | 1 + .../header/__snapshots__/header.test.tsx.snap | 926 ++---------------- src/core/public/chrome/ui/header/header.tsx | 1 + .../chrome/ui/header/header_help_menu.tsx | 377 +++---- src/core/public/chrome/ui/header/index.ts | 1 + src/core/public/chrome/ui/index.ts | 1 + src/core/public/index.ts | 2 + src/core/public/public.api.md | 31 +- src/plugins/data/public/public.api.md | 1 - src/plugins/embeddable/public/public.api.md | 1 - .../plugins/core_plugin_helpmenu/kibana.json | 8 + .../plugins/core_plugin_helpmenu/package.json | 14 + .../public/application.tsx | 63 ++ .../core_plugin_helpmenu/public/index.ts | 24 + .../core_plugin_helpmenu/public/plugin.tsx | 56 ++ .../core_plugin_helpmenu/tsconfig.json | 17 + .../core_plugins/chrome_help_menu_links.ts | 67 ++ .../test_suites/core_plugins/index.ts | 1 + 40 files changed, 734 insertions(+), 1086 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionlinkbase.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.content.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.href.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.linktype.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.href.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.linktype.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.href.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.linktype.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.labels.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.linktype.md create mode 100644 docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.title.md create mode 100644 test/plugin_functional/plugins/core_plugin_helpmenu/kibana.json create mode 100644 test/plugin_functional/plugins/core_plugin_helpmenu/package.json create mode 100644 test/plugin_functional/plugins/core_plugin_helpmenu/public/application.tsx create mode 100644 test/plugin_functional/plugins/core_plugin_helpmenu/public/index.ts create mode 100644 test/plugin_functional/plugins/core_plugin_helpmenu/public/plugin.tsx create mode 100644 test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json create mode 100644 test/plugin_functional/test_suites/core_plugins/chrome_help_menu_links.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionlinkbase.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionlinkbase.md new file mode 100644 index 0000000000000..1faef45c0b2b7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionlinkbase.md @@ -0,0 +1,12 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionLinkBase](./kibana-plugin-core-public.chromehelpextensionlinkbase.md) + +## ChromeHelpExtensionLinkBase type + + +Signature: + +```typescript +export declare type ChromeHelpExtensionLinkBase = Pick; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.content.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.content.md new file mode 100644 index 0000000000000..dc455ca43d24a --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.content.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.md) > [content](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.content.md) + +## ChromeHelpExtensionMenuCustomLink.content property + +Content of the button (in lieu of `children`) + +Signature: + +```typescript +content: React.ReactNode; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.href.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.href.md new file mode 100644 index 0000000000000..feb91acd6d915 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.href.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.md) > [href](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.href.md) + +## ChromeHelpExtensionMenuCustomLink.href property + +URL of the link + +Signature: + +```typescript +href: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.linktype.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.linktype.md new file mode 100644 index 0000000000000..a02b219754042 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.linktype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.md) > [linkType](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.linktype.md) + +## ChromeHelpExtensionMenuCustomLink.linkType property + +Extend EuiButtonEmpty to provide extra functionality + +Signature: + +```typescript +linkType: 'custom'; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.md index 29be9b9539ee0..ff4978e69df62 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenucustomlink.md @@ -2,14 +2,20 @@ [Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.md) -## ChromeHelpExtensionMenuCustomLink type +## ChromeHelpExtensionMenuCustomLink interface Signature: ```typescript -export declare type ChromeHelpExtensionMenuCustomLink = EuiButtonEmptyProps & { - linkType: 'custom'; - content: React.ReactNode; -}; +export interface ChromeHelpExtensionMenuCustomLink extends ChromeHelpExtensionLinkBase ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [content](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.content.md) | React.ReactNode | Content of the button (in lieu of children) | +| [href](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.href.md) | string | URL of the link | +| [linkType](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.linktype.md) | 'custom' | Extend EuiButtonEmpty to provide extra functionality | + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.href.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.href.md new file mode 100644 index 0000000000000..b6714c39a4699 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.href.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md) > [href](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.href.md) + +## ChromeHelpExtensionMenuDiscussLink.href property + +URL to discuss page. i.e. `https://discuss.elastic.co/c/${appName}` + +Signature: + +```typescript +href: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.linktype.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.linktype.md new file mode 100644 index 0000000000000..0141677b26a40 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.linktype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md) > [linkType](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.linktype.md) + +## ChromeHelpExtensionMenuDiscussLink.linkType property + +Creates a generic give feedback link with comment icon + +Signature: + +```typescript +linkType: 'discuss'; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md index 63d0596bd9847..a73f6daad28c2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md @@ -2,14 +2,19 @@ [Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md) -## ChromeHelpExtensionMenuDiscussLink type +## ChromeHelpExtensionMenuDiscussLink interface Signature: ```typescript -export declare type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { - linkType: 'discuss'; - href: string; -}; +export interface ChromeHelpExtensionMenuDiscussLink extends ChromeHelpExtensionLinkBase ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [href](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.href.md) | string | URL to discuss page. i.e. https://discuss.elastic.co/c/${appName} | +| [linkType](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.linktype.md) | 'discuss' | Creates a generic give feedback link with comment icon | + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.href.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.href.md new file mode 100644 index 0000000000000..9897bc6fcd2f7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.href.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md) > [href](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.href.md) + +## ChromeHelpExtensionMenuDocumentationLink.href property + +URL to documentation page. i.e. `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/${appName}.html`, + +Signature: + +```typescript +href: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.linktype.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.linktype.md new file mode 100644 index 0000000000000..b75a70f9518b3 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.linktype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md) > [linkType](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.linktype.md) + +## ChromeHelpExtensionMenuDocumentationLink.linkType property + +Creates a deep-link to app-specific documentation + +Signature: + +```typescript +linkType: 'documentation'; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md index c7c1c4153edf8..fab49d06d4774 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md @@ -2,14 +2,19 @@ [Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md) -## ChromeHelpExtensionMenuDocumentationLink type +## ChromeHelpExtensionMenuDocumentationLink interface Signature: ```typescript -export declare type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { - linkType: 'documentation'; - href: string; -}; +export interface ChromeHelpExtensionMenuDocumentationLink extends ChromeHelpExtensionLinkBase ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [href](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.href.md) | string | URL to documentation page. i.e. ${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/${appName}.html, | +| [linkType](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.linktype.md) | 'documentation' | Creates a deep-link to app-specific documentation | + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.labels.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.labels.md new file mode 100644 index 0000000000000..1976215e7243c --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.labels.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-core-public.chromehelpextensionmenugithublink.md) > [labels](./kibana-plugin-core-public.chromehelpextensionmenugithublink.labels.md) + +## ChromeHelpExtensionMenuGitHubLink.labels property + +Include at least one app-specific label to be applied to the new github issue + +Signature: + +```typescript +labels: string[]; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.linktype.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.linktype.md new file mode 100644 index 0000000000000..b3df27213e5b7 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.linktype.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-core-public.chromehelpextensionmenugithublink.md) > [linkType](./kibana-plugin-core-public.chromehelpextensionmenugithublink.linktype.md) + +## ChromeHelpExtensionMenuGitHubLink.linkType property + +Creates a link to a new github issue in the Kibana repo + +Signature: + +```typescript +linkType: 'github'; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.md index 5cb3a79086e11..ca9ceecffa6f1 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.md @@ -2,15 +2,20 @@ [Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-core-public.chromehelpextensionmenugithublink.md) -## ChromeHelpExtensionMenuGitHubLink type +## ChromeHelpExtensionMenuGitHubLink interface Signature: ```typescript -export declare type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { - linkType: 'github'; - labels: string[]; - title?: string; -}; +export interface ChromeHelpExtensionMenuGitHubLink extends ChromeHelpExtensionLinkBase ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [labels](./kibana-plugin-core-public.chromehelpextensionmenugithublink.labels.md) | string[] | Include at least one app-specific label to be applied to the new github issue | +| [linkType](./kibana-plugin-core-public.chromehelpextensionmenugithublink.linktype.md) | 'github' | Creates a link to a new github issue in the Kibana repo | +| [title](./kibana-plugin-core-public.chromehelpextensionmenugithublink.title.md) | string | Provides initial text for the title of the issue | + diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.title.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.title.md new file mode 100644 index 0000000000000..af6091f9e7252 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenugithublink.title.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-core-public.chromehelpextensionmenugithublink.md) > [title](./kibana-plugin-core-public.chromehelpextensionmenugithublink.title.md) + +## ChromeHelpExtensionMenuGitHubLink.title property + +Provides initial text for the title of the issue + +Signature: + +```typescript +title?: string; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenulink.md b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenulink.md index 7a219d5bfd2f8..cb7d795e3eb8e 100644 --- a/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenulink.md +++ b/docs/development/core/public/kibana-plugin-core-public.chromehelpextensionmenulink.md @@ -8,5 +8,5 @@ Signature: ```typescript -export declare type ChromeHelpExtensionMenuLink = ExclusiveUnion>>; +export declare type ChromeHelpExtensionMenuLink = ChromeHelpExtensionMenuGitHubLink | ChromeHelpExtensionMenuDiscussLink | ChromeHelpExtensionMenuDocumentationLink | ChromeHelpExtensionMenuCustomLink; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md index 5551d52cc1226..842d86db45d73 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.host.md @@ -4,7 +4,7 @@ ## IExternalUrlPolicy.host property -Optional host describing the external destination. May be combined with `protocol`. Required if `protocol` is not defined. +Optional host describing the external destination. May be combined with `protocol`. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md index a87dc69d79e23..3a1e571460974 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.md @@ -17,6 +17,6 @@ export interface IExternalUrlPolicy | Property | Type | Description | | --- | --- | --- | | [allow](./kibana-plugin-core-public.iexternalurlpolicy.allow.md) | boolean | Indicates if this policy allows or denies access to the described destination. | -| [host](./kibana-plugin-core-public.iexternalurlpolicy.host.md) | string | Optional host describing the external destination. May be combined with protocol. Required if protocol is not defined. | -| [protocol](./kibana-plugin-core-public.iexternalurlpolicy.protocol.md) | string | Optional protocol describing the external destination. May be combined with host. Required if host is not defined. | +| [host](./kibana-plugin-core-public.iexternalurlpolicy.host.md) | string | Optional host describing the external destination. May be combined with protocol. | +| [protocol](./kibana-plugin-core-public.iexternalurlpolicy.protocol.md) | string | Optional protocol describing the external destination. May be combined with host. | diff --git a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md index 67b9b439a54f6..ac73412b6e143 100644 --- a/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md +++ b/docs/development/core/public/kibana-plugin-core-public.iexternalurlpolicy.protocol.md @@ -4,7 +4,7 @@ ## IExternalUrlPolicy.protocol property -Optional protocol describing the external destination. May be combined with `host`. Required if `host` is not defined. +Optional protocol describing the external destination. May be combined with `host`. Signature: diff --git a/docs/development/core/public/kibana-plugin-core-public.md b/docs/development/core/public/kibana-plugin-core-public.md index a3df5d30137df..da19377054499 100644 --- a/docs/development/core/public/kibana-plugin-core-public.md +++ b/docs/development/core/public/kibana-plugin-core-public.md @@ -44,6 +44,10 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ChromeBrand](./kibana-plugin-core-public.chromebrand.md) | | | [ChromeDocTitle](./kibana-plugin-core-public.chromedoctitle.md) | APIs for accessing and updating the document title. | | [ChromeHelpExtension](./kibana-plugin-core-public.chromehelpextension.md) | | +| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.md) | | +| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md) | | +| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md) | | +| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-core-public.chromehelpextensionmenugithublink.md) | | | [ChromeNavControl](./kibana-plugin-core-public.chromenavcontrol.md) | | | [ChromeNavControls](./kibana-plugin-core-public.chromenavcontrols.md) | [APIs](./kibana-plugin-core-public.chromenavcontrols.md) for registering new controls to be displayed in the navigation bar. | | [ChromeNavLink](./kibana-plugin-core-public.chromenavlink.md) | | @@ -145,10 +149,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AppUpdatableFields](./kibana-plugin-core-public.appupdatablefields.md) | Defines the list of fields that can be updated via an [AppUpdater](./kibana-plugin-core-public.appupdater.md). | | [AppUpdater](./kibana-plugin-core-public.appupdater.md) | Updater for applications. see [ApplicationSetup](./kibana-plugin-core-public.applicationsetup.md) | | [ChromeBreadcrumb](./kibana-plugin-core-public.chromebreadcrumb.md) | | -| [ChromeHelpExtensionMenuCustomLink](./kibana-plugin-core-public.chromehelpextensionmenucustomlink.md) | | -| [ChromeHelpExtensionMenuDiscussLink](./kibana-plugin-core-public.chromehelpextensionmenudiscusslink.md) | | -| [ChromeHelpExtensionMenuDocumentationLink](./kibana-plugin-core-public.chromehelpextensionmenudocumentationlink.md) | | -| [ChromeHelpExtensionMenuGitHubLink](./kibana-plugin-core-public.chromehelpextensionmenugithublink.md) | | +| [ChromeHelpExtensionLinkBase](./kibana-plugin-core-public.chromehelpextensionlinkbase.md) | | | [ChromeHelpExtensionMenuLink](./kibana-plugin-core-public.chromehelpextensionmenulink.md) | | | [ChromeNavLinkUpdateableFields](./kibana-plugin-core-public.chromenavlinkupdateablefields.md) | | | [FatalErrorsStart](./kibana-plugin-core-public.fatalerrorsstart.md) | FatalErrors stop the Kibana Public Core and displays a fatal error screen with details about the Kibana build and the error. | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md index 474962e614aa7..5201444e69867 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md @@ -4,7 +4,7 @@ ## Embeddable.getUpdated$() method -Merges input$ and output$ streams and denounces emit till next macro-task Could be useful to batch reactions to input$ and output$ updates that happen separately but synchronously In case corresponding state change triggered `reload` this stream is guarantied to emit later which allows to skip any state handling in case `reload` already handled it +Merges input$ and output$ streams and debounces emit till next macro-task. Could be useful to batch reactions to input$ and output$ updates that happen separately but synchronously. In case corresponding state change triggered `reload` this stream is guarantied to emit later, which allows to skip any state handling in case `reload` already handled it. Signature: diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md index 4541afec29fa5..fe64bcf7c1177 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddable.md @@ -44,7 +44,7 @@ export declare abstract class Embeddablereload this stream is guarantied to emit later which allows to skip any state handling in case reload already handled it | +| [getUpdated$()](./kibana-plugin-plugins-embeddable-public.embeddable.getupdated_.md) | | Merges input$ and output$ streams and debounces emit till next macro-task. Could be useful to batch reactions to input$ and output$ updates that happen separately but synchronously. In case corresponding state change triggered reload this stream is guarantied to emit later, which allows to skip any state handling in case reload already handled it. | | [onFatalError(e)](./kibana-plugin-plugins-embeddable-public.embeddable.onfatalerror.md) | | | | [reload()](./kibana-plugin-plugins-embeddable-public.embeddable.reload.md) | | Reload will be called when there is a request to refresh the data or view, even if the input data did not change.In case if input data did change and reload is requested input$ and output$ would still emit before reload is calledThe order would be as follows: input$ output$ reload() \-\-\-- updated$ | | [render(el)](./kibana-plugin-plugins-embeddable-public.embeddable.render.md) | | | diff --git a/src/core/public/chrome/index.ts b/src/core/public/chrome/index.ts index cc1e0851f5944..6483ede0564c1 100644 --- a/src/core/public/chrome/index.ts +++ b/src/core/public/chrome/index.ts @@ -27,6 +27,7 @@ export { ChromeHelpExtension, } from './chrome_service'; export { + ChromeHelpExtensionLinkBase, ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, ChromeHelpExtensionMenuDiscussLink, diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 5ce5a5f635d64..46ed76e72db02 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -2041,7 +2041,7 @@ exports[`Header renders 1`] = ` } /> , - , - - + + } - kibanaDocLink="/docs" - kibanaVersion="1.0.0" - useDefaultContent={true} + closePopover={[Function]} + data-test-subj="helpMenuButton" + display="inlineBlock" + hasArrow={true} + id="headerHelpMenu" + isOpen={false} + ownFocus={false} + panelPaddingSize="m" + repositionOnScroll={true} > - - - - } - closePopover={[Function]} - data-test-subj="helpMenuButton" - display="inlineBlock" - hasArrow={true} - id="headerHelpMenu" - isOpen={false} - ownFocus={false} - panelPaddingSize="m" - repositionOnScroll={true} + -
-
- - - -
+ /> + + +
-
-
-
-
+ + + + , , ], diff --git a/src/core/public/chrome/ui/header/header_help_menu.tsx b/src/core/public/chrome/ui/header/header_help_menu.tsx index c7dd8c7e8bb1c..ad8767cefaba3 100644 --- a/src/core/public/chrome/ui/header/header_help_menu.tsx +++ b/src/core/public/chrome/ui/header/header_help_menu.tsx @@ -17,10 +17,10 @@ * under the License. */ -import * as Rx from 'rxjs'; import React, { Component, Fragment } from 'react'; +import { combineLatest, Observable, Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; -import { InjectedIntl, injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiButtonEmptyProps, @@ -35,14 +35,20 @@ import { EuiHorizontalRule, } from '@elastic/eui'; -import { ExclusiveUnion } from '@elastic/eui'; -import { combineLatest } from 'rxjs'; -import { HeaderExtension } from './header_extension'; -import { ChromeHelpExtension } from '../../chrome_service'; +import { InternalApplicationStart } from '../../../application'; import { GITHUB_CREATE_ISSUE_LINK, KIBANA_FEEDBACK_LINK } from '../../constants'; +import { ChromeHelpExtension } from '../../chrome_service'; +import { HeaderExtension } from './header_extension'; +import { isModifiedOrPrevented } from './nav_link'; + +/** @public */ +export type ChromeHelpExtensionLinkBase = Pick< + EuiButtonEmptyProps, + 'iconType' | 'target' | 'rel' | 'data-test-subj' +>; /** @public */ -export type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { +export interface ChromeHelpExtensionMenuGitHubLink extends ChromeHelpExtensionLinkBase { /** * Creates a link to a new github issue in the Kibana repo */ @@ -55,10 +61,10 @@ export type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { * Provides initial text for the title of the issue */ title?: string; -}; +} /** @public */ -export type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { +export interface ChromeHelpExtensionMenuDiscussLink extends ChromeHelpExtensionLinkBase { /** * Creates a generic give feedback link with comment icon */ @@ -68,10 +74,10 @@ export type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { * i.e. `https://discuss.elastic.co/c/${appName}` */ href: string; -}; +} /** @public */ -export type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { +export interface ChromeHelpExtensionMenuDocumentationLink extends ChromeHelpExtensionLinkBase { /** * Creates a deep-link to app-specific documentation */ @@ -81,35 +87,36 @@ export type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { * i.e. `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/${appName}.html`, */ href: string; -}; +} /** @public */ -export type ChromeHelpExtensionMenuCustomLink = EuiButtonEmptyProps & { +export interface ChromeHelpExtensionMenuCustomLink extends ChromeHelpExtensionLinkBase { /** * Extend EuiButtonEmpty to provide extra functionality */ linkType: 'custom'; + /** + * URL of the link + */ + href: string; /** * Content of the button (in lieu of `children`) */ content: React.ReactNode; -}; +} /** @public */ -export type ChromeHelpExtensionMenuLink = ExclusiveUnion< - ChromeHelpExtensionMenuGitHubLink, - ExclusiveUnion< - ChromeHelpExtensionMenuDiscussLink, - ExclusiveUnion - > ->; +export type ChromeHelpExtensionMenuLink = + | ChromeHelpExtensionMenuGitHubLink + | ChromeHelpExtensionMenuDiscussLink + | ChromeHelpExtensionMenuDocumentationLink + | ChromeHelpExtensionMenuCustomLink; interface Props { - helpExtension$: Rx.Observable; - helpSupportUrl$: Rx.Observable; - intl: InjectedIntl; + navigateToUrl: InternalApplicationStart['navigateToUrl']; + helpExtension$: Observable; + helpSupportUrl$: Observable; kibanaVersion: string; - useDefaultContent?: boolean; kibanaDocLink: string; } @@ -119,8 +126,8 @@ interface State { helpSupportUrl: string; } -class HeaderHelpMenuUI extends Component { - private subscription?: Rx.Subscription; +export class HeaderHelpMenu extends Component { + private subscription?: Subscription; constructor(props: Props) { super(props); @@ -151,41 +158,69 @@ class HeaderHelpMenuUI extends Component { } } - createGithubUrl = (labels: string[], title?: string) => { - const url = new URL('https://github.com/elastic/kibana/issues/new?'); - - if (labels.length) { - url.searchParams.set('labels', labels.join(',')); - } + public render() { + const { kibanaVersion } = this.props; - if (title) { - url.searchParams.set('title', title); - } + const defaultContent = this.renderDefaultContent(); + const customContent = this.renderCustomContent(); - return url.toString(); - }; + const button = ( + + + + ); - createCustomLink = ( - index: number, - text: React.ReactNode, - addSpacer?: boolean, - buttonProps?: EuiButtonEmptyProps - ) => { return ( - - - {text} - - {addSpacer && } - + + + + +

+ +

+
+ + + +
+
+ +
+ {defaultContent} + {defaultContent && customContent && } + {customContent} +
+
); - }; + } - public render() { - const { intl, kibanaVersion, useDefaultContent, kibanaDocLink } = this.props; - const { helpExtension, helpSupportUrl } = this.state; + private renderDefaultContent() { + const { kibanaDocLink } = this.props; + const { helpSupportUrl } = this.state; - const defaultContent = useDefaultContent ? ( + return ( { /> - ) : null; - - let customContent; - if (helpExtension) { - const { appName, links, content } = helpExtension; - - const getFeedbackText = () => - i18n.translate('core.ui.chrome.headerGlobalNav.helpMenuGiveFeedbackOnApp', { - defaultMessage: 'Give feedback on {appName}', - values: { appName: helpExtension.appName }, - }); - - const customLinks = - links && - links.map((link, index) => { - const { linkType, title, labels = [], content: text, ...rest } = link; - switch (linkType) { - case 'documentation': - return this.createCustomLink( - index, - , - index < links.length - 1, - { - target: '_blank', - rel: 'noopener', - ...rest, - } - ); - case 'github': - return this.createCustomLink(index, getFeedbackText(), index < links.length - 1, { - iconType: 'logoGithub', - href: this.createGithubUrl(labels, title), - target: '_blank', - rel: 'noopener', - ...rest, - }); - case 'discuss': - return this.createCustomLink(index, getFeedbackText(), index < links.length - 1, { - iconType: 'editorComment', + ); + } + + private renderCustomContent() { + const { helpExtension } = this.state; + if (!helpExtension) { + return null; + } + const { navigateToUrl } = this.props; + const { appName, links, content } = helpExtension; + + const getFeedbackText = () => + i18n.translate('core.ui.chrome.headerGlobalNav.helpMenuGiveFeedbackOnApp', { + defaultMessage: 'Give feedback on {appName}', + values: { appName: helpExtension.appName }, + }); + + const customLinks = + links && + links.map((link, index) => { + const addSpacer = index < links.length - 1; + switch (link.linkType) { + case 'documentation': { + const { linkType, ...rest } = link; + return createCustomLink( + index, + , + addSpacer, + { target: '_blank', rel: 'noopener', ...rest, - }); - case 'custom': - return this.createCustomLink(index, text, index < links.length - 1, { ...rest }); - default: - break; + } + ); } - }); - - customContent = ( - <> - -

{appName}

-
- - {customLinks} - {content && ( - <> - {customLinks && } - - - )} - - ); - } - - const button = ( - - - - ); + case 'github': { + const { linkType, labels, title, ...rest } = link; + return createCustomLink(index, getFeedbackText(), addSpacer, { + iconType: 'logoGithub', + href: createGithubUrl(labels, title), + target: '_blank', + rel: 'noopener', + ...rest, + }); + } + case 'discuss': { + const { linkType, ...rest } = link; + return createCustomLink(index, getFeedbackText(), addSpacer, { + iconType: 'editorComment', + target: '_blank', + rel: 'noopener', + ...rest, + }); + } + case 'custom': { + const { linkType, content: text, href, ...rest } = link; + return createCustomLink(index, text, addSpacer, { + href, + onClick: this.createOnClickHandler(href, navigateToUrl), + ...rest, + }); + } + default: + break; + } + }); return ( - - - - -

- -

-
- - - -
-
- -
- {defaultContent} - {defaultContent && customContent && } - {customContent} -
-
+ <> + +

{appName}

+
+ + {customLinks} + {content && ( + <> + {customLinks && } + + + )} + ); } @@ -361,10 +360,44 @@ class HeaderHelpMenuUI extends Component { isOpen: false, }); }; + + private createOnClickHandler(href: string, navigate: Props['navigateToUrl']) { + return (event: React.MouseEvent) => { + if (!isModifiedOrPrevented(event) && event.button === 0) { + event.preventDefault(); + this.closeMenu(); + navigate(href); + } + }; + } } -export const HeaderHelpMenu = injectI18n(HeaderHelpMenuUI); +const createGithubUrl = (labels: string[], title?: string) => { + const url = new URL('https://github.com/elastic/kibana/issues/new?'); + + if (labels.length) { + url.searchParams.set('labels', labels.join(',')); + } + + if (title) { + url.searchParams.set('title', title); + } + + return url.toString(); +}; -HeaderHelpMenu.defaultProps = { - useDefaultContent: true, +const createCustomLink = ( + index: number, + text: React.ReactNode, + addSpacer?: boolean, + buttonProps?: EuiButtonEmptyProps +) => { + return ( + + + {text} + + {addSpacer && } + + ); }; diff --git a/src/core/public/chrome/ui/header/index.ts b/src/core/public/chrome/ui/header/index.ts index a492273a65ba8..d75cd10af7bed 100644 --- a/src/core/public/chrome/ui/header/index.ts +++ b/src/core/public/chrome/ui/header/index.ts @@ -20,6 +20,7 @@ export { Header, HeaderProps } from './header'; export { OnIsLockedUpdate, NavType } from './types'; export { + ChromeHelpExtensionLinkBase, ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, ChromeHelpExtensionMenuDiscussLink, diff --git a/src/core/public/chrome/ui/index.ts b/src/core/public/chrome/ui/index.ts index 4f6ad90cb96a3..4894aaac7c914 100644 --- a/src/core/public/chrome/ui/index.ts +++ b/src/core/public/chrome/ui/index.ts @@ -20,6 +20,7 @@ export { LoadingIndicator } from './loading_indicator'; export { Header, + ChromeHelpExtensionLinkBase, ChromeHelpExtensionMenuLink, ChromeHelpExtensionMenuCustomLink, ChromeHelpExtensionMenuDiscussLink, diff --git a/src/core/public/index.ts b/src/core/public/index.ts index 8e240bfe91d48..2e1238df350e0 100644 --- a/src/core/public/index.ts +++ b/src/core/public/index.ts @@ -43,6 +43,7 @@ import { ChromeBreadcrumb, ChromeHelpExtension, ChromeHelpExtensionMenuLink, + ChromeHelpExtensionLinkBase, ChromeHelpExtensionMenuCustomLink, ChromeHelpExtensionMenuDiscussLink, ChromeHelpExtensionMenuDocumentationLink, @@ -300,6 +301,7 @@ export { ChromeBreadcrumb, ChromeHelpExtension, ChromeHelpExtensionMenuLink, + ChromeHelpExtensionLinkBase, ChromeHelpExtensionMenuCustomLink, ChromeHelpExtensionMenuDiscussLink, ChromeHelpExtensionMenuDocumentationLink, diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 65912e0954261..f48fb9092d56d 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -14,7 +14,6 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiFlyoutSize } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; -import { ExclusiveUnion } from '@elastic/eui'; import { History } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; @@ -250,32 +249,36 @@ export interface ChromeHelpExtension { } // @public (undocumented) -export type ChromeHelpExtensionMenuCustomLink = EuiButtonEmptyProps & { - linkType: 'custom'; +export type ChromeHelpExtensionLinkBase = Pick; + +// @public (undocumented) +export interface ChromeHelpExtensionMenuCustomLink extends ChromeHelpExtensionLinkBase { content: React.ReactNode; -}; + href: string; + linkType: 'custom'; +} // @public (undocumented) -export type ChromeHelpExtensionMenuDiscussLink = EuiButtonEmptyProps & { - linkType: 'discuss'; +export interface ChromeHelpExtensionMenuDiscussLink extends ChromeHelpExtensionLinkBase { href: string; -}; + linkType: 'discuss'; +} // @public (undocumented) -export type ChromeHelpExtensionMenuDocumentationLink = EuiButtonEmptyProps & { - linkType: 'documentation'; +export interface ChromeHelpExtensionMenuDocumentationLink extends ChromeHelpExtensionLinkBase { href: string; -}; + linkType: 'documentation'; +} // @public (undocumented) -export type ChromeHelpExtensionMenuGitHubLink = EuiButtonEmptyProps & { - linkType: 'github'; +export interface ChromeHelpExtensionMenuGitHubLink extends ChromeHelpExtensionLinkBase { labels: string[]; + linkType: 'github'; title?: string; -}; +} // @public (undocumented) -export type ChromeHelpExtensionMenuLink = ExclusiveUnion>>; +export type ChromeHelpExtensionMenuLink = ChromeHelpExtensionMenuGitHubLink | ChromeHelpExtensionMenuDiscussLink | ChromeHelpExtensionMenuDocumentationLink | ChromeHelpExtensionMenuCustomLink; // @public (undocumented) export interface ChromeNavControl { diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 179d6c35b3ab6..35546c33aaa80 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -31,7 +31,6 @@ import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiFlyoutSize } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { EventEmitter } from 'events'; -import { ExclusiveUnion } from '@elastic/eui'; import { ExecutionContext } from 'src/plugins/expressions/common'; import { ExpressionAstExpression } from 'src/plugins/expressions/common'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 8f7ea6f51c785..e42eaaf86bdf3 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -31,7 +31,6 @@ import { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiFlyoutSize } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; import { EventEmitter } from 'events'; -import { ExclusiveUnion } from '@elastic/eui'; import { ExpressionAstExpression } from 'src/plugins/expressions/common'; import { History } from 'history'; import { Href } from 'history'; diff --git a/test/plugin_functional/plugins/core_plugin_helpmenu/kibana.json b/test/plugin_functional/plugins/core_plugin_helpmenu/kibana.json new file mode 100644 index 0000000000000..984b96a8bcba1 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_helpmenu/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "core_plugin_helpmenu", + "version": "0.0.1", + "kibanaVersion": "kibana", + "configPath": ["core_plugin_helpmenu"], + "server": false, + "ui": true +} diff --git a/test/plugin_functional/plugins/core_plugin_helpmenu/package.json b/test/plugin_functional/plugins/core_plugin_helpmenu/package.json new file mode 100644 index 0000000000000..bfb203d6a4d86 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_helpmenu/package.json @@ -0,0 +1,14 @@ +{ + "name": "core_plugin_helpmenu", + "version": "1.0.0", + "main": "target/test/plugin_functional/plugins/core_plugin_helpmenu", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "license": "Apache-2.0", + "scripts": { + "kbn": "node ../../../../scripts/kbn.js", + "build": "rm -rf './target' && tsc" + } +} diff --git a/test/plugin_functional/plugins/core_plugin_helpmenu/public/application.tsx b/test/plugin_functional/plugins/core_plugin_helpmenu/public/application.tsx new file mode 100644 index 0000000000000..d0b024f90c737 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_helpmenu/public/application.tsx @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { + EuiPage, + EuiPageBody, + EuiPageContent, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, +} from '@elastic/eui'; + +import { AppMountParameters } from 'kibana/public'; + +const App = ({ appName }: { appName: string }) => ( + + + + + +

Welcome to {appName}!

+
+
+
+ + + + +

{appName} home page section title

+
+
+
+ {appName} page content +
+
+
+); + +export const renderApp = (appName: string, { element }: AppMountParameters) => { + render(, element); + return () => unmountComponentAtNode(element); +}; diff --git a/test/plugin_functional/plugins/core_plugin_helpmenu/public/index.ts b/test/plugin_functional/plugins/core_plugin_helpmenu/public/index.ts new file mode 100644 index 0000000000000..eb04224532c5a --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_helpmenu/public/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from 'kibana/public'; +import { CoreHelpMenuPlugin, CoreHelpMenuPluginSetup, CoreHelpMenuPluginStart } from './plugin'; + +export const plugin: PluginInitializer = () => + new CoreHelpMenuPlugin(); diff --git a/test/plugin_functional/plugins/core_plugin_helpmenu/public/plugin.tsx b/test/plugin_functional/plugins/core_plugin_helpmenu/public/plugin.tsx new file mode 100644 index 0000000000000..db22038e602c8 --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_helpmenu/public/plugin.tsx @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Plugin, CoreSetup } from 'kibana/public'; + +export class CoreHelpMenuPlugin + implements Plugin { + public setup(core: CoreSetup, deps: {}) { + core.application.register({ + id: 'core_help_menu', + title: 'Help Menu Test App', + async mount(context, params) { + const [{ chrome, http }] = await core.getStartServices(); + + chrome.setHelpExtension({ + appName: 'HelpMenuTestApp', + links: [ + { + linkType: 'custom', + href: http.basePath.prepend('/app/management'), + content: 'Go to management', + 'data-test-subj': 'coreHelpMenuInternalLinkTest', + }, + ], + }); + + const { renderApp } = await import('./application'); + return renderApp('Help Menu Test App', params); + }, + }); + + return {}; + } + + public start() {} + public stop() {} +} + +export type CoreHelpMenuPluginSetup = ReturnType; +export type CoreHelpMenuPluginStart = ReturnType; diff --git a/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json b/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json new file mode 100644 index 0000000000000..f9b0443e0a8bf --- /dev/null +++ b/test/plugin_functional/plugins/core_plugin_helpmenu/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target", + "skipLibCheck": true + }, + "include": [ + "index.ts", + "public/**/*.ts", + "public/**/*.tsx", + "../../../../typings/**/*" + ], + "exclude": [], + "references": [ + { "path": "../../../../src/core/tsconfig.json" } + ] +} diff --git a/test/plugin_functional/test_suites/core_plugins/chrome_help_menu_links.ts b/test/plugin_functional/test_suites/core_plugins/chrome_help_menu_links.ts new file mode 100644 index 0000000000000..d82a15a0854ea --- /dev/null +++ b/test/plugin_functional/test_suites/core_plugins/chrome_help_menu_links.ts @@ -0,0 +1,67 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import url from 'url'; +import expect from '@kbn/expect'; +import { PluginFunctionalProviderContext } from '../../services'; + +declare global { + interface Window { + _nonReloadedFlag?: boolean; + } +} + +const getPathWithHash = (absoluteUrl: string) => { + const parsed = url.parse(absoluteUrl); + return `${parsed.path}${parsed.hash ?? ''}`; +}; + +export default function ({ getService, getPageObjects }: PluginFunctionalProviderContext) { + const PageObjects = getPageObjects(['common']); + const browser = getService('browser'); + const testSubjects = getService('testSubjects'); + + const setNonReloadedFlag = () => { + return browser.executeAsync(async (cb) => { + window._nonReloadedFlag = true; + cb(); + }); + }; + const wasReloaded = () => { + return browser.executeAsync(async (cb) => { + const reloaded = window._nonReloadedFlag !== true; + cb(reloaded); + }); + }; + + describe('chrome helpMenu links', () => { + beforeEach(async () => { + await PageObjects.common.navigateToApp('core_help_menu'); + await setNonReloadedFlag(); + }); + + it('navigates to internal custom links without performing a full page refresh', async () => { + await testSubjects.click('helpMenuButton'); + await testSubjects.click('coreHelpMenuInternalLinkTest'); + + expect(getPathWithHash(await browser.getCurrentUrl())).to.eql('/app/management'); + expect(await wasReloaded()).to.eql(false); + }); + }); +} diff --git a/test/plugin_functional/test_suites/core_plugins/index.ts b/test/plugin_functional/test_suites/core_plugins/index.ts index 3d7cc751175c6..e53323d622d58 100644 --- a/test/plugin_functional/test_suites/core_plugins/index.ts +++ b/test/plugin_functional/test_suites/core_plugins/index.ts @@ -29,5 +29,6 @@ export default function ({ loadTestFile }: PluginFunctionalProviderContext) { loadTestFile(require.resolve('./application_leave_confirm')); loadTestFile(require.resolve('./application_status')); loadTestFile(require.resolve('./rendering')); + loadTestFile(require.resolve('./chrome_help_menu_links')); }); } From 31a1cc0541ffeaae88746e7a8557bb27f6199694 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Fri, 11 Dec 2020 14:00:38 -0500 Subject: [PATCH 07/37] [App Search] Add a Result Component (#85046) --- .../documents/document_detail.test.tsx | 2 +- .../components/documents/document_detail.tsx | 2 +- .../documents/document_detail_logic.test.ts | 3 +- .../search_experience/search_experience.scss | 4 + .../search_experience_content.test.tsx | 11 +- .../search_experience_content.tsx | 14 +- .../views/result_view.test.tsx | 14 +- .../search_experience/views/result_view.tsx | 32 +--- .../app_search/components/documents/types.ts | 4 +- .../app_search/components/library/index.ts | 7 + .../app_search/components/library/library.tsx | 178 +++++++++++++++++ .../{result_field_value => result}/index.ts | 0 .../app_search/components/result/result.scss | 30 +++ .../components/result/result.test.tsx | 179 ++++++++++++++++++ .../app_search/components/result/result.tsx | 90 +++++++++ .../components/result/result_field.scss | 25 +++ .../components/result/result_field.test.tsx | 25 +++ .../components/result/result_field.tsx | 30 +++ .../result_field_value.scss | 1 + .../result_field_value.test.tsx | 0 .../result_field_value.tsx | 4 +- .../components/result/result_header.scss | 28 +++ .../components/result/result_header.test.tsx | 72 +++++++ .../components/result/result_header.tsx | 48 +++++ .../components/result/result_header_item.scss | 16 ++ .../result/result_header_item.test.tsx | 60 ++++++ .../components/result/result_header_item.tsx | 44 +++++ .../app_search/components/result/types.ts | 35 ++++ .../public/applications/app_search/index.tsx | 7 + .../public/applications/app_search/routes.ts | 1 + .../public/applications/app_search/types.ts | 2 - .../public/applications/shared/types.ts | 3 +- 32 files changed, 923 insertions(+), 48 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/library/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{result_field_value => result}/index.ts (100%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{result_field_value => result}/result_field_value.scss (97%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{result_field_value => result}/result_field_value.test.tsx (100%) rename x-pack/plugins/enterprise_search/public/applications/app_search/components/{result_field_value => result}/result_field_value.tsx (96%) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx index a289ec78d9e80..dca06988478ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx @@ -14,7 +14,7 @@ import { EuiPageContent, EuiBasicTable } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; import { DocumentDetail } from '.'; -import { ResultFieldValue } from '../result_field_value'; +import { ResultFieldValue } from '../result'; describe('DocumentDetail', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx index 017f5a2f67ad0..1be7e6c53d343 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx @@ -23,7 +23,7 @@ import { i18n } from '@kbn/i18n'; import { Loading } from '../../../shared/loading'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; -import { ResultFieldValue } from '../result_field_value'; +import { ResultFieldValue } from '../result'; import { DocumentDetailLogic } from './document_detail_logic'; import { FieldDetails } from './types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index 4afa3f7aee5c8..d9a3de7c078cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -28,6 +28,7 @@ jest.mock('../../../shared/flash_messages', () => ({ import { setQueuedSuccessMessage, flashAPIErrors } from '../../../shared/flash_messages'; import { DocumentDetailLogic } from './document_detail_logic'; +import { InternalSchemaTypes } from '../../../shared/types'; describe('DocumentDetailLogic', () => { const DEFAULT_VALUES = { @@ -61,7 +62,7 @@ describe('DocumentDetailLogic', () => { describe('actions', () => { describe('setFields', () => { it('should set fields to the provided value and dataLoading to false', () => { - const fields = [{ name: 'foo', value: ['foo'], type: 'string' }]; + const fields = [{ name: 'foo', value: ['foo'], type: 'string' as InternalSchemaTypes }]; mount({ dataLoading: true, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss index cbc72dbffe57a..868a561a27873 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.scss @@ -2,6 +2,10 @@ .sui-results-container { flex-grow: 1; padding: 0; + + > li + li { + margin-top: $euiSize; + } } .documentsSearchExperience__sidebar { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index 22a63f653a294..455e237848a4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -43,6 +43,15 @@ describe('SearchExperienceContent', () => { it('passes engineName to the result view', () => { const props = { result: { + id: { + raw: '1', + }, + _meta: { + id: '1', + scopedId: '1', + score: 100, + engine: 'my-engine', + }, foo: { raw: 'bar', }, @@ -51,7 +60,7 @@ describe('SearchExperienceContent', () => { const wrapper = shallow(); const resultView: any = wrapper.find(Results).prop('resultView'); - expect(resultView(props)).toEqual(); + expect(resultView(props)).toEqual(); }); it('renders pagination', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx index 938c8930f4dd1..9194a3a1db5e4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx @@ -14,24 +14,18 @@ import { useValues } from 'kea'; import { ResultView } from './views'; import { Pagination } from './pagination'; +import { Props as ResultViewProps } from './views/result_view'; import { useSearchContextState } from './hooks'; import { DocumentCreationButton } from '../document_creation_button'; import { AppLogic } from '../../../app_logic'; import { EngineLogic } from '../../engine'; import { DOCS_PREFIX } from '../../../routes'; -// TODO This is temporary until we create real Result type -interface Result { - [key: string]: { - raw: string | string[] | number | number[] | undefined; - }; -} - export const SearchExperienceContent: React.FC = () => { const { resultSearchTerm, totalResults, wasSearched } = useSearchContextState(); const { myRole } = useValues(AppLogic); - const { engineName, isMetaEngine } = useValues(EngineLogic); + const { isMetaEngine } = useValues(EngineLogic); if (!wasSearched) return null; @@ -49,8 +43,8 @@ export const SearchExperienceContent: React.FC = () => { { - return ; + resultView={(props: ResultViewProps) => { + return ; }} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx index 73ddf16e01074..049a3ad1bed66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx @@ -9,16 +9,26 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ResultView } from '.'; +import { Result } from '../../../result/result'; describe('ResultView', () => { const result = { id: { raw: '1', }, + title: { + raw: 'A title', + }, + _meta: { + id: '1', + scopedId: '1', + score: 100, + engine: 'my-engine', + }, }; it('renders', () => { - const wrapper = shallow(); - expect(wrapper.find('div').length).toBe(1); + const wrapper = shallow(); + expect(wrapper.find(Result).exists()).toBe(true); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx index bf472ec3bb21e..52b845a1aee2d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx @@ -5,38 +5,18 @@ */ import React from 'react'; -import { EuiPanel, EuiSpacer } from '@elastic/eui'; -import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; +import { Result as ResultType } from '../../../result/types'; +import { Result } from '../../../result/result'; -// TODO replace this with a real result type when we implement a more sophisticated -// ResultView -interface Result { - [key: string]: { - raw: string | string[] | number | number[] | undefined; - }; +export interface Props { + result: ResultType; } -interface Props { - engineName: string; - result: Result; -} - -export const ResultView: React.FC = ({ engineName, result }) => { - // TODO Replace this entire component when we migrate StuiResult +export const ResultView: React.FC = ({ result }) => { return (
  • - - - {result.id.raw} - - {Object.entries(result).map(([key, value]) => ( -
    - {key}: {value.raw} -
    - ))} -
    - +
  • ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts index 6a7c1cd1d5d2f..adad8ff4e4683 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/types.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { InternalSchemaTypes } from '../../../shared/types'; + export interface FieldDetails { name: string; value: string | string[]; - type: string; + type: InternalSchemaTypes; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/index.ts new file mode 100644 index 0000000000000..04e5b1b6a401d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { Library } from './library'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx new file mode 100644 index 0000000000000..66c0cc165fc05 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiSpacer, + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiPageContentBody, + EuiPageContent, +} from '@elastic/eui'; +import React from 'react'; + +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Result } from '../result/result'; + +export const Library: React.FC = () => { + const props = { + result: { + id: { + raw: '1', + }, + _meta: { + id: '1', + scopedId: '1', + score: 100, + engine: 'my-engine', + }, + title: { + raw: 'A title', + }, + description: { + raw: 'A description', + }, + states: { + raw: ['Pennsylvania', 'Ohio'], + }, + visitors: { + raw: 1000, + }, + size: { + raw: 200, + }, + length: { + raw: 100, + }, + }, + }; + + return ( + <> + + + + +

    Library

    +
    +
    +
    + + + +

    Result

    +
    + + + +

    5 or more fields

    +
    + + + + + +

    5 or less fields

    +
    + + + + + +

    With just an id

    +
    + + + + + +

    With an id and a score

    +
    + + + + + +

    With an id and a score and an engine

    +
    + + + + + +

    With a long id, a long engine name, a long field key, and a long value

    +
    + + + +
    +
    + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/index.ts similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/index.ts rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/result/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss new file mode 100644 index 0000000000000..ed8ce512a2eb8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.scss @@ -0,0 +1,30 @@ +.appSearchResult { + display: flex; + + &__content { + width: 100%; + padding: $euiSize; + overflow: hidden; + } + + &__hiddenFieldsIndicator { + font-size: $euiFontSizeXS; + color: $euiColorDarkShade; + margin-top: $euiSizeS; + } + + &__actionButton { + display: flex; + justify-content: center; + align-items: center; + width: $euiSizeL * 2; + border-left: $euiBorderThin; + + &:hover, + &:focus, + &:active { + background-color: $euiPageBackgroundColor; + cursor: pointer; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx new file mode 100644 index 0000000000000..ade26551039fa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; +import { EuiPanel } from '@elastic/eui'; + +import { ResultField } from './result_field'; +import { ResultHeader } from './result_header'; +import { Result } from './result'; + +describe('Result', () => { + const props = { + result: { + id: { + raw: '1', + }, + title: { + raw: 'A title', + }, + description: { + raw: 'A description', + }, + length: { + raw: 100, + }, + _meta: { + id: '1', + scopedId: '1', + score: 100, + engine: 'my-engine', + }, + }, + }; + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiPanel).exists()).toBe(true); + }); + + it('should render a ResultField for each field except id and _meta', () => { + const wrapper = shallow(); + expect(wrapper.find(ResultField).map((rf) => rf.prop('field'))).toEqual([ + 'title', + 'description', + 'length', + ]); + }); + + it('passes through showScore and resultMeta to ResultHeader', () => { + const wrapper = shallow(); + expect(wrapper.find(ResultHeader).prop('showScore')).toBe(true); + expect(wrapper.find(ResultHeader).prop('resultMeta')).toEqual({ + id: '1', + scopedId: '1', + score: 100, + engine: 'my-engine', + }); + }); + + describe('when there are more than 5 fields', () => { + const propsWithMoreFields = { + result: { + id: { + raw: '1', + }, + title: { + raw: 'A title', + }, + description: { + raw: 'A description', + }, + length: { + raw: 100, + }, + states: { + raw: ['Pennsylvania', 'Ohio'], + }, + visitors: { + raw: 1000, + }, + size: { + raw: 200, + }, + _meta: { + id: '1', + scopedId: '1', + score: 100, + engine: 'my-engine', + }, + }, + }; + + describe('the initial render', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + }); + + it('renders a collapse button', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); + }); + + it('does not render an expand button', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); + }); + + it('renders a hidden fields indicator', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').text()).toEqual( + '1 more fields' + ); + }); + + it('shows no more than 5 fields', () => { + expect(wrapper.find(ResultField).length).toEqual(5); + }); + }); + + describe('after clicking the expand button', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + expect(wrapper.find('.appSearchResult__actionButton').exists()).toBe(true); + wrapper.find('.appSearchResult__actionButton').simulate('click'); + }); + + it('renders a collapse button', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(true); + }); + + it('does not render an expand button', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(false); + }); + + it('does not render a hidden fields indicator', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').exists()).toBe(false); + }); + + it('shows all fields', () => { + expect(wrapper.find(ResultField).length).toEqual(6); + }); + }); + + describe('after clicking the collapse button', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + expect(wrapper.find('.appSearchResult__actionButton').exists()).toBe(true); + wrapper.find('.appSearchResult__actionButton').simulate('click'); + wrapper.find('.appSearchResult__actionButton').simulate('click'); + }); + + it('renders a collapse button', () => { + expect(wrapper.find('[data-test-subj="CollapseResult"]').exists()).toBe(false); + }); + + it('does not render an expand button', () => { + expect(wrapper.find('[data-test-subj="ExpandResult"]').exists()).toBe(true); + }); + + it('renders a hidden fields indicator', () => { + expect(wrapper.find('.appSearchResult__hiddenFieldsIndicator').text()).toEqual( + '1 more fields' + ); + }); + + it('shows no more than 5 fields', () => { + expect(wrapper.find(ResultField).length).toEqual(5); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx new file mode 100644 index 0000000000000..4f343e64b12ae --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useMemo } from 'react'; + +import './result.scss'; + +import { EuiPanel, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { FieldValue, Result as ResultType } from './types'; +import { ResultField } from './result_field'; +import { ResultHeader } from './result_header'; + +interface Props { + result: ResultType; + showScore?: boolean; +} + +const RESULT_CUTOFF = 5; + +export const Result: React.FC = ({ result, showScore }) => { + const [isOpen, setIsOpen] = useState(false); + + const ID = 'id'; + const META = '_meta'; + const resultMeta = result[META]; + const resultFields = useMemo( + () => Object.entries(result).filter(([key]) => key !== META && key !== ID), + [result] + ); + const numResults = resultFields.length; + + return ( + +
    + +
    + {resultFields + .slice(0, isOpen ? resultFields.length : RESULT_CUTOFF) + .map(([field, value]: [string, FieldValue]) => ( + + ))} +
    + {numResults > RESULT_CUTOFF && !isOpen && ( +
    + {i18n.translate('xpack.enterpriseSearch.appSearch.result.numberOfAdditionalFields', { + defaultMessage: '{numberOfAdditionalFields} more fields', + values: { + numberOfAdditionalFields: numResults - RESULT_CUTOFF, + }, + })} +
    + )} +
    + {numResults > RESULT_CUTOFF && ( + + )} +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.scss new file mode 100644 index 0000000000000..15509b98d465b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.scss @@ -0,0 +1,25 @@ +.appSearchResultField { + font-size: $euiFontSizeS; + line-height: $euiLineHeight; + display: grid; + grid-template-columns: 0.85fr $euiSizeXL 1fr; + grid-gap: $euiSizeXS; + + &__separator { + text-align: center; + + &:after { + content: '=>'; + color: $euiColorDarkShade; + } + } + + &__value { + padding-left: $euiSize; + overflow: hidden; + + @include euiBreakpoint('xs') { + padding-left: 0; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx new file mode 100644 index 0000000000000..921e2324d3918 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { ResultField } from './result_field'; + +describe('ResultField', () => { + it('renders', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('ResultFieldValue').exists()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx new file mode 100644 index 0000000000000..bc6329aa4fa4a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ResultFieldValue } from '.'; +import { FieldType, Raw, Snippet } from './types'; + +import './result_field.scss'; + +interface Props { + field: string; + raw?: Raw; + snippet?: Snippet; + type?: FieldType; +} + +export const ResultField: React.FC = ({ field, raw, snippet, type }) => { + return ( +
    +
    {field}
    +
    +
    + +
    +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.scss similarity index 97% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.scss rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.scss index 13a771d24adc9..996124a725aab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.scss +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.scss @@ -11,6 +11,7 @@ font-family: $euiCodeFontFamily; } + &--geolocation, &--location { color: $euiColorSuccessText; font-family: $euiCodeFontFamily; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx similarity index 100% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.test.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.tsx similarity index 96% rename from x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.tsx rename to x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.tsx index 9ee0f1e0ba043..8e4b264017c0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_field_value/result_field_value.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.tsx @@ -8,7 +8,7 @@ import React from 'react'; import classNames from 'classnames'; -import { Raw, Snippet } from '../../types'; +import { FieldType, Raw, Snippet } from './types'; import './result_field_value.scss'; @@ -40,7 +40,7 @@ const isFieldValueEmpty = (type?: string, raw?: Raw, snippet?: Snippet) => { interface Props { raw?: Raw; snippet?: Snippet; - type?: string; + type?: FieldType; className?: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss new file mode 100644 index 0000000000000..73372d7c4aca0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.scss @@ -0,0 +1,28 @@ +.appSearchResultHeader { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: $euiSizeS; + + @include euiBreakpoint('xs') { + flex-direction: column; + } + + &__column { + display: flex; + flex-wrap: wrap; + + @include euiBreakpoint('xs') { + flex-direction: column; + } + + & + &, + .appSearchResultHeaderItem + .appSearchResultHeaderItem { + margin-left: $euiSizeL; + + @include euiBreakpoint('xs') { + margin-left: 0; + } + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx new file mode 100644 index 0000000000000..95b77a0aed7bb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { ResultHeader } from './result_header'; + +describe('ResultHeader', () => { + const resultMeta = { + id: '1', + scopedId: '1', + score: 100, + engine: 'my-engine', + }; + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.isEmptyRender()).toBe(false); + }); + + it('always renders an id', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="ResultId"]').prop('value')).toEqual('1'); + }); + + describe('score', () => { + it('renders score if showScore is true ', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="ResultScore"]').prop('value')).toEqual(100); + }); + + it('does not render score if showScore is false', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="ResultScore"]').exists()).toBe(false); + }); + }); + + describe('engine', () => { + it('renders engine name if the ids dont match, which means it is a meta engine', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="ResultEngine"]').prop('value')).toBe('my-engine'); + }); + + it('does not render an engine name if the ids match, which means it is not a meta engine', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="ResultEngine"]').exists()).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx new file mode 100644 index 0000000000000..9b83014d041dd --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { ResultHeaderItem } from './result_header_item'; +import { ResultMeta } from './types'; + +import './result_header.scss'; + +interface Props { + showScore: boolean; + resultMeta: ResultMeta; +} + +export const ResultHeader: React.FC = ({ showScore, resultMeta }) => { + const showEngineLabel: boolean = resultMeta.id !== resultMeta.scopedId; + + return ( +
    + {showScore && ( +
    + +
    + )} + +
    + {showEngineLabel && ( + + )} + +
    +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss new file mode 100644 index 0000000000000..f1e9343530af3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.scss @@ -0,0 +1,16 @@ +.appSearchResultHeaderItem { + display: flex; + + &__key, + &__value { + line-height: $euiLineHeight; + font-size: $euiFontSizeXS; + } + + &__key { + text-transform: uppercase; + font-weight: $euiFontWeightLight; + color: $euiColorDarkShade; + margin-right: $euiSizeXS; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx new file mode 100644 index 0000000000000..b4368f83b1833 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { mount } from 'enzyme'; + +import { ResultHeaderItem } from './result_header_item'; + +describe('ResultHeaderItem', () => { + it('renders', () => { + const wrapper = mount(); + expect(wrapper.find('.appSearchResultHeaderItem__key').text()).toEqual('id'); + expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual('001'); + }); + + it('will truncate long field names', () => { + const wrapper = mount( + + ); + expect(wrapper.find('.appSearchResultHeaderItem__key').text()).toEqual( + 'a-really-really-really-really-…' + ); + }); + + it('will truncate long values', () => { + const wrapper = mount( + + ); + expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual( + 'a-really-really-really-really-…' + ); + }); + + it('will truncate long values from the beginning if the type is "id"', () => { + const wrapper = mount( + + ); + expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual( + '…lly-really-really-really-value' + ); + }); + + it('will round any numeric values that are passed in to 2 decimals, regardless of the explicit "type" passed', () => { + const wrapper = mount(); + expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual('5.19'); + }); + + it('if the value passed in is undefined, it will render "-"', () => { + const wrapper = mount(); + expect(wrapper.find('.appSearchResultHeaderItem__value').text()).toEqual('-'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx new file mode 100644 index 0000000000000..d67b3f86b0aa7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import './result_header_item.scss'; + +import { TruncatedContent } from '../../../shared/truncate'; + +interface Props { + field: string; + value?: string | number; + type: 'id' | 'score' | 'string'; +} + +const MAX_CHARACTER_LENGTH = 30; + +export const ResultHeaderItem: React.FC = ({ field, type, value }) => { + let formattedValue = '-'; + if (typeof value === 'string') { + formattedValue = value; + } else if (typeof value === 'number') { + formattedValue = parseFloat((value as number).toFixed(2)).toString(); + } + + return ( +
    +
    + +
    +
    + +
    +
    + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts new file mode 100644 index 0000000000000..7db710eeb213a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InternalSchemaTypes, SchemaTypes } from '../../../shared/types'; + +export type FieldType = InternalSchemaTypes | SchemaTypes; + +export type Raw = string | string[] | number | number[]; +export type Snippet = string; +export interface FieldValue { + raw?: Raw; + snippet?: Snippet; +} + +export interface ResultMeta { + id: string; + scopedId: string; + score?: number; + engine: string; +} + +// A search result item +export type Result = { + id: { + raw: string; + }; + _meta: ResultMeta; +} & { + // this should be a FieldType object, but there's no good way to do that in TS: https://github.com/microsoft/TypeScript/issues/17867 + // You'll need to cast it to FieldValue whenever you use it. + [key: string]: object; +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 743cf63fb4bc3..769230ccffd22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -26,6 +26,7 @@ import { ROLE_MAPPINGS_PATH, ENGINES_PATH, ENGINE_PATH, + LIBRARY_PATH, } from './routes'; import { SetupGuide } from './components/setup_guide'; @@ -35,6 +36,7 @@ import { EnginesOverview, ENGINES_TITLE } from './components/engines'; import { Settings, SETTINGS_TITLE } from './components/settings'; import { Credentials, CREDENTIALS_TITLE } from './components/credentials'; import { ROLE_MAPPINGS_TITLE } from './components/role_mappings'; +import { Library } from './components/library'; export const AppSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); @@ -66,6 +68,11 @@ export const AppSearchConfigured: React.FC = (props) => { + {process.env.NODE_ENV === 'development' && ( + + + + )} } />} readOnlyMode={readOnlyMode}> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index a7220b89e4410..ade3c9a917410 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -12,6 +12,7 @@ export const DOCS_PREFIX = `https://www.elastic.co/guide/en/app-search/${CURRENT export const ROOT_PATH = '/'; export const SETUP_GUIDE_PATH = '/setup_guide'; +export const LIBRARY_PATH = '/library'; export const SETTINGS_PATH = '/settings/account'; export const CREDENTIALS_PATH = '/credentials'; export const ROLE_MAPPINGS_PATH = '#/role-mappings'; // This page seems to 404 if the # isn't included diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts index 9af1ff0293fae..7c22a45757a93 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts @@ -7,5 +7,3 @@ export * from '../../../common/types/app_search'; export { Role, RoleTypes, AbilityTypes } from './utils/role'; export { Engine } from './components/engine/types'; -export type Raw = string | string[] | number | number[]; -export type Snippet = string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index c1737142e482e..f5833a0ac9f8e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -7,7 +7,8 @@ import { ADD, UPDATE } from './constants/operations'; export type SchemaTypes = 'text' | 'number' | 'geolocation' | 'date'; - +// Certain API endpoints will use these internal type names, which map to the external names above +export type InternalSchemaTypes = 'string' | 'float' | 'location' | 'date'; export interface Schema { [key: string]: SchemaTypes; } From d7d9e06f3758f98f5ed4406733997f32d4fabd07 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 11 Dec 2020 23:47:01 +0000 Subject: [PATCH 08/37] skip flaky suite (#79389) --- .../cypress/integration/timeline_creation.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts index 831fa8fbbf9fa..4009ac13ab120 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_creation.spec.ts @@ -45,7 +45,8 @@ import { openTimeline } from '../tasks/timelines'; import { OVERVIEW_URL } from '../urls/navigation'; -describe('Timelines', () => { +// FLAKY: https://github.com/elastic/kibana/issues/79389 +describe.skip('Timelines', () => { let timelineId: string; after(() => { From 13d9753bef062a78d8a93afe4ee8503a4419c4a1 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 11 Dec 2020 23:51:39 +0000 Subject: [PATCH 09/37] skip flaky suite (#61612) --- .../security_solution/cypress/integration/url_state.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts index f85e6f683cba5..b911fc5b81f98 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_state.spec.ts @@ -48,7 +48,8 @@ const ABSOLUTE_DATE = { startTimeTimeline: '2019-08-02T20:03:29.186Z', }; -describe('url state', () => { +// FLAKY: https://github.com/elastic/kibana/issues/61612 +describe.skip('url state', () => { it('sets the global start and end dates from the url', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); cy.get(DATE_PICKER_START_DATE_POPOVER_BUTTON).should( From 63cafd7cbfa9bb955613cfa563b47a14998a89af Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Fri, 11 Dec 2020 23:56:35 +0000 Subject: [PATCH 10/37] skip flaky suite (#85671) --- .../integration/alerts_detection_rules_override.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index ab0a53f4cd014..519f69d0d26d0 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -83,7 +83,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; -describe('Detection rules, override', () => { +// FLAKY: https://github.com/elastic/kibana/issues/85671 +describe.skip('Detection rules, override', () => { const expectedUrls = newOverrideRule.referenceUrls.join(''); const expectedFalsePositives = newOverrideRule.falsePositivesExamples.join(''); const expectedTags = newOverrideRule.tags.join(''); From a1f5668577180ade411625e5931da5445866becb Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 12 Dec 2020 00:00:25 +0000 Subject: [PATCH 11/37] skip flaky suite (#84020) --- .../cypress/integration/alerts_detection_rules_override.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts index 519f69d0d26d0..4d013ee7d7232 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_detection_rules_override.spec.ts @@ -84,6 +84,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; import { DETECTIONS_URL } from '../urls/navigation'; // FLAKY: https://github.com/elastic/kibana/issues/85671 +// FLAKY: https://github.com/elastic/kibana/issues/84020 describe.skip('Detection rules, override', () => { const expectedUrls = newOverrideRule.referenceUrls.join(''); const expectedFalsePositives = newOverrideRule.falsePositivesExamples.join(''); From d135b426af5851b668938b3b11edcf3a4bf88f05 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 12 Dec 2020 00:10:44 +0000 Subject: [PATCH 12/37] skip flaky suite (#85098) --- .../cypress/integration/timeline_data_providers.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts index 288e2178d71ae..b1ec797d4cbfe 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts @@ -24,7 +24,8 @@ import { closeTimeline, createNewTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; -describe('timeline data providers', () => { +// FLAKY: https://github.com/elastic/kibana/issues/85098 +describe.skip('timeline data providers', () => { before(() => { loginAndWaitForPage(HOSTS_URL); waitForAllHostsToBeLoaded(); From f28a80fd29031c111bf32921009b3d24010413c1 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Sat, 12 Dec 2020 00:15:58 +0000 Subject: [PATCH 13/37] skip flaky suite (#62060) --- .../cypress/integration/timeline_data_providers.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts index b1ec797d4cbfe..abf6ca38844e2 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timeline_data_providers.spec.ts @@ -25,6 +25,7 @@ import { closeTimeline, createNewTimeline } from '../tasks/timeline'; import { HOSTS_URL } from '../urls/navigation'; // FLAKY: https://github.com/elastic/kibana/issues/85098 +// FLAKY: https://github.com/elastic/kibana/issues/62060 describe.skip('timeline data providers', () => { before(() => { loginAndWaitForPage(HOSTS_URL); From 7b328352268bcb8a738da8819a77e59a8ab6af18 Mon Sep 17 00:00:00 2001 From: Angela Chuang <6295984+angorayc@users.noreply.github.com> Date: Sat, 12 Dec 2020 08:24:32 +0000 Subject: [PATCH 14/37] [Security Solution] Alerts details (#83963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * init alert details tab * styles * readMore button * readmore btn * field mappings * add unit tests * unit test * fix unit test * functional test * isolate lineClamp component * review * unit test * fix rule name in events table * originalvalue * unit test * add close event details button * rollback cypress configs * cypress * close events details * remove Ip * review * review * review * review * review * review * review * fix i18n check * fix import * fix eslint * use connect * close flyout when expanded event doesn't exist in the list * Update x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx Co-authored-by: Patryk Kopyciński * fix types * unit test * fix rule status badge * isolate host name renderer * fixup * cypress * cypress * defaultModel * review comments * unit test * replace findIndex with some * review * remove defaultModel from toggle event action * review * cleanup defaultModel * unit test * rollback handleClearSelection * fixup * fix i18n * cleanup defaultmodel * cleanup * summary value * fix showing timeline details * layout * fix timeline memoization * fix long query * styling Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Patryk Kopyciński --- .../timeline/events/details/index.ts | 1 + .../common/types/timeline/index.ts | 1 - .../integration/alerts_timeline.spec.ts | 13 +- .../cases/components/case_view/index.tsx | 1 - .../event_details/__mocks__/index.ts | 657 ++++++++++++++++++ .../__snapshots__/event_details.test.tsx.snap | 21 + .../__snapshots__/json_view.test.tsx.snap | 3 + .../components/event_details/columns.tsx | 3 + .../event_details/event_details.test.tsx | 53 +- .../event_details/event_details.tsx | 37 +- .../event_details/event_fields_browser.tsx | 28 +- .../event_details/json_view.test.tsx | 3 + .../event_details/summary_view.test.tsx | 78 +++ .../components/event_details/summary_view.tsx | 207 ++++++ .../components/event_details/translations.ts | 11 + .../events_viewer/event_details_flyout.tsx | 39 +- .../events_viewer/events_viewer.test.tsx | 2 + .../events_viewer/events_viewer.tsx | 15 +- .../common/components/events_viewer/index.tsx | 12 +- .../common/components/line_clamp/index.tsx | 77 ++ .../components/line_clamp/translations.ts | 15 + .../public/common/mock/mock_detail_item.ts | 5 + .../public/network/components/ip/index.tsx | 2 +- .../fields_browser/field_items.test.tsx | 60 ++ .../components/open_timeline/translations.ts | 7 - .../row_renderers_browser/index.tsx | 18 +- .../timeline/body/actions/index.tsx | 20 +- .../body/events/event_column_view.tsx | 4 +- .../timeline/body/events/stateful_event.tsx | 9 +- .../timeline/body/renderers/constants.tsx | 1 + .../body/renderers/formatted_field.tsx | 33 +- .../renderers/formatted_field_helpers.tsx | 9 + .../timeline/body/renderers/host_name.tsx | 42 ++ .../timeline/body/renderers/rule_status.tsx | 46 ++ .../components/timeline/body/translations.ts | 12 +- .../components/timeline/event_details.tsx | 37 +- .../timeline/expandable_event/index.tsx | 79 ++- .../expandable_event/translations.tsx | 23 +- .../__snapshots__/index.test.tsx.snap | 2 + .../timeline/query_tab_content/index.test.tsx | 2 + .../timeline/query_tab_content/index.tsx | 47 +- .../public/timelines/containers/index.tsx | 42 +- .../timelines/store/timeline/actions.ts | 4 +- .../timeline/epic_local_storage.test.tsx | 2 + .../timelines/store/timeline/reducer.ts | 2 +- .../timelines/store/timeline/selectors.ts | 2 - .../timeline/factory/events/details/index.ts | 2 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 49 files changed, 1617 insertions(+), 176 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/line_clamp/translations.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts index 9fa7f96599deb..0346f3bb9439b 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/details/index.ts @@ -9,6 +9,7 @@ import { Inspect, Maybe } from '../../../common'; import { TimelineRequestOptionsPaginated } from '../..'; export interface TimelineEventsDetailsItem { + category?: string; field: string; values?: Maybe; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index e447a004fb51c..aa114ff074898 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -411,7 +411,6 @@ export type TimelineEventsType = 'all' | 'raw' | 'alert' | 'signal' | 'custom'; export interface TimelineExpandedEventType { eventId: string; indexName: string; - loading: boolean; } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts index 1cece57c2fea5..2bfe72033135b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/alerts_timeline.spec.ts @@ -4,14 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ALERT_ID } from '../screens/alerts'; import { PROVIDER_BADGE } from '../screens/timeline'; -import { - expandFirstAlert, - investigateFirstAlertInTimeline, - waitForAlertsPanelToBeLoaded, -} from '../tasks/alerts'; +import { investigateFirstAlertInTimeline, waitForAlertsPanelToBeLoaded } from '../tasks/alerts'; import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; import { loginAndWaitForPage } from '../tasks/login'; @@ -29,13 +24,13 @@ describe('Alerts timeline', () => { it('Investigate alert in default timeline', () => { waitForAlertsPanelToBeLoaded(); - expandFirstAlert(); - cy.get(ALERT_ID) + investigateFirstAlertInTimeline(); + cy.get(PROVIDER_BADGE) .first() .invoke('text') .then((eventId) => { investigateFirstAlertInTimeline(); - cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', `_id: "${eventId}"`); + cy.get(PROVIDER_BADGE).filter(':visible').should('have.text', eventId); }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index e5a673b03449f..0e6226f69fce7 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -352,7 +352,6 @@ export const CaseComponent = React.memo( event: { eventId: alertId, indexName: index, - loading: false, }, }) ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts new file mode 100644 index 0000000000000..b4561e6d5bffd --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__mocks__/index.ts @@ -0,0 +1,657 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const mockAlertDetailsData = [ + { category: 'process', field: 'process.name', values: ['-'], originalValue: '-' }, + { category: 'process', field: 'process.pid', values: [0], originalValue: 0 }, + { category: 'process', field: 'process.executable', values: ['-'], originalValue: '-' }, + { + category: 'agent', + field: 'agent.hostname', + values: ['windows-native'], + originalValue: 'windows-native', + }, + { + category: 'agent', + field: 'agent.name', + values: ['windows-native'], + originalValue: 'windows-native', + }, + { + category: 'agent', + field: 'agent.id', + values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], + originalValue: 'abfe4a35-d5b4-42a0-a539-bd054c791769', + }, + { category: 'agent', field: 'agent.type', values: ['winlogbeat'], originalValue: 'winlogbeat' }, + { + category: 'agent', + field: 'agent.ephemeral_id', + values: ['b9850845-c000-4ddd-bd51-9978a07b7e7d'], + originalValue: 'b9850845-c000-4ddd-bd51-9978a07b7e7d', + }, + { category: 'agent', field: 'agent.version', values: ['7.10.0'], originalValue: '7.10.0' }, + { + category: 'winlog', + field: 'winlog.computer_name', + values: ['windows-native'], + originalValue: 'windows-native', + }, + { category: 'winlog', field: 'winlog.process.pid', values: [624], originalValue: 624 }, + { category: 'winlog', field: 'winlog.process.thread.id', values: [1896], originalValue: 1896 }, + { + category: 'winlog', + field: 'winlog.keywords', + values: ['Audit Failure'], + originalValue: ['Audit Failure'], + }, + { + category: 'winlog', + field: 'winlog.logon.failure.reason', + values: ['Unknown user name or bad password.'], + originalValue: 'Unknown user name or bad password.', + }, + { + category: 'winlog', + field: 'winlog.logon.failure.sub_status', + values: ['User logon with misspelled or bad password'], + originalValue: 'User logon with misspelled or bad password', + }, + { + category: 'winlog', + field: 'winlog.logon.failure.status', + values: ['This is either due to a bad username or authentication information'], + originalValue: 'This is either due to a bad username or authentication information', + }, + { category: 'winlog', field: 'winlog.logon.id', values: ['0x0'], originalValue: '0x0' }, + { category: 'winlog', field: 'winlog.logon.type', values: ['Network'], originalValue: 'Network' }, + { category: 'winlog', field: 'winlog.channel', values: ['Security'], originalValue: 'Security' }, + { + category: 'winlog', + field: 'winlog.event_data.Status', + values: ['0xc000006d'], + originalValue: '0xc000006d', + }, + { category: 'winlog', field: 'winlog.event_data.LogonType', values: ['3'], originalValue: '3' }, + { + category: 'winlog', + field: 'winlog.event_data.SubjectLogonId', + values: ['0x0'], + originalValue: '0x0', + }, + { + category: 'winlog', + field: 'winlog.event_data.TransmittedServices', + values: ['-'], + originalValue: '-', + }, + { + category: 'winlog', + field: 'winlog.event_data.LmPackageName', + values: ['-'], + originalValue: '-', + }, + { category: 'winlog', field: 'winlog.event_data.KeyLength', values: ['0'], originalValue: '0' }, + { + category: 'winlog', + field: 'winlog.event_data.SubjectUserName', + values: ['-'], + originalValue: '-', + }, + { + category: 'winlog', + field: 'winlog.event_data.FailureReason', + values: ['%%2313'], + originalValue: '%%2313', + }, + { + category: 'winlog', + field: 'winlog.event_data.SubjectDomainName', + values: ['-'], + originalValue: '-', + }, + { + category: 'winlog', + field: 'winlog.event_data.TargetUserName', + values: ['administrator'], + originalValue: 'administrator', + }, + { + category: 'winlog', + field: 'winlog.event_data.SubStatus', + values: ['0xc000006a'], + originalValue: '0xc000006a', + }, + { + category: 'winlog', + field: 'winlog.event_data.LogonProcessName', + values: ['NtLmSsp '], + originalValue: 'NtLmSsp ', + }, + { + category: 'winlog', + field: 'winlog.event_data.SubjectUserSid', + values: ['S-1-0-0'], + originalValue: 'S-1-0-0', + }, + { + category: 'winlog', + field: 'winlog.event_data.AuthenticationPackageName', + values: ['NTLM'], + originalValue: 'NTLM', + }, + { + category: 'winlog', + field: 'winlog.event_data.TargetUserSid', + values: ['S-1-0-0'], + originalValue: 'S-1-0-0', + }, + { category: 'winlog', field: 'winlog.opcode', values: ['Info'], originalValue: 'Info' }, + { category: 'winlog', field: 'winlog.record_id', values: [890770], originalValue: 890770 }, + { category: 'winlog', field: 'winlog.task', values: ['Logon'], originalValue: 'Logon' }, + { category: 'winlog', field: 'winlog.event_id', values: [4625], originalValue: 4625 }, + { + category: 'winlog', + field: 'winlog.provider_guid', + values: ['{54849625-5478-4994-a5ba-3e3b0328c30d}'], + originalValue: '{54849625-5478-4994-a5ba-3e3b0328c30d}', + }, + { + category: 'winlog', + field: 'winlog.activity_id', + values: ['{e148a943-f9c4-0001-5a39-81b88bbed601}'], + originalValue: '{e148a943-f9c4-0001-5a39-81b88bbed601}', + }, + { + category: 'winlog', + field: 'winlog.api', + values: ['wineventlog'], + originalValue: 'wineventlog', + }, + { + category: 'winlog', + field: 'winlog.provider_name', + values: ['Microsoft-Windows-Security-Auditing'], + originalValue: 'Microsoft-Windows-Security-Auditing', + }, + { category: 'log', field: 'log.level', values: ['information'], originalValue: 'information' }, + { category: 'source', field: 'source.port', values: [0], originalValue: 0 }, + { category: 'source', field: 'source.domain', values: ['-'], originalValue: '-' }, + { + category: 'source', + field: 'source.ip', + values: ['185.156.74.3'], + originalValue: '185.156.74.3', + }, + { + category: 'base', + field: 'message', + values: [ + 'An account failed to log on.\n\nSubject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nLogon Type:\t\t\t3\n\nAccount For Which Logon Failed:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\tadministrator\n\tAccount Domain:\t\t\n\nFailure Information:\n\tFailure Reason:\t\tUnknown user name or bad password.\n\tStatus:\t\t\t0xC000006D\n\tSub Status:\t\t0xC000006A\n\nProcess Information:\n\tCaller Process ID:\t0x0\n\tCaller Process Name:\t-\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t185.156.74.3\n\tSource Port:\t\t0\n\nDetailed Authentication Information:\n\tLogon Process:\t\tNtLmSsp \n\tAuthentication Package:\tNTLM\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon request fails. It is generated on the computer where access was attempted.\n\nThe Subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe Logon Type field indicates the kind of logon that was requested. The most common types are 2 (interactive) and 3 (network).\n\nThe Process Information fields indicate which account and process on the system requested the logon.\n\nThe Network Information fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', + ], + originalValue: + 'An account failed to log on.\n\nSubject:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\t-\n\tAccount Domain:\t\t-\n\tLogon ID:\t\t0x0\n\nLogon Type:\t\t\t3\n\nAccount For Which Logon Failed:\n\tSecurity ID:\t\tS-1-0-0\n\tAccount Name:\t\tadministrator\n\tAccount Domain:\t\t\n\nFailure Information:\n\tFailure Reason:\t\tUnknown user name or bad password.\n\tStatus:\t\t\t0xC000006D\n\tSub Status:\t\t0xC000006A\n\nProcess Information:\n\tCaller Process ID:\t0x0\n\tCaller Process Name:\t-\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t185.156.74.3\n\tSource Port:\t\t0\n\nDetailed Authentication Information:\n\tLogon Process:\t\tNtLmSsp \n\tAuthentication Package:\tNTLM\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon request fails. It is generated on the computer where access was attempted.\n\nThe Subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe Logon Type field indicates the kind of logon that was requested. The most common types are 2 (interactive) and 3 (network).\n\nThe Process Information fields indicate which account and process on the system requested the logon.\n\nThe Network Information fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', + }, + { + category: 'cloud', + field: 'cloud.availability_zone', + values: ['us-central1-a'], + originalValue: 'us-central1-a', + }, + { + category: 'cloud', + field: 'cloud.instance.name', + values: ['windows-native'], + originalValue: 'windows-native', + }, + { + category: 'cloud', + field: 'cloud.instance.id', + values: ['5896613765949631815'], + originalValue: '5896613765949631815', + }, + { category: 'cloud', field: 'cloud.provider', values: ['gcp'], originalValue: 'gcp' }, + { + category: 'cloud', + field: 'cloud.machine.type', + values: ['e2-medium'], + originalValue: 'e2-medium', + }, + { + category: 'cloud', + field: 'cloud.project.id', + values: ['elastic-siem'], + originalValue: 'elastic-siem', + }, + { + category: 'base', + field: '@timestamp', + values: ['2020-11-25T15:42:39.417Z'], + originalValue: '2020-11-25T15:42:39.417Z', + }, + { + category: 'related', + field: 'related.user', + values: ['administrator'], + originalValue: 'administrator', + }, + { category: 'ecs', field: 'ecs.version', values: ['1.5.0'], originalValue: '1.5.0' }, + { + category: 'host', + field: 'host.hostname', + values: ['windows-native'], + originalValue: 'windows-native', + }, + { category: 'host', field: 'host.os.build', values: ['17763.1577'], originalValue: '17763.1577' }, + { + category: 'host', + field: 'host.os.kernel', + values: ['10.0.17763.1577 (WinBuild.160101.0800)'], + originalValue: '10.0.17763.1577 (WinBuild.160101.0800)', + }, + { + category: 'host', + field: 'host.os.name', + values: ['Windows Server 2019 Datacenter'], + originalValue: 'Windows Server 2019 Datacenter', + }, + { category: 'host', field: 'host.os.family', values: ['windows'], originalValue: 'windows' }, + { category: 'host', field: 'host.os.version', values: ['10.0'], originalValue: '10.0' }, + { category: 'host', field: 'host.os.platform', values: ['windows'], originalValue: 'windows' }, + { + category: 'host', + field: 'host.ip', + values: ['fe80::406c:d205:5b46:767f', '10.128.15.228'], + originalValue: ['fe80::406c:d205:5b46:767f', '10.128.15.228'], + }, + { + category: 'host', + field: 'host.name', + values: ['windows-native'], + originalValue: 'windows-native', + }, + { + category: 'host', + field: 'host.id', + values: ['08f50e68-847a-4fae-a8eb-c7dc886447bb'], + originalValue: '08f50e68-847a-4fae-a8eb-c7dc886447bb', + }, + { + category: 'host', + field: 'host.mac', + values: ['42:01:0a:80:0f:e4'], + originalValue: ['42:01:0a:80:0f:e4'], + }, + { category: 'host', field: 'host.architecture', values: ['x86_64'], originalValue: 'x86_64' }, + { + category: 'event', + field: 'event.ingested', + values: ['2020-11-25T15:36:40.924914552Z'], + originalValue: '2020-11-25T15:36:40.924914552Z', + }, + { category: 'event', field: 'event.code', values: [4625], originalValue: 4625 }, + { category: 'event', field: 'event.lag.total', values: [2077], originalValue: 2077 }, + { category: 'event', field: 'event.lag.read', values: [1075], originalValue: 1075 }, + { category: 'event', field: 'event.lag.ingest', values: [1002], originalValue: 1002 }, + { + category: 'event', + field: 'event.provider', + values: ['Microsoft-Windows-Security-Auditing'], + originalValue: 'Microsoft-Windows-Security-Auditing', + }, + { + category: 'event', + field: 'event.created', + values: ['2020-11-25T15:36:39.922Z'], + originalValue: '2020-11-25T15:36:39.922Z', + }, + { category: 'event', field: 'event.kind', values: ['signal'], originalValue: 'signal' }, + { category: 'event', field: 'event.module', values: ['security'], originalValue: 'security' }, + { + category: 'event', + field: 'event.action', + values: ['logon-failed'], + originalValue: 'logon-failed', + }, + { category: 'event', field: 'event.type', values: ['start'], originalValue: 'start' }, + { + category: 'event', + field: 'event.category', + values: ['authentication'], + originalValue: 'authentication', + }, + { category: 'event', field: 'event.outcome', values: ['failure'], originalValue: 'failure' }, + { + category: 'user', + field: 'user.name', + values: ['administrator'], + originalValue: 'administrator', + }, + { category: 'user', field: 'user.id', values: ['S-1-0-0'], originalValue: 'S-1-0-0' }, + { + category: 'signal', + field: 'signal.parents', + values: [ + '{"id":"688MAHYB7WTwW_Glsi_d","type":"event","index":"winlogbeat-7.10.0-2020.11.12-000001","depth":0}', + ], + originalValue: [ + { + id: '688MAHYB7WTwW_Glsi_d', + type: 'event', + index: 'winlogbeat-7.10.0-2020.11.12-000001', + depth: 0, + }, + ], + }, + { + category: 'signal', + field: 'signal.ancestors', + values: [ + '{"id":"688MAHYB7WTwW_Glsi_d","type":"event","index":"winlogbeat-7.10.0-2020.11.12-000001","depth":0}', + ], + originalValue: [ + { + id: '688MAHYB7WTwW_Glsi_d', + type: 'event', + index: 'winlogbeat-7.10.0-2020.11.12-000001', + depth: 0, + }, + ], + }, + { category: 'signal', field: 'signal.status', values: ['open'], originalValue: 'open' }, + { + category: 'signal', + field: 'signal.rule.id', + values: ['b69d086c-325a-4f46-b17b-fb6d227006ba'], + originalValue: 'b69d086c-325a-4f46-b17b-fb6d227006ba', + }, + { + category: 'signal', + field: 'signal.rule.rule_id', + values: ['e7cd9a53-ac62-44b5-bdec-9c94d85bb1a5'], + originalValue: 'e7cd9a53-ac62-44b5-bdec-9c94d85bb1a5', + }, + { category: 'signal', field: 'signal.rule.actions', values: [], originalValue: [] }, + { category: 'signal', field: 'signal.rule.author', values: [], originalValue: [] }, + { category: 'signal', field: 'signal.rule.false_positives', values: [], originalValue: [] }, + { category: 'signal', field: 'signal.rule.meta.from', values: ['1m'], originalValue: '1m' }, + { + category: 'signal', + field: 'signal.rule.meta.kibana_siem_app_url', + values: ['http://localhost:5601/app/security'], + originalValue: 'http://localhost:5601/app/security', + }, + { category: 'signal', field: 'signal.rule.max_signals', values: [100], originalValue: 100 }, + { category: 'signal', field: 'signal.rule.risk_score', values: [21], originalValue: 21 }, + { category: 'signal', field: 'signal.rule.risk_score_mapping', values: [], originalValue: [] }, + { + category: 'signal', + field: 'signal.rule.output_index', + values: ['.siem-signals-angelachuang-default'], + originalValue: '.siem-signals-angelachuang-default', + }, + { category: 'signal', field: 'signal.rule.description', values: ['xxx'], originalValue: 'xxx' }, + { + category: 'signal', + field: 'signal.rule.from', + values: ['now-360s'], + originalValue: 'now-360s', + }, + { + category: 'signal', + field: 'signal.rule.index', + values: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + originalValue: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + }, + { category: 'signal', field: 'signal.rule.interval', values: ['5m'], originalValue: '5m' }, + { category: 'signal', field: 'signal.rule.language', values: ['kuery'], originalValue: 'kuery' }, + { category: 'signal', field: 'signal.rule.license', values: [''], originalValue: '' }, + { category: 'signal', field: 'signal.rule.name', values: ['xxx'], originalValue: 'xxx' }, + { + category: 'signal', + field: 'signal.rule.query', + values: ['@timestamp : * '], + originalValue: '@timestamp : * ', + }, + { category: 'signal', field: 'signal.rule.references', values: [], originalValue: [] }, + { category: 'signal', field: 'signal.rule.severity', values: ['low'], originalValue: 'low' }, + { category: 'signal', field: 'signal.rule.severity_mapping', values: [], originalValue: [] }, + { category: 'signal', field: 'signal.rule.tags', values: [], originalValue: [] }, + { category: 'signal', field: 'signal.rule.type', values: ['query'], originalValue: 'query' }, + { category: 'signal', field: 'signal.rule.to', values: ['now'], originalValue: 'now' }, + { + category: 'signal', + field: 'signal.rule.filters', + values: [ + '{"meta":{"alias":null,"negate":false,"disabled":false,"type":"exists","key":"message","value":"exists"},"exists":{"field":"message"},"$state":{"store":"appState"}}', + ], + originalValue: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'exists', + key: 'message', + value: 'exists', + }, + exists: { field: 'message' }, + $state: { store: 'appState' }, + }, + ], + }, + { + category: 'signal', + field: 'signal.rule.created_by', + values: ['angela'], + originalValue: 'angela', + }, + { + category: 'signal', + field: 'signal.rule.updated_by', + values: ['angela'], + originalValue: 'angela', + }, + { category: 'signal', field: 'signal.rule.threat', values: [], originalValue: [] }, + { category: 'signal', field: 'signal.rule.version', values: [2], originalValue: 2 }, + { + category: 'signal', + field: 'signal.rule.created_at', + values: ['2020-11-24T10:30:33.660Z'], + originalValue: '2020-11-24T10:30:33.660Z', + }, + { + category: 'signal', + field: 'signal.rule.updated_at', + values: ['2020-11-25T15:37:40.939Z'], + originalValue: '2020-11-25T15:37:40.939Z', + }, + { category: 'signal', field: 'signal.rule.exceptions_list', values: [], originalValue: [] }, + { category: 'signal', field: 'signal.depth', values: [1], originalValue: 1 }, + { + category: 'signal', + field: 'signal.parent.id', + values: ['688MAHYB7WTwW_Glsi_d'], + originalValue: '688MAHYB7WTwW_Glsi_d', + }, + { category: 'signal', field: 'signal.parent.type', values: ['event'], originalValue: 'event' }, + { + category: 'signal', + field: 'signal.parent.index', + values: ['winlogbeat-7.10.0-2020.11.12-000001'], + originalValue: 'winlogbeat-7.10.0-2020.11.12-000001', + }, + { category: 'signal', field: 'signal.parent.depth', values: [0], originalValue: 0 }, + { + category: 'signal', + field: 'signal.original_time', + values: ['2020-11-25T15:36:38.847Z'], + originalValue: '2020-11-25T15:36:38.847Z', + }, + { + category: 'signal', + field: 'signal.original_event.ingested', + values: ['2020-11-25T15:36:40.924914552Z'], + originalValue: '2020-11-25T15:36:40.924914552Z', + }, + { category: 'signal', field: 'signal.original_event.code', values: [4625], originalValue: 4625 }, + { + category: 'signal', + field: 'signal.original_event.lag.total', + values: [2077], + originalValue: 2077, + }, + { + category: 'signal', + field: 'signal.original_event.lag.read', + values: [1075], + originalValue: 1075, + }, + { + category: 'signal', + field: 'signal.original_event.lag.ingest', + values: [1002], + originalValue: 1002, + }, + { + category: 'signal', + field: 'signal.original_event.provider', + values: ['Microsoft-Windows-Security-Auditing'], + originalValue: 'Microsoft-Windows-Security-Auditing', + }, + { + category: 'signal', + field: 'signal.original_event.created', + values: ['2020-11-25T15:36:39.922Z'], + originalValue: '2020-11-25T15:36:39.922Z', + }, + { + category: 'signal', + field: 'signal.original_event.kind', + values: ['event'], + originalValue: 'event', + }, + { + category: 'signal', + field: 'signal.original_event.module', + values: ['security'], + originalValue: 'security', + }, + { + category: 'signal', + field: 'signal.original_event.action', + values: ['logon-failed'], + originalValue: 'logon-failed', + }, + { + category: 'signal', + field: 'signal.original_event.type', + values: ['start'], + originalValue: 'start', + }, + { + category: 'signal', + field: 'signal.original_event.category', + values: ['authentication'], + originalValue: 'authentication', + }, + { + category: 'signal', + field: 'signal.original_event.outcome', + values: ['failure'], + originalValue: 'failure', + }, + { + category: '_index', + field: '_index', + values: ['.siem-signals-angelachuang-default-000004'], + originalValue: '.siem-signals-angelachuang-default-000004', + }, + { + category: '_id', + field: '_id', + values: ['5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31'], + originalValue: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', + }, + { category: '_score', field: '_score', values: [1], originalValue: 1 }, + { + category: 'fields', + field: 'fields.agent.name', + values: ['windows-native'], + originalValue: ['windows-native'], + }, + { + category: 'fields', + field: 'fields.cloud.machine.type', + values: ['e2-medium'], + originalValue: ['e2-medium'], + }, + { category: 'fields', field: 'fields.cloud.provider', values: ['gcp'], originalValue: ['gcp'] }, + { + category: 'fields', + field: 'fields.agent.id', + values: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], + originalValue: ['abfe4a35-d5b4-42a0-a539-bd054c791769'], + }, + { + category: 'fields', + field: 'fields.cloud.instance.id', + values: ['5896613765949631815'], + originalValue: ['5896613765949631815'], + }, + { + category: 'fields', + field: 'fields.agent.type', + values: ['winlogbeat'], + originalValue: ['winlogbeat'], + }, + { + category: 'fields', + field: 'fields.@timestamp', + values: ['2020-11-25T15:42:39.417Z'], + originalValue: ['2020-11-25T15:42:39.417Z'], + }, + { + category: 'fields', + field: 'fields.agent.ephemeral_id', + values: ['b9850845-c000-4ddd-bd51-9978a07b7e7d'], + originalValue: ['b9850845-c000-4ddd-bd51-9978a07b7e7d'], + }, + { + category: 'fields', + field: 'fields.cloud.instance.name', + values: ['windows-native'], + originalValue: ['windows-native'], + }, + { + category: 'fields', + field: 'fields.cloud.availability_zone', + values: ['us-central1-a'], + originalValue: ['us-central1-a'], + }, + { + category: 'fields', + field: 'fields.agent.version', + values: ['7.10.0'], + originalValue: ['7.10.0'], + }, +]; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap index 8d807825c246a..973d067d9e379 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap @@ -566,6 +566,13 @@ exports[`EventDetails rendering should match snapshot 1`] = ` "902", ], }, + Object { + "field": "event.kind", + "originalValue": "event", + "values": Array [ + "event", + ], + }, ] } eventId="Y-6TfmcB0WOhS6qyMv3s" @@ -1139,6 +1146,13 @@ exports[`EventDetails rendering should match snapshot 1`] = ` "902", ], }, + Object { + "field": "event.kind", + "originalValue": "event", + "values": Array [ + "event", + ], + }, ] } eventId="Y-6TfmcB0WOhS6qyMv3s" @@ -1296,6 +1310,13 @@ exports[`EventDetails rendering should match snapshot 1`] = ` "902", ], }, + Object { + "field": "event.kind", + "originalValue": "event", + "values": Array [ + "event", + ], + }, ] } /> diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap index af9fc61b9585c..15f00bbf72cf1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -43,6 +43,9 @@ exports[`JSON View rendering should match snapshot 1`] = ` \\"ip\\": \\"10.47.8.200\\", \\"packets\\": 4, \\"port\\": 902 + }, + \\"event\\": { + \\"kind\\": \\"event\\" } }" width="100%" diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx index 1a492eee4ae7a..0b2fbcf703d77 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/columns.tsx @@ -58,6 +58,7 @@ export const getColumns = ({ onUpdateColumns, contextId, toggleColumn, + getLinkValue, }: { browserFields: BrowserFields; columnHeaders: ColumnHeaderOptions[]; @@ -65,6 +66,7 @@ export const getColumns = ({ onUpdateColumns: OnUpdateColumns; contextId: string; toggleColumn: (column: ColumnHeaderOptions) => void; + getLinkValue: (field: string) => string | null; }) => [ { field: 'field', @@ -187,6 +189,7 @@ export const getColumns = ({ fieldName={data.field} fieldType={data.type} value={value} + linkValue={getLinkValue(data.field)} /> )} diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index bafe3df1a9cc7..20fa6e54e044d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -9,37 +9,45 @@ import React from 'react'; import '../../mock/match_media'; import '../../mock/react_beautiful_dnd'; -import { - defaultHeaders, - mockDetailItemData, - mockDetailItemDataId, - TestProviders, -} from '../../mock'; +import { mockDetailItemData, mockDetailItemDataId, TestProviders } from '../../mock'; -import { EventDetails, View } from './event_details'; +import { EventDetails, EventsViewType } from './event_details'; import { mockBrowserFields } from '../../containers/source/mock'; import { useMountAppended } from '../../utils/use_mount_appended'; +import { mockAlertDetailsData } from './__mocks__'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; jest.mock('../link_to'); describe('EventDetails', () => { const mount = useMountAppended(); const defaultProps = { browserFields: mockBrowserFields, - columnHeaders: defaultHeaders, data: mockDetailItemData, id: mockDetailItemDataId, - view: 'table-view' as View, - onUpdateColumns: jest.fn(), + isAlert: false, onViewSelected: jest.fn(), timelineId: 'test', - toggleColumn: jest.fn(), + view: EventsViewType.summaryView, }; + + const alertsProps = { + ...defaultProps, + data: mockAlertDetailsData as TimelineEventsDetailsItem[], + isAlert: true, + }; + const wrapper = mount( ); + const alertsWrapper = mount( + + + + ); + describe('rendering', () => { test('should match snapshot', () => { const shallowWrap = shallow(); @@ -65,4 +73,27 @@ describe('EventDetails', () => { ).toEqual('Table'); }); }); + + describe('alerts tabs', () => { + ['Summary', 'Table', 'JSON View'].forEach((tab) => { + test(`it renders the ${tab} tab`, () => { + expect( + alertsWrapper + .find('[data-test-subj="eventDetails"]') + .find('[role="tablist"]') + .containsMatchingElement({tab}) + ).toBeTruthy(); + }); + }); + + test('the Summary tab is selected by default', () => { + expect( + alertsWrapper + .find('[data-test-subj="eventDetails"]') + .find('.euiTab-isSelected') + .first() + .text() + ).toEqual('Summary'); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index 92c3ff9b9fa97..291893fe682b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiSpacer, EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui'; +import { EuiTabbedContent, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -13,17 +13,20 @@ import { TimelineEventsDetailsItem } from '../../../../common/search_strategy/ti import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; +import { SummaryView } from './summary_view'; -export type View = EventsViewType.tableView | EventsViewType.jsonView; +export type View = EventsViewType.tableView | EventsViewType.jsonView | EventsViewType.summaryView; export enum EventsViewType { tableView = 'table-view', jsonView = 'json-view', + summaryView = 'summary-view', } interface Props { browserFields: BrowserFields; data: TimelineEventsDetailsItem[]; id: string; + isAlert: boolean; view: EventsViewType; onViewSelected: (selected: EventsViewType) => void; timelineId: string; @@ -50,13 +53,33 @@ const EventDetailsComponent: React.FC = ({ view, onViewSelected, timelineId, + isAlert, }) => { - const handleTabClick = useCallback((e) => onViewSelected(e.id as EventsViewType), [ - onViewSelected, - ]); + const handleTabClick = useCallback((e) => onViewSelected(e.id), [onViewSelected]); + const alerts = useMemo( + () => [ + { + id: EventsViewType.summaryView, + name: i18n.SUMMARY, + content: ( + <> + + + + ), + }, + ], + [data, id, browserFields, timelineId] + ); const tabs: EuiTabbedContentTab[] = useMemo( () => [ + ...(isAlert ? alerts : []), { id: EventsViewType.tableView, name: i18n.TABLE, @@ -83,10 +106,10 @@ const EventDetailsComponent: React.FC = ({ ), }, ], - [browserFields, data, id, timelineId] + [alerts, browserFields, data, id, isAlert, timelineId] ); - const selectedTab = view === EventsViewType.tableView ? tabs[0] : tabs[1]; + const selectedTab = useMemo(() => tabs.find((t) => t.id === view) ?? tabs[0], [tabs, view]); return ( ( const fieldsByName = useMemo(() => getAllFieldsByName(browserFields), [browserFields]); const items = useMemo( () => - sortBy(data, ['field']).map((item) => ({ + sortBy(['field'], data).map((item) => ({ ...item, ...fieldsByName[item.field], valuesConcatenated: item.values != null ? item.values.join() : '', @@ -90,6 +90,19 @@ export const EventFieldsBrowser = React.memo( return getColumnHeaders(columns, browserFields); }); + const getLinkValue = useCallback( + (field: string) => { + const linkField = (columnHeaders.find((col) => col.id === field) ?? {}).linkField; + if (!linkField) { + return null; + } + const linkFieldData = (data ?? []).find((d) => d.field === linkField); + const linkFieldValue = getOr(null, 'originalValue', linkFieldData); + return linkFieldValue; + }, + [data, columnHeaders] + ); + const toggleColumn = useCallback( (column: ColumnHeaderOptions) => { if (columnHeaders.some((c) => c.id === column.id)) { @@ -126,8 +139,17 @@ export const EventFieldsBrowser = React.memo( onUpdateColumns, contextId: timelineId, toggleColumn, + getLinkValue, }), - [browserFields, columnHeaders, eventId, onUpdateColumns, timelineId, toggleColumn] + [ + browserFields, + columnHeaders, + eventId, + onUpdateColumns, + timelineId, + toggleColumn, + getLinkValue, + ] ); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx index 0cf158c8ea90b..da93670d647a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx @@ -54,6 +54,9 @@ describe('JSON View', () => { packets: 4, port: 902, }, + event: { + kind: 'event', + }, }; expect(buildJsonView(mockDetailItemData)).toEqual(expectedData); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx new file mode 100644 index 0000000000000..dec1bd9f3ac69 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { waitFor } from '@testing-library/react'; + +import { SummaryViewComponent } from './summary_view'; +import { mockAlertDetailsData } from './__mocks__'; +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; + +import { TestProviders } from '../../mock'; +import { mockBrowserFields } from '../../containers/source/mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +jest.mock('../../../detections/containers/detection_engine/rules/use_rule_async', () => { + return { + useRuleAsync: jest.fn(), + }; +}); + +const props = { + data: mockAlertDetailsData as TimelineEventsDetailsItem[], + browserFields: mockBrowserFields, + eventId: '5d1d53da502f56aacc14c3cb5c669363d102b31f99822e5d369d4804ed370a31', + timelineId: 'detections-page', +}; + +describe('SummaryViewComponent', () => { + const mount = useMountAppended(); + + beforeEach(() => { + jest.clearAllMocks(); + (useRuleAsync as jest.Mock).mockReturnValue({ + rule: { + note: 'investigation guide', + }, + }); + }); + test('render correct items', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="summary-view"]').exists()).toEqual(true); + }); + + test('render investigation guide', async () => { + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(true); + }); + }); + + test("render no investigation guide if it doesn't exist", async () => { + (useRuleAsync as jest.Mock).mockReturnValue({ + rule: { + note: null, + }, + }); + const wrapper = mount( + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="summary-view-guide"]').exists()).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx new file mode 100644 index 0000000000000..13d734657acce --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; +import { + EuiTitle, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiInMemoryTable, + EuiBasicTableColumn, +} from '@elastic/eui'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; +import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; +import * as i18n from './translations'; +import { BrowserFields } from '../../../../common/search_strategy/index_fields'; +import { + ALERTS_HEADERS_RISK_SCORE, + ALERTS_HEADERS_RULE, + ALERTS_HEADERS_SEVERITY, +} from '../../../detections/components/alerts_table/translations'; +import { + IP_FIELD_TYPE, + SIGNAL_RULE_NAME_FIELD_NAME, +} from '../../../timelines/components/timeline/body/renderers/constants'; +import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip'; +import { LineClamp } from '../line_clamp'; +import { useRuleAsync } from '../../../detections/containers/detection_engine/rules/use_rule_async'; + +interface SummaryRow { + title: string; + description: { + contextId: string; + eventId: string; + fieldName: string; + value: string; + fieldType: string; + linkValue: string | undefined; + }; +} +type Summary = SummaryRow[]; + +const fields = [ + { id: 'signal.status' }, + { id: '@timestamp' }, + { + id: SIGNAL_RULE_NAME_FIELD_NAME, + linkField: 'signal.rule.id', + label: ALERTS_HEADERS_RULE, + }, + { id: 'signal.rule.severity', label: ALERTS_HEADERS_SEVERITY }, + { id: 'signal.rule.risk_score', label: ALERTS_HEADERS_RISK_SCORE }, + { id: 'host.name' }, + { id: 'user.name' }, + { id: SOURCE_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, + { id: DESTINATION_IP_FIELD_NAME, fieldType: IP_FIELD_TYPE }, +]; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const StyledEuiInMemoryTable = styled(EuiInMemoryTable as any)` + .euiTableHeaderCell { + border: none; + } + .euiTableRowCell { + border: none; + } +`; + +const StyledEuiDescriptionList = styled(EuiDescriptionList)` + padding: 24px 4px 4px; +`; + +const getTitle = (title: SummaryRow['title']) => ( + +
    {title}
    +
    +); + +getTitle.displayName = 'getTitle'; + +const getDescription = ({ + contextId, + eventId, + fieldName, + value, + fieldType = '', + linkValue, +}: SummaryRow['description']) => ( + +); + +const getSummary = ({ + data, + browserFields, + timelineId, + eventId, +}: { + data: TimelineEventsDetailsItem[]; + browserFields: BrowserFields; + timelineId: string; + eventId: string; +}) => { + return data != null + ? fields.reduce((acc, item) => { + const field = data.find((d) => d.field === item.id); + if (!field) { + return acc; + } + const linkValueField = + item.linkField != null && data.find((d) => d.field === item.linkField); + const linkValue = getOr(null, 'originalValue.0', linkValueField); + const value = getOr(null, 'originalValue.0', field); + const category = field.category; + const fieldType = get(`${category}.fields.${field.field}.type`, browserFields) as string; + const description = { + contextId: timelineId, + eventId, + fieldName: item.id, + value, + fieldType: item.fieldType ?? fieldType, + linkValue: linkValue ?? undefined, + }; + + return [ + ...acc, + { + title: item.label ?? item.id, + description, + }, + ]; + }, []) + : []; +}; + +const summaryColumns: Array> = [ + { + field: 'title', + truncateText: false, + render: getTitle, + width: '120px', + name: '', + }, + { + field: 'description', + truncateText: false, + render: getDescription, + name: '', + }, +]; + +export const SummaryViewComponent: React.FC<{ + browserFields: BrowserFields; + data: TimelineEventsDetailsItem[]; + eventId: string; + timelineId: string; +}> = ({ data, eventId, timelineId, browserFields }) => { + const ruleId = useMemo( + () => + getOr( + null, + 'originalValue', + data.find((d) => d.field === 'signal.rule.id') + ), + [data] + ); + const { rule: maybeRule } = useRuleAsync(ruleId); + const summaryList = useMemo(() => getSummary({ browserFields, data, eventId, timelineId }), [ + browserFields, + data, + eventId, + timelineId, + ]); + + return ( + <> + + {maybeRule?.note && ( + + {i18n.INVESTIGATION_GUIDE} + + + + + )} + + ); +}; + +export const SummaryView = React.memo(SummaryViewComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts index 19e71e0f37da6..76ae2cd4a88a8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/translations.ts @@ -6,6 +6,17 @@ import { i18n } from '@kbn/i18n'; +export const SUMMARY = i18n.translate('xpack.securitySolution.alertDetails.summary', { + defaultMessage: 'Summary', +}); + +export const INVESTIGATION_GUIDE = i18n.translate( + 'xpack.securitySolution.alertDetails.summary.investigationGuide', + { + defaultMessage: 'Investigation guide', + } +); + export const TABLE = i18n.translate('xpack.securitySolution.eventDetails.table', { defaultMessage: 'Table', }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx index b3a838ab088df..6fecf0d739d1a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx @@ -4,19 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import { some } from 'lodash/fp'; +import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { useDispatch } from 'react-redux'; -import { timelineActions } from '../../../timelines/store/timeline'; import { BrowserFields, DocValueFields } from '../../containers/source'; import { ExpandableEvent, ExpandableEventTitle, } from '../../../timelines/components/timeline/expandable_event'; import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { useTimelineEventsDetails } from '../../../timelines/containers/details'; +import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; const StyledEuiFlyout = styled(EuiFlyout)` z-index: ${({ theme }) => theme.eui.euiZLevel7}; @@ -28,27 +31,33 @@ interface EventDetailsFlyoutProps { timelineId: string; } -const emptyExpandedEvent = {}; - const EventDetailsFlyoutComponent: React.FC = ({ browserFields, docValueFields, timelineId, }) => { const dispatch = useDispatch(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const expandedEvent = useDeepEqualSelector( - (state) => state.timeline.timelineById[timelineId]?.expandedEvent ?? emptyExpandedEvent + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent ); const handleClearSelection = useCallback(() => { - dispatch( - timelineActions.toggleExpandedEvent({ - timelineId, - event: emptyExpandedEvent, - }) - ); + dispatch(timelineActions.toggleExpandedEvent({ timelineId })); }, [dispatch, timelineId]); + const [loading, detailsData] = useTimelineEventsDetails({ + docValueFields, + indexName: expandedEvent.indexName!, + eventId: expandedEvent.eventId!, + skip: !expandedEvent.eventId, + }); + + const isAlert = useMemo( + () => some({ category: 'signal', field: 'signal.rule.id' }, detailsData), + [detailsData] + ); + if (!expandedEvent.eventId) { return null; } @@ -56,13 +65,15 @@ const EventDetailsFlyoutComponent: React.FC = ({ return ( - + diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 8710503924d84..5e5bdebffa182 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -65,6 +65,7 @@ const eventsViewerDefaultProps = { deletedEventIds: [], docValueFields: [], end: to, + expandedEvent: {}, filters: [], id: TimelineId.detectionsPage, indexNames: mockIndexNames, @@ -78,6 +79,7 @@ const eventsViewerDefaultProps = { query: '', language: 'kql', }, + handleCloseExpandedEvent: jest.fn(), start: from, sort: [ { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index c578e017c4d95..69c75bfbea56a 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -5,14 +5,16 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; +import { isEmpty, some } from 'lodash/fp'; import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; +import { useDispatch } from 'react-redux'; import { Direction } from '../../../../common/search_strategy'; import { BrowserFields, DocValueFields } from '../../containers/source'; import { useTimelineEvents } from '../../../timelines/containers'; +import { timelineActions } from '../../../timelines/store/timeline'; import { useKibana } from '../../lib/kibana'; import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model'; import { HeaderSection } from '../header_section'; @@ -35,7 +37,7 @@ import { inputsModel } from '../../store'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { ExitFullScreen } from '../exit_full_screen'; import { useFullScreen } from '../../containers/use_full_screen'; -import { TimelineId } from '../../../../common/types/timeline'; +import { TimelineExpandedEvent, TimelineId } from '../../../../common/types/timeline'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; export const EVENTS_VIEWER_HEADER_HEIGHT = 90; // px @@ -101,6 +103,7 @@ interface Props { deletedEventIds: Readonly; docValueFields: DocValueFields[]; end: string; + expandedEvent: TimelineExpandedEvent; filters: Filter[]; headerFilterGroup?: React.ReactNode; height?: number; @@ -128,6 +131,7 @@ const EventsViewerComponent: React.FC = ({ deletedEventIds, docValueFields, end, + expandedEvent, filters, headerFilterGroup, id, @@ -145,6 +149,7 @@ const EventsViewerComponent: React.FC = ({ utilityBar, graphEventId, }) => { + const dispatch = useDispatch(); const { globalFullScreen, timelineFullScreen } = useFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); @@ -226,6 +231,12 @@ const EventsViewerComponent: React.FC = ({ skip: !canQueryTimeline, }); + useEffect(() => { + if (!events || (expandedEvent.eventId && !some(['_id', expandedEvent.eventId], events))) { + dispatch(timelineActions.toggleExpandedEvent({ timelineId: id })); + } + }, [dispatch, events, expandedEvent, id]); + const totalCountMinusDeleted = useMemo( () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), [deletedEventIds.length, totalCount] diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index ec3cbbdef98ad..2570a2b6d1f37 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -51,6 +51,7 @@ const StatefulEventsViewerComponent: React.FC = ({ deletedEventIds, deleteEventQuery, end, + expandedEvent, excludedRowRendererIds, filters, headerFilterGroup, @@ -111,6 +112,7 @@ const StatefulEventsViewerComponent: React.FC = ({ dataProviders={dataProviders!} deletedEventIds={deletedEventIds} end={end} + expandedEvent={expandedEvent} isLoadingIndexPattern={isLoadingIndexPattern} filters={globalFilters} headerFilterGroup={headerFilterGroup} @@ -142,27 +144,29 @@ const makeMapStateToProps = () => { const getInputsTimeline = inputsSelectors.getTimelineSelector(); const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getEvents = timelineSelectors.getEventsByIdSelector(); const getTimeline = timelineSelectors.getTimelineByIdSelector(); const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { const input: inputsModel.InputsRange = getInputsTimeline(state); - const events: TimelineModel = getEvents(state, id) ?? defaultModel; + const timeline: TimelineModel = getTimeline(state, id) ?? defaultModel; const { columns, dataProviders, deletedEventIds, excludedRowRendererIds, + expandedEvent, + graphEventId, itemsPerPage, itemsPerPageOptions, kqlMode, sort, showCheckboxes, - } = events; + } = timeline; return { columns, dataProviders, deletedEventIds, + expandedEvent, excludedRowRendererIds, filters: getGlobalFiltersQuerySelector(state), id, @@ -175,7 +179,7 @@ const makeMapStateToProps = () => { showCheckboxes, // Used to determine whether the footer should show (since it is hidden if the graph is showing.) // `getTimeline` actually returns `TimelineModel | undefined` - graphEventId: (getTimeline(state, id) as TimelineModel | undefined)?.graphEventId, + graphEventId, }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx new file mode 100644 index 0000000000000..1b59b174add4a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/index.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiText } from '@elastic/eui'; +import React, { useRef, useState, useEffect, useCallback } from 'react'; +import styled from 'styled-components'; +import * as i18n from './translations'; + +const LINE_CLAMP = 3; +const LINE_CLAMP_HEIGHT = 4.5; + +const StyledLineClamp = styled.div` + display: -webkit-box; + -webkit-line-clamp: ${LINE_CLAMP}; + -webkit-box-orient: vertical; + overflow: hidden; + max-height: ${`${LINE_CLAMP_HEIGHT}em`}; + height: ${`${LINE_CLAMP_HEIGHT}em`}; +`; + +const ReadMore = styled(EuiButtonEmpty)` + span.euiButtonContent { + padding: 0; + } +`; + +const LineClampComponent: React.FC<{ content?: string | null }> = ({ content }) => { + const [isOverflow, setIsOverflow] = useState(null); + const [isExpanded, setIsExpanded] = useState(null); + const descriptionRef = useRef(null); + const toggleReadMore = useCallback(() => { + setIsExpanded((prevState) => !prevState); + }, []); + + useEffect(() => { + if (content != null && descriptionRef?.current?.clientHeight != null) { + if ( + (descriptionRef?.current?.scrollHeight ?? 0) > (descriptionRef?.current?.clientHeight ?? 0) + ) { + setIsOverflow(true); + } + + if ( + ((content == null || descriptionRef?.current?.scrollHeight) ?? 0) <= + (descriptionRef?.current?.clientHeight ?? 0) + ) { + setIsOverflow(false); + } + } + }, [content]); + + if (!content) { + return null; + } + + return ( + <> + {isExpanded ? ( +

    {content}

    + ) : isOverflow == null || isOverflow === true ? ( + {content} + ) : ( + {content} + )} + {isOverflow && ( + + {isExpanded ? i18n.READ_LESS : i18n.READ_MORE} + + )} + + ); +}; + +export const LineClamp = React.memo(LineClampComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/line_clamp/translations.ts b/x-pack/plugins/security_solution/public/common/components/line_clamp/translations.ts new file mode 100644 index 0000000000000..e332d1a2d2b5c --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/line_clamp/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const READ_MORE = i18n.translate('xpack.securitySolution.alertDetails.summary.readMore', { + defaultMessage: 'Read More', +}); + +export const READ_LESS = i18n.translate('xpack.securitySolution.alertDetails.summary.readLess', { + defaultMessage: 'Read Less', +}); diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts index c5d881c540eec..f074495e65b64 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts @@ -109,4 +109,9 @@ export const mockDetailItemData: TimelineEventsDetailsItem[] = [ originalValue: 902, values: ['902'], }, + { + field: 'event.kind', + originalValue: 'event', + values: ['event'], + }, ]; diff --git a/x-pack/plugins/security_solution/public/network/components/ip/index.tsx b/x-pack/plugins/security_solution/public/network/components/ip/index.tsx index 701094cee88a2..4905fdc2e1f57 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/ip/index.tsx @@ -11,7 +11,7 @@ import { FormattedFieldValue } from '../../../timelines/components/timeline/body export const SOURCE_IP_FIELD_NAME = 'source.ip'; export const DESTINATION_IP_FIELD_NAME = 'destination.ip'; -const IP_FIELD_TYPE = 'ip'; +export const IP_FIELD_TYPE = 'ip'; /** * Renders text containing a draggable IP address (e.g. `source.ip`, diff --git a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx index f4f8adc9f0419..0e92491be8d18 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/fields_browser/field_items.test.tsx @@ -6,6 +6,7 @@ import { omit } from 'lodash/fp'; import React from 'react'; +import { waitFor } from '@testing-library/react'; import { mockBrowserFields } from '../../../common/containers/source/mock'; import { TestProviders } from '../../../common/mock'; @@ -204,6 +205,65 @@ describe('field_items', () => { }); }); + test('it returns the expected signal column settings', async () => { + const mockSelectedCategoryId = 'signal'; + const mockBrowserFieldsWithSignal = { + ...mockBrowserFields, + signal: { + fields: { + 'signal.rule.name': { + aggregatable: true, + category: 'signal', + description: 'rule name', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'signal.rule.name', + searchable: true, + type: 'string', + }, + }, + }, + }; + const toggleColumn = jest.fn(); + const wrapper = mount( + + + + ); + wrapper + .find(`[data-test-subj="field-signal.rule.name-checkbox"]`) + .last() + .simulate('change', { + target: { checked: true }, + }); + + await waitFor(() => { + expect(toggleColumn).toBeCalledWith({ + columnHeaderType: 'not-filtered', + id: 'signal.rule.name', + width: 180, + }); + }); + }); + test('it renders the expected icon for a field', () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts index 3f391714bb058..268c874de7d50 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts @@ -6,13 +6,6 @@ import { i18n } from '@kbn/i18n'; -export const ALL_ACTIONS = i18n.translate( - 'xpack.securitySolution.open.timeline.allActionsTooltip', - { - defaultMessage: 'All actions', - } -); - export const BATCH_ACTIONS = i18n.translate( 'xpack.securitySolution.open.timeline.batchActionsTitle', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx index 00cd5453e9669..2ded93377de93 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/row_renderers_browser/index.tsx @@ -19,16 +19,16 @@ import { EuiFlexItem, EuiInMemoryTable, } from '@elastic/eui'; -import React, { useState, useCallback, useRef } from 'react'; +import React, { useState, useCallback, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { RowRendererId } from '../../../../common/types/timeline'; import { State } from '../../../common/store'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; - +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../store/timeline/actions'; +import { timelineSelectors } from '../../store/timeline'; +import { timelineDefaults } from '../../store/timeline/defaults'; import { renderers } from './catalog'; -import { setExcludedRowRendererIds as dispatchSetExcludedRowRendererIds } from '../../../timelines/store/timeline/actions'; import { RowRenderersBrowser } from './row_renderers_browser'; import * as i18n from './translations'; @@ -78,16 +78,14 @@ interface StatefulRowRenderersBrowserProps { timelineId: string; } -const emptyExcludedRowRendererIds: RowRendererId[] = []; - const StatefulRowRenderersBrowserComponent: React.FC = ({ timelineId, }) => { const tableRef = useRef>(); const dispatch = useDispatch(); - const excludedRowRendererIds = useShallowEqualSelector( - (state: State) => - state.timeline.timelineById[timelineId]?.excludedRowRendererIds || emptyExcludedRowRendererIds + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const excludedRowRendererIds = useDeepEqualSelector( + (state: State) => (getTimeline(state, timelineId) ?? timelineDefaults).excludedRowRendererIds ); const [show, setShow] = useState(false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index e942dce724520..b853dc8c81c00 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useCallback } from 'react'; -import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox } from '@elastic/eui'; +import { EuiButtonIcon, EuiLoadingSpinner, EuiCheckbox, EuiToolTip } from '@elastic/eui'; import { EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; import * as i18n from '../translations'; @@ -66,14 +66,16 @@ const ActionsComponent: React.FC = ({ )} - + + + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 584350f9f7b66..3297d4d613a2b 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -11,6 +11,7 @@ import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { timelineSelectors } from '../../../../store/timeline'; import { AssociateNote } from '../../../notes/helpers'; import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { EventsTrData } from '../../styles'; @@ -85,8 +86,9 @@ export const EventColumnView = React.memo( timelineId, toggleShowNotes, }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { timelineType, status } = useDeepEqualSelector((state) => - pick(['timelineType', 'status'], state.timeline.timelineById[timelineId]) + pick(['timelineType', 'status'], getTimeline(state, timelineId)) ); const handlePinClicked = useCallback( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index 3d3c87be42824..baaf9aa867d90 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -26,8 +26,9 @@ import { NoteCards } from '../../../notes/note_cards'; import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; import { inputsModel } from '../../../../../common/store'; -import { timelineActions } from '../../../../store/timeline'; +import { timelineActions, timelineSelectors } from '../../../../store/timeline'; import { activeTimeline } from '../../../../containers/active_timeline_context'; +import { timelineDefaults } from '../../../../store/timeline/defaults'; interface Props { actionsColumnWidth: number; @@ -77,8 +78,9 @@ const StatefulEventComponent: React.FC = ({ }) => { const dispatch = useDispatch(); const [showNotes, setShowNotes] = useState<{ [eventId: string]: boolean }>({}); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const expandedEvent = useDeepEqualSelector( - (state) => state.timeline.timelineById[timelineId].expandedEvent + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent ); const divElement = useRef(null); @@ -112,13 +114,12 @@ const StatefulEventComponent: React.FC = ({ event: { eventId, indexName, - loading: false, }, }) ); if (timelineId === TimelineId.active) { - activeTimeline.toggleExpandedEvent({ eventId, indexName, loading: false }); + activeTimeline.toggleExpandedEvent({ eventId, indexName }); } }, [dispatch, event._id, event._index, timelineId]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx index 10518141ebb25..7f3d86af7ca8a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/constants.tsx @@ -14,3 +14,4 @@ export const RULE_REFERENCE_FIELD_NAME = 'rule.reference'; export const REFERENCE_URL_FIELD_NAME = 'reference.url'; export const EVENT_URL_FIELD_NAME = 'event.url'; export const SIGNAL_RULE_NAME_FIELD_NAME = 'signal.rule.name'; +export const SIGNAL_STATUS_FIELD_NAME = 'signal.status'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 5bd928021fa0b..9c1169608ccae 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -5,19 +5,15 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { isNumber, isString, isEmpty } from 'lodash/fp'; +import { isNumber, isEmpty } from 'lodash/fp'; import React from 'react'; import { DefaultDraggable } from '../../../../../common/components/draggables'; import { Bytes, BYTES_FORMAT } from './bytes'; import { Duration, EVENT_DURATION_FIELD_NAME } from '../../../duration'; -import { - getOrEmptyTagFromValue, - getEmptyTagValue, -} from '../../../../../common/components/empty_value'; +import { getOrEmptyTagFromValue } from '../../../../../common/components/empty_value'; import { FormattedDate } from '../../../../../common/components/formatted_date'; import { FormattedIp } from '../../../../components/formatted_ip'; -import { HostDetailsLink } from '../../../../../common/components/links'; import { Port, PORT_NAMES } from '../../../../../network/components/port'; import { TruncatableText } from '../../../../../common/components/truncatable_text'; @@ -31,9 +27,12 @@ import { SIGNAL_RULE_NAME_FIELD_NAME, REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME, + SIGNAL_STATUS_FIELD_NAME, GEO_FIELD_TYPE, } from './constants'; import { RenderRuleName, renderEventModule, renderUrl } from './formatted_field_helpers'; +import { RuleStatus } from './rule_status'; +import { HostName } from './host_name'; // simple black-list to prevent dragging and dropping fields such as message name const columnNamesNotDraggable = [MESSAGE_FIELD_NAME]; @@ -80,22 +79,7 @@ const FormattedFieldValueComponent: React.FC<{ ); } else if (fieldName === HOST_NAME_FIELD_NAME) { - const hostname = `${value}`; - - return isString(value) && hostname.length > 0 ? ( - - - {value} - - - ) : ( - getEmptyTagValue() - ); + return ; } else if (fieldFormat === BYTES_FORMAT) { return ( @@ -113,6 +97,10 @@ const FormattedFieldValueComponent: React.FC<{ ); } else if (fieldName === EVENT_MODULE_FIELD_NAME) { return renderEventModule({ contextId, eventId, fieldName, linkValue, truncate, value }); + } else if (fieldName === SIGNAL_STATUS_FIELD_NAME) { + return ( + + ); } else if ( [RULE_REFERENCE_FIELD_NAME, REFERENCE_URL_FIELD_NAME, EVENT_URL_FIELD_NAME].includes(fieldName) ) { @@ -142,7 +130,6 @@ const FormattedFieldValueComponent: React.FC<{ } else { const contentValue = getOrEmptyTagFromValue(value); const content = truncate ? {contentValue} : contentValue; - return ( = ({ {content} + ) : value != null ? ( + + {value} + ) : ( getEmptyTagValue() ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx new file mode 100644 index 0000000000000..fbac27095d4f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/host_name.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { isString } from 'lodash/fp'; + +import { DefaultDraggable } from '../../../../../common/components/draggables'; +import { getEmptyTagValue } from '../../../../../common/components/empty_value'; +import { HostDetailsLink } from '../../../../../common/components/links'; +import { TruncatableText } from '../../../../../common/components/truncatable_text'; + +interface Props { + contextId: string; + eventId: string; + fieldName: string; + value: string | number | undefined | null; +} + +const HostNameComponent: React.FC = ({ fieldName, contextId, eventId, value }) => { + const hostname = `${value}`; + + return isString(value) && hostname.length > 0 ? ( + + + {value} + + + ) : ( + getEmptyTagValue() + ); +}; + +export const HostName = React.memo(HostNameComponent); +HostName.displayName = 'HostName'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx new file mode 100644 index 0000000000000..4dc6d3b2e8e8d --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/rule_status.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { EuiBadge } from '@elastic/eui'; +import { getOr } from 'lodash/fp'; + +import styled from 'styled-components'; +import { DefaultDraggable } from '../../../../../common/components/draggables'; + +const mapping = { + open: 'primary', + 'in-progress': 'warning', + closed: 'default', +}; + +const StyledEuiBadge = styled(EuiBadge)` + text-transform: capitalize; +`; + +interface Props { + contextId: string; + eventId: string; + fieldName: string; + value: string | number | undefined | null; +} + +const RuleStatusComponent: React.FC = ({ contextId, eventId, fieldName, value }) => { + const color = useMemo(() => getOr('default', `${value}`, mapping), [value]); + return ( + + {value} + + ); +}; + +export const RuleStatus = React.memo(RuleStatusComponent); +RuleStatus.displayName = 'RuleStatus'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts index f82f84ec7ad43..c934f50ba0aec 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/translations.ts @@ -73,17 +73,17 @@ export const EXPAND = i18n.translate( } ); -export const COLLAPSE = i18n.translate( - 'xpack.securitySolution.timeline.body.actions.collapseAriaLabel', +export const EXPAND_EVENT = i18n.translate( + 'xpack.securitySolution.timeline.body.actions.expandEventTooltip', { - defaultMessage: 'Collapse', + defaultMessage: 'Expand event', } ); -export const COLLAPSE_EVENT = i18n.translate( - 'xpack.securitySolution.timeline.body.actions.collapseEventTooltip', +export const COLLAPSE = i18n.translate( + 'xpack.securitySolution.timeline.body.actions.collapseAriaLabel', { - defaultMessage: 'Collapse event', + defaultMessage: 'Collapse', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx index ed9b20f7a5e2d..9895f4eda0e6c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/event_details.tsx @@ -10,40 +10,66 @@ * you may not use this file except in compliance with the Elastic License. */ +import { some } from 'lodash/fp'; import { EuiSpacer } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import deepEqual from 'fast-deep-equal'; import { BrowserFields, DocValueFields } from '../../../common/containers/source'; import { ExpandableEvent, ExpandableEventTitle, + HandleOnEventClosed, } from '../../../timelines/components/timeline/expandable_event'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { useTimelineEventsDetails } from '../../containers/details'; +import { timelineSelectors } from '../../store/timeline'; +import { timelineDefaults } from '../../store/timeline/defaults'; interface EventDetailsProps { browserFields: BrowserFields; docValueFields: DocValueFields[]; timelineId: string; + handleOnEventClosed?: HandleOnEventClosed; } const EventDetailsComponent: React.FC = ({ browserFields, docValueFields, timelineId, + handleOnEventClosed, }) => { + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const expandedEvent = useDeepEqualSelector( - (state) => state.timeline.timelineById[timelineId]?.expandedEvent + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent + ); + + const [loading, detailsData] = useTimelineEventsDetails({ + docValueFields, + indexName: expandedEvent.indexName!, + eventId: expandedEvent.eventId!, + skip: !expandedEvent.eventId, + }); + + const isAlert = useMemo( + () => some({ category: 'signal', field: 'signal.rule.id' }, detailsData), + [detailsData] ); return ( <> - + @@ -55,5 +81,6 @@ export const EventDetails = React.memo( (prevProps, nextProps) => deepEqual(prevProps.browserFields, nextProps.browserFields) && deepEqual(prevProps.docValueFields, nextProps.docValueFields) && - prevProps.timelineId === nextProps.timelineId + prevProps.timelineId === nextProps.timelineId && + prevProps.handleOnEventClosed === nextProps.handleOnEventClosed ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index 77a37d8b9a929..5c6bcbccc8e0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -5,45 +5,74 @@ */ import { find } from 'lodash/fp'; -import { EuiTextColor, EuiLoadingContent, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiTextColor, + EuiLoadingContent, + EuiTitle, + EuiSpacer, + EuiDescriptionList, + EuiDescriptionListTitle, + EuiDescriptionListDescription, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; import React, { useMemo, useState } from 'react'; +import styled from 'styled-components'; import { TimelineExpandedEvent } from '../../../../../common/types/timeline'; -import { BrowserFields, DocValueFields } from '../../../../common/containers/source'; +import { BrowserFields } from '../../../../common/containers/source'; import { EventDetails, EventsViewType, View, } from '../../../../common/components/event_details/event_details'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; -import { useTimelineEventsDetails } from '../../../containers/details'; +import { LineClamp } from '../../../../common/components/line_clamp'; import * as i18n from './translations'; +export type HandleOnEventClosed = () => void; interface Props { browserFields: BrowserFields; - docValueFields: DocValueFields[]; + detailsData: TimelineEventsDetailsItem[] | null; event: TimelineExpandedEvent; + isAlert: boolean; + loading: boolean; timelineId: string; } -export const ExpandableEventTitle = React.memo(() => ( - -

    {i18n.EVENT_DETAILS}

    -
    -)); +interface ExpandableEventTitleProps { + isAlert: boolean; + loading: boolean; + handleOnEventClosed?: HandleOnEventClosed; +} + +const StyledEuiFlexGroup = styled(EuiFlexGroup)` + flex: 0; +`; + +export const ExpandableEventTitle = React.memo( + ({ isAlert, loading, handleOnEventClosed }) => ( + + + + {!loading ?

    {isAlert ? i18n.ALERT_DETAILS : i18n.EVENT_DETAILS}

    : <>} +
    +
    + {handleOnEventClosed && ( + + + + )} +
    + ) +); ExpandableEventTitle.displayName = 'ExpandableEventTitle'; export const ExpandableEvent = React.memo( - ({ browserFields, docValueFields, event, timelineId }) => { - const [view, setView] = useState(EventsViewType.tableView); - - const [loading, detailsData] = useTimelineEventsDetails({ - docValueFields, - indexName: event.indexName!, - eventId: event.eventId!, - skip: !event.eventId, - }); + ({ browserFields, event, timelineId, isAlert, loading, detailsData }) => { + const [view, setView] = useState(EventsViewType.summaryView); const message = useMemo(() => { if (detailsData) { @@ -68,12 +97,22 @@ export const ExpandableEvent = React.memo( return ( <> - {message} - + {message && ( + <> + + {i18n.MESSAGE} + + + + + + + )} { columns: defaultHeaders, dataProviders: mockDataProviders, end: endDate, + expandedEvent: {}, eventType: 'all', showEventDetails: false, filters: [], @@ -103,6 +104,7 @@ describe('Timeline', () => { itemsPerPageOptions: [5, 10, 20], kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', + onEventClosed: jest.fn(), showCallOutUnauthorizedMsg: false, sort, start: startDate, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 69a7299b9833d..aa3970bba5884 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -13,8 +13,8 @@ import { EuiFlyoutFooter, EuiSpacer, } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useState, useMemo, useEffect } from 'react'; +import { isEmpty, some } from 'lodash/fp'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; import styled from 'styled-components'; import { Dispatch } from 'redux'; import { connect, ConnectedProps } from 'react-redux'; @@ -32,7 +32,7 @@ import { combineQueries } from '../helpers'; import { TimelineRefetch } from '../refetch_timeline'; import { esQuery, FilterManager } from '../../../../../../../../src/plugins/data/public'; import { useManageTimeline } from '../../manage_timeline'; -import { TimelineEventsType } from '../../../../../common/types/timeline'; +import { TimelineEventsType, TimelineId } from '../../../../../common/types/timeline'; import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { SuperDatePicker } from '../../../../common/components/super_date_picker'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; @@ -45,6 +45,8 @@ import { useSourcererScope } from '../../../../common/containers/sourcerer'; import { TimelineModel } from '../../../../timelines/store/timeline/model'; import { EventDetails } from '../event_details'; import { TimelineDatePickerLock } from '../date_picker_lock'; +import { activeTimeline } from '../../../containers/active_timeline_context'; +import { ToggleExpandedEvent } from '../../../store/timeline/actions'; const TimelineHeaderContainer = styled.div` margin-top: 6px; @@ -141,6 +143,7 @@ export const QueryTabContentComponent: React.FC = ({ dataProviders, end, eventType, + expandedEvent, filters, timelineId, isLive, @@ -148,6 +151,7 @@ export const QueryTabContentComponent: React.FC = ({ itemsPerPageOptions, kqlMode, kqlQueryExpression, + onEventClosed, showCallOutUnauthorizedMsg, showEventDetails, start, @@ -156,18 +160,6 @@ export const QueryTabContentComponent: React.FC = ({ timerangeKind, updateEventTypeAndIndexesName, }) => { - const [showEventDetailsColumn, setShowEventDetailsColumn] = useState(false); - - useEffect(() => { - // it should changed only once to true and then stay visible till the component umount - setShowEventDetailsColumn((current) => { - if (showEventDetails && !current) { - return true; - } - return current; - }); - }, [showEventDetails]); - const { browserFields, docValueFields, @@ -247,10 +239,27 @@ export const QueryTabContentComponent: React.FC = ({ timerangeKind, }); + const handleOnEventClosed = useCallback(() => { + onEventClosed({ timelineId }); + + if (timelineId === TimelineId.active) { + activeTimeline.toggleExpandedEvent({ + eventId: expandedEvent.eventId!, + indexName: expandedEvent.indexName!, + }); + } + }, [timelineId, onEventClosed, expandedEvent.eventId, expandedEvent.indexName]); + useEffect(() => { setIsTimelineLoading({ id: timelineId, isLoading: isQueryLoading || loadingSourcerer }); }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]); + useEffect(() => { + if (!events || (expandedEvent.eventId && !some(['_id', expandedEvent.eventId], events))) { + handleOnEventClosed(); + } + }, [expandedEvent, handleOnEventClosed, events, combinedQueries]); + return ( <> = ({ ) : null} - {showEventDetailsColumn && ( + {showEventDetails && ( <> @@ -332,6 +341,7 @@ export const QueryTabContentComponent: React.FC = ({ browserFields={browserFields} docValueFields={docValueFields} timelineId={timelineId} + handleOnEventClosed={handleOnEventClosed} /> @@ -375,6 +385,7 @@ const makeMapStateToProps = () => { dataProviders, eventType: eventType ?? 'raw', end: input.timerange.to, + expandedEvent, filters: timelineFilter, timelineId, isLive: input.policy.kind === 'interval', @@ -404,6 +415,9 @@ const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ }) ); }, + onEventClosed: (args: ToggleExpandedEvent) => { + dispatch(timelineActions.toggleExpandedEvent(args)); + }, }); const connector = connect(makeMapStateToProps, mapDispatchToProps); @@ -420,6 +434,7 @@ const QueryTabContent = connector( prevProps.itemsPerPage === nextProps.itemsPerPage && prevProps.kqlMode === nextProps.kqlMode && prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && + prevProps.onEventClosed === nextProps.onEventClosed && prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && prevProps.showEventDetails === nextProps.showEventDetails && prevProps.status === nextProps.status && diff --git a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx index 3baab2024558f..9f5aeea695beb 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/index.tsx @@ -5,7 +5,7 @@ */ import deepEqual from 'fast-deep-equal'; -import { noop } from 'lodash/fp'; +import { isEmpty, noop } from 'lodash/fp'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -72,14 +72,6 @@ export const initSortDefault = [ }, ]; -function usePreviousRequest(value: TimelineEventsAllRequestOptions | null) { - const ref = useRef(value); - useEffect(() => { - ref.current = value; - }); - return ref.current; -} - export const useTimelineEvents = ({ docValueFields, endDate, @@ -105,7 +97,7 @@ export const useTimelineEvents = ({ const [timelineRequest, setTimelineRequest] = useState( null ); - const prevTimelineRequest = usePreviousRequest(timelineRequest); + const prevTimelineRequest = useRef(null); const clearSignalsState = useCallback(() => { if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { @@ -159,6 +151,7 @@ export const useTimelineEvents = ({ } let didCancel = false; const asyncSearch = async () => { + prevTimelineRequest.current = request; abortCtrl.current = new AbortController(); setLoading(true); const searchSubscription$ = data.search @@ -223,6 +216,7 @@ export const useTimelineEvents = ({ abortCtrl.current.abort(); setLoading(false); + prevTimelineRequest.current = activeTimeline.getRequest(); refetch.current = asyncSearch.bind(null, activeTimeline.getRequest()); setTimelineResponse((prevResp) => { const resp = activeTimeline.getResponse(); @@ -331,9 +325,35 @@ export const useTimelineEvents = ({ id !== TimelineId.active || timerangeKind === 'absolute' || !deepEqual(prevTimelineRequest, timelineRequest) - ) + ) { timelineSearch(timelineRequest); + } }, [id, prevTimelineRequest, timelineRequest, timelineSearch, timerangeKind]); + /* + cleanup timeline events response when the filters were removed completely + to avoid displaying previous query results + */ + useEffect(() => { + if (isEmpty(filterQuery)) { + setTimelineResponse({ + id, + inspect: { + dsl: [], + response: [], + }, + refetch: refetchGrid, + totalCount: -1, + pageInfo: { + activePage: 0, + querySize: 0, + }, + events: [], + loadPage: wrappedLoadPage, + updatedAt: 0, + }); + } + }, [filterQuery, id, refetchGrid, wrappedLoadPage]); + return [loading, timelineResponse]; }; diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 479c289cdd21d..b0a0a7e6abe34 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -35,9 +35,9 @@ export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventI 'ADD_NOTE_TO_EVENT' ); -interface ToggleExpandedEvent { +export interface ToggleExpandedEvent { timelineId: string; - event: TimelineExpandedEvent; + event?: TimelineExpandedEvent; } export const toggleExpandedEvent = actionCreator('TOGGLE_EXPANDED_EVENT'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 5fcbcf434d3ee..95a916a6858ef 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -80,12 +80,14 @@ describe('epicLocalStorage', () => { dataProviders: mockDataProviders, end: endDate, eventType: 'all', + expandedEvent: {}, filters: [], isLive: false, itemsPerPage: 5, itemsPerPageOptions: [5, 10, 20], kqlMode: 'search' as QueryTabContentComponentProps['kqlMode'], kqlQueryExpression: '', + onEventClosed: jest.fn(), showCallOutUnauthorizedMsg: false, showEventDetails: false, start: startDate, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts index daf57505b6baf..a92a976697eaa 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/reducer.ts @@ -177,7 +177,7 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) ...state, timelineById: addTimelineNoteToEvent({ id, noteId, eventId, timelineById: state.timelineById }), })) - .case(toggleExpandedEvent, (state, { timelineId, event }) => ({ + .case(toggleExpandedEvent, (state, { timelineId, event = {} }) => ({ ...state, timelineById: { ...state.timelineById, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts index e379caba323ca..f6386b30d112e 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts @@ -41,8 +41,6 @@ export const getTimelines = () => timelineByIdSelector; export const getTimelineByIdSelector = () => createSelector(selectTimeline, (timeline) => timeline); -export const getEventsByIdSelector = () => createSelector(selectTimeline, (timeline) => timeline); - export const getKqlFilterQuerySelector = () => createSelector(selectTimeline, (timeline) => timeline && diff --git a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts index 0a011d2bfe878..e5b70e22e90b9 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/timeline/factory/events/details/index.ts @@ -27,7 +27,7 @@ export const timelineEventsDetails: SecuritySolutionTimelineFactory ): Promise => { const { indexName, eventId, docValueFields = [] } = options; - const fieldsData = cloneDeep(response.rawResponse.hits.hits[0].fields ?? {}); + const fieldsData = cloneDeep(response.rawResponse.hits.hits[0]?.fields ?? {}); const hitsData = cloneDeep(response.rawResponse.hits.hits[0] ?? {}); delete hitsData._source; delete hitsData.fields; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8739c2f09c8fb..7845a003b59ee 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17737,7 +17737,6 @@ "xpack.securitySolution.notes.notesTitle": "メモ", "xpack.securitySolution.notes.previewMarkdownTitle": "プレビュー(マークダウン)", "xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "ユーザーまたはメモでフィルター", - "xpack.securitySolution.open.timeline.allActionsTooltip": "すべてのアクション", "xpack.securitySolution.open.timeline.batchActionsTitle": "一斉アクション", "xpack.securitySolution.open.timeline.cancelButton": "キャンセル", "xpack.securitySolution.open.timeline.collapseButton": "縮小", @@ -17945,7 +17944,6 @@ "xpack.securitySolution.timeline.autosave.warning.refresh.title": "タイムラインを更新", "xpack.securitySolution.timeline.autosave.warning.title": "更新されるまで自動保存は無効です", "xpack.securitySolution.timeline.body.actions.collapseAriaLabel": "縮小", - "xpack.securitySolution.timeline.body.actions.collapseEventTooltip": "イベントを折りたたむ", "xpack.securitySolution.timeline.body.actions.expandAriaLabel": "拡張", "xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip": "イベントを分析します", "xpack.securitySolution.timeline.body.copyToClipboardButtonLabel": "クリップボードにコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 3bd6fd13161e9..83c98b794e002 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17755,7 +17755,6 @@ "xpack.securitySolution.notes.notesTitle": "备注", "xpack.securitySolution.notes.previewMarkdownTitle": "预览 (Markdown)", "xpack.securitySolution.notes.search.FilterByUserOrNotePlaceholder": "按用户或备注筛选", - "xpack.securitySolution.open.timeline.allActionsTooltip": "所有操作", "xpack.securitySolution.open.timeline.batchActionsTitle": "批处理操作", "xpack.securitySolution.open.timeline.cancelButton": "取消", "xpack.securitySolution.open.timeline.collapseButton": "折叠", @@ -17963,7 +17962,6 @@ "xpack.securitySolution.timeline.autosave.warning.refresh.title": "刷新时间线", "xpack.securitySolution.timeline.autosave.warning.title": "刷新后才会启用自动保存", "xpack.securitySolution.timeline.body.actions.collapseAriaLabel": "折叠", - "xpack.securitySolution.timeline.body.actions.collapseEventTooltip": "折叠事件", "xpack.securitySolution.timeline.body.actions.expandAriaLabel": "展开", "xpack.securitySolution.timeline.body.actions.investigateInResolverTooltip": "分析事件", "xpack.securitySolution.timeline.body.copyToClipboardButtonLabel": "复制到剪贴板", From 97199322978847293c118e6d238958511d002ae7 Mon Sep 17 00:00:00 2001 From: Madison Caldwell Date: Sat, 12 Dec 2020 20:36:06 -0500 Subject: [PATCH 15/37] [Security Solution][Detections][Threshold Rules] Threshold rule exceptions (#85103) * Threshold rule exceptions * Clean up * Disable value lists for threshold rule exceptions * lint Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/exceptions/builder/entry_item.tsx | 4 ++-- .../timeline_actions/alert_context_menu.tsx | 11 ++--------- .../components/rules/step_about_rule/index.tsx | 5 +---- .../pages/detection_engine/rules/details/index.tsx | 4 +--- .../signals/signal_rule_alert_type.ts | 2 +- 5 files changed, 7 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx index 8b5e0555b57b4..badb29e165573 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/entry_item.tsx @@ -7,7 +7,7 @@ import React, { useCallback } from 'react'; import { EuiFormRow, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; -import { isEqlRule } from '../../../../../common/detection_engine/utils'; +import { isEqlRule, isThresholdRule } from '../../../../../common/detection_engine/utils'; import { Type } from '../../../../../common/detection_engine/schemas/common/schemas'; import { IFieldType, IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { FieldComponent } from '../../autocomplete/field'; @@ -149,7 +149,7 @@ export const BuilderEntryItem: React.FC = ({ entry, listType, entry.field != null && entry.field.type === 'boolean', - isFirst && !isEqlRule(ruleType) + isFirst && !isEqlRule(ruleType) && !isThresholdRule(ruleType) ); const comboBox = ( = ({ setOpenAddExceptionModal('detection'); }, [closePopover]); - const areExceptionsAllowed = useMemo((): boolean => { - const ruleTypes = getOr([], 'signal.rule.type', ecsRowData); - const [ruleType] = ruleTypes as Type[]; - return !isThresholdRule(ruleType); - }, [ecsRowData]); - // eslint-disable-next-line react-hooks/exhaustive-deps const addExceptionComponent = ( = ({ data-test-subj="add-exception-menu-item" id="addException" onClick={handleAddExceptionClick} - disabled={!canUserCRUD || !hasIndexWrite || !areExceptionsAllowed} + disabled={!canUserCRUD || !hasIndexWrite} > {i18n.ACTION_ADD_EXCEPTION} diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx index 65993902d4c28..6fa93f9fb4139 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx @@ -8,7 +8,6 @@ import { EuiAccordion, EuiFlexItem, EuiSpacer, EuiFormRow } from '@elastic/eui'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { isThresholdRule } from '../../../../../common/detection_engine/utils'; import { RuleStepProps, RuleStep, @@ -75,8 +74,6 @@ const StepAboutRuleComponent: FC = ({ const [severityValue, setSeverityValue] = useState(initialState.severity.value); const [indexPatternLoading, { indexPatterns }] = useFetchIndex(defineRuleData?.index ?? []); - const canUseExceptions = defineRuleData?.ruleType && !isThresholdRule(defineRuleData.ruleType); - const { form } = useForm({ defaultValue: initialState, options: { stripEmptyFields: false }, @@ -282,7 +279,7 @@ const StepAboutRuleComponent: FC = ({ idAria: 'detectionEngineStepAboutRuleAssociatedToEndpointList', 'data-test-subj': 'detectionEngineStepAboutRuleAssociatedToEndpointList', euiFieldProps: { - disabled: isLoading || !canUseExceptions, + disabled: isLoading, }, }} /> 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 62f0d12fd67b1..28c7805e968d6 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 @@ -82,7 +82,6 @@ import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; import { useFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports'; -import { isThresholdRule } from '../../../../../../common/detection_engine/utils'; import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; import { showGlobalFilters } from '../../../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../../../timelines/store/timeline'; @@ -104,7 +103,6 @@ enum RuleDetailTabs { } const getRuleDetailsTabs = (rule: Rule | null) => { - const canUseExceptions = rule && !isThresholdRule(rule.type); return [ { id: RuleDetailTabs.alerts, @@ -115,7 +113,7 @@ const getRuleDetailsTabs = (rule: Rule | null) => { { id: RuleDetailTabs.exceptions, name: i18n.EXCEPTIONS_TAB, - disabled: !canUseExceptions, + disabled: false, dataTestSubj: 'exceptionsTab', }, { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 6be4a83d237a5..8d4dd877996db 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -338,7 +338,7 @@ export const signalRulesAlertType = ({ must: [ { term: { - [threshold.field ?? 'signal.rule.rule_id']: bucket.key, + [threshold.field || 'signal.rule.rule_id']: bucket.key, }, }, { From 95beef7637a34396bd257e1c0ceae4cca2c28f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Kopyci=C5=84ski?= Date: Sun, 13 Dec 2020 03:25:24 +0100 Subject: [PATCH 16/37] [Security Solution] Refactor Timeline Notes to use EuiCommentList (#85256) * [Security Solution] Refactor Timeline Notes to use EuiCommentList * notes * fix types * unit tests * selector * uncomment Pinned tab * note event details * cleanup * cleanup * transparent background * don't display elastic as an owner when note is created * review + bugs fixed found Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../timeline_actions/add_to_case_action.tsx | 2 +- .../common/components/charts/barchart.tsx | 10 +- .../__snapshots__/json_view.test.tsx.snap | 26 +- .../event_details/event_fields_browser.tsx | 2 +- .../components/event_details/json_view.tsx | 36 +- .../components/event_details/summary_view.tsx | 15 +- .../events_viewer/event_details_flyout.tsx | 6 +- .../events_viewer/events_viewer.tsx | 12 +- .../public/common/components/links/index.tsx | 28 +- .../public/common/components/top_n/top_n.tsx | 8 +- .../hooks/use_timeline_events_count.tsx | 27 + .../public/common/lib/note/index.ts | 2 + .../public/common/store/app/selectors.ts | 16 +- .../public/common/store/inputs/selectors.ts | 16 +- .../common/store/sourcerer/selectors.ts | 15 +- .../alerts_histogram_panel/helpers.tsx | 4 +- .../investigate_in_timeline_action.tsx | 1 - .../alerts_by_category/index.test.tsx | 4 + .../components/alerts_by_category/index.tsx | 10 +- .../components/event_counts/index.test.tsx | 5 + .../components/event_counts/index.tsx | 51 +- .../components/events_by_dataset/index.tsx | 10 +- .../components/overview_host/index.tsx | 51 +- .../components/overview_network/index.tsx | 51 +- .../components/signals_by_category/index.tsx | 10 +- .../public/overview/pages/overview.tsx | 11 +- .../flyout/header/active_timelines.tsx | 19 +- .../components/flyout/header/index.tsx | 4 +- .../components/flyout/header/translations.ts | 4 + .../timelines/components/notes/columns.tsx | 44 - .../timelines/components/notes/index.tsx | 119 --- .../note_card_body.test.tsx.snap | 759 ------------------ .../components/notes/note_card/index.test.tsx | 40 - .../components/notes/note_card/index.tsx | 29 - .../notes/note_card/note_card_body.test.tsx | 38 - .../notes/note_card/note_card_body.tsx | 41 - .../notes/note_card/note_card_header.test.tsx | 53 -- .../notes/note_card/note_card_header.tsx | 51 -- .../notes/note_card/note_created.test.tsx | 30 - .../notes/note_card/note_created.tsx | 27 - .../notes/note_cards/index.test.tsx | 11 +- .../components/notes/note_cards/index.tsx | 48 +- .../components/notes/translations.ts | 4 + .../components/open_timeline/helpers.test.ts | 2 + .../components/open_timeline/helpers.ts | 2 + .../components/open_timeline/index.test.tsx | 12 +- .../note_previews/index.test.tsx | 6 +- .../open_timeline/note_previews/index.tsx | 103 ++- .../note_previews/note_preview.test.tsx | 154 ---- .../note_previews/note_preview.tsx | 69 -- .../note_previews/translations.ts | 14 + .../components/open_timeline/types.ts | 2 + .../body/actions/action_icon_item.tsx | 4 +- .../body/actions/add_note_icon_item.tsx | 16 +- .../timeline/body/column_headers/index.tsx | 1 + .../body/events/event_column_view.test.tsx | 21 +- .../body/events/event_column_view.tsx | 18 +- .../timeline/body/events/stateful_event.tsx | 1 - .../components/timeline/body/helpers.tsx | 1 - .../timeline/date_picker_lock/translations.ts | 10 +- .../timeline/expandable_event/index.tsx | 4 +- .../timeline/notes_tab_content/index.tsx | 158 +++- .../timeline/properties/helpers.tsx | 124 +-- .../components/timeline/properties/styles.tsx | 12 +- .../properties/use_create_timeline.test.tsx | 3 + .../properties/use_create_timeline.tsx | 2 + .../timeline/query_tab_content/index.tsx | 24 +- .../timelines/components/timeline/styles.tsx | 2 +- .../timeline/tabs_content/index.tsx | 54 +- .../timeline/tabs_content/selectors.ts | 3 + .../public/timelines/store/timeline/model.ts | 2 + 71 files changed, 647 insertions(+), 1927 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/hooks/use_timeline_events_count.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/columns.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_body.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_body.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.tsx create mode 100644 x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx index 7466d34a9938f..8ec5133ef48b0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/timeline_actions/add_to_case_action.tsx @@ -134,7 +134,7 @@ const AddToCaseActionComponent: React.FC = ({ ecsRowData, return ( <> - + = ({ ); }; -export const BarChart = React.memo(BarChartComponent); +export const BarChart = React.memo( + BarChartComponent, + (prevProps, nextProps) => + prevProps.stackByField === nextProps.stackByField && + prevProps.timelineId === nextProps.timelineId && + deepEqual(prevProps.configs, nextProps.configs) && + deepEqual(prevProps.barChart, nextProps.barChart) +); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap index 15f00bbf72cf1..2b681870e92fe 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap @@ -1,17 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`JSON View rendering should match snapshot 1`] = ` - + + width="100%" + /> + `; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx index 315d8b88d15e2..b494960f12fac 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_fields_browser.tsx @@ -98,7 +98,7 @@ export const EventFieldsBrowser = React.memo( } const linkFieldData = (data ?? []).find((d) => d.field === linkField); const linkFieldValue = getOr(null, 'originalValue', linkFieldData); - return linkFieldValue; + return Array.isArray(linkFieldValue) ? linkFieldValue[0] : linkFieldValue; }, [data, columnHeaders] ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx index bf548d04e780b..2944a15cbeb93 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx @@ -16,8 +16,10 @@ interface Props { data: TimelineEventsDetailsItem[]; } -const StyledEuiCodeEditor = styled(EuiCodeEditor)` - flex: 1; +const EuiCodeEditorContainer = styled.div` + .euiCodeEditorWrapper { + position: absolute; + } `; const EDITOR_SET_OPTIONS = { fontSize: '12px' }; @@ -34,19 +36,29 @@ export const JsonView = React.memo(({ data }) => { ); return ( - + + + ); }); JsonView.displayName = 'JsonView'; export const buildJsonView = (data: TimelineEventsDetailsItem[]) => - data.reduce((accumulator, item) => set(item.field, item.originalValue, accumulator), {}); + data.reduce( + (accumulator, item) => + set( + item.field, + Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue, + accumulator + ), + {} + ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx index 13d734657acce..860bf13908855 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/summary_view.tsx @@ -167,15 +167,12 @@ export const SummaryViewComponent: React.FC<{ eventId: string; timelineId: string; }> = ({ data, eventId, timelineId, browserFields }) => { - const ruleId = useMemo( - () => - getOr( - null, - 'originalValue', - data.find((d) => d.field === 'signal.rule.id') - ), - [data] - ); + const ruleId = useMemo(() => { + const item = data.find((d) => d.field === 'signal.rule.id'); + return Array.isArray(item?.originalValue) + ? item?.originalValue[0] + : item?.originalValue ?? null; + }, [data]); const { rule: maybeRule } = useRuleAsync(ruleId); const summaryList = useMemo(() => getSummary({ browserFields, data, eventId, timelineId }), [ browserFields, diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx index 6fecf0d739d1a..48bdebbc0aa4f 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/event_details_flyout.tsx @@ -39,7 +39,7 @@ const EventDetailsFlyoutComponent: React.FC = ({ const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const expandedEvent = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).expandedEvent + (state) => (getTimeline(state, timelineId) ?? timelineDefaults)?.expandedEvent ?? {} ); const handleClearSelection = useCallback(() => { @@ -48,8 +48,8 @@ const EventDetailsFlyoutComponent: React.FC = ({ const [loading, detailsData] = useTimelineEventsDetails({ docValueFields, - indexName: expandedEvent.indexName!, - eventId: expandedEvent.eventId!, + indexName: expandedEvent?.indexName ?? '', + eventId: expandedEvent?.eventId ?? '', skip: !expandedEvent.eventId, }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index 69c75bfbea56a..d6b2efbe43053 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -5,8 +5,8 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import { isEmpty, some } from 'lodash/fp'; -import React, { useEffect, useMemo, useState } from 'react'; +import { isEmpty } from 'lodash/fp'; +import React, { useEffect, useMemo, useState, useRef } from 'react'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { useDispatch } from 'react-redux'; @@ -180,6 +180,9 @@ const EventsViewerComponent: React.FC = ({ [justTitle] ); + const prevCombinedQueries = useRef<{ + filterQuery: string; + } | null>(null); const combinedQueries = combineQueries({ config: esQuery.getEsQueryConfig(kibana.services.uiSettings), dataProviders, @@ -232,10 +235,11 @@ const EventsViewerComponent: React.FC = ({ }); useEffect(() => { - if (!events || (expandedEvent.eventId && !some(['_id', expandedEvent.eventId], events))) { + if (!deepEqual(prevCombinedQueries.current, combinedQueries)) { + prevCombinedQueries.current = combinedQueries; dispatch(timelineActions.toggleExpandedEvent({ timelineId: id })); } - }, [dispatch, events, expandedEvent, id]); + }, [combinedQueries, dispatch, id]); const totalCountMinusDeleted = useMemo( () => (totalCount > 0 ? totalCount - deletedEventIds.length : 0), diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index d6cbd31e86ddb..3964acbc9b766 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -327,6 +327,20 @@ const ReputationLinkComponent: React.FC<{ [ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit] ); + const renderCallback = useCallback( + (rowItem) => + isReputationLink(rowItem) && ( + + <>{rowItem.name ?? domain} + + ), + [allItemsLimit, domain, overflowIndexStart] + ); + return ipReputationLinks?.length > 0 ? (
    { - return ( - isReputationLink(rowItem) && ( - - <>{rowItem.name ?? domain} - - ) - ); - }} + render={renderCallback} moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} overflowIndexStart={overflowIndexStart} /> diff --git a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx index ac03e6c5c0018..fb4cd95ae36f2 100644 --- a/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx +++ b/x-pack/plugins/security_solution/public/common/components/top_n/top_n.tsx @@ -58,20 +58,17 @@ export interface Props extends Pick = ({ combinedQueries, defaultView, deleteQuery, - filters = NO_FILTERS, + filters, field, from, indexPattern, indexNames, options, - query = DEFAULT_QUERY, + query, setAbsoluteRangeDatePickerTarget, setQuery, timelineId, @@ -132,7 +129,6 @@ const TopNComponent: React.FC = ({ filters={filters} from={from} headerChildren={headerChildren} - indexPattern={indexPattern} onlyField={field} query={query} setAbsoluteRangeDatePickerTarget={setAbsoluteRangeDatePickerTarget} diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_timeline_events_count.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_timeline_events_count.tsx new file mode 100644 index 0000000000000..393c844bf5098 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_timeline_events_count.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { createPortalNode, OutPortal } from 'react-reverse-portal'; + +/** + * A singleton portal for rendering content in the global header + */ +const timelineEventsCountPortalNodeSingleton = createPortalNode(); + +export const useTimelineEventsCountPortal = () => { + const [timelineEventsCountPortalNode] = useState(timelineEventsCountPortalNodeSingleton); + + return { timelineEventsCountPortalNode }; +}; + +export const TimelineEventsCountBadge = React.memo(() => { + const { timelineEventsCountPortalNode } = useTimelineEventsCountPortal(); + + return ; +}); + +TimelineEventsCountBadge.displayName = 'TimelineEventsCountBadge'; diff --git a/x-pack/plugins/security_solution/public/common/lib/note/index.ts b/x-pack/plugins/security_solution/public/common/lib/note/index.ts index b803cade326ad..19821753a6cdc 100644 --- a/x-pack/plugins/security_solution/public/common/lib/note/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/note/index.ts @@ -8,6 +8,7 @@ export interface Note { /** When the note was created */ created: Date; + eventId?: string | null; /** Uniquely identifies the note */ id: string; /** When not `null`, this represents the last edit */ @@ -18,5 +19,6 @@ export interface Note { user: string; /** SaveObjectID for note */ saveObjectId: string | null | undefined; + timelineId?: string | null; version: string | null | undefined; } diff --git a/x-pack/plugins/security_solution/public/common/store/app/selectors.ts b/x-pack/plugins/security_solution/public/common/store/app/selectors.ts index 59d783107e587..9808bbb1faed3 100644 --- a/x-pack/plugins/security_solution/public/common/store/app/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/app/selectors.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { keys } from 'lodash/fp'; +import { keys, values } from 'lodash/fp'; import memoizeOne from 'memoize-one'; import { createSelector } from 'reselect'; import { Note } from '../../lib/note'; import { ErrorModel, NotesById } from './model'; import { State } from '../types'; +import { TimelineResultNote } from '../../../timelines/components/open_timeline/types'; const selectNotesById = (state: State): NotesById => state.app.notesById; @@ -25,6 +26,16 @@ export const getNotes = memoizeOne((notesById: NotesById, noteIds: string[]): No }, []) ); +export const getNotesAsCommentsList = (notesById: NotesById): TimelineResultNote[] => + values(notesById).map((note) => ({ + eventId: note.eventId, + savedObjectId: note.saveObjectId, + note: note.note, + noteId: note.id, + updated: (note.lastEdit ?? note.created).getTime(), + updatedBy: note.user, + })); + export const selectNotesByIdSelector = createSelector( selectNotesById, (notesById: NotesById) => notesById @@ -33,4 +44,7 @@ export const selectNotesByIdSelector = createSelector( export const notesByIdsSelector = () => createSelector(selectNotesById, (notesById: NotesById) => notesById); +export const selectNotesAsCommentsListSelector = () => + createSelector(selectNotesById, getNotesAsCommentsList); + export const errorsSelector = () => createSelector(getErrors, (errors) => errors); diff --git a/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts b/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts index 9feb2f87d7e08..47a63ec843073 100644 --- a/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts @@ -6,6 +6,7 @@ import { createSelector } from 'reselect'; +import { Filter, Query } from '../../../../../../../src/plugins/data/public'; import { State } from '../types'; import { InputsModel, InputsRange, GlobalQuery } from './model'; @@ -64,21 +65,18 @@ export const timelineQueryByIdSelector = () => export const globalSelector = () => createSelector(selectGlobal, (global) => global); +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + export const globalQuerySelector = () => - createSelector( - selectGlobal, - (global) => - global.query || { - query: '', - language: 'kuery', - } - ); + createSelector(selectGlobal, (global) => global.query || DEFAULT_QUERY); export const globalSavedQuerySelector = () => createSelector(selectGlobal, (global) => global.savedQuery || null); +const NO_FILTERS: Filter[] = []; + export const globalFiltersQuerySelector = () => - createSelector(selectGlobal, (global) => global.filters || []); + createSelector(selectGlobal, (global) => global.filters || NO_FILTERS); export const getTimelineSelector = () => createSelector(selectTimeline, (timeline) => timeline); diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts index 599cddb605148..88694c66bf960 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/selectors.ts @@ -91,16 +91,23 @@ export const getSourcererScopeSelector = () => { : selectedPatterns; }); + const getIndexPattern = memoizeOne( + (indexPattern, title) => ({ + ...indexPattern, + title, + }), + (newArgs, lastArgs) => newArgs[0] === lastArgs[0] && newArgs[1].length === lastArgs[1].length + ); + const mapStateToProps = (state: State, scopeId: SourcererScopeName): ManageScope => { const scope = getScopeIdSelector(state, scopeId); const selectedPatterns = getSelectedPatterns(scope.selectedPatterns.sort().join()); + const indexPattern = getIndexPattern(scope.indexPattern, selectedPatterns.join()); + return { ...scope, selectedPatterns, - indexPattern: { - ...scope.indexPattern, - title: selectedPatterns.join(), - }, + indexPattern, }; }; return mapStateToProps; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx index bb8cc2267249f..e2ab339fbaa83 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_histogram_panel/helpers.tsx @@ -10,6 +10,8 @@ import { HistogramData, AlertsAggregation, AlertsBucket, AlertsGroupBucket } fro import { AlertSearchResponse } from '../../containers/detection_engine/alerts/types'; import * as i18n from './translations'; +const EMPTY_ALERTS_DATA: HistogramData[] = []; + export const formatAlertsData = (alertsData: AlertSearchResponse<{}, AlertsAggregation> | null) => { const groupBuckets: AlertsGroupBucket[] = alertsData?.aggregations?.alertsByGrouping?.buckets ?? []; @@ -25,7 +27,7 @@ export const formatAlertsData = (alertsData: AlertSearchResponse<{}, AlertsAggre g: group, })), ]; - }, []); + }, EMPTY_ALERTS_DATA); }; export const getAlertsHistogramQuery = ( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx index 8960b7a76660b..d7306e26d3cfe 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/investigate_in_timeline_action.tsx @@ -89,7 +89,6 @@ const InvestigateInTimelineActionComponent: React.FC diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index 704506d9813d9..50b5ae9388fe5 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -37,6 +37,10 @@ describe('Alerts by category', () => { indexPattern: mockIndexPattern, setQuery: jest.fn(), to, + query: { + query: '', + language: 'kuery', + }, }; describe('before loading data', () => { beforeAll(async () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx index 4ab72afc3fb45..a58b5cf315ec1 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx @@ -35,26 +35,24 @@ import { LinkButton } from '../../../common/components/links'; const ID = 'alertsByCategoryOverview'; -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const DEFAULT_STACK_BY = 'event.module'; interface Props extends Pick { - filters?: Filter[]; + filters: Filter[]; hideHeaderChildren?: boolean; indexPattern: IIndexPattern; indexNames: string[]; - query?: Query; + query: Query; } const AlertsByCategoryComponent: React.FC = ({ deleteQuery, - filters = NO_FILTERS, + filters, from, hideHeaderChildren = false, indexPattern, indexNames, - query = DEFAULT_QUERY, + query, setQuery, to, }) => { diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx index 44cb7a65dbc5e..7e96ab8779304 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.test.tsx @@ -21,11 +21,16 @@ describe('EventCounts', () => { const to = '2020-01-21T20:49:57.080Z'; const testProps = { + filters: [], from, indexNames: [], indexPattern: mockIndexPattern, setQuery: jest.fn(), to, + query: { + query: '', + language: 'kuery', + }, }; test('it filters the `Host events` widget with a `host.name` `exists` filter', () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx index 6e47de68221c7..af3c7ecf1f36d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx @@ -5,7 +5,7 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import { OverviewHost } from '../overview_host'; @@ -26,38 +26,52 @@ const HorizontalSpacer = styled(EuiFlexItem)` width: 24px; `; -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; - interface Props extends Pick { - filters?: Filter[]; + filters: Filter[]; indexNames: string[]; indexPattern: IIndexPattern; - query?: Query; + query: Query; } const EventCountsComponent: React.FC = ({ - filters = NO_FILTERS, + filters, from, indexNames, indexPattern, - query = DEFAULT_QUERY, + query, setQuery, to, }) => { - const kibana = useKibana(); + const { uiSettings } = useKibana().services; + + const hostFilterQuery = useMemo( + () => + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters: [...filters, ...filterHostData], + }), + [filters, indexPattern, query, uiSettings] + ); + + const networkFilterQuery = useMemo( + () => + convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(uiSettings), + indexPattern, + queries: [query], + filters: [...filters, ...filterNetworkData], + }), + [filters, indexPattern, uiSettings, query] + ); return ( = ({ { combinedQueries?: string; - filters?: Filter[]; + filters: Filter[]; headerChildren?: React.ReactNode; indexPattern: IIndexPattern; indexNames: string[]; onlyField?: string; - query?: Query; + query: Query; setAbsoluteRangeDatePickerTarget?: InputsModelId; showSpacer?: boolean; timelineId?: string; @@ -63,13 +61,13 @@ const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ const EventsByDatasetComponent: React.FC = ({ combinedQueries, deleteQuery, - filters = NO_FILTERS, + filters, from, headerChildren, indexPattern, indexNames, onlyField, - query = DEFAULT_QUERY, + query, setAbsoluteRangeDatePickerTarget, setQuery, showSpacer = true, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx index f92f004bd2448..a74d7af7140b7 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.tsx @@ -84,33 +84,38 @@ const OverviewHostComponent: React.FC = ({ [goToHost, formatUrl] ); + const title = useMemo( + () => ( + + ), + [] + ); + + const subtitle = useMemo( + () => + !isEmpty(overviewHost) ? ( + + ) : ( + <>{''} + ), + [formattedHostEventsCount, hostEventsCount, overviewHost] + ); + return ( - - ) : ( - <>{''} - ) - } - title={ - - } - > + <>{hostPageButton} diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx index 178a752d1286f..fd4b7bbd386ba 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.tsx @@ -89,34 +89,39 @@ const OverviewNetworkComponent: React.FC = ({ [goToNetwork, formatUrl] ); + const title = useMemo( + () => ( + + ), + [] + ); + + const subtitle = useMemo( + () => + !isEmpty(overviewNetwork) ? ( + + ) : ( + <>{''} + ), + [formattedNetworkEventsCount, networkEventsCount, overviewNetwork] + ); + return ( <> - - ) : ( - <>{''} - ) - } - title={ - - } - > + {networkPageButton} diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index 34722fd147a99..432ad0642be9d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -11,19 +11,15 @@ import { AlertsHistogramPanel } from '../../../detections/components/alerts_hist import { alertsHistogramOptions } from '../../../detections/components/alerts_histogram_panel/config'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; -import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { Filter, Query } from '../../../../../../../src/plugins/data/public'; import { InputsModelId } from '../../../common/store/inputs/constants'; import * as i18n from '../../pages/translations'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const NO_FILTERS: Filter[] = []; - interface Props extends Pick { filters?: Filter[]; headerChildren?: React.ReactNode; - indexPattern: IIndexPattern; /** Override all defaults, and only display this field */ onlyField?: string; query?: Query; @@ -33,11 +29,11 @@ interface Props extends Pick = ({ deleteQuery, - filters = NO_FILTERS, + filters, from, headerChildren, onlyField, - query = DEFAULT_QUERY, + query, setAbsoluteRangeDatePickerTarget = 'global', setQuery, timelineId, diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 0f34734ebf861..2e1a8d3a6e376 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -6,7 +6,6 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import React, { useCallback, useState, useMemo } from 'react'; -import { Query, Filter } from 'src/plugins/data/public'; import styled from 'styled-components'; import { AlertsByCategory } from '../components/alerts_by_category'; @@ -33,9 +32,6 @@ import { Sourcerer } from '../../common/components/sourcerer'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const NO_FILTERS: Filter[] = []; - const SidebarFlexItem = styled(EuiFlexItem)` margin-right: 24px; `; @@ -46,10 +42,8 @@ const OverviewComponent = () => { [] ); const getGlobalQuerySelector = useMemo(() => inputsSelectors.globalQuerySelector(), []); - const query = useDeepEqualSelector((state) => getGlobalQuerySelector(state) ?? DEFAULT_QUERY); - const filters = useDeepEqualSelector( - (state) => getGlobalFiltersQuerySelector(state) ?? NO_FILTERS - ); + const query = useDeepEqualSelector(getGlobalQuerySelector); + const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); const { from, deleteQuery, setQuery, to } = useGlobalTime(); const { indicesExist, indexPattern, selectedPatterns } = useSourcererScope(); @@ -97,7 +91,6 @@ const OverviewComponent = () => { span { + padding: 0; + + > span { + display: flex; + flex-direction: row; + } + } +`; + const ActiveTimelinesComponent: React.FC = ({ timelineId, timelineType, @@ -46,16 +58,17 @@ const ActiveTimelinesComponent: React.FC = ({ : UNTITLED_TIMELINE; return ( - + - {title} - + {!isOpen && } + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index e09eedcd34dd1..368cb53eccc34 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -175,7 +175,7 @@ const TimelineStatusInfoComponent: React.FC = ({ timelineId } return ( - {'Unsaved'} + {i18n.UNSAVED} ); @@ -198,7 +198,7 @@ const TimelineStatusInfoComponent: React.FC = ({ timelineId } const TimelineStatusInfo = React.memo(TimelineStatusInfoComponent); const FlyoutHeaderComponent: React.FC = ({ timelineId }) => ( - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts index ef9b88d65c551..2633faf4e3e43 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/translations.ts @@ -13,6 +13,10 @@ export const CLOSE_TIMELINE = i18n.translate( } ); +export const UNSAVED = i18n.translate('xpack.securitySolution.timeline.properties.unsavedLabel', { + defaultMessage: 'Unsaved', +}); + export const AUTOSAVED = i18n.translate( 'xpack.securitySolution.timeline.properties.autosavedLabel', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/columns.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/columns.tsx deleted file mode 100644 index 32e10ac3eb77d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/columns.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable react/display-name */ - -import React from 'react'; - -import { EuiTableDataType } from '@elastic/eui'; -import { NoteCard } from './note_card'; -import * as i18n from './translations'; - -const Column = React.memo<{ text: string }>(({ text }) => {text}); -Column.displayName = 'Column'; - -interface Item { - created: Date; - note: string; - user: string; -} - -interface Column { - field: string; - dataType?: EuiTableDataType; - name: string; - sortable: boolean; - truncateText: boolean; - render: (value: string, item: Item) => JSX.Element; -} - -export const columns: Column[] = [ - { - field: 'note', - dataType: 'string', - name: i18n.NOTE, - sortable: true, - truncateText: false, - render: (_, { created, note, user }) => ( - - ), - }, -]; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx deleted file mode 100644 index 1ba573c0ac6c3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/index.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiInMemoryTable, - EuiInMemoryTableProps, - EuiModalBody, - EuiModalHeader, - EuiSpacer, -} from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; -import { useDispatch } from 'react-redux'; - -import { Note } from '../../../common/lib/note'; - -import { AddNote } from './add_note'; -import { columns } from './columns'; -import { AssociateNote, NotesCount, search } from './helpers'; -import { TimelineStatusLiteral, TimelineStatus } from '../../../../common/types/timeline'; -import { timelineActions } from '../../store/timeline'; -import { appSelectors } from '../../../common/store/app'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; - -interface Props { - associateNote: AssociateNote; - noteIds: string[]; - status: TimelineStatusLiteral; -} - -export const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( - EuiInMemoryTable as React.ComponentType> -)` - & thead { - display: none; - } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -InMemoryTable.displayName = 'InMemoryTable'; - -/** A view for entering and reviewing notes */ -export const Notes = React.memo(({ associateNote, noteIds, status }) => { - const getNotesByIds = appSelectors.notesByIdsSelector(); - const [newNote, setNewNote] = useState(''); - const isImmutable = status === TimelineStatus.immutable; - - const notesById = useDeepEqualSelector(getNotesByIds); - - const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); - - return ( - <> - - - - - - {!isImmutable && ( - - )} - - - - - ); -}); - -Notes.displayName = 'Notes'; - -interface NotesTabContentPros { - noteIds: string[]; - timelineId: string; - timelineStatus: TimelineStatusLiteral; -} - -/** A view for entering and reviewing notes */ -export const NotesTabContent = React.memo( - ({ noteIds, timelineStatus, timelineId }) => { - const dispatch = useDispatch(); - const getNotesByIds = appSelectors.notesByIdsSelector(); - const [newNote, setNewNote] = useState(''); - const isImmutable = timelineStatus === TimelineStatus.immutable; - const notesById = useDeepEqualSelector(getNotesByIds); - - const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); - - const associateNote = useCallback( - (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - [dispatch, timelineId] - ); - - return ( - <> - - - {!isImmutable && ( - - )} - - ); - } -); - -NotesTabContent.displayName = 'NotesTabContent'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap deleted file mode 100644 index 58cf0ae1e9f8f..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap +++ /dev/null @@ -1,759 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NoteCardBody renders correctly against snapshot 1`] = ` - - - -`; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.test.tsx deleted file mode 100644 index 161671ed730f3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import '../../../../common/mock/formatted_relative'; - -import { NoteCard } from '.'; - -describe('NoteCard', () => { - const created = new Date(); - const rawNote = 'noteworthy'; - const user = 'elastic'; - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - - test('it renders a note card header', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="note-card-header"]').exists()).toEqual(true); - }); - - test('it renders a note card body', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="note-card-body"]').exists()).toEqual(true); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.tsx deleted file mode 100644 index e02ebc2a25fd0..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiPanel } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { NoteCardBody } from './note_card_body'; -import { NoteCardHeader } from './note_card_header'; - -const NoteCardContainer = styled(EuiPanel)` - width: 100%; -`; - -NoteCardContainer.displayName = 'NoteCardContainer'; - -export const NoteCard = React.memo<{ created: Date; rawNote: string; user: string }>( - ({ created, rawNote, user }) => ( - - - - - ) -); - -NoteCard.displayName = 'NoteCard'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_body.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_body.test.tsx deleted file mode 100644 index 77f1375b7a3c0..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_body.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount, shallow } from 'enzyme'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; - -import { NoteCardBody } from './note_card_body'; - -describe('NoteCardBody', () => { - const markdownHeaderPrefix = '# '; // translates to an h1 in markdown - const noteText = 'This is a note'; - const rawNote = `${markdownHeaderPrefix} ${noteText}`; - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the text of the note in an h1', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('h1').first().text()).toEqual(noteText); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_body.tsx deleted file mode 100644 index efda3737cd177..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_body.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiPanel, EuiToolTip } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import styled from 'styled-components'; - -import { WithCopyToClipboard } from '../../../../common/lib/clipboard/with_copy_to_clipboard'; -import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; -import { WithHoverActions } from '../../../../common/components/with_hover_actions'; -import * as i18n from '../translations'; - -const BodyContainer = styled(EuiPanel)` - border: none; -`; - -BodyContainer.displayName = 'BodyContainer'; - -export const NoteCardBody = React.memo<{ rawNote: string }>(({ rawNote }) => { - const hoverContent = useMemo( - () => ( - - - - ), - [rawNote] - ); - - const render = useCallback(() => {rawNote}, [rawNote]); - - return ( - - - - ); -}); - -NoteCardBody.displayName = 'NoteCardBody'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.test.tsx deleted file mode 100644 index 4fbb7ce3f46eb..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; -import '../../../../common/mock/formatted_relative'; - -import * as i18n from '../translations'; - -import { NoteCardHeader } from './note_card_header'; - -describe('NoteCardHeader', () => { - beforeEach(() => { - moment.tz.setDefault('UTC'); - }); - afterEach(() => { - moment.tz.setDefault('Browser'); - }); - - moment.locale('en'); - - const date = moment('2019-02-19 06:21:00'); - - const user = 'elastic'; - - test('it renders an avatar containing the first letter of the username', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="avatar"]').first().text()).toEqual(user[0]); - }); - - test('it renders the username', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="user"]').first().text()).toEqual(user); - }); - - test('it renders the expected action', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="action"]').first().text()).toEqual(i18n.ADDED_A_NOTE); - }); - - test('it renders a humanized date when the note was created', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.exists()).toEqual(true); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.tsx deleted file mode 100644 index e6aa0542df4b3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_card_header.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiAvatar, EuiPanel } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import * as i18n from '../translations'; - -import { NoteCreated } from './note_created'; - -const Action = styled.span` - margin-right: 5px; -`; - -Action.displayName = 'Action'; - -const Avatar = styled(EuiAvatar)` - margin-right: 5px; -`; - -Avatar.displayName = 'Avatar'; - -const HeaderContainer = styled.div` - align-items: center; - display: flex; - user-select: none; -`; - -HeaderContainer.displayName = 'HeaderContainer'; - -const User = styled.span` - font-weight: 700; - margin: 5px; -`; - -export const NoteCardHeader = React.memo<{ created: Date; user: string }>(({ created, user }) => ( - - - - {user} - {i18n.ADDED_A_NOTE} - - - -)); - -NoteCardHeader.displayName = 'NoteCardHeader'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.test.tsx deleted file mode 100644 index 92d334a059ae9..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.test.tsx +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment-timezone'; -import React from 'react'; -import { mountWithIntl } from '@kbn/test/jest'; -import '../../../../common/mock/formatted_relative'; - -import { NoteCreated } from './note_created'; - -describe('NoteCreated', () => { - beforeEach(() => { - moment.tz.setDefault('UTC'); - }); - afterEach(() => { - moment.tz.setDefault('Browser'); - }); - - moment.locale('en'); - const date = moment('2019-02-19 06:21:00'); - - test('it renders a humanized date when the note was created', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-created"]').first().exists()).toEqual(true); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.tsx deleted file mode 100644 index dc97373660bd1..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_card/note_created.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FormattedRelative } from '@kbn/i18n/react'; -import React from 'react'; -import styled from 'styled-components'; - -import { LocalizedDateTooltip } from '../../../../common/components/localized_date_tooltip'; - -const NoteCreatedContainer = styled.span` - user-select: none; -`; - -NoteCreatedContainer.displayName = 'NoteCreatedContainer'; - -export const NoteCreated = React.memo<{ created: Date }>(({ created }) => ( - - - - - -)); - -NoteCreated.displayName = 'NoteCreated'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx index 8fd95feba6031..724f49e9bd481 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.test.tsx @@ -77,14 +77,9 @@ describe('NoteCards', () => { ); - expect( - wrapper - .find('[data-test-subj="note-card"]') - .find('[data-test-subj="note-card-body"]') - .find('.euiMarkdownFormat') - .first() - .text() - ).toEqual(getNotesByIds().abc.note); + expect(wrapper.find('.euiCommentEvent__body .euiMarkdownFormat').first().text()).toEqual( + getNotesByIds().abc.note + ); }); test('it shows controls for adding notes when showAddNote is true', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx index 4ce4de1851863..6c3fd2b50ae6a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/note_cards/index.tsx @@ -12,7 +12,8 @@ import { appSelectors } from '../../../../common/store'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { AddNote } from '../add_note'; import { AssociateNote } from '../helpers'; -import { NoteCard } from '../note_card'; +import { NotePreviews, NotePreviewsContainer } from '../../open_timeline/note_previews'; +import { TimelineResultNote } from '../../open_timeline/types'; const AddNoteContainer = styled.div``; AddNoteContainer.displayName = 'AddNoteContainer'; @@ -22,23 +23,17 @@ const NoteContainer = styled.div` `; NoteContainer.displayName = 'NoteContainer'; -interface NoteCardsCompProps { - children: React.ReactNode; -} const NoteCardsCompContainer = styled(EuiPanel)` border: none; background-color: transparent; box-shadow: none; + + &.euiPanel--plain { + background-color: transparent; + } `; NoteCardsCompContainer.displayName = 'NoteCardsCompContainer'; -const NoteCardsComp = React.memo(({ children }) => ( - - {children} - -)); -NoteCardsComp.displayName = 'NoteCardsComp'; - const NotesContainer = styled(EuiFlexGroup)` margin-bottom: 5px; `; @@ -56,7 +51,6 @@ export const NoteCards = React.memo( ({ associateNote, noteIds, showAddNote, toggleShowAddNote }) => { const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); const notesById = useDeepEqualSelector(getNotesByIds); - const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); const [newNote, setNewNote] = useState(''); const associateNoteAndToggleShow = useCallback( @@ -67,16 +61,26 @@ export const NoteCards = React.memo( [associateNote, toggleShowAddNote] ); + const notes: TimelineResultNote[] = useMemo( + () => + appSelectors.getNotes(notesById, noteIds).map((note) => ({ + savedObjectId: note.saveObjectId, + note: note.note, + noteId: note.id, + updated: (note.lastEdit ?? note.created).getTime(), + updatedBy: note.user, + })), + [notesById, noteIds] + ); + return ( - - {noteIds.length ? ( - - {items.map((note) => ( - - - - ))} - + + {notes.length ? ( + + + + + ) : null} {showAddNote ? ( @@ -89,7 +93,7 @@ export const NoteCards = React.memo( /> ) : null} - + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/notes/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/notes/translations.ts index 4827481c7c5f3..e92b918a525d0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/notes/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/notes/translations.ts @@ -50,3 +50,7 @@ export const COPY_TO_CLIPBOARD = i18n.translate( defaultMessage: 'Copy to Clipboard', } ); + +export const CREATED_BY = i18n.translate('xpack.securitySolution.notes.createdByLabel', { + defaultMessage: 'Created by', +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 6c76da44c8557..61b0c004dcb9d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -1502,11 +1502,13 @@ describe('helpers', () => { notes: [ { created: new Date('2020-03-26T14:35:56.356Z'), + eventId: null, id: 'note-id', lastEdit: new Date('2020-03-26T14:35:56.356Z'), note: 'I am a note', user: 'unknown', saveObjectId: 'note-id', + timelineId: null, version: undefined, }, ], diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index 76eb9196e8c5c..df12194e264de 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -462,6 +462,8 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli user: note.updatedBy || 'unknown', saveObjectId: note.noteId, version: note.version, + eventId: note.eventId ?? null, + timelineId: note.timelineId ?? null, })) : [], }) diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index 9ca5d0c7b438a..ffff6af3f1351 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -568,16 +568,8 @@ describe('StatefulOpenTimeline', () => { wrapper.update(); wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true); - expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true); - - expect( - wrapper - .find('[data-test-subj="note-previews-container"]') - .find('[data-test-subj="updated-by"]') - .first() - .text() - ).toEqual('elastic'); + expect(wrapper.find('.euiCommentEvent__headerUsername').exists()).toEqual(true); + expect(wrapper.find('.euiCommentEvent__headerUsername').first().text()).toEqual('elastic'); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx index d791e6ebe4366..18e276a0914b4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -104,7 +104,7 @@ describe('NotePreviews', () => { ); - expect(wrapper.find(`[data-test-subj="updated-by"]`).at(2).text()).toEqual('bob'); + expect(wrapper.find('.euiCommentEvent__headerUsername').at(1).text()).toEqual('bob'); }); test('it filters-out null savedObjectIds', () => { @@ -135,7 +135,7 @@ describe('NotePreviews', () => { ); - expect(wrapper.find(`[data-test-subj="updated-by"]`).at(2).text()).toEqual('bob'); + expect(wrapper.find(`.euiCommentEvent__headerUsername`).at(2).text()).toEqual('bob'); }); test('it filters-out undefined savedObjectIds', () => { @@ -165,6 +165,6 @@ describe('NotePreviews', () => { ); - expect(wrapper.find(`[data-test-subj="updated-by"]`).at(2).text()).toEqual('bob'); + expect(wrapper.find(`.euiCommentEvent__headerUsername`).at(2).text()).toEqual('bob'); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx index 8c804dbe4b70d..7efa16d8168e7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/index.tsx @@ -5,46 +5,101 @@ */ import { uniqBy } from 'lodash/fp'; -import React from 'react'; +import { EuiAvatar, EuiButtonIcon, EuiCommentList } from '@elastic/eui'; +import { FormattedRelative } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; +import { useDispatch } from 'react-redux'; -import { NotePreview } from './note_preview'; import { TimelineResultNote } from '../types'; +import { getEmptyValue, defaultToEmptyTag } from '../../../../common/components/empty_value'; +import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; +import { timelineActions } from '../../../store/timeline'; +import * as i18n from './translations'; -const NotePreviewsContainer = styled.section` - padding: ${(props) => - `${props.theme.eui.euiSizeS} 0 ${props.theme.eui.euiSizeS} ${props.theme.eui.euiSizeXXL}`}; +export const NotePreviewsContainer = styled.section` + padding-top: ${({ theme }) => `${theme.eui.euiSizeS}`}; `; NotePreviewsContainer.displayName = 'NotePreviewsContainer'; +interface ToggleEventDetailsButtonProps { + eventId: string; + timelineId: string; +} + +const ToggleEventDetailsButtonComponent: React.FC = ({ + eventId, + timelineId, +}) => { + const dispatch = useDispatch(); + const handleClick = useCallback(() => { + dispatch( + timelineActions.toggleExpandedEvent({ + timelineId, + event: { + eventId, + // we don't store yet info about event index name in note + indexName: '', + }, + }) + ); + }, [dispatch, eventId, timelineId]); + + return ( + + ); +}; + +const ToggleEventDetailsButton = React.memo(ToggleEventDetailsButtonComponent); /** * Renders a preview of a note in the All / Open Timelines table */ -export const NotePreviews = React.memo<{ + +interface NotePreviewsProps { notes?: TimelineResultNote[] | null; -}>(({ notes }) => { + timelineId?: string; +} + +export const NotePreviews = React.memo(({ notes, timelineId }) => { + const notesList = useMemo( + () => + uniqBy('savedObjectId', notes).map((note) => ({ + 'data-test-subj': `note-preview-${note.savedObjectId}`, + username: defaultToEmptyTag(note.updatedBy), + event: 'added a comment', + timestamp: note.updated ? ( + + ) : ( + getEmptyValue() + ), + children: {note.note ?? ''}, + actions: + note.eventId && timelineId ? ( + + ) : null, + timelineIcon: ( + + ), + })), + [notes, timelineId] + ); + if (notes == null || notes.length === 0) { return null; } - const uniqueNotes = uniqBy('savedObjectId', notes); - - return ( - - {uniqueNotes.map(({ note, savedObjectId, updated, updatedBy }) => - savedObjectId != null ? ( - - ) : null - )} - - ); + return ; }); NotePreviews.displayName = 'NotePreviews'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx deleted file mode 100644 index 484b3e5a60015..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mountWithIntl } from '@kbn/test/jest'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import '../../../../common/mock/formatted_relative'; - -import { getEmptyValue } from '../../../../common/components/empty_value'; -import { NotePreview } from './note_preview'; - -import * as i18n from '../translations'; - -describe('NotePreview', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - - describe('Avatar', () => { - test('it renders an avatar with the expected initials when updatedBy is provided', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="avatar"]').first().text()).toEqual('a'); - }); - - test('it renders an avatar with a "?" when updatedBy is undefined', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="avatar"]').first().text()).toEqual('?'); - }); - - test('it renders an avatar with a "?" when updatedBy is null', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="avatar"]').first().text()).toEqual('?'); - }); - }); - - describe('UpdatedBy', () => { - test('it renders the username when updatedBy is provided', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="updated-by"]').first().text()).toEqual('admin'); - }); - - test('it renders placeholder text when updatedBy is undefined', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="updated-by"]').first().text()).toEqual(getEmptyValue()); - }); - - test('it renders placeholder text when updatedBy is null', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="updated-by"]').first().text()).toEqual(getEmptyValue()); - }); - }); - - describe('Updated', () => { - const updated = 1553300753 * 1000; - - test('it is always prefixed by "Posted:"', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="posted"]').first().text().startsWith(i18n.POSTED)).toBe( - true - ); - }); - - test('it renders the relative date when updated is provided', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="updated"]').first().exists()).toBe(true); - }); - - test('it does NOT render the relative date when updated is undefined', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="updated"]').first().exists()).toBe(false); - }); - - test('it does NOT render the relative date when updated is null', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="updated"]').first().exists()).toBe(false); - }); - - test('it renders placeholder text when updated is undefined', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="posted"]').first().text()).toEqual( - `Posted: ${getEmptyValue()}` - ); - }); - - test('it renders placeholder text when updated is null', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="posted"]').first().text()).toEqual( - `Posted: ${getEmptyValue()}` - ); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.tsx deleted file mode 100644 index a8e7a2c465e0c..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/note_preview.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiAvatar, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle, EuiToolTip } from '@elastic/eui'; -import { FormattedRelative } from '@kbn/i18n/react'; -import React from 'react'; -import styled from 'styled-components'; - -import { getEmptyValue, defaultToEmptyTag } from '../../../../common/components/empty_value'; -import { FormattedDate } from '../../../../common/components/formatted_date'; -import { MarkdownRenderer } from '../../../../common/components/markdown_editor'; -import * as i18n from '../translations'; -import { TimelineResultNote } from '../types'; - -const NotePreviewGroup = styled.article` - & + & { - margin-top: ${(props) => props.theme.eui.euiSizeL}; - } -`; - -NotePreviewGroup.displayName = 'NotePreviewGroup'; - -const NotePreviewHeader = styled.header` - margin-bottom: ${(props) => props.theme.eui.euiSizeS}; -`; - -NotePreviewHeader.displayName = 'NotePreviewHeader'; - -/** - * Renders a preview of a note in the All / Open Timelines table - */ -export const NotePreview = React.memo>( - ({ note, updated, updatedBy }) => ( - - - - - - - - - -
    {defaultToEmptyTag(updatedBy)}
    -
    - - -

    - {i18n.POSTED}{' '} - {updated != null ? ( - }> - - - ) : ( - getEmptyValue() - )} -

    -
    -
    - {note ?? ''} -
    -
    -
    - ) -); - -NotePreview.displayName = 'NotePreview'; diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts new file mode 100644 index 0000000000000..9857e55e36570 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/note_previews/translations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const TOGGLE_EXPAND_EVENT_DETAILS = i18n.translate( + 'xpack.securitySolution.timeline.toggleEventDetailsTitle', + { + defaultMessage: 'Expand event details', + } +); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index 4e7e99a5d3e49..1556cd65ddd0a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -25,9 +25,11 @@ export interface FavoriteTimelineResult { } export interface TimelineResultNote { + eventId?: string | null; savedObjectId?: string | null; note?: string | null; noteId?: string | null; + timelineId?: string | null; updated?: number | null; updatedBy?: string | null; } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx index 12cfbbc04222f..fd4a7e91ddb79 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/action_icon_item.tsx @@ -12,7 +12,6 @@ import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; interface ActionIconItemProps { ariaLabel?: string; - id: string; width?: number; dataTestSubj?: string; content?: string; @@ -23,7 +22,6 @@ interface ActionIconItemProps { } const ActionIconItemComponent: React.FC = ({ - id, width = DEFAULT_ICON_BUTTON_WIDTH, dataTestSubj, content, @@ -33,7 +31,7 @@ const ActionIconItemComponent: React.FC = ({ onClick, children, }) => ( - + {children ?? ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx index af8045bf624c3..3f9f680ee1913 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/add_note_icon_item.tsx @@ -6,38 +6,26 @@ import React from 'react'; -import { TimelineType, TimelineStatus } from '../../../../../../common/types/timeline'; -import { AssociateNote } from '../../../notes/helpers'; +import { TimelineType } from '../../../../../../common/types/timeline'; import * as i18n from '../translations'; import { NotesButton } from '../../properties/helpers'; import { ActionIconItem } from './action_icon_item'; interface AddEventNoteActionProps { - associateNote: AssociateNote; - noteIds: string[]; showNotes: boolean; - status: TimelineStatus; timelineType: TimelineType; toggleShowNotes: () => void; } const AddEventNoteActionComponent: React.FC = ({ - associateNote, - noteIds, showNotes, - status, timelineType, toggleShowNotes, }) => ( - + + { - const origin = jest.requireActual('react-redux'); - return { - ...origin, - useSelector: jest.fn(), - }; -}); +jest.mock('../../../../../common/hooks/use_selector'); describe('EventColumnView', () => { - (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); + (useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default); const props = { id: 'event-id', @@ -82,17 +76,14 @@ describe('EventColumnView', () => { }); test('it renders correct tooltip for NotesButton - timeline template', () => { - (useSelector as jest.Mock).mockReturnValue({ - ...mockTimelineModel, - timelineType: TimelineType.template, - }); + (useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.template); const wrapper = mount(, { wrappingComponent: TestProviders }); expect(wrapper.find('[data-test-subj="add-note"]').prop('toolTip')).toEqual( i18n.NOTES_DISABLE_TOOLTIP ); - (useSelector as jest.Mock).mockReturnValue(mockTimelineModel); + (useShallowEqualSelector as jest.Mock).mockReturnValue(TimelineType.default); }); test('it does NOT render a pin button when isEventViewer is true', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx index 3297d4d613a2b..cbb7bb9d0c6a1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pick } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; -import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; +import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; import { Ecs } from '../../../../../../common/ecs'; import { TimelineNonEcsData } from '../../../../../../common/search_strategy/timeline'; import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; -import { timelineSelectors } from '../../../../store/timeline'; -import { AssociateNote } from '../../../notes/helpers'; import { OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { EventsTrData } from '../../styles'; import { Actions } from '../actions'; @@ -30,12 +27,13 @@ import { AddEventNoteAction } from '../actions/add_note_icon_item'; import { PinEventAction } from '../actions/pin_event_action'; import { inputsModel } from '../../../../../common/store'; import { TimelineId } from '../../../../../../common/types/timeline'; +import { timelineSelectors } from '../../../../store/timeline'; +import { timelineDefaults } from '../../../../store/timeline/defaults'; import { AddToCaseAction } from '../../../../../cases/components/timeline_actions/add_to_case_action'; interface Props { id: string; actionsColumnWidth: number; - associateNote: AssociateNote; columnHeaders: ColumnHeaderOptions[]; columnRenderers: ColumnRenderer[]; data: TimelineNonEcsData[]; @@ -64,7 +62,6 @@ export const EventColumnView = React.memo( ({ id, actionsColumnWidth, - associateNote, columnHeaders, columnRenderers, data, @@ -87,8 +84,8 @@ export const EventColumnView = React.memo( toggleShowNotes, }) => { const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { timelineType, status } = useDeepEqualSelector((state) => - pick(['timelineType', 'status'], getTimeline(state, timelineId)) + const timelineType = useShallowEqualSelector( + (state) => (getTimeline(state, timelineId) ?? timelineDefaults).timelineType ); const handlePinClicked = useCallback( @@ -125,11 +122,8 @@ export const EventColumnView = React.memo( ? [ , ( />, ], [ - associateNote, data, ecsData, eventIdToNoteIds, @@ -176,7 +169,6 @@ export const EventColumnView = React.memo( refetch, onRuleChange, showNotes, - status, timelineId, timelineType, toggleShowNotes, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx index baaf9aa867d90..917b4a4e7a762 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -156,7 +156,6 @@ const StatefulEventComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/translations.ts index 58729f69402e1..ecd06faed7253 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/date_picker_lock/translations.ts @@ -10,7 +10,7 @@ export const LOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate( 'xpack.securitySolution.timeline.properties.lockDatePickerTooltip', { defaultMessage: - 'Disable syncing of date/time range between the currently viewed page and your timeline', + 'Disable syncing of date/time range bteween the currently viewed page and your timeline', } ); @@ -25,27 +25,27 @@ export const UNLOCK_SYNC_MAIN_DATE_PICKER_TOOL_TIP = i18n.translate( export const LOCK_SYNC_MAIN_DATE_PICKER_LABEL = i18n.translate( 'xpack.securitySolution.timeline.properties.lockedDatePickerLabel', { - defaultMessage: 'Date picker is locked to global date picker', + defaultMessage: 'Global date picker is locked to timeline date picker', } ); export const UNLOCK_SYNC_MAIN_DATE_PICKER_LABEL = i18n.translate( 'xpack.securitySolution.timeline.properties.unlockedDatePickerLabel', { - defaultMessage: 'Date picker is NOT locked to global date picker', + defaultMessage: 'Global date picker NOT locked to timeline date picker', } ); export const LOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( 'xpack.securitySolution.timeline.properties.lockDatePickerDescription', { - defaultMessage: 'Lock date picker to global date picker', + defaultMessage: 'Lock global date picker to timeline date picker', } ); export const UNLOCK_SYNC_MAIN_DATE_PICKER_ARIA = i18n.translate( 'xpack.securitySolution.timeline.properties.unlockDatePickerDescription', { - defaultMessage: 'Unlock date picker to global date picker', + defaultMessage: 'Unlock global date picker from timeline date picker', } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx index 5c6bcbccc8e0a..df8e84b4e2a78 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/expandable_event/index.tsx @@ -81,7 +81,9 @@ export const ExpandableEvent = React.memo( | undefined; if (messageField?.originalValue) { - return messageField?.originalValue; + return Array.isArray(messageField?.originalValue) + ? messageField?.originalValue.join() + : messageField?.originalValue; } } return null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx index 9855a0124b8f5..20c528c701890 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/notes_tab_content/index.tsx @@ -4,21 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pick } from 'lodash/fp'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiPanel } from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; +import { filter, pick, uniqBy } from 'lodash/fp'; +import { + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTitle, + EuiPanel, + EuiHorizontalRule, +} from '@elastic/eui'; +import React, { Fragment, useCallback, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { useSourcererScope } from '../../../../common/containers/sourcerer'; +import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { TimelineStatus } from '../../../../../common/types/timeline'; import { appSelectors } from '../../../../common/store/app'; import { timelineDefaults } from '../../../store/timeline/defaults'; import { AddNote } from '../../notes/add_note'; -import { InMemoryTable } from '../../notes'; -import { columns } from '../../notes/columns'; -import { search } from '../../notes/helpers'; +import { CREATED_BY, NOTES } from '../../notes/translations'; +import { PARTICIPANTS } from '../../../../cases/translations'; +import { NotePreviews } from '../../open_timeline/note_previews'; +import { TimelineResultNote } from '../../open_timeline/types'; +import { EventDetails } from '../event_details'; const FullWidthFlexGroup = styled(EuiFlexGroup)` width: 100%; @@ -27,7 +40,8 @@ const FullWidthFlexGroup = styled(EuiFlexGroup)` `; const ScrollableFlexItem = styled(EuiFlexItem)` - overflow: auto; + overflow-x: hidden; + overflow-y: auto; `; const VerticalRule = styled.div` @@ -41,6 +55,66 @@ const StyledPanel = styled(EuiPanel)` box-shadow: none; `; +const StyledEuiFlexGroup = styled(EuiFlexGroup)` + flex: 0; +`; + +const Username = styled(EuiText)` + font-weight: bold; +`; + +interface UsernameWithAvatar { + username: string; +} + +const UsernameWithAvatarComponent: React.FC = ({ username }) => ( + + + + + + {username} + + +); + +const UsernameWithAvatar = React.memo(UsernameWithAvatarComponent); + +interface ParticipantsProps { + users: TimelineResultNote[]; +} + +const ParticipantsComponent: React.FC = ({ users }) => { + const List = useMemo( + () => + users.map((user) => ( + + + + + )), + [users] + ); + + if (!users.length) { + return null; + } + + return ( + <> + +

    {PARTICIPANTS}

    +
    + + {List} + + ); +}; + +ParticipantsComponent.displayName = 'ParticipantsComponent'; + +const Participants = React.memo(ParticipantsComponent); + interface NotesTabContentProps { timelineId: string; } @@ -48,37 +122,77 @@ interface NotesTabContentProps { const NotesTabContentComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { status: timelineStatus, noteIds } = useDeepEqualSelector((state) => - pick(['noteIds', 'status'], getTimeline(state, timelineId) ?? timelineDefaults) + const { createdBy, expandedEvent, status: timelineStatus } = useDeepEqualSelector((state) => + pick( + ['createdBy', 'expandedEvent', 'status'], + getTimeline(state, timelineId) ?? timelineDefaults + ) ); - const getNotesByIds = useMemo(() => appSelectors.notesByIdsSelector(), []); + const { browserFields, docValueFields } = useSourcererScope(SourcererScopeName.timeline); + + const getNotesAsCommentsList = useMemo( + () => appSelectors.selectNotesAsCommentsListSelector(), + [] + ); const [newNote, setNewNote] = useState(''); const isImmutable = timelineStatus === TimelineStatus.immutable; - const notesById = useDeepEqualSelector(getNotesByIds); + const notes: TimelineResultNote[] = useDeepEqualSelector(getNotesAsCommentsList); - const items = useMemo(() => appSelectors.getNotes(notesById, noteIds), [notesById, noteIds]); + // filter for savedObjectId to make sure we don't display `elastic` user while saving the note + const participants = useMemo(() => uniqBy('updatedBy', filter('savedObjectId', notes)), [notes]); const associateNote = useCallback( (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), [dispatch, timelineId] ); + const handleOnEventClosed = useCallback(() => { + dispatch(timelineActions.toggleExpandedEvent({ timelineId })); + }, [dispatch, timelineId]); + + const EventDetailsContent = useMemo( + () => + expandedEvent.eventId ? ( + + ) : null, + [browserFields, docValueFields, expandedEvent.eventId, handleOnEventClosed, timelineId] + ); + + const SidebarContent = useMemo( + () => ( + <> + {createdBy && ( + <> + + +

    {CREATED_BY}

    +
    + + + + + )} + + + ), + [createdBy, participants] + ); + return ( - + -

    {'Notes'}

    +

    {NOTES}

    - + {!isImmutable && ( @@ -86,7 +200,7 @@ const NotesTabContentComponent: React.FC = ({ timelineId }
    - {/* SIDEBAR PLACEHOLDER */} + {EventDetailsContent ?? SidebarContent}
    ); }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 494b3cefba6f1..673efa1857cb8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -9,11 +9,6 @@ import { EuiButton, EuiButtonIcon, EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiModal, - EuiOverlayMask, EuiToolTip, EuiTextArea, } from '@elastic/eui'; @@ -22,22 +17,14 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import { - TimelineTypeLiteral, - TimelineType, - TimelineStatusLiteral, -} from '../../../../../common/types/timeline'; +import { TimelineTypeLiteral, TimelineType } from '../../../../../common/types/timeline'; import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; import { useDeepEqualSelector, useShallowEqualSelector, } from '../../../../common/hooks/use_selector'; -import { Notes } from '../../notes'; -import { AssociateNote } from '../../notes/helpers'; - -import { NOTES_PANEL_WIDTH } from './notes_size'; -import { ButtonContainer, DescriptionContainer, LabelText, NameField, NameWrapper } from './styles'; +import { DescriptionContainer, NameField, NameWrapper } from './styles'; import * as i18n from './translations'; import { TimelineInput } from '../../../store/timeline/actions'; import { useCreateTimelineButton } from './use_create_timeline'; @@ -259,43 +246,12 @@ export const NewTimeline = React.memo( NewTimeline.displayName = 'NewTimeline'; interface NotesButtonProps { - animate?: boolean; - associateNote: AssociateNote; - noteIds: string[]; - size: 's' | 'l'; - status: TimelineStatusLiteral; showNotes: boolean; toggleShowNotes: () => void; - text?: string; toolTip?: string; timelineType: TimelineTypeLiteral; } -interface LargeNotesButtonProps { - noteIds: string[]; - text?: string; - toggleShowNotes: () => void; -} - -const LargeNotesButton = React.memo(({ noteIds, text, toggleShowNotes }) => ( - - - - - - - {text && text.length ? {text} : null} - - - - {noteIds.length} - - - - -)); -LargeNotesButton.displayName = 'LargeNotesButton'; - interface SmallNotesButtonProps { toggleShowNotes: () => void; timelineType: TimelineTypeLiteral; @@ -316,83 +272,13 @@ const SmallNotesButton = React.memo(({ toggleShowNotes, t }); SmallNotesButton.displayName = 'SmallNotesButton'; -/** - * The internal implementation of the `NotesButton` - */ -const NotesButtonComponent = React.memo( - ({ - animate = true, - associateNote, - noteIds, - showNotes, - size, - status, - toggleShowNotes, - text, - timelineType, - }) => ( - - <> - {size === 'l' ? ( - - ) : ( - - )} - {size === 'l' && showNotes ? ( - - - - - - ) : null} - - - ) -); -NotesButtonComponent.displayName = 'NotesButtonComponent'; - export const NotesButton = React.memo( - ({ - animate = true, - associateNote, - noteIds, - showNotes, - size, - status, - timelineType, - toggleShowNotes, - toolTip, - text, - }) => + ({ showNotes, timelineType, toggleShowNotes, toolTip }) => showNotes ? ( - + ) : ( - + ) ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx index 7dc5b8601955a..c1f9b18f05c60 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/styles.tsx @@ -5,12 +5,7 @@ */ import { EuiFieldText } from '@elastic/eui'; -import styled, { keyframes } from 'styled-components'; - -const fadeInEffect = keyframes` - from { opacity: 0; } - to { opacity: 1; } -`; +import styled from 'styled-components'; export const NameField = styled(EuiFieldText)` .euiToolTipAnchor { @@ -33,11 +28,6 @@ export const DescriptionContainer = styled.div` `; DescriptionContainer.displayName = 'DescriptionContainer'; -export const ButtonContainer = styled.div<{ animate: boolean }>` - animation: ${fadeInEffect} ${({ animate }) => (animate ? '0.3s' : '0s')}; -`; -ButtonContainer.displayName = 'ButtonContainer'; - export const LabelText = styled.div` margin-left: 10px; `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx index 10b505da5c76f..ddf180f7d2286 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.test.tsx @@ -145,6 +145,9 @@ describe('useCreateTimelineButton', () => { 'x-pack/security_solution/local/inputs/ADD_TIMELINE_LINK_TO' ); expect(mockDispatch.mock.calls[4][0].type).toEqual( + 'x-pack/security_solution/local/app/ADD_NOTE' + ); + expect(mockDispatch.mock.calls[5][0].type).toEqual( 'x-pack/security_solution/local/inputs/SET_RELATIVE_RANGE_DATE_PICKER' ); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index 4043ceeb85b7e..7fab0374d791d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -19,6 +19,7 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { inputsActions, inputsSelectors } from '../../../../common/store/inputs'; import { sourcererActions, sourcererSelectors } from '../../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; +import { appActions } from '../../../../common/store/app'; interface Props { timelineId?: string; @@ -57,6 +58,7 @@ export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: P ); dispatch(inputsActions.addGlobalLinkTo({ linkToId: 'timeline' })); dispatch(inputsActions.addTimelineLinkTo({ linkToId: 'global' })); + dispatch(appActions.addNotes({ notes: [] })); if (globalTimeRange.kind === 'absolute') { dispatch( inputsActions.setAbsoluteRangeDatePicker({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index aa3970bba5884..8da3c257a5db8 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -12,13 +12,15 @@ import { EuiFlyoutBody, EuiFlyoutFooter, EuiSpacer, + EuiBadge, } from '@elastic/eui'; -import { isEmpty, some } from 'lodash/fp'; -import React, { useState, useMemo, useEffect, useCallback } from 'react'; +import { isEmpty } from 'lodash/fp'; +import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import styled from 'styled-components'; import { Dispatch } from 'redux'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; +import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { Direction } from '../../../../../common/search_strategy'; @@ -42,6 +44,7 @@ import { sourcererActions } from '../../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; import { useSourcererScope } from '../../../../common/containers/sourcerer'; +import { useTimelineEventsCountPortal } from '../../../../common/hooks/use_timeline_events_count'; import { TimelineModel } from '../../../../timelines/store/timeline/model'; import { EventDetails } from '../event_details'; import { TimelineDatePickerLock } from '../date_picker_lock'; @@ -127,6 +130,10 @@ const StyledEuiTabbedContent = styled(EuiTabbedContent)` StyledEuiTabbedContent.displayName = 'StyledEuiTabbedContent'; +const EventsCountBadge = styled(EuiBadge)` + margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + const isTimerangeSame = (prevProps: Props, nextProps: Props) => prevProps.end === nextProps.end && prevProps.start === nextProps.start && @@ -160,6 +167,7 @@ export const QueryTabContentComponent: React.FC = ({ timerangeKind, updateEventTypeAndIndexesName, }) => { + const { timelineEventsCountPortalNode } = useTimelineEventsCountPortal(); const { browserFields, docValueFields, @@ -174,6 +182,10 @@ export const QueryTabContentComponent: React.FC = ({ const kqlQuery = useMemo(() => ({ query: kqlQueryExpression, language: 'kuery' }), [ kqlQueryExpression, ]); + + const prevCombinedQueries = useRef<{ + filterQuery: string; + } | null>(null); const combinedQueries = useMemo( () => combineQueries({ @@ -255,13 +267,17 @@ export const QueryTabContentComponent: React.FC = ({ }, [loadingSourcerer, timelineId, isQueryLoading, setIsTimelineLoading]); useEffect(() => { - if (!events || (expandedEvent.eventId && !some(['_id', expandedEvent.eventId], events))) { + if (!deepEqual(prevCombinedQueries.current, combinedQueries)) { + prevCombinedQueries.current = combinedQueries; handleOnEventClosed(); } - }, [expandedEvent, handleOnEventClosed, events, combinedQueries]); + }, [combinedQueries, handleOnEventClosed]); return ( <> + + {totalCount >= 0 ? {totalCount} : null} + ({ }))<{ className: string }>` font-size: ${({ theme }) => theme.eui.euiFontSizeXS}; line-height: ${({ theme }) => theme.eui.euiLineHeight}; - padding: 0 ${({ theme }) => theme.eui.paddingSizes.m}; + padding-left: ${({ theme }) => theme.eui.paddingSizes.m}; .euiAccordion + div { background-color: ${({ theme }) => theme.eui.euiColorEmptyShade}; padding: 0 ${({ theme }) => theme.eui.paddingSizes.s}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx index c9c2b1b1c2af9..7ffe661e78517 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs_content/index.tsx @@ -10,9 +10,10 @@ import { useDispatch } from 'react-redux'; import styled from 'styled-components'; import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; +import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count'; import { timelineActions } from '../../../store/timeline'; import { TimelineTabs } from '../../../store/timeline/model'; -import { getActiveTabSelector } from './selectors'; +import { getActiveTabSelector, getShowTimelineSelector } from './selectors'; import * as i18n from './translations'; const HideShowContainer = styled.div.attrs<{ $isVisible: boolean }>(({ $isVisible = false }) => ({ @@ -99,18 +100,35 @@ const ActiveTimelineTab = memo(({ activeTimelineTab, tim ActiveTimelineTab.displayName = 'ActiveTimelineTab'; +const StyledEuiTab = styled(EuiTab)` + > span { + display: flex; + flex-direction: row; + white-space: pre; + } + + :focus { + text-decoration: none; + + > span > span { + text-decoration: underline; + } + } +`; + const TabsContentComponent: React.FC = ({ timelineId, graphEventId }) => { const dispatch = useDispatch(); const getActiveTab = useMemo(() => getActiveTabSelector(), []); + const getShowTimeline = useMemo(() => getShowTimelineSelector(), []); const activeTab = useShallowEqualSelector((state) => getActiveTab(state, timelineId)); + const showTimeline = useShallowEqualSelector((state) => getShowTimeline(state, timelineId)); - const setQueryAsActiveTab = useCallback( - () => - dispatch( - timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query }) - ), - [dispatch, timelineId] - ); + const setQueryAsActiveTab = useCallback(() => { + dispatch(timelineActions.toggleExpandedEvent({ timelineId })); + dispatch( + timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.query }) + ); + }, [dispatch, timelineId]); const setGraphAsActiveTab = useCallback( () => @@ -120,13 +138,12 @@ const TabsContentComponent: React.FC = ({ timelineId, graphEve [dispatch, timelineId] ); - const setNotesAsActiveTab = useCallback( - () => - dispatch( - timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.notes }) - ), - [dispatch, timelineId] - ); + const setNotesAsActiveTab = useCallback(() => { + dispatch(timelineActions.toggleExpandedEvent({ timelineId })); + dispatch( + timelineActions.setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.notes }) + ); + }, [dispatch, timelineId]); const setPinnedAsActiveTab = useCallback( () => @@ -145,15 +162,16 @@ const TabsContentComponent: React.FC = ({ timelineId, graphEve return ( <> - - {i18n.QUERY_TAB} - + {i18n.QUERY_TAB} + {showTimeline && } + createSelector(selectTimeline, (timeline) => timeline?.activeTab ?? TimelineTabs.query); + +export const getShowTimelineSelector = () => + createSelector(selectTimeline, (timeline) => timeline?.show ?? false); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts index 9c71fabfffac5..79d7460c7e117 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/model.ts @@ -55,6 +55,8 @@ export interface TimelineModel { activeTab: TimelineTabs; /** The columns displayed in the timeline */ columns: ColumnHeaderOptions[]; + /** Timeline saved object owner */ + createdBy?: string; /** The sources of the event data shown in the timeline */ dataProviders: DataProvider[]; /** Events to not be rendered **/ From 1e8f2f66eb20a549f5b708448f851ada943789f6 Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Sun, 13 Dec 2020 10:20:29 -0500 Subject: [PATCH 17/37] [Monitoring] Some progress on making alerts better in the UI (#81569) * Some progress on making alerts better in the UI * Handle edge case * Updates * More updates * Show kibana instances alerts better * Stop showing missing nodes and improve the detail alert UI * WIP * Fix the badge display * Okay I think this is finally working * Fix type issues * Fix tests * Fix tests * Fix alert counts * Fix setup mode listing * Better detail page view of alerts * Feedback * Sorting * Fix a couple small issues * Start of unit tests * I don't think we need this Mock type * Fix types * More tests * Improve tests and fix sorting * Make this test more resilient * Updates after merging master * Fix tests * Fix types, and improve tests * PR comments * Remove nextStep logic * PR feedback * PR feedback * Removing unnecessary changes * Fixing bad merge issues * Remove unused imports * Add tooltip to alerts grouped by node * Fix up stateFilter usage * Code clean up * PR feedback * Fix state filtering in the category list * Fix types * Fix test * Fix types * Update snapshots Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/monitoring/common/constants.ts | 36 + .../common/{formatting.js => formatting.ts} | 17 +- .../plugins/monitoring/common/types/alerts.ts | 3 + .../monitoring/public/alerts/badge.tsx | 236 ++- .../monitoring/public/alerts/callout.tsx | 158 +- .../public/alerts/configuration.tsx | 166 +++ .../monitoring/public/alerts/context.ts | 16 + ...get_alert_panels_by_category.test.tsx.snap | 1287 +++++++++++++++++ .../get_alert_panels_by_node.test.tsx.snap | 660 +++++++++ .../lib/get_alert_panels_by_category.test.tsx | 212 +++ .../lib/get_alert_panels_by_category.tsx | 228 +++ .../lib/get_alert_panels_by_node.test.tsx | 128 ++ .../alerts/lib/get_alert_panels_by_node.tsx | 138 ++ .../alerts/lib/sort_by_newest_alert.test.ts | 51 + .../public/alerts/lib/sort_by_newest_alert.ts | 14 + .../monitoring/public/alerts/panel.tsx | 175 +-- .../plugins/monitoring/public/alerts/types.ts | 29 + .../components/elasticsearch/node/advanced.js | 4 +- .../components/elasticsearch/nodes/nodes.js | 1 - .../public/views/base_controller.js | 13 +- .../public/views/elasticsearch/node/index.js | 29 +- .../monitoring/server/alerts/base_alert.ts | 3 + .../alerts/cluster_health_alert.test.ts | 1 + .../server/alerts/cluster_health_alert.ts | 3 + .../server/alerts/cpu_usage_alert.ts | 7 +- .../server/alerts/disk_usage_alert.ts | 10 +- ...asticsearch_version_mismatch_alert.test.ts | 1 + .../elasticsearch_version_mismatch_alert.ts | 6 + .../kibana_version_mismatch_alert.test.ts | 1 + .../alerts/kibana_version_mismatch_alert.ts | 6 + .../alerts/license_expiration_alert.test.ts | 1 + .../server/alerts/license_expiration_alert.ts | 3 + .../logstash_version_mismatch_alert.test.ts | 1 + .../alerts/logstash_version_mismatch_alert.ts | 6 + .../server/alerts/memory_usage_alert.ts | 10 +- .../missing_monitoring_data_alert.test.ts | 4 +- .../alerts/missing_monitoring_data_alert.ts | 7 +- .../server/alerts/nodes_changed_alert.test.ts | 1 + .../server/alerts/nodes_changed_alert.ts | 3 + .../thread_pool_rejections_alert_base.ts | 2 +- .../lib/alerts/fetch_legacy_alerts.test.ts | 1 + .../server/lib/alerts/fetch_legacy_alerts.ts | 1 + .../handle_response.test.js.snap | 8 + .../nodes/get_nodes/get_nodes.js | 3 +- .../nodes/get_nodes/handle_response.js | 1 + .../server/lib/pagination/filter.js | 2 +- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../fixtures/nodes_listing_cgroup.json | 6 +- .../fixtures/nodes_listing_green.json | 2 + .../fixtures/nodes_listing_red.json | 2 + 51 files changed, 3289 insertions(+), 418 deletions(-) rename x-pack/plugins/monitoring/common/{formatting.js => formatting.ts} (65%) create mode 100644 x-pack/plugins/monitoring/public/alerts/configuration.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/context.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_category.test.tsx.snap create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_node.test.tsx.snap create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.test.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.ts create mode 100644 x-pack/plugins/monitoring/public/alerts/types.ts diff --git a/x-pack/plugins/monitoring/common/constants.ts b/x-pack/plugins/monitoring/common/constants.ts index 8d1ffc89f5dd6..cf382701ca40f 100644 --- a/x-pack/plugins/monitoring/common/constants.ts +++ b/x-pack/plugins/monitoring/common/constants.ts @@ -453,6 +453,42 @@ export const ALERT_DETAILS = { }, }; +export const ALERT_PANEL_MENU = [ + { + label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.clusterHealth', { + defaultMessage: 'Cluster health', + }), + alerts: [ + { alertName: ALERT_NODES_CHANGED }, + { alertName: ALERT_CLUSTER_HEALTH }, + { alertName: ALERT_ELASTICSEARCH_VERSION_MISMATCH }, + { alertName: ALERT_KIBANA_VERSION_MISMATCH }, + { alertName: ALERT_LOGSTASH_VERSION_MISMATCH }, + ], + }, + { + label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.resourceUtilization', { + defaultMessage: 'Resource utilization', + }), + alerts: [ + { alertName: ALERT_CPU_USAGE }, + { alertName: ALERT_DISK_USAGE }, + { alertName: ALERT_MEMORY_USAGE }, + ], + }, + { + label: i18n.translate('xpack.monitoring.alerts.badge.panelCategory.errors', { + defaultMessage: 'Errors and exceptions', + }), + alerts: [ + { alertName: ALERT_MISSING_MONITORING_DATA }, + { alertName: ALERT_LICENSE_EXPIRATION }, + { alertName: ALERT_THREAD_POOL_SEARCH_REJECTIONS }, + { alertName: ALERT_THREAD_POOL_WRITE_REJECTIONS }, + ], + }, +]; + /** * A listing of all alert types */ diff --git a/x-pack/plugins/monitoring/common/formatting.js b/x-pack/plugins/monitoring/common/formatting.ts similarity index 65% rename from x-pack/plugins/monitoring/common/formatting.js rename to x-pack/plugins/monitoring/common/formatting.ts index b2a67b3cd48da..65159f532d2aa 100644 --- a/x-pack/plugins/monitoring/common/formatting.js +++ b/x-pack/plugins/monitoring/common/formatting.ts @@ -11,13 +11,14 @@ export const SMALL_FLOAT = '0.[00]'; export const LARGE_BYTES = '0,0.0 b'; export const SMALL_BYTES = '0.0 b'; export const LARGE_ABBREVIATED = '0,0.[0]a'; +export const ROUNDED_FLOAT = '00.[00]'; /** * Format the {@code date} in the user's expected date/time format using their guessed local time zone. * @param date Either a numeric Unix timestamp or a {@code Date} object * @returns The date formatted using 'LL LTS' */ -export function formatDateTimeLocal(date, useUTC = false, timezone = null) { +export function formatDateTimeLocal(date: number | Date, useUTC = false, timezone = null) { return useUTC ? moment.utc(date).format('LL LTS') : moment.tz(date, timezone || moment.tz.guess()).format('LL LTS'); @@ -28,6 +29,18 @@ export function formatDateTimeLocal(date, useUTC = false, timezone = null) { * @param {string} hash The complete hash * @return {string} The shortened hash */ -export function shortenPipelineHash(hash) { +export function shortenPipelineHash(hash: string) { return hash.substr(0, 6); } + +export function getDateFromNow(timestamp: string | number | Date, tz: string) { + return moment(timestamp) + .tz(tz === 'Browser' ? moment.tz.guess() : tz) + .fromNow(); +} + +export function getCalendar(timestamp: string | number | Date, tz: string) { + return moment(timestamp) + .tz(tz === 'Browser' ? moment.tz.guess() : tz) + .calendar(); +} diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 0daa947b1c82a..0f10e0e48962b 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -7,6 +7,8 @@ import { Alert, SanitizedAlert } from '../../../alerts/common'; import { AlertParamType, AlertMessageTokenType, AlertSeverity } from '../enums'; +export type CommonAlert = Alert | SanitizedAlert; + export interface CommonAlertStatus { states: CommonAlertState[]; rawAlert: Alert | SanitizedAlert; @@ -179,6 +181,7 @@ export interface LegacyAlert { message: string; resolved_timestamp: string; metadata: LegacyAlertMetadata; + nodeName: string; nodes?: LegacyAlertNodesChangedList; } diff --git a/x-pack/plugins/monitoring/public/alerts/badge.tsx b/x-pack/plugins/monitoring/public/alerts/badge.tsx index b9e39e43ff73d..31a86757cac8e 100644 --- a/x-pack/plugins/monitoring/public/alerts/badge.tsx +++ b/x-pack/plugins/monitoring/public/alerts/badge.tsx @@ -4,187 +4,115 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiContextMenu, - EuiPopover, - EuiBadge, - EuiFlexGrid, - EuiFlexItem, - EuiText, -} from '@elastic/eui'; -import { CommonAlertStatus, CommonAlertState } from '../../common/types/alerts'; +import { EuiContextMenu, EuiPopover, EuiBadge, EuiSwitch } from '@elastic/eui'; +import { AlertState, CommonAlertStatus } from '../../common/types/alerts'; import { AlertSeverity } from '../../common/enums'; // @ts-ignore import { formatDateTimeLocal } from '../../common/formatting'; -import { AlertState } from '../../common/types/alerts'; -import { AlertPanel } from './panel'; -import { Legacy } from '../legacy_shims'; import { isInSetupMode } from '../lib/setup_mode'; import { SetupModeContext } from '../components/setup_mode/setup_mode_context'; - -function getDateFromState(state: CommonAlertState) { - const timestamp = state.state.ui.triggeredMS; - const tz = Legacy.shims.uiSettings.get('dateFormat:tz'); - return formatDateTimeLocal(timestamp, false, tz === 'Browser' ? null : tz); -} +import { AlertsContext } from './context'; +import { getAlertPanelsByCategory } from './lib/get_alert_panels_by_category'; +import { getAlertPanelsByNode } from './lib/get_alert_panels_by_node'; export const numberOfAlertsLabel = (count: number) => `${count} alert${count > 1 ? 's' : ''}`; -interface AlertInPanel { - alert: CommonAlertStatus; - alertState: CommonAlertState; -} +const MAX_TO_SHOW_BY_CATEGORY = 8; + +const PANEL_TITLE = i18n.translate('xpack.monitoring.alerts.badge.panelTitle', { + defaultMessage: 'Alerts', +}); + +const GROUP_BY_NODE = i18n.translate('xpack.monitoring.alerts.badge.groupByNode', { + defaultMessage: 'Group by node', +}); + +const GROUP_BY_TYPE = i18n.translate('xpack.monitoring.alerts.badge.groupByType', { + defaultMessage: 'Group by alert type', +}); interface Props { alerts: { [alertTypeId: string]: CommonAlertStatus }; stateFilter: (state: AlertState) => boolean; } export const AlertsBadge: React.FC = (props: Props) => { + // We do not always have the alerts that each consumer wants due to licensing const { stateFilter = () => true } = props; + const alerts = Object.values(props.alerts).filter((alertItem) => Boolean(alertItem?.rawAlert)); const [showPopover, setShowPopover] = React.useState(null); const inSetupMode = isInSetupMode(React.useContext(SetupModeContext)); - const alerts = Object.values(props.alerts).filter((alertItem) => Boolean(alertItem?.rawAlert)); - - if (alerts.length === 0) { - return null; - } - - const badges = []; - - if (inSetupMode) { - const button = ( - setShowPopover(true)} - > - {numberOfAlertsLabel(alerts.length)} - - ); - const panels = [ - { - id: 0, - title: i18n.translate('xpack.monitoring.alerts.badge.panelTitle', { - defaultMessage: 'Alerts', - }), - items: alerts.map(({ rawAlert }, index) => { - return { - name: {rawAlert.name}, - panel: index + 1, - }; - }), - }, - ...alerts.map((alertStatus, index) => { - return { - id: index + 1, - title: alertStatus.rawAlert.name, - width: 400, - content: , - }; - }), - ]; - - badges.push( - setShowPopover(null)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - ); - } else { - const byType = { - [AlertSeverity.Danger]: [] as AlertInPanel[], - [AlertSeverity.Warning]: [] as AlertInPanel[], - [AlertSeverity.Success]: [] as AlertInPanel[], - }; + const alertsContext = React.useContext(AlertsContext); + const alertCount = inSetupMode + ? alerts.length + : alerts.reduce( + (sum, { states }) => sum + states.filter(({ state }) => stateFilter(state)).length, + 0 + ); + const [showByNode, setShowByNode] = React.useState( + !inSetupMode && alertCount > MAX_TO_SHOW_BY_CATEGORY + ); - for (const alert of alerts) { - for (const alertState of alert.states) { - if (alertState.firing && stateFilter(alertState.state)) { - const state = alertState.state as AlertState; - byType[state.ui.severity].push({ - alertState, - alert, - }); - } - } + React.useEffect(() => { + if (inSetupMode && showByNode) { + setShowByNode(false); } + }, [inSetupMode, showByNode]); - const typesToShow = [AlertSeverity.Danger, AlertSeverity.Warning]; - for (const type of typesToShow) { - const list = byType[type]; - if (list.length === 0) { - continue; - } + if (alertCount === 0) { + return null; + } - const button = ( - setShowPopover(type)} - > - {numberOfAlertsLabel(list.length)} - - ); + const groupByType = GROUP_BY_NODE; + const panels = showByNode + ? getAlertPanelsByNode(PANEL_TITLE, alerts, stateFilter) + : getAlertPanelsByCategory(PANEL_TITLE, inSetupMode, alerts, alertsContext, stateFilter); - const panels = [ + if (panels.length && !inSetupMode && panels[0].items) { + panels[0].items.push( + ...[ { - id: 0, - title: `Alerts`, - items: list.map(({ alert, alertState }, index) => { - return { - name: ( - - -

    {getDateFromState(alertState)}

    -
    - {alert.rawAlert.name} -
    - ), - panel: index + 1, - }; - }), + isSeparator: true as const, }, - ...list.map((alertStatus, index) => { - return { - id: index + 1, - title: getDateFromState(alertStatus.alertState), - width: 400, - content: , - }; - }), - ]; - - badges.push( - setShowPopover(null)} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - ); - } + { + name: ( + setShowByNode(!showByNode)} + label={showByNode ? GROUP_BY_TYPE : groupByType} + /> + ), + }, + ] + ); } + const button = ( + setShowPopover(true)} + > + {numberOfAlertsLabel(alertCount)} + + ); + return ( - - {badges.map((badge, index) => ( - - {badge} - - ))} - + setShowPopover(null)} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + ); }; diff --git a/x-pack/plugins/monitoring/public/alerts/callout.tsx b/x-pack/plugins/monitoring/public/alerts/callout.tsx index 2f670ac221bf2..d3feb148cf986 100644 --- a/x-pack/plugins/monitoring/public/alerts/callout.tsx +++ b/x-pack/plugins/monitoring/public/alerts/callout.tsx @@ -5,78 +5,108 @@ */ import React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import { CommonAlertStatus } from '../../common/types/alerts'; -import { AlertSeverity } from '../../common/enums'; +import { + EuiPanel, + EuiSpacer, + EuiAccordion, + EuiListGroup, + EuiListGroupItem, + EuiTextColor, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, +} from '@elastic/eui'; import { replaceTokens } from './lib/replace_tokens'; -import { AlertMessage, AlertState } from '../../common/types/alerts'; - -const TYPES = [ - { - severity: AlertSeverity.Warning, - color: 'warning', - label: i18n.translate('xpack.monitoring.alerts.callout.warningLabel', { - defaultMessage: 'Warning alert(s)', - }), - }, - { - severity: AlertSeverity.Danger, - color: 'danger', - label: i18n.translate('xpack.monitoring.alerts.callout.dangerLabel', { - defaultMessage: 'Danger alert(s)', - }), - }, -]; +import { AlertMessage } from '../../common/types/alerts'; +import { AlertsByName } from './types'; +import { isInSetupMode } from '../lib/setup_mode'; +import { SetupModeContext } from '../components/setup_mode/setup_mode_context'; +import { AlertConfiguration } from './configuration'; interface Props { - alerts: { [alertTypeId: string]: CommonAlertStatus }; - stateFilter: (state: AlertState) => boolean; + alerts: AlertsByName; } export const AlertsCallout: React.FC = (props: Props) => { - const { alerts, stateFilter = () => true } = props; + const { alerts } = props; + const inSetupMode = isInSetupMode(React.useContext(SetupModeContext)); + + if (inSetupMode) { + return null; + } - const callouts = TYPES.map((type) => { - const list = []; - for (const alertTypeId of Object.keys(alerts)) { - const alertInstance = alerts[alertTypeId]; - for (const { firing, state } of alertInstance.states) { - if (firing && stateFilter(state) && state.ui.severity === type.severity) { - list.push(state); - } - } + const list = []; + for (const alertTypeId of Object.keys(alerts)) { + const alertInstance = alerts[alertTypeId]; + for (const state of alertInstance.states) { + list.push({ + alert: alertInstance, + state, + }); } + } - if (list.length) { - return ( - - -
      - {list.map((state, index) => { - const nextStepsUi = - state.ui.message.nextSteps && state.ui.message.nextSteps.length ? ( -
        - {state.ui.message.nextSteps.map( - (step: AlertMessage, nextStepIndex: number) => ( -
      • {replaceTokens(step)}
      • - ) - )} -
      - ) : null; + if (list.length === 0) { + return null; + } - return ( -
    • - {replaceTokens(state.ui.message)} - {nextStepsUi} -
    • - ); - })} -
    -
    - -
    - ); - } + const accordions = list.map((status, index) => { + const buttonContent = ( +
    + + + + + + + + {replaceTokens(status.state.state.ui.message)} + + + +
    + ); + + const accordion = ( + + + {(status.state.state.ui.message.nextSteps || []).map((step: AlertMessage) => { + return {}} label={replaceTokens(step)} />; + })} + } + /> + + + ); + + const spacer = index !== list.length - 1 ? : null; + return ( +
    + {accordion} + {spacer} +
    + ); }); - return {callouts}; + + return ( + + {accordions} + + + ); }; diff --git a/x-pack/plugins/monitoring/public/alerts/configuration.tsx b/x-pack/plugins/monitoring/public/alerts/configuration.tsx new file mode 100644 index 0000000000000..c570e2c840f01 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/configuration.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; +import { CommonAlert } from '../../common/types/alerts'; +import { Legacy } from '../legacy_shims'; +import { hideBottomBar, showBottomBar } from '../lib/setup_mode'; +import { BASE_ALERT_API_PATH } from '../../../alerts/common'; + +interface Props { + alert: CommonAlert; + compressed?: boolean; +} +export const AlertConfiguration: React.FC = (props: Props) => { + const { alert, compressed } = props; + const [showFlyout, setShowFlyout] = React.useState(false); + const [isEnabled, setIsEnabled] = React.useState(alert.enabled); + const [isMuted, setIsMuted] = React.useState(alert.muteAll); + const [isSaving, setIsSaving] = React.useState(false); + + async function disableAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.id}/_disable`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', { + defaultMessage: `Unable to disable alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function enableAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.id}/_enable`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', { + defaultMessage: `Unable to enable alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function muteAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.id}/_mute_all`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', { + defaultMessage: `Unable to mute alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + async function unmuteAlert() { + setIsSaving(true); + try { + await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${alert.id}/_unmute_all`); + } catch (err) { + Legacy.shims.toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', { + defaultMessage: `Unable to unmute alert`, + }), + text: err.message, + }); + } + setIsSaving(false); + } + + const flyoutUi = useMemo( + () => + showFlyout && + Legacy.shims.triggersActionsUi.getEditAlertFlyout({ + initialAlert: alert, + onClose: () => { + setShowFlyout(false); + showBottomBar(); + }, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [showFlyout] + ); + + return ( + + + + { + setShowFlyout(true); + hideBottomBar(); + }} + > + {i18n.translate('xpack.monitoring.alerts.panel.editAlert', { + defaultMessage: `Edit alert`, + })} + + + + { + if (isEnabled) { + setIsEnabled(false); + await disableAlert(); + } else { + setIsEnabled(true); + await enableAlert(); + } + }} + label={ + + } + /> + + + { + if (isMuted) { + setIsMuted(false); + await unmuteAlert(); + } else { + setIsMuted(true); + await muteAlert(); + } + }} + label={ + + } + /> + + + {flyoutUi} + + ); +}; diff --git a/x-pack/plugins/monitoring/public/alerts/context.ts b/x-pack/plugins/monitoring/public/alerts/context.ts new file mode 100644 index 0000000000000..1017a4ade6c73 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/context.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { AlertsByName } from './types'; + +export interface IAlertsContext { + allAlerts: AlertsByName; +} + +export const AlertsContext = React.createContext({ + allAlerts: {} as AlertsByName, +}); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_category.test.tsx.snap b/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_category.test.tsx.snap new file mode 100644 index 0000000000000..75637b5bfd6c2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_category.test.tsx.snap @@ -0,0 +1,1287 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertPanelsByCategory non setup mode should allow for state filtering 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + + Resource utilization + ( + 1 + ) + + , + "panel": 1, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_cpu_usage_label + ( + 1 + ) + , + "panel": 2, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 3, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_cpu_usage_label", + }, + Object { + "content": , + "id": 3, + "title": "monitoring_alert_cpu_usage_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory non setup mode should not show any alert if none are firing 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [], + "title": "Alerts", + }, +] +`; + +exports[`getAlertPanelsByCategory non setup mode should properly group for alerts in a single category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + + Resource utilization + ( + 2 + ) + + , + "panel": 1, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_jvm_memory_usage_label + ( + 2 + ) + , + "panel": 2, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + + + triggered:1 + + + + es_name_1 + + , + "panel": 3, + }, + Object { + "isSeparator": true, + }, + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 4, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_jvm_memory_usage_label", + }, + Object { + "content": , + "id": 3, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory non setup mode should properly group for alerts in each category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + + Cluster health + ( + 2 + ) + + , + "panel": 1, + }, + Object { + "name": + + Resource utilization + ( + 1 + ) + + , + "panel": 2, + }, + Object { + "name": + + Errors and exceptions + ( + 2 + ) + + , + "panel": 3, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_nodes_changed_label + ( + 2 + ) + , + "panel": 4, + }, + ], + "title": "Cluster health", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + monitoring_alert_disk_usage_label + ( + 1 + ) + , + "panel": 5, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 3, + "items": Array [ + Object { + "name": + monitoring_alert_license_expiration_label + ( + 2 + ) + , + "panel": 6, + }, + ], + "title": "Errors and exceptions", + }, + Object { + "id": 4, + "items": Array [ + Object { + "name": + + + triggered:1 + + + + es_name_1 + + , + "panel": 7, + }, + Object { + "isSeparator": true, + }, + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 8, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_nodes_changed_label", + }, + Object { + "id": 5, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 9, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_disk_usage_label", + }, + Object { + "id": 6, + "items": Array [ + Object { + "name": + + + triggered:1 + + + + es_name_1 + + , + "panel": 10, + }, + Object { + "isSeparator": true, + }, + Object { + "name": + + + triggered:0 + + + + es_name_0 + + , + "panel": 11, + }, + Object { + "isSeparator": true, + }, + ], + "title": "monitoring_alert_license_expiration_label", + }, + Object { + "content": , + "id": 7, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 8, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 9, + "title": "monitoring_alert_disk_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 10, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, + Object { + "content": , + "id": 11, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory setup mode should properly group for alerts in a single category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + Resource utilization + , + "panel": 1, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_jvm_memory_usage_label + , + "panel": 2, + }, + ], + "title": "Resource utilization", + }, + Object { + "content": , + "id": 2, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory setup mode should properly group for alerts in each category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + Cluster health + , + "panel": 1, + }, + Object { + "name": + Resource utilization + , + "panel": 2, + }, + Object { + "name": + Errors and exceptions + , + "panel": 3, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_nodes_changed_label + , + "panel": 4, + }, + ], + "title": "Cluster health", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + monitoring_alert_disk_usage_label + , + "panel": 5, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 3, + "items": Array [ + Object { + "name": + monitoring_alert_license_expiration_label + , + "panel": 6, + }, + ], + "title": "Errors and exceptions", + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 5, + "title": "monitoring_alert_disk_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 6, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByCategory setup mode should still show alerts if none are firing 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + Cluster health + , + "panel": 1, + }, + Object { + "name": + Resource utilization + , + "panel": 2, + }, + Object { + "name": + Errors and exceptions + , + "panel": 3, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + monitoring_alert_logstash_version_mismatch_label + , + "panel": 4, + }, + ], + "title": "Cluster health", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + monitoring_alert_cpu_usage_label + , + "panel": 5, + }, + ], + "title": "Resource utilization", + }, + Object { + "id": 3, + "items": Array [ + Object { + "name": + monitoring_alert_thread_pool_write_rejections_label + , + "panel": 6, + }, + ], + "title": "Errors and exceptions", + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_logstash_version_mismatch_label", + "width": 400, + }, + Object { + "content": , + "id": 5, + "title": "monitoring_alert_cpu_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 6, + "title": "monitoring_alert_thread_pool_write_rejections_label", + "width": 400, + }, +] +`; diff --git a/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_node.test.tsx.snap b/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_node.test.tsx.snap new file mode 100644 index 0000000000000..e9e89112a9758 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/__snapshots__/get_alert_panels_by_node.test.tsx.snap @@ -0,0 +1,660 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getAlertPanelsByNode should not show any alert if none are firing 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [], + "title": "Alerts", + }, +] +`; + +exports[`getAlertPanelsByNode should properly group for alerts in a single category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + es_name_0 + ( + 1 + ) + , + "panel": 1, + }, + Object { + "name": + es_name_1 + ( + 1 + ) + , + "panel": 2, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + monitoring_alert_jvm_memory_usage_label + + , + "panel": 3, + }, + ], + "title": "es_name_0", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + monitoring_alert_jvm_memory_usage_label + + , + "panel": 4, + }, + ], + "title": "es_name_1", + }, + Object { + "content": , + "id": 3, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_jvm_memory_usage_label", + "width": 400, + }, +] +`; + +exports[`getAlertPanelsByNode should properly group for alerts in each category 1`] = ` +Array [ + Object { + "id": 0, + "items": Array [ + Object { + "name": + es_name_0 + ( + 3 + ) + , + "panel": 1, + }, + Object { + "name": + es_name_1 + ( + 2 + ) + , + "panel": 2, + }, + ], + "title": "Alerts", + }, + Object { + "id": 1, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + monitoring_alert_nodes_changed_label + + , + "panel": 3, + }, + Object { + "name": + + + triggered:0 + + + + monitoring_alert_disk_usage_label + + , + "panel": 4, + }, + Object { + "name": + + + triggered:0 + + + + monitoring_alert_license_expiration_label + + , + "panel": 5, + }, + ], + "title": "es_name_0", + }, + Object { + "id": 2, + "items": Array [ + Object { + "name": + + + triggered:0 + + + + monitoring_alert_nodes_changed_label + + , + "panel": 6, + }, + Object { + "name": + + + triggered:0 + + + + monitoring_alert_license_expiration_label + + , + "panel": 7, + }, + ], + "title": "es_name_1", + }, + Object { + "content": , + "id": 3, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 4, + "title": "monitoring_alert_disk_usage_label", + "width": 400, + }, + Object { + "content": , + "id": 5, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, + Object { + "content": , + "id": 6, + "title": "monitoring_alert_nodes_changed_label", + "width": 400, + }, + Object { + "content": , + "id": 7, + "title": "monitoring_alert_license_expiration_label", + "width": 400, + }, +] +`; diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx new file mode 100644 index 0000000000000..16b20119c9607 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.test.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ALERTS, + ALERT_CPU_USAGE, + ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_THREAD_POOL_WRITE_REJECTIONS, +} from '../../../common/constants'; +import { AlertSeverity } from '../../../common/enums'; +import { getAlertPanelsByCategory } from './get_alert_panels_by_category'; +import { + ALERT_LICENSE_EXPIRATION, + ALERT_NODES_CHANGED, + ALERT_DISK_USAGE, + ALERT_MEMORY_USAGE, +} from '../../../common/constants'; +import { AlertsByName } from '../types'; +import { AlertExecutionStatusValues } from '../../../../alerts/common'; +import { AlertState } from '../../../common/types/alerts'; + +jest.mock('../../legacy_shims', () => ({ + Legacy: { + shims: { + uiSettings: { + get: () => '', + }, + }, + }, +})); + +jest.mock('../../../common/formatting', () => ({ + getDateFromNow: (timestamp: number) => `triggered:${timestamp}`, + getCalendar: (timestamp: number) => `triggered:${timestamp}`, +})); + +const mockAlert = { + id: '', + enabled: true, + tags: [], + consumer: '', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date('2020-12-08'), + updatedAt: new Date('2020-12-08'), + apiKey: null, + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: AlertExecutionStatusValues[0], + lastExecutionDate: new Date('2020-12-08'), + }, + notifyWhen: null, +}; + +function getAllAlerts() { + return ALERTS.reduce((accum: AlertsByName, alertType) => { + accum[alertType] = { + states: [], + rawAlert: { + alertTypeId: alertType, + name: `${alertType}_label`, + ...mockAlert, + }, + }; + return accum; + }, {}); +} + +describe('getAlertPanelsByCategory', () => { + const ui = { + isFiring: false, + severity: AlertSeverity.Danger, + message: { text: '' }, + resolvedMS: 0, + lastCheckedMS: 0, + triggeredMS: 0, + }; + + const cluster = { clusterUuid: '1', clusterName: 'one' }; + + function getAlert(type: string, firingCount: number) { + const states = []; + + for (let fi = 0; fi < firingCount; fi++) { + states.push({ + firing: true, + meta: {}, + state: { + cluster, + ui: { + ...ui, + triggeredMS: fi, + }, + nodeId: `es${fi}`, + nodeName: `es_name_${fi}`, + }, + }); + } + + return { + states, + rawAlert: { + alertTypeId: type, + name: `${type}_label`, + ...mockAlert, + }, + }; + } + + const alertsContext = { + allAlerts: getAllAlerts(), + }; + + const stateFilter = (state: AlertState) => true; + const panelTitle = 'Alerts'; + + describe('non setup mode', () => { + it('should properly group for alerts in each category', () => { + const alerts = [ + getAlert(ALERT_NODES_CHANGED, 2), + getAlert(ALERT_DISK_USAGE, 1), + getAlert(ALERT_LICENSE_EXPIRATION, 2), + ]; + const result = getAlertPanelsByCategory( + panelTitle, + false, + alerts, + alertsContext, + stateFilter + ); + expect(result).toMatchSnapshot(); + }); + + it('should properly group for alerts in a single category', () => { + const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const result = getAlertPanelsByCategory( + panelTitle, + false, + alerts, + alertsContext, + stateFilter + ); + expect(result).toMatchSnapshot(); + }); + + it('should not show any alert if none are firing', () => { + const alerts = [ + getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(ALERT_CPU_USAGE, 0), + getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + ]; + const result = getAlertPanelsByCategory( + panelTitle, + false, + alerts, + alertsContext, + stateFilter + ); + expect(result).toMatchSnapshot(); + }); + + it('should allow for state filtering', () => { + const alerts = [getAlert(ALERT_CPU_USAGE, 2)]; + const customStateFilter = (state: AlertState) => state.nodeName === 'es_name_0'; + const result = getAlertPanelsByCategory( + panelTitle, + false, + alerts, + alertsContext, + customStateFilter + ); + expect(result).toMatchSnapshot(); + }); + }); + + describe('setup mode', () => { + it('should properly group for alerts in each category', () => { + const alerts = [ + getAlert(ALERT_NODES_CHANGED, 2), + getAlert(ALERT_DISK_USAGE, 1), + getAlert(ALERT_LICENSE_EXPIRATION, 2), + ]; + const result = getAlertPanelsByCategory(panelTitle, true, alerts, alertsContext, stateFilter); + expect(result).toMatchSnapshot(); + }); + + it('should properly group for alerts in a single category', () => { + const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const result = getAlertPanelsByCategory(panelTitle, true, alerts, alertsContext, stateFilter); + expect(result).toMatchSnapshot(); + }); + + it('should still show alerts if none are firing', () => { + const alerts = [ + getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(ALERT_CPU_USAGE, 0), + getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + ]; + const result = getAlertPanelsByCategory(panelTitle, true, alerts, alertsContext, stateFilter); + expect(result).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx new file mode 100644 index 0000000000000..82a1a1f841a22 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_category.tsx @@ -0,0 +1,228 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { EuiText, EuiToolTip } from '@elastic/eui'; +import { AlertPanel } from '../panel'; +import { ALERT_PANEL_MENU } from '../../../common/constants'; +import { getDateFromNow, getCalendar } from '../../../common/formatting'; +import { IAlertsContext } from '../context'; +import { AlertState, CommonAlertStatus } from '../../../common/types/alerts'; +import { PanelItem } from '../types'; +import { sortByNewestAlert } from './sort_by_newest_alert'; +import { Legacy } from '../../legacy_shims'; + +export function getAlertPanelsByCategory( + panelTitle: string, + inSetupMode: boolean, + alerts: CommonAlertStatus[], + alertsContext: IAlertsContext, + stateFilter: (state: AlertState) => boolean +) { + const menu = []; + for (const category of ALERT_PANEL_MENU) { + let categoryFiringAlertCount = 0; + if (inSetupMode) { + const alertsInCategory = []; + for (const categoryAlert of category.alerts) { + if ( + Boolean(alerts.find(({ rawAlert }) => rawAlert.alertTypeId === categoryAlert.alertName)) + ) { + alertsInCategory.push(categoryAlert); + } + } + if (alertsInCategory.length > 0) { + menu.push({ + ...category, + alerts: alertsInCategory.map(({ alertName }) => { + const alertStatus = alertsContext.allAlerts[alertName]; + return { + alert: alertStatus.rawAlert, + states: [], + alertName, + }; + }), + alertCount: 0, + }); + } + } else { + const firingAlertsInCategory = []; + for (const { alertName } of category.alerts) { + const foundAlert = alerts.find( + ({ rawAlert: { alertTypeId } }) => alertName === alertTypeId + ); + if (foundAlert && foundAlert.states.length > 0) { + const states = foundAlert.states.filter(({ state }) => stateFilter(state)); + if (states.length > 0) { + firingAlertsInCategory.push({ + alert: foundAlert.rawAlert, + states: foundAlert.states, + alertName, + }); + categoryFiringAlertCount += states.length; + } + } + } + + if (firingAlertsInCategory.length > 0) { + menu.push({ + ...category, + alertCount: categoryFiringAlertCount, + alerts: firingAlertsInCategory, + }); + } + } + } + + for (const item of menu) { + for (const alert of item.alerts) { + alert.states.sort(sortByNewestAlert); + } + } + + const panels: PanelItem[] = [ + { + id: 0, + title: panelTitle, + items: [ + ...menu.map((category, index) => { + const name = inSetupMode ? ( + {category.label} + ) : ( + + + {category.label} ({category.alertCount}) + + + ); + return { + name, + panel: index + 1, + }; + }), + ], + }, + ]; + + if (inSetupMode) { + let secondaryPanelIndex = menu.length; + let tertiaryPanelIndex = menu.length; + let nodeIndex = 0; + for (const category of menu) { + panels.push({ + id: nodeIndex + 1, + title: `${category.label}`, + items: category.alerts.map(({ alertName }) => { + const alertStatus = alertsContext.allAlerts[alertName]; + return { + name: {alertStatus.rawAlert.name}, + panel: ++secondaryPanelIndex, + }; + }), + }); + nodeIndex++; + } + + for (const category of menu) { + for (const { alert, alertName } of category.alerts) { + const alertStatus = alertsContext.allAlerts[alertName]; + panels.push({ + id: ++tertiaryPanelIndex, + title: `${alert.name}`, + width: 400, + content: , + }); + } + } + } else { + let primaryPanelIndex = menu.length; + let nodeIndex = 0; + for (const category of menu) { + panels.push({ + id: nodeIndex + 1, + title: `${category.label}`, + items: category.alerts.map(({ alertName, states }) => { + const filteredStates = states.filter(({ state }) => stateFilter(state)); + const alertStatus = alertsContext.allAlerts[alertName]; + const name = inSetupMode ? ( + {alertStatus.rawAlert.name} + ) : ( + + {alertStatus.rawAlert.name} ({filteredStates.length}) + + ); + return { + name, + panel: ++primaryPanelIndex, + }; + }), + }); + nodeIndex++; + } + + let secondaryPanelIndex = menu.length; + let tertiaryPanelIndex = menu.reduce((count, category) => { + count += category.alerts.length; + return count; + }, menu.length); + for (const category of menu) { + for (const { alert, states } of category.alerts) { + const items = []; + for (const alertState of states.filter(({ state }) => stateFilter(state))) { + items.push({ + name: ( + + + + {getDateFromNow( + alertState.state.ui.triggeredMS, + Legacy.shims.uiSettings.get('dateFormat:tz') + )} + + + {alertState.state.nodeName} + + ), + panel: ++tertiaryPanelIndex, + }); + items.push({ + isSeparator: true as const, + }); + } + + panels.push({ + id: ++secondaryPanelIndex, + title: `${alert.name}`, + items, + }); + } + } + + let tertiaryPanelIndex2 = menu.reduce((count, category) => { + count += category.alerts.length; + return count; + }, menu.length); + for (const category of menu) { + for (const { alert, states } of category.alerts) { + for (const state of states.filter(({ state: _state }) => stateFilter(_state))) { + panels.push({ + id: ++tertiaryPanelIndex2, + title: `${alert.name}`, + width: 400, + content: , + }); + } + } + } + } + + return panels; +} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx new file mode 100644 index 0000000000000..be6ccb1e0981b --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.test.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ALERT_CPU_USAGE, + ALERT_LOGSTASH_VERSION_MISMATCH, + ALERT_THREAD_POOL_WRITE_REJECTIONS, +} from '../../../common/constants'; +import { AlertSeverity } from '../../../common/enums'; +import { getAlertPanelsByNode } from './get_alert_panels_by_node'; +import { + ALERT_LICENSE_EXPIRATION, + ALERT_NODES_CHANGED, + ALERT_DISK_USAGE, + ALERT_MEMORY_USAGE, +} from '../../../common/constants'; +import { AlertExecutionStatusValues } from '../../../../alerts/common'; +import { AlertState } from '../../../common/types/alerts'; + +jest.mock('../../legacy_shims', () => ({ + Legacy: { + shims: { + uiSettings: { + get: () => '', + }, + }, + }, +})); + +jest.mock('../../../common/formatting', () => ({ + getDateFromNow: (timestamp: number) => `triggered:${timestamp}`, + getCalendar: (timestamp: number) => `triggered:${timestamp}`, +})); + +const mockAlert = { + id: '', + enabled: true, + tags: [], + consumer: '', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date('2020-12-08'), + updatedAt: new Date('2020-12-08'), + apiKey: null, + apiKeyOwner: null, + throttle: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: AlertExecutionStatusValues[0], + lastExecutionDate: new Date('2020-12-08'), + }, + notifyWhen: null, +}; + +describe('getAlertPanelsByNode', () => { + const ui = { + isFiring: false, + severity: AlertSeverity.Danger, + message: { text: '' }, + resolvedMS: 0, + lastCheckedMS: 0, + triggeredMS: 0, + }; + + const cluster = { clusterUuid: '1', clusterName: 'one' }; + + function getAlert(type: string, firingCount: number) { + const states = []; + + for (let fi = 0; fi < firingCount; fi++) { + states.push({ + firing: true, + meta: {}, + state: { + cluster, + ui, + nodeId: `es${fi}`, + nodeName: `es_name_${fi}`, + }, + }); + } + + return { + rawAlert: { + alertTypeId: type, + name: `${type}_label`, + ...mockAlert, + }, + states, + }; + } + + const panelTitle = 'Alerts'; + const stateFilter = (state: AlertState) => true; + + it('should properly group for alerts in each category', () => { + const alerts = [ + getAlert(ALERT_NODES_CHANGED, 2), + getAlert(ALERT_DISK_USAGE, 1), + getAlert(ALERT_LICENSE_EXPIRATION, 2), + ]; + const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter); + expect(result).toMatchSnapshot(); + }); + + it('should properly group for alerts in a single category', () => { + const alerts = [getAlert(ALERT_MEMORY_USAGE, 2)]; + const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter); + expect(result).toMatchSnapshot(); + }); + + it('should not show any alert if none are firing', () => { + const alerts = [ + getAlert(ALERT_LOGSTASH_VERSION_MISMATCH, 0), + getAlert(ALERT_CPU_USAGE, 0), + getAlert(ALERT_THREAD_POOL_WRITE_REJECTIONS, 0), + ]; + const result = getAlertPanelsByNode(panelTitle, alerts, stateFilter); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx new file mode 100644 index 0000000000000..c48706f4edcb9 --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/get_alert_panels_by_node.tsx @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { Fragment } from 'react'; +import { EuiText, EuiToolTip } from '@elastic/eui'; +import { AlertPanel } from '../panel'; +import { + CommonAlertStatus, + CommonAlertState, + CommonAlert, + AlertState, +} from '../../../common/types/alerts'; +import { getDateFromNow, getCalendar } from '../../../common/formatting'; +import { PanelItem } from '../types'; +import { sortByNewestAlert } from './sort_by_newest_alert'; +import { Legacy } from '../../legacy_shims'; + +export function getAlertPanelsByNode( + panelTitle: string, + alerts: CommonAlertStatus[], + stateFilter: (state: AlertState) => boolean +) { + const alertsByNodes: { + [uuid: string]: { + [alertName: string]: { + alert: CommonAlert; + states: CommonAlertState[]; + count: number; + }; + }; + } = {}; + const statesByNodes: { + [uuid: string]: CommonAlertState[]; + } = {}; + + for (const { states, rawAlert } of alerts) { + const { alertTypeId } = rawAlert; + for (const alertState of states.filter(({ state: _state }) => stateFilter(_state))) { + const { state } = alertState; + statesByNodes[state.nodeId] = statesByNodes[state.nodeId] || []; + statesByNodes[state.nodeId].push(alertState); + + alertsByNodes[state.nodeId] = alertsByNodes[state.nodeId] || {}; + alertsByNodes[state.nodeId][alertTypeId] = alertsByNodes[alertState.state.nodeId][ + alertTypeId + ] || { alert: rawAlert, states: [], count: 0 }; + alertsByNodes[state.nodeId][alertTypeId].count++; + alertsByNodes[state.nodeId][alertTypeId].states.push(alertState); + } + } + + for (const types of Object.values(alertsByNodes)) { + for (const { states } of Object.values(types)) { + states.sort(sortByNewestAlert); + } + } + + const nodeCount = Object.keys(statesByNodes).length; + let secondaryPanelIndex = nodeCount; + let tertiaryPanelIndex = nodeCount; + const panels: PanelItem[] = [ + { + id: 0, + title: panelTitle, + items: [ + ...Object.keys(statesByNodes).map((nodeUuid, index) => { + const states = (statesByNodes[nodeUuid] as CommonAlertState[]).filter(({ state }) => + stateFilter(state) + ); + return { + name: ( + + {states[0].state.nodeName} ({states.length}) + + ), + panel: index + 1, + }; + }), + ], + }, + ...Object.keys(statesByNodes).reduce((accum: PanelItem[], nodeUuid, nodeIndex) => { + const alertsForNode = Object.values(alertsByNodes[nodeUuid]); + const panelItems = []; + let title = ''; + for (const { alert, states } of alertsForNode) { + for (const alertState of states) { + title = alertState.state.nodeName; + panelItems.push({ + name: ( + + + + {getDateFromNow( + alertState.state.ui.triggeredMS, + Legacy.shims.uiSettings.get('dateFormat:tz') + )} + + + {alert.name} + + ), + panel: ++secondaryPanelIndex, + }); + } + } + accum.push({ + id: nodeIndex + 1, + title, + items: panelItems, + }); + return accum; + }, []), + ...Object.keys(statesByNodes).reduce((accum: PanelItem[], nodeUuid, nodeIndex) => { + const alertsForNode = Object.values(alertsByNodes[nodeUuid]); + for (const { alert, states } of alertsForNode) { + for (const alertState of states) { + accum.push({ + id: ++tertiaryPanelIndex, + title: alert.name, + width: 400, + content: , + }); + } + } + return accum; + }, []), + ]; + + return panels; +} diff --git a/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.test.ts b/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.test.ts new file mode 100644 index 0000000000000..29981c3ed32fe --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.test.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AlertSeverity } from '../../../common/enums'; +import { sortByNewestAlert } from './sort_by_newest_alert'; + +describe('sortByNewestAlert', () => { + const ui = { + isFiring: false, + severity: AlertSeverity.Danger, + message: { text: '' }, + resolvedMS: 0, + lastCheckedMS: 0, + triggeredMS: 0, + }; + + const cluster = { clusterUuid: '1', clusterName: 'one' }; + + it('should sort properly', () => { + const a = { + firing: false, + meta: {}, + state: { + cluster, + ui: { + ...ui, + triggeredMS: 2, + }, + nodeId: `es1`, + nodeName: `es_name_1`, + }, + }; + const b = { + firing: false, + meta: {}, + state: { + cluster, + ui: { + ...ui, + triggeredMS: 1, + }, + nodeId: `es1`, + nodeName: `es_name_1`, + }, + }; + expect(sortByNewestAlert(a, b)).toBe(-1); + }); +}); diff --git a/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.ts b/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.ts new file mode 100644 index 0000000000000..f5a20e0bae37e --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/lib/sort_by_newest_alert.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CommonAlertState } from '../../../common/types/alerts'; + +export function sortByNewestAlert(a: CommonAlertState, b: CommonAlertState) { + if (a.state.ui.triggeredMS === b.state.ui.triggeredMS) { + return 0; + } + return a.state.ui.triggeredMS < b.state.ui.triggeredMS ? 1 : -1; +} diff --git a/x-pack/plugins/monitoring/public/alerts/panel.tsx b/x-pack/plugins/monitoring/public/alerts/panel.tsx index b480e46215108..139010a3d2446 100644 --- a/x-pack/plugins/monitoring/public/alerts/panel.tsx +++ b/x-pack/plugins/monitoring/public/alerts/panel.tsx @@ -3,186 +3,39 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { Fragment } from 'react'; import { EuiSpacer, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiSwitch, EuiTitle, EuiHorizontalRule, EuiListGroup, EuiListGroupItem, } from '@elastic/eui'; -import { CommonAlertStatus, CommonAlertState, AlertMessage } from '../../common/types/alerts'; -import { Legacy } from '../legacy_shims'; +import { CommonAlert, CommonAlertState, AlertMessage } from '../../common/types/alerts'; import { replaceTokens } from './lib/replace_tokens'; -import { isInSetupMode, hideBottomBar, showBottomBar } from '../lib/setup_mode'; -import { BASE_ALERT_API_PATH } from '../../../alerts/common'; +import { isInSetupMode } from '../lib/setup_mode'; import { SetupModeContext } from '../components/setup_mode/setup_mode_context'; +import { AlertConfiguration } from './configuration'; interface Props { - alert: CommonAlertStatus; + alert: CommonAlert; alertState?: CommonAlertState; } export const AlertPanel: React.FC = (props: Props) => { - const { - alert: { rawAlert }, - alertState, - } = props; - - const [showFlyout, setShowFlyout] = React.useState(false); - const [isEnabled, setIsEnabled] = React.useState(rawAlert?.enabled); - const [isMuted, setIsMuted] = React.useState(rawAlert?.muteAll); - const [isSaving, setIsSaving] = React.useState(false); + const { alert, alertState } = props; const inSetupMode = isInSetupMode(React.useContext(SetupModeContext)); - const flyoutUi = useMemo( - () => - showFlyout && - Legacy.shims.triggersActionsUi.getEditAlertFlyout({ - initialAlert: rawAlert, - onClose: () => { - setShowFlyout(false); - showBottomBar(); - }, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [showFlyout] - ); - - if (!rawAlert) { + if (!alert) { return null; } - async function disableAlert() { - setIsSaving(true); - try { - await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${rawAlert.id}/_disable`); - } catch (err) { - Legacy.shims.toastNotifications.addDanger({ - title: i18n.translate('xpack.monitoring.alerts.panel.disableAlert.errorTitle', { - defaultMessage: `Unable to disable alert`, - }), - text: err.message, - }); - } - setIsSaving(false); - } - async function enableAlert() { - setIsSaving(true); - try { - await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${rawAlert.id}/_enable`); - } catch (err) { - Legacy.shims.toastNotifications.addDanger({ - title: i18n.translate('xpack.monitoring.alerts.panel.enableAlert.errorTitle', { - defaultMessage: `Unable to enable alert`, - }), - text: err.message, - }); - } - setIsSaving(false); - } - async function muteAlert() { - setIsSaving(true); - try { - await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${rawAlert.id}/_mute_all`); - } catch (err) { - Legacy.shims.toastNotifications.addDanger({ - title: i18n.translate('xpack.monitoring.alerts.panel.muteAlert.errorTitle', { - defaultMessage: `Unable to mute alert`, - }), - text: err.message, - }); - } - setIsSaving(false); - } - async function unmuteAlert() { - setIsSaving(true); - try { - await Legacy.shims.http.post(`${BASE_ALERT_API_PATH}/alert/${rawAlert.id}/_unmute_all`); - } catch (err) { - Legacy.shims.toastNotifications.addDanger({ - title: i18n.translate('xpack.monitoring.alerts.panel.ummuteAlert.errorTitle', { - defaultMessage: `Unable to unmute alert`, - }), - text: err.message, - }); - } - setIsSaving(false); - } - - const configurationUi = ( - - - - { - setShowFlyout(true); - hideBottomBar(); - }} - > - {i18n.translate('xpack.monitoring.alerts.panel.editAlert', { - defaultMessage: `Edit alert`, - })} - - - - { - if (isEnabled) { - setIsEnabled(false); - await disableAlert(); - } else { - setIsEnabled(true); - await enableAlert(); - } - }} - label={ - - } - /> - - - { - if (isMuted) { - setIsMuted(false); - await unmuteAlert(); - } else { - setIsMuted(true); - await muteAlert(); - } - }} - label={ - - } - /> - - - {flyoutUi} - - ); - if (inSetupMode || !alertState) { - return
    {configurationUi}
    ; + return ( +
    + +
    + ); } const nextStepsUi = @@ -204,7 +57,9 @@ export const AlertPanel: React.FC = (props: Props) => { {nextStepsUi}
    -
    {configurationUi}
    +
    + +
    ); }; diff --git a/x-pack/plugins/monitoring/public/alerts/types.ts b/x-pack/plugins/monitoring/public/alerts/types.ts new file mode 100644 index 0000000000000..80918b5d8767a --- /dev/null +++ b/x-pack/plugins/monitoring/public/alerts/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { CommonAlertStatus } from '../../common/types/alerts'; + +export interface AlertsByName { + [name: string]: CommonAlertStatus; +} + +export interface PanelItem { + id: number; + title: string; + width?: number; + content?: React.ReactElement; + items?: Array; +} + +export interface ContextMenuItem { + name: React.ReactElement; + panel?: number; + onClick?: () => void; +} + +export interface ContextMenuItemSeparator { + isSeparator: true; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js index 86e8a7d9095f5..78b5a6f58e238 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/advanced.js @@ -20,7 +20,7 @@ import { MonitoringTimeseriesContainer } from '../../chart'; import { FormattedMessage } from '@kbn/i18n/react'; import { AlertsCallout } from '../../../alerts/callout'; -export const AdvancedNode = ({ nodeSummary, metrics, alerts, nodeId, ...props }) => { +export const AdvancedNode = ({ nodeSummary, metrics, alerts, ...props }) => { const metricsToShow = [ metrics.node_gc, metrics.node_gc_time, @@ -54,7 +54,7 @@ export const AdvancedNode = ({ nodeSummary, metrics, alerts, nodeId, ...props }) - state.nodeId === nodeId} /> + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js index 84e7e5f8b1547..364886516bbd7 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.js @@ -130,7 +130,6 @@ const getColumns = (showCgroupMetricsElasticsearch, setupMode, clusterUuid, aler defaultMessage: 'Alerts', }), field: 'alerts', - // width: '175px', sortable: true, render: (_field, node) => { return ( diff --git a/x-pack/plugins/monitoring/public/views/base_controller.js b/x-pack/plugins/monitoring/public/views/base_controller.js index 18c3a59d6b9da..f9c6cfb6024da 100644 --- a/x-pack/plugins/monitoring/public/views/base_controller.js +++ b/x-pack/plugins/monitoring/public/views/base_controller.js @@ -13,6 +13,7 @@ import { Legacy } from '../legacy_shims'; import { PromiseWithCancel } from '../../common/cancel_promise'; import { SetupModeFeature } from '../../common/enums'; import { updateSetupModeData, isSetupModeFeatureEnabled } from '../lib/setup_mode'; +import { AlertsContext } from '../alerts/context'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; /** @@ -243,11 +244,13 @@ export class MonitoringViewBaseController { const wrappedComponent = ( - {!this._isDataInitialized ? ( - - ) : ( - component - )} + + {!this._isDataInitialized ? ( + + ) : ( + component + )} + ); diff --git a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js index 586261eecb250..2f0aa67e4e16b 100644 --- a/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js +++ b/x-pack/plugins/monitoring/public/views/elasticsearch/node/index.js @@ -14,6 +14,7 @@ import { uiRoutes } from '../../../angular/helpers/routes'; import { routeInitProvider } from '../../../lib/route_init'; import { getPageData } from './get_page_data'; import template from './index.html'; +import { SetupModeRenderer } from '../../../components/renderers'; import { Node } from '../../../components/elasticsearch/node/node'; import { labels } from '../../../components/elasticsearch/shard_allocation/lib/labels'; import { nodesByIndices } from '../../../components/elasticsearch/shard_allocation/transformers/nodes_by_indices'; @@ -26,7 +27,9 @@ import { ALERT_MISSING_MONITORING_DATA, ALERT_DISK_USAGE, ALERT_MEMORY_USAGE, + ELASTICSEARCH_SYSTEM_ID, } from '../../../../common/constants'; +import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; uiRoutes.when('/elasticsearch/nodes/:node', { template, @@ -122,14 +125,26 @@ uiRoutes.when('/elasticsearch/nodes/:node', { $scope.labels = labels.node; this.renderReact( - ( + + {flyoutComponent} + + {bottomBarComponent} + + )} /> ); } diff --git a/x-pack/plugins/monitoring/server/alerts/base_alert.ts b/x-pack/plugins/monitoring/server/alerts/base_alert.ts index 1d8de2bab015c..4f989b37421ef 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_alert.ts @@ -42,6 +42,7 @@ import { mapLegacySeverity } from '../lib/alerts/map_legacy_severity'; interface LegacyOptions { watchName: string; + nodeNameLabel: string; changeDataValues?: Partial; } @@ -322,6 +323,7 @@ export class BaseAlert { shouldFire: !legacyAlert.resolved_timestamp, severity: mapLegacySeverity(legacyAlert.metadata.severity), meta: legacyAlert, + nodeName: this.alertOptions.legacy!.nodeNameLabel, ...this.alertOptions.legacy!.changeDataValues, }; }); @@ -394,6 +396,7 @@ export class BaseAlert { } const cluster = clusters.find((c: AlertCluster) => c.clusterUuid === item.clusterUuid); const alertState: AlertState = this.getDefaultAlertState(cluster!, item); + alertState.nodeName = item.nodeName; alertState.ui.triggeredMS = currentUTC; alertState.ui.isFiring = true; alertState.ui.severity = item.severity; diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts index a4e9f56109698..fbf81bc3513f6 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.test.ts @@ -119,6 +119,7 @@ describe('ClusterHealthAlert', () => { { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs: undefined, + nodeName: 'Elasticsearch cluster alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts index 3b375654548d8..d7c33ae85cc9d 100644 --- a/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cluster_health_alert.ts @@ -38,6 +38,9 @@ export class ClusterHealthAlert extends BaseAlert { name: LEGACY_ALERT_DETAILS[ALERT_CLUSTER_HEALTH].label, legacy: { watchName: 'elasticsearch_cluster_status', + nodeNameLabel: i18n.translate('xpack.monitoring.alerts.clusterHealth.nodeNameLabel', { + defaultMessage: 'Elasticsearch cluster alert', + }), }, actionVariables: [ { diff --git a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts index 7bdef1ee2c2c4..c587ca52f26ab 100644 --- a/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/cpu_usage_alert.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import numeral from '@elastic/numeral'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -16,8 +17,8 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, - CommonAlertFilter, CommonAlertParams, + CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; import { @@ -25,6 +26,8 @@ import { ALERT_CPU_USAGE, ALERT_DETAILS, } from '../../common/constants'; +// @ts-ignore +import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchCpuUsageNodeStats } from '../lib/alerts/fetch_cpu_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -121,7 +124,7 @@ export class CpuUsageAlert extends BaseAlert { defaultMessage: `Node #start_link{nodeName}#end_link is reporting cpu usage of {cpuUsage}% at #absolute`, values: { nodeName: stat.nodeName, - cpuUsage: stat.cpuUsage, + cpuUsage: numeral(stat.cpuUsage).format(ROUNDED_FLOAT), }, }), nextSteps: [ diff --git a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts index 133fe261d0791..4a736f27320eb 100644 --- a/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/disk_usage_alert.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import numeral from '@elastic/numeral'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -15,8 +16,9 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, - CommonAlertFilter, CommonAlertParams, + AlertDiskUsageNodeStats, + CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; import { @@ -24,6 +26,8 @@ import { ALERT_DISK_USAGE, ALERT_DETAILS, } from '../../common/constants'; +// @ts-ignore +import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchDiskUsageNodeStats } from '../lib/alerts/fetch_disk_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -102,13 +106,13 @@ export class DiskUsageAlert extends BaseAlert { } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const stat = item.meta as AlertDiskUsageState; + const stat = item.meta as AlertDiskUsageNodeStats; return { text: i18n.translate('xpack.monitoring.alerts.diskUsage.ui.firingMessage', { defaultMessage: `Node #start_link{nodeName}#end_link is reporting disk usage of {diskUsage}% at #absolute`, values: { nodeName: stat.nodeName, - diskUsage: stat.diskUsage, + diskUsage: numeral(stat.diskUsage).format(ROUNDED_FLOAT), }, }), nextSteps: [ diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts index 46fdd1fa98563..ed39cbea3381c 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.test.ts @@ -124,6 +124,7 @@ describe('ElasticsearchVersionMismatchAlert', () => { { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs: undefined, + nodeName: 'Elasticsearch node alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts index 88b5b708d41f3..9002fb54fcf23 100644 --- a/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/elasticsearch_version_mismatch_alert.ts @@ -26,6 +26,12 @@ export class ElasticsearchVersionMismatchAlert extends BaseAlert { name: LEGACY_ALERT_DETAILS[ALERT_ELASTICSEARCH_VERSION_MISMATCH].label, legacy: { watchName: 'elasticsearch_version_mismatch', + nodeNameLabel: i18n.translate( + 'xpack.monitoring.alerts.elasticsearchVersionMismatch.nodeNameLabel', + { + defaultMessage: 'Elasticsearch node alert', + } + ), changeDataValues: { severity: AlertSeverity.Warning }, }, interval: '1d', diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts index 2367b53330ec5..96464e16f8c72 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.test.ts @@ -126,6 +126,7 @@ describe('KibanaVersionMismatchAlert', () => { { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs: undefined, + nodeName: 'Kibana instance alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts index c9e5786484899..0d394b8f45ecc 100644 --- a/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/kibana_version_mismatch_alert.ts @@ -26,6 +26,12 @@ export class KibanaVersionMismatchAlert extends BaseAlert { name: LEGACY_ALERT_DETAILS[ALERT_KIBANA_VERSION_MISMATCH].label, legacy: { watchName: 'kibana_version_mismatch', + nodeNameLabel: i18n.translate( + 'xpack.monitoring.alerts.kibanaVersionMismatch.nodeNameLabel', + { + defaultMessage: 'Kibana instance alert', + } + ), changeDataValues: { severity: AlertSeverity.Warning }, }, interval: '1d', diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts index f7a3d321b960b..c64b6e4b92984 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.test.ts @@ -130,6 +130,7 @@ describe('LicenseExpirationAlert', () => { { cluster: { clusterUuid, clusterName }, ccs: undefined, + nodeName: 'Elasticsearch cluster alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts index 80479023a3a60..9dacba1dfe0ec 100644 --- a/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/license_expiration_alert.ts @@ -34,6 +34,9 @@ export class LicenseExpirationAlert extends BaseAlert { name: LEGACY_ALERT_DETAILS[ALERT_LICENSE_EXPIRATION].label, legacy: { watchName: 'xpack_license_expiration', + nodeNameLabel: i18n.translate('xpack.monitoring.alerts.licenseExpiration.nodeNameLabel', { + defaultMessage: 'Elasticsearch cluster alert', + }), }, interval: '1d', actionVariables: [ diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts index a021a0e6fe179..dd23cfc76dc6d 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.test.ts @@ -125,6 +125,7 @@ describe('LogstashVersionMismatchAlert', () => { { cluster: { clusterUuid: 'abc123', clusterName: 'testCluster' }, ccs: undefined, + nodeName: 'Logstash node alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts index 98640fb6e183a..4eae3cd12eed4 100644 --- a/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/logstash_version_mismatch_alert.ts @@ -26,6 +26,12 @@ export class LogstashVersionMismatchAlert extends BaseAlert { name: LEGACY_ALERT_DETAILS[ALERT_LOGSTASH_VERSION_MISMATCH].label, legacy: { watchName: 'logstash_version_mismatch', + nodeNameLabel: i18n.translate( + 'xpack.monitoring.alerts.logstashVersionMismatch.nodeNameLabel', + { + defaultMessage: 'Logstash node alert', + } + ), changeDataValues: { severity: AlertSeverity.Warning }, }, interval: '1d', diff --git a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts index 860cd41f9057d..d5ea291aa52ed 100644 --- a/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/memory_usage_alert.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import numeral from '@elastic/numeral'; import { BaseAlert } from './base_alert'; import { AlertData, @@ -15,8 +16,9 @@ import { AlertMessageTimeToken, AlertMessageLinkToken, AlertInstanceState, - CommonAlertFilter, CommonAlertParams, + AlertMemoryUsageNodeStats, + CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; import { @@ -24,6 +26,8 @@ import { ALERT_MEMORY_USAGE, ALERT_DETAILS, } from '../../common/constants'; +// @ts-ignore +import { ROUNDED_FLOAT } from '../../common/formatting'; import { fetchMemoryUsageNodeStats } from '../lib/alerts/fetch_memory_usage_node_stats'; import { getCcsIndexPattern } from '../lib/alerts/get_ccs_index_pattern'; import { AlertMessageTokenType, AlertSeverity } from '../../common/enums'; @@ -108,13 +112,13 @@ export class MemoryUsageAlert extends BaseAlert { } protected getUiMessage(alertState: AlertState, item: AlertData): AlertMessage { - const stat = item.meta as AlertMemoryUsageState; + const stat = item.meta as AlertMemoryUsageNodeStats; return { text: i18n.translate('xpack.monitoring.alerts.memoryUsage.ui.firingMessage', { defaultMessage: `Node #start_link{nodeName}#end_link is reporting JVM memory usage of {memoryUsage}% at #absolute`, values: { nodeName: stat.nodeName, - memoryUsage: stat.memoryUsage, + memoryUsage: numeral(stat.memoryUsage).format(ROUNDED_FLOAT), }, }), nextSteps: [ diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts index 12bb27ce132d0..6ba4333309f00 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.test.ts @@ -128,9 +128,9 @@ describe('MissingMonitoringDataAlert', () => { { ccs: undefined, cluster: { clusterUuid, clusterName }, - gapDuration, - nodeName, nodeId, + nodeName, + gapDuration, ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts index 1c93ff4a28719..b4c8a667a1ce8 100644 --- a/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/missing_monitoring_data_alert.ts @@ -12,10 +12,9 @@ import { AlertCluster, AlertState, AlertMessage, - AlertNodeState, AlertMessageTimeToken, - CommonAlertFilter, CommonAlertParams, + CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; import { @@ -40,12 +39,12 @@ export class MissingMonitoringDataAlert extends BaseAlert { super(rawAlert, { id: ALERT_MISSING_MONITORING_DATA, name: ALERT_DETAILS[ALERT_MISSING_MONITORING_DATA].label, + accessorKey: 'gapDuration', defaultParams: { duration: '15m', limit: '1d', }, throttle: '6h', - accessorKey: 'gapDuration', actionVariables: [ { name: 'nodes', @@ -153,7 +152,7 @@ export class MissingMonitoringDataAlert extends BaseAlert { protected executeActions( instance: AlertInstance, - { alertStates }: { alertStates: AlertNodeState[] }, + { alertStates }: { alertStates: AlertState[] }, item: AlertData | null, cluster: AlertCluster ) { diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts index 99be91dc293cb..e6017e799cba8 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.test.ts @@ -137,6 +137,7 @@ describe('NodesChangedAlert', () => { { cluster: { clusterUuid, clusterName }, ccs: undefined, + nodeName: 'Elasticsearch nodes alert', ui: { isFiring: true, message: { diff --git a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts index 47d5c5ac2c241..09c12d345d930 100644 --- a/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts +++ b/x-pack/plugins/monitoring/server/alerts/nodes_changed_alert.ts @@ -26,6 +26,9 @@ export class NodesChangedAlert extends BaseAlert { name: LEGACY_ALERT_DETAILS[ALERT_NODES_CHANGED].label, legacy: { watchName: 'elasticsearch_nodes', + nodeNameLabel: i18n.translate('xpack.monitoring.alerts.nodesChanged.nodeNameLabel', { + defaultMessage: 'Elasticsearch nodes alert', + }), changeDataValues: { shouldFire: true }, }, actionVariables: [ diff --git a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts index 2d8ccabaac853..1e539c52eeedc 100644 --- a/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts +++ b/x-pack/plugins/monitoring/server/alerts/thread_pool_rejections_alert_base.ts @@ -13,8 +13,8 @@ import { AlertThreadPoolRejectionsState, AlertMessageTimeToken, AlertMessageLinkToken, - CommonAlertFilter, ThreadPoolRejectionsAlertParams, + CommonAlertFilter, } from '../../common/types/alerts'; import { AlertInstance } from '../../../alerts/server'; import { INDEX_PATTERN_ELASTICSEARCH } from '../../common/constants'; diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts index a3743a8ff206f..5055017051816 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.test.ts @@ -47,6 +47,7 @@ describe('fetchLegacyAlerts', () => { message, metadata, nodes, + nodeName: '', prefix, }, ]); diff --git a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts index fbf7608a737ba..0ea37b4ac4daa 100644 --- a/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts +++ b/x-pack/plugins/monitoring/server/lib/alerts/fetch_legacy_alerts.ts @@ -86,6 +86,7 @@ export async function fetchLegacyAlerts( message: get(hit, '_source.message'), resolved_timestamp: get(hit, '_source.resolved_timestamp'), nodes: get(hit, '_source.nodes'), + nodeName: '', // This is set by BaseAlert metadata: get(hit, '_source.metadata') as LegacyAlertMetadata, }; return legacyAlert; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap index db74cc5e330a1..ba8506bfd0087 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/__test__/__snapshots__/handle_response.test.js.snap @@ -11,6 +11,7 @@ Array [ "shardCount": 6, "transport_address": "127.0.0.1:9300", "type": "node", + "uuid": "_x_V2YzPQU-a9KRRBxUxZQ", }, Object { "isOnline": false, @@ -21,6 +22,7 @@ Array [ "shardCount": 6, "transport_address": "127.0.0.1:9301", "type": "node", + "uuid": "DAiX7fFjS3Wii7g2HYKrOg", }, ] `; @@ -156,6 +158,7 @@ Array [ "shardCount": 0, "transport_address": "127.0.0.1:9300", "type": "master", + "uuid": "_x_V2YzPQU-a9KRRBxUxZQ", }, Object { "isOnline": true, @@ -265,6 +268,7 @@ Array [ "shardCount": 0, "transport_address": "127.0.0.1:9301", "type": "node", + "uuid": "DAiX7fFjS3Wii7g2HYKrOg", }, ] `; @@ -286,6 +290,7 @@ Array [ "shardCount": 6, "transport_address": "127.0.0.1:9300", "type": "master", + "uuid": "_x_V2YzPQU-a9KRRBxUxZQ", }, Object { "isOnline": true, @@ -302,6 +307,7 @@ Array [ "shardCount": 6, "transport_address": "127.0.0.1:9301", "type": "node", + "uuid": "DAiX7fFjS3Wii7g2HYKrOg", }, ] `; @@ -435,6 +441,7 @@ Array [ "shardCount": 6, "transport_address": "127.0.0.1:9300", "type": "master", + "uuid": "_x_V2YzPQU-a9KRRBxUxZQ", }, Object { "isOnline": true, @@ -544,6 +551,7 @@ Array [ "shardCount": 6, "transport_address": "127.0.0.1:9301", "type": "node", + "uuid": "DAiX7fFjS3Wii7g2HYKrOg", }, ] `; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js index 3766845d39b4f..ac4fcea6150a0 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js @@ -25,8 +25,9 @@ import { LISTING_METRICS_NAMES, LISTING_METRICS_PATHS } from './nodes_listing_me * * @param {Object} req: server request object * @param {String} esIndexPattern: index pattern for elasticsearch data in monitoring indices + * @param {Object} pageOfNodes: server-side paginated current page of ES nodes * @param {Object} clusterStats: cluster stats from cluster state document - * @param {Object} shardStats: per-node information about shards + * @param {Object} nodesShardCount: per-node information about shards * @return {Array} node info combined with metrics for each node from handle_response */ export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, nodesShardCount) { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js index 62cf138c99506..3f82e8ec3e646 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/handle_response.js @@ -47,6 +47,7 @@ export function handleResponse( // nodesInfo is the source of truth for the nodeIds, where nodesMetrics will lack metrics for offline nodes const nodes = pageOfNodes.map((node) => ({ + ...node, ...nodesInfo[node.uuid], ...nodesMetrics[node.uuid], resolver: node.uuid, diff --git a/x-pack/plugins/monitoring/server/lib/pagination/filter.js b/x-pack/plugins/monitoring/server/lib/pagination/filter.js index 7592f2bb3afde..e906081a8eb5a 100644 --- a/x-pack/plugins/monitoring/server/lib/pagination/filter.js +++ b/x-pack/plugins/monitoring/server/lib/pagination/filter.js @@ -15,7 +15,7 @@ function defaultFilterFn(value, query) { export function filter(data, queryText, fields, filterFn = defaultFilterFn) { return data.filter((item) => { for (const field of fields) { - if (filterFn(get(item, field), queryText)) { + if (filterFn(get(item, field, ''), queryText)) { return true; } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7845a003b59ee..4ec88a500b42a 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13564,8 +13564,6 @@ "xpack.monitoring.alerts.actionVariables.internalShortMessage": "内部メッセージ(省略あり)はElasticで生成されました。", "xpack.monitoring.alerts.actionVariables.state": "現在のアラートの状態。", "xpack.monitoring.alerts.badge.panelTitle": "アラート", - "xpack.monitoring.alerts.callout.dangerLabel": "危険アラート", - "xpack.monitoring.alerts.callout.warningLabel": "警告アラート", "xpack.monitoring.alerts.clusterHealth.action.danger": "見つからないプライマリおよびレプリカシャードを割り当てます。", "xpack.monitoring.alerts.clusterHealth.action.warning": "見つからないレプリカシャードを割り当てます。", "xpack.monitoring.alerts.clusterHealth.actionVariables.clusterHealth": "クラスターの正常性。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 83c98b794e002..c3a63660ad057 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13581,8 +13581,6 @@ "xpack.monitoring.alerts.actionVariables.internalShortMessage": "Elastic 生成的简短内部消息。", "xpack.monitoring.alerts.actionVariables.state": "告警的当前状态。", "xpack.monitoring.alerts.badge.panelTitle": "告警", - "xpack.monitoring.alerts.callout.dangerLabel": "危险告警", - "xpack.monitoring.alerts.callout.warningLabel": "警告告警", "xpack.monitoring.alerts.clusterHealth.action.danger": "分配缺失的主分片和副本分片。", "xpack.monitoring.alerts.clusterHealth.action.warning": "分配缺失的副本分片。", "xpack.monitoring.alerts.clusterHealth.actionVariables.clusterHealth": "集群的运行状况。", diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json index 2e71a6a1551e4..b533480292e2f 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_cgroup.json @@ -139,7 +139,8 @@ "slope": -1 } }, - "resolver": "_x_V2YzPQU-a9KRRBxUxZQ" + "resolver": "_x_V2YzPQU-a9KRRBxUxZQ", + "uuid": "_x_V2YzPQU-a9KRRBxUxZQ" }, { "name": "hello02", @@ -247,7 +248,8 @@ "slope": -1 } }, - "resolver": "DAiX7fFjS3Wii7g2HYKrOg" + "resolver": "DAiX7fFjS3Wii7g2HYKrOg", + "uuid": "DAiX7fFjS3Wii7g2HYKrOg" } ], "totalNodeCount": 2 diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json index 0a18664faf445..ef9fb46909715 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_green.json @@ -97,6 +97,7 @@ } }, "resolver": "ENVgDIKRSdCVJo-YqY4kUQ", + "uuid": "ENVgDIKRSdCVJo-YqY4kUQ", "shardCount": 54, "transport_address": "127.0.0.1:9300", "type": "master" @@ -185,6 +186,7 @@ } }, "resolver": "t9J9jvHpQ2yDw9c1LJ0tHA", + "uuid": "t9J9jvHpQ2yDw9c1LJ0tHA", "shardCount": 54, "transport_address": "127.0.0.1:9301", "type": "node" diff --git a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json index d9c04838fab10..f2e129cca32d1 100644 --- a/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json +++ b/x-pack/test/api_integration/apis/monitoring/elasticsearch/fixtures/nodes_listing_red.json @@ -97,6 +97,7 @@ } }, "resolver": "_WmX0plYQwm2z6tfCPyCQw", + "uuid": "_WmX0plYQwm2z6tfCPyCQw", "shardCount": 23, "transport_address": "127.0.0.1:9300", "type": "master" @@ -107,6 +108,7 @@ "nodeTypeClass": "storage", "nodeTypeLabel": "Node", "resolver": "1jxg5T33TWub-jJL4qP0Wg", + "uuid": "1jxg5T33TWub-jJL4qP0Wg", "shardCount": 0, "transport_address": "127.0.0.1:9302", "type": "node" From 96bb72f68d4a239df4fc160a3011f11d495aa609 Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Sun, 13 Dec 2020 12:40:50 -0500 Subject: [PATCH 18/37] Fix fleet route protections (#85626) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/fleet/server/routes/security.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts index c2348c313e583..ec89668111860 100644 --- a/x-pack/plugins/fleet/server/routes/security.ts +++ b/x-pack/plugins/fleet/server/routes/security.ts @@ -14,7 +14,12 @@ export function enforceSuperUser( const security = appContextService.getSecurity(); const user = security.authc.getCurrentUser(req); if (!user) { - return res.unauthorized(); + return res.forbidden({ + body: { + message: + 'Access to Fleet API require the superuser role, and for stack security features to be enabled.', + }, + }); } const userRoles = user.roles || []; From 7c9bc47bf824986b9ac3194edc2ee363d28b2f85 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Sun, 13 Dec 2020 15:36:21 -0500 Subject: [PATCH 19/37] ini `1.3.5` -> `1.3.7` (#85707) --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9f29f8ed94a00..cc7edd2aaeccb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16104,9 +16104,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.2.0, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.7" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" + integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== inline-source-map@~0.6.0: version "0.6.2" From ae64fc259222f1147c1500104d7dcb4cfa263b63 Mon Sep 17 00:00:00 2001 From: Andrew Wilkins Date: Mon, 14 Dec 2020 13:47:35 +0800 Subject: [PATCH 20/37] [APM] enable 'log_level' for Go (#85511) https://github.com/elastic/apm-agent-go/pull/859 adds central config support for 'log_level' to the Go agent, so we can now enable it in the UI too. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../agent_configuration/setting_definitions/general_settings.ts | 2 +- .../agent_configuration/setting_definitions/index.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index f243bcc0c694e..d3063c9715992 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -110,7 +110,7 @@ export const generalSettings: RawSettingDefinition[] = [ { text: 'critical', value: 'critical' }, { text: 'off', value: 'off' }, ], - includeAgents: ['dotnet', 'ruby', 'java', 'python', 'nodejs'], + includeAgents: ['dotnet', 'ruby', 'java', 'python', 'nodejs', 'go'], }, // Recording diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index ac0820309e77c..e0e7e84810090 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -45,6 +45,7 @@ describe('filterByAgent', () => { expect(getSettingKeysForAgent('go')).toEqual([ 'capture_body', 'capture_headers', + 'log_level', 'recording', 'sanitize_field_names', 'span_frames_min_duration', From a719990616da8237e726afdd1d41401182453493 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Mon, 14 Dec 2020 09:08:34 +0100 Subject: [PATCH 21/37] fixes EQL tests (#85712) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security_solution/cypress/screens/shared.ts | 2 +- .../cypress/tasks/create_new_rule.ts | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/screens/shared.ts b/x-pack/plugins/security_solution/cypress/screens/shared.ts index ccfe0f97c732c..eae8de6d5ee8b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/shared.ts +++ b/x-pack/plugins/security_solution/cypress/screens/shared.ts @@ -6,4 +6,4 @@ export const NOTIFICATION_TOASTS = '[data-test-subj="globalToastList"]'; -export const TOAST_ERROR_CLASS = 'euiToast--danger'; +export const TOAST_ERROR = '.euiToast--danger'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index cb099ea26d37b..026813e4a11cf 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -71,7 +71,7 @@ import { MITRE_ATTACK_ADD_SUBTECHNIQUE_BUTTON, MITRE_ATTACK_ADD_TECHNIQUE_BUTTON, } from '../screens/create_new_rule'; -import { NOTIFICATION_TOASTS, TOAST_ERROR_CLASS } from '../screens/shared'; +import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { TIMELINE } from '../screens/timelines'; import { refreshPage } from './security_header'; @@ -262,11 +262,20 @@ export const fillDefineThresholdRuleAndContinue = (rule: ThresholdRule) => { }; export const fillDefineEqlRuleAndContinue = (rule: CustomRule) => { + cy.get(EQL_QUERY_INPUT).should('exist'); + cy.get(EQL_QUERY_INPUT).should('be.visible'); cy.get(EQL_QUERY_INPUT).type(rule.customQuery!); cy.get(EQL_QUERY_VALIDATION_SPINNER).should('not.exist'); cy.get(QUERY_PREVIEW_BUTTON).should('not.be.disabled').click({ force: true }); - cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); - cy.get(NOTIFICATION_TOASTS).children().should('not.have.class', TOAST_ERROR_CLASS); // asserts no error toast on page + cy.get(EQL_QUERY_PREVIEW_HISTOGRAM) + .invoke('text') + .then((text) => { + if (text !== 'Hits') { + cy.get(QUERY_PREVIEW_BUTTON).click({ force: true }); + cy.get(EQL_QUERY_PREVIEW_HISTOGRAM).should('contain.text', 'Hits'); + } + }); + cy.get(TOAST_ERROR).should('not.exist'); cy.get(DEFINE_CONTINUE_BUTTON).should('exist').click({ force: true }); cy.get(EQL_QUERY_INPUT).should('not.exist'); From acdca75563da8e4d4a5194d904f673dd341892a7 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 14 Dec 2020 11:02:08 +0200 Subject: [PATCH 22/37] [Visualize] Removes the external link icon from OSS badges (#85580) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/wizard/group_selection/group_selection.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx index 8520b84cc42ad..dcf885a817e91 100644 --- a/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx +++ b/src/plugins/visualizations/public/wizard/group_selection/group_selection.tsx @@ -204,6 +204,7 @@ const VisGroup = ({ visType, onVisTypeSelected }: VisCardProps) => { target="_blank" color="text" className="visNewVisDialog__groupsCardLink" + external={false} > Date: Mon, 14 Dec 2020 14:20:52 +0300 Subject: [PATCH 23/37] [TSVB] Fields lists do not populate all the times (#85530) --- .../application/components/vis_editor.js | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js index 520ad281576cd..89e7a50ab79b0 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor.js @@ -74,20 +74,26 @@ export class VisEditor extends Component { this.props.eventEmitter.emit('dirtyStateChange', { isDirty: false, }); + + const extractedIndexPatterns = extractIndexPatterns(this.state.model); + if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) { + this.abortableFetchFields(extractedIndexPatterns).then((visFields) => { + this.setState({ + visFields, + extractedIndexPatterns, + }); + }); + } }, VIS_STATE_DEBOUNCE_DELAY); - debouncedFetchFields = debounce( - (extractedIndexPatterns) => { - if (this.abortControllerFetchFields) { - this.abortControllerFetchFields.abort(); - } - this.abortControllerFetchFields = new AbortController(); + abortableFetchFields = (extractedIndexPatterns) => { + if (this.abortControllerFetchFields) { + this.abortControllerFetchFields.abort(); + } + this.abortControllerFetchFields = new AbortController(); - return fetchFields(extractedIndexPatterns, this.abortControllerFetchFields.signal); - }, - VIS_STATE_DEBOUNCE_DELAY, - { leading: true } - ); + return fetchFields(extractedIndexPatterns, this.abortControllerFetchFields.signal); + }; handleChange = (partialModel) => { if (isEmpty(partialModel)) { @@ -105,16 +111,6 @@ export class VisEditor extends Component { dirty = false; } - const extractedIndexPatterns = extractIndexPatterns(nextModel); - if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) { - this.debouncedFetchFields(extractedIndexPatterns).then((visFields) => - this.setState({ - visFields, - extractedIndexPatterns, - }) - ); - } - this.setState({ dirty, model: nextModel, From 5a8a5bfd4c96438d10a7a23b9247d54e21233382 Mon Sep 17 00:00:00 2001 From: Thom Heymann <190132+thomheymann@users.noreply.github.com> Date: Mon, 14 Dec 2020 11:34:52 +0000 Subject: [PATCH 24/37] Add session id to audit log (#85451) * Add session id to audit log * fix naming Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../security/server/audit/audit_events.ts | 40 ++++++++++++++++--- .../server/audit/audit_service.test.ts | 20 ++++++++-- .../security/server/audit/audit_service.ts | 15 ++++--- x-pack/plugins/security/server/plugin.ts | 17 ++++---- .../server/session_management/session.mock.ts | 1 + .../server/session_management/session.test.ts | 14 +++++++ .../server/session_management/session.ts | 11 +++++ 7 files changed, 92 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/security/server/audit/audit_events.ts b/x-pack/plugins/security/server/audit/audit_events.ts index 0bb2f8ba1a246..59184562b67ff 100644 --- a/x-pack/plugins/security/server/audit/audit_events.ts +++ b/x-pack/plugins/security/server/audit/audit_events.ts @@ -8,8 +8,11 @@ import { KibanaRequest } from 'src/core/server'; import { AuthenticationResult } from '../authentication/authentication_result'; /** - * Audit event schema using ECS format. - * https://www.elastic.co/guide/en/ecs/1.6/index.html + * Audit event schema using ECS format: https://www.elastic.co/guide/en/ecs/1.6/index.html + * + * If you add additional fields to the schema ensure you update the Kibana Filebeat module: + * https://github.com/elastic/beats/tree/master/filebeat/module/kibana + * * @public */ export interface AuditEvent { @@ -37,20 +40,45 @@ export interface AuditEvent { }; kibana?: { /** - * Current space id of the request. + * The ID of the space associated with this event. */ space_id?: string; /** - * Saved object that was created, changed, deleted or accessed as part of the action. + * The ID of the user session associated with this event. Each login attempt + * results in a unique session id. + */ + session_id?: string; + /** + * Saved object that was created, changed, deleted or accessed as part of this event. */ saved_object?: { type: string; id: string; }; /** - * Any additional event specific fields. + * Name of authentication provider associated with a login event. + */ + authentication_provider?: string; + /** + * Type of authentication provider associated with a login event. + */ + authentication_type?: string; + /** + * Name of Elasticsearch realm that has authenticated the user. + */ + authentication_realm?: string; + /** + * Name of Elasticsearch realm where the user details were retrieved from. + */ + lookup_realm?: string; + /** + * Set of space IDs that a saved object was shared to. + */ + add_to_spaces?: readonly string[]; + /** + * Set of space IDs that a saved object was removed from. */ - [x: string]: any; + delete_from_spaces?: readonly string[]; }; error?: { code?: string; diff --git a/x-pack/plugins/security/server/audit/audit_service.test.ts b/x-pack/plugins/security/server/audit/audit_service.test.ts index a9c7668871248..91c656ad69f18 100644 --- a/x-pack/plugins/security/server/audit/audit_service.test.ts +++ b/x-pack/plugins/security/server/audit/audit_service.test.ts @@ -27,6 +27,7 @@ const { logging } = coreMock.createSetup(); const http = httpServiceMock.createSetupContract(); const getCurrentUser = jest.fn().mockReturnValue({ username: 'jdoe', roles: ['admin'] }); const getSpaceId = jest.fn().mockReturnValue('default'); +const getSID = jest.fn().mockResolvedValue('SESSION_ID'); beforeEach(() => { logger.info.mockClear(); @@ -45,6 +46,7 @@ describe('#setup', () => { http, getCurrentUser, getSpaceId, + getSID, }) ).toMatchInlineSnapshot(` Object { @@ -70,6 +72,7 @@ describe('#setup', () => { http, getCurrentUser, getSpaceId, + getSID, }); expect(logging.configure).toHaveBeenCalledWith(expect.any(Observable)); }); @@ -82,6 +85,7 @@ describe('#setup', () => { http, getCurrentUser, getSpaceId, + getSID, }); expect(http.registerOnPostAuth).toHaveBeenCalledWith(expect.any(Function)); }); @@ -96,16 +100,17 @@ describe('#asScoped', () => { http, getCurrentUser, getSpaceId, + getSID, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + await audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); expect(logger.info).toHaveBeenCalledWith('MESSAGE', { ecs: { version: '1.6.0' }, event: { action: 'ACTION' }, - kibana: { space_id: 'default' }, + kibana: { space_id: 'default', session_id: 'SESSION_ID' }, message: 'MESSAGE', trace: { id: 'REQUEST_ID' }, user: { name: 'jdoe', roles: ['admin'] }, @@ -123,12 +128,13 @@ describe('#asScoped', () => { http, getCurrentUser, getSpaceId, + getSID, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); + await audit.asScoped(request).log({ message: 'MESSAGE', event: { action: 'ACTION' } }); expect(logger.info).not.toHaveBeenCalled(); }); @@ -143,12 +149,13 @@ describe('#asScoped', () => { http, getCurrentUser, getSpaceId, + getSID, }); const request = httpServerMock.createKibanaRequest({ kibanaRequestState: { requestId: 'REQUEST_ID', requestUuid: 'REQUEST_UUID' }, }); - audit.asScoped(request).log(undefined); + await audit.asScoped(request).log(undefined); expect(logger.info).not.toHaveBeenCalled(); }); }); @@ -368,6 +375,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID, }); const auditLogger = auditService.getLogger(pluginId); @@ -398,6 +406,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID, }); const auditLogger = auditService.getLogger(pluginId); @@ -436,6 +445,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID, }); const auditLogger = auditService.getLogger(pluginId); @@ -464,6 +474,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID, }); const auditLogger = auditService.getLogger(pluginId); @@ -493,6 +504,7 @@ describe('#getLogger', () => { http, getCurrentUser, getSpaceId, + getSID, }); const auditLogger = auditService.getLogger(pluginId); diff --git a/x-pack/plugins/security/server/audit/audit_service.ts b/x-pack/plugins/security/server/audit/audit_service.ts index 8dbdc48c7dee9..4ad1f873581c9 100644 --- a/x-pack/plugins/security/server/audit/audit_service.ts +++ b/x-pack/plugins/security/server/audit/audit_service.ts @@ -36,9 +36,6 @@ interface AuditLogMeta extends AuditEvent { ecs: { version: string; }; - session?: { - id: string; - }; trace: { id: string; }; @@ -57,6 +54,7 @@ interface AuditServiceSetupParams { getCurrentUser( request: KibanaRequest ): ReturnType | undefined; + getSID(request: KibanaRequest): Promise; getSpaceId( request: KibanaRequest ): ReturnType | undefined; @@ -84,6 +82,7 @@ export class AuditService { logging, http, getCurrentUser, + getSID, getSpaceId, }: AuditServiceSetupParams): AuditServiceSetup { if (config.enabled && !config.appender) { @@ -134,12 +133,13 @@ export class AuditService { * }); * ``` */ - const log: AuditLogger['log'] = (event) => { + const log: AuditLogger['log'] = async (event) => { if (!event) { return; } - const user = getCurrentUser(request); const spaceId = getSpaceId(request); + const user = getCurrentUser(request); + const sessionId = await getSID(request); const meta: AuditLogMeta = { ecs: { version: ECS_VERSION }, ...event, @@ -151,11 +151,10 @@ export class AuditService { event.user, kibana: { space_id: spaceId, + session_id: sessionId, ...event.kibana, }, - trace: { - id: request.id, - }, + trace: { id: request.id }, }; if (filterEvent(meta, config.ignore_filters)) { this.ecsLogger.info(event.message!, meta); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 4016b78b6d998..070e187e869b1 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -188,24 +188,25 @@ export class Plugin { registerSecurityUsageCollector({ usageCollection, config, license }); + const { session } = this.sessionManagementService.setup({ + config, + clusterClient, + http: core.http, + kibanaIndexName: legacyConfig.kibana.index, + taskManager, + }); + const audit = this.auditService.setup({ license, config: config.audit, logging: core.logging, http: core.http, getSpaceId: (request) => spaces?.spacesService.getSpaceId(request), + getSID: (request) => session.getSID(request), getCurrentUser: (request) => authenticationSetup.getCurrentUser(request), }); const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger()); - const { session } = this.sessionManagementService.setup({ - config, - clusterClient, - http: core.http, - kibanaIndexName: legacyConfig.kibana.index, - taskManager, - }); - const authenticationSetup = this.authenticationService.setup({ legacyAuditLogger, audit, diff --git a/x-pack/plugins/security/server/session_management/session.mock.ts b/x-pack/plugins/security/server/session_management/session.mock.ts index 973341acbfce3..b740249180407 100644 --- a/x-pack/plugins/security/server/session_management/session.mock.ts +++ b/x-pack/plugins/security/server/session_management/session.mock.ts @@ -10,6 +10,7 @@ import { sessionIndexMock } from './session_index.mock'; export const sessionMock = { create: (): jest.Mocked> => ({ + getSID: jest.fn(), get: jest.fn(), create: jest.fn(), update: jest.fn(), diff --git a/x-pack/plugins/security/server/session_management/session.test.ts b/x-pack/plugins/security/server/session_management/session.test.ts index 3010e70c31421..47e391ed57925 100644 --- a/x-pack/plugins/security/server/session_management/session.test.ts +++ b/x-pack/plugins/security/server/session_management/session.test.ts @@ -56,6 +56,20 @@ describe('Session', () => { }); }); + describe('#getSID', () => { + const mockRequest = httpServerMock.createKibanaRequest(); + + it('returns `undefined` if session cookie does not exist', async () => { + mockSessionCookie.get.mockResolvedValue(null); + await expect(session.getSID(mockRequest)).resolves.toBeUndefined(); + }); + + it('returns session id', async () => { + mockSessionCookie.get.mockResolvedValue(sessionCookieMock.createValue()); + await expect(session.getSID(mockRequest)).resolves.toEqual('some-long-sid'); + }); + }); + describe('#get', () => { const mockAAD = Buffer.from([2, ...Array(255).keys()]).toString('base64'); diff --git a/x-pack/plugins/security/server/session_management/session.ts b/x-pack/plugins/security/server/session_management/session.ts index 4dc83a1abe4af..3c97c13c2d41d 100644 --- a/x-pack/plugins/security/server/session_management/session.ts +++ b/x-pack/plugins/security/server/session_management/session.ts @@ -99,6 +99,17 @@ export class Session { this.crypto = nodeCrypto({ encryptionKey: this.options.config.encryptionKey }); } + /** + * Extracts session id for the specified request. + * @param request Request instance to get session value for. + */ + async getSID(request: KibanaRequest) { + const sessionCookieValue = await this.options.sessionCookie.get(request); + if (sessionCookieValue) { + return sessionCookieValue.sid; + } + } + /** * Extracts session value for the specified request. Under the hood it can clear session if it is * invalid or created by the legacy versions of Kibana. From b01a3270769dd33309b25a91ae28e0db74f6c3ed Mon Sep 17 00:00:00 2001 From: Vadim Dalecky Date: Mon, 14 Dec 2020 13:28:23 +0100 Subject: [PATCH 25/37] Row trigger 2 (#83167) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 add ROW_CLICK_TRIGGER * feat: 🎸 wire row click event to UI Actions trigger in Lens * feat: 🎸 add row click trigger to url drilldown * feat: 🎸 add datatable to row click context * feat: 🎸 pass in row index in row click trigger context * feat: 🎸 add columns to row click trigger context * feat: 🎸 fill values and keys event scope array * feat: 🎸 generate correct row scope variables * fix: 🐛 report triggers from lens embeddable * feat: 🎸 add sample preview for row click trigger * feat: 🎸 remove url drilldown preview box * chore: 🤖 remove mock variable generation functions * feat: 🎸 generate context and global variable lists * feat: 🎸 preview event variable list * feat: 🎸 show empty url error on blur * feat: 🎸 add ability to always show popup for executed actions * refactor: 💡 rename multiple action execution method * fix: 🐛 don't add separator befor group on no main items * feat: 🎸 wire in uiActions service into datatable renderer * feat: 🎸 check each row if it has compatible row click actions * feat: 🎸 allow passing data to expression renderer * feat: 🎸 add isEmbeddable helper * feat: 🎸 pass embeddable to lens table renderer * feat: 🎸 hide lens table row actions which are empty * feat: 🎸 re-render lens embeddable when dynamic actions chagne * feat: 🎸 hide actions column if there are no row actions * feat: 🎸 re-render lens embeddable on view mode chagne * fix: 🐛 fix TypeScript errors * chore: 🤖 fix TypeScript errors * docs: ✏️ update auto-generated docs * feat: 🎸 add hasCompatibleActions to expression layer * feat: 🎸 remove "data" from expression renderer handlers * fix: 🐛 fix TypeScript errors * test: 💍 fix Jest tests * docs: ✏️ update autogenerated docs * fix: 🐛 wrap event payload into data * test: 💍 add "alwaysShowPopup" test * chore: 🤖 add comment requested in review https://github.com/elastic/kibana/pull/83167#discussion_r537340216 * test: 💍 add hasCompatibleActions test * test: 💍 add datatable renderer test * test: 💍 add Lens embeddable input change tests * test: 💍 add embeddable row click test * fix: 🐛 add url validation * test: 💍 add url drilldown tests * docs: ✏️ remove url drilldown preview from docs * docs: ✏️ remove preview from url templating * docs: ✏️ add row click description * chore: 🤖 move 36.5 KB bundle balance to url_drilldown * test: 💍 simplify test case * style: 💄 change types places * refactor: 💡 clean up panel variable generation * test: 💍 add getPanelVariables() tests * fix: 🐛 generate runtime variables correctly * fix: 🐛 improve getVariableList() and add tests for it * feat: 🎸 add translation, improve types --- ...ns-embeddable-public.chartactioncontext.md | 2 +- ...-plugins-embeddable-public.isembeddable.md | 11 + ...eddable-public.isrowclicktriggercontext.md | 11 + ...kibana-plugin-plugins-embeddable-public.md | 2 + ...c.expressionrenderhandler._constructor_.md | 4 +- ...ressions-public.expressionrenderhandler.md | 4 +- ...s-public.expressionrenderhandler.render.md | 2 +- ...essionloaderparams.hascompatibleactions.md | 11 + ...ressions-public.iexpressionloaderparams.md | 1 + ...eterrenderhandlers.hascompatibleactions.md | 11 + ...sions-public.iinterpreterrenderhandlers.md | 1 + ...eterrenderhandlers.hascompatibleactions.md | 11 + ...sions-server.iinterpreterrenderhandlers.md | 1 + ...kibana-plugin-plugins-ui_actions-public.md | 3 + ...ins-ui_actions-public.row_click_trigger.md | 11 + ...-ui_actions-public.rowclickcontext.data.md | 15 + ...tions-public.rowclickcontext.embeddable.md | 11 + ...ugins-ui_actions-public.rowclickcontext.md | 19 ++ ...ugins-ui_actions-public.rowclicktrigger.md | 11 + ...ui_actions-public.triggercontextmapping.md | 1 + ...triggercontextmapping.row_click_trigger.md | 11 + ...ublic.uiactionsservice.addtriggeraction.md | 2 +- ...ns-public.uiactionsservice.attachaction.md | 2 +- ....uiactionsservice.executetriggeractions.md | 2 +- ...ions-public.uiactionsservice.gettrigger.md | 2 +- ...blic.uiactionsservice.gettriggeractions.md | 2 +- ...ionsservice.gettriggercompatibleactions.md | 2 +- ...gins-ui_actions-public.uiactionsservice.md | 12 +- docs/user/dashboard/drilldowns.asciidoc | 2 +- .../images/url_drilldown_url_template.png | Bin 21025 -> 17667 bytes docs/user/dashboard/url-drilldown.asciidoc | 28 +- packages/kbn-optimizer/limits.yml | 4 +- src/plugins/embeddable/public/index.ts | 2 + .../public/lib/embeddables/i_embeddable.ts | 2 +- .../public/lib/embeddables/index.ts | 1 + .../public/lib/embeddables/is_embeddable.ts | 29 ++ .../public/lib/triggers/triggers.ts | 10 +- src/plugins/embeddable/public/public.api.md | 13 +- .../common/expression_renderers/types.ts | 1 + src/plugins/expressions/public/loader.ts | 1 + src/plugins/expressions/public/public.api.md | 8 +- src/plugins/expressions/public/render.test.ts | 25 ++ src/plugins/expressions/public/render.ts | 32 +- src/plugins/expressions/public/types/index.ts | 2 + src/plugins/expressions/server/server.api.md | 2 + src/plugins/ui_actions/public/index.ts | 3 + src/plugins/ui_actions/public/plugin.ts | 2 + src/plugins/ui_actions/public/public.api.md | 44 ++- .../service/ui_actions_execution_service.ts | 47 ++- .../tests/execute_trigger_actions.test.ts | 27 +- .../ui_actions/public/triggers/index.ts | 1 + .../public/triggers/row_click_trigger.ts | 53 ++++ .../public/triggers/trigger_contract.ts | 4 +- .../public/triggers/trigger_internal.ts | 15 +- src/plugins/ui_actions/public/types.ts | 3 + .../public/embeddable/events.ts | 5 +- .../url_drilldown/public/lib/test/data.ts | 173 +++++++++++ .../public/lib/url_drilldown.test.ts | 203 +++++++++++- .../public/lib/url_drilldown.tsx | 67 ++-- .../public/lib/url_drilldown_scope.test.ts | 294 +++++++++++++----- .../public/lib/url_drilldown_scope.ts | 201 +++++++----- .../__snapshots__/expression.test.tsx.snap | 56 ++++ .../expression.test.tsx | 18 ++ .../datatable_visualization/expression.tsx | 257 ++++++++++----- .../public/datatable_visualization/index.ts | 3 +- .../embeddable/embeddable.test.tsx | 101 +++++- .../embeddable/embeddable.tsx | 70 ++++- .../embeddable/embeddable_factory.ts | 1 + .../embeddable/expression_wrapper.tsx | 4 + x-pack/plugins/lens/public/types.ts | 15 +- .../test_samples/demo.tsx | 32 +- .../url_drilldown_collect_config.test.tsx | 47 --- .../url_drilldown_collect_config.tsx | 87 ++---- .../public/drilldowns/url_drilldown/index.ts | 4 - .../url_drilldown/url_drilldown_scope.test.ts | 52 ---- .../url_drilldown/url_drilldown_scope.ts | 39 --- 76 files changed, 1684 insertions(+), 584 deletions(-) create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isembeddable.md create mode 100644 docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md create mode 100644 docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md create mode 100644 docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md create mode 100644 docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.row_click_trigger.md create mode 100644 docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md create mode 100644 docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md create mode 100644 docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.md create mode 100644 docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md create mode 100644 docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md create mode 100644 src/plugins/embeddable/public/lib/embeddables/is_embeddable.ts create mode 100644 src/plugins/ui_actions/public/triggers/row_click_trigger.ts create mode 100644 x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts delete mode 100644 x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx delete mode 100644 x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts delete mode 100644 x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.chartactioncontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.chartactioncontext.md index 1c9fc27d53f19..9447c8a4e50a7 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.chartactioncontext.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.chartactioncontext.md @@ -7,5 +7,5 @@ Signature: ```typescript -export declare type ChartActionContext = ValueClickContext | RangeSelectContext; +export declare type ChartActionContext = ValueClickContext | RangeSelectContext | RowClickContext; ``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isembeddable.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isembeddable.md new file mode 100644 index 0000000000000..ea8d3870dc055 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isembeddable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isEmbeddable](./kibana-plugin-plugins-embeddable-public.isembeddable.md) + +## isEmbeddable variable + +Signature: + +```typescript +isEmbeddable: (x: unknown) => x is IEmbeddable +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md new file mode 100644 index 0000000000000..91e0f988db69c --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [isRowClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md) + +## isRowClickTriggerContext variable + +Signature: + +```typescript +isRowClickTriggerContext: (context: ChartActionContext) => context is RowClickContext +``` diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md index 06f792837e4fe..f1ea605703e59 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.md @@ -78,7 +78,9 @@ | [defaultEmbeddableFactoryProvider](./kibana-plugin-plugins-embeddable-public.defaultembeddablefactoryprovider.md) | | | [EmbeddableRenderer](./kibana-plugin-plugins-embeddable-public.embeddablerenderer.md) | Helper react component to render an embeddable Can be used if you have an embeddable object or an embeddable factory Supports updating input by passing input prop | | [isContextMenuTriggerContext](./kibana-plugin-plugins-embeddable-public.iscontextmenutriggercontext.md) | | +| [isEmbeddable](./kibana-plugin-plugins-embeddable-public.isembeddable.md) | | | [isRangeSelectTriggerContext](./kibana-plugin-plugins-embeddable-public.israngeselecttriggercontext.md) | | +| [isRowClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isrowclicktriggercontext.md) | | | [isValueClickTriggerContext](./kibana-plugin-plugins-embeddable-public.isvalueclicktriggercontext.md) | | | [PANEL\_BADGE\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_badge_trigger.md) | | | [PANEL\_NOTIFICATION\_TRIGGER](./kibana-plugin-plugins-embeddable-public.panel_notification_trigger.md) | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md index fcccd3f6b9618..1565202e84674 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `ExpressionRenderHandler` class Signature: ```typescript -constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial); +constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(element: HTMLElement, { onRenderError, renderMode }?: PartialHTMLElement | | -| { onRenderError, renderMode } | Partial<ExpressionRenderHandlerParams> | | +| { onRenderError, renderMode, hasCompatibleActions, } | ExpressionRenderHandlerParams | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md index 12c663273bd8c..d65c06bdaed83 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.md @@ -14,7 +14,7 @@ export declare class ExpressionRenderHandler | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(element, { onRenderError, renderMode })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the ExpressionRenderHandler class | +| [(constructor)(element, { onRenderError, renderMode, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the ExpressionRenderHandler class | ## Properties @@ -24,7 +24,7 @@ export declare class ExpressionRenderHandler | [events$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.events_.md) | | Observable<ExpressionRendererEvent> | | | [getElement](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.getelement.md) | | () => HTMLElement | | | [handleRenderError](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.handlerendererror.md) | | (error: ExpressionRenderError) => void | | -| [render](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md) | | (data: any, uiState?: any) => Promise<void> | | +| [render](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md) | | (value: any, uiState?: any) => Promise<void> | | | [render$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.render_.md) | | Observable<number> | | | [update$](./kibana-plugin-plugins-expressions-public.expressionrenderhandler.update_.md) | | Observable<UpdateValue | null> | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md index dec17d60ffd14..87f378fd58344 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderhandler.render.md @@ -7,5 +7,5 @@ Signature: ```typescript -render: (data: any, uiState?: any) => Promise; +render: (value: any, uiState?: any) => Promise; ``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md new file mode 100644 index 0000000000000..4d2b76cb323fb --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) > [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) + +## IExpressionLoaderParams.hasCompatibleActions property + +Signature: + +```typescript +hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md index 54eecad0deb50..22a73fff039e6 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md @@ -19,6 +19,7 @@ export interface IExpressionLoaderParams | [customRenderers](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.customrenderers.md) | [] | | | [debug](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.debug.md) | boolean | | | [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | boolean | | +| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | ExpressionRenderHandlerParams['hasCompatibleActions'] | | | [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | Adapters | | | [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | RenderErrorHandlerFnType | | | [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | RenderMode | | diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md new file mode 100644 index 0000000000000..d178af55ae2d9 --- /dev/null +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) > [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md) + +## IInterpreterRenderHandlers.hasCompatibleActions property + +Signature: + +```typescript +hasCompatibleActions?: (event: any) => Promise; +``` diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md index a65e025451636..931e474a41006 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md @@ -17,6 +17,7 @@ export interface IInterpreterRenderHandlers | [done](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.done.md) | () => void | Done increments the number of rendering successes | | [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [getRenderMode](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.getrendermode.md) | () => RenderMode | | +| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md) | (event: any) => Promise<boolean> | | | [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | () => void | | | [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | PersistedState | | diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md new file mode 100644 index 0000000000000..55419279f5d21 --- /dev/null +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) > [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) > [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md) + +## IInterpreterRenderHandlers.hasCompatibleActions property + +Signature: + +```typescript +hasCompatibleActions?: (event: any) => Promise; +``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md index b1496386944fa..273703cacca06 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md @@ -17,6 +17,7 @@ export interface IInterpreterRenderHandlers | [done](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.done.md) | () => void | Done increments the number of rendering successes | | [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | (event: any) => void | | | [getRenderMode](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.getrendermode.md) | () => RenderMode | | +| [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md) | (event: any) => Promise<boolean> | | | [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | (fn: () => void) => void | | | [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | () => void | | | [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | PersistedState | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md index 5e10de4e0f2a5..fd1ea7df4fb74 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.md @@ -26,6 +26,7 @@ | [Action](./kibana-plugin-plugins-ui_actions-public.action.md) | | | [ActionContextMapping](./kibana-plugin-plugins-ui_actions-public.actioncontextmapping.md) | | | [ActionExecutionMeta](./kibana-plugin-plugins-ui_actions-public.actionexecutionmeta.md) | During action execution we can provide additional information, for example, trigger, that caused the action execution | +| [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) | | | [Trigger](./kibana-plugin-plugins-ui_actions-public.trigger.md) | This is a convenience interface used to register a \*trigger\*.Trigger specifies a named anchor to which Action can be attached. When Trigger is being \*called\* it creates a Context object and passes it to the execute method of an Action.More than one action can be attached to a single trigger, in which case when trigger is \*called\* it first displays a context menu for user to pick a single action to execute. | | [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) | | | [UiActionsActionDefinition](./kibana-plugin-plugins-ui_actions-public.uiactionsactiondefinition.md) | A convenience interface used to register an action. | @@ -42,6 +43,8 @@ | [ACTION\_VISUALIZE\_LENS\_FIELD](./kibana-plugin-plugins-ui_actions-public.action_visualize_lens_field.md) | | | [APPLY\_FILTER\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.apply_filter_trigger.md) | | | [applyFilterTrigger](./kibana-plugin-plugins-ui_actions-public.applyfiltertrigger.md) | | +| [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.row_click_trigger.md) | | +| [rowClickTrigger](./kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md) | | | [SELECT\_RANGE\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.select_range_trigger.md) | | | [selectRangeTrigger](./kibana-plugin-plugins-ui_actions-public.selectrangetrigger.md) | | | [VALUE\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.value_click_trigger.md) | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.row_click_trigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.row_click_trigger.md new file mode 100644 index 0000000000000..3541b53ab1d61 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.row_click_trigger.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.row_click_trigger.md) + +## ROW\_CLICK\_TRIGGER variable + +Signature: + +```typescript +ROW_CLICK_TRIGGER = "ROW_CLICK_TRIGGER" +``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md new file mode 100644 index 0000000000000..1068cc9146893 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) > [data](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md) + +## RowClickContext.data property + +Signature: + +```typescript +data: { + rowIndex: number; + table: Datatable; + columns?: string[]; + }; +``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md new file mode 100644 index 0000000000000..e8baf44ff9cbc --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) > [embeddable](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md) + +## RowClickContext.embeddable property + +Signature: + +```typescript +embeddable?: IEmbeddable; +``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.md new file mode 100644 index 0000000000000..74b55d85f10e3 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclickcontext.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [RowClickContext](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.md) + +## RowClickContext interface + +Signature: + +```typescript +export interface RowClickContext +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [data](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.data.md) | {
    rowIndex: number;
    table: Datatable;
    columns?: string[];
    } | | +| [embeddable](./kibana-plugin-plugins-ui_actions-public.rowclickcontext.embeddable.md) | IEmbeddable | | + diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md new file mode 100644 index 0000000000000..aa1097d8c0864 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [rowClickTrigger](./kibana-plugin-plugins-ui_actions-public.rowclicktrigger.md) + +## rowClickTrigger variable + +Signature: + +```typescript +rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'> +``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md index 9db44d4dc7b05..2f0d22cf6dd74 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md @@ -16,6 +16,7 @@ export interface TriggerContextMapping | --- | --- | --- | | [""](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.__.md) | TriggerContext | | | [FILTER\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.filter_trigger.md) | ApplyGlobalFilterActionContext | | +| [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md) | RowClickContext | | | [SELECT\_RANGE\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.select_range_trigger.md) | RangeSelectContext | | | [VALUE\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.value_click_trigger.md) | ValueClickContext | | | [VISUALIZE\_FIELD\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.visualize_field_trigger.md) | VisualizeFieldContext | | diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md new file mode 100644 index 0000000000000..cf253df337378 --- /dev/null +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-ui\_actions-public](./kibana-plugin-plugins-ui_actions-public.md) > [TriggerContextMapping](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.md) > [ROW\_CLICK\_TRIGGER](./kibana-plugin-plugins-ui_actions-public.triggercontextmapping.row_click_trigger.md) + +## TriggerContextMapping.ROW\_CLICK\_TRIGGER property + +Signature: + +```typescript +[ROW_CLICK_TRIGGER]: RowClickContext; +``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md index fd6ade88479af..ca999322b7a56 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md @@ -11,5 +11,5 @@ Signature: ```typescript -readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; +readonly addTriggerAction: (triggerId: T, action: ActionDefinition | Action) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md index 19f215a96b23b..e95e7e1eb38b6 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly attachAction: (triggerId: T, actionId: string) => void; +readonly attachAction: (triggerId: T, actionId: string) => void; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md index 1bb6ca1115248..8e7fb8b8bbf29 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md @@ -12,5 +12,5 @@ Signature: ```typescript -readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise; +readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md index d44dc4e43a52e..b996620686a28 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTrigger: (triggerId: T) => TriggerContract; +readonly getTrigger: (triggerId: T) => TriggerContract; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md index 0a9b674a45de2..f94b34ecc2d90 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerActions: (triggerId: T) => Action[]; +readonly getTriggerActions: (triggerId: T) => Action[]; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md index faed81236342d..dff958608ef9e 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md @@ -7,5 +7,5 @@ Signature: ```typescript -readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; +readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; ``` diff --git a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md index e3c5dbb92ae90..e35eb503ab62b 100644 --- a/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md +++ b/docs/development/plugins/ui_actions/public/kibana-plugin-plugins-ui_actions-public.uiactionsservice.md @@ -21,17 +21,17 @@ export declare class UiActionsService | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [actions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.actions.md) | | ActionRegistry | | -| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | -| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void | | +| [addTriggerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.addtriggeraction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, action: ActionDefinition<TriggerContextMapping[T]> | Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">) => void | addTriggerAction is similar to attachAction as it attaches action to a trigger, but it also registers the action, if it has not been registered, yet.addTriggerAction also infers better typing of the action argument. | +| [attachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.attachaction.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, actionId: string) => void | | | [clear](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.clear.md) | | () => void | Removes all registered triggers and actions. | | [detachAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.detachaction.md) | | (triggerId: TriggerId, actionId: string) => void | | -| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void> | | +| [executeTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executetriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContext<T>) => Promise<void> | | | [executionService](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.executionservice.md) | | UiActionsExecutionService | | | [fork](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.fork.md) | | () => UiActionsService | "Fork" a separate instance of UiActionsService that inherits all existing triggers and actions, but going forward all new triggers and actions added to this instance of UiActionsService are only available within this instance. | | [getAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.getaction.md) | | <T extends ActionDefinition<{}>>(id: string) => Action<ActionContext<T>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV"> | | -| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T> | | -| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[] | | -| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]> | | +| [getTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettrigger.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => TriggerContract<T> | | +| [getTriggerActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggeractions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T) => Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[] | | +| [getTriggerCompatibleActions](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.gettriggercompatibleactions.md) | | <T extends "" | "SELECT_RANGE_TRIGGER" | "VALUE_CLICK_TRIGGER" | "ROW_CLICK_TRIGGER" | "FILTER_TRIGGER" | "VISUALIZE_FIELD_TRIGGER" | "VISUALIZE_GEO_FIELD_TRIGGER" | "CONTEXT_MENU_TRIGGER" | "PANEL_BADGE_TRIGGER" | "PANEL_NOTIFICATION_TRIGGER">(triggerId: T, context: TriggerContextMapping[T]) => Promise<Action<TriggerContextMapping[T], "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV">[]> | | | [hasAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.hasaction.md) | | (actionId: string) => boolean | | | [registerAction](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registeraction.md) | | <A extends ActionDefinition<{}>>(definition: A) => Action<ActionContext<A>, "" | "ACTION_VISUALIZE_FIELD" | "ACTION_VISUALIZE_GEO_FIELD" | "ACTION_VISUALIZE_LENS_FIELD" | "ACTION_GLOBAL_APPLY_FILTER" | "ACTION_SELECT_RANGE" | "ACTION_VALUE_CLICK" | "ACTION_CUSTOMIZE_PANEL" | "ACTION_ADD_PANEL" | "openInspector" | "deletePanel" | "editPanel" | "togglePanel" | "replacePanel" | "clonePanel" | "addToFromLibrary" | "unlinkFromLibrary" | "ACTION_LIBRARY_NOTIFICATION" | "ACTION_EXPORT_CSV"> | | | [registerTrigger](./kibana-plugin-plugins-ui_actions-public.uiactionsservice.registertrigger.md) | | (trigger: Trigger) => void | | diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index ff2c321f667c8..3db5bd6d97ff0 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -233,7 +233,7 @@ image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigate https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}} ---- + -The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice. In *URL preview*, `{{event.value}}` is substituted with a <> value. +The example URL navigates to {kib} issues on Github. `{{event.value}}` is substituted with a value associated with a selected pie slice. + [role="screenshot"] image:images/url_drilldown_url_template.png[URL template input] diff --git a/docs/user/dashboard/images/url_drilldown_url_template.png b/docs/user/dashboard/images/url_drilldown_url_template.png index d8515afe66a80beb0d63c862b954249c89683d2e..746ce62733618c6e3fc75223a02fe9c364dc2264 100644 GIT binary patch literal 17667 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 { + if (!x) return false; + if (typeof x !== 'object') return false; + if (typeof (x as IEmbeddable).id !== 'string') return false; + if (typeof (x as IEmbeddable).getInput !== 'function') return false; + if (typeof (x as IEmbeddable).supportedTriggers !== 'function') return false; + return true; +}; diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index b2965b55dbdfa..c3b1496b8eca8 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -19,7 +19,7 @@ import { i18n } from '@kbn/i18n'; import { Datatable } from '../../../../expressions'; -import { Trigger } from '../../../../ui_actions/public'; +import { Trigger, RowClickContext } from '../../../../ui_actions/public'; import { IEmbeddable } from '..'; export interface EmbeddableContext { @@ -52,7 +52,8 @@ export interface RangeSelectContext { export type ChartActionContext = | ValueClickContext - | RangeSelectContext; + | RangeSelectContext + | RowClickContext; export const CONTEXT_MENU_TRIGGER = 'CONTEXT_MENU_TRIGGER'; export const contextMenuTrigger: Trigger<'CONTEXT_MENU_TRIGGER'> = { @@ -95,6 +96,11 @@ export const isRangeSelectTriggerContext = ( context: ChartActionContext ): context is RangeSelectContext => context.data && 'range' in context.data; +export const isRowClickTriggerContext = (context: ChartActionContext): context is RowClickContext => + !!context.data && + typeof context.data === 'object' && + typeof (context as RowClickContext).data.rowIndex === 'number'; + export const isContextMenuTriggerContext = (context: unknown): context is EmbeddableContext => !!context && typeof context === 'object' && diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index e42eaaf86bdf3..4b7d60b4dc9ec 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -176,10 +176,11 @@ export class AttributeService>; } +// Warning: (ae-forgotten-export) The symbol "RowClickContext" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "ChartActionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type ChartActionContext = ValueClickContext | RangeSelectContext; +export type ChartActionContext = ValueClickContext | RangeSelectContext | RowClickContext; // Warning: (ae-missing-release-tag) "Container" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -726,6 +727,11 @@ export interface IEmbeddable context is EmbeddableContext; +// Warning: (ae-missing-release-tag) "isEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isEmbeddable: (x: unknown) => x is IEmbeddable; + // Warning: (ae-missing-release-tag) "isErrorEmbeddable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -741,6 +747,11 @@ export const isRangeSelectTriggerContext: (context: ChartActionContext) => conte // @public (undocumented) export function isReferenceOrValueEmbeddable(incoming: unknown): incoming is ReferenceOrValueEmbeddable; +// Warning: (ae-missing-release-tag) "isRowClickTriggerContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isRowClickTriggerContext: (context: ChartActionContext) => context is RowClickContext; + // Warning: (ae-missing-release-tag) "isSavedObjectEmbeddableInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts index dd3124c7d17ee..88aca4c07ee31 100644 --- a/src/plugins/expressions/common/expression_renderers/types.ts +++ b/src/plugins/expressions/common/expression_renderers/types.ts @@ -82,6 +82,7 @@ export interface IInterpreterRenderHandlers { reload: () => void; update: (params: any) => void; event: (event: any) => void; + hasCompatibleActions?: (event: any) => Promise; getRenderMode: () => RenderMode; uiState?: PersistedState; } diff --git a/src/plugins/expressions/public/loader.ts b/src/plugins/expressions/public/loader.ts index 983a344c0e1a1..e9e0fa18af6c3 100644 --- a/src/plugins/expressions/public/loader.ts +++ b/src/plugins/expressions/public/loader.ts @@ -64,6 +64,7 @@ export class ExpressionLoader { this.renderHandler = new ExpressionRenderHandler(element, { onRenderError: params && params.onRenderError, renderMode: params?.renderMode, + hasCompatibleActions: params?.hasCompatibleActions, }); this.render$ = this.renderHandler.render$; this.update$ = this.renderHandler.update$; diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 97ff00db0966c..6eb0e71c58e3f 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -532,7 +532,7 @@ export interface ExpressionRenderError extends Error { // @public (undocumented) export class ExpressionRenderHandler { // Warning: (ae-forgotten-export) The symbol "ExpressionRenderHandlerParams" needs to be exported by the entry point index.d.ts - constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial); + constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams); // (undocumented) destroy: () => void; // (undocumented) @@ -544,7 +544,7 @@ export class ExpressionRenderHandler { // (undocumented) render$: Observable; // (undocumented) - render: (data: any, uiState?: any) => Promise; + render: (value: any, uiState?: any) => Promise; // Warning: (ae-forgotten-export) The symbol "UpdateValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -888,6 +888,8 @@ export interface IExpressionLoaderParams { // (undocumented) disableCaching?: boolean; // (undocumented) + hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; + // (undocumented) inspectorAdapters?: Adapters; // Warning: (ae-forgotten-export) The symbol "RenderErrorHandlerFnType" needs to be exported by the entry point index.d.ts // @@ -917,6 +919,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) getRenderMode: () => RenderMode; // (undocumented) + hasCompatibleActions?: (event: any) => Promise; + // (undocumented) onDestroy: (fn: () => void) => void; // (undocumented) reload: () => void; diff --git a/src/plugins/expressions/public/render.test.ts b/src/plugins/expressions/public/render.test.ts index c44683f6779c0..3fc0100db489d 100644 --- a/src/plugins/expressions/public/render.test.ts +++ b/src/plugins/expressions/public/render.test.ts @@ -126,6 +126,31 @@ describe('ExpressionRenderHandler', () => { expect(getHandledError()!.message).toEqual('renderer error'); }); + it('should pass through provided "hasCompatibleActions" to the expression renderer', async () => { + const hasCompatibleActions = jest.fn(); + (getRenderersRegistry as jest.Mock).mockReturnValueOnce({ get: () => true }); + (getRenderersRegistry as jest.Mock).mockReturnValueOnce({ + get: () => ({ + render: (domNode: HTMLElement, config: unknown, handlers: IInterpreterRenderHandlers) => { + handlers.hasCompatibleActions!({ + foo: 'bar', + }); + }, + }), + }); + + const expressionRenderHandler = new ExpressionRenderHandler(element, { + onRenderError: mockMockErrorRenderFunction, + hasCompatibleActions, + }); + expect(hasCompatibleActions).toHaveBeenCalledTimes(0); + await expressionRenderHandler.render({ type: 'render', as: 'something' }); + expect(hasCompatibleActions).toHaveBeenCalledTimes(1); + expect(hasCompatibleActions.mock.calls[0][0]).toEqual({ + foo: 'bar', + }); + }); + it('sends a next observable once rendering is complete', () => { const expressionRenderHandler = new ExpressionRenderHandler(element); expect.assertions(1); diff --git a/src/plugins/expressions/public/render.ts b/src/plugins/expressions/public/render.ts index 4390033b5be60..717776a2861b4 100644 --- a/src/plugins/expressions/public/render.ts +++ b/src/plugins/expressions/public/render.ts @@ -29,8 +29,9 @@ import { getRenderersRegistry } from './services'; export type IExpressionRendererExtraHandlers = Record; export interface ExpressionRenderHandlerParams { - onRenderError: RenderErrorHandlerFnType; - renderMode: RenderMode; + onRenderError?: RenderErrorHandlerFnType; + renderMode?: RenderMode; + hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise; } export interface ExpressionRendererEvent { @@ -59,7 +60,11 @@ export class ExpressionRenderHandler { constructor( element: HTMLElement, - { onRenderError, renderMode }: Partial = {} + { + onRenderError, + renderMode, + hasCompatibleActions = async () => false, + }: ExpressionRenderHandlerParams = {} ) { this.element = element; @@ -96,17 +101,18 @@ export class ExpressionRenderHandler { getRenderMode: () => { return renderMode || 'display'; }, + hasCompatibleActions, }; } - render = async (data: any, uiState: any = {}) => { - if (!data || typeof data !== 'object') { + render = async (value: any, uiState: any = {}) => { + if (!value || typeof value !== 'object') { return this.handleRenderError(new Error('invalid data provided to the expression renderer')); } - if (data.type !== 'render' || !data.as) { - if (data.type === 'error') { - return this.handleRenderError(data.error); + if (value.type !== 'render' || !value.as) { + if (value.type === 'error') { + return this.handleRenderError(value.error); } else { return this.handleRenderError( new Error('invalid data provided to the expression renderer') @@ -114,15 +120,15 @@ export class ExpressionRenderHandler { } } - if (!getRenderersRegistry().get(data.as)) { - return this.handleRenderError(new Error(`invalid renderer id '${data.as}'`)); + if (!getRenderersRegistry().get(value.as)) { + return this.handleRenderError(new Error(`invalid renderer id '${value.as}'`)); } try { // Rendering is asynchronous, completed by handlers.done() await getRenderersRegistry() - .get(data.as)! - .render(this.element, data.value, { + .get(value.as)! + .render(this.element, value.value, { ...this.handlers, uiState, } as any); @@ -152,7 +158,7 @@ export class ExpressionRenderHandler { export function render( element: HTMLElement, data: any, - options?: Partial + options?: ExpressionRenderHandlerParams ): ExpressionRenderHandler { const handler = new ExpressionRenderHandler(element, options); handler.render(data); diff --git a/src/plugins/expressions/public/types/index.ts b/src/plugins/expressions/public/types/index.ts index 5bae985699476..f37107abbb716 100644 --- a/src/plugins/expressions/public/types/index.ts +++ b/src/plugins/expressions/public/types/index.ts @@ -25,6 +25,7 @@ import { SerializableState, RenderMode, } from '../../common'; +import { ExpressionRenderHandlerParams } from '../render'; /** * @deprecated @@ -56,6 +57,7 @@ export interface IExpressionLoaderParams { onRenderError?: RenderErrorHandlerFnType; searchSessionId?: string; renderMode?: RenderMode; + hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions']; } export interface ExpressionRenderError extends Error { diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 761ddba8f9270..7c1ab11f75027 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -736,6 +736,8 @@ export interface IInterpreterRenderHandlers { // (undocumented) getRenderMode: () => RenderMode; // (undocumented) + hasCompatibleActions?: (event: any) => Promise; + // (undocumented) onDestroy: (fn: () => void) => void; // (undocumented) reload: () => void; diff --git a/src/plugins/ui_actions/public/index.ts b/src/plugins/ui_actions/public/index.ts index b9f4a4a0426bf..d223c0abcccb7 100644 --- a/src/plugins/ui_actions/public/index.ts +++ b/src/plugins/ui_actions/public/index.ts @@ -50,6 +50,9 @@ export { visualizeFieldTrigger, VISUALIZE_GEO_FIELD_TRIGGER, visualizeGeoFieldTrigger, + ROW_CLICK_TRIGGER, + rowClickTrigger, + RowClickContext, } from './triggers'; export { TriggerContextMapping, diff --git a/src/plugins/ui_actions/public/plugin.ts b/src/plugins/ui_actions/public/plugin.ts index 87a1df959ccbd..fdb75e9a426e9 100644 --- a/src/plugins/ui_actions/public/plugin.ts +++ b/src/plugins/ui_actions/public/plugin.ts @@ -23,6 +23,7 @@ import { UiActionsService } from './service'; import { selectRangeTrigger, valueClickTrigger, + rowClickTrigger, applyFilterTrigger, visualizeFieldTrigger, visualizeGeoFieldTrigger, @@ -48,6 +49,7 @@ export class UiActionsPlugin implements Plugin { public setup(core: CoreSetup): UiActionsSetup { this.service.registerTrigger(selectRangeTrigger); this.service.registerTrigger(valueClickTrigger); + this.service.registerTrigger(rowClickTrigger); this.service.registerTrigger(applyFilterTrigger); this.service.registerTrigger(visualizeFieldTrigger); this.service.registerTrigger(visualizeGeoFieldTrigger); diff --git a/src/plugins/ui_actions/public/public.api.md b/src/plugins/ui_actions/public/public.api.md index ca27e19b247c2..2384dfab13c8c 100644 --- a/src/plugins/ui_actions/public/public.api.md +++ b/src/plugins/ui_actions/public/public.api.md @@ -133,6 +133,32 @@ export class IncompatibleActionError extends Error { // @public (undocumented) export function plugin(initializerContext: PluginInitializerContext): UiActionsPlugin; +// Warning: (ae-missing-release-tag) "ROW_CLICK_TRIGGER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ROW_CLICK_TRIGGER = "ROW_CLICK_TRIGGER"; + +// Warning: (ae-missing-release-tag) "RowClickContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface RowClickContext { + // (undocumented) + data: { + rowIndex: number; + table: Datatable; + columns?: string[]; + }; + // Warning: (ae-forgotten-export) The symbol "IEmbeddable" needs to be exported by the entry point index.d.ts + // + // (undocumented) + embeddable?: IEmbeddable; +} + +// Warning: (ae-missing-release-tag) "rowClickTrigger" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'>; + // Warning: (ae-missing-release-tag) "SELECT_RANGE_TRIGGER" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -170,6 +196,8 @@ export interface TriggerContextMapping { // // (undocumented) [APPLY_FILTER_TRIGGER]: ApplyGlobalFilterActionContext; + // (undocumented) + [ROW_CLICK_TRIGGER]: RowClickContext; // Warning: (ae-forgotten-export) The symbol "RangeSelectContext" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -234,14 +262,14 @@ export class UiActionsService { // // (undocumented) protected readonly actions: ActionRegistry; - readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; + readonly addTriggerAction: (triggerId: T, action: UiActionsActionDefinition | Action) => void; // (undocumented) - readonly attachAction: (triggerId: T, actionId: string) => void; + readonly attachAction: (triggerId: T, actionId: string) => void; readonly clear: () => void; // (undocumented) readonly detachAction: (triggerId: TriggerId, actionId: string) => void; // @deprecated (undocumented) - readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise; + readonly executeTriggerActions: (triggerId: T, context: TriggerContext) => Promise; // Warning: (ae-forgotten-export) The symbol "UiActionsExecutionService" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -252,11 +280,11 @@ export class UiActionsService { // Warning: (ae-forgotten-export) The symbol "TriggerContract" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly getTrigger: (triggerId: T) => TriggerContract; + readonly getTrigger: (triggerId: T) => TriggerContract; // (undocumented) - readonly getTriggerActions: (triggerId: T) => Action[]; + readonly getTriggerActions: (triggerId: T) => Action[]; // (undocumented) - readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; + readonly getTriggerCompatibleActions: (triggerId: T, context: TriggerContextMapping[T]) => Promise[]>; // (undocumented) readonly hasAction: (actionId: string) => boolean; // Warning: (ae-forgotten-export) The symbol "ActionContext" needs to be exported by the entry point index.d.ts @@ -341,6 +369,10 @@ export const visualizeFieldTrigger: Trigger<'VISUALIZE_FIELD_TRIGGER'>; export const visualizeGeoFieldTrigger: Trigger<'VISUALIZE_GEO_FIELD_TRIGGER'>; +// Warnings were encountered during analysis: +// +// src/plugins/ui_actions/public/triggers/row_click_trigger.ts:45:5 - (ae-forgotten-export) The symbol "Datatable" needs to be exported by the entry point index.d.ts + // (No @packageDocumentation comment for this package) ``` diff --git a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts index 4f0ab52501a95..59616dcf3f38d 100644 --- a/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts +++ b/src/plugins/ui_actions/public/service/ui_actions_execution_service.ts @@ -29,6 +29,7 @@ interface ExecuteActionTask { context: BaseContext; trigger: Trigger; defer: Defer; + alwaysShowPopup?: boolean; } export class UiActionsExecutionService { @@ -37,21 +38,25 @@ export class UiActionsExecutionService { constructor() {} - async execute({ - action, - context, - trigger, - }: { - action: Action; - context: BaseContext; - trigger: Trigger; - }): Promise { + async execute( + { + action, + context, + trigger, + }: { + action: Action; + context: BaseContext; + trigger: Trigger; + }, + alwaysShowPopup?: boolean + ): Promise { const shouldBatch = !(await action.shouldAutoExecute?.({ ...context, trigger })) ?? false; const task: ExecuteActionTask = { action, context, trigger, defer: createDefer(), + alwaysShowPopup: !!alwaysShowPopup, }; if (shouldBatch) { @@ -84,11 +89,23 @@ export class UiActionsExecutionService { setTimeout(() => { if (this.pendingTasks.size === 0) { const tasks = uniqBy(this.batchingQueue, (t) => t.action.id); - if (tasks.length === 1) { - this.executeSingleTask(tasks[0]); - } - if (tasks.length > 1) { - this.executeMultipleActions(tasks); + if (tasks.length > 0) { + let alwaysShowPopup = false; + for (const task of tasks) { + if (task.alwaysShowPopup) { + alwaysShowPopup = true; + break; + } + } + if (alwaysShowPopup) { + this.showActionPopupMenu(tasks); + } else { + if (tasks.length === 1) { + this.executeSingleTask(tasks[0]); + } else if (tasks.length > 1) { + this.showActionPopupMenu(tasks); + } + } } this.batchingQueue.splice(0, this.batchingQueue.length); @@ -108,7 +125,7 @@ export class UiActionsExecutionService { } } - private async executeMultipleActions(tasks: ExecuteActionTask[]) { + private async showActionPopupMenu(tasks: ExecuteActionTask[]) { const panels = await buildContextMenuForActions({ actions: tasks.map(({ action, context, trigger }) => ({ action, diff --git a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts index af2510467ba87..51ba165ba730b 100644 --- a/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts +++ b/src/plugins/ui_actions/public/tests/execute_trigger_actions.test.ts @@ -143,7 +143,32 @@ test('shows a context menu when more than one action is mapped to a trigger', as const start = doStart(); const context = {}; - await start.executeTriggerActions('MY-TRIGGER' as TriggerId, context); + await start.getTrigger('MY-TRIGGER' as TriggerId)!.exec(context); + + jest.runAllTimers(); + + await waitFor(() => { + expect(executeFn).toBeCalledTimes(0); + expect(openContextMenu).toHaveBeenCalledTimes(1); + }); +}); + +test('shows a context menu when there is only one action mapped to a trigger and "alwaysShowPopup" is set', async () => { + const { setup, doStart } = uiActions; + const trigger: Trigger = { + id: 'MY-TRIGGER' as TriggerId, + title: 'My trigger', + }; + const action1 = createTestAction('test1', () => true); + + setup.registerTrigger(trigger); + setup.addTriggerAction(trigger.id, action1); + + expect(openContextMenu).toHaveBeenCalledTimes(0); + + const start = doStart(); + const context = {}; + await start.getTrigger('MY-TRIGGER' as TriggerId)!.exec(context, true); jest.runAllTimers(); diff --git a/src/plugins/ui_actions/public/triggers/index.ts b/src/plugins/ui_actions/public/triggers/index.ts index b7039d287c6e2..ecbf4d1f7b988 100644 --- a/src/plugins/ui_actions/public/triggers/index.ts +++ b/src/plugins/ui_actions/public/triggers/index.ts @@ -22,6 +22,7 @@ export * from './trigger_contract'; export * from './trigger_internal'; export * from './select_range_trigger'; export * from './value_click_trigger'; +export * from './row_click_trigger'; export * from './apply_filter_trigger'; export * from './visualize_field_trigger'; export * from './visualize_geo_field_trigger'; diff --git a/src/plugins/ui_actions/public/triggers/row_click_trigger.ts b/src/plugins/ui_actions/public/triggers/row_click_trigger.ts new file mode 100644 index 0000000000000..87bca03f8c3ba --- /dev/null +++ b/src/plugins/ui_actions/public/triggers/row_click_trigger.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { i18n } from '@kbn/i18n'; +import { IEmbeddable } from '../../../embeddable/public'; +import { Trigger } from '.'; +import { Datatable } from '../../../expressions'; + +export const ROW_CLICK_TRIGGER = 'ROW_CLICK_TRIGGER'; + +export const rowClickTrigger: Trigger<'ROW_CLICK_TRIGGER'> = { + id: ROW_CLICK_TRIGGER, + title: i18n.translate('uiActions.triggers.rowClickTitle', { + defaultMessage: 'Table row click', + }), + description: i18n.translate('uiActions.triggers.rowClickkDescription', { + defaultMessage: 'A click on a table row', + }), +}; + +export interface RowClickContext { + embeddable?: IEmbeddable; + data: { + /** + * Row index, starting from 0, where user clicked. + */ + rowIndex: number; + + table: Datatable; + + /** + * Sorted list column IDs that were visible to the user. Useful when only + * a subset of datatable columns should be used. + */ + columns?: string[]; + }; +} diff --git a/src/plugins/ui_actions/public/triggers/trigger_contract.ts b/src/plugins/ui_actions/public/triggers/trigger_contract.ts index ba1c5a693f937..04a75cb3a53d0 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_contract.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_contract.ts @@ -49,7 +49,7 @@ export class TriggerContract { /** * Use this method to execute action attached to this trigger. */ - public readonly exec = async (context: TriggerContextMapping[T]) => { - await this.internal.execute(context); + public readonly exec = async (context: TriggerContextMapping[T], alwaysShowPopup?: boolean) => { + await this.internal.execute(context, alwaysShowPopup); }; } diff --git a/src/plugins/ui_actions/public/triggers/trigger_internal.ts b/src/plugins/ui_actions/public/triggers/trigger_internal.ts index c766b5c798ecb..fd43a020504c0 100644 --- a/src/plugins/ui_actions/public/triggers/trigger_internal.ts +++ b/src/plugins/ui_actions/public/triggers/trigger_internal.ts @@ -31,17 +31,20 @@ export class TriggerInternal { constructor(public readonly service: UiActionsService, public readonly trigger: Trigger) {} - public async execute(context: TriggerContextMapping[T]) { + public async execute(context: TriggerContextMapping[T], alwaysShowPopup?: boolean) { const triggerId = this.trigger.id; const actions = await this.service.getTriggerCompatibleActions!(triggerId, context); await Promise.all([ actions.map((action) => - this.service.executionService.execute({ - action, - context, - trigger: this.trigger, - }) + this.service.executionService.execute( + { + action, + context, + trigger: this.trigger, + }, + alwaysShowPopup + ) ), ]); } diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index 0be3c19fc1c4d..0266a755be926 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -22,10 +22,12 @@ import { TriggerInternal } from './triggers/trigger_internal'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, + ROW_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, VISUALIZE_FIELD_TRIGGER, VISUALIZE_GEO_FIELD_TRIGGER, DEFAULT_TRIGGER, + RowClickContext, } from './triggers'; import type { RangeSelectContext, ValueClickContext } from '../../embeddable/public'; import type { ApplyGlobalFilterActionContext } from '../../data/public'; @@ -49,6 +51,7 @@ export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; [SELECT_RANGE_TRIGGER]: RangeSelectContext; [VALUE_CLICK_TRIGGER]: ValueClickContext; + [ROW_CLICK_TRIGGER]: RowClickContext; [APPLY_FILTER_TRIGGER]: ApplyGlobalFilterActionContext; [VISUALIZE_FIELD_TRIGGER]: VisualizeFieldContext; [VISUALIZE_GEO_FIELD_TRIGGER]: VisualizeFieldContext; diff --git a/src/plugins/visualizations/public/embeddable/events.ts b/src/plugins/visualizations/public/embeddable/events.ts index 52cac59fbffaa..41e52c3ac1327 100644 --- a/src/plugins/visualizations/public/embeddable/events.ts +++ b/src/plugins/visualizations/public/embeddable/events.ts @@ -21,16 +21,19 @@ import { APPLY_FILTER_TRIGGER, SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, -} from '../../../../plugins/ui_actions/public'; + ROW_CLICK_TRIGGER, +} from '../../../ui_actions/public'; export interface VisEventToTrigger { ['applyFilter']: typeof APPLY_FILTER_TRIGGER; ['brush']: typeof SELECT_RANGE_TRIGGER; ['filter']: typeof VALUE_CLICK_TRIGGER; + ['tableRowContextMenuClick']: typeof ROW_CLICK_TRIGGER; } export const VIS_EVENT_TO_TRIGGER: VisEventToTrigger = { applyFilter: APPLY_FILTER_TRIGGER, brush: SELECT_RANGE_TRIGGER, filter: VALUE_CLICK_TRIGGER, + tableRowContextMenuClick: ROW_CLICK_TRIGGER, }; diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts new file mode 100644 index 0000000000000..e0627c521bb79 --- /dev/null +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/test/data.ts @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DatatableColumnType } from '../../../../../../../src/plugins/expressions/common'; +import { + Embeddable, + EmbeddableInput, + EmbeddableOutput, +} from '../../../../../../../src/plugins/embeddable/public'; + +export const createPoint = ({ + field, + value, +}: { + field: string; + value: string | null | number | boolean; +}) => ({ + table: { + columns: [ + { + name: field, + id: '1-1', + meta: { + type: 'date' as DatatableColumnType, + field, + source: 'esaggs', + sourceParams: { + type: 'histogram', + indexPatternId: 'logstash-*', + interval: 30, + otherBucket: true, + }, + }, + }, + ], + rows: [ + { + '1-1': '2048', + }, + ], + }, + column: 0, + row: 0, + value, +}); + +export const rowClickData = { + rowIndex: 1, + table: { + type: 'datatable', + rows: [ + { + '6ced5344-2596-4545-b626-8b449924e2d4': 'IT', + '6890e417-c5f1-4565-a45c-92f55380e14c': '0', + '93b8ef16-2483-45b8-ad27-6cc1f790578b': 13, + 'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 0, + 'e0719f1a-04fb-4036-a63c-c25deac3f011': 7, + }, + { + '6ced5344-2596-4545-b626-8b449924e2d4': 'IT', + '6890e417-c5f1-4565-a45c-92f55380e14c': '2.25', + '93b8ef16-2483-45b8-ad27-6cc1f790578b': 3, + 'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 0, + 'e0719f1a-04fb-4036-a63c-c25deac3f011': 2, + }, + { + '6ced5344-2596-4545-b626-8b449924e2d4': 'IT', + '6890e417-c5f1-4565-a45c-92f55380e14c': '0.020939215995129826', + '93b8ef16-2483-45b8-ad27-6cc1f790578b': 2, + 'b0c5dcc2-4012-4d7e-b983-0e089badc43c': 12.490584373474121, + 'e0719f1a-04fb-4036-a63c-c25deac3f011': 1, + }, + ], + columns: [ + { + id: '6ced5344-2596-4545-b626-8b449924e2d4', + name: 'Top values of DestCountry', + meta: { + type: 'string', + field: 'DestCountry', + index: 'kibana_sample_data_flights', + params: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: '(missing value)', + }, + }, + source: 'esaggs', + }, + }, + { + id: '6890e417-c5f1-4565-a45c-92f55380e14c', + name: 'Top values of FlightTimeHour', + meta: { + type: 'string', + field: 'FlightTimeHour', + index: 'kibana_sample_data_flights', + params: { + id: 'terms', + params: { + id: 'string', + otherBucketLabel: 'Other', + missingBucketLabel: '(missing value)', + }, + }, + source: 'esaggs', + }, + }, + { + id: '93b8ef16-2483-45b8-ad27-6cc1f790578b', + name: 'Count of records', + meta: { + type: 'number', + index: 'kibana_sample_data_flights', + params: { + id: 'number', + }, + }, + }, + { + id: 'b0c5dcc2-4012-4d7e-b983-0e089badc43c', + name: 'Average of DistanceMiles', + meta: { + type: 'number', + field: 'DistanceMiles', + index: 'kibana_sample_data_flights', + params: { + id: 'number', + }, + }, + }, + { + id: 'e0719f1a-04fb-4036-a63c-c25deac3f011', + name: 'Unique count of OriginAirportID', + meta: { + type: 'string', + field: 'OriginAirportID', + index: 'kibana_sample_data_flights', + params: { + id: 'number', + }, + }, + }, + ], + }, + columns: [ + '6ced5344-2596-4545-b626-8b449924e2d4', + '6890e417-c5f1-4565-a45c-92f55380e14c', + '93b8ef16-2483-45b8-ad27-6cc1f790578b', + 'b0c5dcc2-4012-4d7e-b983-0e089badc43c', + 'e0719f1a-04fb-4036-a63c-c25deac3f011', + ], +}; + +interface TestInput extends EmbeddableInput { + savedObjectId?: string; +} + +interface TestOutput extends EmbeddableOutput { + indexPatterns?: Array<{ id: string }>; +} + +export class TestEmbeddable extends Embeddable { + type = 'test'; + + destroy() {} + reload() {} +} diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts index 79d380991f5fd..d9f63f233e1c2 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.test.ts @@ -7,6 +7,11 @@ import { UrlDrilldown, ActionContext, Config } from './url_drilldown'; import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables'; import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common'; +import { createPoint, rowClickData, TestEmbeddable } from './test/data'; +import { + VALUE_CLICK_TRIGGER, + ROW_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; const mockDataPoints = [ { @@ -99,7 +104,8 @@ describe('UrlDrilldown', () => { embeddable: mockEmbeddable, }; - await expect(urlDrilldown.isCompatible(config, context)).resolves.toBe(true); + const result = urlDrilldown.isCompatible(config, context); + await expect(result).resolves.toBe(true); }); test('not compatible if url is invalid', async () => { @@ -168,4 +174,199 @@ describe('UrlDrilldown', () => { expect(mockNavigateToUrl).not.toBeCalled(); }); }); + + describe('variables', () => { + const embeddable1 = new TestEmbeddable( + { + id: 'test', + title: 'The Title', + savedObjectId: 'SAVED_OBJECT_IDxx', + }, + { + indexPatterns: [{ id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }], + } + ); + const data: any = { + data: [ + createPoint({ field: 'field0', value: 'value0' }), + createPoint({ field: 'field1', value: 'value1' }), + createPoint({ field: 'field2', value: 'value2' }), + ], + }; + + const embeddable2 = new TestEmbeddable( + { + id: 'the-id', + query: { + language: 'C++', + query: 'std::cout << 123;', + }, + timeRange: { + from: 'FROM', + to: 'TO', + }, + filters: [ + { + meta: { + alias: 'asdf', + disabled: false, + negate: false, + }, + }, + ], + savedObjectId: 'SAVED_OBJECT_ID', + }, + { + title: 'The Title', + indexPatterns: [ + { id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }, + { id: 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' }, + ], + } + ); + + describe('getRuntimeVariables()', () => { + test('builds runtime variables for VALUE_CLICK_TRIGGER trigger', () => { + const variables = urlDrilldown.getRuntimeVariables({ + embeddable: embeddable1, + data, + }); + + expect(variables).toMatchObject({ + kibanaUrl: 'http://localhost:5601/', + context: { + panel: { + id: 'test', + title: 'The Title', + savedObjectId: 'SAVED_OBJECT_IDxx', + indexPatternId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + }, + }, + event: { + key: 'field0', + value: 'value0', + negate: false, + points: [ + { + value: 'value0', + key: 'field0', + }, + { + value: 'value1', + key: 'field1', + }, + { + value: 'value2', + key: 'field2', + }, + ], + }, + }); + }); + + test('builds runtime variables for ROW_CLICK_TRIGGER trigger', () => { + const variables = urlDrilldown.getRuntimeVariables({ + embeddable: embeddable2, + data: rowClickData as any, + }); + + expect(variables).toMatchObject({ + kibanaUrl: 'http://localhost:5601/', + context: { + panel: { + id: 'the-id', + title: 'The Title', + savedObjectId: 'SAVED_OBJECT_ID', + query: { + language: 'C++', + query: 'std::cout << 123;', + }, + timeRange: { + from: 'FROM', + to: 'TO', + }, + filters: [ + { + meta: { + alias: 'asdf', + disabled: false, + negate: false, + }, + }, + ], + }, + }, + event: { + rowIndex: 1, + values: ['IT', '2.25', 3, 0, 2], + keys: ['DestCountry', 'FlightTimeHour', '', 'DistanceMiles', 'OriginAirportID'], + columnNames: [ + 'Top values of DestCountry', + 'Top values of FlightTimeHour', + 'Count of records', + 'Average of DistanceMiles', + 'Unique count of OriginAirportID', + ], + }, + }); + }); + }); + + describe('getVariableList()', () => { + test('builds variable list for VALUE_CLICK_TRIGGER trigger', () => { + const list = urlDrilldown.getVariableList({ + triggers: [VALUE_CLICK_TRIGGER], + embeddable: embeddable1, + }); + + const expectedList = [ + 'event.key', + 'event.value', + 'event.negate', + 'event.points', + + 'context.panel.id', + 'context.panel.title', + 'context.panel.indexPatternId', + 'context.panel.savedObjectId', + + 'kibanaUrl', + ]; + + for (const expectedItem of expectedList) { + expect(list.includes(expectedItem)).toBe(true); + } + }); + + test('builds variable list for ROW_CLICK_TRIGGER trigger', () => { + const list = urlDrilldown.getVariableList({ + triggers: [ROW_CLICK_TRIGGER], + embeddable: embeddable2, + }); + + const expectedList = [ + 'event.columnNames', + 'event.keys', + 'event.rowIndex', + 'event.values', + + 'context.panel.id', + 'context.panel.title', + 'context.panel.filters', + 'context.panel.query.language', + 'context.panel.query.query', + 'context.panel.indexPatternIds', + 'context.panel.savedObjectId', + 'context.panel.timeRange.from', + 'context.panel.timeRange.to', + + 'kibanaUrl', + ]; + + for (const expectedItem of expectedList) { + expect(list.includes(expectedItem)).toBe(true); + } + }); + }); + }); }); diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx index 807dfeed21d1f..3a989c1b0b4cd 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { getFlattenedObject } from '@kbn/std'; import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public'; import { ChartActionContext, @@ -13,6 +14,7 @@ import { } from '../../../../../../src/plugins/embeddable/public'; import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public'; import { + ROW_CLICK_TRIGGER, SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, } from '../../../../../../src/plugins/ui_actions/public'; @@ -22,11 +24,10 @@ import { UrlDrilldownConfig, UrlDrilldownCollectConfig, urlDrilldownValidateUrlTemplate, - urlDrilldownBuildScope, urlDrilldownCompileUrl, UiActionsEnhancedBaseActionFactoryContext as BaseActionFactoryContext, } from '../../../../ui_actions_enhanced/public'; -import { getContextScope, getEventScope, getMockEventScope } from './url_drilldown_scope'; +import { getPanelVariables, getEventScope, getEventVariableList } from './url_drilldown_scope'; import { txtUrlDrilldownDisplayName } from './i18n'; interface UrlDrilldownDeps { @@ -39,9 +40,11 @@ interface UrlDrilldownDeps { export type ActionContext = ChartActionContext; export type Config = UrlDrilldownConfig; export type UrlTrigger = - | typeof CONTEXT_MENU_TRIGGER | typeof VALUE_CLICK_TRIGGER - | typeof SELECT_RANGE_TRIGGER; + | typeof SELECT_RANGE_TRIGGER + | typeof ROW_CLICK_TRIGGER + | typeof CONTEXT_MENU_TRIGGER; + export interface ActionFactoryContext extends BaseActionFactoryContext { embeddable?: IEmbeddable; } @@ -65,7 +68,7 @@ export class UrlDrilldown implements Drilldown = ({ @@ -74,12 +77,12 @@ export class UrlDrilldown implements Drilldown { // eslint-disable-next-line react-hooks/rules-of-hooks - const scope = React.useMemo(() => this.buildEditorScope(context), [context]); + const variables = React.useMemo(() => this.getVariableList(context), [context]); return ( @@ -93,19 +96,13 @@ export class UrlDrilldown implements Drilldown { - const { isValid } = urlDrilldownValidateUrlTemplate(config.url, this.buildEditorScope(context)); - return isValid; + public readonly isConfigValid = (config: Config): config is Config => { + return !!config.url.template; }; public readonly isCompatible = async (config: Config, context: ActionContext) => { - const { isValid, error } = urlDrilldownValidateUrlTemplate( - config.url, - await this.buildRuntimeScope(context) - ); + const scope = this.getRuntimeVariables(context); + const { isValid, error } = urlDrilldownValidateUrlTemplate(config.url, scope); if (!isValid) { // eslint-disable-next-line no-console @@ -117,11 +114,13 @@ export class UrlDrilldown implements Drilldown - urlDrilldownCompileUrl(config.url.template, this.buildRuntimeScope(context)); + public readonly getHref = async (config: Config, context: ActionContext) => { + const scope = this.getRuntimeVariables(context); + return urlDrilldownCompileUrl(config.url.template, scope); + }; public readonly execute = async (config: Config, context: ActionContext) => { - const url = urlDrilldownCompileUrl(config.url.template, this.buildRuntimeScope(context)); + const url = urlDrilldownCompileUrl(config.url.template, this.getRuntimeVariables(context)); if (config.openInNewTab) { window.open(url, '_blank', 'noopener'); } else { @@ -129,19 +128,23 @@ export class UrlDrilldown implements Drilldown { - return urlDrilldownBuildScope({ - globalScope: this.deps.getGlobalScope(), - contextScope: getContextScope(context), - eventScope: getMockEventScope(context.triggers), - }); + public readonly getRuntimeVariables = (context: ActionContext) => { + return { + ...this.deps.getGlobalScope(), + context: { + panel: getPanelVariables(context), + }, + event: getEventScope(context), + }; }; - private buildRuntimeScope = (context: ActionContext) => { - return urlDrilldownBuildScope({ - globalScope: this.deps.getGlobalScope(), - contextScope: getContextScope(context), - eventScope: getEventScope(context), - }); + public readonly getVariableList = (context: ActionFactoryContext): string[] => { + const eventVariables = getEventVariableList(context); + const contextVariables = Object.keys(getFlattenedObject(getPanelVariables(context))).map( + (key) => 'context.panel.' + key + ); + const globalVariables = Object.keys(getFlattenedObject(this.deps.getGlobalScope())); + + return [...eventVariables.sort(), ...contextVariables.sort(), ...globalVariables.sort()]; }; } diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts index a93e150deee8f..5917737d15eda 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.test.ts @@ -6,46 +6,15 @@ import { getEventScope, - getMockEventScope, ValueClickTriggerEventScope, + getEventVariableList, + getPanelVariables, } from './url_drilldown_scope'; -import { DatatableColumnType } from '../../../../../../src/plugins/expressions/common'; - -const createPoint = ({ - field, - value, -}: { - field: string; - value: string | null | number | boolean; -}) => ({ - table: { - columns: [ - { - name: field, - id: '1-1', - meta: { - type: 'date' as DatatableColumnType, - field, - source: 'esaggs', - sourceParams: { - type: 'histogram', - indexPatternId: 'logstash-*', - interval: 30, - otherBucket: true, - }, - }, - }, - ], - rows: [ - { - '1-1': '2048', - }, - ], - }, - column: 0, - row: 0, - value, -}); +import { + RowClickContext, + ROW_CLICK_TRIGGER, +} from '../../../../../../src/plugins/ui_actions/public'; +import { createPoint, rowClickData, TestEmbeddable } from './test/data'; describe('VALUE_CLICK_TRIGGER', () => { describe('supports `points[]`', () => { @@ -80,33 +49,6 @@ describe('VALUE_CLICK_TRIGGER', () => { ] `); }); - - test('getMockEventScope()', () => { - const mockEventScope = getMockEventScope([ - 'VALUE_CLICK_TRIGGER', - ]) as ValueClickTriggerEventScope; - expect(mockEventScope.points.length).toBeGreaterThan(3); - expect(mockEventScope.points).toMatchInlineSnapshot(` - Array [ - Object { - "key": "event.points.0.key", - "value": "event.points.0.value", - }, - Object { - "key": "event.points.1.key", - "value": "event.points.1.value", - }, - Object { - "key": "event.points.2.key", - "value": "event.points.2.value", - }, - Object { - "key": "event.points.3.key", - "value": "event.points.3.value", - }, - ] - `); - }); }); describe('handles undefined, null or missing values', () => { @@ -131,11 +73,221 @@ describe('VALUE_CLICK_TRIGGER', () => { }); }); -describe('CONTEXT_MENU_TRIGGER', () => { - test('getMockEventScope() results in empty scope', () => { - const mockEventScope = getMockEventScope([ - 'CONTEXT_MENU_TRIGGER', - ]) as ValueClickTriggerEventScope; - expect(mockEventScope).toEqual({}); +describe('ROW_CLICK_TRIGGER', () => { + test('getEventVariableList() returns correct list of runtime variables', () => { + const vars = getEventVariableList({ + triggers: [ROW_CLICK_TRIGGER], + }); + expect(vars).toEqual(['event.rowIndex', 'event.values', 'event.keys', 'event.columnNames']); + }); + + test('getEventScope() returns correct variables for row click trigger', () => { + const context = ({ + embeddable: {}, + data: rowClickData as any, + } as unknown) as RowClickContext; + const res = getEventScope(context); + + expect(res).toEqual({ + rowIndex: 1, + values: ['IT', '2.25', 3, 0, 2], + keys: ['DestCountry', 'FlightTimeHour', '', 'DistanceMiles', 'OriginAirportID'], + columnNames: [ + 'Top values of DestCountry', + 'Top values of FlightTimeHour', + 'Count of records', + 'Average of DistanceMiles', + 'Unique count of OriginAirportID', + ], + }); + }); +}); + +describe('getPanelVariables()', () => { + test('returns only ID for empty embeddable', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + }, + {} + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + }); + }); + + test('returns title as specified in input', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + title: 'title1', + }, + {} + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + title: 'title1', + }); + }); + + test('returns output title if input and output titles are specified', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + title: 'title1', + }, + { + title: 'title2', + } + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + title: 'title2', + }); + }); + + test('returns title from output if title in input is missing', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + }, + { + title: 'title2', + } + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + title: 'title2', + }); + }); + + test('returns saved object ID from output', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + savedObjectId: '5678', + }, + { + savedObjectId: '1234', + } + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + savedObjectId: '1234', + }); + }); + + test('returns saved object ID from input if it is not set on output', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + savedObjectId: '5678', + }, + {} + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + savedObjectId: '5678', + }); + }); + + test('returns query, timeRange and filters from input', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + query: { + language: 'C++', + query: 'std::cout << 123;', + }, + timeRange: { + from: 'FROM', + to: 'TO', + }, + filters: [ + { + meta: { + alias: 'asdf', + disabled: false, + negate: false, + }, + }, + ], + }, + {} + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + query: { + language: 'C++', + query: 'std::cout << 123;', + }, + timeRange: { + from: 'FROM', + to: 'TO', + }, + filters: [ + { + meta: { + alias: 'asdf', + disabled: false, + negate: false, + }, + }, + ], + }); + }); + + test('returns a single index pattern from output', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + }, + { + indexPatterns: [{ id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }], + } + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + indexPatternId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + }); + }); + + test('returns multiple index patterns from output', () => { + const embeddable = new TestEmbeddable( + { + id: 'test', + }, + { + indexPatterns: [ + { id: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }, + { id: 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' }, + ], + } + ); + const vars = getPanelVariables({ embeddable }); + + expect(vars).toEqual({ + id: 'test', + indexPatternIds: [ + 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', + 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy', + ], + }); }); }); diff --git a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts index 234af380689e9..3e5fc0a968d39 100644 --- a/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts +++ b/x-pack/plugins/drilldowns/url_drilldown/public/lib/url_drilldown_scope.ts @@ -14,48 +14,54 @@ import { IEmbeddable, isRangeSelectTriggerContext, isValueClickTriggerContext, + isRowClickTriggerContext, isContextMenuTriggerContext, RangeSelectContext, ValueClickContext, + EmbeddableOutput, } from '../../../../../../src/plugins/embeddable/public'; -import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown'; +import type { ActionContext, ActionFactoryContext } from './url_drilldown'; import { SELECT_RANGE_TRIGGER, + RowClickContext, VALUE_CLICK_TRIGGER, + ROW_CLICK_TRIGGER, } from '../../../../../../src/plugins/ui_actions/public'; -type ContextScopeInput = ActionContext | ActionFactoryContext; - /** * Part of context scope extracted from an embeddable * Expose on the scope as: `{{context.panel.id}}`, `{{context.panel.filters.[0]}}` */ interface EmbeddableUrlDrilldownContextScope { + /** + * ID of the embeddable panel. + */ id: string; + + /** + * Title of the embeddable panel. + */ title?: string; - query?: Query; - filters?: Filter[]; - timeRange?: TimeRange; - savedObjectId?: string; + /** - * In case panel supports only 1 index patterns + * In case panel supports only 1 index pattern. */ indexPatternId?: string; + /** - * In case panel supports more then 1 index patterns + * In case panel supports more then 1 index pattern. */ indexPatternIds?: string[]; -} -/** - * Url drilldown context scope - * `{{context.$}}` - */ -interface UrlDrilldownContextScope { - panel?: EmbeddableUrlDrilldownContextScope; + query?: Query; + filters?: Filter[]; + timeRange?: TimeRange; + savedObjectId?: string; } -export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilldownContextScope { +export function getPanelVariables(contextScopeInput: { + embeddable?: IEmbeddable; +}): EmbeddableUrlDrilldownContextScope { function hasEmbeddable(val: unknown): val is { embeddable: IEmbeddable } { if (val && typeof val === 'object' && 'embeddable' in val) return true; return false; @@ -64,41 +70,52 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld throw new Error( "UrlDrilldown [getContextScope] can't build scope because embeddable object is missing in context" ); - const embeddable = contextScopeInput.embeddable; + + return getEmbeddableVariables(embeddable); +} + +function hasSavedObjectId(obj: Record): obj is { savedObjectId: string } { + return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string'; +} + +/** + * @todo Same functionality is implemented in x-pack/plugins/discover_enhanced/public/actions/explore_data/shared.ts, + * combine both implementations into a common approach. + */ +function getIndexPatternIds(output: EmbeddableOutput): string[] { + function hasIndexPatterns( + _output: Record + ): _output is { indexPatterns: Array<{ id?: string }> } { + return ( + 'indexPatterns' in _output && + Array.isArray(_output.indexPatterns) && + _output.indexPatterns.length > 0 + ); + } + return hasIndexPatterns(output) + ? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[]) + : []; +} + +export function getEmbeddableVariables( + embeddable: IEmbeddable +): EmbeddableUrlDrilldownContextScope { const input = embeddable.getInput(); const output = embeddable.getOutput(); - function hasSavedObjectId(obj: Record): obj is { savedObjectId: string } { - return 'savedObjectId' in obj && typeof obj.savedObjectId === 'string'; - } - function getIndexPatternIds(): string[] { - function hasIndexPatterns( - _output: Record - ): _output is { indexPatterns: Array<{ id?: string }> } { - return ( - 'indexPatterns' in _output && - Array.isArray(_output.indexPatterns) && - _output.indexPatterns.length > 0 - ); - } - return hasIndexPatterns(output) - ? (output.indexPatterns.map((ip) => ip.id).filter(Boolean) as string[]) - : []; - } - const indexPatternsIds = getIndexPatternIds(); - return { - panel: cleanEmptyKeys({ - id: input.id, - title: output.title ?? input.title, - savedObjectId: - output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined), - query: input.query, - timeRange: input.timeRange, - filters: input.filters, - indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined, - indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined, - }), - }; + const indexPatternsIds = getIndexPatternIds(output); + + return deleteUndefinedKeys({ + id: input.id, + title: output.title ?? input.title, + savedObjectId: + output.savedObjectId ?? (hasSavedObjectId(input) ? input.savedObjectId : undefined), + query: input.query, + timeRange: input.timeRange, + filters: input.filters, + indexPatternIds: indexPatternsIds.length > 1 ? indexPatternsIds : undefined, + indexPatternId: indexPatternsIds.length === 1 ? indexPatternsIds[0] : undefined, + }); } /** @@ -108,7 +125,9 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld export type UrlDrilldownEventScope = | ValueClickTriggerEventScope | RangeSelectTriggerEventScope + | RowClickTriggerEventScope | ContextMenuTriggerEventScope; + export type EventScopeInput = ActionContext; export interface ValueClickTriggerEventScope { key?: string; @@ -122,6 +141,12 @@ export interface RangeSelectTriggerEventScope { to?: string | number; } +export interface RowClickTriggerEventScope { + rowIndex: number; + values: Primitive[]; + keys: string[]; + columnNames: string[]; +} export type ContextMenuTriggerEventScope = object; export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEventScope { @@ -129,6 +154,8 @@ export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEve return getEventScopeFromRangeSelectTriggerContext(eventScopeInput); } else if (isValueClickTriggerContext(eventScopeInput)) { return getEventScopeFromValueClickTriggerContext(eventScopeInput); + } else if (isRowClickTriggerContext(eventScopeInput)) { + return getEventScopeFromRowClickTriggerContext(eventScopeInput); } else if (isContextMenuTriggerContext(eventScopeInput)) { return {}; } else { @@ -141,7 +168,7 @@ function getEventScopeFromRangeSelectTriggerContext( ): RangeSelectTriggerEventScope { const { table, column: columnIndex, range } = eventScopeInput.data; const column = table.columns[columnIndex]; - return cleanEmptyKeys({ + return deleteUndefinedKeys({ key: toPrimitiveOrUndefined(column?.meta.field) as string, from: toPrimitiveOrUndefined(range[0]) as string | number | undefined, to: toPrimitiveOrUndefined(range[range.length - 1]) as string | number | undefined, @@ -160,7 +187,7 @@ function getEventScopeFromValueClickTriggerContext( }; }); - return cleanEmptyKeys({ + return deleteUndefinedKeys({ key: points[0]?.key, value: points[0]?.value, negate, @@ -168,37 +195,53 @@ function getEventScopeFromValueClickTriggerContext( }); } -/** - * @remarks - * Difference between `event` and `context` variables, is that real `context` variables are available during drilldown creation (e.g. embeddable panel) - * `event` variables are mapped from trigger context. Since there is no trigger context during drilldown creation, we have to provide some _mock_ variables for validating and previewing the URL - */ -export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventScope { - if (trigger === SELECT_RANGE_TRIGGER) { - return { - key: 'event.key', - from: new Date(Date.now() - 15 * 60 * 1000).toISOString(), // 15 minutes ago - to: new Date().toISOString(), - }; +function getEventScopeFromRowClickTriggerContext({ + embeddable, + data, +}: RowClickContext): RowClickTriggerEventScope { + const { rowIndex } = data; + const columns = data.columns || data.table.columns.map(({ id }) => id); + const values: Primitive[] = []; + const keys: string[] = []; + const columnNames: string[] = []; + const row = data.table.rows[rowIndex]; + + for (const columnId of columns) { + const column = data.table.columns.find(({ id }) => id === columnId); + if (!column) { + // This should never happe, but in case it does we log data necessary for debugging. + // eslint-disable-next-line no-console + console.error(data, embeddable ? `Embeddable [${embeddable.getTitle()}]` : null); + throw new Error('Could not find a datatable column.'); + } + values.push(row[columnId]); + keys.push(column.meta.field || ''); + columnNames.push(column.name || column.meta.field || ''); } - if (trigger === VALUE_CLICK_TRIGGER) { - // number of mock points to generate - // should be larger or equal of any possible data points length emitted by VALUE_CLICK_TRIGGER - const nPoints = 4; - const points = new Array(nPoints).fill(0).map((_, index) => ({ - key: `event.points.${index}.key`, - value: `event.points.${index}.value`, - })); - return { - key: `event.key`, - value: `event.value`, - negate: false, - points, - }; + const scope: RowClickTriggerEventScope = { + rowIndex, + values, + keys, + columnNames, + }; + + return scope; +} + +export function getEventVariableList(context: ActionFactoryContext): string[] { + const [trigger] = context.triggers; + + switch (trigger) { + case SELECT_RANGE_TRIGGER: + return ['event.key', 'event.from', 'event.to']; + case VALUE_CLICK_TRIGGER: + return ['event.key', 'event.value', 'event.negate', 'event.points']; + case ROW_CLICK_TRIGGER: + return ['event.rowIndex', 'event.values', 'event.keys', 'event.columnNames']; } - return {}; + return []; } type Primitive = string | number | boolean | null; @@ -210,7 +253,7 @@ function toPrimitiveOrUndefined(v: unknown): Primitive | undefined { return String(v); } -function cleanEmptyKeys>(obj: T): T { +function deleteUndefinedKeys>(obj: T): T { Object.keys(obj).forEach((key) => { if (obj[key] === undefined) { delete obj[key]; diff --git a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap index 9c7bdc3397f9c..d340d002b242b 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/datatable_visualization/__snapshots__/expression.test.tsx.snap @@ -1,5 +1,60 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`datatable_expression DatatableComponent it renders actions column when there are row actions 1`] = ` + + + +`; + exports[`datatable_expression DatatableComponent it renders the title and value 1`] = ` { ).toMatchSnapshot(); }); + test('it renders actions column when there are row actions', () => { + const { data, args } = sampleArgs(); + + expect( + shallow( + x as IFieldFormat} + onClickValue={onClickValue} + getType={jest.fn()} + onRowContextMenuClick={() => undefined} + rowHasRowClickTriggerActions={[true, true, true]} + /> + ) + ).toMatchSnapshot(); + }); + test('it invokes executeTriggerActions with correct context on click on top value', () => { const { args, data } = sampleArgs(); diff --git a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx index 6502e07697816..f1eaab908717a 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/datatable_visualization/expression.tsx @@ -10,13 +10,22 @@ import React, { useMemo } from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; -import { EuiBasicTable, EuiFlexGroup, EuiButtonIcon, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { + EuiBasicTable, + EuiFlexGroup, + EuiButtonIcon, + EuiFlexItem, + EuiToolTip, + EuiBasicTableColumn, + EuiTableActionsColumnType, +} from '@elastic/eui'; import { IAggType } from 'src/plugins/data/public'; import { FormatFactory, ILensInterpreterRenderHandlers, LensFilterEvent, LensMultiTable, + LensTableRowContextMenuEvent, } from '../types'; import { ExpressionFunctionDefinition, @@ -45,7 +54,14 @@ export interface DatatableProps { type DatatableRenderProps = DatatableProps & { formatFactory: FormatFactory; onClickValue: (data: LensFilterEvent['data']) => void; + onRowContextMenuClick?: (data: LensTableRowContextMenuEvent['data']) => void; getType: (name: string) => IAggType; + + /** + * A boolean for each table row, which is true if the row active + * ROW_CLICK_TRIGGER actions attached to it, otherwise false. + */ + rowHasRowClickTriggerActions?: boolean[]; }; export interface DatatableRender { @@ -143,13 +159,47 @@ export const getDatatableRenderer = (dependencies: { const onClickValue = (data: LensFilterEvent['data']) => { handlers.event({ name: 'filter', data }); }; + const onRowContextMenuClick = (data: LensTableRowContextMenuEvent['data']) => { + handlers.event({ name: 'tableRowContextMenuClick', data }); + }; + const { hasCompatibleActions } = handlers; + + // An entry for each table row, whether it has any actions attached to + // ROW_CLICK_TRIGGER trigger. + let rowHasRowClickTriggerActions: boolean[] = []; + if (hasCompatibleActions) { + const table = Object.values(config.data.tables)[0]; + if (!!table) { + rowHasRowClickTriggerActions = await Promise.all( + table.rows.map(async (row, rowIndex) => { + try { + const hasActions = await hasCompatibleActions({ + name: 'tableRowContextMenuClick', + data: { + rowIndex, + table, + columns: config.args.columns.columnIds, + }, + }); + + return hasActions; + } catch { + return false; + } + }) + ); + } + } + ReactDOM.render( , domNode, @@ -169,7 +219,7 @@ export function DatatableComponent(props: DatatableRenderProps) { formatters[column.id] = props.formatFactory(column.meta?.params); }); - const { onClickValue } = props; + const { onClickValue, onRowContextMenuClick } = props; const handleFilterClick = useMemo( () => (field: string, value: unknown, colIndex: number, negate: boolean = false) => { const col = firstTable.columns[colIndex]; @@ -214,6 +264,124 @@ export function DatatableComponent(props: DatatableRenderProps) { return ; } + const tableColumns: Array< + EuiBasicTableColumn<{ rowIndex: number; [key: string]: unknown }> + > = props.args.columns.columnIds + .map((field) => { + const col = firstTable.columns.find((c) => c.id === field); + const filterable = bucketColumns.includes(field); + const colIndex = firstTable.columns.findIndex((c) => c.id === field); + return { + field, + name: (col && col.name) || '', + render: (value: unknown) => { + const formattedValue = formatters[field]?.convert(value); + const fieldName = col?.meta?.field; + + if (filterable) { + return ( + + {formattedValue} + + + + handleFilterClick(field, value, colIndex)} + /> + + + + handleFilterClick(field, value, colIndex, true)} + /> + + + + + + ); + } + return {formattedValue}; + }, + }; + }) + .filter(({ field }) => !!field); + + if (!!props.rowHasRowClickTriggerActions && !!onRowContextMenuClick) { + const hasAtLeastOneRowClickAction = props.rowHasRowClickTriggerActions.find((x) => x); + if (hasAtLeastOneRowClickAction) { + const actions: EuiTableActionsColumnType<{ rowIndex: number; [key: string]: unknown }> = { + name: i18n.translate('xpack.lens.datatable.actionsColumnName', { + defaultMessage: 'Actions', + }), + actions: [ + { + name: i18n.translate('xpack.lens.tableRowMore', { + defaultMessage: 'More', + }), + description: i18n.translate('xpack.lens.tableRowMoreDescription', { + defaultMessage: 'Table row context menu', + }), + type: 'icon', + icon: ({ rowIndex }: { rowIndex: number }) => { + if ( + !!props.rowHasRowClickTriggerActions && + !props.rowHasRowClickTriggerActions[rowIndex] + ) + return 'empty'; + return 'boxesVertical'; + }, + onClick: ({ rowIndex }) => { + onRowContextMenuClick({ + rowIndex, + table: firstTable, + columns: props.args.columns.columnIds, + }); + }, + }, + ], + }; + tableColumns.push(actions); + } + } + return ( { - const col = firstTable.columns.find((c) => c.id === field); - const filterable = bucketColumns.includes(field); - const colIndex = firstTable.columns.findIndex((c) => c.id === field); - return { - field, - name: (col && col.name) || '', - render: (value: unknown) => { - const formattedValue = formatters[field]?.convert(value); - const fieldName = col?.meta?.field; - - if (filterable) { - return ( - - {formattedValue} - - - - handleFilterClick(field, value, colIndex)} - /> - - - - handleFilterClick(field, value, colIndex, true)} - /> - - - - - - ); - } - return {formattedValue}; - }, - }; - }) - .filter(({ field }) => !!field)} - items={firstTable ? firstTable.rows : []} + columns={tableColumns} + items={firstTable ? firstTable.rows.map((row, rowIndex) => ({ ...row, rowIndex })) : []} /> ); diff --git a/x-pack/plugins/lens/public/datatable_visualization/index.ts b/x-pack/plugins/lens/public/datatable_visualization/index.ts index 5d9be46db7fb5..9c7d7ae1f2d43 100644 --- a/x-pack/plugins/lens/public/datatable_visualization/index.ts +++ b/x-pack/plugins/lens/public/datatable_visualization/index.ts @@ -7,11 +7,9 @@ import { CoreSetup } from 'kibana/public'; import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import { EditorFrameSetup, FormatFactory } from '../types'; -import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; interface DatatableVisualizationPluginStartPlugins { - uiActions: UiActionsStart; data: DataPublicPluginStart; } export interface DatatableVisualizationPluginSetupPlugins { @@ -34,6 +32,7 @@ export class DatatableVisualization { getDatatableRenderer, datatableVisualization, } = await import('../async_services'); + expressions.registerFunction(() => datatableColumns); expressions.registerFunction(() => datatable); expressions.registerRenderer(() => diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx index 54517e4ee8c84..175c573d3be3a 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx @@ -25,7 +25,7 @@ import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks' import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable'; import { coreMock, httpServiceMock } from '../../../../../../src/core/public/mocks'; import { IBasePath } from '../../../../../../src/core/public'; -import { AttributeService } from '../../../../../../src/plugins/embeddable/public'; +import { AttributeService, ViewMode } from '../../../../../../src/plugins/embeddable/public'; import { LensAttributeService } from '../../lens_attribute_service'; import { OnSaveProps } from '../../../../../../src/plugins/saved_objects/public/save_modal'; import { act } from 'react-dom/test-utils'; @@ -221,6 +221,74 @@ describe('embeddable', () => { expect(expressionRenderer).toHaveBeenCalledTimes(2); }); + it('should re-render when dashboard view/edit mode changes', async () => { + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + editable: true, + getTrigger, + documentToExpression: () => + Promise.resolve({ + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }), + }, + { id: '123' } as LensEmbeddableInput + ); + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + expect(expressionRenderer).toHaveBeenCalledTimes(1); + + embeddable.updateInput({ + viewMode: ViewMode.VIEW, + }); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); + }); + + it('should re-render when dynamic actions input changes', async () => { + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + editable: true, + getTrigger, + documentToExpression: () => + Promise.resolve({ + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }), + }, + { id: '123' } as LensEmbeddableInput + ); + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + expect(expressionRenderer).toHaveBeenCalledTimes(1); + + embeddable.updateInput({ + enhancements: { + dynamicActions: {}, + }, + }); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); + }); + it('should pass context to embeddable', async () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; @@ -396,6 +464,37 @@ describe('embeddable', () => { ); }); + it('should execute trigger on row click event from expression renderer', async () => { + const embeddable = new Embeddable( + { + timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter, + attributeService, + expressionRenderer, + basePath, + indexPatternService: {} as IndexPatternsContract, + editable: true, + getTrigger, + documentToExpression: () => + Promise.resolve({ + type: 'expression', + chain: [ + { type: 'function', function: 'my', arguments: {} }, + { type: 'function', function: 'expression', arguments: {} }, + ], + }), + }, + { id: '123' } as LensEmbeddableInput + ); + await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput); + embeddable.render(mountpoint); + + const onEvent = expressionRenderer.mock.calls[0][0].onEvent!; + + onEvent({ name: 'tableRowContextMenuClick', data: {} }); + + expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick); + }); + it('should not re-render if only change is in disabled filter', async () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index e7d3e1a4bfa5b..6c86ae5cff2c8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -21,6 +21,8 @@ import { PaletteOutput } from 'src/plugins/charts/public'; import { Subscription } from 'rxjs'; import { toExpression, Ast } from '@kbn/interpreter/common'; import { RenderMode } from 'src/plugins/expressions'; +import { map, distinctUntilChanged, skip } from 'rxjs/operators'; +import isEqual from 'fast-deep-equal'; import { ExpressionRendererEvent, ReactExpressionRendererType, @@ -38,7 +40,11 @@ import { import { Document, injectFilterReferences } from '../../persistence'; import { ExpressionWrapper } from './expression_wrapper'; import { UiActionsStart } from '../../../../../../src/plugins/ui_actions/public'; -import { isLensBrushEvent, isLensFilterEvent } from '../../types'; +import { + isLensBrushEvent, + isLensFilterEvent, + isLensTableRowContextMenuClickEvent, +} from '../../types'; import { IndexPatternsContract } from '../../../../../../src/plugins/data/public'; import { getEditPath, DOC_TYPE } from '../../../common'; @@ -71,6 +77,7 @@ export interface LensEmbeddableDeps { timefilter: TimefilterContract; basePath: IBasePath; getTrigger?: UiActionsStart['getTrigger'] | undefined; + getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; } export class Embeddable @@ -117,6 +124,36 @@ export class Embeddable this.autoRefreshFetchSubscription = deps.timefilter .getAutoRefreshFetch$() .subscribe(this.reload.bind(this)); + + const input$ = this.getInput$(); + + // Lens embeddable does not re-render when embeddable input changes in + // general, to improve performance. This line makes sure the Lens embeddable + // re-renders when anything in ".dynamicActions" (e.g. drilldowns) changes. + input$ + .pipe( + map((input) => input.enhancements?.dynamicActions), + distinctUntilChanged((a, b) => isEqual(a, b)), + skip(1) + ) + .subscribe((input) => { + this.reload(); + }); + + // Lens embeddable does not re-render when embeddable input changes in + // general, to improve performance. This line makes sure the Lens embeddable + // re-renders when dashboard view mode switches between "view/edit". This is + // needed to see the changes to ".dynamicActions" (e.g. drilldowns) when + // dashboard's mode is toggled. + input$ + .pipe( + map((input) => input.viewMode), + distinctUntilChanged(), + skip(1) + ) + .subscribe((input) => { + this.reload(); + }); } public supportedTriggers() { @@ -127,6 +164,7 @@ export class Embeddable case 'lnsXY': return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; case 'lnsDatatable': + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick]; case 'lnsPie': return [VIS_EVENT_TO_TRIGGER.filter]; case 'lnsMetric': @@ -217,11 +255,31 @@ export class Embeddable handleEvent={this.handleEvent} onData$={this.updateActiveData} renderMode={input.renderMode} + hasCompatibleActions={this.hasCompatibleActions} />, domNode ); } + private readonly hasCompatibleActions = async ( + event: ExpressionRendererEvent + ): Promise => { + if (isLensTableRowContextMenuClickEvent(event)) { + const { getTriggerCompatibleActions } = this.deps; + if (!getTriggerCompatibleActions) { + return false; + } + const actions = await getTriggerCompatibleActions(VIS_EVENT_TO_TRIGGER[event.name], { + data: event.data, + embeddable: this, + }); + + return actions.length > 0; + } + + return false; + }; + /** * Combines the embeddable context with the saved object context, and replaces * any references to index patterns @@ -264,6 +322,16 @@ export class Embeddable embeddable: this, }); } + + if (isLensTableRowContextMenuClickEvent(event)) { + this.deps.getTrigger(VIS_EVENT_TO_TRIGGER[event.name]).exec( + { + data: event.data, + embeddable: this, + }, + true + ); + } }; async reload() { diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts index 65e9c22d24eaf..175ec0dbcfd54 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable_factory.ts @@ -94,6 +94,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { editable: await this.isEditable(), basePath: coreHttp.basePath, getTrigger: uiActions?.getTrigger, + getTriggerCompatibleActions: uiActions?.getTriggerCompatibleActions, documentToExpression, }, input, diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx index 4645420898314..2fc1cfee82fd3 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/expression_wrapper.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; import { ExpressionRendererEvent, ReactExpressionRendererType, + ReactExpressionRendererProps, } from 'src/plugins/expressions/public'; import { ExecutionContextSearch } from 'src/plugins/data/public'; import { RenderMode } from 'src/plugins/expressions'; @@ -26,6 +27,7 @@ export interface ExpressionWrapperProps { handleEvent: (event: ExpressionRendererEvent) => void; onData$: (data: unknown, inspectorAdapters?: LensInspectorAdapters | undefined) => void; renderMode?: RenderMode; + hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions']; } export function ExpressionWrapper({ @@ -37,6 +39,7 @@ export function ExpressionWrapper({ searchSessionId, onData$, renderMode, + hasCompatibleActions, }: ExpressionWrapperProps) { return ( @@ -80,6 +83,7 @@ export function ExpressionWrapper({ )} onEvent={handleEvent} + hasCompatibleActions={hasCompatibleActions} /> )} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index b0da6cf2e8434..23d026bf2b443 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -8,6 +8,7 @@ import { IconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'kibana/public'; import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; import { SavedObjectReference } from 'kibana/public'; +import { ROW_CLICK_TRIGGER } from '../../../../src/plugins/ui_actions/public'; import { ExpressionAstExpression, ExpressionRendererEvent, @@ -614,11 +615,17 @@ export interface LensFilterEvent { name: 'filter'; data: TriggerContext['data']; } + export interface LensBrushEvent { name: 'brush'; data: TriggerContext['data']; } +export interface LensTableRowContextMenuEvent { + name: 'tableRowContextMenuClick'; + data: TriggerContext['data']; +} + export function isLensFilterEvent(event: ExpressionRendererEvent): event is LensFilterEvent { return event.name === 'filter'; } @@ -627,11 +634,17 @@ export function isLensBrushEvent(event: ExpressionRendererEvent): event is LensB return event.name === 'brush'; } +export function isLensTableRowContextMenuClickEvent( + event: ExpressionRendererEvent +): event is LensBrushEvent { + return event.name === 'tableRowContextMenuClick'; +} + /** * Expression renderer handlers specifically for lens renderers. This is a narrowed down * version of the general render handlers, specifying supported event types. If this type is * used, dispatched events will be handled correctly. */ export interface ILensInterpreterRenderHandlers extends IInterpreterRenderHandlers { - event: (event: LensFilterEvent | LensBrushEvent) => void; + event: (event: LensFilterEvent | LensBrushEvent | LensTableRowContextMenuEvent) => void; } diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx index e6c9797623e9f..1b975da0b369d 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/test_samples/demo.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { UrlDrilldownConfig, UrlDrilldownScope } from '../../../types'; +import { UrlDrilldownConfig } from '../../../types'; import { UrlDrilldownCollectConfig } from '../url_drilldown_collect_config'; export const Demo = () => { @@ -14,33 +14,13 @@ export const Demo = () => { url: { template: '' }, }); - const fakeScope: UrlDrilldownScope = { - kibanaUrl: 'http://localhost:5601/', - context: { - filters: [ - { - query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, - }, - { - query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, - }, - { - query: { match: { _type: { query: 'nginx', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, - }, - ], - }, - event: { - key: 'fakeKey', - value: 'fakeValue', - }, - }; - return ( <> - + {JSON.stringify(config)} ); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx deleted file mode 100644 index a6fcd77d75040..0000000000000 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Demo } from './test_samples/demo'; -import { fireEvent, render } from '@testing-library/react'; -import React from 'react'; - -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => `id-${Math.random()}`, -})); - -test('configure valid URL template', () => { - const screen = render(); - - const urlTemplate = 'https://elastic.co/?{{event.key}}={{event.value}}'; - fireEvent.change(screen.getByLabelText(/Enter URL template/i), { - target: { value: urlTemplate }, - }); - - const preview = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement; - expect(preview.value).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`); - expect(preview.disabled).toEqual(true); - const previewLink = screen.getByText('Preview') as HTMLAnchorElement; - expect(previewLink.href).toMatchInlineSnapshot(`"https://elastic.co/?fakeKey=fakeValue"`); - expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`); -}); - -test('configure invalid URL template', () => { - const screen = render(); - - const urlTemplate = 'https://elastic.co/?{{event.wrongKey}}={{event.wrongValue}}'; - fireEvent.change(screen.getByLabelText(/Enter URL template/i), { - target: { value: urlTemplate }, - }); - - const previewTextArea = screen.getByLabelText(/URL preview/i) as HTMLTextAreaElement; - expect(previewTextArea.disabled).toEqual(true); - expect(previewTextArea.value).toEqual(urlTemplate); - expect(screen.getByText(/invalid format/i)).toBeInTheDocument(); // check that error is shown - - const previewLink = screen.getByText('Preview') as HTMLAnchorElement; - expect(previewLink.href).toEqual(urlTemplate); - expect(previewLink.target).toMatchInlineSnapshot(`"_blank"`); -}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx index 3251e85841d86..eb8d01afbf420 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx @@ -18,52 +18,40 @@ import { EuiTextArea, EuiSelectableOption, } from '@elastic/eui'; -import { UrlDrilldownConfig, UrlDrilldownScope } from '../../types'; -import { compile } from '../../url_template'; -import { validateUrlTemplate } from '../../url_validation'; -import { buildScopeSuggestions } from '../../url_drilldown_scope'; +import { UrlDrilldownConfig } from '../../types'; import './index.scss'; import { txtAddVariableButtonTitle, - txtUrlPreviewHelpText, txtUrlTemplateSyntaxHelpLinkText, txtUrlTemplateVariablesHelpLinkText, txtUrlTemplateVariablesFilterPlaceholderText, txtUrlTemplateLabel, txtUrlTemplateOpenInNewTab, txtUrlTemplatePlaceholder, - txtUrlTemplatePreviewLabel, - txtUrlTemplatePreviewLinkText, } from './i18n'; export interface UrlDrilldownCollectConfig { config: UrlDrilldownConfig; + variables: string[]; onConfig: (newConfig: UrlDrilldownConfig) => void; - scope: UrlDrilldownScope; syntaxHelpDocsLink?: string; variablesHelpDocsLink?: string; } export const UrlDrilldownCollectConfig: React.FC = ({ config, + variables, onConfig, - scope, syntaxHelpDocsLink, variablesHelpDocsLink, }) => { const textAreaRef = useRef(null); + const [showUrlError, setShowUrlError] = React.useState(false); const urlTemplate = config.url.template ?? ''; - const compiledUrl = React.useMemo(() => { - try { - return compile(urlTemplate, scope); - } catch { - return urlTemplate; - } - }, [urlTemplate, scope]); - const scopeVariables = React.useMemo(() => buildScopeSuggestions(scope), [scope]); function updateUrlTemplate(newUrlTemplate: string) { if (config.url.template !== newUrlTemplate) { + setShowUrlError(true); onConfig({ ...config, url: { @@ -73,18 +61,31 @@ export const UrlDrilldownCollectConfig: React.FC = ({ }); } } - const { error, isValid } = React.useMemo( - () => validateUrlTemplate({ template: urlTemplate }, scope), - [urlTemplate, scope] - ); const isEmpty = !urlTemplate; - const isInvalid = !isValid && !isEmpty; + const isInvalid = showUrlError && isEmpty; + const variablesDropdown = ( + { + if (textAreaRef.current) { + updateUrlTemplate( + urlTemplate.substr(0, textAreaRef.current!.selectionStart) + + `{{${variable}}}` + + urlTemplate.substr(textAreaRef.current!.selectionEnd) + ); + } else { + updateUrlTemplate(urlTemplate + `{{${variable}}}`); + } + }} + /> + ); + return ( <> = ({ ) } - labelAppend={ - { - if (textAreaRef.current) { - updateUrlTemplate( - urlTemplate.substr(0, textAreaRef.current!.selectionStart) + - `{{${variable}}}` + - urlTemplate.substr(textAreaRef.current!.selectionEnd) - ); - } else { - updateUrlTemplate(urlTemplate + `{{${variable}}}`); - } - }} - /> - } + labelAppend={variablesDropdown} > = ({ value={urlTemplate} placeholder={txtUrlTemplatePlaceholder} onChange={(event) => updateUrlTemplate(event.target.value)} + onBlur={() => setShowUrlError(true)} rows={3} inputRef={textAreaRef} /> - - - {txtUrlTemplatePreviewLinkText} - - - } - helpText={txtUrlPreviewHelpText} - > - - { - expect( - buildScopeSuggestions( - buildScope({ - globalScope: { - kibanaUrl: 'http://localhost:5061/', - }, - eventScope: { - key: '__testKey__', - value: '__testValue__', - }, - contextScope: { - filters: [ - { - query: { match: { extension: { query: 'jpg', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, - }, - { - query: { match: { '@tags': { query: 'info', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, - }, - { - query: { match: { _type: { query: 'nginx', type: 'phrase' } } }, - meta: { index: 'logstash-*', negate: false, disabled: false, alias: null }, - }, - ], - query: { - query: '', - language: 'kquery', - }, - }, - }) - ) - ).toMatchInlineSnapshot(` - Array [ - "event.key", - "event.value", - "context.filters", - "context.query.language", - "context.query.query", - "kibanaUrl", - ] - `); -}); diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts deleted file mode 100644 index 74940c4b07077..0000000000000 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { partition } from 'lodash'; -import { getFlattenedObject } from '@kbn/std'; -import { UrlDrilldownGlobalScope, UrlDrilldownScope } from './types'; - -export function buildScope< - ContextScope extends object = object, - EventScope extends object = object ->({ - globalScope, - contextScope, - eventScope, -}: { - globalScope: UrlDrilldownGlobalScope; - contextScope?: ContextScope; - eventScope?: EventScope; -}): UrlDrilldownScope { - return { - ...globalScope, - context: contextScope, - event: eventScope, - }; -} - -/** - * Builds list of variables for suggestion from scope - * keys sorted alphabetically, except {{event.$}} variables are pulled to the top - * @param scope - */ -export function buildScopeSuggestions(scope: UrlDrilldownGlobalScope): string[] { - const allKeys = Object.keys(getFlattenedObject(scope)).sort(); - const [eventKeys, otherKeys] = partition(allKeys, (key) => key.startsWith('event')); - return [...eventKeys, ...otherKeys]; -} From 4f48401b20a7a1daec9fc3f7a68e22036778b06d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 14 Dec 2020 13:35:01 +0100 Subject: [PATCH 26/37] unskip tests and make sure submit is not triggered too quickly (#85567) --- .../operations/definitions/shared_components/label_input.tsx | 2 +- x-pack/test/functional/apps/lens/smokescreen.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx index ddcb5633b376f..de7a826485831 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/shared_components/label_input.tsx @@ -47,7 +47,7 @@ export const LabelInput = ({ inputRef.current = node; } }} - onKeyDown={({ key }: React.KeyboardEvent) => { + onKeyUp={({ key }: React.KeyboardEvent) => { if (keys.ENTER === key && onSubmit) { onSubmit(); } diff --git a/x-pack/test/functional/apps/lens/smokescreen.ts b/x-pack/test/functional/apps/lens/smokescreen.ts index 462b385f27e5d..b91399a4a6756 100644 --- a/x-pack/test/functional/apps/lens/smokescreen.ts +++ b/x-pack/test/functional/apps/lens/smokescreen.ts @@ -13,8 +13,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const listingTable = getService('listingTable'); const testSubjects = getService('testSubjects'); - // FLAKY: https://github.com/elastic/kibana/issues/77969 - describe.skip('lens smokescreen tests', () => { + describe('lens smokescreen tests', () => { it('should allow creation of lens xy chart', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); From fbb83af63d347086f816bc7c2a1cba62b80ad28c Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 14 Dec 2020 15:57:28 +0300 Subject: [PATCH 27/37] align cors settings names with elasticsearch (#85738) * align cors settings names with elasticsearch * server.cors.origin: * --> server.cors.origin: ["*"] --- docs/setup/settings.asciidoc | 6 +-- .../__snapshots__/http_config.test.ts.snap | 6 ++- src/core/server/http/http_config.test.ts | 46 +++++++++++++------ src/core/server/http/http_config.ts | 17 ++++--- src/core/server/http/http_tools.test.ts | 6 +-- src/core/server/http/http_tools.ts | 4 +- x-pack/test/functional_cors/config.ts | 4 +- x-pack/test/functional_cors/tests/cors.ts | 4 +- 8 files changed, 57 insertions(+), 36 deletions(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 6cd848e963431..8b50fc38167d3 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -453,11 +453,11 @@ deprecation warning at startup. This setting cannot end in a slash (`/`). | `server.cors.enabled:` | experimental[] Set to `true` to allow cross-origin API calls. *Default:* `false` -| `server.cors.credentials:` +| `server.cors.allowCredentials:` | experimental[] Set to `true` to allow browser code to access response body whenever request performed with user credentials. *Default:* `false` -| `server.cors.origin:` - | experimental[] List of origins permitted to access resources. You must specify explicit hostnames and not use `*` for `server.cors.origin` when `server.cors.credentials: true`. *Default:* "*" +| `server.cors.allowOrigin:` + | experimental[] List of origins permitted to access resources. You must specify explicit hostnames and not use `server.cors.allowOrigin: ["*"]` when `server.cors.allowCredentials: true`. *Default:* ["*"] | `server.compression.referrerWhitelist:` | Specifies an array of trusted hostnames, such as the {kib} host, or a reverse diff --git a/src/core/server/http/__snapshots__/http_config.test.ts.snap b/src/core/server/http/__snapshots__/http_config.test.ts.snap index a440c67944fab..9b667f888771e 100644 --- a/src/core/server/http/__snapshots__/http_config.test.ts.snap +++ b/src/core/server/http/__snapshots__/http_config.test.ts.snap @@ -39,9 +39,11 @@ Object { "enabled": true, }, "cors": Object { - "credentials": false, + "allowCredentials": false, + "allowOrigin": Array [ + "*", + ], "enabled": false, - "origin": "*", }, "customResponseHeaders": Object {}, "host": "localhost", diff --git a/src/core/server/http/http_config.test.ts b/src/core/server/http/http_config.test.ts index f893e7783ac8f..b71763e8a2e14 100644 --- a/src/core/server/http/http_config.test.ts +++ b/src/core/server/http/http_config.test.ts @@ -331,51 +331,67 @@ describe('with compression', () => { }); describe('cors', () => { - describe('origin', () => { + describe('allowOrigin', () => { it('list cannot be empty', () => { expect(() => config.schema.validate({ cors: { - origin: [], + allowOrigin: [], }, }) ).toThrowErrorMatchingInlineSnapshot(` - "[cors.origin]: types that failed validation: - - [cors.origin.0]: expected value to equal [*] - - [cors.origin.1]: array size is [0], but cannot be smaller than [1]" - `); + "[cors.allowOrigin]: types that failed validation: + - [cors.allowOrigin.0]: array size is [0], but cannot be smaller than [1] + - [cors.allowOrigin.1]: array size is [0], but cannot be smaller than [1]" + `); }); it('list of valid URLs', () => { - const origin = ['http://127.0.0.1:3000', 'https://elastic.co']; + const allowOrigin = ['http://127.0.0.1:3000', 'https://elastic.co']; expect( config.schema.validate({ - cors: { origin }, - }).cors.origin - ).toStrictEqual(origin); + cors: { allowOrigin }, + }).cors.allowOrigin + ).toStrictEqual(allowOrigin); expect(() => config.schema.validate({ cors: { - origin: ['*://elastic.co/*'], + allowOrigin: ['*://elastic.co/*'], }, }) ).toThrow(); }); it('can be configured as "*" wildcard', () => { - expect(config.schema.validate({ cors: { origin: '*' } }).cors.origin).toBe('*'); + expect(config.schema.validate({ cors: { allowOrigin: ['*'] } }).cors.allowOrigin).toEqual([ + '*', + ]); + }); + + it('cannot mix wildcard "*" with valid URLs', () => { + expect( + () => + config.schema.validate({ cors: { allowOrigin: ['*', 'https://elastic.co'] } }).cors + .allowOrigin + ).toThrowErrorMatchingInlineSnapshot(` + "[cors.allowOrigin]: types that failed validation: + - [cors.allowOrigin.0.0]: expected URI with scheme [http|https]. + - [cors.allowOrigin.1.1]: expected value to equal [*]" + `); }); }); describe('credentials', () => { - it('cannot use wildcard origin if "credentials: true"', () => { + it('cannot use wildcard allowOrigin if "credentials: true"', () => { expect( - () => config.schema.validate({ cors: { credentials: true, origin: '*' } }).cors.origin + () => + config.schema.validate({ cors: { allowCredentials: true, allowOrigin: ['*'] } }).cors + .allowOrigin ).toThrowErrorMatchingInlineSnapshot( `"[cors]: Cannot specify wildcard origin \\"*\\" with \\"credentials: true\\". Please provide a list of allowed origins."` ); expect( - () => config.schema.validate({ cors: { credentials: true } }).cors.origin + () => config.schema.validate({ cors: { allowCredentials: true } }).cors.allowOrigin ).toThrowErrorMatchingInlineSnapshot( `"[cors]: Cannot specify wildcard origin \\"*\\" with \\"credentials: true\\". Please provide a list of allowed origins."` ); diff --git a/src/core/server/http/http_config.ts b/src/core/server/http/http_config.ts index 74cdbfbedeea9..2bd296fe338ab 100644 --- a/src/core/server/http/http_config.ts +++ b/src/core/server/http/http_config.ts @@ -48,17 +48,20 @@ export const config = { cors: schema.object( { enabled: schema.boolean({ defaultValue: false }), - credentials: schema.boolean({ defaultValue: false }), - origin: schema.oneOf( - [schema.literal('*'), schema.arrayOf(hostURISchema, { minSize: 1 })], + allowCredentials: schema.boolean({ defaultValue: false }), + allowOrigin: schema.oneOf( + [ + schema.arrayOf(hostURISchema, { minSize: 1 }), + schema.arrayOf(schema.literal('*'), { minSize: 1, maxSize: 1 }), + ], { - defaultValue: '*', + defaultValue: ['*'], } ), }, { validate(value) { - if (value.credentials === true && value.origin === '*') { + if (value.allowCredentials === true && value.allowOrigin.includes('*')) { return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.'; } }, @@ -168,8 +171,8 @@ export class HttpConfig { public port: number; public cors: { enabled: boolean; - credentials: boolean; - origin: '*' | string[]; + allowCredentials: boolean; + allowOrigin: string[]; }; public customResponseHeaders: Record; public maxPayload: ByteSizeValue; diff --git a/src/core/server/http/http_tools.test.ts b/src/core/server/http/http_tools.test.ts index 4098b631b19d8..962c2107513b5 100644 --- a/src/core/server/http/http_tools.test.ts +++ b/src/core/server/http/http_tools.test.ts @@ -196,8 +196,8 @@ describe('getServerOptions', () => { config.schema.validate({ cors: { enabled: true, - credentials: false, - origin: '*', + allowCredentials: false, + allowOrigin: ['*'], }, }), {} as any, @@ -206,7 +206,7 @@ describe('getServerOptions', () => { expect(getServerOptions(httpConfig).routes?.cors).toEqual({ credentials: false, - origin: '*', + origin: ['*'], headers: ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'], }); }); diff --git a/src/core/server/http/http_tools.ts b/src/core/server/http/http_tools.ts index 61688a51345b5..8bec26f31fa26 100644 --- a/src/core/server/http/http_tools.ts +++ b/src/core/server/http/http_tools.ts @@ -39,8 +39,8 @@ const corsAllowedHeaders = ['Accept', 'Authorization', 'Content-Type', 'If-None- export function getServerOptions(config: HttpConfig, { configureTLS = true } = {}) { const cors: RouteOptionsCors | false = config.cors.enabled ? { - credentials: config.cors.credentials, - origin: config.cors.origin, + credentials: config.cors.allowCredentials, + origin: config.cors.allowOrigin, headers: corsAllowedHeaders, } : false; diff --git a/x-pack/test/functional_cors/config.ts b/x-pack/test/functional_cors/config.ts index da03fee476f13..b792aa2d183b6 100644 --- a/x-pack/test/functional_cors/config.ts +++ b/x-pack/test/functional_cors/config.ts @@ -55,8 +55,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--plugin-path=${corsTestPlugin}`, `--test.cors.port=${pluginPort}`, '--server.cors.enabled=true', - '--server.cors.credentials=true', - `--server.cors.origin=["${originUrl}"]`, + '--server.cors.allowCredentials=true', + `--server.cors.allowOrigin=["${originUrl}"]`, ], }, }; diff --git a/x-pack/test/functional_cors/tests/cors.ts b/x-pack/test/functional_cors/tests/cors.ts index ff5da26b4e275..774ffe1719f07 100644 --- a/x-pack/test/functional_cors/tests/cors.ts +++ b/x-pack/test/functional_cors/tests/cors.ts @@ -15,9 +15,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('CORS', () => { it('Communicates to Kibana with configured CORS', async () => { const args: string[] = config.get('kbnTestServer.serverArgs'); - const originSetting = args.find((str) => str.includes('server.cors.origin')); + const originSetting = args.find((str) => str.includes('server.cors.allowOrigin')); if (!originSetting) { - throw new Error('Cannot find "server.cors.origin" argument'); + throw new Error('Cannot find "server.cors.allowOrigin" argument'); } const [, value] = originSetting.split('='); const url = JSON.parse(value); From ab07a003d4b79bc601234612ea0f5b099263af2f Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 14 Dec 2020 07:58:32 -0500 Subject: [PATCH 28/37] Increasing default api key removalDelay to 1h (#85576) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/alerts/server/config.test.ts | 2 +- x-pack/plugins/alerts/server/config.ts | 2 +- x-pack/plugins/alerts/server/plugin.test.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/alerts/server/config.test.ts b/x-pack/plugins/alerts/server/config.test.ts index bf3b30b5d2378..e4691ad6229a0 100644 --- a/x-pack/plugins/alerts/server/config.test.ts +++ b/x-pack/plugins/alerts/server/config.test.ts @@ -15,7 +15,7 @@ describe('config validation', () => { }, "invalidateApiKeysTask": Object { "interval": "5m", - "removalDelay": "5m", + "removalDelay": "1h", }, } `); diff --git a/x-pack/plugins/alerts/server/config.ts b/x-pack/plugins/alerts/server/config.ts index 41340c7dfe5fc..e53b99852c354 100644 --- a/x-pack/plugins/alerts/server/config.ts +++ b/x-pack/plugins/alerts/server/config.ts @@ -13,7 +13,7 @@ export const configSchema = schema.object({ }), invalidateApiKeysTask: schema.object({ interval: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), - removalDelay: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), + removalDelay: schema.string({ validate: validateDurationSchema, defaultValue: '1h' }), }), }); diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index fee7901c4ea55..48fd2e12336a8 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -24,7 +24,7 @@ describe('Alerting Plugin', () => { }, invalidateApiKeysTask: { interval: '5m', - removalDelay: '5m', + removalDelay: '1h', }, }); const plugin = new AlertingPlugin(context); @@ -73,7 +73,7 @@ describe('Alerting Plugin', () => { }, invalidateApiKeysTask: { interval: '5m', - removalDelay: '5m', + removalDelay: '1h', }, }); const plugin = new AlertingPlugin(context); @@ -124,7 +124,7 @@ describe('Alerting Plugin', () => { }, invalidateApiKeysTask: { interval: '5m', - removalDelay: '5m', + removalDelay: '1h', }, }); const plugin = new AlertingPlugin(context); From 1c16bcf8512851748ec69b583172ec73029f6238 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 14 Dec 2020 07:34:54 -0600 Subject: [PATCH 29/37] Fix UX E2E tests (#85722) They look for `.kbnLoadingIndicator` which is no longer there in the new loading indicator design. This changes it to look for an element that does exist and makes it a function in utils. Change not.be.visible to not.exist in places where the element does not exist at in that state. --- .../apm/e2e/cypress/integration/snapshots.js | 2 +- .../step_definitions/csm/breakdown_filter.ts | 8 ++++---- .../csm/client_metrics_helper.ts | 8 ++++---- .../step_definitions/csm/csm_dashboard.ts | 19 +++++++++---------- .../step_definitions/csm/csm_filters.ts | 11 +++++------ .../support/step_definitions/csm/js_errors.ts | 4 ++-- .../step_definitions/csm/percentile_select.ts | 5 ++--- .../csm/service_name_filter.ts | 4 ++-- .../step_definitions/csm/url_search_filter.ts | 14 +++++++------- .../support/step_definitions/csm/utils.ts | 7 +++++-- 10 files changed, 41 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js index 0ecda7a113de7..152186a8a738a 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js +++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js @@ -1,3 +1,3 @@ module.exports = { - "__version": "5.4.0" + "__version": "6.0.1" } diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/breakdown_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/breakdown_filter.ts index 342f3e0aa5267..e558d1ef9c0bc 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/breakdown_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/breakdown_filter.ts @@ -6,13 +6,13 @@ import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'; import { DEFAULT_TIMEOUT } from './csm_dashboard'; +import { waitForLoadingToFinish } from './utils'; /** The default time in ms to wait for a Cypress command to complete */ Given(`a user clicks the page load breakdown filter`, () => { - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); + waitForLoadingToFinish(); + cy.get('.euiStat__title-isLoading').should('not.exist'); const breakDownBtn = cy.get( '[data-test-subj=pldBreakdownFilter]', DEFAULT_TIMEOUT @@ -27,7 +27,7 @@ When(`the user selected the breakdown`, () => { }); Then(`breakdown series should appear in chart`, () => { - cy.get('.euiLoadingChart').should('not.be.visible'); + cy.get('.euiLoadingChart').should('not.exist'); cy.get('[data-cy=pageLoadDist]').within(() => { cy.get('div.echLegendItem__label[title=Chrome] ', DEFAULT_TIMEOUT) diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/client_metrics_helper.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/client_metrics_helper.ts index 0b26c6de66f4b..d8d8c7c3a62e9 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/client_metrics_helper.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/client_metrics_helper.ts @@ -5,6 +5,7 @@ */ import { DEFAULT_TIMEOUT } from './csm_dashboard'; +import { waitForLoadingToFinish } from './utils'; /** * Verifies the behavior of the client metrics component @@ -17,15 +18,14 @@ export function verifyClientMetrics( ) { const clientMetricsSelector = '[data-cy=client-metrics] .euiStat__title'; - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); + waitForLoadingToFinish(); if (checkTitleStatus) { cy.get('.euiStat__title', DEFAULT_TIMEOUT).should('be.visible'); - cy.get('.euiSelect-isLoading').should('not.be.visible'); + cy.get('.euiSelect-isLoading').should('not.exist'); } - cy.get('.euiStat__title-isLoading').should('not.be.visible'); + cy.get('.euiStat__title-isLoading').should('not.exist'); cy.get(clientMetricsSelector).eq(0).should('have.text', metrics[0]); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts index 452d8b719b3cb..5207ea39c959f 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_dashboard.ts @@ -7,6 +7,7 @@ import { Given, Then } from 'cypress-cucumber-preprocessor/steps'; import { loginAndWaitForPage } from '../../../integration/helpers'; import { verifyClientMetrics } from './client_metrics_helper'; +import { waitForLoadingToFinish } from './utils'; /** The default time in ms to wait for a Cypress command to complete */ export const DEFAULT_TIMEOUT = { timeout: 60 * 1000 }; @@ -36,9 +37,9 @@ Then(`should display percentile for page load chart`, () => { cy.get('.euiLoadingChart', DEFAULT_TIMEOUT).should('be.visible'); - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); + waitForLoadingToFinish(); + + cy.get('.euiStat__title-isLoading').should('not.exist'); cy.get(pMarkers).eq(0).should('have.text', '50th'); @@ -52,21 +53,19 @@ Then(`should display percentile for page load chart`, () => { Then(`should display chart legend`, () => { const chartLegend = 'div.echLegendItem__label'; - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiLoadingChart').should('not.be.visible'); + waitForLoadingToFinish(); + cy.get('.euiLoadingChart').should('not.exist'); cy.get(chartLegend, DEFAULT_TIMEOUT).eq(0).should('have.text', 'Overall'); }); Then(`should display tooltip on hover`, () => { - cy.get('.euiLoadingChart').should('not.be.visible'); + cy.get('.euiLoadingChart').should('not.exist'); const pMarkers = '[data-cy=percentile-markers] span.euiToolTipAnchor'; - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiLoadingChart').should('not.be.visible'); + waitForLoadingToFinish(); + cy.get('.euiLoadingChart').should('not.exist'); const marker = cy.get(pMarkers, DEFAULT_TIMEOUT).eq(0); marker.invoke('show'); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts index 88287286c66c5..9aeddad686385 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/csm_filters.ts @@ -7,11 +7,11 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; import { DEFAULT_TIMEOUT } from './csm_dashboard'; import { verifyClientMetrics } from './client_metrics_helper'; +import { waitForLoadingToFinish } from './utils'; When(/^the user filters by "([^"]*)"$/, (filterName) => { - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); + waitForLoadingToFinish(); + cy.get('.euiStat__title-isLoading').should('not.exist'); cy.get(`#local-filter-${filterName}`).click(); cy.get(`#local-filter-popover-${filterName}`, DEFAULT_TIMEOUT).within(() => { @@ -51,9 +51,8 @@ When(/^the user filters by "([^"]*)"$/, (filterName) => { }); Then(/^it filters the client metrics "([^"]*)"$/, (filterName) => { - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); + waitForLoadingToFinish(); + cy.get('.euiStat__title-isLoading').should('not.exist'); const data = filterName === 'os' diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/js_errors.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/js_errors.ts index 9e10e2fd59914..bc53de0bac6a7 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/js_errors.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/js_errors.ts @@ -9,8 +9,8 @@ import { DEFAULT_TIMEOUT } from './csm_dashboard'; import { getDataTestSubj } from './utils'; Then(`it displays list of relevant js errors`, () => { - cy.get('.euiBasicTable-loading').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); + cy.get('.euiBasicTable-loading').should('not.exist'); + cy.get('.euiStat__title-isLoading').should('not.exist'); getDataTestSubj('uxJsErrorsTotal').should('have.text', 'Total errors112'); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts index 44802bbce6208..80b90422366d5 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/percentile_select.ts @@ -6,11 +6,10 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; import { verifyClientMetrics } from './client_metrics_helper'; -import { getDataTestSubj } from './utils'; +import { getDataTestSubj, waitForLoadingToFinish } from './utils'; When('the user changes the selected percentile', () => { - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); + waitForLoadingToFinish(); getDataTestSubj('uxPercentileSelect').select('95'); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts index 609d0d18f5bc8..5c0e8c6238238 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/service_name_filter.ts @@ -7,10 +7,10 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; import { verifyClientMetrics } from './client_metrics_helper'; import { DEFAULT_TIMEOUT } from './csm_dashboard'; +import { waitForLoadingToFinish } from './utils'; When('the user changes the selected service name', () => { - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); + waitForLoadingToFinish(); cy.get(`[data-cy=serviceNameFilter]`, DEFAULT_TIMEOUT).select('client'); }); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts index 3dc98625baf85..cc9dc177d57a0 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/url_search_filter.ts @@ -6,18 +6,18 @@ import { When, Then } from 'cypress-cucumber-preprocessor/steps'; import { DEFAULT_TIMEOUT } from './csm_dashboard'; +import { waitForLoadingToFinish } from './utils'; When(`a user clicks inside url search field`, () => { - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); - cy.get('.euiStat__title-isLoading').should('not.be.visible'); + waitForLoadingToFinish(); + cy.get('.euiStat__title-isLoading').should('not.exist'); cy.get('span[data-cy=csmUrlFilter]', DEFAULT_TIMEOUT).within(() => { cy.get('input.euiFieldSearch').click(); }); }); Then(`it displays top pages in the suggestion popover`, () => { - cy.get('kbnLoadingIndicator').should('not.be.visible'); + waitForLoadingToFinish(); cy.get('div.euiPopover__panel-isOpen', DEFAULT_TIMEOUT).within(() => { const listOfUrls = cy.get('li.euiSelectableListItem'); @@ -38,17 +38,17 @@ Then(`it displays top pages in the suggestion popover`, () => { }); When(`a user enters a query in url search field`, () => { - cy.get('kbnLoadingIndicator').should('not.be.visible'); + waitForLoadingToFinish(); cy.get('[data-cy=csmUrlFilter]').within(() => { cy.get('input.euiSelectableSearch').type('cus'); }); - cy.get('kbnLoadingIndicator').should('not.be.visible'); + waitForLoadingToFinish(); }); Then(`it should filter results based on query`, () => { - cy.get('kbnLoadingIndicator').should('not.be.visible'); + waitForLoadingToFinish(); cy.get('div.euiPopover__panel-isOpen', DEFAULT_TIMEOUT).within(() => { const listOfUrls = cy.get('li.euiSelectableListItem'); diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/utils.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/utils.ts index 87b3a1d70d073..0819a27ff16cb 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/utils.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/csm/utils.ts @@ -6,9 +6,12 @@ import { DEFAULT_TIMEOUT } from './csm_dashboard'; +export function waitForLoadingToFinish() { + cy.get('[data-test-subj=globalLoadingIndicator-hidden]', DEFAULT_TIMEOUT); +} + export function getDataTestSubj(dataTestSubj: string) { - // wait for all loading to finish - cy.get('kbnLoadingIndicator').should('not.be.visible'); + waitForLoadingToFinish(); return cy.get(`[data-test-subj=${dataTestSubj}]`, DEFAULT_TIMEOUT); } From a06b8d1a7b943e186877d04f25036574c14cf05a Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 14 Dec 2020 15:01:05 +0100 Subject: [PATCH 30/37] [Uptime] Log es queries in kibana logs on debug flag (#85387) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/uptime/public/state/api/utils.ts | 10 +- x-pack/plugins/uptime/server/lib/lib.ts | 109 ++++++++++++++---- .../uptime/server/rest_api/certs/certs.ts | 11 +- .../server/rest_api/create_route_with_auth.ts | 17 ++- .../server/rest_api/dynamic_settings.ts | 10 +- .../rest_api/index_state/get_index_pattern.ts | 16 +-- .../rest_api/index_state/get_index_status.ts | 21 ++-- .../server/rest_api/monitors/monitor_list.ts | 65 +++++------ .../rest_api/monitors/monitor_locations.ts | 17 ++- .../rest_api/monitors/monitor_status.ts | 12 +- .../rest_api/monitors/monitors_details.ts | 19 ++- .../rest_api/monitors/monitors_durations.ts | 17 ++- .../overview_filters/get_overview_filters.ts | 7 +- .../rest_api/pings/get_ping_histogram.ts | 11 +- .../uptime/server/rest_api/pings/get_pings.ts | 11 +- .../rest_api/pings/journey_screenshots.ts | 4 +- .../uptime/server/rest_api/pings/journeys.ts | 25 ++-- .../rest_api/snapshot/get_snapshot_count.ts | 11 +- .../rest_api/telemetry/log_page_view.ts | 14 +-- .../plugins/uptime/server/rest_api/types.ts | 27 ++--- .../server/rest_api/uptime_route_wrapper.ts | 39 ++++++- 21 files changed, 258 insertions(+), 215 deletions(-) diff --git a/x-pack/plugins/uptime/public/state/api/utils.ts b/x-pack/plugins/uptime/public/state/api/utils.ts index 965cbefd13114..54e129c0811c2 100644 --- a/x-pack/plugins/uptime/public/state/api/utils.ts +++ b/x-pack/plugins/uptime/public/state/api/utils.ts @@ -8,6 +8,7 @@ import { PathReporter } from 'io-ts/lib/PathReporter'; import { isRight } from 'fp-ts/lib/Either'; import { HttpFetchQuery, HttpSetup } from 'src/core/public'; import * as t from 'io-ts'; +import { startsWith } from 'lodash'; function isObject(value: unknown) { const type = typeof value; @@ -60,7 +61,14 @@ class ApiService { } public async get(apiUrl: string, params?: HttpFetchQuery, decodeType?: any, asResponse = false) { - const response = await this._http!.fetch({ path: apiUrl, query: params, asResponse }); + const debugEnabled = + sessionStorage.getItem('uptime_debug') === 'true' && startsWith(apiUrl, '/api/uptime'); + + const response = await this._http!.fetch({ + path: apiUrl, + query: { ...params, ...(debugEnabled ? { _debug: true } : {}) }, + asResponse, + }); if (decodeType) { const decoded = decodeType.decode(response); diff --git a/x-pack/plugins/uptime/server/lib/lib.ts b/x-pack/plugins/uptime/server/lib/lib.ts index 39dd868462525..ee84cf4463ceb 100644 --- a/x-pack/plugins/uptime/server/lib/lib.ts +++ b/x-pack/plugins/uptime/server/lib/lib.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchClient, SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, KibanaRequest } from 'kibana/server'; +import chalk from 'chalk'; import { UMBackendFrameworkAdapter } from './adapters'; import { UMLicenseCheck } from './domains'; import { UptimeRequests } from './requests'; @@ -19,53 +20,83 @@ export interface UMServerLibs extends UMDomainLibs { framework: UMBackendFrameworkAdapter; } +interface CountResponse { + body: { + count: number; + _shards: { + total: number; + successful: number; + skipped: number; + failed: number; + }; + }; +} + export type UptimeESClient = ReturnType; export function createUptimeESClient({ esClient, + request, savedObjectsClient, }: { esClient: ElasticsearchClient; + request?: KibanaRequest; savedObjectsClient: SavedObjectsClientContract; }) { + const { _debug = false } = (request?.query as { _debug: boolean }) ?? {}; + return { baseESClient: esClient, async search(params: TParams): Promise<{ body: ESSearchResponse }> { + let res: any; + let esError: any; const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( savedObjectsClient! ); - let res: any; + const esParams = { index: dynamicSettings!.heartbeatIndices, ...params }; + const startTime = process.hrtime(); + try { - res = await esClient.search({ index: dynamicSettings!.heartbeatIndices, ...params }); + res = await esClient.search(esParams); } catch (e) { - throw e; + esError = e; + } + if (_debug && request) { + debugESCall({ startTime, request, esError, operationName: 'search', params: esParams }); } + + if (esError) { + throw esError; + } + return res; }, - async count( - params: TParams - ): Promise<{ - body: { - count: number; - _shards: { - total: number; - successful: number; - skipped: number; - failed: number; - }; - }; - }> { + async count(params: TParams): Promise { + let res: any; + let esError: any; + const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings( savedObjectsClient! ); - let res: any; + const esParams = { index: dynamicSettings!.heartbeatIndices, ...params }; + const startTime = process.hrtime(); + try { - res = await esClient.count({ index: dynamicSettings!.heartbeatIndices, ...params }); + res = await esClient.count(esParams); } catch (e) { - throw e; + esError = e; } + + if (_debug && request) { + debugESCall({ startTime, request, esError, operationName: 'count', params: esParams }); + } + + if (esError) { + throw esError; + } + return res; }, getSavedObjectsClient() { @@ -73,3 +104,41 @@ export function createUptimeESClient({ }, }; } + +/* eslint-disable no-console */ + +function formatObj(obj: Record) { + return JSON.stringify(obj); +} + +export function debugESCall({ + operationName, + params, + request, + esError, + startTime, +}: { + operationName: string; + params: Record; + request: KibanaRequest; + esError: any; + startTime: [number, number]; +}) { + const highlightColor = esError ? 'bgRed' : 'inverse'; + const diff = process.hrtime(startTime); + const duration = `${Math.round(diff[0] * 1000 + diff[1] / 1e6)}ms`; + const routeInfo = `${request.route.method.toUpperCase()} ${request.route.path}`; + + console.log(chalk.bold[highlightColor](`=== Debug: ${routeInfo} (${duration}) ===`)); + + if (operationName === 'search') { + console.log(`GET ${params.index}/_${operationName}`); + console.log(formatObj(params.body)); + } else { + console.log(chalk.bold('ES operation:'), operationName); + + console.log(chalk.bold('ES query:')); + console.log(formatObj(params)); + } + console.log(`\n`); +} diff --git a/x-pack/plugins/uptime/server/rest_api/certs/certs.ts b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts index d377095a2a370..7af5717d8416d 100644 --- a/x-pack/plugins/uptime/server/rest_api/certs/certs.ts +++ b/x-pack/plugins/uptime/server/rest_api/certs/certs.ts @@ -30,7 +30,7 @@ export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = direction: schema.maybe(schema.string()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response): Promise => { + handler: async ({ uptimeEsClient, request }): Promise => { const index = request.query?.index ?? 0; const size = request.query?.size ?? DEFAULT_SIZE; const from = request.query?.from ?? DEFAULT_FROM; @@ -38,7 +38,8 @@ export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = const sortBy = request.query?.sortBy ?? DEFAULT_SORT; const direction = request.query?.direction ?? DEFAULT_DIRECTION; const { search } = request.query; - const result = await libs.requests.getCerts({ + + return await libs.requests.getCerts({ uptimeEsClient, index, search, @@ -48,11 +49,5 @@ export const createGetCertsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = sortBy, direction, }); - return response.ok({ - body: { - certs: result.certs, - total: result.total, - }, - }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts b/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts index 966dc20e27a7e..93593cc315d62 100644 --- a/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts +++ b/x-pack/plugins/uptime/server/rest_api/create_route_with_auth.ts @@ -13,10 +13,22 @@ export const createRouteWithAuth = ( ): UptimeRoute => { const restRoute = routeCreator(libs); const { handler, method, path, options, ...rest } = restRoute; - const licenseCheckHandler: UMRouteHandler = async (customParams, context, request, response) => { + const licenseCheckHandler: UMRouteHandler = async ({ + uptimeEsClient, + context, + request, + response, + savedObjectsClient, + }) => { const { statusCode, message } = libs.license(context.licensing.license); if (statusCode === 200) { - return handler(customParams, context, request, response); + return handler({ + uptimeEsClient, + context, + request, + response, + savedObjectsClient, + }); } switch (statusCode) { case 400: @@ -29,6 +41,7 @@ export const createRouteWithAuth = ( return response.internalError(); } }; + return { method, path, diff --git a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts index f7be9e10d1004..3dd77be6eaf8c 100644 --- a/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts +++ b/x-pack/plugins/uptime/server/rest_api/dynamic_settings.ts @@ -20,12 +20,8 @@ export const createGetDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMSer method: 'GET', path: '/api/uptime/dynamic_settings', validate: false, - handler: async ({ savedObjectsClient }, _context, _request, response): Promise => { - const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); - - return response.ok({ - body: dynamicSettings, - }); + handler: async ({ savedObjectsClient }): Promise => { + return savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient); }, }); @@ -60,7 +56,7 @@ export const createPostDynamicSettingsRoute: UMRestApiRouteFactory = (libs: UMSe }), }, writeAccess: true, - handler: async ({ savedObjectsClient }, _context, request, response): Promise => { + handler: async ({ savedObjectsClient, request, response }): Promise => { const decoded = DynamicSettingsType.decode(request.body); const certThresholdErrors = validateCertsValues(request.body as DynamicSettings); diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts index 5c1be4cdd8143..671c6f0a71aa8 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_pattern.ts @@ -12,17 +12,9 @@ export const createGetIndexPatternRoute: UMRestApiRouteFactory = (libs: UMServer method: 'GET', path: API_URLS.INDEX_PATTERN, validate: false, - handler: async ({ uptimeEsClient }, _context, _request, response): Promise => { - try { - return response.ok({ - body: { - ...(await libs.requests.getIndexPattern({ - uptimeEsClient, - })), - }, - }); - } catch (e) { - return response.internalError({ body: { message: e.message } }); - } + handler: async ({ uptimeEsClient }): Promise => { + return await libs.requests.getIndexPattern({ + uptimeEsClient, + }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts index e57643aed7e36..a347baf770d6a 100644 --- a/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/index_state/get_index_status.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { schema } from '@kbn/config-schema'; import { UMServerLibs } from '../../lib/lib'; import { UMRestApiRouteFactory } from '../types'; import { API_URLS } from '../../../common/constants'; @@ -11,18 +12,12 @@ import { API_URLS } from '../../../common/constants'; export const createGetIndexStatusRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', path: API_URLS.INDEX_STATUS, - validate: false, - handler: async ({ uptimeEsClient }, _context, _request, response): Promise => { - try { - return response.ok({ - body: { - ...(await libs.requests.getIndexStatus({ - uptimeEsClient, - })), - }, - }); - } catch (e) { - return response.internalError({ body: { message: e.message } }); - } + validate: { + query: schema.object({ + _debug: schema.maybe(schema.boolean()), + }), + }, + handler: async ({ uptimeEsClient }): Promise => { + return await libs.requests.getIndexStatus({ uptimeEsClient }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts index 50fe616ae9cb5..2c3c649bf68c6 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_list.ts @@ -19,52 +19,39 @@ export const createMonitorListRoute: UMRestApiRouteFactory = (libs) => ({ pagination: schema.maybe(schema.string()), statusFilter: schema.maybe(schema.string()), pageSize: schema.number(), + _debug: schema.maybe(schema.boolean()), }), }, options: { tags: ['access:uptime-read'], }, - handler: async ({ uptimeEsClient }, _context, request, response): Promise => { - try { - const { - dateRangeStart, - dateRangeEnd, - filters, - pagination, - statusFilter, - pageSize, - } = request.query; + handler: async ({ uptimeEsClient, request }): Promise => { + const { + dateRangeStart, + dateRangeEnd, + filters, + pagination, + statusFilter, + pageSize, + } = request.query; - const decodedPagination = pagination - ? JSON.parse(decodeURIComponent(pagination)) - : CONTEXT_DEFAULTS.CURSOR_PAGINATION; + const decodedPagination = pagination + ? JSON.parse(decodeURIComponent(pagination)) + : CONTEXT_DEFAULTS.CURSOR_PAGINATION; - const { - summaries, - nextPagePagination, - prevPagePagination, - } = await libs.requests.getMonitorStates({ - uptimeEsClient, - dateRangeStart, - dateRangeEnd, - pagination: decodedPagination, - pageSize, - filters, - // this is added to make typescript happy, - // this sort of reassignment used to be further downstream but I've moved it here - // because this code is going to be decomissioned soon - statusFilter: statusFilter || undefined, - }); + const result = await libs.requests.getMonitorStates({ + uptimeEsClient, + dateRangeStart, + dateRangeEnd, + pagination: decodedPagination, + pageSize, + filters, + // this is added to make typescript happy, + // this sort of reassignment used to be further downstream but I've moved it here + // because this code is going to be decomissioned soon + statusFilter: statusFilter || undefined, + }); - return response.ok({ - body: { - summaries, - nextPagePagination, - prevPagePagination, - }, - }); - } catch (e) { - return response.internalError({ body: { message: e.message } }); - } + return result; }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts index e2dbf7114fc91..48a39d94ac69e 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_locations.ts @@ -17,20 +17,17 @@ export const createGetMonitorLocationsRoute: UMRestApiRouteFactory = (libs: UMSe monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response): Promise => { + handler: async ({ uptimeEsClient, request }): Promise => { const { monitorId, dateStart, dateEnd } = request.query; - return response.ok({ - body: { - ...(await libs.requests.getMonitorLocations({ - uptimeEsClient, - monitorId, - dateStart, - dateEnd, - })), - }, + return await libs.requests.getMonitorLocations({ + uptimeEsClient, + monitorId, + dateStart, + dateEnd, }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts index 3d47f0eab8640..94edc6e4d7efe 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitor_status.ts @@ -12,26 +12,22 @@ import { API_URLS } from '../../../common/constants'; export const createGetStatusBarRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => ({ method: 'GET', path: API_URLS.MONITOR_STATUS, - validate: { query: schema.object({ monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response): Promise => { + handler: async ({ uptimeEsClient, request }): Promise => { const { monitorId, dateStart, dateEnd } = request.query; - const result = await libs.requests.getLatestMonitor({ + + return await libs.requests.getLatestMonitor({ uptimeEsClient, monitorId, dateStart, dateEnd, }); - return response.ok({ - body: { - ...result, - }, - }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts index 0982fc1986604..efbdf69a883d6 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_details.ts @@ -17,23 +17,20 @@ export const createGetMonitorDetailsRoute: UMRestApiRouteFactory = (libs: UMServ monitorId: schema.string(), dateStart: schema.maybe(schema.string()), dateEnd: schema.maybe(schema.string()), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, context, request, response): Promise => { + handler: async ({ uptimeEsClient, context, request }): Promise => { const { monitorId, dateStart, dateEnd } = request.query; const alertsClient = context.alerting?.getAlertsClient(); - return response.ok({ - body: { - ...(await libs.requests.getMonitorDetails({ - uptimeEsClient, - monitorId, - dateStart, - dateEnd, - alertsClient, - })), - }, + return await libs.requests.getMonitorDetails({ + uptimeEsClient, + monitorId, + dateStart, + dateEnd, + alertsClient, }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts index eec3fdf9e7257..09d0ce4555309 100644 --- a/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts +++ b/x-pack/plugins/uptime/server/rest_api/monitors/monitors_durations.ts @@ -18,20 +18,17 @@ export const createGetMonitorDurationRoute: UMRestApiRouteFactory = (libs: UMSer monitorId: schema.string(), dateStart: schema.string(), dateEnd: schema.string(), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response): Promise => { + handler: async ({ uptimeEsClient, request }): Promise => { const { monitorId, dateStart, dateEnd } = request.query; - return response.ok({ - body: { - ...(await libs.requests.getMonitorDurationChart({ - uptimeEsClient, - monitorId, - dateStart, - dateEnd, - })), - }, + return await libs.requests.getMonitorDurationChart({ + uptimeEsClient, + monitorId, + dateStart, + dateEnd, }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts index 163fbd4f8dd6e..ac58a8002899b 100644 --- a/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts +++ b/x-pack/plugins/uptime/server/rest_api/overview_filters/get_overview_filters.ts @@ -26,9 +26,10 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi schemes: arrayOrStringType, ports: arrayOrStringType, tags: arrayOrStringType, + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response) => { + handler: async ({ uptimeEsClient, request, response }): Promise => { const { dateRangeStart, dateRangeEnd, locations, schemes, search, ports, tags } = request.query; let parsedSearch: Record | undefined; @@ -40,7 +41,7 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi } } - const filtersResponse = await libs.requests.getFilterBar({ + return await libs.requests.getFilterBar({ uptimeEsClient, dateRangeStart, dateRangeEnd, @@ -52,7 +53,5 @@ export const createGetOverviewFilters: UMRestApiRouteFactory = (libs: UMServerLi tags, }), }); - - return response.ok({ body: { ...filtersResponse } }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts index ba36b171793b7..4797e4aae94bf 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_ping_histogram.ts @@ -19,12 +19,13 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe monitorId: schema.maybe(schema.string()), filters: schema.maybe(schema.string()), bucketSize: schema.maybe(schema.string()), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response): Promise => { + handler: async ({ uptimeEsClient, request }): Promise => { const { dateStart, dateEnd, monitorId, filters, bucketSize } = request.query; - const result = await libs.requests.getPingHistogram({ + return await libs.requests.getPingHistogram({ uptimeEsClient, from: dateStart, to: dateEnd, @@ -32,11 +33,5 @@ export const createGetPingHistogramRoute: UMRestApiRouteFactory = (libs: UMServe filters, bucketSize, }); - - return response.ok({ - body: { - ...result, - }, - }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts index 2a1a401ec3153..fa1ef565d0146 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/get_pings.ts @@ -22,12 +22,13 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = size: schema.maybe(schema.number()), sort: schema.maybe(schema.string()), status: schema.maybe(schema.string()), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response): Promise => { + handler: async ({ uptimeEsClient, request, response }): Promise => { const { from, to, index, monitorId, status, sort, size, locations } = request.query; - const result = await libs.requests.getPings({ + return await libs.requests.getPings({ uptimeEsClient, dateRange: { from, to }, index, @@ -37,11 +38,5 @@ export const createGetPingsRoute: UMRestApiRouteFactory = (libs: UMServerLibs) = size, locations: locations ? JSON.parse(locations) : [], }); - - return response.ok({ - body: { - ...result, - }, - }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts index 080fc8ab8f8ee..8343b24e601a1 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journey_screenshots.ts @@ -15,10 +15,12 @@ export const createJourneyScreenshotRoute: UMRestApiRouteFactory = (libs: UMServ params: schema.object({ checkGroup: schema.string(), stepIndex: schema.number(), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response) => { + handler: async ({ uptimeEsClient, request, response }) => { const { checkGroup, stepIndex } = request.params; + const result = await libs.requests.getJourneyScreenshot({ uptimeEsClient, checkGroup, diff --git a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts index f46cf3b990951..8ebd4b4609c75 100644 --- a/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts +++ b/x-pack/plugins/uptime/server/rest_api/pings/journeys.ts @@ -14,21 +14,20 @@ export const createJourneyRoute: UMRestApiRouteFactory = (libs: UMServerLibs) => validate: { params: schema.object({ checkGroup: schema.string(), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response) => { + handler: async ({ uptimeEsClient, request }): Promise => { const { checkGroup } = request.params; const result = await libs.requests.getJourneySteps({ uptimeEsClient, checkGroup, }); - return response.ok({ - body: { - checkGroup, - steps: result, - }, - }); + return { + checkGroup, + steps: result, + }; }, }); @@ -40,18 +39,16 @@ export const createJourneyFailedStepsRoute: UMRestApiRouteFactory = (libs: UMSer checkGroups: schema.arrayOf(schema.string()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response) => { + handler: async ({ uptimeEsClient, request }): Promise => { const { checkGroups } = request.query; const result = await libs.requests.getJourneyFailedSteps({ uptimeEsClient, checkGroups, }); - return response.ok({ - body: { - checkGroups, - steps: result, - }, - }); + return { + checkGroups, + steps: result, + }; }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts index 224ef87fd90af..2d22259fbf786 100644 --- a/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts +++ b/x-pack/plugins/uptime/server/rest_api/snapshot/get_snapshot_count.ts @@ -17,20 +17,17 @@ export const createGetSnapshotCount: UMRestApiRouteFactory = (libs: UMServerLibs dateRangeStart: schema.string(), dateRangeEnd: schema.string(), filters: schema.maybe(schema.string()), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ({ uptimeEsClient }, _context, request, response): Promise => { + handler: async ({ uptimeEsClient, request }): Promise => { const { dateRangeStart, dateRangeEnd, filters } = request.query; - const result = await libs.requests.getSnapshotCount({ + + return await libs.requests.getSnapshotCount({ uptimeEsClient, dateRangeStart, dateRangeEnd, filters, }); - return response.ok({ - body: { - ...result, - }, - }); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts index 85f274c96cf9a..e69556837af44 100644 --- a/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts +++ b/x-pack/plugins/uptime/server/rest_api/telemetry/log_page_view.ts @@ -21,14 +21,10 @@ export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ autoRefreshEnabled: schema.boolean(), autorefreshInterval: schema.number(), refreshTelemetryHistory: schema.maybe(schema.boolean()), + _debug: schema.maybe(schema.boolean()), }), }, - handler: async ( - { savedObjectsClient, uptimeEsClient }, - _context, - request, - response - ): Promise => { + handler: async ({ savedObjectsClient, uptimeEsClient, request }): Promise => { const pageView = request.body as PageViewParams; if (pageView.refreshTelemetryHistory) { KibanaTelemetryAdapter.clearLocalTelemetry(); @@ -37,10 +33,6 @@ export const createLogPageViewRoute: UMRestApiRouteFactory = () => ({ uptimeEsClient, savedObjectsClient ); - const pageViewResult = KibanaTelemetryAdapter.countPageView(pageView as PageViewParams); - - return response.ok({ - body: pageViewResult, - }); + return KibanaTelemetryAdapter.countPageView(pageView as PageViewParams); }, }); diff --git a/x-pack/plugins/uptime/server/rest_api/types.ts b/x-pack/plugins/uptime/server/rest_api/types.ts index df1762a3b318d..4e627cebb3459 100644 --- a/x-pack/plugins/uptime/server/rest_api/types.ts +++ b/x-pack/plugins/uptime/server/rest_api/types.ts @@ -14,7 +14,6 @@ import { KibanaRequest, KibanaResponseFactory, IKibanaResponse, - IScopedClusterClient, } from 'kibana/server'; import { UMServerLibs, UptimeESClient } from '../lib/lib'; @@ -59,20 +58,18 @@ export type UMRestApiRouteFactory = (libs: UMServerLibs) => UptimeRoute; export type UMKibanaRouteWrapper = (uptimeRoute: UptimeRoute) => UMKibanaRoute; /** - * This type can store custom parameters used by the internal Uptime route handlers. + * This is the contract we specify internally for route handling. */ -export interface UMRouteParams { +export type UMRouteHandler = ({ + uptimeEsClient, + context, + request, + response, + savedObjectsClient, +}: { uptimeEsClient: UptimeESClient; - esClient: IScopedClusterClient; + context: RequestHandlerContext; + request: KibanaRequest, Record, Record>; + response: KibanaResponseFactory; savedObjectsClient: SavedObjectsClientContract; -} - -/** - * This is the contract we specify internally for route handling. - */ -export type UMRouteHandler = ( - params: UMRouteParams, - context: RequestHandlerContext, - request: KibanaRequest, Record, Record>, - response: KibanaResponseFactory -) => IKibanaResponse | Promise>; +}) => IKibanaResponse | Promise>; diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index a1cf3c05e2de3..c8769dc4ea3df 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -7,6 +7,9 @@ import { UMKibanaRouteWrapper } from './types'; import { createUptimeESClient } from '../lib/lib'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaResponse } from '../../../../../src/core/server/http/router'; + export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ ...uptimeRoute, options: { @@ -17,15 +20,39 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute) => ({ const { client: savedObjectsClient } = context.core.savedObjects; const uptimeEsClient = createUptimeESClient({ + request, savedObjectsClient, esClient: esClient.asCurrentUser, }); - return uptimeRoute.handler( - { uptimeEsClient, esClient, savedObjectsClient }, - context, - request, - response - ); + try { + const res = await uptimeRoute.handler({ + uptimeEsClient, + savedObjectsClient, + context, + request, + response, + }); + + if (res instanceof KibanaResponse) { + return res; + } + + return response.ok({ + body: { + ...res, + }, + }); + } catch (e) { + // please don't remove this, this will be really helpful during debugging + /* eslint-disable-next-line no-console */ + console.error(e); + + return response.internalError({ + body: { + message: e.message, + }, + }); + } }, }); From d4f4a2c94fc5fc468bd336f7679bc4045df9d77a Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Mon, 14 Dec 2020 15:17:25 +0100 Subject: [PATCH 31/37] [ML] API integration tests - security_linux and security_windows modules (#85743) This PR adds API integration tests to create and setup the security_linux and security_windows modules. --- x-pack/test/api_integration/apis/ml/index.ts | 2 + .../apis/ml/modules/recognize_module.ts | 10 + .../apis/ml/modules/setup_module.ts | 128 + .../ml/module_security_endpoint/data.json.gz | Bin 0 -> 1315756 bytes .../ml/module_security_endpoint/mappings.json | 7755 +++++++++++++++++ 5 files changed, 7895 insertions(+) create mode 100644 x-pack/test/functional/es_archives/ml/module_security_endpoint/data.json.gz create mode 100644 x-pack/test/functional/es_archives/ml/module_security_endpoint/mappings.json diff --git a/x-pack/test/api_integration/apis/ml/index.ts b/x-pack/test/api_integration/apis/ml/index.ts index 1bee29daf7b7d..05d682625d8c8 100644 --- a/x-pack/test/api_integration/apis/ml/index.ts +++ b/x-pack/test/api_integration/apis/ml/index.ts @@ -34,6 +34,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.deleteIndexPatternByTitle('ft_module_siem_packetbeat'); await ml.testResources.deleteIndexPatternByTitle('ft_module_siem_winlogbeat'); await ml.testResources.deleteIndexPatternByTitle('ft_farequote'); + await ml.testResources.deleteIndexPatternByTitle('ft_logs-endpoint.events.*'); await esArchiver.unload('ml/ecommerce'); await esArchiver.unload('ml/categorization'); @@ -45,6 +46,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await esArchiver.unload('ml/module_nginx'); await esArchiver.unload('ml/module_sample_ecommerce'); await esArchiver.unload('ml/module_sample_logs'); + await esArchiver.unload('ml/module_security_endpoint'); await esArchiver.unload('ml/module_siem_auditbeat'); await esArchiver.unload('ml/module_siem_packetbeat'); await esArchiver.unload('ml/module_siem_winlogbeat'); diff --git a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index 64f53bbe76c5e..5b70b669aa876 100644 --- a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -135,6 +135,16 @@ export default ({ getService }: FtrProviderContext) => { moduleIds: ['auditbeat_process_hosts_ecs', 'security_linux', 'siem_auditbeat'], }, }, + { + testTitleSuffix: 'for security endpoint dataset', + sourceDataArchive: 'ml/module_security_endpoint', + indexPattern: 'ft_logs-endpoint.events.*', + user: USER.ML_POWERUSER, + expected: { + responseCode: 200, + moduleIds: ['security_linux', 'security_windows'], + }, + }, ]; async function executeRecognizeModuleRequest(indexPattern: string, user: USER, rspCode: number) { diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index c86cd8400a71a..d1316a0ededc6 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -520,6 +520,134 @@ export default ({ getService }: FtrProviderContext) => { ] as string[], }, }, + { + testTitleSuffix: + 'for security_linux with prefix, startDatafeed true and estimateModelMemory true', + sourceDataArchive: 'ml/module_security_endpoint', + indexPattern: { name: 'ft_logs-endpoint.events.*', timeField: '@timestamp' }, + module: 'security_linux', + user: USER.ML_POWERUSER, + requestBody: { + prefix: 'pf15_', + indexPatternName: 'ft_logs-endpoint.events.*', + startDatafeed: true, + end: Date.now(), + }, + expected: { + responseCode: 200, + jobs: [ + { + jobId: 'pf15_v2_rare_process_by_host_linux_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf15_v2_linux_rare_metadata_user', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf15_v2_linux_rare_metadata_process', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf15_v2_linux_anomalous_user_name_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf15_v2_linux_anomalous_process_all_hosts_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf15_v2_linux_anomalous_network_port_activity_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + ], + searches: [] as string[], + visualizations: [] as string[], + dashboards: [] as string[], + }, + }, + { + testTitleSuffix: + 'for security_windows with prefix, startDatafeed true and estimateModelMemory true', + sourceDataArchive: 'ml/module_security_endpoint', + indexPattern: { name: 'ft_logs-endpoint.events.*', timeField: '@timestamp' }, + module: 'security_windows', + user: USER.ML_POWERUSER, + requestBody: { + prefix: 'pf16_', + indexPatternName: 'ft_logs-endpoint.events.*', + startDatafeed: true, + end: Date.now(), + }, + expected: { + responseCode: 200, + jobs: [ + { + jobId: 'pf16_v2_rare_process_by_host_windows_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf16_v2_windows_anomalous_network_activity_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf16_v2_windows_anomalous_path_activity_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '10mb', + }, + { + jobId: 'pf16_v2_windows_anomalous_process_all_hosts_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf16_v2_windows_anomalous_process_creation', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf16_v2_windows_anomalous_user_name_ecs', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf16_v2_windows_rare_metadata_process', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + { + jobId: 'pf16_v2_windows_rare_metadata_user', + jobState: JOB_STATE.CLOSED, + datafeedState: DATAFEED_STATE.STOPPED, + modelMemoryLimit: '11mb', + }, + ], + searches: [] as string[], + visualizations: [] as string[], + dashboards: [] as string[], + }, + }, ]; const testDataListNegative = [ diff --git a/x-pack/test/functional/es_archives/ml/module_security_endpoint/data.json.gz b/x-pack/test/functional/es_archives/ml/module_security_endpoint/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..f9033c722776a58ca4f290ae285a58ef6f21f245 GIT binary patch literal 1315756 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%0AF$+AH>I(E)lRyh+=-d_ik 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 O_!W9=5s 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%0AWB5R4L>XHS&$jCV%Br5e9biTHW06zaEc!?3 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

    F^ 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

    Ie9E8r($7 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

    l_OXJyCEI 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

    }`)XPv5hL6*z4-v9$k` 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

    e-ZY`I$HxeXWjh#h3e_ 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ÿ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 F{{Wvb@hAWQ literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/ml/module_security_endpoint/mappings.json b/x-pack/test/functional/es_archives/ml/module_security_endpoint/mappings.json new file mode 100644 index 0000000000000..e9e8b542849a0 --- /dev/null +++ b/x-pack/test/functional/es_archives/ml/module_security_endpoint/mappings.json @@ -0,0 +1,7755 @@ +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.file-default-000001", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "Ext": { + "properties": { + "correlation": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "Ext": { + "properties": { + "original": { + "properties": { + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "windows": { + "properties": { + "zone_identifier": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "args_count": { + "type": "long" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.file-default-000002", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "Ext": { + "properties": { + "correlation": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "Ext": { + "properties": { + "original": { + "properties": { + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "windows": { + "properties": { + "zone_identifier": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "ctime": { + "type": "date" + }, + "device": { + "ignore_above": 1024, + "type": "keyword" + }, + "directory": { + "ignore_above": 1024, + "type": "keyword" + }, + "drive_letter": { + "ignore_above": 1, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "gid": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "inode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "mtime": { + "type": "date" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "size": { + "type": "long" + }, + "target_path": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "args_count": { + "type": "long" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.library-default-000001", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "Ext": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "Ext": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.library-default-000002", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "Ext": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "Ext": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + } + } + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.network-default-000001", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "dns": { + "properties": { + "Ext": { + "properties": { + "options": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "type": "long" + } + } + }, + "question": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + } + } + }, + "response": { + "properties": { + "Ext": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + } + } + }, + "message": { + "type": "text" + }, + "network": { + "properties": { + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.network-default-000002", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "dns": { + "properties": { + "Ext": { + "properties": { + "options": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "type": "long" + } + } + }, + "question": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolved_ip": { + "type": "ip" + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + } + } + }, + "response": { + "properties": { + "Ext": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "status_code": { + "type": "long" + } + } + } + } + }, + "message": { + "type": "text" + }, + "network": { + "properties": { + "bytes": { + "type": "long" + }, + "community_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "direction": { + "ignore_above": 1024, + "type": "keyword" + }, + "iana_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "packets": { + "type": "long" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "transport": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "bytes": { + "type": "long" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "packets": { + "type": "long" + }, + "port": { + "type": "long" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.process-default-000001", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "package": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + }, + "authentication_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + }, + "session": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "Ext": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + }, + "real": { + "properties": { + "pid": { + "type": "long" + } + } + } + } + }, + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.process-default-000002", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "package": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + }, + "authentication_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + }, + "session": { + "ignore_above": 1024, + "type": "keyword" + }, + "token": { + "properties": { + "elevation": { + "type": "boolean" + }, + "elevation_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "integrity_level_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "parent": { + "properties": { + "Ext": { + "properties": { + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + }, + "type": "nested" + }, + "real": { + "properties": { + "pid": { + "type": "long" + } + } + } + } + }, + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "exists": { + "type": "boolean" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "exit_code": { + "type": "long" + }, + "hash": { + "properties": { + "md5": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha1": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha256": { + "ignore_above": 1024, + "type": "keyword" + }, + "sha512": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pgid": { + "type": "long" + }, + "pid": { + "type": "long" + }, + "ppid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.registry-default-000001", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.registry-default-000002", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hive": { + "ignore_above": 1024, + "type": "keyword" + }, + "key": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "value": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.security-default-000001", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "ft_logs-endpoint.events.security-default-000002", + "mappings": { + "_meta": { + "managed": true, + "managed_by": "ingest-manager", + "package": { + "name": "endpoint" + } + }, + "date_detection": false, + "dynamic": "false", + "dynamic_templates": [ + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "destination": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "ecs": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "category": { + "ignore_above": 1024, + "type": "keyword" + }, + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "created": { + "type": "date" + }, + "dataset": { + "ignore_above": 1024, + "type": "keyword" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ingested": { + "type": "date" + }, + "kind": { + "ignore_above": 1024, + "type": "keyword" + }, + "module": { + "ignore_above": 1024, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "sequence": { + "type": "long" + }, + "severity": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "Ext": { + "properties": { + "variant": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "message": { + "type": "text" + }, + "process": { + "properties": { + "Ext": { + "properties": { + "ancestry": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "caseless": { + "ignore_above": 1024, + "type": "keyword" + }, + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pid": { + "type": "long" + }, + "thread": { + "properties": { + "id": { + "type": "long" + } + } + } + } + }, + "source": { + "properties": { + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "country_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "type": "geo_point" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "Ext": { + "properties": { + "real": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "number_of_replicas": "0", + "number_of_shards": "1", + "refresh_interval": "-1" + } + } + } +} \ No newline at end of file From 1ff233189fc2bb22dce0c4534eab345421987853 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 14 Dec 2020 15:23:10 +0100 Subject: [PATCH 32/37] [Lens] Better disabled messages for Value labels popup (#85592) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../xy_visualization/xy_config_panel.tsx | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index dc6ce285754fc..cc4df1f0f9315 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -154,6 +154,28 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) { ); } +function getValueLabelDisableReason({ + isAreaPercentage, + isHistogramSeries, +}: { + isAreaPercentage: boolean; + isHistogramSeries: boolean; +}): string { + if (isHistogramSeries) { + return i18n.translate('xpack.lens.xyChart.valuesHistogramDisabledHelpText', { + defaultMessage: 'This setting cannot be changed on histograms.', + }); + } + if (isAreaPercentage) { + return i18n.translate('xpack.lens.xyChart.valuesPercentageDisabledHelpText', { + defaultMessage: 'This setting cannot be changed on percentage area charts.', + }); + } + return i18n.translate('xpack.lens.xyChart.valuesStackedDisabledHelpText', { + defaultMessage: 'This setting cannot be changed on stacked or percentage bar charts', + }); +} + export function XyToolbar(props: VisualizationToolbarProps) { const { state, setState, frame } = props; @@ -246,20 +268,17 @@ export function XyToolbar(props: VisualizationToolbarProps) { const isValueLabelsEnabled = !hasNonBarSeries && hasBarNotStacked && !isHistogramSeries; const isFittingEnabled = hasNonBarSeries; + const valueLabelsDisabledReason = getValueLabelDisableReason({ + isAreaPercentage, + isHistogramSeries, + }); + return ( Date: Mon, 14 Dec 2020 16:16:28 +0100 Subject: [PATCH 33/37] Added transaction_ignore_urls as central config for the Python, .NET and Ruby APM agents (#85734) --- .../setting_definitions/general_settings.ts | 2 +- .../agent_configuration/setting_definitions/index.test.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts index d3063c9715992..90e98e64814a1 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/general_settings.ts @@ -254,6 +254,6 @@ export const generalSettings: RawSettingDefinition[] = [ 'Used to restrict requests to certain URLs from being instrumented. This config accepts a comma-separated list of wildcard patterns of URL paths that should be ignored. When an incoming HTTP request is detected, its request path will be tested against each element in this list. For example, adding `/home/index` to this list would match and remove instrumentation from `http://localhost/home/index` as well as `http://whatever.com/home/index?value1=123`', } ), - includeAgents: ['java', 'nodejs'], + includeAgents: ['java', 'nodejs', 'python', 'dotnet', 'ruby'], }, ]; diff --git a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts index e0e7e84810090..88cf3e288abf1 100644 --- a/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts +++ b/x-pack/plugins/apm/common/agent_configuration/setting_definitions/index.test.ts @@ -120,6 +120,7 @@ describe('filterByAgent', () => { 'recording', 'sanitize_field_names', 'span_frames_min_duration', + 'transaction_ignore_urls', 'transaction_max_spans', 'transaction_sample_rate', ]); @@ -134,6 +135,7 @@ describe('filterByAgent', () => { 'sanitize_field_names', 'span_frames_min_duration', 'stack_trace_limit', + 'transaction_ignore_urls', 'transaction_max_spans', 'transaction_sample_rate', ]); @@ -148,6 +150,7 @@ describe('filterByAgent', () => { 'log_level', 'recording', 'span_frames_min_duration', + 'transaction_ignore_urls', 'transaction_max_spans', 'transaction_sample_rate', ]); From 0dfcbe92ed0fb7eabd18e9416a63ab406965c5f0 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 14 Dec 2020 10:33:59 -0500 Subject: [PATCH 34/37] [SECURITY SOLUTIONS] Ask user to save timeline before leaving the app + bugs (#85693) * fix clicking on host on netwrok detail page * Fetch signal index at plugin level to avoid weird behavior * bing back full screen timeline * Show health check on timeline * fix focus on modal of description and title * fix focus on modal of description and title * allow to know the next appId * if user leave security solution and timeline has not been saved, ask them if they want to save it before leaving * fix test + types * Fix siem signal loading on plugin + UX on timeline with no data * Add a callback to cleaner from solution + test * fix bug + improve prompt leaving msg * update note * css improvements * fix code to be true to our test * miss one test * update test * fix unit test * core review Co-authored-by: Patryk Kopycinski Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Angela Chuang --- ...e-public.appleaveconfirmaction.callback.md | 11 + ...lugin-core-public.appleaveconfirmaction.md | 1 + ...bana-plugin-core-public.appleavehandler.md | 2 +- .../application/application_leave.test.ts | 20 ++ .../public/application/application_leave.tsx | 8 +- .../application/application_service.test.ts | 13 ++ .../application/application_service.tsx | 14 +- src/core/public/application/types.ts | 10 +- src/core/public/public.api.md | 4 +- .../cypress/screens/timeline.ts | 3 +- .../security_solution/public/app/app.tsx | 19 +- .../public/app/home/index.tsx | 6 +- .../security_solution/public/app/index.tsx | 9 +- .../security_solution/public/app/routes.tsx | 12 +- .../common/components/alerts_viewer/index.tsx | 4 +- .../events_viewer/events_viewer.tsx | 6 +- .../common/components/events_viewer/index.tsx | 4 +- .../components/exit_full_screen/index.tsx | 4 +- .../common/components/header_global/index.tsx | 5 +- .../common/components/wrapper_page/index.tsx | 4 +- .../common/containers/sourcerer/index.tsx | 52 ++++- .../containers/use_full_screen/index.tsx | 37 +++- .../public/common/store/reducer.test.ts | 2 + .../public/common/store/reducer.ts | 8 +- .../detection_engine/detection_engine.tsx | 4 +- .../detection_engine/rules/details/index.tsx | 4 +- .../public/hosts/pages/details/index.tsx | 4 +- .../public/hosts/pages/hosts.tsx | 4 +- .../navigation/events_query_tab_body.tsx | 4 +- .../public/network/pages/network.tsx | 4 +- .../security_solution/public/plugin.tsx | 11 + .../flyout/__snapshots__/index.test.tsx.snap | 1 + .../flyout/header/active_timelines.tsx | 55 ++++- .../components/flyout/header/index.tsx | 60 ++++-- .../components/flyout/header/selectors.ts | 16 ++ .../components/flyout/index.test.tsx | 12 +- .../timelines/components/flyout/index.tsx | 65 +++++- .../timelines/components/flyout/selectors.ts | 17 ++ .../components/graph_overlay/index.test.tsx | 32 ++- .../components/graph_overlay/index.tsx | 13 +- .../components/open_timeline/helpers.ts | 16 +- .../timeline/body/column_headers/index.tsx | 25 ++- .../components/timeline/footer/index.tsx | 7 +- .../header/save_timeline_button.test.tsx | 76 ++++--- .../timeline/header/save_timeline_button.tsx | 58 +++-- .../components/timeline/header/selectors.ts | 12 ++ .../header/title_and_description.test.tsx | 68 +++--- .../timeline/header/title_and_description.tsx | 204 ++++++++++-------- .../timelines/components/timeline/index.tsx | 8 +- .../timeline/properties/helpers.test.tsx | 13 -- .../timeline/properties/helpers.tsx | 61 ++---- .../properties/use_create_timeline.tsx | 4 +- .../timeline/query_tab_content/index.test.tsx | 11 +- .../timeline/query_tab_content/index.tsx | 107 ++++----- .../timelines/components/timeline/styles.tsx | 11 +- .../public/timelines/containers/index.tsx | 1 - .../timelines/store/timeline/actions.ts | 5 + .../public/timelines/store/timeline/model.ts | 1 + .../timelines/store/timeline/reducer.ts | 11 + .../network/details/__mocks__/index.ts | 57 ++++- .../factory/network/details/helpers.ts | 22 +- .../factory/network/details/index.test.ts | 2 +- .../apis/security_solution/network_details.ts | 11 +- 63 files changed, 886 insertions(+), 469 deletions(-) create mode 100644 docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.callback.md create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/header/selectors.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/flyout/selectors.ts create mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/header/selectors.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.callback.md b/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.callback.md new file mode 100644 index 0000000000000..8ebc9068aa612 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.callback.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [AppLeaveConfirmAction](./kibana-plugin-core-public.appleaveconfirmaction.md) > [callback](./kibana-plugin-core-public.appleaveconfirmaction.callback.md) + +## AppLeaveConfirmAction.callback property + +Signature: + +```typescript +callback?: () => void; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.md b/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.md index 969d5ddd44c3e..8650cd9868940 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.md +++ b/docs/development/core/public/kibana-plugin-core-public.appleaveconfirmaction.md @@ -18,6 +18,7 @@ export interface AppLeaveConfirmAction | Property | Type | Description | | --- | --- | --- | +| [callback](./kibana-plugin-core-public.appleaveconfirmaction.callback.md) | () => void | | | [text](./kibana-plugin-core-public.appleaveconfirmaction.text.md) | string | | | [title](./kibana-plugin-core-public.appleaveconfirmaction.title.md) | string | | | [type](./kibana-plugin-core-public.appleaveconfirmaction.type.md) | AppLeaveActionType.confirm | | diff --git a/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md b/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md index a5f8336f6424a..d86f7b7a1a5f9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md +++ b/docs/development/core/public/kibana-plugin-core-public.appleavehandler.md @@ -11,5 +11,5 @@ See [AppMountParameters](./kibana-plugin-core-public.appmountparameters.md) for Signature: ```typescript -export declare type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction; +export declare type AppLeaveHandler = (factory: AppLeaveActionFactory, nextAppId?: string) => AppLeaveAction; ``` diff --git a/src/core/public/application/application_leave.test.ts b/src/core/public/application/application_leave.test.ts index b560bbc0cbc25..9d0da6fe0096d 100644 --- a/src/core/public/application/application_leave.test.ts +++ b/src/core/public/application/application_leave.test.ts @@ -35,7 +35,18 @@ describe('getLeaveAction', () => { type: AppLeaveActionType.default, }); }); + + it('returns the default action provided by the handle and nextAppId', () => { + expect(getLeaveAction((actions) => actions.default(), 'futureAppId')).toEqual({ + type: AppLeaveActionType.default, + }); + }); + it('returns the confirm action provided by the handler', () => { + expect(getLeaveAction((actions) => actions.confirm('some message'), 'futureAppId')).toEqual({ + type: AppLeaveActionType.confirm, + text: 'some message', + }); expect(getLeaveAction((actions) => actions.confirm('some message'))).toEqual({ type: AppLeaveActionType.confirm, text: 'some message', @@ -45,5 +56,14 @@ describe('getLeaveAction', () => { text: 'another message', title: 'a title', }); + const callback = jest.fn(); + expect( + getLeaveAction((actions) => actions.confirm('another message', 'a title', callback)) + ).toEqual({ + type: AppLeaveActionType.confirm, + text: 'another message', + title: 'a title', + callback, + }); }); }); diff --git a/src/core/public/application/application_leave.tsx b/src/core/public/application/application_leave.tsx index 7b69d70d3f6f6..e6170daaff0a0 100644 --- a/src/core/public/application/application_leave.tsx +++ b/src/core/public/application/application_leave.tsx @@ -26,8 +26,8 @@ import { } from './types'; const appLeaveActionFactory: AppLeaveActionFactory = { - confirm(text: string, title?: string) { - return { type: AppLeaveActionType.confirm, text, title }; + confirm(text: string, title?: string, callback?: () => void) { + return { type: AppLeaveActionType.confirm, text, title, callback }; }, default() { return { type: AppLeaveActionType.default }; @@ -38,9 +38,9 @@ export function isConfirmAction(action: AppLeaveAction): action is AppLeaveConfi return action.type === AppLeaveActionType.confirm; } -export function getLeaveAction(handler?: AppLeaveHandler): AppLeaveAction { +export function getLeaveAction(handler?: AppLeaveHandler, nextAppId?: string): AppLeaveAction { if (!handler) { return appLeaveActionFactory.default(); } - return handler(appLeaveActionFactory); + return handler(appLeaveActionFactory, nextAppId); } diff --git a/src/core/public/application/application_service.test.ts b/src/core/public/application/application_service.test.ts index cd186f87b3a87..912ab40cbe1db 100644 --- a/src/core/public/application/application_service.test.ts +++ b/src/core/public/application/application_service.test.ts @@ -755,6 +755,19 @@ describe('#start()', () => { `); }); + it('should call private function shouldNavigate with overlays and the nextAppId', async () => { + service.setup(setupDeps); + const shouldNavigateSpy = jest.spyOn(service as any, 'shouldNavigate'); + + const { navigateToApp } = await service.start(startDeps); + + await navigateToApp('myTestApp'); + expect(shouldNavigateSpy).toHaveBeenCalledWith(startDeps.overlays, 'myTestApp'); + + await navigateToApp('myOtherApp'); + expect(shouldNavigateSpy).toHaveBeenCalledWith(startDeps.overlays, 'myOtherApp'); + }); + describe('when `replace` option is true', () => { it('use `history.replace` instead of `history.push`', async () => { service.setup(setupDeps); diff --git a/src/core/public/application/application_service.tsx b/src/core/public/application/application_service.tsx index 4d54d4831698b..67281170957c6 100644 --- a/src/core/public/application/application_service.tsx +++ b/src/core/public/application/application_service.tsx @@ -244,7 +244,9 @@ export class ApplicationService { ) => { const currentAppId = this.currentAppId$.value; const navigatingToSameApp = currentAppId === appId; - const shouldNavigate = navigatingToSameApp ? true : await this.shouldNavigate(overlays); + const shouldNavigate = navigatingToSameApp + ? true + : await this.shouldNavigate(overlays, appId); if (shouldNavigate) { if (path === undefined) { @@ -332,18 +334,24 @@ export class ApplicationService { this.currentActionMenu$.next(currentActionMenu); }; - private async shouldNavigate(overlays: OverlayStart): Promise { + private async shouldNavigate(overlays: OverlayStart, nextAppId: string): Promise { const currentAppId = this.currentAppId$.value; if (currentAppId === undefined) { return true; } - const action = getLeaveAction(this.appInternalStates.get(currentAppId)?.leaveHandler); + const action = getLeaveAction( + this.appInternalStates.get(currentAppId)?.leaveHandler, + nextAppId + ); if (isConfirmAction(action)) { const confirmed = await overlays.openConfirm(action.text, { title: action.title, 'data-test-subj': 'appLeaveConfirmModal', }); if (!confirmed) { + if (action.callback) { + setTimeout(action.callback, 0); + } return false; } } diff --git a/src/core/public/application/types.ts b/src/core/public/application/types.ts index d9f326c7a59ab..c161a7f166541 100644 --- a/src/core/public/application/types.ts +++ b/src/core/public/application/types.ts @@ -578,7 +578,10 @@ export interface AppMountParameters { * * @public */ -export type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction; +export type AppLeaveHandler = ( + factory: AppLeaveActionFactory, + nextAppId?: string +) => AppLeaveAction; /** * Possible type of actions on application leave. @@ -614,6 +617,7 @@ export interface AppLeaveConfirmAction { type: AppLeaveActionType.confirm; text: string; title?: string; + callback?: () => void; } /** @@ -636,8 +640,10 @@ export interface AppLeaveActionFactory { * * @param text The text to display in the confirmation message * @param title (optional) title to display in the confirmation message + * @param callback (optional) to know that the user want to stay on the page + * so we can show to the user the right UX for him to saved his/her/their changes */ - confirm(text: string, title?: string): AppLeaveConfirmAction; + confirm(text: string, title?: string, callback?: () => void): AppLeaveConfirmAction; /** * Returns a default action, resulting on executing the default behavior when * the user tries to leave an application diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index f48fb9092d56d..3852792547062 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -91,6 +91,8 @@ export enum AppLeaveActionType { // // @public export interface AppLeaveConfirmAction { + // (undocumented) + callback?: () => void; // (undocumented) text: string; // (undocumented) @@ -110,7 +112,7 @@ export interface AppLeaveDefaultAction { // Warning: (ae-forgotten-export) The symbol "AppLeaveActionFactory" needs to be exported by the entry point index.d.ts // // @public -export type AppLeaveHandler = (factory: AppLeaveActionFactory) => AppLeaveAction; +export type AppLeaveHandler = (factory: AppLeaveActionFactory, nextAppId?: string) => AppLeaveAction; // @public (undocumented) export interface ApplicationSetup { diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index 9397307684d6a..0f5e8c133f0d0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -40,7 +40,8 @@ export const GRAPH_TAB_BUTTON = '[data-test-subj="timelineTabs-graph"]'; export const HEADER = '[data-test-subj="header"]'; -export const HEADERS_GROUP = '[data-test-subj="headers-group"]'; +export const HEADERS_GROUP = + '[data-test-subj="events-viewer-panel"] [data-test-subj="headers-group"]'; export const ID_HEADER_FIELD = '[data-test-subj="timeline"] [data-test-subj="header-text-_id"]'; diff --git a/x-pack/plugins/security_solution/public/app/app.tsx b/x-pack/plugins/security_solution/public/app/app.tsx index 54b02c374e43f..ffed557f28511 100644 --- a/x-pack/plugins/security_solution/public/app/app.tsx +++ b/x-pack/plugins/security_solution/public/app/app.tsx @@ -14,6 +14,7 @@ import { ThemeProvider } from 'styled-components'; import { EuiErrorBoundary } from '@elastic/eui'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { AppLeaveHandler } from '../../../../../src/core/public'; import { ManageUserInfo } from '../detections/components/user_info'; import { DEFAULT_DARK_MODE, APP_NAME } from '../../common/constants'; @@ -28,13 +29,21 @@ import { ApolloClientContext } from '../common/utils/apollo_context'; import { ManageGlobalTimeline } from '../timelines/components/manage_timeline'; import { StartServices } from '../types'; import { PageRouter } from './routes'; + interface StartAppComponent extends AppFrontendLibs { children: React.ReactNode; history: History; + onAppLeave: (handler: AppLeaveHandler) => void; store: Store; } -const StartAppComponent: FC = ({ children, apolloClient, history, store }) => { +const StartAppComponent: FC = ({ + children, + apolloClient, + history, + onAppLeave, + store, +}) => { const { i18n } = useKibana().services; const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); @@ -57,7 +66,9 @@ const StartAppComponent: FC = ({ children, apolloClient, hist - {children} + + {children} + @@ -78,6 +89,7 @@ const StartApp = memo(StartAppComponent); interface SecurityAppComponentProps extends AppFrontendLibs { children: React.ReactNode; history: History; + onAppLeave: (handler: AppLeaveHandler) => void; services: StartServices; store: Store; } @@ -86,6 +98,7 @@ const SecurityAppComponent: React.FC = ({ children, apolloClient, history, + onAppLeave, services, store, }) => ( @@ -95,7 +108,7 @@ const SecurityAppComponent: React.FC = ({ ...services, }} > - + {children} diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 3b64c1f7f1f65..30c4e87f695b2 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -23,6 +23,7 @@ import { DETECTIONS_SUB_PLUGIN_ID } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useUpgradeEndpointPackage } from '../../common/hooks/endpoint/upgrade'; import { useThrottledResizeObserver } from '../../common/components/utils'; +import { AppLeaveHandler } from '../../../../../../src/core/public'; const Main = styled.main.attrs<{ paddingTop: number }>(({ paddingTop }) => ({ style: { @@ -39,9 +40,10 @@ Main.displayName = 'Main'; interface HomePageProps { children: React.ReactNode; + onAppLeave: (handler: AppLeaveHandler) => void; } -const HomePageComponent: React.FC = ({ children }) => { +const HomePageComponent: React.FC = ({ children, onAppLeave }) => { const { application, overlays } = useKibana().services; const subPluginId = useRef(''); const { ref, height = 0 } = useThrottledResizeObserver(300); @@ -87,7 +89,7 @@ const HomePageComponent: React.FC = ({ children }) => { {indicesExist && showTimeline && ( <> - + )} diff --git a/x-pack/plugins/security_solution/public/app/index.tsx b/x-pack/plugins/security_solution/public/app/index.tsx index 4c8e87c4abfba..d45c5393c01d6 100644 --- a/x-pack/plugins/security_solution/public/app/index.tsx +++ b/x-pack/plugins/security_solution/public/app/index.tsx @@ -14,12 +14,19 @@ export const renderApp = ({ apolloClient, element, history, + onAppLeave, services, store, SubPluginRoutes, }: RenderAppProps): (() => void) => { render( - + , element diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx index 1d3a59856caa9..ed6d1f319b7e6 100644 --- a/x-pack/plugins/security_solution/public/app/routes.tsx +++ b/x-pack/plugins/security_solution/public/app/routes.tsx @@ -7,20 +7,22 @@ import { History } from 'history'; import React, { FC, memo, useEffect } from 'react'; import { Route, Router, Switch } from 'react-router-dom'; - import { useDispatch } from 'react-redux'; -import { NotFoundPage } from './404'; -import { HomePage } from './home'; + +import { AppLeaveHandler } from '../../../../../src/core/public'; import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes'; import { RouteCapture } from '../common/components/endpoint/route_capture'; import { AppAction } from '../common/store/actions'; +import { NotFoundPage } from './404'; +import { HomePage } from './home'; interface RouterProps { children: React.ReactNode; history: History; + onAppLeave: (handler: AppLeaveHandler) => void; } -const PageRouterComponent: FC = ({ history, children }) => { +const PageRouterComponent: FC = ({ children, history, onAppLeave }) => { const dispatch = useDispatch<(action: AppAction) => void>(); useEffect(() => { return () => { @@ -39,7 +41,7 @@ const PageRouterComponent: FC = ({ history, children }) => { - {children} + {children} diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx index 0dcd29a2d965b..52ab414811cec 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_viewer/index.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useCallback, useMemo } from 'react'; import numeral from '@elastic/numeral'; import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; -import { useFullScreen } from '../../containers/use_full_screen'; +import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { AlertsComponentsProps } from './types'; import { AlertsTable } from './alerts_table'; @@ -30,7 +30,7 @@ const AlertsViewComponent: React.FC = ({ startDate, }) => { const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); const getSubtitle = useCallback( (totalCount: number) => diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx index d6b2efbe43053..4aa8361a0b8e4 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx @@ -36,7 +36,7 @@ import { import { inputsModel } from '../../store'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; import { ExitFullScreen } from '../exit_full_screen'; -import { useFullScreen } from '../../containers/use_full_screen'; +import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { TimelineExpandedEvent, TimelineId } from '../../../../common/types/timeline'; import { GraphOverlay } from '../../../timelines/components/graph_overlay'; @@ -150,7 +150,7 @@ const EventsViewerComponent: React.FC = ({ graphEventId, }) => { const dispatch = useDispatch(); - const { globalFullScreen, timelineFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); const columnsHeader = isEmpty(columns) ? defaultHeaders : columns; const kibana = useKibana(); const [isQueryLoading, setIsQueryLoading] = useState(false); @@ -286,7 +286,7 @@ const EventsViewerComponent: React.FC = ({ id={!resolverIsShowing(graphEventId) ? id : undefined} height={headerFilterGroup ? COMPACT_HEADER_HEIGHT : EVENTS_VIEWER_HEADER_HEIGHT} subtitle={utilityBar ? undefined : subtitle} - title={timelineFullScreen ? justTitle : titleWithExitFullScreen} + title={globalFullScreen ? titleWithExitFullScreen : justTitle} > {HeaderSectionContent} diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 2570a2b6d1f37..3272b0306f9c9 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -16,7 +16,7 @@ import { SubsetTimelineModel, TimelineModel } from '../../../timelines/store/tim import { Filter } from '../../../../../../../src/plugins/data/public'; import { EventsViewer } from './events_viewer'; import { InspectButtonContainer } from '../inspect'; -import { useFullScreen } from '../../containers/use_full_screen'; +import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { SourcererScopeName } from '../../store/sourcerer/model'; import { useSourcererScope } from '../../containers/sourcerer'; import { EventDetailsFlyout } from './event_details_flyout'; @@ -78,7 +78,7 @@ const StatefulEventsViewerComponent: React.FC = ({ selectedPatterns, loading: isLoadingIndexPattern, } = useSourcererScope(scopeId); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); useEffect(() => { if (createTimeline != null) { diff --git a/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx index cd4740bc8c464..12e7df9a11302 100644 --- a/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exit_full_screen/index.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiWindowEvent } from '@elastic/eui'; import React, { useCallback } from 'react'; import styled from 'styled-components'; -import { useFullScreen } from '../../../common/containers/use_full_screen'; +import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from './translations'; @@ -17,7 +17,7 @@ const StyledEuiButton = styled(EuiButton)` `; export const ExitFullScreen: React.FC = () => { - const { globalFullScreen, setGlobalFullScreen } = useFullScreen(); + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); const exitFullScreen = useCallback(() => { setGlobalFullScreen(false); diff --git a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx index 7e8c93e86376a..e8a17d78644df 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_global/index.tsx @@ -11,7 +11,7 @@ import styled from 'styled-components'; import { OutPortal } from 'react-reverse-portal'; import { navTabs } from '../../../app/home/home_navigations'; -import { useFullScreen } from '../../containers/use_full_screen'; +import { useGlobalFullScreen, useTimelineFullScreen } from '../../containers/use_full_screen'; import { SecurityPageName } from '../../../app/types'; import { getAppOverviewUrl } from '../link_to'; import { MlPopover } from '../ml_popover/ml_popover'; @@ -68,7 +68,8 @@ export const HeaderGlobal = React.memo( forwardRef( ({ hideDetectionEngine = false, isFixed = true }, ref) => { const { globalHeaderPortalNode } = useGlobalHeaderPortal(); - const { globalFullScreen, timelineFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen } = useTimelineFullScreen(); const search = useGetUrlSearch(navTabs.overview); const { application, http } = useKibana().services; const { navigateToApp } = application; diff --git a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx index 23f9a8a6bce01..e1f6310644be0 100644 --- a/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/wrapper_page/index.tsx @@ -9,7 +9,7 @@ import React, { useEffect } from 'react'; import styled from 'styled-components'; import { CommonProps } from '@elastic/eui'; -import { useFullScreen } from '../../containers/use_full_screen'; +import { useGlobalFullScreen } from '../../containers/use_full_screen'; import { gutterTimeline } from '../../lib/helpers'; import { AppGlobalStyle } from '../page/index'; @@ -53,7 +53,7 @@ const WrapperPageComponent: React.FC = ({ noTimeline, ...otherProps }) => { - const { globalFullScreen, setGlobalFullScreen } = useFullScreen(); + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); useEffect(() => { setGlobalFullScreen(false); // exit full screen mode on page load }, [setGlobalFullScreen]); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index b7938a5f3d755..577d7aa78e35c 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; @@ -19,7 +19,8 @@ export const useInitSourcerer = ( scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default ) => { const dispatch = useDispatch(); - + const initialTimelineSourcerer = useRef(true); + const initialDetectionSourcerer = useRef(true); const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo(); const getConfigIndexPatternsSelector = useMemo( () => sourcererSelectors.configIndexPatternsSelector(), @@ -27,6 +28,12 @@ export const useInitSourcerer = ( ); const ConfigIndexPatterns = useDeepEqualSelector(getConfigIndexPatternsSelector); + const getSignalIndexNameSelector = useMemo( + () => sourcererSelectors.signalIndexNameSelector(), + [] + ); + const signalIndexNameSelector = useDeepEqualSelector(getSignalIndexNameSelector); + const getTimelineSelector = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const activeTimeline = useDeepEqualSelector((state) => getTimelineSelector(state, TimelineId.active) @@ -36,42 +43,71 @@ export const useInitSourcerer = ( useIndexFields(SourcererScopeName.timeline); useEffect(() => { - if (!loadingSignalIndex && signalIndexName != null) { + if (!loadingSignalIndex && signalIndexName != null && signalIndexNameSelector == null) { dispatch(sourcererActions.setSignalIndexName({ signalIndexName })); } - }, [dispatch, loadingSignalIndex, signalIndexName]); + }, [dispatch, loadingSignalIndex, signalIndexName, signalIndexNameSelector]); // Related to timeline useEffect(() => { if ( !loadingSignalIndex && signalIndexName != null && - (activeTimeline == null || (activeTimeline != null && activeTimeline.savedObjectId == null)) + signalIndexNameSelector == null && + (activeTimeline == null || + (activeTimeline != null && activeTimeline.savedObjectId == null)) && + initialTimelineSourcerer.current ) { + initialTimelineSourcerer.current = false; dispatch( sourcererActions.setSelectedIndexPatterns({ id: SourcererScopeName.timeline, selectedPatterns: [...ConfigIndexPatterns, signalIndexName], }) ); + } else if (signalIndexNameSelector != null && initialTimelineSourcerer.current) { + initialTimelineSourcerer.current = false; + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: SourcererScopeName.timeline, + selectedPatterns: [...ConfigIndexPatterns, signalIndexNameSelector], + }) + ); } - }, [activeTimeline, ConfigIndexPatterns, dispatch, loadingSignalIndex, signalIndexName]); + }, [ + activeTimeline, + ConfigIndexPatterns, + dispatch, + loadingSignalIndex, + signalIndexName, + signalIndexNameSelector, + ]); // Related to the detection page useEffect(() => { if ( scopeId === SourcererScopeName.detections && isSignalIndexExists && - signalIndexName != null + signalIndexName != null && + initialDetectionSourcerer.current ) { + initialDetectionSourcerer.current = false; dispatch( sourcererActions.setSelectedIndexPatterns({ id: scopeId, selectedPatterns: [signalIndexName], }) ); + } else if (signalIndexNameSelector != null && initialTimelineSourcerer.current) { + initialDetectionSourcerer.current = false; + dispatch( + sourcererActions.setSelectedIndexPatterns({ + id: scopeId, + selectedPatterns: [signalIndexNameSelector], + }) + ); } - }, [dispatch, isSignalIndexExists, scopeId, signalIndexName]); + }, [dispatch, isSignalIndexExists, scopeId, signalIndexName, signalIndexNameSelector]); }; export const useSourcererScope = (scope: SourcererScopeName = SourcererScopeName.default) => { diff --git a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx index 8357a9d22739e..874005bf07428 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_full_screen/index.tsx @@ -28,13 +28,20 @@ export const resetScroll = () => { }, 0); }; -export const useFullScreen = () => { +interface GlobalFullScreen { + globalFullScreen: boolean; + setGlobalFullScreen: (fullScreen: boolean) => void; +} + +interface TimelineFullScreen { + timelineFullScreen: boolean; + setTimelineFullScreen: (fullScreen: boolean) => void; +} + +export const useGlobalFullScreen = (): GlobalFullScreen => { const dispatch = useDispatch(); const globalFullScreen = useShallowEqualSelector(inputsSelectors.globalFullScreenSelector) ?? false; - const timelineFullScreen = - useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; - const setGlobalFullScreen = useCallback( (fullScreen: boolean) => { if (fullScreen) { @@ -49,21 +56,31 @@ export const useFullScreen = () => { }, [dispatch] ); + const memoizedReturn = useMemo( + () => ({ + globalFullScreen, + setGlobalFullScreen, + }), + [globalFullScreen, setGlobalFullScreen] + ); + return memoizedReturn; +}; + +export const useTimelineFullScreen = (): TimelineFullScreen => { + const dispatch = useDispatch(); + const timelineFullScreen = + useShallowEqualSelector(inputsSelectors.timelineFullScreenSelector) ?? false; const setTimelineFullScreen = useCallback( (fullScreen: boolean) => dispatch(inputsActions.setFullScreen({ id: 'timeline', fullScreen })), [dispatch] ); - const memoizedReturn = useMemo( () => ({ - globalFullScreen, - setGlobalFullScreen, - setTimelineFullScreen, timelineFullScreen, + setTimelineFullScreen, }), - [globalFullScreen, setGlobalFullScreen, setTimelineFullScreen, timelineFullScreen] + [timelineFullScreen, setTimelineFullScreen] ); - return memoizedReturn; }; diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts index 3e47478b783eb..fcf7dfec0f2a4 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.test.ts @@ -20,6 +20,7 @@ describe('createInitialState', () => { { kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }], configIndexPatterns: ['auditbeat-*', 'filebeat'], + signalIndexName: 'siem-signals-default', } ); @@ -32,6 +33,7 @@ describe('createInitialState', () => { { kibanaIndexPatterns: [{ id: '1234567890987654321', title: 'mock-kibana' }], configIndexPatterns: [], + signalIndexName: 'siem-signals-default', } ); diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index 8d528f4279955..f48bf31e62575 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -34,7 +34,12 @@ export const createInitialState = ( { kibanaIndexPatterns, configIndexPatterns, - }: { kibanaIndexPatterns: KibanaIndexPatterns; configIndexPatterns: string[] } + signalIndexName, + }: { + kibanaIndexPatterns: KibanaIndexPatterns; + configIndexPatterns: string[]; + signalIndexName: string | null; + } ): PreloadedState => { const preloadedState: PreloadedState = { app: initialAppState, @@ -52,6 +57,7 @@ export const createInitialState = ( }, kibanaIndexPatterns, configIndexPatterns, + signalIndexName, }, }; return preloadedState; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 13be87846df80..dda35ad26a685 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -37,7 +37,7 @@ import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unau import * as i18n from './translations'; import { LinkButton } from '../../../common/components/links'; import { useFormatUrl } from '../../../common/components/link_to'; -import { useFullScreen } from '../../../common/containers/use_full_screen'; +import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'; import { Display } from '../../../hosts/pages/display'; import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; import { timelineSelectors } from '../../../timelines/store/timeline'; @@ -61,7 +61,7 @@ const DetectionEnginePageComponent = () => { const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); const { to, from, deleteQuery, setQuery } = useGlobalTime(); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); const [ { loading: userInfoLoading, 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 28c7805e968d6..3986e02b5b9b9 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 @@ -79,7 +79,7 @@ import { LinkButton } from '../../../../../common/components/links'; import { useFormatUrl } from '../../../../../common/components/link_to'; import { ExceptionsViewer } from '../../../../../common/components/exceptions/viewer'; import { DEFAULT_INDEX_PATTERN } from '../../../../../../common/constants'; -import { useFullScreen } from '../../../../../common/containers/use_full_screen'; +import { useGlobalFullScreen } from '../../../../../common/containers/use_full_screen'; import { Display } from '../../../../../hosts/pages/display'; import { ExceptionListTypeEnum, ExceptionListIdentifiers } from '../../../../../shared_imports'; import { useRuleAsync } from '../../../../containers/detection_engine/rules/use_rule_async'; @@ -178,7 +178,7 @@ const RuleDetailsPageComponent = () => { const mlCapabilities = useMlCapabilities(); const history = useHistory(); const { formatUrl } = useFormatUrl(SecurityPageName.detections); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); // TODO: Refactor license check + hasMlAdminPermissions to common check const hasMlPermissions = hasMlLicense(mlCapabilities) && hasMlAdminPermissions(mlCapabilities); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 58474f05bb2b9..7eef46a480707 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -43,7 +43,7 @@ import { HostDetailsProps } from './types'; import { type } from './utils'; import { getHostDetailsPageFilters } from './helpers'; import { showGlobalFilters } from '../../../timelines/components/timeline/helpers'; -import { useFullScreen } from '../../../common/containers/use_full_screen'; +import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'; import { Display } from '../display'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { TimelineId } from '../../../../common/types/timeline'; @@ -68,7 +68,7 @@ const HostDetailsComponent: React.FC = ({ detailName, hostDeta const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); const capabilities = useMlCapabilities(); const kibana = useKibana(); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index d54891ba573fd..52ec837a09eb6 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -20,7 +20,7 @@ import { SiemNavigation } from '../../common/components/navigation'; import { HostsKpiComponent } from '../components/kpi_hosts'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { useFullScreen } from '../../common/containers/use_full_screen'; +import { useGlobalFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { TimelineId } from '../../../common/types/timeline'; import { LastEventIndexKey } from '../../../common/search_strategy'; @@ -66,7 +66,7 @@ const HostsComponent = () => { const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); const capabilities = useMlCapabilities(); const { uiSettings } = useKibana().services; const { tabName } = useParams<{ tabName: string }>(); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx index e30071ec04f0c..1540ffd59f2de 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -16,7 +16,7 @@ import { MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import { MatrixHistogram } from '../../../common/components/matrix_histogram'; -import { useFullScreen } from '../../../common/containers/use_full_screen'; +import { useGlobalFullScreen } from '../../../common/containers/use_full_screen'; import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; import { useManageTimeline } from '../../../timelines/components/manage_timeline'; @@ -62,7 +62,7 @@ const EventsQueryTabBodyComponent: React.FC = ({ }) => { const dispatch = useDispatch(); const { initializeTimeline } = useManageTimeline(); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); useEffect(() => { initializeTimeline({ id: TimelineId.hostsPageEvents, diff --git a/x-pack/plugins/security_solution/public/network/pages/network.tsx b/x-pack/plugins/security_solution/public/network/pages/network.tsx index f9e30e30472d9..3a095cfb21f0e 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.tsx @@ -22,7 +22,7 @@ import { SiemNavigation } from '../../common/components/navigation'; import { NetworkKpiComponent } from '../components/kpi_network'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { useFullScreen } from '../../common/containers/use_full_screen'; +import { useGlobalFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { LastEventIndexKey } from '../../../common/search_strategy'; import { useKibana } from '../../common/lib/kibana'; @@ -62,7 +62,7 @@ const NetworkComponent = React.memo( const filters = useDeepEqualSelector(getGlobalFiltersQuerySelector); const { to, from, setQuery, isInitializing } = useGlobalTime(); - const { globalFullScreen } = useFullScreen(); + const { globalFullScreen } = useGlobalFullScreen(); const kibana = useKibana(); const { tabName } = useParams<{ tabName: string }>(); diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 4f37b5b15d73a..0b5093ff50c39 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -41,6 +41,7 @@ import { APP_CASES_PATH, APP_PATH, DEFAULT_INDEX_KEY, + DETECTION_ENGINE_INDEX_URL, } from '../common/constants'; import { SecurityPageName } from './app/types'; @@ -435,6 +436,15 @@ export class Plugin implements IPlugin `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx index 749bc4b1f010d..6a51c7180587f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/active_timelines.tsx @@ -4,44 +4,49 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiHealth, EuiToolTip } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash/fp'; import styled from 'styled-components'; +import { FormattedRelative } from '@kbn/i18n/react'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; import { TimelineEventsCountBadge } from '../../../../common/hooks/use_timeline_events_count'; import { UNTITLED_TIMELINE, UNTITLED_TEMPLATE } from '../../timeline/properties/translations'; import { timelineActions } from '../../../store/timeline'; +import * as i18n from './translations'; const ButtonWrapper = styled(EuiFlexItem)` flex-direction: row; align-items: center; `; +const EuiHealthStyled = styled(EuiHealth)` + display: block; +`; + interface ActiveTimelinesProps { timelineId: string; + timelineStatus: TimelineStatus; timelineTitle: string; timelineType: TimelineType; isOpen: boolean; + updated?: number; } const StyledEuiButtonEmpty = styled(EuiButtonEmpty)` > span { padding: 0; - - > span { - display: flex; - flex-direction: row; - } } `; const ActiveTimelinesComponent: React.FC = ({ timelineId, + timelineStatus, timelineType, timelineTitle, + updated, isOpen, }) => { const dispatch = useDispatch(); @@ -57,17 +62,47 @@ const ActiveTimelinesComponent: React.FC = ({ ? UNTITLED_TEMPLATE : UNTITLED_TIMELINE; + const tooltipContent = useMemo(() => { + if (timelineStatus === TimelineStatus.draft) { + return <>{i18n.UNSAVED}; + } + return ( + <> + {i18n.AUTOSAVED}{' '} + + + ); + }, [timelineStatus, updated]); + return ( - {title} - {!isOpen && } + + + + + + + {title} + {!isOpen && ( + + + + )} + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx index 368cb53eccc34..063e968a6c51a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/index.tsx @@ -32,6 +32,8 @@ import { InspectButton } from '../../../../common/components/inspect'; import { ActiveTimelines } from './active_timelines'; import * as i18n from './translations'; import * as commonI18n from '../../timeline/properties/translations'; +import { getTimelineStatusByIdSelector } from './selectors'; +import { TimelineTabs } from '../../../store/timeline/model'; // to hide side borders const StyledPanel = styled(EuiPanel)` @@ -49,9 +51,27 @@ interface FlyoutHeaderPanelProps { const FlyoutHeaderPanelComponent: React.FC = ({ timelineId }) => { const dispatch = useDispatch(); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const { dataProviders, kqlQuery, title, timelineType, show } = useDeepEqualSelector((state) => + const { + activeTab, + dataProviders, + kqlQuery, + title, + timelineType, + status: timelineStatus, + updated, + show, + } = useDeepEqualSelector((state) => pick( - ['dataProviders', 'kqlQuery', 'title', 'timelineType', 'show'], + [ + 'activeTab', + 'dataProviders', + 'kqlQuery', + 'status', + 'title', + 'timelineType', + 'updated', + 'show', + ], getTimeline(state, timelineId) ?? timelineDefaults ) ); @@ -67,29 +87,33 @@ const FlyoutHeaderPanelComponent: React.FC = ({ timeline return ( - + {show && ( - - - + {activeTab === TimelineTabs.query && ( + + + + )} = ({ timelineId const TimelineDescription = React.memo(TimelineDescriptionComponent); const TimelineStatusInfoComponent: React.FC = ({ timelineId }) => { - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const getTimelineStatus = useMemo(() => getTimelineStatusByIdSelector(), []); const { status: timelineStatus, updated } = useDeepEqualSelector((state) => - pick(['status', 'updated'], getTimeline(state, timelineId) ?? timelineDefaults) + getTimelineStatus(state, timelineId) ); const isUnsaved = useMemo(() => timelineStatus === TimelineStatus.draft, [timelineStatus]); @@ -198,16 +222,16 @@ const TimelineStatusInfoComponent: React.FC = ({ timelineId } const TimelineStatusInfo = React.memo(TimelineStatusInfoComponent); const FlyoutHeaderComponent: React.FC = ({ timelineId }) => ( - + - + - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/header/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/selectors.ts new file mode 100644 index 0000000000000..634fa5a775f1a --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/header/selectors.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { TimelineStatus } from '../../../../../common/types/timeline'; +import { timelineSelectors } from '../../../store/timeline'; + +export const getTimelineStatusByIdSelector = () => + createSelector(timelineSelectors.selectTimeline, (timeline) => ({ + status: timeline?.status ?? TimelineStatus.draft, + updated: timeline?.updated ?? undefined, + })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx index 5d118b357c8ef..41e2a569f41bb 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.test.tsx @@ -40,6 +40,10 @@ jest.mock('../timeline', () => ({ describe('Flyout', () => { const state: State = mockGlobalState; const { storage } = createSecuritySolutionStorageMock(); + const props = { + onAppLeave: jest.fn(), + timelineId: 'test', + }; beforeEach(() => { mockDispatch.mockClear(); @@ -49,7 +53,7 @@ describe('Flyout', () => { test('it renders correctly against snapshot', () => { const wrapper = shallow( - + ); expect(wrapper.find('Flyout')).toMatchSnapshot(); @@ -58,7 +62,7 @@ describe('Flyout', () => { test('it renders the default flyout state as a bottom bar', () => { const wrapper = mount( - + ); @@ -79,7 +83,7 @@ describe('Flyout', () => { const wrapper = mount( - + ); @@ -91,7 +95,7 @@ describe('Flyout', () => { test('should call the onOpen when the mouse is clicked for rendering', () => { const wrapper = mount( - + ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx index a1e61b9fa4ae6..0636b76ef61bc 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/index.tsx @@ -4,14 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import React, { useEffect, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import styled from 'styled-components'; +import { AppLeaveHandler } from '../../../../../../../src/core/public'; +import { TimelineId, TimelineStatus } from '../../../../common/types/timeline'; +import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; +import { timelineActions } from '../../store/timeline'; +import { TimelineTabs } from '../../store/timeline/model'; import { FlyoutBottomBar } from './bottom_bar'; import { Pane } from './pane'; -import { timelineSelectors } from '../../store/timeline'; -import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; -import { timelineDefaults } from '../../store/timeline/defaults'; +import { getTimelineShowStatusByIdSelector } from './selectors'; const Visible = styled.div<{ show?: boolean }>` visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; @@ -21,14 +26,58 @@ Visible.displayName = 'Visible'; interface OwnProps { timelineId: string; + onAppLeave: (handler: AppLeaveHandler) => void; } -const FlyoutComponent: React.FC = ({ timelineId }) => { - const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const show = useShallowEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).show +const FlyoutComponent: React.FC = ({ timelineId, onAppLeave }) => { + const dispatch = useDispatch(); + const getTimelineShowStatus = useMemo(() => getTimelineShowStatusByIdSelector(), []); + const { show, status: timelineStatus, updated } = useDeepEqualSelector((state) => + getTimelineShowStatus(state, timelineId) ); + useEffect(() => { + onAppLeave((actions, nextAppId) => { + if (show) { + dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: false })); + } + // Confirm when the user has made any changes to a timeline + if ( + !(nextAppId ?? '').includes('securitySolution') && + timelineStatus === TimelineStatus.draft && + updated != null + ) { + const showSaveTimelineModal = () => { + dispatch(timelineActions.showTimeline({ id: TimelineId.active, show: true })); + dispatch( + timelineActions.setActiveTabTimeline({ + id: TimelineId.active, + activeTab: TimelineTabs.query, + }) + ); + dispatch( + timelineActions.toggleModalSaveTimeline({ + id: TimelineId.active, + showModalSaveTimeline: true, + }) + ); + }; + + return actions.confirm( + i18n.translate('xpack.securitySolution.timeline.unsavedWorkMessage', { + defaultMessage: 'Leave Timeline with unsaved work?', + }), + i18n.translate('xpack.securitySolution.timeline.unsavedWorkTitle', { + defaultMessage: 'Unsaved changes', + }), + showSaveTimelineModal + ); + } else { + return actions.default(); + } + }); + }, [dispatch, onAppLeave, show, timelineStatus, updated]); + return ( <> diff --git a/x-pack/plugins/security_solution/public/timelines/components/flyout/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/flyout/selectors.ts new file mode 100644 index 0000000000000..ca811afd164f6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/flyout/selectors.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { TimelineStatus } from '../../../../common/types/timeline'; +import { timelineSelectors } from '../../store/timeline'; + +export const getTimelineShowStatusByIdSelector = () => + createSelector(timelineSelectors.selectTimeline, (timeline) => ({ + status: timeline?.status ?? TimelineStatus.draft, + show: timeline?.show ?? false, + updated: timeline?.updated ?? undefined, + })); diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx index 3d5e548e726e5..ececded801b45 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.test.tsx @@ -8,7 +8,10 @@ import { waitFor } from '@testing-library/react'; import { mount } from 'enzyme'; import React from 'react'; -import { useFullScreen } from '../../../common/containers/use_full_screen'; +import { + useGlobalFullScreen, + useTimelineFullScreen, +} from '../../../common/containers/use_full_screen'; import { mockTimelineModel, TestProviders } from '../../../common/mock'; import { TimelineId } from '../../../../common/types/timeline'; @@ -20,17 +23,20 @@ jest.mock('../../../common/hooks/use_selector', () => ({ })); jest.mock('../../../common/containers/use_full_screen', () => ({ - useFullScreen: jest.fn(), + useGlobalFullScreen: jest.fn(), + useTimelineFullScreen: jest.fn(), })); describe('GraphOverlay', () => { beforeEach(() => { - (useFullScreen as jest.Mock).mockReturnValue({ - timelineFullScreen: false, - setTimelineFullScreen: jest.fn(), + (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: false, setGlobalFullScreen: jest.fn(), }); + (useTimelineFullScreen as jest.Mock).mockReturnValue({ + timelineFullScreen: false, + setTimelineFullScreen: jest.fn(), + }); }); describe('when used in an events viewer (i.e. in the Detections view, or the Host > Events view)', () => { @@ -51,12 +57,14 @@ describe('GraphOverlay', () => { }); test('it has a calculated width that makes room for the Timeline flyout button when isEventViewer is true in full screen mode', async () => { - (useFullScreen as jest.Mock).mockReturnValue({ - timelineFullScreen: false, - setTimelineFullScreen: jest.fn(), + (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: true, // <-- true when an events viewer is in full screen mode setGlobalFullScreen: jest.fn(), }); + (useTimelineFullScreen as jest.Mock).mockReturnValue({ + timelineFullScreen: false, + setTimelineFullScreen: jest.fn(), + }); const wrapper = mount( @@ -89,12 +97,14 @@ describe('GraphOverlay', () => { }); test('it has 100% width when isEventViewer is false and the active timeline is in full screen mode', async () => { - (useFullScreen as jest.Mock).mockReturnValue({ - timelineFullScreen: true, // <-- true when the active timeline is in full screen mode - setTimelineFullScreen: jest.fn(), + (useGlobalFullScreen as jest.Mock).mockReturnValue({ globalFullScreen: false, setGlobalFullScreen: jest.fn(), }); + (useTimelineFullScreen as jest.Mock).mockReturnValue({ + timelineFullScreen: true, // <-- true when the active timeline is in full screen mode + setTimelineFullScreen: jest.fn(), + }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx index 069f46c40e6af..8fac8fec0b61d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/graph_overlay/index.tsx @@ -20,7 +20,10 @@ import styled from 'styled-components'; import { FULL_SCREEN } from '../timeline/body/column_headers/translations'; import { EXIT_FULL_SCREEN } from '../../../common/components/exit_full_screen/translations'; import { DEFAULT_INDEX_KEY, FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../common/constants'; -import { useFullScreen } from '../../../common/containers/use_full_screen'; +import { + useGlobalFullScreen, + useTimelineFullScreen, +} from '../../../common/containers/use_full_screen'; import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; import { TimelineId } from '../../../../common/types/timeline'; import { timelineSelectors } from '../../store/timeline'; @@ -114,12 +117,8 @@ const GraphOverlayComponent: React.FC = ({ isEventViewer, timelineId } (state) => (getTimeline(state, timelineId) ?? timelineDefaults).graphEventId ); - const { - timelineFullScreen, - setTimelineFullScreen, - globalFullScreen, - setGlobalFullScreen, - } = useFullScreen(); + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const fullScreen = useMemo( () => isFullScreen({ globalFullScreen, timelineId, timelineFullScreen }), diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index df12194e264de..37de75fd736af 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -399,13 +399,15 @@ export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeli to, ruleNote, }: UpdateTimeline): (() => void) => () => { - dispatch( - sourcererActions.initTimelineIndexPatterns({ - id: SourcererScopeName.timeline, - selectedPatterns: timeline.indexNames, - eventType: timeline.eventType, - }) - ); + if (!isEmpty(timeline.indexNames)) { + dispatch( + sourcererActions.initTimelineIndexPatterns({ + id: SourcererScopeName.timeline, + selectedPatterns: timeline.indexNames, + eventType: timeline.eventType, + }) + ); + } if ( timeline.status === TimelineStatus.immutable && timeline.timelineType === TimelineType.template diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx index 54cb4d5d14462..e3808514856e7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/index.tsx @@ -27,7 +27,10 @@ import { } from '../../../../../common/components/drag_and_drop/helpers'; import { EXIT_FULL_SCREEN } from '../../../../../common/components/exit_full_screen/translations'; import { FULL_SCREEN_TOGGLED_CLASS_NAME } from '../../../../../../common/constants'; -import { useFullScreen } from '../../../../../common/containers/use_full_screen'; +import { + useGlobalFullScreen, + useTimelineFullScreen, +} from '../../../../../common/containers/use_full_screen'; import { TimelineId } from '../../../../../../common/types/timeline'; import { OnSelectAll } from '../../events'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; @@ -50,8 +53,16 @@ import * as i18n from './translations'; import { timelineActions } from '../../../../store/timeline'; const SortingColumnsContainer = styled.div` - .euiPopover .euiButtonEmpty .euiButtonContent .euiButtonEmpty__text { - display: none; + button { + color: ${({ theme }) => theme.eui.euiColorPrimary}; + } + + .euiPopover .euiButtonEmpty .euiButtonContent { + padding: 0; + + .euiButtonEmpty__text { + display: none; + } } `; @@ -115,12 +126,8 @@ export const ColumnHeadersComponent = ({ }: Props) => { const dispatch = useDispatch(); const [draggingIndex, setDraggingIndex] = useState(null); - const { - timelineFullScreen, - setTimelineFullScreen, - globalFullScreen, - setGlobalFullScreen, - } = useFullScreen(); + const { globalFullScreen, setGlobalFullScreen } = useGlobalFullScreen(); + const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const toggleFullScreen = useCallback(() => { if (timelineId === TimelineId.active) { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx index 17d57b46d730c..c66a6e830ccf7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/footer/index.tsx @@ -130,6 +130,9 @@ export const EventsCountComponent = ({ serverSideEventCount: number; footerText: string; }) => { + const totalCount = useMemo(() => (serverSideEventCount > 0 ? serverSideEventCount : 0), [ + serverSideEventCount, + ]); return (

    - + - {serverSideEventCount} + {totalCount} {' '} {documentType} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx index e9dc312ee8d19..18b2ebc2ec253 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.test.tsx @@ -3,13 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - import React from 'react'; -import { shallow, mount } from 'enzyme'; - +import { mount } from 'enzyme'; import { SaveTimelineButton } from './save_timeline_button'; -import { act } from '@testing-library/react-hooks'; - +import { TestProviders } from '../../../../common/mock'; jest.mock('react-redux', () => { const actual = jest.requireActual('react-redux'); return { @@ -17,60 +14,59 @@ jest.mock('react-redux', () => { useDispatch: jest.fn(), }; }); - +jest.mock('../../../../common/lib/kibana'); jest.mock('./title_and_description'); - describe('SaveTimelineButton', () => { const props = { + initialFocus: 'title' as const, timelineId: 'timeline-1', - showOverlay: false, toolTip: 'tooltip message', - toggleSaveTimeline: jest.fn(), - onSaveTimeline: jest.fn(), - updateTitle: jest.fn(), - updateDescription: jest.fn(), }; test('Show tooltip', () => { - const component = shallow(); + const component = mount( + + + + ); expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true); }); - test('Hide tooltip', () => { - const testProps = { - ...props, - showOverlay: true, - }; - const component = mount(); + const component = mount( + + + + ); component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click'); - - act(() => { - expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual( - false - ); - }); + expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(false); }); - test('should show a button with pencil icon', () => { - const component = shallow(); - expect(component.find('[data-test-subj="save-timeline-button-icon"]').prop('iconType')).toEqual( - 'pencil' + const component = mount( + + + ); + expect( + component.find('[data-test-subj="save-timeline-button-icon"]').first().prop('iconType') + ).toEqual('pencil'); }); - test('should not show a modal when showOverlay equals false', () => { - const component = shallow(); + const component = mount( + + + + ); expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(false); }); - test('should show a modal when showOverlay equals true', () => { - const testProps = { - ...props, - showOverlay: true, - }; - const component = mount(); + const component = mount( + + + + ); + expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(true); + expect(component.find('[data-test-subj="save-timeline-modal-comp"]').exists()).toEqual(false); component.find('[data-test-subj="save-timeline-button-icon"]').first().simulate('click'); - act(() => { - expect(component.find('[data-test-subj="save-timeline-modal"]').exists()).toEqual(true); - }); + expect(component.find('[data-test-subj="save-timeline-btn-tooltip"]').exists()).toEqual(false); + expect(component.find('[data-test-subj="save-timeline-modal-comp"]').exists()).toEqual(true); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx index f3bd4a88ca236..46898a8daaf89 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/save_timeline_button.tsx @@ -4,53 +4,69 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButtonIcon, EuiOverlayMask, EuiModal, EuiToolTip } from '@elastic/eui'; - +import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; -import { NOTES_PANEL_WIDTH } from '../properties/notes_size'; +import { useDispatch } from 'react-redux'; +import { TimelineId } from '../../../../../common/types/timeline'; +import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { timelineActions } from '../../../store/timeline'; +import { getTimelineSaveModalByIdSelector } from './selectors'; import { TimelineTitleAndDescription } from './title_and_description'; import { EDIT } from './translations'; export interface SaveTimelineComponentProps { + initialFocus: 'title' | 'description'; timelineId: string; toolTip?: string; } export const SaveTimelineButton = React.memo( - ({ timelineId, toolTip }) => { + ({ initialFocus, timelineId, toolTip }) => { + const dispatch = useDispatch(); + const getTimelineSaveModal = useMemo(() => getTimelineSaveModalByIdSelector(), []); + const show = useDeepEqualSelector((state) => getTimelineSaveModal(state, timelineId)); const [showSaveTimelineOverlay, setShowSaveTimelineOverlay] = useState(false); - const onToggleSaveTimeline = useCallback(() => { - setShowSaveTimelineOverlay((prevShowSaveTimelineOverlay) => !prevShowSaveTimelineOverlay); + + const closeSaveTimeline = useCallback(() => { + setShowSaveTimelineOverlay(false); + if (show) { + dispatch( + timelineActions.toggleModalSaveTimeline({ + id: TimelineId.active, + showModalSaveTimeline: false, + }) + ); + } + }, [dispatch, setShowSaveTimelineOverlay, show]); + + const openSaveTimeline = useCallback(() => { + setShowSaveTimelineOverlay(true); }, [setShowSaveTimelineOverlay]); const saveTimelineButtonIcon = useMemo( () => ( ), - [onToggleSaveTimeline] + [openSaveTimeline] ); - return showSaveTimelineOverlay ? ( + return (initialFocus === 'title' && show) || showSaveTimelineOverlay ? ( <> {saveTimelineButtonIcon} - - - - - + ) : ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/selectors.ts b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/selectors.ts new file mode 100644 index 0000000000000..8aa895e68dc7e --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/selectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; + +import { timelineSelectors } from '../../../store/timeline'; + +export const getTimelineSaveModalByIdSelector = () => + createSelector(timelineSelectors.selectTimeline, (timeline) => timeline?.showSaveModal ?? false); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx index cb31765bd9c37..2b8ec62199478 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.test.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TimelineTitleAndDescription } from './title_and_description'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; -import { useCreateTimelineButton } from '../properties/use_create_timeline'; -import { TimelineType } from '../../../../../common/types/timeline'; +import { TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; import * as i18n from './translations'; jest.mock('../../../../common/hooks/use_selector', () => ({ @@ -17,7 +16,7 @@ jest.mock('../../../../common/hooks/use_selector', () => ({ })); jest.mock('../properties/use_create_timeline', () => ({ - useCreateTimelineButton: jest.fn(), + useCreateTimeline: jest.fn(), })); jest.mock('react-redux', () => { @@ -31,8 +30,10 @@ jest.mock('react-redux', () => { describe('TimelineTitleAndDescription', () => { describe('save timeline', () => { const props = { + initialFocus: 'title' as const, + closeSaveTimeline: jest.fn(), + openSaveTimeline: jest.fn(), timelineId: 'timeline-1', - toggleSaveTimeline: jest.fn(), onSaveTimeline: jest.fn(), updateTitle: jest.fn(), updateDescription: jest.fn(), @@ -44,22 +45,18 @@ describe('TimelineTitleAndDescription', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: '', isSaving: true, - savedObjectId: null, + status: TimelineStatus.draft, title: 'my timeline', timelineType: TimelineType.default, }); - (useCreateTimelineButton as jest.Mock).mockReturnValue({ - getButton: mockGetButton, - }); }); afterEach(() => { (useDeepEqualSelector as jest.Mock).mockReset(); - (useCreateTimelineButton as jest.Mock).mockReset(); mockGetButton.mockClear(); }); - test('show proress bar while saving', () => { + test('show process bar while saving', () => { const component = shallow(); expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true); }); @@ -75,7 +72,7 @@ describe('TimelineTitleAndDescription', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: '', isSaving: true, - savedObjectId: null, + status: TimelineStatus.draft, title: 'my timeline', timelineType: TimelineType.template, }); @@ -108,6 +105,9 @@ describe('TimelineTitleAndDescription', () => { describe('update timeline', () => { const props = { + initialFocus: 'title' as const, + closeSaveTimeline: jest.fn(), + openSaveTimeline: jest.fn(), timelineId: 'timeline-1', toggleSaveTimeline: jest.fn(), onSaveTimeline: jest.fn(), @@ -121,22 +121,18 @@ describe('TimelineTitleAndDescription', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: 'xxxx', isSaving: true, - savedObjectId: '1234', + status: TimelineStatus.active, title: 'my timeline', timelineType: TimelineType.default, }); - (useCreateTimelineButton as jest.Mock).mockReturnValue({ - getButton: mockGetButton, - }); }); afterEach(() => { (useDeepEqualSelector as jest.Mock).mockReset(); - (useCreateTimelineButton as jest.Mock).mockReset(); mockGetButton.mockClear(); }); - test('show proress bar while saving', () => { + test('show process bar while saving', () => { const component = shallow(); expect(component.find('[data-test-subj="progress-bar"]').exists()).toEqual(true); }); @@ -152,7 +148,7 @@ describe('TimelineTitleAndDescription', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: 'xxxx', isSaving: true, - savedObjectId: '1234', + status: TimelineStatus.active, title: 'my timeline', timelineType: TimelineType.template, }); @@ -180,6 +176,9 @@ describe('TimelineTitleAndDescription', () => { describe('showWarning', () => { const props = { + initialFocus: 'title' as const, + closeSaveTimeline: jest.fn(), + openSaveTimeline: jest.fn(), timelineId: 'timeline-1', toggleSaveTimeline: jest.fn(), onSaveTimeline: jest.fn(), @@ -194,19 +193,15 @@ describe('TimelineTitleAndDescription', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: '', isSaving: true, - savedObjectId: null, + status: TimelineStatus.draft, title: 'my timeline', timelineType: TimelineType.default, showWarnging: true, }); - (useCreateTimelineButton as jest.Mock).mockReturnValue({ - getButton: mockGetButton, - }); }); afterEach(() => { (useDeepEqualSelector as jest.Mock).mockReset(); - (useCreateTimelineButton as jest.Mock).mockReset(); mockGetButton.mockClear(); }); @@ -217,34 +212,23 @@ describe('TimelineTitleAndDescription', () => { test('Show discardTimelineButton', () => { const component = shallow(); - expect(component.find('[data-test-subj="mock-discard-button"]').exists()).toEqual(true); - }); - - test('get discardTimelineButton with correct props', () => { - shallow(); - expect(mockGetButton).toBeCalledWith({ - title: i18n.DISCARD_TIMELINE, - outline: true, - iconType: '', - fill: false, - }); + expect(component.find('[data-test-subj="close-button"]').dive().text()).toEqual( + 'Discard Timeline' + ); }); test('get discardTimelineTemplateButton with correct props', () => { (useDeepEqualSelector as jest.Mock).mockReturnValue({ description: 'xxxx', isSaving: true, - savedObjectId: null, + status: TimelineStatus.draft, title: 'my timeline', timelineType: TimelineType.template, }); - shallow(); - expect(mockGetButton).toBeCalledWith({ - title: i18n.DISCARD_TIMELINE_TEMPLATE, - outline: true, - iconType: '', - fill: false, - }); + const component = shallow(); + expect(component.find('[data-test-subj="close-button"]').dive().text()).toEqual( + 'Discard Timeline Template' + ); }); test('Show saveButton', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx index 72e7778347f44..87d4fcdb7075f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/header/title_and_description.tsx @@ -9,6 +9,8 @@ import { EuiFlexGroup, EuiFormRow, EuiFlexItem, + EuiOverlayMask, + EuiModal, EuiModalBody, EuiModalHeader, EuiSpacer, @@ -18,19 +20,23 @@ import { import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useDispatch } from 'react-redux'; import styled from 'styled-components'; -import { TimelineType } from '../../../../../common/types/timeline'; + +import { TimelineId, TimelineStatus, TimelineType } from '../../../../../common/types/timeline'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import { timelineActions, timelineSelectors } from '../../../../timelines/store/timeline'; import { TimelineInput } from '../../../store/timeline/actions'; import { Description, Name } from '../properties/helpers'; +import { NOTES_PANEL_WIDTH } from '../properties/notes_size'; import { TIMELINE_TITLE, DESCRIPTION, OPTIONAL } from '../properties/translations'; -import { useCreateTimelineButton } from '../properties/use_create_timeline'; +import { useCreateTimeline } from '../properties/use_create_timeline'; import * as i18n from './translations'; interface TimelineTitleAndDescriptionProps { - showWarning?: boolean; + closeSaveTimeline: () => void; + initialFocus: 'title' | 'description'; + openSaveTimeline: () => void; timelineId: string; - toggleSaveTimeline: () => void; + showWarning?: boolean; } const Wrapper = styled(EuiModalBody)` @@ -61,16 +67,18 @@ const usePrevious = (value: unknown) => { // the modal is used as a reminder for users to save / discard // the unsaved timeline / template export const TimelineTitleAndDescription = React.memo( - ({ timelineId, toggleSaveTimeline, showWarning }) => { + ({ closeSaveTimeline, initialFocus, openSaveTimeline, timelineId, showWarning }) => { // TODO: Refactor to use useForm() instead const [isFormSubmitted, setFormSubmitted] = useState(false); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const timeline = useDeepEqualSelector((state) => getTimeline(state, timelineId)); - - const { isSaving, savedObjectId, title, timelineType } = timeline; - + const { isSaving, status, title, timelineType } = timeline; const prevIsSaving = usePrevious(isSaving); const dispatch = useDispatch(); + const handleCreateNewTimeline = useCreateTimeline({ + timelineId: TimelineId.active, + timelineType: TimelineType.default, + }); const onSaveTimeline = useCallback( (args: TimelineInput) => dispatch(timelineActions.saveTimeline(args)), [dispatch] @@ -85,30 +93,30 @@ export const TimelineTitleAndDescription = React.memo - getButton({ - title: - timelineType === TimelineType.template - ? i18n.DISCARD_TIMELINE_TEMPLATE - : i18n.DISCARD_TIMELINE, - outline: true, - iconType: '', - fill: false, - }), - [getButton, timelineType] - ); + const handleCancel = useCallback(() => { + if (showWarning) { + handleCreateNewTimeline(); + } + closeSaveTimeline(); + }, [closeSaveTimeline, handleCreateNewTimeline, showWarning]); + + const closeModalText = useMemo(() => { + if (status === TimelineStatus.draft && showWarning) { + return timelineType === TimelineType.template + ? i18n.DISCARD_TIMELINE_TEMPLATE + : i18n.DISCARD_TIMELINE; + } + return i18n.CLOSE_MODAL; + }, [showWarning, status, timelineType]); useEffect(() => { if (isFormSubmitted && !isSaving && prevIsSaving) { - toggleSaveTimeline(); + closeSaveTimeline(); } - }, [isFormSubmitted, isSaving, prevIsSaving, toggleSaveTimeline]); + }, [isFormSubmitted, isSaving, prevIsSaving, closeSaveTimeline]); const modalHeader = - savedObjectId == null + status === TimelineStatus.draft ? timelineType === TimelineType.template ? i18n.SAVE_TIMELINE_TEMPLATE : i18n.SAVE_TIMELINE @@ -117,7 +125,7 @@ export const TimelineTitleAndDescription = React.memo - {isSaving && ( - - )} - {modalHeader} - - - {showWarning && ( + + + {isSaving && ( + + )} + {modalHeader} + + + {showWarning && ( + + + + + )} - - + + + + - )} - - - - - - - - - - - - - - - - {savedObjectId == null && showWarning ? ( - discardTimelineButton - ) : ( + + + + + + + + + - {i18n.CLOSE_MODAL} + {closeModalText} - )} - - - - {saveButtonTitle} - - - - - - + + + + {saveButtonTitle} + + + + + + + ); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 4e6bca7fd9625..5a1d2ef7a1800 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -21,7 +21,8 @@ import { useDeepEqualSelector, useShallowEqualSelector } from '../../../common/h import { activeTimeline } from '../../containers/active_timeline_context'; import * as i18n from './translations'; import { TabsContent } from './tabs_content'; -import { TimelineContainer } from './styles'; +import { HideShowContainer, TimelineContainer } from './styles'; +import { useTimelineFullScreen } from '../../../common/containers/use_full_screen'; const TimelineTemplateBadge = styled.div` background: ${({ theme }) => theme.eui.euiColorVis3_behindText}; @@ -55,6 +56,7 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { getTimeline(state, timelineId) ?? timelineDefaults ) ); + const { timelineFullScreen } = useTimelineFullScreen(); useEffect(() => { if (!savedObjectId) { @@ -79,7 +81,9 @@ const StatefulTimelineComponent: React.FC = ({ timelineId }) => { )} - + + + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx index 6eb9286871b68..3a75922ab72bd 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.test.tsx @@ -105,19 +105,6 @@ describe('Description', () => { ).toEqual(i18n.DESCRIPTION_TOOL_TIP); }); - test('should not render textarea if isTextArea is false', () => { - const component = mount( - - - - ); - expect(component.find('[data-test-subj="timeline-description-textarea"]').exists()).toEqual( - false - ); - - expect(component.find('[data-test-subj="timeline-description-input"]').exists()).toEqual(true); - }); - test('should render textarea if isTextArea is true', () => { const testProps = { ...props, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx index 673efa1857cb8..d17399a0fb180 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/helpers.tsx @@ -4,16 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiBadge, - EuiButton, - EuiButtonIcon, - EuiFieldText, - EuiToolTip, - EuiTextArea, -} from '@elastic/eui'; +import { EuiBadge, EuiButton, EuiButtonIcon, EuiToolTip, EuiTextArea } from '@elastic/eui'; import { pick } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; @@ -77,8 +70,8 @@ AddToFavoritesButtonComponent.displayName = 'AddToFavoritesButtonComponent'; export const AddToFavoritesButton = React.memo(AddToFavoritesButtonComponent); interface DescriptionProps { + autoFocus?: boolean; timelineId: string; - isTextArea?: boolean; disableAutoSave?: boolean; disableTooltip?: boolean; disabled?: boolean; @@ -86,8 +79,8 @@ interface DescriptionProps { export const Description = React.memo( ({ + autoFocus = false, timelineId, - isTextArea = false, disableAutoSave = false, disableTooltip = false, disabled = false, @@ -113,28 +106,21 @@ export const Description = React.memo( ); const inputField = useMemo( - () => - isTextArea ? ( - - ) : ( - - ), - [description, isTextArea, onDescriptionChanged, disabled] + () => ( + + ), + [autoFocus, description, onDescriptionChanged, disabled] ); + return ( {disableTooltip ? ( @@ -170,7 +156,6 @@ export const Name = React.memo( timelineId, }) => { const dispatch = useDispatch(); - const timelineNameRef = useRef(null); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); const { title, timelineType } = useDeepEqualSelector((state) => @@ -185,15 +170,10 @@ export const Name = React.memo( [dispatch, timelineId, disableAutoSave] ); - useEffect(() => { - if (autoFocus && timelineNameRef && timelineNameRef.current) { - timelineNameRef.current.focus(); - } - }, [autoFocus]); - const nameField = useMemo( () => ( ( } spellCheck={true} value={title} - inputRef={timelineNameRef} /> ), - [handleChange, timelineType, title, disabled] + [autoFocus, handleChange, timelineType, title, disabled] ); return ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx index 7fab0374d791d..12845477e0f39 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/properties/use_create_timeline.tsx @@ -9,7 +9,7 @@ import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { timelineActions } from '../../../store/timeline'; -import { useFullScreen } from '../../../../common/containers/use_full_screen'; +import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen'; import { TimelineId, TimelineType, @@ -34,7 +34,7 @@ export const useCreateTimeline = ({ timelineId, timelineType, closeGearMenu }: P [] ); const existingIndexNames = useDeepEqualSelector(existingIndexNamesSelector); - const { timelineFullScreen, setTimelineFullScreen } = useFullScreen(); + const { timelineFullScreen, setTimelineFullScreen } = useTimelineFullScreen(); const globalTimeRange = useDeepEqualSelector(inputsSelectors.globalTimeRangeSelector); const createTimeline = useCallback( ({ id, show }) => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx index 962e09d1a6237..d045cc6160c9c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.test.tsx @@ -145,7 +145,7 @@ describe('Timeline', () => { expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(true); }); - test('it does NOT render the timeline table when the source is loading', () => { + test('it does render the timeline table when the source is loading with no events', () => { (useSourcererScope as jest.Mock).mockReturnValue({ browserFields: {}, docValueFields: [], @@ -159,7 +159,8 @@ describe('Timeline', () => { ); - expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="events"]').exists()).toEqual(false); }); test('it does NOT render the timeline table when start is empty', () => { @@ -169,7 +170,8 @@ describe('Timeline', () => { ); - expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="events"]').exists()).toEqual(false); }); test('it does NOT render the timeline table when end is empty', () => { @@ -179,7 +181,8 @@ describe('Timeline', () => { ); - expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(false); + expect(wrapper.find('[data-test-subj="events-table"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="events"]').exists()).toEqual(false); }); test('it does NOT render the paging footer when you do NOT have any data providers', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 8da3c257a5db8..e93d23a816911 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -23,7 +23,7 @@ import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import { timelineActions, timelineSelectors } from '../../../store/timeline'; -import { Direction } from '../../../../../common/search_strategy'; +import { Direction, TimelineItem } from '../../../../../common/search_strategy'; import { useTimelineEvents } from '../../../containers/index'; import { useKibana } from '../../../../common/lib/kibana'; import { defaultHeaders } from '../body/column_headers/default_headers'; @@ -48,6 +48,8 @@ import { useTimelineEventsCountPortal } from '../../../../common/hooks/use_timel import { TimelineModel } from '../../../../timelines/store/timeline/model'; import { EventDetails } from '../event_details'; import { TimelineDatePickerLock } from '../date_picker_lock'; +import { HideShowContainer } from '../styles'; +import { useTimelineFullScreen } from '../../../../common/containers/use_full_screen'; import { activeTimeline } from '../../../containers/active_timeline_context'; import { ToggleExpandedEvent } from '../../../store/timeline/actions'; @@ -143,6 +145,8 @@ interface OwnProps { timelineId: string; } +const EMPTY_EVENTS: TimelineItem[] = []; + export type Props = OwnProps & PropsFromRedux; export const QueryTabContentComponent: React.FC = ({ @@ -168,6 +172,7 @@ export const QueryTabContentComponent: React.FC = ({ updateEventTypeAndIndexesName, }) => { const { timelineEventsCountPortalNode } = useTimelineEventsCountPortal(); + const { timelineFullScreen } = useTimelineFullScreen(); const { browserFields, docValueFields, @@ -200,6 +205,11 @@ export const QueryTabContentComponent: React.FC = ({ [browserFields, dataProviders, esQueryConfig, filters, indexPattern, kqlMode, kqlQuery] ); + const isBlankTimeline: boolean = useMemo( + () => isEmpty(dataProviders) && isEmpty(filters) && isEmpty(kqlQuery.query), + [dataProviders, filters, kqlQuery] + ); + const canQueryTimeline = useMemo( () => combinedQueries != null && @@ -287,67 +297,66 @@ export const QueryTabContentComponent: React.FC = ({ /> - - - - - - - + + + + + + + + + +
    + + + +
    + + -
    -
    -
    - - - -
    - - +
    + + + + - - - {canQueryTimeline ? ( - - - - - + + + {!isBlankTimeline && (